From 5e1d475f7936aafd8dc8f56fa7cb01a644591dab Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 11 Oct 2024 14:30:48 -0500 Subject: [PATCH 01/26] Fix exp() result not being able to be passed to variable() Also change name to be static and less ambiguous. It is not used in cypher query. --- src/core/database/query-augmentation/condition-variables.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/database/query-augmentation/condition-variables.ts b/src/core/database/query-augmentation/condition-variables.ts index 03843f6b86..a4c96705ff 100644 --- a/src/core/database/query-augmentation/condition-variables.ts +++ b/src/core/database/query-augmentation/condition-variables.ts @@ -5,12 +5,12 @@ import { Parameter, ParameterBag, Pattern } from 'cypher-query-builder'; export class Variable extends Parameter { - constructor(variable: string, name = variable) { - super(name, variable); + constructor(variable: string) { + super('variable', String(variable)); } toString() { - return `${this.value}`; + return this.value; } } From 024e8d9214b41faf557b23a267f40f993ab4ca5e Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 11 Oct 2024 14:38:13 -0500 Subject: [PATCH 02/26] Implement base notification create, list, mark read --- .../notifications/dto/notification.dto.ts | 2 +- .../notifications/notification.module.ts | 13 +- .../notifications/notification.repository.ts | 135 ++++++++++++++++++ .../notifications/notification.resolver.ts | 14 +- .../notifications/notification.service.ts | 47 ++++++ src/core/database/query/cypher-functions.ts | 3 + 6 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 src/components/notifications/notification.repository.ts create mode 100644 src/components/notifications/notification.service.ts diff --git a/src/components/notifications/dto/notification.dto.ts b/src/components/notifications/dto/notification.dto.ts index db7bcb561c..920e44c0bd 100644 --- a/src/components/notifications/dto/notification.dto.ts +++ b/src/components/notifications/dto/notification.dto.ts @@ -11,7 +11,7 @@ export class Notification extends Resource { static readonly Props = keysOf(); static readonly SecuredProps = keysOf>(); - readonly owner: LinkTo<'User'>; + readonly for: LinkTo<'User'>; @Field(() => Boolean) readonly unread: boolean; diff --git a/src/components/notifications/notification.module.ts b/src/components/notifications/notification.module.ts index d4e9cb205f..c688d18401 100644 --- a/src/components/notifications/notification.module.ts +++ b/src/components/notifications/notification.module.ts @@ -1,7 +1,18 @@ import { Module } from '@nestjs/common'; +import { NotificationRepository } from './notification.repository'; import { NotificationResolver } from './notification.resolver'; +import { + NotificationService, + NotificationServiceImpl, +} from './notification.service'; @Module({ - providers: [NotificationResolver], + providers: [ + NotificationResolver, + { provide: NotificationService, useExisting: NotificationServiceImpl }, + NotificationServiceImpl, + NotificationRepository, + ], + exports: [NotificationService], }) export class NotificationModule {} diff --git a/src/components/notifications/notification.repository.ts b/src/components/notifications/notification.repository.ts new file mode 100644 index 0000000000..f0468d4edf --- /dev/null +++ b/src/components/notifications/notification.repository.ts @@ -0,0 +1,135 @@ +import { Injectable } from '@nestjs/common'; +import { node, Query, relation } from 'cypher-query-builder'; +import { DateTime } from 'luxon'; +import { + EnhancedResource, + ID, + NotFoundException, + ResourceShape, + Session, + UnsecuredDto, +} from '~/common'; +import { CommonRepository } from '~/core/database'; +import { + apoc, + createRelationships, + filter, + merge, + paginate, + requestingUser, + variable, +} from '~/core/database/query'; +import { + MarkNotificationReadArgs, + Notification, + NotificationFilters, + NotificationListInput, +} from './dto'; + +@Injectable() +export class NotificationRepository extends CommonRepository { + async create( + recipients: ReadonlyArray>, + type: ResourceShape, + input: unknown, + session: Session, + ) { + const createdAt = DateTime.now(); + await this.db + .query() + .match(requestingUser(session)) + .create([ + node('source', 'NotificationSource', { + id: variable(apoc.create.uuid()), + createdAt, + }), + relation('out', '', 'producer'), + node('requestingUser'), + ]) + .with('source') + .unwind(recipients.slice(), 'userId') + .match(node('for', 'User', { id: variable('userId') })) + .create( + node('node', EnhancedResource.of(type).dbLabels, { + id: variable(apoc.create.uuid()), + createdAt, + unread: variable('true'), + type: this.getType(type), + }), + ) + .with('*') + .apply( + createRelationships(Notification, { + in: { produced: variable('source') }, + out: { for: variable('for') }, + }), + ) + .return('node') + .run(); + } + + async markRead({ id, unread }: MarkNotificationReadArgs, session: Session) { + const result = await this.db + .query() + .match([ + node('node', 'Notification', { id }), + relation('out', '', 'for'), + requestingUser(session), + ]) + .setValues({ node: { unread } }, true) + .with('node') + .apply(this.hydrate()) + .first(); + if (!result) { + throw new NotFoundException(); + } + return result.dto; + } + + async list(input: NotificationListInput, session: Session) { + const result = await this.db + .query() + .match(requestingUser(session)) + .subQuery('requestingUser', (q) => + q + .match([ + node('node', 'Notification'), + relation('out', '', 'for'), + node('requestingUser'), + ]) + .apply(notificationFilters(input.filter)) + .with('node') + .orderBy('node.createdAt', 'DESC') + .apply(paginate(input, this.hydrate())), + ) + .subQuery('requestingUser', (q) => + q + .match([ + node('node', 'Notification', { unread: variable('true') }), + relation('out', '', 'for'), + node('requestingUser'), + ]) + .return<{ totalUnread: number }>('count(node) as totalUnread'), + ) + .return(['items', 'hasMore', 'total', 'totalUnread']) + .first(); + return result!; + } + + protected hydrate() { + return (query: Query) => + query.return<{ dto: UnsecuredDto }>( + merge('node', { + __typename: 'node.type + "Notification"', + }).as('dto'), + ); + } + + private getType(dtoCls: ResourceShape) { + return dtoCls.name.replace('Notification', ''); + } +} + +const notificationFilters = filter.define(() => NotificationFilters, { + unread: ({ value }) => ({ node: { unread: value } }), +}); diff --git a/src/components/notifications/notification.resolver.ts b/src/components/notifications/notification.resolver.ts index 348d783e31..bad502d79f 100644 --- a/src/components/notifications/notification.resolver.ts +++ b/src/components/notifications/notification.resolver.ts @@ -1,25 +1,23 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { - ListArg, - LoggedInSession, - NotImplementedException, - Session, -} from '~/common'; +import { ListArg, LoggedInSession, Session } from '~/common'; import { MarkNotificationReadArgs, Notification, NotificationList, NotificationListInput, } from './dto'; +import { NotificationServiceImpl } from './notification.service'; @Resolver() export class NotificationResolver { + constructor(private readonly service: NotificationServiceImpl) {} + @Query(() => NotificationList) async notifications( @LoggedInSession() session: Session, @ListArg(NotificationListInput) input: NotificationListInput, ): Promise { - throw new NotImplementedException().with(input, session); + return await this.service.list(input, session); } @Mutation(() => Notification) @@ -27,6 +25,6 @@ export class NotificationResolver { @LoggedInSession() session: Session, @Args() input: MarkNotificationReadArgs, ): Promise { - throw new NotImplementedException().with(input, session); + return await this.service.markRead(input, session); } } diff --git a/src/components/notifications/notification.service.ts b/src/components/notifications/notification.service.ts new file mode 100644 index 0000000000..32db562d8f --- /dev/null +++ b/src/components/notifications/notification.service.ts @@ -0,0 +1,47 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { ID, ResourceShape, Session, UnsecuredDto } from '~/common'; +import { + MarkNotificationReadArgs, + Notification, + NotificationList, + NotificationListInput, +} from './dto'; +import { NotificationRepository } from './notification.repository'; + +@Injectable() +export abstract class NotificationService { + @Inject(forwardRef(() => NotificationRepository)) + protected readonly repo: NotificationRepository & {}; + + async create>( + recipients: ReadonlyArray>, + type: T, + input: unknown, + session: Session, + ) { + await this.repo.create(recipients, type, input, session); + } +} + +@Injectable() +export class NotificationServiceImpl extends NotificationService { + async list( + input: NotificationListInput, + session: Session, + ): Promise { + const result = await this.repo.list(input, session); + return { + ...result, + items: result.items.map((dto) => this.secure(dto)), + }; + } + + async markRead(input: MarkNotificationReadArgs, session: Session) { + const result = await this.repo.markRead(input, session); + return this.secure(result); + } + + private secure(dto: UnsecuredDto) { + return { ...dto, canDelete: true }; + } +} diff --git a/src/core/database/query/cypher-functions.ts b/src/core/database/query/cypher-functions.ts index 7228d5301a..1ecc602763 100644 --- a/src/core/database/query/cypher-functions.ts +++ b/src/core/database/query/cypher-functions.ts @@ -16,6 +16,8 @@ const fn = /** Create a function with a name that takes a single argument */ const fn1 = (name: string) => (arg: ExpressionInput) => fn(name)(arg); +const fn0 = (name: string) => () => fn(name)(); + /** * Returns a list containing the values returned by an expression. * Using this function aggregates data by amalgamating multiple records or @@ -79,6 +81,7 @@ export const apoc = { fromJsonList: fn1('apoc.convert.fromJsonList'), }, create: { + uuid: fn0('apoc.create.uuid'), setLabels: (node: ExpressionInput, labels: readonly string[]) => procedure('apoc.create.setLabels', ['node'])({ node: exp(node), labels }), }, From b0afe2b7adc834479458ee85df106cc69a198df7 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 11 Oct 2024 17:20:07 -0500 Subject: [PATCH 03/26] Create a notification strategy to handle unique info of concrete notification types --- .../notifications/dto/notification.dto.ts | 4 +- src/components/notifications/index.ts | 4 ++ .../notifications/notification.repository.ts | 48 +++++++++++--- .../notifications/notification.service.ts | 64 +++++++++++++++++-- .../notifications/notification.strategy.ts | 32 ++++++++++ src/core/database/changes.ts | 2 +- 6 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 src/components/notifications/index.ts create mode 100644 src/components/notifications/notification.strategy.ts diff --git a/src/components/notifications/dto/notification.dto.ts b/src/components/notifications/dto/notification.dto.ts index 920e44c0bd..28e719440d 100644 --- a/src/components/notifications/dto/notification.dto.ts +++ b/src/components/notifications/dto/notification.dto.ts @@ -8,8 +8,8 @@ import { LinkTo, RegisterResource } from '~/core/resources'; implements: [Resource], }) export class Notification extends Resource { - static readonly Props = keysOf(); - static readonly SecuredProps = keysOf>(); + static readonly Props: string[] = keysOf(); + static readonly SecuredProps: string[] = keysOf>(); readonly for: LinkTo<'User'>; diff --git a/src/components/notifications/index.ts b/src/components/notifications/index.ts new file mode 100644 index 0000000000..5dceafc081 --- /dev/null +++ b/src/components/notifications/index.ts @@ -0,0 +1,4 @@ +export * from './dto/notification.dto'; +export { NotificationService } from './notification.service'; +export * from './notification.strategy'; +export * from './notification.module'; diff --git a/src/components/notifications/notification.repository.ts b/src/components/notifications/notification.repository.ts index f0468d4edf..f6ce4078aa 100644 --- a/src/components/notifications/notification.repository.ts +++ b/src/components/notifications/notification.repository.ts @@ -1,5 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { node, Query, relation } from 'cypher-query-builder'; +import { omit } from 'lodash'; import { DateTime } from 'luxon'; import { EnhancedResource, @@ -25,15 +26,24 @@ import { NotificationFilters, NotificationListInput, } from './dto'; +import { NotificationServiceImpl } from './notification.service'; @Injectable() export class NotificationRepository extends CommonRepository { + constructor( + @Inject(forwardRef(() => NotificationServiceImpl)) + private readonly service: NotificationServiceImpl & {}, + ) { + super(); + } + async create( recipients: ReadonlyArray>, - type: ResourceShape, - input: unknown, + type: ResourceShape, + input: Record, session: Session, ) { + const extra = omit(input, Notification.Props); const createdAt = DateTime.now(); await this.db .query() @@ -58,6 +68,8 @@ export class NotificationRepository extends CommonRepository { }), ) .with('*') + .apply(this.service.getStrategy(type).saveForNeo4j(extra)) + .with('*') .apply( createRelationships(Notification, { in: { produced: variable('source') }, @@ -118,11 +130,31 @@ export class NotificationRepository extends CommonRepository { protected hydrate() { return (query: Query) => - query.return<{ dto: UnsecuredDto }>( - merge('node', { - __typename: 'node.type + "Notification"', - }).as('dto'), - ); + query + .subQuery((q) => { + const concreteHydrates = [...this.service.strategyMap].map( + ([dtoCls, strategy]) => + (q: Query) => { + const type = this.getType(dtoCls); + const hydrate = strategy.hydrateExtraForNeo4j('extra'); + return q + .with('node') + .with('node') + .where({ 'node.type': type }) + .apply(hydrate ?? ((q) => q.return('{} as extra'))); + }, + ); + return concreteHydrates.reduce( + (acc: Query | undefined, concreteHydrate) => + (!acc ? q : acc.union()).apply(concreteHydrate), + undefined, + )!; + }) + .return<{ dto: UnsecuredDto }>( + merge('node', 'extra', { + __typename: 'node.type + "Notification"', + }).as('dto'), + ); } private getType(dtoCls: ResourceShape) { diff --git a/src/components/notifications/notification.service.ts b/src/components/notifications/notification.service.ts index 32db562d8f..f9738211f5 100644 --- a/src/components/notifications/notification.service.ts +++ b/src/components/notifications/notification.service.ts @@ -1,5 +1,15 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { ID, ResourceShape, Session, UnsecuredDto } from '~/common'; +import { DiscoveryService } from '@golevelup/nestjs-discovery'; +import { forwardRef, Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { mapEntries } from '@seedcompany/common'; +import { + ID, + ResourceShape, + ServerException, + Session, + UnsecuredDto, +} from '~/common'; +import { sessionFromContext } from '~/common/session'; +import { GqlContextHost } from '~/core/graphql'; import { MarkNotificationReadArgs, Notification, @@ -7,24 +17,51 @@ import { NotificationListInput, } from './dto'; import { NotificationRepository } from './notification.repository'; +import { + INotificationStrategy, + InputOf, + NotificationStrategy, +} from './notification.strategy'; @Injectable() export abstract class NotificationService { @Inject(forwardRef(() => NotificationRepository)) protected readonly repo: NotificationRepository & {}; + @Inject(GqlContextHost) + protected readonly gqlContextHost: GqlContextHost; async create>( - recipients: ReadonlyArray>, type: T, - input: unknown, - session: Session, + recipients: ReadonlyArray>, + input: T extends { Input: infer Input } ? Input : InputOf, ) { + const session = sessionFromContext(this.gqlContextHost.context); await this.repo.create(recipients, type, input, session); } } @Injectable() -export class NotificationServiceImpl extends NotificationService { +export class NotificationServiceImpl + extends NotificationService + implements OnModuleInit +{ + strategyMap: ReadonlyMap< + ResourceShape, + INotificationStrategy + >; + + constructor(private readonly discovery: DiscoveryService) { + super(); + } + + getStrategy(type: ResourceShape) { + const strategy = this.strategyMap.get(type); + if (!strategy) { + throw new ServerException('Notification type has not been registered'); + } + return strategy; + } + async list( input: NotificationListInput, session: Session, @@ -44,4 +81,19 @@ export class NotificationServiceImpl extends NotificationService { private secure(dto: UnsecuredDto) { return { ...dto, canDelete: true }; } + + async onModuleInit() { + const discovered = await this.discovery.providersWithMetaAtKey< + ResourceShape + >(NotificationStrategy.KEY); + this.strategyMap = mapEntries(discovered, ({ meta, discoveredClass }) => { + const { instance } = discoveredClass; + if (!(instance instanceof INotificationStrategy)) { + throw new ServerException( + `Strategy for ${meta.name} does not implement INotificationStrategy`, + ); + } + return [meta, instance]; + }).asMap; + } } diff --git a/src/components/notifications/notification.strategy.ts b/src/components/notifications/notification.strategy.ts new file mode 100644 index 0000000000..4c39f47396 --- /dev/null +++ b/src/components/notifications/notification.strategy.ts @@ -0,0 +1,32 @@ +import { createMetadataDecorator } from '@seedcompany/nest'; +import { Query } from 'cypher-query-builder'; +import { AbstractClass, Simplify } from 'type-fest'; +import type { UnwrapSecured } from '~/common'; +import type { RawChangeOf } from '~/core/database/changes'; +import type { QueryFragment } from '~/core/database/query-augmentation/apply'; +import type { Notification } from './dto'; + +export const NotificationStrategy = createMetadataDecorator({ + types: ['class'], + setter: (cls: AbstractClass) => cls, +}); + +export type InputOf = Simplify<{ + [K in keyof T as Exclude]: + | RawChangeOf & {}> + | (null extends UnwrapSecured ? null : never); +}>; + +export abstract class INotificationStrategy< + TNotification extends Notification, + TInput = InputOf, +> { + saveForNeo4j(input: TInput) { + return (query: Query) => query.setValues({ node: input }, true); + } + + hydrateExtraForNeo4j(outVar: string): QueryFragment | undefined { + const _used = outVar; + return undefined; + } +} diff --git a/src/core/database/changes.ts b/src/core/database/changes.ts index bd887ea26a..898df713f3 100644 --- a/src/core/database/changes.ts +++ b/src/core/database/changes.ts @@ -71,7 +71,7 @@ type ChangeOf = Val extends SetChangeType | RawChangeOf & {}> | (null extends UnwrapSecured ? null : never); -type RawChangeOf = IsFileField extends true +export type RawChangeOf = IsFileField extends true ? CreateDefinedFileVersionInput : Val extends LinkTo ? ID From 4bc0dca18b12818cc818348e9737eabfaee60ab9 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 11 Oct 2024 17:24:43 -0500 Subject: [PATCH 04/26] Expand simple text notification to demonstrate real concrete with complex data --- src/app.module.ts | 2 + .../simple-text-notification.dto.ts | 19 +++++++ .../simple-text-notification.module.ts | 10 ++++ .../simple-text-notification.resolver.ts | 56 +++++++++++++++++++ .../simple-text-notification.strategy.ts | 34 +++++++++++ .../notifications/dto/notification.dto.ts | 10 +--- 6 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 src/components/notification-simple-text/simple-text-notification.dto.ts create mode 100644 src/components/notification-simple-text/simple-text-notification.module.ts create mode 100644 src/components/notification-simple-text/simple-text-notification.resolver.ts create mode 100644 src/components/notification-simple-text/simple-text-notification.strategy.ts diff --git a/src/app.module.ts b/src/app.module.ts index 204134b85a..08c986e502 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,6 +18,7 @@ import { FilmModule } from './components/film/film.module'; import { FundingAccountModule } from './components/funding-account/funding-account.module'; import { LanguageModule } from './components/language/language.module'; import { LocationModule } from './components/location/location.module'; +import { SimpleTextNotificationModule } from './components/notification-simple-text/simple-text-notification.module'; import { NotificationModule } from './components/notifications/notification.module'; import { OrganizationModule } from './components/organization/organization.module'; import { PartnerModule } from './components/partner/partner.module'; @@ -91,6 +92,7 @@ if (process.env.NODE_ENV !== 'production') { PromptsModule, PnpExtractionResultModule, NotificationModule, + SimpleTextNotificationModule, ], }) export class AppModule {} diff --git a/src/components/notification-simple-text/simple-text-notification.dto.ts b/src/components/notification-simple-text/simple-text-notification.dto.ts new file mode 100644 index 0000000000..e2d8412581 --- /dev/null +++ b/src/components/notification-simple-text/simple-text-notification.dto.ts @@ -0,0 +1,19 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { keys as keysOf } from 'ts-transformer-keys'; +import { SecuredProps } from '~/common'; +import { LinkTo } from '~/core/resources'; +import { Notification } from '../notifications'; + +@ObjectType({ + implements: [Notification], +}) +export class SimpleTextNotification extends Notification { + static readonly Props = keysOf(); + static readonly SecuredProps = keysOf>(); + + @Field(() => String) + readonly content: string; + + // Here to demonstrate relationships in DB & GQL + readonly reference: LinkTo<'User'> | null; +} diff --git a/src/components/notification-simple-text/simple-text-notification.module.ts b/src/components/notification-simple-text/simple-text-notification.module.ts new file mode 100644 index 0000000000..590b5d953d --- /dev/null +++ b/src/components/notification-simple-text/simple-text-notification.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { NotificationModule } from '../notifications'; +import { SimpleTextNotificationResolver } from './simple-text-notification.resolver'; +import { SimpleTextNotificationStrategy } from './simple-text-notification.strategy'; + +@Module({ + imports: [NotificationModule], + providers: [SimpleTextNotificationResolver, SimpleTextNotificationStrategy], +}) +export class SimpleTextNotificationModule {} diff --git a/src/components/notification-simple-text/simple-text-notification.resolver.ts b/src/components/notification-simple-text/simple-text-notification.resolver.ts new file mode 100644 index 0000000000..ec11578583 --- /dev/null +++ b/src/components/notification-simple-text/simple-text-notification.resolver.ts @@ -0,0 +1,56 @@ +import { + Args, + ID as IDScalar, + Mutation, + Parent, + ResolveField, + Resolver, +} from '@nestjs/graphql'; +import { ID, LoggedInSession, Session, UnauthorizedException } from '~/common'; +import { isAdmin } from '~/common/session'; +import { Loader, LoaderOf } from '~/core'; +import { NotificationService } from '../notifications'; +import { UserLoader } from '../user'; +import { User } from '../user/dto'; +import { SimpleTextNotification as SimpleText } from './simple-text-notification.dto'; + +@Resolver(SimpleText) +export class SimpleTextNotificationResolver { + constructor(private readonly notifications: NotificationService) {} + + @ResolveField(() => User, { nullable: true }) + async reference( + @Parent() { reference }: SimpleText, + @Loader(UserLoader) users: LoaderOf, + ) { + if (!reference) { + return null; + } + return await users.load(reference.id); + } + + @Mutation(() => Boolean) + async testCreateSimpleTextNotification( + @Args('content') content: string, + @Args({ name: 'reference', nullable: true, type: () => IDScalar }) + reference: ID | null, + @LoggedInSession() session: Session, + ) { + if (!isAdmin(session)) { + throw new UnauthorizedException(); + } + + // @ts-expect-error this is just for testing + const allUsers = await this.notifications.repo.db + .query<{ id: ID }>('match (u:User) return u.id as id') + .map('id') + .run(); + + await this.notifications.create(SimpleText, allUsers, { + content, + reference, + }); + + return true; + } +} diff --git a/src/components/notification-simple-text/simple-text-notification.strategy.ts b/src/components/notification-simple-text/simple-text-notification.strategy.ts new file mode 100644 index 0000000000..fb5e6578e7 --- /dev/null +++ b/src/components/notification-simple-text/simple-text-notification.strategy.ts @@ -0,0 +1,34 @@ +import { node, Query, relation } from 'cypher-query-builder'; +import { createRelationships, exp } from '~/core/database/query'; +import { + INotificationStrategy, + InputOf, + NotificationStrategy, +} from '../notifications'; +import { SimpleTextNotification as SimpleText } from './simple-text-notification.dto'; + +@NotificationStrategy(SimpleText) +export class SimpleTextNotificationStrategy extends INotificationStrategy { + saveForNeo4j(input: InputOf) { + return (query: Query) => + query + .apply( + createRelationships(SimpleText, 'out', { + reference: ['User', input.reference], + }), + ) + .with('*') + .setValues({ node: { content: input.content } }, true); + } + + hydrateExtraForNeo4j(outVar: string) { + return (query: Query) => + query + .optionalMatch([ + node('node'), + relation('out', '', 'reference'), + node('u', 'User'), + ]) + .return(exp({ reference: 'u { .id }' }).as(outVar)); + } +} diff --git a/src/components/notifications/dto/notification.dto.ts b/src/components/notifications/dto/notification.dto.ts index 28e719440d..8846bf49b0 100644 --- a/src/components/notifications/dto/notification.dto.ts +++ b/src/components/notifications/dto/notification.dto.ts @@ -1,4 +1,4 @@ -import { Field, InterfaceType, ObjectType } from '@nestjs/graphql'; +import { Field, InterfaceType } from '@nestjs/graphql'; import { keys as keysOf } from 'ts-transformer-keys'; import { Resource, SecuredProps } from '~/common'; import { LinkTo, RegisterResource } from '~/core/resources'; @@ -17,14 +17,6 @@ export class Notification extends Resource { readonly unread: boolean; } -@ObjectType({ - implements: [Notification], -}) -export class SimpleTextNotification extends Notification { - @Field(() => String) - readonly content: string; -} - declare module '~/core/resources/map' { interface ResourceMap { Notification: typeof Notification; From ad1d7743805e07807b868b1a20f83c4a20a56591 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 11 Oct 2024 18:56:25 -0500 Subject: [PATCH 05/26] Add CommentViaMentionNotification --- src/components/comments/comment.module.ts | 2 ++ .../comment-via-mention-notification.dto.ts | 16 +++++++++ ...comment-via-mention-notification.module.ts | 13 +++++++ ...mment-via-mention-notification.resolver.ts | 16 +++++++++ ...mment-via-mention-notification.strategy.ts | 35 +++++++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 src/components/comments/mention-notification/comment-via-mention-notification.dto.ts create mode 100644 src/components/comments/mention-notification/comment-via-mention-notification.module.ts create mode 100644 src/components/comments/mention-notification/comment-via-mention-notification.resolver.ts create mode 100644 src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts diff --git a/src/components/comments/comment.module.ts b/src/components/comments/comment.module.ts index 4c14e52fc1..973d65605e 100644 --- a/src/components/comments/comment.module.ts +++ b/src/components/comments/comment.module.ts @@ -10,11 +10,13 @@ import { CommentResolver } from './comment.resolver'; import { CommentService } from './comment.service'; import { CommentableResolver } from './commentable.resolver'; import { CreateCommentResolver } from './create-comment.resolver'; +import { CommentViaMentionNotificationModule } from './mention-notification/comment-via-mention-notification.module'; @Module({ imports: [ forwardRef(() => UserModule), forwardRef(() => AuthorizationModule), + CommentViaMentionNotificationModule, ], providers: [ CreateCommentResolver, diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.dto.ts b/src/components/comments/mention-notification/comment-via-mention-notification.dto.ts new file mode 100644 index 0000000000..f6e508fc35 --- /dev/null +++ b/src/components/comments/mention-notification/comment-via-mention-notification.dto.ts @@ -0,0 +1,16 @@ +import { ObjectType } from '@nestjs/graphql'; +import { keys as keysOf } from 'ts-transformer-keys'; +import { SecuredProps } from '~/common'; +import { LinkTo } from '~/core/resources'; +import { Notification } from '../../notifications'; + +@ObjectType({ + implements: [Notification], +}) +export class CommentViaMentionNotification extends Notification { + static readonly Props = keysOf(); + static readonly SecuredProps = + keysOf>(); + + readonly comment: LinkTo<'Comment'>; +} diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.module.ts b/src/components/comments/mention-notification/comment-via-mention-notification.module.ts new file mode 100644 index 0000000000..da71b58210 --- /dev/null +++ b/src/components/comments/mention-notification/comment-via-mention-notification.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { NotificationModule } from '../../notifications'; +import { CommentViaMentionNotificationResolver } from './comment-via-mention-notification.resolver'; +import { CommentViaMentionNotificationStrategy } from './comment-via-mention-notification.strategy'; + +@Module({ + imports: [NotificationModule], + providers: [ + CommentViaMentionNotificationResolver, + CommentViaMentionNotificationStrategy, + ], +}) +export class CommentViaMentionNotificationModule {} diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.resolver.ts b/src/components/comments/mention-notification/comment-via-mention-notification.resolver.ts new file mode 100644 index 0000000000..9f057a7651 --- /dev/null +++ b/src/components/comments/mention-notification/comment-via-mention-notification.resolver.ts @@ -0,0 +1,16 @@ +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { Loader, LoaderOf } from '~/core'; +import { CommentLoader } from '../comment.loader'; +import { Comment } from '../dto'; +import { CommentViaMentionNotification as Notification } from './comment-via-mention-notification.dto'; + +@Resolver(Notification) +export class CommentViaMentionNotificationResolver { + @ResolveField(() => Comment) + async comment( + @Parent() { comment }: Notification, + @Loader(CommentLoader) comments: LoaderOf, + ) { + return await comments.load(comment.id); + } +} diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts b/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts new file mode 100644 index 0000000000..be5097511e --- /dev/null +++ b/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts @@ -0,0 +1,35 @@ +import { node, Query, relation } from 'cypher-query-builder'; +import { createRelationships, exp } from '~/core/database/query'; +import { + INotificationStrategy, + InputOf, + NotificationStrategy, +} from '../../notifications'; +import { CommentViaMentionNotification } from './comment-via-mention-notification.dto'; + +@NotificationStrategy(CommentViaMentionNotification) +export class CommentViaMentionNotificationStrategy extends INotificationStrategy { + saveForNeo4j(input: InputOf) { + return (query: Query) => + query.apply( + createRelationships(CommentViaMentionNotification, 'out', { + comment: ['Comment', input.comment], + }), + ); + } + + hydrateExtraForNeo4j(outVar: string) { + return (query: Query) => + query + .match([ + node('node'), + relation('out', '', 'comment'), + node('comment', 'Comment'), + ]) + .return( + exp({ + comment: 'comment { .id }', + }).as(outVar), + ); + } +} From 1dfc981b59c5ba726da0222ae1f1fa34542a1559 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 11 Oct 2024 19:11:15 -0500 Subject: [PATCH 06/26] Add service to help extract mentions & notify on new/updated comments --- src/components/comments/comment.service.ts | 20 ++++++++++++++-- ...comment-via-mention-notification.module.ts | 3 +++ ...omment-via-mention-notification.service.ts | 23 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/components/comments/mention-notification/comment-via-mention-notification.service.ts diff --git a/src/components/comments/comment.service.ts b/src/components/comments/comment.service.ts index 9f74f4d4b6..968a864323 100644 --- a/src/components/comments/comment.service.ts +++ b/src/components/comments/comment.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { difference } from 'lodash'; import { ID, InvalidIdForTypeException, @@ -26,6 +27,7 @@ import { CreateCommentInput, UpdateCommentInput, } from './dto'; +import { CommentViaMentionNotificationService } from './mention-notification/comment-via-mention-notification.service'; type CommentableRef = ID | BaseNode | Commentable; @@ -36,6 +38,7 @@ export class CommentService { private readonly privileges: Privileges, private readonly resources: ResourceLoader, private readonly resourcesHost: ResourcesHost, + private readonly mentionNotificationService: CommentViaMentionNotificationService, ) {} async create(input: CreateCommentInput, session: Session) { @@ -45,12 +48,13 @@ export class CommentService { ); perms.verifyCan('create'); + let dto; try { const result = await this.repo.create(input, session); if (!result) { throw new ServerException('Failed to create comment'); } - return await this.readOne(result.id, session); + dto = await this.repo.readOne(result.id); } catch (exception) { if ( input.threadId && @@ -64,6 +68,11 @@ export class CommentService { throw new ServerException('Failed to create comment', exception); } + + const mentionees = this.mentionNotificationService.extract(dto); + await this.mentionNotificationService.notify(mentionees, dto); + + return this.secureComment(dto, session); } async getPermissionsFromResource(resource: CommentableRef, session: Session) { @@ -134,7 +143,14 @@ export class CommentService { this.privileges.for(session, Comment, object).verifyChanges(changes); await this.repo.update(object, changes); - return await this.readOne(input.id, session); + const updated = await this.repo.readOne(object.id); + + const prevMentionees = this.mentionNotificationService.extract(object); + const nowMentionees = this.mentionNotificationService.extract(updated); + const newMentionees = difference(prevMentionees, nowMentionees); + await this.mentionNotificationService.notify(newMentionees, updated); + + return this.secureComment(updated, session); } async delete(id: ID, session: Session): Promise { diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.module.ts b/src/components/comments/mention-notification/comment-via-mention-notification.module.ts index da71b58210..e6b539f0ce 100644 --- a/src/components/comments/mention-notification/comment-via-mention-notification.module.ts +++ b/src/components/comments/mention-notification/comment-via-mention-notification.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { NotificationModule } from '../../notifications'; import { CommentViaMentionNotificationResolver } from './comment-via-mention-notification.resolver'; +import { CommentViaMentionNotificationService } from './comment-via-mention-notification.service'; import { CommentViaMentionNotificationStrategy } from './comment-via-mention-notification.strategy'; @Module({ @@ -8,6 +9,8 @@ import { CommentViaMentionNotificationStrategy } from './comment-via-mention-not providers: [ CommentViaMentionNotificationResolver, CommentViaMentionNotificationStrategy, + CommentViaMentionNotificationService, ], + exports: [CommentViaMentionNotificationService], }) export class CommentViaMentionNotificationModule {} diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.service.ts b/src/components/comments/mention-notification/comment-via-mention-notification.service.ts new file mode 100644 index 0000000000..76b0b13225 --- /dev/null +++ b/src/components/comments/mention-notification/comment-via-mention-notification.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { ID, UnsecuredDto } from '~/common'; +import { NotificationService } from '../../notifications'; +import { Comment } from '../dto'; +import { CommentViaMentionNotification } from './comment-via-mention-notification.dto'; + +@Injectable() +export class CommentViaMentionNotificationService { + constructor(private readonly notifications: NotificationService) {} + + extract(_comment: UnsecuredDto): ReadonlyArray> { + return []; // TODO + } + + async notify( + mentionees: ReadonlyArray>, + comment: UnsecuredDto, + ) { + await this.notifications.create(CommentViaMentionNotification, mentionees, { + comment: comment.id, + }); + } +} From 3ec31aec3d7597c08c70e8201b3921fb849c84ff Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 15 Oct 2024 16:46:30 -0500 Subject: [PATCH 07/26] Rewrite neo4j schema to have a single Notification with multiple recipient relationships --- .../notifications/notification.repository.ts | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/src/components/notifications/notification.repository.ts b/src/components/notifications/notification.repository.ts index f6ce4078aa..05275bfd03 100644 --- a/src/components/notifications/notification.repository.ts +++ b/src/components/notifications/notification.repository.ts @@ -1,5 +1,5 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { node, Query, relation } from 'cypher-query-builder'; +import { inArray, node, Query, relation } from 'cypher-query-builder'; import { omit } from 'lodash'; import { DateTime } from 'luxon'; import { @@ -13,6 +13,7 @@ import { import { CommonRepository } from '~/core/database'; import { apoc, + coalesce, createRelationships, filter, merge, @@ -44,40 +45,41 @@ export class NotificationRepository extends CommonRepository { session: Session, ) { const extra = omit(input, Notification.Props); - const createdAt = DateTime.now(); - await this.db + const res = await this.db .query() - .match(requestingUser(session)) - .create([ - node('source', 'NotificationSource', { - id: variable(apoc.create.uuid()), - createdAt, - }), - relation('out', '', 'producer'), - node('requestingUser'), - ]) - .with('source') - .unwind(recipients.slice(), 'userId') - .match(node('for', 'User', { id: variable('userId') })) .create( node('node', EnhancedResource.of(type).dbLabels, { id: variable(apoc.create.uuid()), - createdAt, - unread: variable('true'), + createdAt: DateTime.now(), type: this.getType(type), }), ) - .with('*') + .with('node') .apply(this.service.getStrategy(type).saveForNeo4j(extra)) .with('*') + .match(requestingUser(session)) .apply( - createRelationships(Notification, { - in: { produced: variable('source') }, - out: { for: variable('for') }, + createRelationships(Notification, 'out', { + creator: variable('requestingUser'), }), ) - .return('node') - .run(); + .subQuery(['node', 'requestingUser'], (sub) => + sub + .match(node('recipient', 'User')) + .where({ 'recipient.id': inArray(recipients) }) + .create([ + node('node'), + relation('out', '', 'recipient', { unread: variable('true') }), + node('recipient'), + ]) + .return<{ totalRecipients: number }>( + 'count(recipient) as totalRecipients', + ), + ) + .subQuery('node', this.hydrate(session)) + .return('dto, totalRecipients') + .first(); + return res!; } async markRead({ id, unread }: MarkNotificationReadArgs, session: Session) { @@ -85,12 +87,12 @@ export class NotificationRepository extends CommonRepository { .query() .match([ node('node', 'Notification', { id }), - relation('out', '', 'for'), + relation('out', 'recipient', 'recipient'), requestingUser(session), ]) - .setValues({ node: { unread } }, true) + .setValues({ recipient: { unread } }, true) .with('node') - .apply(this.hydrate()) + .apply(this.hydrate(session)) .first(); if (!result) { throw new NotFoundException(); @@ -106,19 +108,19 @@ export class NotificationRepository extends CommonRepository { q .match([ node('node', 'Notification'), - relation('out', '', 'for'), + relation('out', 'recipient', 'recipient'), node('requestingUser'), ]) .apply(notificationFilters(input.filter)) .with('node') .orderBy('node.createdAt', 'DESC') - .apply(paginate(input, this.hydrate())), + .apply(paginate(input, this.hydrate(session))), ) .subQuery('requestingUser', (q) => q .match([ - node('node', 'Notification', { unread: variable('true') }), - relation('out', '', 'for'), + node('node', 'Notification'), + relation('out', '', 'recipient', { unread: variable('true') }), node('requestingUser'), ]) .return<{ totalUnread: number }>('count(node) as totalUnread'), @@ -128,7 +130,7 @@ export class NotificationRepository extends CommonRepository { return result!; } - protected hydrate() { + protected hydrate(session: Session) { return (query: Query) => query .subQuery((q) => { @@ -150,9 +152,15 @@ export class NotificationRepository extends CommonRepository { undefined, )!; }) + .optionalMatch([ + node('node'), + relation('out', 'recipient', 'recipient'), + requestingUser(session), + ]) .return<{ dto: UnsecuredDto }>( merge('node', 'extra', { __typename: 'node.type + "Notification"', + unread: coalesce('recipient.unread', false), }).as('dto'), ); } @@ -163,5 +171,5 @@ export class NotificationRepository extends CommonRepository { } const notificationFilters = filter.define(() => NotificationFilters, { - unread: ({ value }) => ({ node: { unread: value } }), + unread: ({ value }) => ({ recipient: { unread: value } }), }); From e1808ad9875fa0f74ed74b295f15f706a5ce5b26 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 15 Oct 2024 16:50:17 -0500 Subject: [PATCH 08/26] Return new notification and recipient count from service --- .../notifications/notification.service.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/notifications/notification.service.ts b/src/components/notifications/notification.service.ts index f9738211f5..5861b73a63 100644 --- a/src/components/notifications/notification.service.ts +++ b/src/components/notifications/notification.service.ts @@ -36,7 +36,20 @@ export abstract class NotificationService { input: T extends { Input: infer Input } ? Input : InputOf, ) { const session = sessionFromContext(this.gqlContextHost.context); - await this.repo.create(recipients, type, input, session); + const { dto, ...rest } = await this.repo.create( + recipients, + type, + input, + session, + ); + return { + ...rest, + notification: this.secure(dto) as T['prototype'], + }; + } + + protected secure(dto: UnsecuredDto) { + return { ...dto, canDelete: true }; } } @@ -78,10 +91,6 @@ export class NotificationServiceImpl return this.secure(result); } - private secure(dto: UnsecuredDto) { - return { ...dto, canDelete: true }; - } - async onModuleInit() { const discovered = await this.discovery.providersWithMetaAtKey< ResourceShape From e14fb7d5ff4127187549a276fc6955286958ab4f Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 15 Oct 2024 16:53:02 -0500 Subject: [PATCH 09/26] SimpleTextNotification -> SystemNotification A bit more of a permanent path --- src/app.module.ts | 4 +- .../simple-text-notification.dto.ts | 19 ------- .../simple-text-notification.module.ts | 10 ---- .../simple-text-notification.resolver.ts | 56 ------------------- .../simple-text-notification.strategy.ts | 34 ----------- .../system-notification.dto.ts | 15 +++++ .../system-notification.module.ts | 10 ++++ .../system-notification.resolver.ts | 46 +++++++++++++++ .../system-notification.strategy.ts | 5 ++ 9 files changed, 78 insertions(+), 121 deletions(-) delete mode 100644 src/components/notification-simple-text/simple-text-notification.dto.ts delete mode 100644 src/components/notification-simple-text/simple-text-notification.module.ts delete mode 100644 src/components/notification-simple-text/simple-text-notification.resolver.ts delete mode 100644 src/components/notification-simple-text/simple-text-notification.strategy.ts create mode 100644 src/components/notification-system/system-notification.dto.ts create mode 100644 src/components/notification-system/system-notification.module.ts create mode 100644 src/components/notification-system/system-notification.resolver.ts create mode 100644 src/components/notification-system/system-notification.strategy.ts diff --git a/src/app.module.ts b/src/app.module.ts index 08c986e502..4d5c7b75d9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,7 +18,7 @@ import { FilmModule } from './components/film/film.module'; import { FundingAccountModule } from './components/funding-account/funding-account.module'; import { LanguageModule } from './components/language/language.module'; import { LocationModule } from './components/location/location.module'; -import { SimpleTextNotificationModule } from './components/notification-simple-text/simple-text-notification.module'; +import { SystemNotificationModule } from './components/notification-system/system-notification.module'; import { NotificationModule } from './components/notifications/notification.module'; import { OrganizationModule } from './components/organization/organization.module'; import { PartnerModule } from './components/partner/partner.module'; @@ -92,7 +92,7 @@ if (process.env.NODE_ENV !== 'production') { PromptsModule, PnpExtractionResultModule, NotificationModule, - SimpleTextNotificationModule, + SystemNotificationModule, ], }) export class AppModule {} diff --git a/src/components/notification-simple-text/simple-text-notification.dto.ts b/src/components/notification-simple-text/simple-text-notification.dto.ts deleted file mode 100644 index e2d8412581..0000000000 --- a/src/components/notification-simple-text/simple-text-notification.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; -import { keys as keysOf } from 'ts-transformer-keys'; -import { SecuredProps } from '~/common'; -import { LinkTo } from '~/core/resources'; -import { Notification } from '../notifications'; - -@ObjectType({ - implements: [Notification], -}) -export class SimpleTextNotification extends Notification { - static readonly Props = keysOf(); - static readonly SecuredProps = keysOf>(); - - @Field(() => String) - readonly content: string; - - // Here to demonstrate relationships in DB & GQL - readonly reference: LinkTo<'User'> | null; -} diff --git a/src/components/notification-simple-text/simple-text-notification.module.ts b/src/components/notification-simple-text/simple-text-notification.module.ts deleted file mode 100644 index 590b5d953d..0000000000 --- a/src/components/notification-simple-text/simple-text-notification.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { NotificationModule } from '../notifications'; -import { SimpleTextNotificationResolver } from './simple-text-notification.resolver'; -import { SimpleTextNotificationStrategy } from './simple-text-notification.strategy'; - -@Module({ - imports: [NotificationModule], - providers: [SimpleTextNotificationResolver, SimpleTextNotificationStrategy], -}) -export class SimpleTextNotificationModule {} diff --git a/src/components/notification-simple-text/simple-text-notification.resolver.ts b/src/components/notification-simple-text/simple-text-notification.resolver.ts deleted file mode 100644 index ec11578583..0000000000 --- a/src/components/notification-simple-text/simple-text-notification.resolver.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - Args, - ID as IDScalar, - Mutation, - Parent, - ResolveField, - Resolver, -} from '@nestjs/graphql'; -import { ID, LoggedInSession, Session, UnauthorizedException } from '~/common'; -import { isAdmin } from '~/common/session'; -import { Loader, LoaderOf } from '~/core'; -import { NotificationService } from '../notifications'; -import { UserLoader } from '../user'; -import { User } from '../user/dto'; -import { SimpleTextNotification as SimpleText } from './simple-text-notification.dto'; - -@Resolver(SimpleText) -export class SimpleTextNotificationResolver { - constructor(private readonly notifications: NotificationService) {} - - @ResolveField(() => User, { nullable: true }) - async reference( - @Parent() { reference }: SimpleText, - @Loader(UserLoader) users: LoaderOf, - ) { - if (!reference) { - return null; - } - return await users.load(reference.id); - } - - @Mutation(() => Boolean) - async testCreateSimpleTextNotification( - @Args('content') content: string, - @Args({ name: 'reference', nullable: true, type: () => IDScalar }) - reference: ID | null, - @LoggedInSession() session: Session, - ) { - if (!isAdmin(session)) { - throw new UnauthorizedException(); - } - - // @ts-expect-error this is just for testing - const allUsers = await this.notifications.repo.db - .query<{ id: ID }>('match (u:User) return u.id as id') - .map('id') - .run(); - - await this.notifications.create(SimpleText, allUsers, { - content, - reference, - }); - - return true; - } -} diff --git a/src/components/notification-simple-text/simple-text-notification.strategy.ts b/src/components/notification-simple-text/simple-text-notification.strategy.ts deleted file mode 100644 index fb5e6578e7..0000000000 --- a/src/components/notification-simple-text/simple-text-notification.strategy.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { node, Query, relation } from 'cypher-query-builder'; -import { createRelationships, exp } from '~/core/database/query'; -import { - INotificationStrategy, - InputOf, - NotificationStrategy, -} from '../notifications'; -import { SimpleTextNotification as SimpleText } from './simple-text-notification.dto'; - -@NotificationStrategy(SimpleText) -export class SimpleTextNotificationStrategy extends INotificationStrategy { - saveForNeo4j(input: InputOf) { - return (query: Query) => - query - .apply( - createRelationships(SimpleText, 'out', { - reference: ['User', input.reference], - }), - ) - .with('*') - .setValues({ node: { content: input.content } }, true); - } - - hydrateExtraForNeo4j(outVar: string) { - return (query: Query) => - query - .optionalMatch([ - node('node'), - relation('out', '', 'reference'), - node('u', 'User'), - ]) - .return(exp({ reference: 'u { .id }' }).as(outVar)); - } -} diff --git a/src/components/notification-system/system-notification.dto.ts b/src/components/notification-system/system-notification.dto.ts new file mode 100644 index 0000000000..40799db42c --- /dev/null +++ b/src/components/notification-system/system-notification.dto.ts @@ -0,0 +1,15 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { keys as keysOf } from 'ts-transformer-keys'; +import { SecuredProps } from '~/common'; +import { Notification } from '../notifications'; + +@ObjectType({ + implements: [Notification], +}) +export class SystemNotification extends Notification { + static readonly Props = keysOf(); + static readonly SecuredProps = keysOf>(); + + @Field() + readonly message: string; +} diff --git a/src/components/notification-system/system-notification.module.ts b/src/components/notification-system/system-notification.module.ts new file mode 100644 index 0000000000..d9038a8ee0 --- /dev/null +++ b/src/components/notification-system/system-notification.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { NotificationModule } from '../notifications'; +import { SystemNotificationResolver } from './system-notification.resolver'; +import { SystemNotificationStrategy } from './system-notification.strategy'; + +@Module({ + imports: [NotificationModule], + providers: [SystemNotificationResolver, SystemNotificationStrategy], +}) +export class SystemNotificationModule {} diff --git a/src/components/notification-system/system-notification.resolver.ts b/src/components/notification-system/system-notification.resolver.ts new file mode 100644 index 0000000000..f8d5edec09 --- /dev/null +++ b/src/components/notification-system/system-notification.resolver.ts @@ -0,0 +1,46 @@ +import { + Args, + Field, + Int, + Mutation, + ObjectType, + Resolver, +} from '@nestjs/graphql'; +import { ID, LoggedInSession, Session, UnauthorizedException } from '~/common'; +import { isAdmin } from '~/common/session'; +import { NotificationService } from '../notifications'; +import { SystemNotification } from './system-notification.dto'; + +@ObjectType() +export class SystemNotificationCreationOutput { + @Field(() => SystemNotification) + notification: SystemNotification; + + @Field(() => Int) + totalRecipients: number; +} + +@Resolver(SystemNotification) +export class SystemNotificationResolver { + constructor(private readonly notifications: NotificationService) {} + + @Mutation(() => SystemNotificationCreationOutput) + async createSystemNotification( + @Args('message') message: string, + @LoggedInSession() session: Session, + ): Promise { + if (!isAdmin(session)) { + throw new UnauthorizedException(); + } + + // @ts-expect-error this is just for testing + const allUsers = await this.notifications.repo.db + .query<{ id: ID }>('match (u:User) return u.id as id') + .map('id') + .run(); + + return await this.notifications.create(SystemNotification, allUsers, { + message, + }); + } +} diff --git a/src/components/notification-system/system-notification.strategy.ts b/src/components/notification-system/system-notification.strategy.ts new file mode 100644 index 0000000000..2e08f545bc --- /dev/null +++ b/src/components/notification-system/system-notification.strategy.ts @@ -0,0 +1,5 @@ +import { INotificationStrategy, NotificationStrategy } from '../notifications'; +import { SystemNotification } from './system-notification.dto'; + +@NotificationStrategy(SystemNotification) +export class SystemNotificationStrategy extends INotificationStrategy {} From 60215db3cf319d6c773c5650ef66d3d3806a4223 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 15 Oct 2024 16:54:47 -0500 Subject: [PATCH 10/26] Change SystemNotification message type: String -> Markdown --- src/common/markdown.scalar.ts | 13 ++++++++----- src/common/scalars.ts | 3 ++- .../notification-system/system-notification.dto.ts | 3 ++- .../system-notification.resolver.ts | 3 ++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/common/markdown.scalar.ts b/src/common/markdown.scalar.ts index e0de2cb2c5..57c2bb3360 100644 --- a/src/common/markdown.scalar.ts +++ b/src/common/markdown.scalar.ts @@ -1,11 +1,9 @@ import { CustomScalar, Scalar } from '@nestjs/graphql'; import { GraphQLError, Kind, ValueNode } from 'graphql'; -@Scalar('InlineMarkdown') -export class InlineMarkdownScalar - implements CustomScalar -{ - description = 'A string that holds inline Markdown formatted text'; +@Scalar('Markdown') +export class MarkdownScalar implements CustomScalar { + description = 'A string that holds Markdown formatted text'; parseLiteral(ast: ValueNode): string | null { if (ast.kind !== Kind.STRING) { @@ -22,3 +20,8 @@ export class InlineMarkdownScalar return value; } } + +@Scalar('InlineMarkdown') +export class InlineMarkdownScalar extends MarkdownScalar { + description = 'A string that holds inline Markdown formatted text'; +} diff --git a/src/common/scalars.ts b/src/common/scalars.ts index cc02e14002..681a9d30f7 100644 --- a/src/common/scalars.ts +++ b/src/common/scalars.ts @@ -3,7 +3,7 @@ import { CustomScalar } from '@nestjs/graphql'; import { GraphQLScalarType } from 'graphql'; import UploadScalar from 'graphql-upload/GraphQLUpload.mjs'; import { DateScalar, DateTimeScalar } from './luxon.graphql'; -import { InlineMarkdownScalar } from './markdown.scalar'; +import { InlineMarkdownScalar, MarkdownScalar } from './markdown.scalar'; import { RichTextScalar } from './rich-text.scalar'; import { UrlScalar } from './url.field'; @@ -16,5 +16,6 @@ export const getRegisteredScalars = (): Scalar[] => [ RichTextScalar, UploadScalar, UrlScalar, + MarkdownScalar, InlineMarkdownScalar, ]; diff --git a/src/components/notification-system/system-notification.dto.ts b/src/components/notification-system/system-notification.dto.ts index 40799db42c..8c2b1d2bf1 100644 --- a/src/components/notification-system/system-notification.dto.ts +++ b/src/components/notification-system/system-notification.dto.ts @@ -1,6 +1,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { keys as keysOf } from 'ts-transformer-keys'; import { SecuredProps } from '~/common'; +import { MarkdownScalar } from '~/common/markdown.scalar'; import { Notification } from '../notifications'; @ObjectType({ @@ -10,6 +11,6 @@ export class SystemNotification extends Notification { static readonly Props = keysOf(); static readonly SecuredProps = keysOf>(); - @Field() + @Field(() => MarkdownScalar) readonly message: string; } diff --git a/src/components/notification-system/system-notification.resolver.ts b/src/components/notification-system/system-notification.resolver.ts index f8d5edec09..5c9fb3f3df 100644 --- a/src/components/notification-system/system-notification.resolver.ts +++ b/src/components/notification-system/system-notification.resolver.ts @@ -7,6 +7,7 @@ import { Resolver, } from '@nestjs/graphql'; import { ID, LoggedInSession, Session, UnauthorizedException } from '~/common'; +import { MarkdownScalar } from '~/common/markdown.scalar'; import { isAdmin } from '~/common/session'; import { NotificationService } from '../notifications'; import { SystemNotification } from './system-notification.dto'; @@ -26,7 +27,7 @@ export class SystemNotificationResolver { @Mutation(() => SystemNotificationCreationOutput) async createSystemNotification( - @Args('message') message: string, + @Args({ name: 'message', type: () => MarkdownScalar }) message: string, @LoggedInSession() session: Session, ): Promise { if (!isAdmin(session)) { From 9cbceb5e21fca81cd6af411665eb3afac48d1ee6 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 15 Oct 2024 17:02:56 -0500 Subject: [PATCH 11/26] Allow omitting the recipient list when creating a notification to pull a dynamic list from DB defined in strategy --- .../system-notification.resolver.ts | 10 ++-------- .../system-notification.strategy.ts | 8 +++++++- .../notifications/notification.repository.ts | 14 +++++++++++--- .../notifications/notification.service.ts | 7 +++++-- .../notifications/notification.strategy.ts | 8 ++++++++ 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/components/notification-system/system-notification.resolver.ts b/src/components/notification-system/system-notification.resolver.ts index 5c9fb3f3df..883bf871f9 100644 --- a/src/components/notification-system/system-notification.resolver.ts +++ b/src/components/notification-system/system-notification.resolver.ts @@ -6,7 +6,7 @@ import { ObjectType, Resolver, } from '@nestjs/graphql'; -import { ID, LoggedInSession, Session, UnauthorizedException } from '~/common'; +import { LoggedInSession, Session, UnauthorizedException } from '~/common'; import { MarkdownScalar } from '~/common/markdown.scalar'; import { isAdmin } from '~/common/session'; import { NotificationService } from '../notifications'; @@ -34,13 +34,7 @@ export class SystemNotificationResolver { throw new UnauthorizedException(); } - // @ts-expect-error this is just for testing - const allUsers = await this.notifications.repo.db - .query<{ id: ID }>('match (u:User) return u.id as id') - .map('id') - .run(); - - return await this.notifications.create(SystemNotification, allUsers, { + return await this.notifications.create(SystemNotification, null, { message, }); } diff --git a/src/components/notification-system/system-notification.strategy.ts b/src/components/notification-system/system-notification.strategy.ts index 2e08f545bc..db25164f2a 100644 --- a/src/components/notification-system/system-notification.strategy.ts +++ b/src/components/notification-system/system-notification.strategy.ts @@ -1,5 +1,11 @@ +import { node, Query } from 'cypher-query-builder'; import { INotificationStrategy, NotificationStrategy } from '../notifications'; import { SystemNotification } from './system-notification.dto'; @NotificationStrategy(SystemNotification) -export class SystemNotificationStrategy extends INotificationStrategy {} +export class SystemNotificationStrategy extends INotificationStrategy { + recipientsForNeo4j() { + return (query: Query) => + query.match(node('recipient', 'User')).return('recipient'); + } +} diff --git a/src/components/notifications/notification.repository.ts b/src/components/notifications/notification.repository.ts index 05275bfd03..b4aa313c6c 100644 --- a/src/components/notifications/notification.repository.ts +++ b/src/components/notifications/notification.repository.ts @@ -1,4 +1,5 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Nil } from '@seedcompany/common'; import { inArray, node, Query, relation } from 'cypher-query-builder'; import { omit } from 'lodash'; import { DateTime } from 'luxon'; @@ -39,7 +40,7 @@ export class NotificationRepository extends CommonRepository { } async create( - recipients: ReadonlyArray>, + recipients: ReadonlyArray> | Nil, type: ResourceShape, input: Record, session: Session, @@ -65,8 +66,15 @@ export class NotificationRepository extends CommonRepository { ) .subQuery(['node', 'requestingUser'], (sub) => sub - .match(node('recipient', 'User')) - .where({ 'recipient.id': inArray(recipients) }) + .apply((q) => + recipients == null + ? q.subQuery( + this.service.getStrategy(type).recipientsForNeo4j(input), + ) + : q + .match(node('recipient', 'User')) + .where({ 'recipient.id': inArray(recipients) }), + ) .create([ node('node'), relation('out', '', 'recipient', { unread: variable('true') }), diff --git a/src/components/notifications/notification.service.ts b/src/components/notifications/notification.service.ts index 5861b73a63..b0d6a3674d 100644 --- a/src/components/notifications/notification.service.ts +++ b/src/components/notifications/notification.service.ts @@ -1,6 +1,6 @@ import { DiscoveryService } from '@golevelup/nestjs-discovery'; import { forwardRef, Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { mapEntries } from '@seedcompany/common'; +import { mapEntries, Nil } from '@seedcompany/common'; import { ID, ResourceShape, @@ -30,9 +30,12 @@ export abstract class NotificationService { @Inject(GqlContextHost) protected readonly gqlContextHost: GqlContextHost; + /** + * If the recipient list is given (not nil), it will override the strategy's recipient resolution. + */ async create>( type: T, - recipients: ReadonlyArray>, + recipients: ReadonlyArray> | Nil, input: T extends { Input: infer Input } ? Input : InputOf, ) { const session = sessionFromContext(this.gqlContextHost.context); diff --git a/src/components/notifications/notification.strategy.ts b/src/components/notifications/notification.strategy.ts index 4c39f47396..658318ebef 100644 --- a/src/components/notifications/notification.strategy.ts +++ b/src/components/notifications/notification.strategy.ts @@ -21,6 +21,14 @@ export abstract class INotificationStrategy< TNotification extends Notification, TInput = InputOf, > { + /** + * Expected to return rows with a user as `recipient` + */ + // eslint-disable-next-line @seedcompany/no-unused-vars + recipientsForNeo4j(input: TInput) { + return (query: Query) => query.unwind([], 'recipient').return('recipient'); + } + saveForNeo4j(input: TInput) { return (query: Query) => query.setValues({ node: input }, true); } From baefb9893fcc33eda9521bb7e029a79543f32845 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 15 Oct 2024 17:17:10 -0500 Subject: [PATCH 12/26] Change to store read state as nullable datetime This gives more info and can still be used as boolean flag --- .../notifications/dto/notification.dto.ts | 6 ++++- .../notifications/notification.repository.ts | 24 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/components/notifications/dto/notification.dto.ts b/src/components/notifications/dto/notification.dto.ts index 8846bf49b0..2e3409c143 100644 --- a/src/components/notifications/dto/notification.dto.ts +++ b/src/components/notifications/dto/notification.dto.ts @@ -1,6 +1,7 @@ import { Field, InterfaceType } from '@nestjs/graphql'; +import { DateTime } from 'luxon'; import { keys as keysOf } from 'ts-transformer-keys'; -import { Resource, SecuredProps } from '~/common'; +import { DateTimeField, Resource, SecuredProps } from '~/common'; import { LinkTo, RegisterResource } from '~/core/resources'; @RegisterResource() @@ -15,6 +16,9 @@ export class Notification extends Resource { @Field(() => Boolean) readonly unread: boolean; + + @DateTimeField({ nullable: true }) + readonly readAt: DateTime | null; } declare module '~/core/resources/map' { diff --git a/src/components/notifications/notification.repository.ts b/src/components/notifications/notification.repository.ts index b4aa313c6c..1c0befc4b9 100644 --- a/src/components/notifications/notification.repository.ts +++ b/src/components/notifications/notification.repository.ts @@ -1,6 +1,13 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { Nil } from '@seedcompany/common'; -import { inArray, node, Query, relation } from 'cypher-query-builder'; +import { + inArray, + isNull, + node, + not, + Query, + relation, +} from 'cypher-query-builder'; import { omit } from 'lodash'; import { DateTime } from 'luxon'; import { @@ -14,7 +21,6 @@ import { import { CommonRepository } from '~/core/database'; import { apoc, - coalesce, createRelationships, filter, merge, @@ -77,7 +83,7 @@ export class NotificationRepository extends CommonRepository { ) .create([ node('node'), - relation('out', '', 'recipient', { unread: variable('true') }), + relation('out', '', 'recipient'), node('recipient'), ]) .return<{ totalRecipients: number }>( @@ -98,7 +104,7 @@ export class NotificationRepository extends CommonRepository { relation('out', 'recipient', 'recipient'), requestingUser(session), ]) - .setValues({ recipient: { unread } }, true) + .setValues({ 'recipient.readAt': unread ? null : DateTime.now() }) .with('node') .apply(this.hydrate(session)) .first(); @@ -128,9 +134,10 @@ export class NotificationRepository extends CommonRepository { q .match([ node('node', 'Notification'), - relation('out', '', 'recipient', { unread: variable('true') }), + relation('out', 'recipient', 'recipient'), node('requestingUser'), ]) + .where({ 'recipient.readAt': isNull() }) .return<{ totalUnread: number }>('count(node) as totalUnread'), ) .return(['items', 'hasMore', 'total', 'totalUnread']) @@ -168,7 +175,8 @@ export class NotificationRepository extends CommonRepository { .return<{ dto: UnsecuredDto }>( merge('node', 'extra', { __typename: 'node.type + "Notification"', - unread: coalesce('recipient.unread', false), + unread: 'recipient.readAt is null', + readAt: 'recipient.readAt', }).as('dto'), ); } @@ -179,5 +187,7 @@ export class NotificationRepository extends CommonRepository { } const notificationFilters = filter.define(() => NotificationFilters, { - unread: ({ value }) => ({ recipient: { unread: value } }), + unread: ({ value }) => ({ + 'recipient.readAt': value ? isNull() : not(isNull()), + }), }); From 723402b21d9f74ecfb0fdb543d94c19561a2dac0 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 15 Oct 2024 17:30:09 -0500 Subject: [PATCH 13/26] Tweak DTO --- .../notifications/dto/notification.dto.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/notifications/dto/notification.dto.ts b/src/components/notifications/dto/notification.dto.ts index 2e3409c143..3ceb2218fd 100644 --- a/src/components/notifications/dto/notification.dto.ts +++ b/src/components/notifications/dto/notification.dto.ts @@ -2,7 +2,7 @@ import { Field, InterfaceType } from '@nestjs/graphql'; import { DateTime } from 'luxon'; import { keys as keysOf } from 'ts-transformer-keys'; import { DateTimeField, Resource, SecuredProps } from '~/common'; -import { LinkTo, RegisterResource } from '~/core/resources'; +import { RegisterResource } from '~/core/resources'; @RegisterResource() @InterfaceType({ @@ -12,12 +12,15 @@ export class Notification extends Resource { static readonly Props: string[] = keysOf(); static readonly SecuredProps: string[] = keysOf>(); - readonly for: LinkTo<'User'>; - - @Field(() => Boolean) + @Field(() => Boolean, { + description: 'Whether the notification is unread for the requesting user', + }) readonly unread: boolean; - @DateTimeField({ nullable: true }) + @DateTimeField({ + nullable: true, + description: 'When the notification was read for the requesting user', + }) readonly readAt: DateTime | null; } From 7e83e14cc00e5407663f36f63fbb7117de2cbbde Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 16 Oct 2024 11:01:17 -0500 Subject: [PATCH 14/26] Add EdgeDB notification schema --- dbschema/migrations/00008-m146fzf.edgeql | 35 ++++++++++++++++++++++++ dbschema/notifications.esdl | 34 +++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 dbschema/migrations/00008-m146fzf.edgeql create mode 100644 dbschema/notifications.esdl diff --git a/dbschema/migrations/00008-m146fzf.edgeql b/dbschema/migrations/00008-m146fzf.edgeql new file mode 100644 index 0000000000..cf3826b5ff --- /dev/null +++ b/dbschema/migrations/00008-m146fzf.edgeql @@ -0,0 +1,35 @@ +CREATE MIGRATION m146fzf6hhpimpivjwcus4r6zr3onj2rygzw7f4qtni3yvcunlfv2a + ONTO m1uvuqdzrjlvsf6g6caqgswsczfl2ior55flcx3j6klxacgfgclyqa +{ + CREATE MODULE Notification IF NOT EXISTS; + CREATE ABSTRACT TYPE default::Notification EXTENDING Mixin::Audited; + CREATE ABSTRACT TYPE Notification::Comment EXTENDING default::Notification { + CREATE REQUIRED LINK comment: Comments::Comment { + ON TARGET DELETE DELETE SOURCE; + }; + }; + CREATE TYPE Notification::Recipient { + CREATE REQUIRED LINK notification: default::Notification { + ON TARGET DELETE DELETE SOURCE; + }; + CREATE REQUIRED LINK user: default::User { + ON TARGET DELETE DELETE SOURCE; + }; + CREATE PROPERTY readAt: std::datetime; + }; + ALTER TYPE default::Notification { + CREATE LINK recipients := (. Date: Wed, 16 Oct 2024 20:58:13 -0500 Subject: [PATCH 15/26] Declare EdgeDB types for resources --- .../comment-via-mention-notification.dto.ts | 13 ++++++++++++- .../notification-system/system-notification.dto.ts | 12 ++++++++++++ .../notifications/dto/notification.dto.ts | 9 +++++---- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.dto.ts b/src/components/comments/mention-notification/comment-via-mention-notification.dto.ts index f6e508fc35..aa1bdaf687 100644 --- a/src/components/comments/mention-notification/comment-via-mention-notification.dto.ts +++ b/src/components/comments/mention-notification/comment-via-mention-notification.dto.ts @@ -1,9 +1,11 @@ import { ObjectType } from '@nestjs/graphql'; import { keys as keysOf } from 'ts-transformer-keys'; import { SecuredProps } from '~/common'; -import { LinkTo } from '~/core/resources'; +import { e } from '~/core/edgedb'; +import { LinkTo, RegisterResource } from '~/core/resources'; import { Notification } from '../../notifications'; +@RegisterResource({ db: e.Notification.CommentViaMention }) @ObjectType({ implements: [Notification], }) @@ -14,3 +16,12 @@ export class CommentViaMentionNotification extends Notification { readonly comment: LinkTo<'Comment'>; } + +declare module '~/core/resources/map' { + interface ResourceMap { + CommentMentionedNotification: typeof CommentViaMentionNotification; + } + interface ResourceDBMap { + CommentMentionedNotification: typeof e.Notification.CommentViaMention; + } +} diff --git a/src/components/notification-system/system-notification.dto.ts b/src/components/notification-system/system-notification.dto.ts index 8c2b1d2bf1..a9fd8f2d79 100644 --- a/src/components/notification-system/system-notification.dto.ts +++ b/src/components/notification-system/system-notification.dto.ts @@ -2,8 +2,11 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { keys as keysOf } from 'ts-transformer-keys'; import { SecuredProps } from '~/common'; import { MarkdownScalar } from '~/common/markdown.scalar'; +import { RegisterResource } from '~/core'; +import { e } from '~/core/edgedb'; import { Notification } from '../notifications'; +@RegisterResource({ db: e.Notification.System }) @ObjectType({ implements: [Notification], }) @@ -14,3 +17,12 @@ export class SystemNotification extends Notification { @Field(() => MarkdownScalar) readonly message: string; } + +declare module '~/core/resources/map' { + interface ResourceMap { + SystemNotification: typeof SystemNotification; + } + interface ResourceDBMap { + SystemNotification: typeof e.Notification.System; + } +} diff --git a/src/components/notifications/dto/notification.dto.ts b/src/components/notifications/dto/notification.dto.ts index 3ceb2218fd..04d5a7b7c2 100644 --- a/src/components/notifications/dto/notification.dto.ts +++ b/src/components/notifications/dto/notification.dto.ts @@ -2,9 +2,10 @@ import { Field, InterfaceType } from '@nestjs/graphql'; import { DateTime } from 'luxon'; import { keys as keysOf } from 'ts-transformer-keys'; import { DateTimeField, Resource, SecuredProps } from '~/common'; +import { e } from '~/core/edgedb'; import { RegisterResource } from '~/core/resources'; -@RegisterResource() +@RegisterResource({ db: e.default.Notification }) @InterfaceType({ implements: [Resource], }) @@ -28,7 +29,7 @@ declare module '~/core/resources/map' { interface ResourceMap { Notification: typeof Notification; } - // interface ResourceDBMap { - // Notification: typeof e.Notification; - // } + interface ResourceDBMap { + Notification: typeof e.default.Notification; + } } From 90dd13ca1fc426f6c10b464c8503122defa1a15d Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 16 Oct 2024 21:29:31 -0500 Subject: [PATCH 16/26] Split paginate helper for composition --- .../location/location.edgedb.repository.ts | 2 +- src/core/edgedb/dto.repository.ts | 29 +++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/components/location/location.edgedb.repository.ts b/src/components/location/location.edgedb.repository.ts index db0e7e5460..e4ad50fadb 100644 --- a/src/components/location/location.edgedb.repository.ts +++ b/src/components/location/location.edgedb.repository.ts @@ -67,6 +67,6 @@ export class LocationEdgeDBRepository ...this.applyFilter(obj, input), ...this.applyOrderBy(obj, input), })); - return await this.paginate(all, input); + return await this.paginateAndRun(all, input); } } diff --git a/src/core/edgedb/dto.repository.ts b/src/core/edgedb/dto.repository.ts index 653d69ddfc..fcd547bbeb 100644 --- a/src/core/edgedb/dto.repository.ts +++ b/src/core/edgedb/dto.repository.ts @@ -185,7 +185,22 @@ export const RepoFor = < : {}; } - protected async paginate( + protected async paginateAndRun( + listOfAllQuery: $expr_Select< + $.TypeSet<$.ObjectType>, $.Cardinality.Many> + >, + input: PaginationInput, + ): Promise> { + const paginated = this.paginate(listOfAllQuery, input); + const query = e.select({ + ...paginated, + items: e.select(paginated.items, this.hydrate), + }); + const result = await this.db.run(query); + return result as any; + } + + protected paginate( listOfAllQuery: $expr_Select< $.TypeSet<$.ObjectType>, $.Cardinality.Many> >, @@ -195,18 +210,14 @@ export const RepoFor = < offset: (input.page - 1) * input.count, limit: input.count + 1, })); - const items = e.select(thisPage, (obj: any) => ({ - ...this.hydrate(obj), + const items = e.select(thisPage, () => ({ limit: input.count, })); - const query = e.select({ + return { items: items as any, total: e.count(listOfAllQuery), hasMore: e.op(e.count(thisPage), '>', input.count), - }); - - const result = await this.db.run(query); - return result as PaginatedListType; + }; } // endregion @@ -272,7 +283,7 @@ export const RepoFor = < ...this.applyFilter(obj, input), ...this.applyOrderBy(obj, input), })); - return await this.paginate(all as any, input); + return await this.paginateAndRun(all as any, input); } async create(input: EasyInsertShape): Promise { From 3adad58eb33f98ce75531f9996079d567b532c21 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 16 Oct 2024 21:29:59 -0500 Subject: [PATCH 17/26] Implement notification queries for EdgeDB --- ...mment-via-mention-notification.strategy.ts | 13 ++ .../system-notification.strategy.ts | 17 +++ .../notifications/dto/notification.dto.ts | 10 +- .../notification.edgedb.repository.ts | 113 ++++++++++++++++++ .../notifications/notification.module.ts | 6 +- .../notifications/notification.strategy.ts | 21 ++++ 6 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/components/notifications/notification.edgedb.repository.ts diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts b/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts index be5097511e..e8f9672daf 100644 --- a/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts +++ b/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts @@ -1,5 +1,6 @@ import { node, Query, relation } from 'cypher-query-builder'; import { createRelationships, exp } from '~/core/database/query'; +import { e } from '~/core/edgedb'; import { INotificationStrategy, InputOf, @@ -18,6 +19,12 @@ export class CommentViaMentionNotificationStrategy extends INotificationStrategy ); } + insertForEdgeDB(input: InputOf) { + return e.insert(e.Notification.CommentMentioned, { + comment: e.cast(e.Comments.Comment, e.uuid(input.comment)), + }); + } + hydrateExtraForNeo4j(outVar: string) { return (query: Query) => query @@ -32,4 +39,10 @@ export class CommentViaMentionNotificationStrategy extends INotificationStrategy }).as(outVar), ); } + + hydrateExtraForEdgeDB() { + return e.is(e.Notification.CommentMentioned, { + comment: true, + }); + } } diff --git a/src/components/notification-system/system-notification.strategy.ts b/src/components/notification-system/system-notification.strategy.ts index db25164f2a..fbf610692b 100644 --- a/src/components/notification-system/system-notification.strategy.ts +++ b/src/components/notification-system/system-notification.strategy.ts @@ -1,4 +1,5 @@ import { node, Query } from 'cypher-query-builder'; +import { e } from '~/core/edgedb'; import { INotificationStrategy, NotificationStrategy } from '../notifications'; import { SystemNotification } from './system-notification.dto'; @@ -8,4 +9,20 @@ export class SystemNotificationStrategy extends INotificationStrategy query.match(node('recipient', 'User')).return('recipient'); } + + recipientsForEdgeDB() { + return e.User; // all users + } + + insertForEdgeDB(input: SystemNotification) { + return e.insert(e.Notification.System, { + message: input.message, + }); + } + + hydrateExtraForEdgeDB() { + return e.is(e.Notification.System, { + message: true, + }); + } } diff --git a/src/components/notifications/dto/notification.dto.ts b/src/components/notifications/dto/notification.dto.ts index 04d5a7b7c2..4813d42c94 100644 --- a/src/components/notifications/dto/notification.dto.ts +++ b/src/components/notifications/dto/notification.dto.ts @@ -1,13 +1,19 @@ import { Field, InterfaceType } from '@nestjs/graphql'; import { DateTime } from 'luxon'; import { keys as keysOf } from 'ts-transformer-keys'; -import { DateTimeField, Resource, SecuredProps } from '~/common'; +import { + DateTimeField, + resolveByTypename, + Resource, + SecuredProps, +} from '~/common'; import { e } from '~/core/edgedb'; import { RegisterResource } from '~/core/resources'; @RegisterResource({ db: e.default.Notification }) @InterfaceType({ implements: [Resource], + resolveType: resolveByTypename(Notification.name), }) export class Notification extends Resource { static readonly Props: string[] = keysOf(); @@ -18,6 +24,8 @@ export class Notification extends Resource { }) readonly unread: boolean; + declare readonly __typename: string; + @DateTimeField({ nullable: true, description: 'When the notification was read for the requesting user', diff --git a/src/components/notifications/notification.edgedb.repository.ts b/src/components/notifications/notification.edgedb.repository.ts new file mode 100644 index 0000000000..a7779d1db3 --- /dev/null +++ b/src/components/notifications/notification.edgedb.repository.ts @@ -0,0 +1,113 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Nil } from '@seedcompany/common'; +import { ID, PublicOf, ResourceShape } from '~/common'; +import { e, RepoFor, ScopeOf } from '~/core/edgedb'; +import { + MarkNotificationReadArgs, + Notification, + NotificationListInput, +} from './dto'; +import { NotificationRepository as Neo4jRepository } from './notification.repository'; +import { NotificationServiceImpl } from './notification.service'; + +@Injectable() +export class NotificationRepository + extends RepoFor(Notification, { + hydrate: (notification) => ({ + __typename: notification.__type__.name, + ...notification['*'], + }), + omit: ['create', 'update', 'list', 'readOne', 'readMany'], + }) + implements PublicOf +{ + constructor( + @Inject(forwardRef(() => NotificationServiceImpl)) + private readonly service: NotificationServiceImpl & {}, + ) { + super(); + } + + onModuleInit() { + (this as any).hydrate = e.shape(e.Notification, (notification) => { + return Object.assign( + { + __typename: notification.__type__.name, + }, + notification['*'], + ...[...this.service.strategyMap.values()].flatMap( + (strategy) => strategy.hydrateExtraForEdgeDB() ?? [], + ), + ); + }); + } + + async create( + recipients: ReadonlyArray> | Nil, + type: ResourceShape, + input: Record, + ) { + const strategy = this.service.getStrategy(type); + + const created = strategy.insertForEdgeDB(input); + + const recipientsQuery = recipients + ? e.cast(e.User, e.cast(e.uuid, e.set(...recipients))) + : strategy.recipientsForEdgeDB(input); + + const insertedRecipients = e.for(recipientsQuery, (user) => + e.insert(e.Notification.Recipient, { + notification: created, + user: user, + }), + ); + + const query = e.select({ + dto: e.select(created, this.hydrate), + totalRecipients: e.count(insertedRecipients), + }); + + return await this.db.run(query); + } + + async markRead({ id, unread }: MarkNotificationReadArgs) { + const notification = e.cast(e.Notification, e.uuid(id)); + const next = unread ? null : e.datetime_of_transaction(); + + const recipient = e.assert_exists(notification.currentRecipient); + const updated = e.update(recipient, () => ({ + set: { readAt: next }, + })); + const query = e.select(updated.notification, this.hydrate); + return await this.db.run(query); + } + + async list(input: NotificationListInput) { + const myNotifications = e.select(e.Notification, (notification) => ({ + filter: e.op(e.global.currentUser, 'in', notification.recipients.user), + })); + + const paginated = this.paginate(myNotifications, input); + + const myUnread = e.select(myNotifications, (notification) => ({ + filter: e.op(notification.unread, '=', true), + })); + + const query = e.select({ + ...paginated, + items: e.select(paginated.items as typeof e.Notification, this.hydrate), + totalUnread: e.count(myUnread), + }); + + return await this.db.run(query); + } + + protected listFilters( + notification: ScopeOf, + { filter }: NotificationListInput, + ) { + return [ + filter?.unread != null && e.op(notification.unread, '=', filter.unread), + ]; + } +} diff --git a/src/components/notifications/notification.module.ts b/src/components/notifications/notification.module.ts index c688d18401..e67160c326 100644 --- a/src/components/notifications/notification.module.ts +++ b/src/components/notifications/notification.module.ts @@ -1,5 +1,7 @@ import { Module } from '@nestjs/common'; -import { NotificationRepository } from './notification.repository'; +import { splitDb } from '~/core'; +import { NotificationRepository as EdgeDBRepository } from './notification.edgedb.repository'; +import { NotificationRepository as Neo4jRepository } from './notification.repository'; import { NotificationResolver } from './notification.resolver'; import { NotificationService, @@ -11,7 +13,7 @@ import { NotificationResolver, { provide: NotificationService, useExisting: NotificationServiceImpl }, NotificationServiceImpl, - NotificationRepository, + splitDb(Neo4jRepository, EdgeDBRepository), ], exports: [NotificationService], }) diff --git a/src/components/notifications/notification.strategy.ts b/src/components/notifications/notification.strategy.ts index 658318ebef..a596ab6f0e 100644 --- a/src/components/notifications/notification.strategy.ts +++ b/src/components/notifications/notification.strategy.ts @@ -4,6 +4,7 @@ import { AbstractClass, Simplify } from 'type-fest'; import type { UnwrapSecured } from '~/common'; import type { RawChangeOf } from '~/core/database/changes'; import type { QueryFragment } from '~/core/database/query-augmentation/apply'; +import { $, e } from '~/core/edgedb'; import type { Notification } from './dto'; export const NotificationStrategy = createMetadataDecorator({ @@ -26,15 +27,35 @@ export abstract class INotificationStrategy< */ // eslint-disable-next-line @seedcompany/no-unused-vars recipientsForNeo4j(input: TInput) { + // No recipients. Only those explicitly specified in the service create call. return (query: Query) => query.unwind([], 'recipient').return('recipient'); } + recipientsForEdgeDB( + // eslint-disable-next-line @seedcompany/no-unused-vars + input: TInput, + ): $.Expression<$.TypeSet> { + // No recipients. Only those explicitly specified in the service create call. + return e.cast(e.User, e.set()); + } + saveForNeo4j(input: TInput) { return (query: Query) => query.setValues({ node: input }, true); } + abstract insertForEdgeDB( + input: TInput, + ): $.Expression< + $.TypeSet< + $.ObjectType, + $.Cardinality.One + > + >; + hydrateExtraForNeo4j(outVar: string): QueryFragment | undefined { const _used = outVar; return undefined; } + + abstract hydrateExtraForEdgeDB(): Record; } From 064cc0c49f32e1300034057659385b37918a2755 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 16 Oct 2024 22:26:06 -0500 Subject: [PATCH 18/26] Ensure strategy discovery is done before using --- .../notifications/notification.edgedb.repository.ts | 4 +++- src/components/notifications/notification.service.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/notifications/notification.edgedb.repository.ts b/src/components/notifications/notification.edgedb.repository.ts index a7779d1db3..ee9361d405 100644 --- a/src/components/notifications/notification.edgedb.repository.ts +++ b/src/components/notifications/notification.edgedb.repository.ts @@ -28,7 +28,9 @@ export class NotificationRepository super(); } - onModuleInit() { + async onModuleInit() { + await this.service.ready.wait(); + (this as any).hydrate = e.shape(e.Notification, (notification) => { return Object.assign( { diff --git a/src/components/notifications/notification.service.ts b/src/components/notifications/notification.service.ts index b0d6a3674d..6bde9fa7d0 100644 --- a/src/components/notifications/notification.service.ts +++ b/src/components/notifications/notification.service.ts @@ -1,6 +1,7 @@ import { DiscoveryService } from '@golevelup/nestjs-discovery'; import { forwardRef, Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { mapEntries, Nil } from '@seedcompany/common'; +import Event from 'edgedb/dist/primitives/event.js'; import { ID, ResourceShape, @@ -65,6 +66,7 @@ export class NotificationServiceImpl ResourceShape, INotificationStrategy >; + readonly ready = new ((Event as any).default as typeof Event)(); constructor(private readonly discovery: DiscoveryService) { super(); @@ -107,5 +109,6 @@ export class NotificationServiceImpl } return [meta, instance]; }).asMap; + this.ready.set(); } } From 65f63ee6942d3b1bd6433349574cdb935953b100 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 16 Oct 2024 22:32:28 -0500 Subject: [PATCH 19/26] Apply sane defaults for insert & hydrate for EdgeDB to reduce boilerplate in the concrete strategies --- ...mment-via-mention-notification.strategy.ts | 13 ------ .../system-notification.strategy.ts | 12 ----- .../notification.edgedb.repository.ts | 45 +++++++++++++------ .../notifications/notification.strategy.ts | 21 ++++++--- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts b/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts index e8f9672daf..be5097511e 100644 --- a/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts +++ b/src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts @@ -1,6 +1,5 @@ import { node, Query, relation } from 'cypher-query-builder'; import { createRelationships, exp } from '~/core/database/query'; -import { e } from '~/core/edgedb'; import { INotificationStrategy, InputOf, @@ -19,12 +18,6 @@ export class CommentViaMentionNotificationStrategy extends INotificationStrategy ); } - insertForEdgeDB(input: InputOf) { - return e.insert(e.Notification.CommentMentioned, { - comment: e.cast(e.Comments.Comment, e.uuid(input.comment)), - }); - } - hydrateExtraForNeo4j(outVar: string) { return (query: Query) => query @@ -39,10 +32,4 @@ export class CommentViaMentionNotificationStrategy extends INotificationStrategy }).as(outVar), ); } - - hydrateExtraForEdgeDB() { - return e.is(e.Notification.CommentMentioned, { - comment: true, - }); - } } diff --git a/src/components/notification-system/system-notification.strategy.ts b/src/components/notification-system/system-notification.strategy.ts index fbf610692b..2391f5b45e 100644 --- a/src/components/notification-system/system-notification.strategy.ts +++ b/src/components/notification-system/system-notification.strategy.ts @@ -13,16 +13,4 @@ export class SystemNotificationStrategy extends INotificationStrategy { - return Object.assign( - { - __typename: notification.__type__.name, - }, - notification['*'], - ...[...this.service.strategyMap.values()].flatMap( - (strategy) => strategy.hydrateExtraForEdgeDB() ?? [], - ), - ); - }); + const basePointers = setOf( + Object.keys(this.resource.db.__element__.__pointers__), + ); + const hydrateConcretes = Object.assign( + {}, + ...[...this.service.strategyMap].flatMap(([type, strategy]) => { + if (strategy.hydrateExtraForEdgeDB) { + return strategy.hydrateExtraForEdgeDB(); + } + const dbType = EnhancedResource.of(type as typeof Notification).db; + const ownPointers = Object.keys(dbType.__element__.__pointers__).filter( + (p) => !p.startsWith('<') && !basePointers.has(p), + ); + return e.is( + dbType, + mapValues.fromList(ownPointers, () => true).asRecord, + ); + }), + ); + (this as any).hydrate = e.shape(e.Notification, (notification) => ({ + __typename: notification.__type__.name, + ...notification['*'], + ...hydrateConcretes, + })); } async create( @@ -51,7 +65,10 @@ export class NotificationRepository ) { const strategy = this.service.getStrategy(type); - const created = strategy.insertForEdgeDB(input); + const dbType = EnhancedResource.of(type as typeof Notification).db; + const created = + strategy.insertForEdgeDB?.(input) ?? + e.insert(dbType, mapToSetBlock(dbType, input, false)); const recipientsQuery = recipients ? e.cast(e.User, e.cast(e.uuid, e.set(...recipients))) diff --git a/src/components/notifications/notification.strategy.ts b/src/components/notifications/notification.strategy.ts index a596ab6f0e..cae4b00343 100644 --- a/src/components/notifications/notification.strategy.ts +++ b/src/components/notifications/notification.strategy.ts @@ -43,7 +43,19 @@ export abstract class INotificationStrategy< return (query: Query) => query.setValues({ node: input }, true); } - abstract insertForEdgeDB( + hydrateExtraForNeo4j(outVar: string): QueryFragment | undefined { + const _used = outVar; + return undefined; + } +} + +/* eslint-disable @typescript-eslint/method-signature-style */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface INotificationStrategy< + TNotification extends Notification, + TInput = InputOf, +> { + insertForEdgeDB?( input: TInput, ): $.Expression< $.TypeSet< @@ -52,10 +64,5 @@ export abstract class INotificationStrategy< > >; - hydrateExtraForNeo4j(outVar: string): QueryFragment | undefined { - const _used = outVar; - return undefined; - } - - abstract hydrateExtraForEdgeDB(): Record; + hydrateExtraForEdgeDB?(): Record; } From 8ae44100a289000e227c5fd801b509ac248beff4 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 18 Oct 2024 11:33:35 -0500 Subject: [PATCH 20/26] Everyone can read & create comments (#3314) --- dbschema/migrations/00009-m17teec.edgeql | 26 +++++++++++++++++++ .../by-feature/everyone-can-comment.policy.ts | 10 +++++++ .../authorization/policies/index.ts | 1 + 3 files changed, 37 insertions(+) create mode 100644 dbschema/migrations/00009-m17teec.edgeql create mode 100644 src/components/authorization/policies/by-feature/everyone-can-comment.policy.ts diff --git a/dbschema/migrations/00009-m17teec.edgeql b/dbschema/migrations/00009-m17teec.edgeql new file mode 100644 index 0000000000..8461405d9e --- /dev/null +++ b/dbschema/migrations/00009-m17teec.edgeql @@ -0,0 +1,26 @@ +CREATE MIGRATION m17teecxqpduefkkjxf64pokydi5e2ztcip6ayk23fvm6snddjbksa + ONTO m146fzf6hhpimpivjwcus4r6zr3onj2rygzw7f4qtni3yvcunlfv2a +{ + ALTER TYPE Comments::Comment { + CREATE ACCESS POLICY CanDeleteGeneratedFromAppPoliciesForComment + ALLOW DELETE USING (.isCreator); + }; + ALTER TYPE Comments::Comment { + DROP ACCESS POLICY CanSelectUpdateReadDeleteGeneratedFromAppPoliciesForComment; + }; + ALTER TYPE Comments::Comment { + CREATE ACCESS POLICY CanSelectUpdateReadInsertGeneratedFromAppPoliciesForComment + ALLOW SELECT, UPDATE READ, INSERT ; + }; + ALTER TYPE Comments::Thread { + CREATE ACCESS POLICY CanDeleteGeneratedFromAppPoliciesForCommentThread + ALLOW DELETE USING (.isCreator); + }; + ALTER TYPE Comments::Thread { + DROP ACCESS POLICY CanSelectUpdateReadDeleteGeneratedFromAppPoliciesForCommentThread; + }; + ALTER TYPE Comments::Thread { + CREATE ACCESS POLICY CanSelectUpdateReadInsertGeneratedFromAppPoliciesForCommentThread + ALLOW SELECT, UPDATE READ, INSERT ; + }; +}; diff --git a/src/components/authorization/policies/by-feature/everyone-can-comment.policy.ts b/src/components/authorization/policies/by-feature/everyone-can-comment.policy.ts new file mode 100644 index 0000000000..ad09b1a135 --- /dev/null +++ b/src/components/authorization/policies/by-feature/everyone-can-comment.policy.ts @@ -0,0 +1,10 @@ +import { Policy } from '../util'; + +@Policy('all', (r) => [ + // Technically, we want only when the Commentable is readable. + // I think this is sufficient for practical use at this point in time. + ...[r.CommentThread, r.Comment].flatMap((it) => it.read.create), + // This shouldn't be needed, but it is. children() needs rewrite. + r.Commentable.children((c) => c.commentThreads.read.create), +]) +export class EveryoneCanCommentPolicy {} diff --git a/src/components/authorization/policies/index.ts b/src/components/authorization/policies/index.ts index bed96143b9..39b1e3bb73 100644 --- a/src/components/authorization/policies/index.ts +++ b/src/components/authorization/policies/index.ts @@ -23,5 +23,6 @@ export * from './by-feature/progress-report-media-owner.policy'; export * from './by-feature/project-change-requests-beta.policy'; export * from './by-feature/read-util-objects.policy'; export * from './by-feature/user-can-edit-self.policy'; +export * from './by-feature/everyone-can-comment.policy'; export * from './by-feature/user-can-manage-own-comments.policy'; export * from './by-feature/new-progress-reports-beta.policy'; From 65879492dfa99238f04b3e0e80c3133df14c94d0 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 20 Sep 2024 16:13:00 -0500 Subject: [PATCH 21/26] Express -> Fastify --- .eslintrc.cjs | 5 - package.json | 15 +- .../authentication/current-user.provider.ts | 4 +- src/core/exception/exception.filter.ts | 2 +- src/core/graphql/graphql.module.ts | 35 +- src/core/graphql/graphql.options.ts | 25 +- src/core/http/http.adapter.ts | 63 +- src/core/http/types.ts | 15 +- src/core/timeout.interceptor.ts | 2 +- yarn.lock | 1053 +++++++++-------- 10 files changed, 643 insertions(+), 576 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a877417b82..1078eb4d92 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -37,11 +37,6 @@ const oldRestrictedImports = [ importNames: ['Dictionary', 'SafeDictionary'], message: 'Use a type with strict keys instead', }, - { - name: 'express-serve-static-core', - importNames: ['Dictionary'], - message: 'Use a type with strict keys instead', - }, ]; /** @type {import('@seedcompany/eslint-plugin').ImportRestriction[]} */ diff --git a/package.json b/package.json index d0fcac4a66..27701f5946 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,12 @@ "dependencies": { "@apollo/server": "^4.9.5", "@apollo/subgraph": "^2.5.6", + "@as-integrations/fastify": "^2.1.1", "@aws-sdk/client-s3": "^3.440.0", "@aws-sdk/s3-request-presigner": "^3.440.0", "@faker-js/faker": "^8.2.0", + "@fastify/cookie": "^9.4.0", + "@fastify/cors": "^9.0.1", "@ffprobe-installer/ffprobe": "^2.1.2", "@golevelup/nestjs-discovery": "^4.0.0", "@leeoniya/ufuzzy": "^1.0.11", @@ -43,7 +46,7 @@ "@nestjs/common": "^10.2.7", "@nestjs/core": "^10.2.7", "@nestjs/graphql": "^12.0.9", - "@nestjs/platform-express": "^10.2.7", + "@nestjs/platform-fastify": "^10.4.3", "@patarapolw/prettyprint": "^1.0.3", "@seedcompany/cache": "^2.0.0", "@seedcompany/common": ">=0.13.1 <1", @@ -59,16 +62,15 @@ "cli-table3": "^0.6.3", "clipanion": "^4.0.0-rc.3", "common-tags": "^1.8.2", - "cookie-parser": "^1.4.6", "cypher-query-builder": "patch:cypher-query-builder@npm%3A6.0.4#~/.yarn/patches/cypher-query-builder-npm-6.0.4-e8707a5e8e.patch", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", "edgedb": "^1.6.0-canary.20240827T111834", "execa": "^8.0.1", - "express": "^4.18.2", "extensionless": "^1.7.0", "fast-safe-stringify": "^2.1.1", "fastest-levenshtein": "^1.0.16", + "fastify": "^4.28.1", "file-type": "^18.6.0", "glob": "^10.3.10", "got": "^14.3.0", @@ -116,9 +118,6 @@ "@seedcompany/eslint-plugin": "^3.4.1", "@tsconfig/strictest": "^2.0.2", "@types/common-tags": "^1.8.3", - "@types/cookie-parser": "^1.4.5", - "@types/express": "^4.17.20", - "@types/express-serve-static-core": "^4.17.39", "@types/ffprobe": "^1.1.7", "@types/graphql-upload": "^16.0.4", "@types/jest": "^29.5.7", @@ -154,9 +153,13 @@ "neo4j-driver-bolt-connection@npm:5.20.0": "patch:neo4j-driver-bolt-connection@npm%3A5.20.0#~/.yarn/patches/neo4j-driver-bolt-connection-npm-5.20.0-1f7809f435.patch", "neo4j-driver-core@npm:5.20.0": "patch:neo4j-driver-core@npm%3A5.20.0#~/.yarn/patches/neo4j-driver-core-npm-5.20.0-99216f6938.patch", "@apollo/server-plugin-landing-page-graphql-playground": "npm:empty-npm-package@*", + "@apollo/server/express": "npm:empty-npm-package@*", "@nestjs/cli/fork-ts-checker-webpack-plugin": "npm:empty-npm-package@*", "@nestjs/cli/webpack": "npm:empty-npm-package@*", "@nestjs/cli/typescript": "^5.1.6", + "@types/express": "npm:@types/stack-trace@*", + "@types/express-serve-static-core": "npm:@types/stack-trace@*", + "@types/koa": "npm:@types/stack-trace@*", "subscriptions-transport-ws": "npm:empty-npm-package@*" }, "dependenciesMeta": { diff --git a/src/components/authentication/current-user.provider.ts b/src/components/authentication/current-user.provider.ts index 1440533413..fbfb445302 100644 --- a/src/components/authentication/current-user.provider.ts +++ b/src/components/authentication/current-user.provider.ts @@ -47,14 +47,14 @@ export class EdgeDBCurrentUserProvider const { request, session$ } = GqlExecutionContext.create(context).getContext(); if (request) { - const optionsHolder = this.optionsHolderByRequest.get(request)!; + const optionsHolder = this.optionsHolderByRequest.get(request.raw)!; session$.subscribe((session) => { this.applyToOptions(session, optionsHolder); }); } } else if (type === 'http') { const request = context.switchToHttp().getRequest(); - const optionsHolder = this.optionsHolderByRequest.get(request)!; + const optionsHolder = this.optionsHolderByRequest.get(request.raw)!; this.applyToOptions(request.session, optionsHolder); } diff --git a/src/core/exception/exception.filter.ts b/src/core/exception/exception.filter.ts index c1475ec2c2..8b899b9175 100644 --- a/src/core/exception/exception.filter.ts +++ b/src/core/exception/exception.filter.ts @@ -26,7 +26,7 @@ export class ExceptionFilter implements GqlExceptionFilter { const hack = isFromHackAttempt(exception, args); if (hack) { - hack.destroy(); + hack.raw.destroy(); return; } diff --git a/src/core/graphql/graphql.module.ts b/src/core/graphql/graphql.module.ts index ff98a4cac5..d3e8eba52d 100644 --- a/src/core/graphql/graphql.module.ts +++ b/src/core/graphql/graphql.module.ts @@ -2,7 +2,10 @@ import { ApolloDriver } from '@nestjs/apollo'; import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { GraphQLModule as NestGraphqlModule } from '@nestjs/graphql'; -import createUploadMiddleware from 'graphql-upload/graphqlUploadExpress.mjs'; +import processUploadRequest, { + UploadOptions, +} from 'graphql-upload/processRequest.mjs'; +import { HttpAdapterHost } from '~/core/http'; import { TracingModule } from '../tracing'; import { GqlContextHost, GqlContextHostImpl } from './gql-context.host'; import { GraphqlErrorFormatter } from './graphql-error-formatter'; @@ -13,6 +16,8 @@ import { GraphqlOptions } from './graphql.options'; import './types'; +const FileUploadOptions: UploadOptions = {}; + @Module({ imports: [TracingModule], providers: [ @@ -42,15 +47,35 @@ export class GraphqlOptionsModule {} exports: [NestGraphqlModule, GqlContextHost], }) export class GraphqlModule implements NestModule { - constructor(private readonly middleware: GqlContextHostImpl) {} + constructor( + private readonly middleware: GqlContextHostImpl, + private readonly app: HttpAdapterHost, + ) {} configure(consumer: MiddlewareConsumer) { // Always attach our GQL Context middleware. // It has its own logic to handle non-gql requests. consumer.apply(this.middleware.use).forRoutes('*'); - // Attach the graphql-upload middleware to the graphql endpoint. - const uploadMiddleware = createUploadMiddleware(); - consumer.apply(uploadMiddleware).forRoutes('/graphql', '/graphql/*'); + // Setup file upload handling + const fastify = this.app.httpAdapter.getInstance(); + const multipartRequests = new WeakSet(); + fastify.addContentTypeParser( + 'multipart/form-data', + (req, payload, done) => { + multipartRequests.add(req); + done(null); + }, + ); + fastify.addHook('preValidation', async (req, reply) => { + if (!multipartRequests.has(req) || !req.url.startsWith('/graphql')) { + return; + } + req.body = await processUploadRequest( + req.raw, + reply.raw, + FileUploadOptions, + ); + }); } } diff --git a/src/core/graphql/graphql.options.ts b/src/core/graphql/graphql.options.ts index 75548e5254..29f1e66d9f 100644 --- a/src/core/graphql/graphql.options.ts +++ b/src/core/graphql/graphql.options.ts @@ -1,10 +1,9 @@ -import { ContextFunction } from '@apollo/server'; -import { ExpressContextFunctionArgument } from '@apollo/server/express4'; import { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault, } from '@apollo/server/plugin/landingPage/default'; -import { ApolloDriverConfig } from '@nestjs/apollo'; +import { ApolloFastifyContextFunctionArgument } from '@as-integrations/fastify'; +import { ApolloDriverConfig as DriverConfig } from '@nestjs/apollo'; import { Injectable } from '@nestjs/common'; import { GqlOptionsFactory } from '@nestjs/graphql'; import { CacheService } from '@seedcompany/cache'; @@ -29,7 +28,7 @@ export class GraphqlOptions implements GqlOptionsFactory { private readonly errorFormatter: GraphqlErrorFormatter, ) {} - async createGqlOptions(): Promise { + async createGqlOptions(): Promise { // Apply git hash to Apollo Studio. // They only look for env, so applying that way. const version = await this.versionService.version; @@ -44,6 +43,7 @@ export class GraphqlOptions implements GqlOptionsFactory { ).asRecord; return { + path: '/graphql/:opName?', autoSchemaFile: 'schema.graphql', context: this.context, playground: false, @@ -80,14 +80,15 @@ export class GraphqlOptions implements GqlOptionsFactory { }; } - context: ContextFunction<[ExpressContextFunctionArgument], GqlContextType> = - async ({ req, res }) => ({ - [isGqlContext.KEY]: true, - request: req, - response: res, - operation: createFakeStubOperation(), - session$: new BehaviorSubject(undefined), - }); + context = ( + ...[request, response]: ApolloFastifyContextFunctionArgument + ): GqlContextType => ({ + [isGqlContext.KEY]: true, + request, + response, + operation: createFakeStubOperation(), + session$: new BehaviorSubject(undefined), + }); } export const createFakeStubOperation = () => { diff --git a/src/core/http/http.adapter.ts b/src/core/http/http.adapter.ts index d07383ae32..c606100d98 100644 --- a/src/core/http/http.adapter.ts +++ b/src/core/http/http.adapter.ts @@ -1,24 +1,30 @@ +import cookieParser from '@fastify/cookie'; +import cors from '@fastify/cors'; // eslint-disable-next-line @seedcompany/no-restricted-imports import { HttpAdapterHost as HttpAdapterHostImpl } from '@nestjs/core'; import { - NestExpressApplication as BaseApplication, - ExpressAdapter, -} from '@nestjs/platform-express'; -import cookieParser from 'cookie-parser'; -import { ConfigService } from '../config/config.service'; -import type { CorsOptions } from './index'; -import { CookieOptions, IResponse } from './types'; - -export type NestHttpApplication = BaseApplication & { - configure: (app: BaseApplication, config: ConfigService) => Promise; + FastifyAdapter, + NestFastifyApplication, +} from '@nestjs/platform-fastify'; +import { ConfigService } from '~/core/config/config.service'; +import type { CookieOptions, CorsOptions, IResponse } from './types'; + +export type NestHttpApplication = NestFastifyApplication & { + configure: ( + app: NestFastifyApplication, + config: ConfigService, + ) => Promise; }; export class HttpAdapterHost extends HttpAdapterHostImpl {} -export class HttpAdapter extends ExpressAdapter { - async configure(app: BaseApplication, config: ConfigService) { - app.enableCors(config.cors as CorsOptions); // typecast to undo deep readonly - app.use(cookieParser()); +export class HttpAdapter extends FastifyAdapter { + async configure(app: NestFastifyApplication, config: ConfigService) { + await app.register(cors, { + // typecast to undo deep readonly + ...(config.cors as CorsOptions), + }); + await app.register(cookieParser); app.setGlobalPrefix(config.hostUrl$.value.pathname.slice(1)); @@ -31,6 +37,33 @@ export class HttpAdapter extends ExpressAdapter { value: string, options: CookieOptions, ) { - response.cookie(name, value, options); + // Avoid linter wanting us to await sending response. + // This method just returns the response instance for fluent interface. + void response.cookie(name, value, options); + } + + // @ts-expect-error we don't need to be compatible with base + reply( + response: IResponse | IResponse['raw'], + body: any, + statusCode?: number, + ) { + // Avoid linter wanting us to await sending response. + // This method just returns the response instance for fluent interface. + void super.reply(response, body, statusCode); + } + + // @ts-expect-error we don't need to be compatible with base + setHeader(response: IResponse, name: string, value: string) { + // Avoid linter wanting us to await sending response. + // This method just returns the response instance for fluent interface. + void super.setHeader(response, name, value); + } + + // @ts-expect-error we don't need to be compatible with base + redirect(response: IResponse, statusCode: number, url: string) { + // Avoid linter wanting us to await sending response. + // This method just returns the response instance for fluent interface. + void super.redirect(response, statusCode, url); } } diff --git a/src/core/http/types.ts b/src/core/http/types.ts index d2980d860b..ee96ee3e63 100644 --- a/src/core/http/types.ts +++ b/src/core/http/types.ts @@ -1,19 +1,22 @@ /* eslint-disable @typescript-eslint/method-signature-style */ // eslint-disable-next-line @seedcompany/no-restricted-imports import type { NestMiddleware } from '@nestjs/common'; -import type { Request, Response } from 'express'; +import type { + FastifyRequest as Request, + FastifyReply as Response, +} from 'fastify'; import type { Session } from '~/common'; // Exporting with I prefix to avoid ambiguity with web global types export type { Request as IRequest, Response as IResponse }; -export type HttpMiddleware = NestMiddleware; +export type HttpMiddleware = NestMiddleware; -export { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; -export { CookieOptions } from 'express'; +export { FastifyCorsOptions as CorsOptions } from '@fastify/cors'; +export { SerializeOptions as CookieOptions } from '@fastify/cookie'; -declare module 'express' { - export interface Request { +declare module 'fastify' { + export interface FastifyRequest { session?: Session; } } diff --git a/src/core/timeout.interceptor.ts b/src/core/timeout.interceptor.ts index 4699a7e1b3..f2eb8a79f0 100644 --- a/src/core/timeout.interceptor.ts +++ b/src/core/timeout.interceptor.ts @@ -25,7 +25,7 @@ export class TimeoutInterceptor implements NestInterceptor { return next.handle(); } - const timeout$ = fromEvent(response, 'timeout').pipe( + const timeout$ = fromEvent(response.raw, 'timeout').pipe( map(() => { throw new ServiceUnavailableException( 'Unable to fulfill request in a timely manner', diff --git a/yarn.lock b/yarn.lock index 604b2e4f02..20f7f2b727 100644 --- a/yarn.lock +++ b/yarn.lock @@ -130,7 +130,7 @@ __metadata: languageName: node linkType: hard -"@apollo/server-plugin-landing-page-graphql-playground@npm:empty-npm-package@*, fork-ts-checker-webpack-plugin@npm:empty-npm-package@*, subscriptions-transport-ws@npm:empty-npm-package@*, webpack@npm:empty-npm-package@*": +"@apollo/server-plugin-landing-page-graphql-playground@npm:empty-npm-package@*, express@npm:empty-npm-package@*, fork-ts-checker-webpack-plugin@npm:empty-npm-package@*, subscriptions-transport-ws@npm:empty-npm-package@*, webpack@npm:empty-npm-package@*": version: 1.0.0 resolution: "empty-npm-package@npm:1.0.0" checksum: 10c0/86dc2266288d7e3456205b8fe69ef30a1561d2365783439df0870124e5f4cf64b845ef3fea9b3312107d5a08e8d0b56813392be80a98825b4ad75d22b7e68ce0 @@ -305,6 +305,18 @@ __metadata: languageName: node linkType: hard +"@as-integrations/fastify@npm:^2.1.1": + version: 2.1.1 + resolution: "@as-integrations/fastify@npm:2.1.1" + dependencies: + fastify-plugin: "npm:^4.4.0" + peerDependencies: + "@apollo/server": ^4.0.0 + fastify: ^4.4.0 + checksum: 10c0/c2669ac8b80fc78069fb9bdbc5bda2f6c5eca00e40554005aef05d5933ff5fbe461ff4c02ed4782ee4c139a7cc35392d118e9a937fd2df177884e988f8876b84 + languageName: node + linkType: hard + "@aws-crypto/crc32@npm:5.2.0": version: 5.2.0 resolution: "@aws-crypto/crc32@npm:5.2.0" @@ -1584,6 +1596,84 @@ __metadata: languageName: node linkType: hard +"@fastify/ajv-compiler@npm:^3.5.0": + version: 3.6.0 + resolution: "@fastify/ajv-compiler@npm:3.6.0" + dependencies: + ajv: "npm:^8.11.0" + ajv-formats: "npm:^2.1.1" + fast-uri: "npm:^2.0.0" + checksum: 10c0/f0be2ca1f75833492829c52c5f5ef0ec118bdd010614e002a6366952c27297c0f6a7dafb5917a0f9c4aaa84aa32a39e520c6d837fa251748717d58590cfc8177 + languageName: node + linkType: hard + +"@fastify/cookie@npm:^9.4.0": + version: 9.4.0 + resolution: "@fastify/cookie@npm:9.4.0" + dependencies: + cookie-signature: "npm:^1.1.0" + fastify-plugin: "npm:^4.0.0" + checksum: 10c0/24d2fd4648fc28078c793dc862c53c7f3b356813e5482058b004e6c3733fd2ea432fc5e123ffb77d767b833f8c177501f2c45eda5d5d01bd6ed7040d3e3fd9b6 + languageName: node + linkType: hard + +"@fastify/cors@npm:9.0.1, @fastify/cors@npm:^9.0.1": + version: 9.0.1 + resolution: "@fastify/cors@npm:9.0.1" + dependencies: + fastify-plugin: "npm:^4.0.0" + mnemonist: "npm:0.39.6" + checksum: 10c0/4db9d3d02edbca741c8ed053819bf3b235ecd70e07c640ed91ba0fc1ee2dc8abedbbffeb79ae1a38ccbf59832e414cad90a554ee44227d0811d5a2d062940611 + languageName: node + linkType: hard + +"@fastify/error@npm:^3.2.0, @fastify/error@npm:^3.3.0, @fastify/error@npm:^3.4.0": + version: 3.4.1 + resolution: "@fastify/error@npm:3.4.1" + checksum: 10c0/1f1a0faa8c86639afb6f4bd47a9cdc1f0f20ce0d6944340fbdec8218aaba91dc9cae9ed78e24e61bceb782a867efda2b9a6320091f00dcbb896d9c8a9bdf5f96 + languageName: node + linkType: hard + +"@fastify/fast-json-stringify-compiler@npm:^4.3.0": + version: 4.3.0 + resolution: "@fastify/fast-json-stringify-compiler@npm:4.3.0" + dependencies: + fast-json-stringify: "npm:^5.7.0" + checksum: 10c0/513ef296f5ed682f7a460cfa6c5fb917a32fc540111b873c9937f944558e021492b18f30f9fd8dd20db252381a4428adbcc9f03a077f16c86d02f081eb490c7b + languageName: node + linkType: hard + +"@fastify/formbody@npm:7.4.0": + version: 7.4.0 + resolution: "@fastify/formbody@npm:7.4.0" + dependencies: + fast-querystring: "npm:^1.0.0" + fastify-plugin: "npm:^4.0.0" + checksum: 10c0/128aa7c2a4e975242bf40f18519cb7a26e768478e56754423ba99cbc05946fcb98f53d87c65eafab7ef2ed68e4b95f4d7e1d14e31355e346d9c717dd689e1fa8 + languageName: node + linkType: hard + +"@fastify/merge-json-schemas@npm:^0.1.0": + version: 0.1.1 + resolution: "@fastify/merge-json-schemas@npm:0.1.1" + dependencies: + fast-deep-equal: "npm:^3.1.3" + checksum: 10c0/7979ce12724f7b98aea06f0bb9afb20dd869f0ff6fc697517135cbb54e0a36b062cbb38ec176fe43d1fc455576839240df8f33533939ace2d64a6218a6e6b9c1 + languageName: node + linkType: hard + +"@fastify/middie@npm:8.3.3": + version: 8.3.3 + resolution: "@fastify/middie@npm:8.3.3" + dependencies: + "@fastify/error": "npm:^3.2.0" + fastify-plugin: "npm:^4.0.0" + path-to-regexp: "npm:^6.3.0" + reusify: "npm:^1.0.4" + checksum: 10c0/802a1a51d25d2651fcdd223f058530cad8c8b55a9adb764bd47e55e4656d1c4b3ce71a58fe40c8307ddef56750ce0ba93b9df14ead5d01e2a6590b677ec393ab + languageName: node + linkType: hard + "@ffprobe-installer/darwin-arm64@npm:5.0.1": version: 5.0.1 resolution: "@ffprobe-installer/darwin-arm64@npm:5.0.1" @@ -2368,19 +2458,28 @@ __metadata: languageName: node linkType: hard -"@nestjs/platform-express@npm:^10.2.7": - version: 10.3.7 - resolution: "@nestjs/platform-express@npm:10.3.7" +"@nestjs/platform-fastify@npm:^10.4.3": + version: 10.4.4 + resolution: "@nestjs/platform-fastify@npm:10.4.4" dependencies: - body-parser: "npm:1.20.2" - cors: "npm:2.8.5" - express: "npm:4.19.2" - multer: "npm:1.4.4-lts.1" - tslib: "npm:2.6.2" + "@fastify/cors": "npm:9.0.1" + "@fastify/formbody": "npm:7.4.0" + "@fastify/middie": "npm:8.3.3" + fastify: "npm:4.28.1" + light-my-request: "npm:6.0.0" + path-to-regexp: "npm:3.3.0" + tslib: "npm:2.7.0" peerDependencies: + "@fastify/static": ^6.0.0 || ^7.0.0 + "@fastify/view": ^7.0.0 || ^8.0.0 "@nestjs/common": ^10.0.0 "@nestjs/core": ^10.0.0 - checksum: 10c0/281ce6149bb9c7288dda3eabe6d3732d6874c9c47ad923a74022659ec765c1c76d1c05323d6f570faf6f976014b7bc8dabd328e4e86e0db9e4f0f1752c6df7d1 + peerDependenciesMeta: + "@fastify/static": + optional: true + "@fastify/view": + optional: true + checksum: 10c0/fed6c8b8306e7a373dbd67befbc49fe61a3511f12a62a86bec537053d8fb065aa16d3147b544b176b6c4b8591fad8cbd8ac2208df78628b3d6fdf45fcaa66c10 languageName: node linkType: hard @@ -3455,15 +3554,6 @@ __metadata: languageName: node linkType: hard -"@types/accepts@npm:*": - version: 1.3.6 - resolution: "@types/accepts@npm:1.3.6" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/ff2fc4e9384d9edbdba59850f241a1cb92a97565344d1a1193c7ca539ca1037c2931acbeb1f637109b1e5de40fa4ec3f2985e8af747469b19efc0172b69e96af - languageName: node - linkType: hard - "@types/babel__core@npm:^7.1.14": version: 7.20.3 resolution: "@types/babel__core@npm:7.20.3" @@ -3505,16 +3595,6 @@ __metadata: languageName: node linkType: hard -"@types/body-parser@npm:*": - version: 1.19.4 - resolution: "@types/body-parser@npm:1.19.4" - dependencies: - "@types/connect": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/bec2b8a97861a960ee415f7ab3c2aeb7f4d779fd364d27ddee46057897ea571735f1f854f5ee41682964315d4e3699f62427998b9c21851d773398ef535f0612 - languageName: node - linkType: hard - "@types/busboy@npm:^1.5.0": version: 1.5.2 resolution: "@types/busboy@npm:1.5.2" @@ -3540,64 +3620,10 @@ __metadata: languageName: node linkType: hard -"@types/connect@npm:*": - version: 3.4.37 - resolution: "@types/connect@npm:3.4.37" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/79fd5c32a8bb5c9548369e6da3221b6a820f3a8c5396d50f6f642712b9f4c1c881ef86bdf48994a4a279e81998563410b8843c5a10dde5521d5ef6a8ae944c3b - languageName: node - linkType: hard - -"@types/content-disposition@npm:*": - version: 0.5.7 - resolution: "@types/content-disposition@npm:0.5.7" - checksum: 10c0/6be25cb1b5a7f93f6490655901c0c9dadb5d54b853d5e052abd68f6157cfc4c8e6a4fda186a43490f1547066481234bb645d83e4fe07a11bfc4727169d4ddf3b - languageName: node - linkType: hard - -"@types/cookie-parser@npm:^1.4.5": - version: 1.4.5 - resolution: "@types/cookie-parser@npm:1.4.5" - dependencies: - "@types/express": "npm:*" - checksum: 10c0/6828de5179fbe69dab7d5ff16890dc728102941ecdcb7a295bfb0645cdd6abcc8cc17854d57f4001ed778b08d4c51961d1c5c2ea1a4643ae90d3ff22588567c4 - languageName: node - linkType: hard - -"@types/cookies@npm:*": - version: 0.7.9 - resolution: "@types/cookies@npm:0.7.9" - dependencies: - "@types/connect": "npm:*" - "@types/express": "npm:*" - "@types/keygrip": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/0f8a09cf0b420dbe41dab25c3c13bbb27f00ce186a3e154ebe6ac7d6b74d64ffb9f11d11ecca8071dc197bd591255242890974df6baa473f44d746f3173546bf - languageName: node - linkType: hard - -"@types/express-serve-static-core@npm:^4.17.30, @types/express-serve-static-core@npm:^4.17.33, @types/express-serve-static-core@npm:^4.17.39": - version: 4.17.39 - resolution: "@types/express-serve-static-core@npm:4.17.39" - dependencies: - "@types/node": "npm:*" - "@types/qs": "npm:*" - "@types/range-parser": "npm:*" - "@types/send": "npm:*" - checksum: 10c0/b23b005fddd2ba3f7142ec9713f06b5582c7712cdf99c3419d3972364903b348a103c3264d9a761d6497140e3b89bd416454684c4bdeff206b4c59b86e96428a - languageName: node - linkType: hard - -"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.20": - version: 4.17.20 - resolution: "@types/express@npm:4.17.20" - dependencies: - "@types/body-parser": "npm:*" - "@types/express-serve-static-core": "npm:^4.17.33" - "@types/qs": "npm:*" - "@types/serve-static": "npm:*" - checksum: 10c0/f73f5f92bd0a0fa4697598be3122c89522caa9e3bcb14c28b5e6d58a8e47f0301027478997153ae9ee4cf3d432576fb3fb0918ea0db521cc1204f8b759828a32 +"@types/express-serve-static-core@npm:@types/stack-trace@*, @types/express@npm:@types/stack-trace@*, @types/koa@npm:@types/stack-trace@*": + version: 0.0.33 + resolution: "@types/stack-trace@npm:0.0.33" + checksum: 10c0/cc8345f042f5de17f960652974d67aac71bf864b748f3efbd10a8c5315c0a7a8a13ab17931c239b9fe6b531a44378347509dc8a97a24dfa1247b93af3f943650 languageName: node linkType: hard @@ -3630,13 +3656,6 @@ __metadata: languageName: node linkType: hard -"@types/http-assert@npm:*": - version: 1.5.4 - resolution: "@types/http-assert@npm:1.5.4" - checksum: 10c0/86db2aa1fae12e68f481937c707887dfc1accb0b8056d363e5d3dd91cecae24de028e9475d3bf95bd3760b4b3eb86e4069bf0901d8221f494f5f279f7e90f1ab - languageName: node - linkType: hard - "@types/http-cache-semantics@npm:^4.0.4": version: 4.0.4 resolution: "@types/http-cache-semantics@npm:4.0.4" @@ -3644,13 +3663,6 @@ __metadata: languageName: node linkType: hard -"@types/http-errors@npm:*": - version: 2.0.3 - resolution: "@types/http-errors@npm:2.0.3" - checksum: 10c0/717ce3e8f49a1facb7130fed934108fa8a51ab02089a1049c782e353e0e08e79bdfaac054c2a94db14ea400302e523276387363aa820eaf0031af8ba5d2941dc - languageName: node - linkType: hard - "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.5 resolution: "@types/istanbul-lib-coverage@npm:2.0.5" @@ -3709,38 +3721,6 @@ __metadata: languageName: node linkType: hard -"@types/keygrip@npm:*": - version: 1.0.4 - resolution: "@types/keygrip@npm:1.0.4" - checksum: 10c0/12a706c2902336e0ab23fdc3d3c76230e8e557b57ef639100c4e5ee7119e81d2ba073de337f130e13e6cb576f2de1802380c516eafff2905fb0c90b5104d9666 - languageName: node - linkType: hard - -"@types/koa-compose@npm:*": - version: 3.2.7 - resolution: "@types/koa-compose@npm:3.2.7" - dependencies: - "@types/koa": "npm:*" - checksum: 10c0/9b4e86b5d5a920c19ac73c636acd0a3a8637f9eb11afde65da360d609b4dfe5a70ab8a096a1190c54ae49c8a7a65022e93d3a8ea663eee1d6c8b5379b7859c75 - languageName: node - linkType: hard - -"@types/koa@npm:*": - version: 2.13.10 - resolution: "@types/koa@npm:2.13.10" - dependencies: - "@types/accepts": "npm:*" - "@types/content-disposition": "npm:*" - "@types/cookies": "npm:*" - "@types/http-assert": "npm:*" - "@types/http-errors": "npm:*" - "@types/keygrip": "npm:*" - "@types/koa-compose": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/fc13a614093f2b48a32dea3af8c932d77edb9e571d18faabb6ce8f814222351caead1d2c8e835146d0b9e780e8ac675f9f5a56c03d3b1ad7ea85735174e5f947 - languageName: node - linkType: hard - "@types/lodash@npm:^4.14.136, @types/lodash@npm:^4.14.200": version: 4.14.200 resolution: "@types/lodash@npm:4.14.200" @@ -3762,20 +3742,6 @@ __metadata: languageName: node linkType: hard -"@types/mime@npm:*": - version: 3.0.3 - resolution: "@types/mime@npm:3.0.3" - checksum: 10c0/cef99f8cdc42af9de698027c2a20ba5df12bc9a89dcf5513e70103ebb55e00c5f5c585d02411f4b42fde0e78488342f1b1d3e3546a59a3da42e95fdc616e01eb - languageName: node - linkType: hard - -"@types/mime@npm:^1": - version: 1.3.4 - resolution: "@types/mime@npm:1.3.4" - checksum: 10c0/a0a16d26c0e70a1b133e26e7c46b70b3136b7e894396bdb7de1c642f4ac87fdbbba26bf56cf73f001312289d89de4f1c06ab745d9445850df45a5a802564c4d6 - languageName: node - linkType: hard - "@types/node-fetch@npm:^2.6.1": version: 2.6.8 resolution: "@types/node-fetch@npm:2.6.8" @@ -3837,20 +3803,6 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:*": - version: 6.9.9 - resolution: "@types/qs@npm:6.9.9" - checksum: 10c0/aede2a4181a49ae8548a1354bac3f8235cb0c5aab066b10875a3e68e88a199e220f4284e7e2bb75a3c18e5d4ff6abe1a6ce0389ef31b63952cc45e0f4d885ba0 - languageName: node - linkType: hard - -"@types/range-parser@npm:*": - version: 1.2.6 - resolution: "@types/range-parser@npm:1.2.6" - checksum: 10c0/46e7fffc54cdacc8fb0cd576f8f9a6436453f0176205d6ec55434a460c7677e78e688673426d5db5e480501b2943ba08a16ececa3a354c222093551c7217fb8f - languageName: node - linkType: hard - "@types/react@npm:^18.2.33": version: 18.2.33 resolution: "@types/react@npm:18.2.33" @@ -3883,27 +3835,6 @@ __metadata: languageName: node linkType: hard -"@types/send@npm:*": - version: 0.17.3 - resolution: "@types/send@npm:0.17.3" - dependencies: - "@types/mime": "npm:^1" - "@types/node": "npm:*" - checksum: 10c0/773a0cb55ea03eefbe9a0e6d42114e0f84968db30954a131aae9ba7e9ab984a4776915447ebdeab4412d7f11750126614b0b75e99413f75810045bdb3196554a - languageName: node - linkType: hard - -"@types/serve-static@npm:*": - version: 1.15.4 - resolution: "@types/serve-static@npm:1.15.4" - dependencies: - "@types/http-errors": "npm:*" - "@types/mime": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/061b38993bf8f2b5033f57147c8ec90e1d1a0d6f734958ceb531ba7cc31192fd272c999cdbc57ede8672787e3aa171ec142dc65a467c04078e43823e7476eb49 - languageName: node - linkType: hard - "@types/stack-trace@npm:^0.0.32": version: 0.0.32 resolution: "@types/stack-trace@npm:0.0.32" @@ -4115,13 +4046,19 @@ __metadata: languageName: node linkType: hard -"accepts@npm:~1.3.8": - version: 1.3.8 - resolution: "accepts@npm:1.3.8" +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" dependencies: - mime-types: "npm:~2.1.34" - negotiator: "npm:0.6.3" - checksum: 10c0/3a35c5f5586cfb9a21163ca47a5f77ac34fa8ceb5d17d2fa2c0d81f41cbd7f8c6fa52c77e2c039acc0f4d09e71abdc51144246900f6bef5e3c4b333f77d89362 + event-target-shim: "npm:^5.0.0" + checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5 + languageName: node + linkType: hard + +"abstract-logging@npm:^2.0.1": + version: 2.0.1 + resolution: "abstract-logging@npm:2.0.1" + checksum: 10c0/304879d9babcf6772260e5ddde632e6428e1f42f7a7a116d4689e97ad813a20e0ec2dd1e0a122f3617557f40091b9ca85735de4b48c17a2041268cb47b3f8ef1 languageName: node linkType: hard @@ -4178,7 +4115,7 @@ __metadata: languageName: node linkType: hard -"ajv-formats@npm:2.1.1": +"ajv-formats@npm:2.1.1, ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" dependencies: @@ -4192,7 +4129,21 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.12.0, ajv@npm:^8.0.0": +"ajv-formats@npm:^3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10c0/168d6bca1ea9f163b41c8147bae537e67bd963357a5488a1eaf3abe8baa8eec806d4e45f15b10767e6020679315c7e1e5e6803088dfb84efa2b4e9353b83dd0a + languageName: node + linkType: hard + +"ajv@npm:8.12.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -4216,6 +4167,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0": + version: 8.17.1 + resolution: "ajv@npm:8.17.1" + dependencies: + fast-deep-equal: "npm:^3.1.3" + fast-uri: "npm:^3.0.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + checksum: 10c0/ec3ba10a573c6b60f94639ffc53526275917a2df6810e4ab5a6b959d87459f9ef3f00d5e7865b82677cb7d21590355b34da14d1d0b9c32d75f95a187e76fff35 + languageName: node + linkType: hard + "ansi-colors@npm:4.1.3, ansi-colors@npm:^4.1.1": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -4297,13 +4260,6 @@ __metadata: languageName: node linkType: hard -"append-field@npm:^1.0.0": - version: 1.0.0 - resolution: "append-field@npm:1.0.0" - checksum: 10c0/1b5abcc227e5179936a9e4f7e2af4769fa1f00eda85bbaed907f7964b0fd1f7d61f0f332b35337f391389ff13dd5310c2546ba670f8e5a743b23ec85185c73ef - languageName: node - linkType: hard - "aproba@npm:^1.0.3 || ^2.0.0": version: 2.0.0 resolution: "aproba@npm:2.0.0" @@ -4374,13 +4330,6 @@ __metadata: languageName: node linkType: hard -"array-flatten@npm:1.1.1": - version: 1.1.1 - resolution: "array-flatten@npm:1.1.1" - checksum: 10c0/806966c8abb2f858b08f5324d9d18d7737480610f3bd5d3498aaae6eb5efdc501a884ba019c9b4a8f02ff67002058749d05548fd42fa8643f02c9c7f22198b91 - languageName: node - linkType: hard - "array-includes@npm:^3.1.6, array-includes@npm:^3.1.7": version: 3.1.7 resolution: "array-includes@npm:3.1.7" @@ -4528,6 +4477,13 @@ __metadata: languageName: node linkType: hard +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: 10c0/e329a6665512736a9bbb073e1761b4ec102f7926cce35037753146a9db9c8104f5044c1662e4a863576ce544fb8be27cd2be6bc8c1a40147d03f31eb1cfb6e8a + languageName: node + linkType: hard + "available-typed-arrays@npm:^1.0.5": version: 1.0.5 resolution: "available-typed-arrays@npm:1.0.5" @@ -4535,6 +4491,16 @@ __metadata: languageName: node linkType: hard +"avvio@npm:^8.3.0": + version: 8.4.0 + resolution: "avvio@npm:8.4.0" + dependencies: + "@fastify/error": "npm:^3.3.0" + fastq: "npm:^1.17.1" + checksum: 10c0/bea7f28e38b57755786852226f380ea087d572f8bbcfe14b59d1239551ef89cecc40229a6ac85e17af44c81a481d03280576586385e93d76bb9f2c5bc75c6067 + languageName: node + linkType: hard + "aws-xray-sdk-core@npm:^3.5.3": version: 3.5.3 resolution: "aws-xray-sdk-core@npm:3.5.3" @@ -4673,7 +4639,7 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.2, body-parser@npm:^1.20.0": +"body-parser@npm:^1.20.0": version: 1.20.2 resolution: "body-parser@npm:1.20.2" dependencies: @@ -4810,7 +4776,7 @@ __metadata: languageName: node linkType: hard -"busboy@npm:^1.0.0, busboy@npm:^1.6.0": +"busboy@npm:^1.6.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" dependencies: @@ -5374,18 +5340,6 @@ __metadata: languageName: node linkType: hard -"concat-stream@npm:^1.5.2": - version: 1.6.2 - resolution: "concat-stream@npm:1.6.2" - dependencies: - buffer-from: "npm:^1.0.0" - inherits: "npm:^2.0.3" - readable-stream: "npm:^2.2.2" - typedarray: "npm:^0.0.6" - checksum: 10c0/2e9864e18282946dabbccb212c5c7cec0702745e3671679eb8291812ca7fd12023f7d8cb36493942a62f770ac96a7f90009dc5c82ad69893438371720fa92617 - languageName: node - linkType: hard - "config-chain@npm:^1.1.13": version: 1.1.13 resolution: "config-chain@npm:1.1.13" @@ -5417,16 +5371,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.4": - version: 0.5.4 - resolution: "content-disposition@npm:0.5.4" - dependencies: - safe-buffer: "npm:5.2.1" - checksum: 10c0/bac0316ebfeacb8f381b38285dc691c9939bf0a78b0b7c2d5758acadad242d04783cee5337ba7d12a565a19075af1b3c11c728e1e4946de73c6ff7ce45f3f1bb - languageName: node - linkType: hard - -"content-type@npm:~1.0.4, content-type@npm:~1.0.5": +"content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af @@ -5440,31 +5385,14 @@ __metadata: languageName: node linkType: hard -"cookie-parser@npm:^1.4.6": - version: 1.4.6 - resolution: "cookie-parser@npm:1.4.6" - dependencies: - cookie: "npm:0.4.1" - cookie-signature: "npm:1.0.6" - checksum: 10c0/9c2ade5459290802cd472a2d2a6e46fbd7de3e8514e02bfed5edfde892d77733c7f89d9d2015f752a9087680429b416972d7aba748bf6824e21eb680c8556383 - languageName: node - linkType: hard - -"cookie-signature@npm:1.0.6": - version: 1.0.6 - resolution: "cookie-signature@npm:1.0.6" - checksum: 10c0/b36fd0d4e3fef8456915fcf7742e58fbfcc12a17a018e0eb9501c9d5ef6893b596466f03b0564b81af29ff2538fd0aa4b9d54fe5ccbfb4c90ea50ad29fe2d221 - languageName: node - linkType: hard - -"cookie@npm:0.4.1": - version: 0.4.1 - resolution: "cookie@npm:0.4.1" - checksum: 10c0/4d7bc798df3d0f34035977949cd6b7d05bbab47d7dcb868667f460b578a550cd20dec923832b8a3a107ef35aba091a3975e14f79efacf6e39282dc0fed6db4a1 +"cookie-signature@npm:^1.1.0": + version: 1.2.1 + resolution: "cookie-signature@npm:1.2.1" + checksum: 10c0/1f71acf64931d7e7684aa228a0dad70162f6993b65b2957e076833cbd6f9a2f507b8d731b15e3895dce0e7ba4c63551f4686d1a3120199fe28060c41fd493a73 languageName: node linkType: hard -"cookie@npm:0.6.0": +"cookie@npm:^0.6.0": version: 0.6.0 resolution: "cookie@npm:0.6.0" checksum: 10c0/f2318b31af7a31b4ddb4a678d024514df5e705f9be5909a192d7f116cfb6d45cbacf96a473fa733faa95050e7cff26e7832bb3ef94751592f1387b71c8956686 @@ -5477,10 +5405,13 @@ __metadata: dependencies: "@apollo/server": "npm:^4.9.5" "@apollo/subgraph": "npm:^2.5.6" + "@as-integrations/fastify": "npm:^2.1.1" "@aws-sdk/client-s3": "npm:^3.440.0" "@aws-sdk/s3-request-presigner": "npm:^3.440.0" "@edgedb/generate": "github:CarsonF/edgedb-js#workspace=@edgedb/generate&head=temp-host" "@faker-js/faker": "npm:^8.2.0" + "@fastify/cookie": "npm:^9.4.0" + "@fastify/cors": "npm:^9.0.1" "@ffprobe-installer/ffprobe": "npm:^2.1.2" "@golevelup/nestjs-discovery": "npm:^4.0.0" "@leeoniya/ufuzzy": "npm:^1.0.11" @@ -5489,7 +5420,7 @@ __metadata: "@nestjs/common": "npm:^10.2.7" "@nestjs/core": "npm:^10.2.7" "@nestjs/graphql": "npm:^12.0.9" - "@nestjs/platform-express": "npm:^10.2.7" + "@nestjs/platform-fastify": "npm:^10.4.3" "@nestjs/schematics": "npm:^10.0.3" "@nestjs/testing": "npm:^10.2.7" "@patarapolw/prettyprint": "npm:^1.0.3" @@ -5502,9 +5433,6 @@ __metadata: "@seedcompany/scripture": "npm:^0.3.0" "@tsconfig/strictest": "npm:^2.0.2" "@types/common-tags": "npm:^1.8.3" - "@types/cookie-parser": "npm:^1.4.5" - "@types/express": "npm:^4.17.20" - "@types/express-serve-static-core": "npm:^4.17.39" "@types/ffprobe": "npm:^1.1.7" "@types/graphql-upload": "npm:^16.0.4" "@types/jest": "npm:^29.5.7" @@ -5527,7 +5455,6 @@ __metadata: cli-table3: "npm:^0.6.3" clipanion: "npm:^4.0.0-rc.3" common-tags: "npm:^1.8.2" - cookie-parser: "npm:^1.4.6" cypher-query-builder: "patch:cypher-query-builder@npm%3A6.0.4#~/.yarn/patches/cypher-query-builder-npm-6.0.4-e8707a5e8e.patch" debugger-is-attached: "npm:^1.2.0" dotenv: "npm:^16.3.1" @@ -5537,10 +5464,10 @@ __metadata: eslint-plugin-no-only-tests: "npm:^3.1.0" eslint-plugin-typescript-sort-keys: "npm:^2.3.0" execa: "npm:^8.0.1" - express: "npm:^4.18.2" extensionless: "npm:^1.7.0" fast-safe-stringify: "npm:^2.1.1" fastest-levenshtein: "npm:^1.0.16" + fastify: "npm:^4.28.1" file-type: "npm:^18.6.0" glob: "npm:^10.3.10" got: "npm:^14.3.0" @@ -5609,14 +5536,14 @@ __metadata: languageName: node linkType: hard -"core-util-is@npm:^1.0.3, core-util-is@npm:~1.0.0": +"core-util-is@npm:^1.0.3": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" checksum: 10c0/90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 languageName: node linkType: hard -"cors@npm:2.8.5, cors@npm:^2.8.5": +"cors@npm:^2.8.5": version: 2.8.5 resolution: "cors@npm:2.8.5" dependencies: @@ -6208,13 +6135,6 @@ __metadata: languageName: node linkType: hard -"encodeurl@npm:~1.0.2": - version: 1.0.2 - resolution: "encodeurl@npm:1.0.2" - checksum: 10c0/f6c2387379a9e7c1156c1c3d4f9cb7bb11cf16dd4c1682e1f6746512564b053df5781029b6061296832b59fb22f459dbe250386d217c2f6e203601abb2ee0bec - languageName: node - linkType: hard - "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -6408,13 +6328,6 @@ __metadata: languageName: node linkType: hard -"escape-html@npm:~1.0.3": - version: 1.0.3 - resolution: "escape-html@npm:1.0.3" - checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 - languageName: node - linkType: hard - "escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -6739,10 +6652,10 @@ __metadata: languageName: node linkType: hard -"etag@npm:~1.8.1": - version: 1.8.1 - resolution: "etag@npm:1.8.1" - checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10c0/0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b languageName: node linkType: hard @@ -6753,6 +6666,13 @@ __metadata: languageName: node linkType: hard +"events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 + languageName: node + linkType: hard + "execa@npm:7.2.0": version: 7.2.0 resolution: "execa@npm:7.2.0" @@ -6848,45 +6768,6 @@ __metadata: languageName: node linkType: hard -"express@npm:4.19.2, express@npm:^4.17.1, express@npm:^4.18.2": - version: 4.19.2 - resolution: "express@npm:4.19.2" - dependencies: - accepts: "npm:~1.3.8" - array-flatten: "npm:1.1.1" - body-parser: "npm:1.20.2" - content-disposition: "npm:0.5.4" - content-type: "npm:~1.0.4" - cookie: "npm:0.6.0" - cookie-signature: "npm:1.0.6" - debug: "npm:2.6.9" - depd: "npm:2.0.0" - encodeurl: "npm:~1.0.2" - escape-html: "npm:~1.0.3" - etag: "npm:~1.8.1" - finalhandler: "npm:1.2.0" - fresh: "npm:0.5.2" - http-errors: "npm:2.0.0" - merge-descriptors: "npm:1.0.1" - methods: "npm:~1.1.2" - on-finished: "npm:2.4.1" - parseurl: "npm:~1.3.3" - path-to-regexp: "npm:0.1.7" - proxy-addr: "npm:~2.0.7" - qs: "npm:6.11.0" - range-parser: "npm:~1.2.1" - safe-buffer: "npm:5.2.1" - send: "npm:0.18.0" - serve-static: "npm:1.15.0" - setprototypeof: "npm:1.2.0" - statuses: "npm:2.0.1" - type-is: "npm:~1.6.18" - utils-merge: "npm:1.0.1" - vary: "npm:~1.1.2" - checksum: 10c0/e82e2662ea9971c1407aea9fc3c16d6b963e55e3830cd0ef5e00b533feda8b770af4e3be630488ef8a752d7c75c4fcefb15892868eeaafe7353cb9e3e269fdcb - languageName: node - linkType: hard - "extensionless@npm:^1.7.0": version: 1.7.0 resolution: "extensionless@npm:1.7.0" @@ -6905,6 +6786,20 @@ __metadata: languageName: node linkType: hard +"fast-content-type-parse@npm:^1.1.0": + version: 1.1.0 + resolution: "fast-content-type-parse@npm:1.1.0" + checksum: 10c0/882bf990fa5d64be1825ce183818db43900ece0d7ef184cb9409bae8ed1001acbe536a657b1496382cb3e308e71ab39cc399bbdae70cba1745eecaeca4e55384 + languageName: node + linkType: hard + +"fast-decode-uri-component@npm:^1.0.1": + version: 1.0.1 + resolution: "fast-decode-uri-component@npm:1.0.1" + checksum: 10c0/039d50c2e99d64f999c3f2126c23fbf75a04a4117e218a149ca0b1d2aeb8c834b7b19d643b9d35d4eabce357189a6a94085f78cf48869e6e26cc59b036284bc3 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -6939,6 +6834,21 @@ __metadata: languageName: node linkType: hard +"fast-json-stringify@npm:^5.7.0, fast-json-stringify@npm:^5.8.0": + version: 5.16.1 + resolution: "fast-json-stringify@npm:5.16.1" + dependencies: + "@fastify/merge-json-schemas": "npm:^0.1.0" + ajv: "npm:^8.10.0" + ajv-formats: "npm:^3.0.1" + fast-deep-equal: "npm:^3.1.3" + fast-uri: "npm:^2.1.0" + json-schema-ref-resolver: "npm:^1.0.1" + rfdc: "npm:^1.2.0" + checksum: 10c0/bbf955d9912fb827dff0e097fdbff3c11aec540ea8019a19593a16224cac70d49d0cebd98e412843fc72259184f73a78a45e63040d3c44349f4735a492f2f1a4 + languageName: node + linkType: hard + "fast-levenshtein@npm:^2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" @@ -6946,6 +6856,22 @@ __metadata: languageName: node linkType: hard +"fast-querystring@npm:^1.0.0": + version: 1.1.2 + resolution: "fast-querystring@npm:1.1.2" + dependencies: + fast-decode-uri-component: "npm:^1.0.1" + checksum: 10c0/e8223273a9b199722f760f5a047a77ad049a14bd444b821502cb8218f5925e3a5fffb56b64389bca73ab2ac6f1aa7aebbe4e203e5f6e53ff5978de97c0fde4e3 + languageName: node + linkType: hard + +"fast-redact@npm:^3.1.1": + version: 3.5.0 + resolution: "fast-redact@npm:3.5.0" + checksum: 10c0/7e2ce4aad6e7535e0775bf12bd3e4f2e53d8051d8b630e0fa9e67f68cb0b0e6070d2f7a94b1d0522ef07e32f7c7cda5755e2b677a6538f1e9070ca053c42343a + languageName: node + linkType: hard + "fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" @@ -6953,6 +6879,20 @@ __metadata: languageName: node linkType: hard +"fast-uri@npm:^2.0.0, fast-uri@npm:^2.1.0": + version: 2.4.0 + resolution: "fast-uri@npm:2.4.0" + checksum: 10c0/300453cfe2f7d5ec16be0f2c8dc5b280edbaca59440b2deb4ab56ac0f584637179e9ee7539d0b70ef0fce9608245ebfa75307c84fa4829b1065c3b7ef7dcf706 + languageName: node + linkType: hard + +"fast-uri@npm:^3.0.1": + version: 3.0.2 + resolution: "fast-uri@npm:3.0.2" + checksum: 10c0/8cdd3da7b4022a037d348d587d55caff74b7e4f862bbdd2cc35c1e6e3f97d0aedb567894d44c57ee8798d3192cceb97dcf41dbdabfa07dd2842a0474a6c6eeef + languageName: node + linkType: hard + "fast-xml-parser@npm:4.4.1": version: 4.4.1 resolution: "fast-xml-parser@npm:4.4.1" @@ -6971,12 +6911,43 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.6.0": - version: 1.15.0 - resolution: "fastq@npm:1.15.0" +"fastify-plugin@npm:^4.0.0, fastify-plugin@npm:^4.4.0": + version: 4.5.1 + resolution: "fastify-plugin@npm:4.5.1" + checksum: 10c0/f58f79cd9d3c88fd7f79a3270276c6339fc57bbe72ef14d20b73779193c404e317ac18e8eae2c5071b3909ebee45d7eb6871da4e65464ac64ed0d9746b4e9b9f + languageName: node + linkType: hard + +"fastify@npm:4.28.1, fastify@npm:^4.28.1": + version: 4.28.1 + resolution: "fastify@npm:4.28.1" + dependencies: + "@fastify/ajv-compiler": "npm:^3.5.0" + "@fastify/error": "npm:^3.4.0" + "@fastify/fast-json-stringify-compiler": "npm:^4.3.0" + abstract-logging: "npm:^2.0.1" + avvio: "npm:^8.3.0" + fast-content-type-parse: "npm:^1.1.0" + fast-json-stringify: "npm:^5.8.0" + find-my-way: "npm:^8.0.0" + light-my-request: "npm:^5.11.0" + pino: "npm:^9.0.0" + process-warning: "npm:^3.0.0" + proxy-addr: "npm:^2.0.7" + rfdc: "npm:^1.3.0" + secure-json-parse: "npm:^2.7.0" + semver: "npm:^7.5.4" + toad-cache: "npm:^3.3.0" + checksum: 10c0/9c212e9a72c42a27ebc9b0bc7fda8f94ff208250158093374942b0e156a3f55fa848c926921f99bdf7f38f6f8103ac28ecc72cc507f33893cd121ce4f3eda069 + languageName: node + linkType: hard + +"fastq@npm:^1.17.1, fastq@npm:^1.6.0": + version: 1.17.1 + resolution: "fastq@npm:1.17.1" dependencies: reusify: "npm:^1.0.4" - checksum: 10c0/5ce4f83afa5f88c9379e67906b4d31bc7694a30826d6cc8d0f0473c966929017fda65c2174b0ec89f064ede6ace6c67f8a4fe04cef42119b6a55b0d465554c24 + checksum: 10c0/1095f16cea45fb3beff558bb3afa74ca7a9250f5a670b65db7ed585f92b4b48381445cd328b3d87323da81e43232b5d5978a8201bde84e0cd514310f1ea6da34 languageName: node linkType: hard @@ -7034,18 +7005,14 @@ __metadata: languageName: node linkType: hard -"finalhandler@npm:1.2.0": - version: 1.2.0 - resolution: "finalhandler@npm:1.2.0" +"find-my-way@npm:^8.0.0": + version: 8.2.2 + resolution: "find-my-way@npm:8.2.2" dependencies: - debug: "npm:2.6.9" - encodeurl: "npm:~1.0.2" - escape-html: "npm:~1.0.3" - on-finished: "npm:2.4.1" - parseurl: "npm:~1.3.3" - statuses: "npm:2.0.1" - unpipe: "npm:~1.0.0" - checksum: 10c0/64b7e5ff2ad1fcb14931cd012651631b721ce657da24aedb5650ddde9378bf8e95daa451da43398123f5de161a81e79ff5affe4f9f2a6d2df4a813d6d3e254b7 + fast-deep-equal: "npm:^3.1.3" + fast-querystring: "npm:^1.0.0" + safe-regex2: "npm:^3.1.0" + checksum: 10c0/ce462b2033e08a82fa79b837e4ef9e637d5f3e6763564631ad835b4e50b22e2123c0bf27c4fe6b02bc4006cd7949c0351d2b6b6f32248e839b10bdcbd3a3269f languageName: node linkType: hard @@ -7157,13 +7124,6 @@ __metadata: languageName: node linkType: hard -"fresh@npm:0.5.2": - version: 0.5.2 - resolution: "fresh@npm:0.5.2" - checksum: 10c0/c6d27f3ed86cc5b601404822f31c900dd165ba63fff8152a3ef714e2012e7535027063bc67ded4cb5b3a49fa596495d46cacd9f47d6328459cf570f08b7d9e5a - languageName: node - linkType: hard - "fs-capacitor@npm:^8.0.0": version: 8.0.0 resolution: "fs-capacitor@npm:8.0.0" @@ -8401,13 +8361,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:~1.0.0": - version: 1.0.0 - resolution: "isarray@npm:1.0.0" - checksum: 10c0/18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -9059,6 +9012,15 @@ __metadata: languageName: node linkType: hard +"json-schema-ref-resolver@npm:^1.0.1": + version: 1.0.1 + resolution: "json-schema-ref-resolver@npm:1.0.1" + dependencies: + fast-deep-equal: "npm:^3.1.3" + checksum: 10c0/aa89d88108c0109ae35b913c89c132fb50c00f3b99fc8a8309b524b9e3a6a77414f19a6a35a1253871462984cbabc74279ebbd9bf103c6629fb7b37c9fb59bcf + languageName: node + linkType: hard + "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -9264,6 +9226,28 @@ __metadata: languageName: node linkType: hard +"light-my-request@npm:6.0.0": + version: 6.0.0 + resolution: "light-my-request@npm:6.0.0" + dependencies: + cookie: "npm:^0.6.0" + process-warning: "npm:^4.0.0" + set-cookie-parser: "npm:^2.6.0" + checksum: 10c0/521fde58b0e52d05cc38b23a7ef5b9b7ce35b9a943a4e5a27eb219de2ac08ff566bfaa194b8db3bf99cf0800adc5fe5ed9c0c604c18fce8a0d4554e2237ee89b + languageName: node + linkType: hard + +"light-my-request@npm:^5.11.0": + version: 5.13.0 + resolution: "light-my-request@npm:5.13.0" + dependencies: + cookie: "npm:^0.6.0" + process-warning: "npm:^3.0.0" + set-cookie-parser: "npm:^2.4.1" + checksum: 10c0/460117f30e09c2eec3a62e6ba4264111a28b881fdd0ea79493ed889ebf69a56482d603f0685a0e2930b5ec53205d28c46f3cdf13d7888914852eb7c4dac83285 + languageName: node + linkType: hard + "lilconfig@npm:2.1.0": version: 2.1.0 resolution: "lilconfig@npm:2.1.0" @@ -9652,13 +9636,6 @@ __metadata: languageName: node linkType: hard -"merge-descriptors@npm:1.0.1": - version: 1.0.1 - resolution: "merge-descriptors@npm:1.0.1" - checksum: 10c0/b67d07bd44cfc45cebdec349bb6e1f7b077ee2fd5beb15d1f7af073849208cb6f144fe403e29a36571baf3f4e86469ac39acf13c318381e958e186b2766f54ec - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -9673,13 +9650,6 @@ __metadata: languageName: node linkType: hard -"methods@npm:~1.1.2": - version: 1.1.2 - resolution: "methods@npm:1.1.2" - checksum: 10c0/bdf7cc72ff0a33e3eede03708c08983c4d7a173f91348b4b1e4f47d4cdbf734433ad971e7d1e8c77247d9e5cd8adb81ea4c67b0a2db526b758b2233d7814b8b2 - languageName: node - linkType: hard - "micromatch@npm:4.0.5, micromatch@npm:^4.0.4": version: 4.0.5 resolution: "micromatch@npm:4.0.5" @@ -9697,7 +9667,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:~2.1.24": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -9706,15 +9676,6 @@ __metadata: languageName: node linkType: hard -"mime@npm:1.6.0": - version: 1.6.0 - resolution: "mime@npm:1.6.0" - bin: - mime: cli.js - checksum: 10c0/b92cd0adc44888c7135a185bfd0dddc42c32606401c72896a842ae15da71eb88858f17669af41e498b463cd7eb998f7b48939a25b08374c7924a9c8a6f8a81b0 - languageName: node - linkType: hard - "mime@npm:^2.4.6": version: 2.6.0 resolution: "mime@npm:2.6.0" @@ -10316,17 +10277,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.4": - version: 0.5.6 - resolution: "mkdirp@npm:0.5.6" - dependencies: - minimist: "npm:^1.2.6" - bin: - mkdirp: bin/cmd.js - checksum: 10c0/e2e2be789218807b58abced04e7b49851d9e46e88a2f9539242cc8a92c9b5c3a0b9bab360bd3014e02a140fc4fbc58e31176c408b493f8a2a6f4986bd7527b01 - languageName: node - linkType: hard - "mkdirp@npm:^1.0.3": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -10345,6 +10295,15 @@ __metadata: languageName: node linkType: hard +"mnemonist@npm:0.39.6": + version: 0.39.6 + resolution: "mnemonist@npm:0.39.6" + dependencies: + obliterator: "npm:^2.0.1" + checksum: 10c0/a538945ea547976136ee6e16f224c0a50983143619941f6c4d2c82159e36eb6f8ee93d69d3a1267038fc5b16f88e2d43390023de10dfb145fa15c5e2befa1cdf + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -10359,28 +10318,13 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.1.1": +"ms@npm:^2.1.1": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 languageName: node linkType: hard -"multer@npm:1.4.4-lts.1": - version: 1.4.4-lts.1 - resolution: "multer@npm:1.4.4-lts.1" - dependencies: - append-field: "npm:^1.0.0" - busboy: "npm:^1.0.0" - concat-stream: "npm:^1.5.2" - mkdirp: "npm:^0.5.4" - object-assign: "npm:^4.1.1" - type-is: "npm:^1.6.4" - xtend: "npm:^4.0.0" - checksum: 10c0/63277d3483869f424274ef8ce6ab7ff4ce9d2c1cc69e707fc8b5d9b2b348ae6f742809e0b357a591dea885d147594bcd06528d3d6bbe32046115d4a7e126b954 - languageName: node - linkType: hard - "mute-stream@npm:0.0.8": version: 0.0.8 resolution: "mute-stream@npm:0.0.8" @@ -10411,7 +10355,7 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": +"negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 @@ -10735,6 +10679,20 @@ __metadata: languageName: node linkType: hard +"obliterator@npm:^2.0.1": + version: 2.0.4 + resolution: "obliterator@npm:2.0.4" + checksum: 10c0/ff2c10d4de7d62cd1d588b4d18dfc42f246c9e3a259f60d5716f7f88e5b3a3f79856b3207db96ec9a836a01d0958a21c15afa62a3f4e73a1e0b75f2c2f6bab40 + languageName: node + linkType: hard + +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10c0/faea2e1c9d696ecee919026c32be8d6a633a7ac1240b3b87e944a380e8a11dc9c95c4a1f8fb0568de7ab8db3823e790f12bda45296b1d111e341aad3922a0570 + languageName: node + linkType: hard + "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -10995,13 +10953,6 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:~1.3.3": - version: 1.3.3 - resolution: "parseurl@npm:1.3.3" - checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 - languageName: node - linkType: hard - "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -11061,13 +11012,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:0.1.7": - version: 0.1.7 - resolution: "path-to-regexp@npm:0.1.7" - checksum: 10c0/50a1ddb1af41a9e68bd67ca8e331a705899d16fb720a1ea3a41e310480948387daf603abb14d7b0826c58f10146d49050a1291ba6a82b78a382d1c02c0b8f905 - languageName: node - linkType: hard - "path-to-regexp@npm:3.2.0": version: 3.2.0 resolution: "path-to-regexp@npm:3.2.0" @@ -11075,6 +11019,20 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:3.3.0": + version: 3.3.0 + resolution: "path-to-regexp@npm:3.3.0" + checksum: 10c0/ffa0ebe7088d38d435a8d08b0fe6e8c93ceb2a81a65d4dd1d9a538f52e09d5e3474ed5f553cb3b180d894b0caa10698a68737ab599fd1e56b4663d1a64c9f77b + languageName: node + linkType: hard + +"path-to-regexp@npm:^6.3.0": + version: 6.3.0 + resolution: "path-to-regexp@npm:6.3.0" + checksum: 10c0/73b67f4638b41cde56254e6354e46ae3a2ebc08279583f6af3d96fe4664fc75788f74ed0d18ca44fa4a98491b69434f9eee73b97bb5314bd1b5adb700f5c18d6 + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -11119,6 +11077,44 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^1.2.0": + version: 1.2.0 + resolution: "pino-abstract-transport@npm:1.2.0" + dependencies: + readable-stream: "npm:^4.0.0" + split2: "npm:^4.0.0" + checksum: 10c0/b4ab59529b7a91f488440147fc58ee0827a6c1c5ca3627292339354b1381072c1a6bfa9b46d03ad27872589e8477ecf74da12cf286e1e6b665ac64a3b806bf07 + languageName: node + linkType: hard + +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10c0/73e694d542e8de94445a03a98396cf383306de41fd75ecc07085d57ed7a57896198508a0dec6eefad8d701044af21eb27253ccc352586a03cf0d4a0bd25b4133 + languageName: node + linkType: hard + +"pino@npm:^9.0.0": + version: 9.4.0 + resolution: "pino@npm:9.4.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + fast-redact: "npm:^3.1.1" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^1.2.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^4.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10c0/12a3d74968964d92b18ca7d6095a3c5b86478dc22264a37486d64e102085ed08820fcbe75e640acc3542fdf2937a34e5050b624f98e6ac62dd10f5e1328058a2 + languageName: node + linkType: hard + "pirates@npm:^4.0.4": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -11239,10 +11235,24 @@ __metadata: languageName: node linkType: hard -"process-nextick-args@npm:~2.0.0": - version: 2.0.1 - resolution: "process-nextick-args@npm:2.0.1" - checksum: 10c0/bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 +"process-warning@npm:^3.0.0": + version: 3.0.0 + resolution: "process-warning@npm:3.0.0" + checksum: 10c0/60f3c8ddee586f0706c1e6cb5aa9c86df05774b9330d792d7c8851cf0031afd759d665404d07037e0b4901b55c44a423f07bdc465c63de07d8d23196bb403622 + languageName: node + linkType: hard + +"process-warning@npm:^4.0.0": + version: 4.0.0 + resolution: "process-warning@npm:4.0.0" + checksum: 10c0/5312a72b69d37a1b82ad03f3dfa0090dab3804a8fd995d06c28e3c002852bd82f5584217d9f4a3f197892bb2afc22d57e2c662c7e906b5abb48c0380c7b0880d + languageName: node + linkType: hard + +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 languageName: node linkType: hard @@ -11284,7 +11294,7 @@ __metadata: languageName: node linkType: hard -"proxy-addr@npm:~2.0.7": +"proxy-addr@npm:^2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" dependencies: @@ -11352,6 +11362,13 @@ __metadata: languageName: node linkType: hard +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10c0/fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4 + languageName: node + linkType: hard + "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -11359,13 +11376,6 @@ __metadata: languageName: node linkType: hard -"range-parser@npm:~1.2.1": - version: 1.2.1 - resolution: "range-parser@npm:1.2.1" - checksum: 10c0/96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0 - languageName: node - linkType: hard - "raw-body@npm:2.5.2": version: 2.5.2 resolution: "raw-body@npm:2.5.2" @@ -11413,21 +11423,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.2.2": - version: 2.3.8 - resolution: "readable-stream@npm:2.3.8" - dependencies: - core-util-is: "npm:~1.0.0" - inherits: "npm:~2.0.3" - isarray: "npm:~1.0.0" - process-nextick-args: "npm:~2.0.0" - safe-buffer: "npm:~5.1.1" - string_decoder: "npm:~1.1.1" - util-deprecate: "npm:~1.0.1" - checksum: 10c0/7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa - languageName: node - linkType: hard - "readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" @@ -11439,6 +11434,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.0.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10c0/a2c80e0e53aabd91d7df0330929e32d0a73219f9477dbbb18472f6fdd6a11a699fc5d172a1beff98d50eae4f1496c950ffa85b7cc2c4c196963f289a5f39275d + languageName: node + linkType: hard + "readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" @@ -11457,6 +11465,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10c0/23eea5623642f0477412ef8b91acd3969015a1501ed34992ada0e3af521d3c865bb2fe4cdbfec5fe4b505f6d1ef6a03e5c3652520837a8c3b53decff7e74b6a0 + languageName: node + linkType: hard + "rechoir@npm:^0.6.2": version: 0.6.2 resolution: "rechoir@npm:0.6.2" @@ -11667,6 +11682,13 @@ __metadata: languageName: node linkType: hard +"ret@npm:~0.4.0": + version: 0.4.3 + resolution: "ret@npm:0.4.3" + checksum: 10c0/93e4e81cf393ebbafa1a26816e0b22ad0e2539c10e267d46ce8754c3f385b7aa839772ee1f83fdd2487b43d1081f29af41a19160e85456311f6f1778e14ba66b + languageName: node + linkType: hard + "retry@npm:0.13.1, retry@npm:^0.13.1": version: 0.13.1 resolution: "retry@npm:0.13.1" @@ -11688,10 +11710,10 @@ __metadata: languageName: node linkType: hard -"rfdc@npm:^1.3.0": - version: 1.3.0 - resolution: "rfdc@npm:1.3.0" - checksum: 10c0/a17fd7b81f42c7ae4cb932abd7b2f677b04cc462a03619fb46945ae1ccae17c3bc87c020ffdde1751cbfa8549860a2883486fdcabc9b9de3f3108af32b69a667 +"rfdc@npm:^1.2.0, rfdc@npm:^1.3.0": + version: 1.4.1 + resolution: "rfdc@npm:1.4.1" + checksum: 10c0/4614e4292356cafade0b6031527eea9bc90f2372a22c012313be1dcc69a3b90c7338158b414539be863fa95bfcb2ddcd0587be696841af4e6679d85e62c060c7 languageName: node linkType: hard @@ -11781,20 +11803,13 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 languageName: node linkType: hard -"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": - version: 5.1.2 - resolution: "safe-buffer@npm:5.1.2" - checksum: 10c0/780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 - languageName: node - linkType: hard - "safe-regex-test@npm:^1.0.0": version: 1.0.0 resolution: "safe-regex-test@npm:1.0.0" @@ -11806,6 +11821,15 @@ __metadata: languageName: node linkType: hard +"safe-regex2@npm:^3.1.0": + version: 3.1.0 + resolution: "safe-regex2@npm:3.1.0" + dependencies: + ret: "npm:~0.4.0" + checksum: 10c0/5e5e7f9f116ddfd324b1fdc65ad4470937eebc8883d34669ce8c5afbda64f1954e5e4c2e754ef6281e5f6762e0b8c4e20fb9eec4d47355526f8cc1f6a9764624 + languageName: node + linkType: hard + "safe-stable-stringify@npm:^2.3.1": version: 2.4.3 resolution: "safe-stable-stringify@npm:2.4.3" @@ -11838,6 +11862,13 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.7.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 + languageName: node + linkType: hard + "selderee@npm:^0.11.0": version: 0.11.0 resolution: "selderee@npm:0.11.0" @@ -11888,39 +11919,6 @@ __metadata: languageName: node linkType: hard -"send@npm:0.18.0": - version: 0.18.0 - resolution: "send@npm:0.18.0" - dependencies: - debug: "npm:2.6.9" - depd: "npm:2.0.0" - destroy: "npm:1.2.0" - encodeurl: "npm:~1.0.2" - escape-html: "npm:~1.0.3" - etag: "npm:~1.8.1" - fresh: "npm:0.5.2" - http-errors: "npm:2.0.0" - mime: "npm:1.6.0" - ms: "npm:2.1.3" - on-finished: "npm:2.4.1" - range-parser: "npm:~1.2.1" - statuses: "npm:2.0.1" - checksum: 10c0/0eb134d6a51fc13bbcb976a1f4214ea1e33f242fae046efc311e80aff66c7a43603e26a79d9d06670283a13000e51be6e0a2cb80ff0942eaf9f1cd30b7ae736a - languageName: node - linkType: hard - -"serve-static@npm:1.15.0": - version: 1.15.0 - resolution: "serve-static@npm:1.15.0" - dependencies: - encodeurl: "npm:~1.0.2" - escape-html: "npm:~1.0.3" - parseurl: "npm:~1.3.3" - send: "npm:0.18.0" - checksum: 10c0/fa9f0e21a540a28f301258dfe1e57bb4f81cd460d28f0e973860477dd4acef946a1f41748b5bd41c73b621bea2029569c935faa38578fd34cd42a9b4947088ba - languageName: node - linkType: hard - "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -11928,6 +11926,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.4.1, set-cookie-parser@npm:^2.6.0": + version: 2.7.0 + resolution: "set-cookie-parser@npm:2.7.0" + checksum: 10c0/5ccb2d0389bda27631d57e44644319f0b77200e7c8bd1515824eb83dbd2d351864a29581f7e7f977a5aeb83c3ec9976e69b706a80ac654152fd26353011ffef4 + languageName: node + linkType: hard + "set-function-length@npm:^1.1.1": version: 1.1.1 resolution: "set-function-length@npm:1.1.1" @@ -12106,6 +12111,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.1.0 + resolution: "sonic-boom@npm:4.1.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10c0/4c9e082db296fbfb02e22a1a9b8de8b82f5965697dda3fe7feadc4759bf25d1de0094e3c35f16e015bfdc00fad7b8cf15bef5b0144501a2a5c5b86efb5684096 + languageName: node + linkType: hard + "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13" @@ -12140,6 +12154,13 @@ __metadata: languageName: node linkType: hard +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 + languageName: node + linkType: hard + "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" @@ -12307,15 +12328,6 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:~1.1.1": - version: 1.1.1 - resolution: "string_decoder@npm:1.1.1" - dependencies: - safe-buffer: "npm:~5.1.0" - checksum: 10c0/b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e - languageName: node - linkType: hard - "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -12492,6 +12504,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10c0/c36118379940b77a6ef3e6f4d5dd31e97b8210c3f7b9a54eb8fe6358ab173f6d0acfaf69b9c3db024b948c0c5fd2a7df93e2e49151af02076b35ada3205ec9a6 + languageName: node + linkType: hard + "through@npm:^2.3.6": version: 2.3.8 resolution: "through@npm:2.3.8" @@ -12538,6 +12559,13 @@ __metadata: languageName: node linkType: hard +"toad-cache@npm:^3.3.0": + version: 3.7.0 + resolution: "toad-cache@npm:3.7.0" + checksum: 10c0/7dae2782ee20b22c9798bb8b71dec7ec6a0091021d2ea9dd6e8afccab6b65b358fdba49a02209fac574499702e2c000660721516c87c2538d1b2c0ba03e8c0c3 + languageName: node + linkType: hard + "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -12747,6 +12775,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.7.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.2": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 10c0/469e1d5bf1af585742128827000711efa61010b699cb040ab1800bcd3ccdd37f63ec30642c9e07c4439c1db6e46345582614275daca3e0f4abae29b0083f04a6 + languageName: node + linkType: hard + "tslib@npm:^1.10.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -12754,13 +12789,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.2": - version: 2.7.0 - resolution: "tslib@npm:2.7.0" - checksum: 10c0/469e1d5bf1af585742128827000711efa61010b699cb040ab1800bcd3ccdd37f63ec30642c9e07c4439c1db6e46345582614275daca3e0f4abae29b0083f04a6 - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -12830,7 +12858,7 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^1.6.4, type-is@npm:~1.6.18": +"type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" dependencies: @@ -12887,13 +12915,6 @@ __metadata: languageName: node linkType: hard -"typedarray@npm:^0.0.6": - version: 0.0.6 - resolution: "typedarray@npm:0.0.6" - checksum: 10c0/6005cb31df50eef8b1f3c780eb71a17925f3038a100d82f9406ac2ad1de5eb59f8e6decbdc145b3a1f8e5836e17b0c0002fb698b9fe2516b8f9f9ff602d36412 - languageName: node - linkType: hard - "typescript-transform-paths@npm:^3.4.6": version: 3.4.6 resolution: "typescript-transform-paths@npm:3.4.6" @@ -12989,7 +13010,7 @@ __metadata: languageName: node linkType: hard -"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": +"unpipe@npm:1.0.0": version: 1.0.0 resolution: "unpipe@npm:1.0.0" checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c @@ -13033,20 +13054,13 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": +"util-deprecate@npm:^1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 languageName: node linkType: hard -"utils-merge@npm:1.0.1": - version: 1.0.1 - resolution: "utils-merge@npm:1.0.1" - checksum: 10c0/02ba649de1b7ca8854bfe20a82f1dfbdda3fb57a22ab4a8972a63a34553cf7aa51bc9081cf7e001b035b88186d23689d69e71b510e610a09a4c66f68aa95b672 - languageName: node - linkType: hard - "uuid@npm:9.0.1, uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" @@ -13113,7 +13127,7 @@ __metadata: languageName: node linkType: hard -"vary@npm:^1, vary@npm:~1.1.2": +"vary@npm:^1": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f @@ -13412,13 +13426,6 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.0": - version: 4.0.2 - resolution: "xtend@npm:4.0.2" - checksum: 10c0/366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e - languageName: node - linkType: hard - "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" From d2c736bf9059595d4af4e0dbf36072fe00d95a69 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Mon, 23 Sep 2024 17:42:20 -0500 Subject: [PATCH 22/26] Enable compression for HTTP responses --- package.json | 1 + src/core/http/http.adapter.ts | 11 +++ yarn.lock | 158 ++++++++++++++++++++++++++++++++-- 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 27701f5946..3490e26777 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@aws-sdk/client-s3": "^3.440.0", "@aws-sdk/s3-request-presigner": "^3.440.0", "@faker-js/faker": "^8.2.0", + "@fastify/compress": "^7.0.3", "@fastify/cookie": "^9.4.0", "@fastify/cors": "^9.0.1", "@ffprobe-installer/ffprobe": "^2.1.2", diff --git a/src/core/http/http.adapter.ts b/src/core/http/http.adapter.ts index c606100d98..93b91793e5 100644 --- a/src/core/http/http.adapter.ts +++ b/src/core/http/http.adapter.ts @@ -1,3 +1,4 @@ +import compression from '@fastify/compress'; import cookieParser from '@fastify/cookie'; import cors from '@fastify/cors'; // eslint-disable-next-line @seedcompany/no-restricted-imports @@ -6,6 +7,7 @@ import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; +import * as zlib from 'node:zlib'; import { ConfigService } from '~/core/config/config.service'; import type { CookieOptions, CorsOptions, IResponse } from './types'; @@ -20,6 +22,15 @@ export class HttpAdapterHost extends HttpAdapterHostImpl {} export class HttpAdapter extends FastifyAdapter { async configure(app: NestFastifyApplication, config: ConfigService) { + await app.register(compression, { + brotliOptions: { + params: { + // This API returns text (JSON), so optimize for that + [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, + }, + }, + }); + await app.register(cors, { // typecast to undo deep readonly ...(config.cors as CorsOptions), diff --git a/yarn.lock b/yarn.lock index 20f7f2b727..c5c76622d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1596,6 +1596,13 @@ __metadata: languageName: node linkType: hard +"@fastify/accept-negotiator@npm:^1.1.0": + version: 1.1.0 + resolution: "@fastify/accept-negotiator@npm:1.1.0" + checksum: 10c0/1cb9a298c992b812869158ddc6093557a877b30e5f77618a7afea985a0667c50bc7113593bf0f7f9dc9b82b94c16e8ab127a0afc3efde6677fd645539f6d08e5 + languageName: node + linkType: hard + "@fastify/ajv-compiler@npm:^3.5.0": version: 3.6.0 resolution: "@fastify/ajv-compiler@npm:3.6.0" @@ -1607,6 +1614,22 @@ __metadata: languageName: node linkType: hard +"@fastify/compress@npm:^7.0.3": + version: 7.0.3 + resolution: "@fastify/compress@npm:7.0.3" + dependencies: + "@fastify/accept-negotiator": "npm:^1.1.0" + fastify-plugin: "npm:^4.5.0" + mime-db: "npm:^1.52.0" + minipass: "npm:^7.0.2" + peek-stream: "npm:^1.1.3" + pump: "npm:^3.0.0" + pumpify: "npm:^2.0.1" + readable-stream: "npm:^4.5.2" + checksum: 10c0/0c914ca347944d4fb893c5d08503ae7d3f1ecc2ec6812b4b837753587009e0b6a13531c7f9cb86750ed891c8b0b728fc2e98b42a072a0153b4504686e41b6a16 + languageName: node + linkType: hard + "@fastify/cookie@npm:^9.4.0": version: 9.4.0 resolution: "@fastify/cookie@npm:9.4.0" @@ -5410,6 +5433,7 @@ __metadata: "@aws-sdk/s3-request-presigner": "npm:^3.440.0" "@edgedb/generate": "github:CarsonF/edgedb-js#workspace=@edgedb/generate&head=temp-host" "@faker-js/faker": "npm:^8.2.0" + "@fastify/compress": "npm:^7.0.3" "@fastify/cookie": "npm:^9.4.0" "@fastify/cors": "npm:^9.0.1" "@ffprobe-installer/ffprobe": "npm:^2.1.2" @@ -5536,7 +5560,7 @@ __metadata: languageName: node linkType: hard -"core-util-is@npm:^1.0.3": +"core-util-is@npm:^1.0.3, core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" checksum: 10c0/90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 @@ -6027,6 +6051,30 @@ __metadata: languageName: node linkType: hard +"duplexify@npm:^3.5.0": + version: 3.7.1 + resolution: "duplexify@npm:3.7.1" + dependencies: + end-of-stream: "npm:^1.0.0" + inherits: "npm:^2.0.1" + readable-stream: "npm:^2.0.0" + stream-shift: "npm:^1.0.0" + checksum: 10c0/59d1440c1b4e3a4db35ae96933392703ce83518db1828d06b9b6322920d6cbbf0b7159e88be120385fe459e77f1eb0c7622f26e9ec1f47c9ff05c2b35747dbd3 + languageName: node + linkType: hard + +"duplexify@npm:^4.1.1": + version: 4.1.3 + resolution: "duplexify@npm:4.1.3" + dependencies: + end-of-stream: "npm:^1.4.1" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + stream-shift: "npm:^1.0.2" + checksum: 10c0/8a7621ae95c89f3937f982fe36d72ea997836a708471a75bb2a0eecde3330311b1e128a6dad510e0fd64ace0c56bff3484ed2e82af0e465600c82117eadfbda5 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -6144,7 +6192,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0": +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -6911,7 +6959,7 @@ __metadata: languageName: node linkType: hard -"fastify-plugin@npm:^4.0.0, fastify-plugin@npm:^4.4.0": +"fastify-plugin@npm:^4.0.0, fastify-plugin@npm:^4.4.0, fastify-plugin@npm:^4.5.0": version: 4.5.1 resolution: "fastify-plugin@npm:4.5.1" checksum: 10c0/f58f79cd9d3c88fd7f79a3270276c6339fc57bbe72ef14d20b73779193c404e317ac18e8eae2c5071b3909ebee45d7eb6871da4e65464ac64ed0d9746b4e9b9f @@ -8361,6 +8409,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10c0/18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -9667,6 +9722,13 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:^1.52.0": + version: 1.53.0 + resolution: "mime-db@npm:1.53.0" + checksum: 10c0/1dcc37ba8ed5d1c179f5c6f0837e8db19371d5f2ea3690c3c2f3fa8c3858f976851d3460b172b4dee78ebd606762cbb407aa398545fbacd539e519f858cd7bf4 + languageName: node + linkType: hard + "mime-types@npm:^2.1.12, mime-types@npm:~2.1.24": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -11054,6 +11116,17 @@ __metadata: languageName: node linkType: hard +"peek-stream@npm:^1.1.3": + version: 1.1.3 + resolution: "peek-stream@npm:1.1.3" + dependencies: + buffer-from: "npm:^1.0.0" + duplexify: "npm:^3.5.0" + through2: "npm:^2.0.3" + checksum: 10c0/3c35d1951b8640036f93b1b5628a90f849e49ca4f2e6aba393ff4978413931d9c491c83f71a92f878d5ea4c670af0bba04dfcfb79b310ead22601db7c1420e36 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -11235,6 +11308,13 @@ __metadata: languageName: node linkType: hard +"process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: 10c0/bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 + languageName: node + linkType: hard + "process-warning@npm:^3.0.0": version: 3.0.0 resolution: "process-warning@npm:3.0.0" @@ -11314,6 +11394,17 @@ __metadata: languageName: node linkType: hard +"pumpify@npm:^2.0.1": + version: 2.0.1 + resolution: "pumpify@npm:2.0.1" + dependencies: + duplexify: "npm:^4.1.1" + inherits: "npm:^2.0.3" + pump: "npm:^3.0.0" + checksum: 10c0/f9c12190dc65f8c347fe82e993708e4d14ce82c96f7cbd24b52f488cfa4dbc2ebbcc49e0f54655f1ca118fea59ddeec6ca5a34ef45558c8bb1de2f1ffa307198 + languageName: node + linkType: hard + "punycode@npm:^2.1.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -11423,7 +11514,22 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:^2.0.0, readable-stream@npm:~2.3.6": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~2.0.0" + safe-buffer: "npm:~5.1.1" + string_decoder: "npm:~1.1.1" + util-deprecate: "npm:~1.0.1" + checksum: 10c0/7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa + languageName: node + linkType: hard + +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -11434,7 +11540,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^4.0.0": +"readable-stream@npm:^4.0.0, readable-stream@npm:^4.5.2": version: 4.5.2 resolution: "readable-stream@npm:4.5.2" dependencies: @@ -11810,6 +11916,13 @@ __metadata: languageName: node linkType: hard +"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": + version: 5.1.2 + resolution: "safe-buffer@npm:5.1.2" + checksum: 10c0/780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 + languageName: node + linkType: hard + "safe-regex-test@npm:^1.0.0": version: 1.0.0 resolution: "safe-regex-test@npm:1.0.0" @@ -12214,6 +12327,13 @@ __metadata: languageName: node linkType: hard +"stream-shift@npm:^1.0.0, stream-shift@npm:^1.0.2": + version: 1.0.3 + resolution: "stream-shift@npm:1.0.3" + checksum: 10c0/939cd1051ca750d240a0625b106a2b988c45fb5a3be0cebe9a9858cb01bc1955e8c7b9fac17a9462976bea4a7b704e317c5c2200c70f0ca715a3363b9aa4fd3b + languageName: node + linkType: hard + "streamsearch@npm:^1.1.0": version: 1.1.0 resolution: "streamsearch@npm:1.1.0" @@ -12328,6 +12448,15 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: "npm:~5.1.0" + checksum: 10c0/b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -12513,6 +12642,16 @@ __metadata: languageName: node linkType: hard +"through2@npm:^2.0.3": + version: 2.0.5 + resolution: "through2@npm:2.0.5" + dependencies: + readable-stream: "npm:~2.3.6" + xtend: "npm:~4.0.1" + checksum: 10c0/cbfe5b57943fa12b4f8c043658c2a00476216d79c014895cef1ac7a1d9a8b31f6b438d0e53eecbb81054b93128324a82ecd59ec1a4f91f01f7ac113dcb14eade + languageName: node + linkType: hard + "through@npm:^2.3.6": version: 2.3.8 resolution: "through@npm:2.3.8" @@ -13054,7 +13193,7 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1": +"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 @@ -13426,6 +13565,13 @@ __metadata: languageName: node linkType: hard +"xtend@npm:~4.0.1": + version: 4.0.2 + resolution: "xtend@npm:4.0.2" + checksum: 10c0/366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" From a9ba9bec1732e81dbc2bbea590101589dacd89ed Mon Sep 17 00:00:00 2001 From: Carson Full Date: Mon, 30 Sep 2024 10:24:03 -0500 Subject: [PATCH 23/26] Create our own fastify decorators with stricter types --- .eslintrc.cjs | 5 +++++ src/core/http/decorators.ts | 19 +++++++++++++++++++ src/core/http/index.ts | 1 + 3 files changed, 25 insertions(+) create mode 100644 src/core/http/decorators.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1078eb4d92..9e77cd14b3 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -83,6 +83,11 @@ const restrictedImports = [ path: '@nestjs/common', replacement: { importName: 'HttpMiddleware', path: '~/core/http' }, }, + { + importNames: ['RouteConfig', 'RouteConstraints'], + path: '@nestjs/platform-fastify', + replacement: { path: '~/core/http' }, + }, ]; const namingConvention = [ diff --git a/src/core/http/decorators.ts b/src/core/http/decorators.ts new file mode 100644 index 0000000000..9316d5dbab --- /dev/null +++ b/src/core/http/decorators.ts @@ -0,0 +1,19 @@ +import { + FASTIFY_ROUTE_CONFIG_METADATA, + FASTIFY_ROUTE_CONSTRAINTS_METADATA, +} from '@nestjs/platform-fastify/constants.js'; +import { createMetadataDecorator } from '@seedcompany/nest'; +import { FastifyContextConfig } from 'fastify'; +import type { RouteConstraint } from 'fastify/types/route'; + +export const RouteConstraints = createMetadataDecorator({ + key: FASTIFY_ROUTE_CONSTRAINTS_METADATA, + types: ['class', 'method'], + setter: (config: RouteConstraint) => config, +}); + +export const RouteConfig = createMetadataDecorator({ + key: FASTIFY_ROUTE_CONFIG_METADATA, + types: ['class', 'method'], + setter: (config: FastifyContextConfig) => config, +}); diff --git a/src/core/http/index.ts b/src/core/http/index.ts index 542d05e0a2..60228bbc70 100644 --- a/src/core/http/index.ts +++ b/src/core/http/index.ts @@ -1,3 +1,4 @@ export type * from './types'; export * from './http.adapter'; export * from './http.module'; +export * from './decorators'; From 681c65dcf9a945cdfcbac382b8b7ec5ea4d30c37 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Mon, 30 Sep 2024 10:25:06 -0500 Subject: [PATCH 24/26] Configure fastify route injection ourselves This is prep for our own expansion --- src/core/http/http.adapter.ts | 49 ++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/core/http/http.adapter.ts b/src/core/http/http.adapter.ts index 93b91793e5..7aaf8db403 100644 --- a/src/core/http/http.adapter.ts +++ b/src/core/http/http.adapter.ts @@ -1,14 +1,20 @@ import compression from '@fastify/compress'; import cookieParser from '@fastify/cookie'; import cors from '@fastify/cors'; +import { + VERSION_NEUTRAL, + type VersionValue, +} from '@nestjs/common/interfaces/version-options.interface.js'; // eslint-disable-next-line @seedcompany/no-restricted-imports import { HttpAdapterHost as HttpAdapterHostImpl } from '@nestjs/core'; import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; +import type { FastifyInstance, HTTPMethods, RouteOptions } from 'fastify'; import * as zlib from 'node:zlib'; import { ConfigService } from '~/core/config/config.service'; +import { RouteConfig, RouteConstraints } from './decorators'; import type { CookieOptions, CorsOptions, IResponse } from './types'; export type NestHttpApplication = NestFastifyApplication & { @@ -20,7 +26,18 @@ export type NestHttpApplication = NestFastifyApplication & { export class HttpAdapterHost extends HttpAdapterHostImpl {} -export class HttpAdapter extends FastifyAdapter { +// @ts-expect-error Convert private methods to protected +class PatchedFastifyAdapter extends FastifyAdapter { + protected injectRouteOptions( + routerMethodKey: Uppercase, + ...args: any[] + ): FastifyInstance { + // @ts-expect-error work around being marked as private + return super.injectRouteOptions(routerMethodKey, ...args); + } +} + +export class HttpAdapter extends PatchedFastifyAdapter { async configure(app: NestFastifyApplication, config: ConfigService) { await app.register(compression, { brotliOptions: { @@ -42,6 +59,36 @@ export class HttpAdapter extends FastifyAdapter { config.applyTimeouts(app.getHttpServer(), config.httpTimeouts); } + protected injectRouteOptions( + method: Uppercase, + urlOrHandler: string | RouteOptions['handler'], + maybeHandler?: RouteOptions['handler'], + ) { + // I don't know why NestJS allows url/path parameter to be omitted. + const url = typeof urlOrHandler === 'function' ? '' : urlOrHandler; + const handler = + typeof urlOrHandler === 'function' ? urlOrHandler : maybeHandler!; + + const config = RouteConfig.get(handler) ?? {}; + const constraints = RouteConstraints.get(handler) ?? {}; + + let version: VersionValue | undefined = (handler as any).version; + version = version === VERSION_NEUTRAL ? undefined : version; + if (version) { + // @ts-expect-error this is what upstream does + constraints.version = version; + } + + const route: RouteOptions = { + method, + url, + handler, + ...(Object.keys(constraints).length > 0 ? { constraints } : {}), + ...(Object.keys(config).length > 0 ? { config } : {}), + }; + return this.instance.route(route); + } + setCookie( response: IResponse, name: string, From 531d326129ddbfd14367652ae3054a7391d471f0 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 27 Sep 2024 20:21:38 -0500 Subject: [PATCH 25/26] Configure raw body handling at http foundation layer --- package.json | 1 + .../file/local-bucket.controller.ts | 11 +++--- src/core/http/decorators.ts | 37 +++++++++++++++++++ src/core/http/http.adapter.ts | 30 ++++++++++++++- yarn.lock | 16 +++++++- 5 files changed, 86 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 3490e26777..a1743394d2 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "fast-safe-stringify": "^2.1.1", "fastest-levenshtein": "^1.0.16", "fastify": "^4.28.1", + "fastify-raw-body": "^4.3.0", "file-type": "^18.6.0", "glob": "^10.3.10", "got": "^14.3.0", diff --git a/src/components/file/local-bucket.controller.ts b/src/components/file/local-bucket.controller.ts index 3b6a942d9e..2744019596 100644 --- a/src/components/file/local-bucket.controller.ts +++ b/src/components/file/local-bucket.controller.ts @@ -1,4 +1,5 @@ import { + Body, Controller, Get, Headers, @@ -9,9 +10,8 @@ import { } from '@nestjs/common'; import { DateTime } from 'luxon'; import { URL } from 'node:url'; -import rawBody from 'raw-body'; import { InputException } from '~/common'; -import { HttpAdapter, IRequest, IResponse } from '~/core/http'; +import { HttpAdapter, IRequest, IResponse, RawBody } from '~/core/http'; import { FileBucket, InvalidSignedUrlException } from './bucket'; /** @@ -27,14 +27,13 @@ export class LocalBucketController { ) {} @Put() + @RawBody({ passthrough: true }) async upload( @Headers('content-type') contentType: string, @Request() req: IRequest, + @Body() contents: Buffer, ) { - // Chokes on json files because they are parsed with body-parser. - // Need to disable it for this path or create a workaround. - const contents = await rawBody(req); - if (!contents) { + if (!contents || !Buffer.isBuffer(contents)) { throw new InputException(); } diff --git a/src/core/http/decorators.ts b/src/core/http/decorators.ts index 9316d5dbab..5e71193f69 100644 --- a/src/core/http/decorators.ts +++ b/src/core/http/decorators.ts @@ -2,6 +2,7 @@ import { FASTIFY_ROUTE_CONFIG_METADATA, FASTIFY_ROUTE_CONSTRAINTS_METADATA, } from '@nestjs/platform-fastify/constants.js'; +import { Many } from '@seedcompany/common'; import { createMetadataDecorator } from '@seedcompany/nest'; import { FastifyContextConfig } from 'fastify'; import type { RouteConstraint } from 'fastify/types/route'; @@ -17,3 +18,39 @@ export const RouteConfig = createMetadataDecorator({ types: ['class', 'method'], setter: (config: FastifyContextConfig) => config, }); + +/** + * @example + * ```ts + * @RawBody() + * route( + * @Request('rawBody') raw: string, + * @Body() contents: JSON + * ) {} + * ``` + * @example + * ```ts + * @RawBody({ passthrough: true }) + * route( + * @Body() contents: Buffer + * ) {} + * ``` + */ +export const RawBody = createMetadataDecorator({ + types: ['class', 'method'], + setter: ( + config: { + /** + * Pass the raw body through to the handler or + * just to keep the raw body in addition to regular content parsing. + */ + passthrough?: boolean; + /** + * The allowed content types. + * Only applicable if passthrough is true. + * Defaults to '*' + */ + allowContentTypes?: Many | RegExp; + } = {}, + ) => config, +}); diff --git a/src/core/http/http.adapter.ts b/src/core/http/http.adapter.ts index 7aaf8db403..7491d98d93 100644 --- a/src/core/http/http.adapter.ts +++ b/src/core/http/http.adapter.ts @@ -12,9 +12,10 @@ import { NestFastifyApplication, } from '@nestjs/platform-fastify'; import type { FastifyInstance, HTTPMethods, RouteOptions } from 'fastify'; +import rawBody from 'fastify-raw-body'; import * as zlib from 'node:zlib'; import { ConfigService } from '~/core/config/config.service'; -import { RouteConfig, RouteConstraints } from './decorators'; +import { RawBody, RouteConfig, RouteConstraints } from './decorators'; import type { CookieOptions, CorsOptions, IResponse } from './types'; export type NestHttpApplication = NestFastifyApplication & { @@ -54,6 +55,9 @@ export class HttpAdapter extends PatchedFastifyAdapter { }); await app.register(cookieParser); + // Only on routes we've decorated. + await app.register(rawBody, { global: false }); + app.setGlobalPrefix(config.hostUrl$.value.pathname.slice(1)); config.applyTimeouts(app.getHttpServer(), config.httpTimeouts); @@ -71,6 +75,7 @@ export class HttpAdapter extends PatchedFastifyAdapter { const config = RouteConfig.get(handler) ?? {}; const constraints = RouteConstraints.get(handler) ?? {}; + const rawBody = RawBody.get(handler); let version: VersionValue | undefined = (handler as any).version; version = version === VERSION_NEUTRAL ? undefined : version; @@ -79,6 +84,13 @@ export class HttpAdapter extends PatchedFastifyAdapter { constraints.version = version; } + // Plugin configured to just add the rawBody property while continuing + // to parse the content type normally. + // Useful for signed webhook payload validation. + if (rawBody && !rawBody.passthrough) { + config.rawBody = true; + } + const route: RouteOptions = { method, url, @@ -86,6 +98,22 @@ export class HttpAdapter extends PatchedFastifyAdapter { ...(Object.keys(constraints).length > 0 ? { constraints } : {}), ...(Object.keys(config).length > 0 ? { config } : {}), }; + + if (rawBody?.passthrough) { + const { allowContentTypes } = rawBody; + const contentTypes = Array.isArray(allowContentTypes) + ? allowContentTypes.slice() + : ((allowContentTypes ?? '*') as string | RegExp); + return this.instance.register(async (child) => { + child.removeAllContentTypeParsers(); + child.addContentTypeParser( + contentTypes, + { parseAs: 'buffer' }, + (req, payload, done) => done(null, payload), + ); + child.route(route); + }); + } return this.instance.route(route); } diff --git a/yarn.lock b/yarn.lock index c5c76622d1..710b9fae65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5492,6 +5492,7 @@ __metadata: fast-safe-stringify: "npm:^2.1.1" fastest-levenshtein: "npm:^1.0.16" fastify: "npm:^4.28.1" + fastify-raw-body: "npm:^4.3.0" file-type: "npm:^18.6.0" glob: "npm:^10.3.10" got: "npm:^14.3.0" @@ -6966,6 +6967,17 @@ __metadata: languageName: node linkType: hard +"fastify-raw-body@npm:^4.3.0": + version: 4.3.0 + resolution: "fastify-raw-body@npm:4.3.0" + dependencies: + fastify-plugin: "npm:^4.0.0" + raw-body: "npm:^2.5.1" + secure-json-parse: "npm:^2.4.0" + checksum: 10c0/3260ab2fc3483a1668442b0a2b60a3f671948d8fc6e7a811ac782cfc28d31d8f064e7b3835ca21cb542d41c4a2a7bc84dd5c18ef0c38f90d7387dd6bbb83161d + languageName: node + linkType: hard + "fastify@npm:4.28.1, fastify@npm:^4.28.1": version: 4.28.1 resolution: "fastify@npm:4.28.1" @@ -11467,7 +11479,7 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:2.5.2": +"raw-body@npm:2.5.2, raw-body@npm:^2.5.1": version: 2.5.2 resolution: "raw-body@npm:2.5.2" dependencies: @@ -11975,7 +11987,7 @@ __metadata: languageName: node linkType: hard -"secure-json-parse@npm:^2.7.0": +"secure-json-parse@npm:^2.4.0, secure-json-parse@npm:^2.7.0": version: 2.7.0 resolution: "secure-json-parse@npm:2.7.0" checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 From a1ece6aa235737dff30d5bbf3271a111d8a101dc Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 22 Oct 2024 12:50:09 -0500 Subject: [PATCH 26/26] Serve to all IPs (fastify) --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 37a27a63ff..475c23b6b8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,7 +33,7 @@ async function bootstrap() { await app.configure(app, config); app.enableShutdownHooks(); - await app.listen(config.port, () => { + await app.listen(config.port, '0.0.0.0', () => { app.get(Logger).log(`Listening at ${config.hostUrl$.value}graphql`); }); }