From 45c5272e203a91639a508b9e3bbc04d088755eb6 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 4 Sep 2024 14:44:37 -0500 Subject: [PATCH 01/11] Merge DB Names into ResourceNameLike --- src/common/id-field.ts | 8 ++------ src/core/resources/resource-name.types.ts | 7 ++++--- src/core/resources/resources.host.ts | 3 +-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/common/id-field.ts b/src/common/id-field.ts index 4a6bf38eae..36d076ad72 100644 --- a/src/common/id-field.ts +++ b/src/common/id-field.ts @@ -2,11 +2,7 @@ import { applyDecorators } from '@nestjs/common'; import { Field, FieldOptions, ID as IDType } from '@nestjs/graphql'; import { ValidationOptions } from 'class-validator'; import { IsAny, IsNever, Tagged } from 'type-fest'; -import type { - AllResourceDBNames, - ResourceName, - ResourceNameLike, -} from '~/core'; +import type { ResourceName, ResourceNameLike } from '~/core'; import { IsId } from './validators'; export const IdField = ({ @@ -40,4 +36,4 @@ type IDTag = IsAny extends true : Kind : never; -type IDKindLike = ResourceNameLike | AllResourceDBNames | object; +type IDKindLike = ResourceNameLike | object; diff --git a/src/core/resources/resource-name.types.ts b/src/core/resources/resource-name.types.ts index ee60438aed..37840ecc01 100644 --- a/src/core/resources/resource-name.types.ts +++ b/src/core/resources/resource-name.types.ts @@ -2,8 +2,9 @@ import { ConditionalKeys, IsAny, LiteralUnion, ValueOf } from 'type-fest'; import { DBName, ResourceShape } from '~/common'; import { ResourceDBMap, ResourceMap } from './map'; -export type AllResourceNames = keyof ResourceMap; +export type AllResourceAppNames = keyof ResourceMap; export type AllResourceDBNames = DBName>; +export type AllResourceNames = AllResourceAppNames | AllResourceDBNames; export type ResourceNameLike = LiteralUnion; //region ResourceName @@ -32,13 +33,13 @@ export type ResourceName< T, IncludeSubclasses extends boolean = false, > = IsAny extends true - ? AllResourceNames // short-circuit and prevent many seemly random circular definitions + ? AllResourceAppNames // short-circuit and prevent many seemly random circular definitions : T extends AllResourceDBNames ? ResourceNameFromStatic< ResourceMap[ResourceNameFromDBName], IncludeSubclasses > - : T extends AllResourceNames + : T extends AllResourceAppNames ? ResourceNameFromStatic : T extends ResourceShape ? ResourceNameFromStatic diff --git a/src/core/resources/resources.host.ts b/src/core/resources/resources.host.ts index 2c31901830..ba56b36ba9 100644 --- a/src/core/resources/resources.host.ts +++ b/src/core/resources/resources.host.ts @@ -14,7 +14,6 @@ import { import { ResourceMap } from './map'; import { __privateDontUseThis } from './resource-map-holder'; import { - AllResourceDBNames, ResourceName, ResourceNameLike, ResourceStaticFromName, @@ -70,7 +69,7 @@ export class ResourcesHost { return this.getByName(name as any); } - getByEdgeDB( + getByEdgeDB( name: Name, ): EnhancedResource< string extends Name From db10842ee32f4efad9390691ab25e96ca4d76c30 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 4 Sep 2024 14:45:21 -0500 Subject: [PATCH 02/11] Change ResourcesHost.getByName to handle DB names --- src/core/resources/resources.host.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/resources/resources.host.ts b/src/core/resources/resources.host.ts index ba56b36ba9..c1b3b11f85 100644 --- a/src/core/resources/resources.host.ts +++ b/src/core/resources/resources.host.ts @@ -4,7 +4,6 @@ import { CachedByArg, mapKeys } from '@seedcompany/common'; import { isObjectType } from 'graphql'; import { LazyGetter as Once } from 'lazy-get-decorator'; import { mapValues } from 'lodash'; -import { ValueOf } from 'type-fest'; import { EnhancedResource, InvalidIdForTypeException, @@ -14,6 +13,7 @@ import { import { ResourceMap } from './map'; import { __privateDontUseThis } from './resource-map-holder'; import { + AllResourceNames, ResourceName, ResourceNameLike, ResourceStaticFromName, @@ -52,17 +52,20 @@ export class ResourcesHost { return mapValues(map, EnhancedResource.of) as any; } - getByName( - name: K, - ): EnhancedResource>> { + getByName( + name: Name, + ): EnhancedResource>> { + if (name.includes('::')) { + return this.getByEdgeDB(name) as any; + } const map = this.getEnhancedMap(); - const resource = map[name]; + const resource = map[name as keyof ResourceMap]; if (!resource) { throw new ServerException( `Unable to determine resource from ResourceMap for type: ${name}`, ); } - return resource; + return resource as any; } getByDynamicName(name: ResourceNameLike): EnhancedResource { From d6f3110c8214482cdb702e697e8ef4bcd8639d69 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 4 Sep 2024 14:53:03 -0500 Subject: [PATCH 03/11] Change ResourceResolver to handle DB names --- .../resources/resource-resolver.service.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/core/resources/resource-resolver.service.ts b/src/core/resources/resource-resolver.service.ts index 5cd30fc6ec..e4597f254f 100644 --- a/src/core/resources/resource-resolver.service.ts +++ b/src/core/resources/resource-resolver.service.ts @@ -7,6 +7,7 @@ import { ID, many, Many, ObjectView, ServerException, Session } from '~/common'; import { BaseNode } from '../database/results'; import { ILogger, Logger } from '../logger'; import { ResourceMap } from './map'; +import { ResourcesHost } from './resources.host'; const RESOLVE_BY_ID = 'RESOLVE_BY_ID'; interface Shape { @@ -48,6 +49,7 @@ export class ResourceResolver { constructor( private readonly discover: DiscoveryService, + private readonly resourcesHost: ResourcesHost, private readonly schemaHost: GraphQLSchemaHost, @Logger('resource-resolver') private readonly logger: ILogger, ) {} @@ -144,13 +146,19 @@ export class ResourceResolver { const names = many(types).map((t) => t.replace(/^Deleted_/, '')); const schema = this.schemaHost.schema; - const resolved = names.filter( - (name) => schema.getType(name) instanceof GraphQLObjectType, - ); + const resolved = names + .flatMap((name) => { + try { + return this.resourcesHost.getByDynamicName(name).name; + } catch (e) { + // Ignore names/`labels` that don't have corresponding resources. + return []; + } + }) + .filter((name) => schema.getType(name) instanceof GraphQLObjectType); if (resolved.length === 1) { - // This is mostly true... - return resolved[0] as keyof ResourceMap; + return resolved[0]; } const namesStr = names.join(', '); From 923444234343600ae3647aff76ed0f9e029733d3 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 5 Sep 2024 09:54:04 -0500 Subject: [PATCH 04/11] Resolve EdgeDB FQNs with `Resource`'s `resolveType` This is an UGLY hack to use ResourcesHost statically --- src/common/resource.dto.ts | 18 ++++++++++++++++-- src/core/resources/resources.host.ts | 4 +++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/common/resource.dto.ts b/src/common/resource.dto.ts index 0f3ed5804a..5ba4ef9b68 100644 --- a/src/common/resource.dto.ts +++ b/src/common/resource.dto.ts @@ -4,7 +4,12 @@ import { LazyGetter as Once } from 'lazy-get-decorator'; import { DateTime } from 'luxon'; import { keys as keysOf } from 'ts-transformer-keys'; import { inspect } from 'util'; -import type { ResourceDBMap, ResourceName } from '~/core'; +import type { + ResourceDBMap, + ResourceLike, + ResourceName, + ResourcesHost, +} from '~/core'; import { $, e } from '~/core/edgedb/reexports'; import { ScopedRole } from '../components/authorization/dto'; import { CalculatedSymbol } from './calculated.decorator'; @@ -27,7 +32,7 @@ const hasTypename = (value: unknown): value is { __typename: string } => export const resolveByTypename = (interfaceName: string) => (value: unknown) => { if (hasTypename(value)) { - return value.__typename; + return EnhancedResource.resolve(value.__typename).name; } throw new ServerException(`Cannot resolve ${interfaceName} type`); }; @@ -87,6 +92,8 @@ export class EnhancedResource> { static readonly dbTypes = new WeakMap, $.$expr_PathNode>(); /** @internal */ static readonly dbSkipAccessPolicies = new Set(); + /** @internal */ + static resourcesHost?: ResourcesHost; private constructor(readonly type: T) {} private static readonly refs = new WeakMap< @@ -94,6 +101,13 @@ export class EnhancedResource> { EnhancedResource >(); + static resolve(ref: ResourceLike) { + if (!EnhancedResource.resourcesHost) { + throw new ServerException('Cannot resolve without ResourcesHost'); + } + return EnhancedResource.resourcesHost.enhance(ref); + } + static of>( resource: T | EnhancedResource, ): EnhancedResource { diff --git a/src/core/resources/resources.host.ts b/src/core/resources/resources.host.ts index c1b3b11f85..16e2e5b472 100644 --- a/src/core/resources/resources.host.ts +++ b/src/core/resources/resources.host.ts @@ -30,7 +30,9 @@ export type ResourceLike = @Injectable() export class ResourcesHost { - constructor(private readonly gqlSchema: GraphQLSchemaHost) {} + constructor(private readonly gqlSchema: GraphQLSchemaHost) { + EnhancedResource.resourcesHost = this; + } getMap() { // @ts-expect-error Yeah we are assuming each type has been correctly From 6cfca45c6ba5349d2e5a5617ed84812e7eada2a6 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 29 Aug 2024 16:33:57 -0500 Subject: [PATCH 05/11] Bump edgedb libs & opt-in to new discriminated unions --- package.json | 4 +-- src/common/resource.dto.ts | 10 ++++++ src/core/edgedb/dto.repository.ts | 6 ++-- src/core/edgedb/index.ts | 9 ++++++ yarn.lock | 52 +++++++++++++++---------------- 5 files changed, 50 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index d8b2def34b..de858b12a9 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "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.20240506T235920", + "edgedb": "^1.6.0-canary.20240827T111834", "execa": "^8.0.1", "express": "^4.18.2", "extensionless": "^1.7.0", @@ -107,7 +107,7 @@ "yaml": "^2.3.3" }, "devDependencies": { - "@edgedb/generate": "^0.6.0-canary.20240506T235941", + "@edgedb/generate": "github:CarsonF/edgedb-js#workspace=@edgedb/generate&head=temp-host", "@nestjs/cli": "^10.2.1", "@nestjs/schematics": "^10.0.3", "@nestjs/testing": "^10.2.7", diff --git a/src/common/resource.dto.ts b/src/common/resource.dto.ts index 5ba4ef9b68..cfb3e6ca1b 100644 --- a/src/common/resource.dto.ts +++ b/src/common/resource.dto.ts @@ -319,7 +319,17 @@ export type DBType> = : never : never; +/** + * The name of the EdgeDB type, it could be abstract. + */ export type DBName = T['__element__']['__name__']; +/** + * The name(s) of the concrete EdgeDB types. + * If the type is abstract, then it is a string union of the concrete type's names. + * If the type is concrete, then it is just the name, just as {@link DBName}. + */ +export type DBNames = + T['__element__']['__polyTypenames__']; export type MaybeUnsecuredInstance> = MaybeSecured>; diff --git a/src/core/edgedb/dto.repository.ts b/src/core/edgedb/dto.repository.ts index 7620cf0d86..653d69ddfc 100644 --- a/src/core/edgedb/dto.repository.ts +++ b/src/core/edgedb/dto.repository.ts @@ -66,7 +66,9 @@ export const RepoFor = < $.ObjectType< DBName, Root['__element__']['__pointers__'], - normaliseShape + normaliseShape, + Root['__element__']['__exclusives__'], + Root['__element__']['__polyTypenames__'] > >; @@ -198,7 +200,7 @@ export const RepoFor = < limit: input.count, })); const query = e.select({ - items, + items: items as any, total: e.count(listOfAllQuery), hasMore: e.op(e.count(thisPage), '>', input.count), }); diff --git a/src/core/edgedb/index.ts b/src/core/edgedb/index.ts index 2401d1d06f..a8ac24b33c 100644 --- a/src/core/edgedb/index.ts +++ b/src/core/edgedb/index.ts @@ -9,3 +9,12 @@ export * from './common.repository'; export * from './dto.repository'; export * from './query-util/disable-access-policies.option'; export * from './query-util/cast-to-enum'; + +declare module './generated-client/typesystem' { + export interface SetTypesystemOptions { + future: { + polymorphismAsDiscriminatedUnions: true; + strictTypeNames: true; + }; + } +} diff --git a/yarn.lock b/yarn.lock index 545d5e7dff..16507a6747 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1485,16 +1485,17 @@ __metadata: languageName: node linkType: hard -"@edgedb/generate@npm:^0.6.0-canary.20240506T235941": - version: 0.6.0-canary.20240506T235941 - resolution: "@edgedb/generate@npm:0.6.0-canary.20240506T235941" +"@edgedb/generate@github:CarsonF/edgedb-js#workspace=@edgedb/generate&head=temp-host": + version: 0.5.5 + resolution: "@edgedb/generate@https://github.com/CarsonF/edgedb-js.git#workspace=%40edgedb%2Fgenerate&commit=4c238572cae9762323c5ed359f399f6c76c3d2e5" dependencies: "@iarna/toml": "npm:^2.2.5" + debug: "npm:^4.3.4" peerDependencies: - edgedb: ^1.5.0 + edgedb: ^1.5.10 bin: generate: dist/cli.js - checksum: 10c0/67f42c695caa0d586f62bd62d1d5ffd810d78bdc60f88d2135cb6fec3f065a31f2e69a45ca8745be05f9f881f22d739f2f8053505354dba497876c3742fb2d8a + checksum: 10c0/b6527c8d92b53be49c4d859b2267224f52651eaf53fed5120a3954c6f1b82b6b1cb8afc4e1d99ee936f02be914d944ca69e39d057d618d456090e19d9756c49a languageName: node linkType: hard @@ -5339,7 +5340,7 @@ __metadata: "@apollo/subgraph": "npm:^2.5.6" "@aws-sdk/client-s3": "npm:^3.440.0" "@aws-sdk/s3-request-presigner": "npm:^3.440.0" - "@edgedb/generate": "npm:^0.6.0-canary.20240506T235941" + "@edgedb/generate": "github:CarsonF/edgedb-js#workspace=@edgedb/generate&head=temp-host" "@faker-js/faker": "npm:^8.2.0" "@ffprobe-installer/ffprobe": "npm:^2.1.2" "@golevelup/nestjs-discovery": "npm:^4.0.0" @@ -5390,7 +5391,7 @@ __metadata: debugger-is-attached: "npm:^1.2.0" dotenv: "npm:^16.3.1" dotenv-expand: "npm:^10.0.0" - edgedb: "npm:^1.6.0-canary.20240506T235920" + edgedb: "npm:^1.6.0-canary.20240827T111834" eslint: "npm:^8.52.0" eslint-plugin-no-only-tests: "npm:^3.1.0" eslint-plugin-typescript-sort-keys: "npm:^2.3.0" @@ -5984,17 +5985,18 @@ __metadata: languageName: node linkType: hard -"edgedb@npm:^1.6.0-canary.20240506T235920": - version: 1.6.0-canary.20240506T235920 - resolution: "edgedb@npm:1.6.0-canary.20240506T235920" +"edgedb@npm:^1.6.0-canary.20240827T111834": + version: 1.6.0-canary.20240827T111834 + resolution: "edgedb@npm:1.6.0-canary.20240827T111834" dependencies: debug: "npm:^4.3.4" env-paths: "npm:^3.0.0" - semver: "npm:^7.6.0" + semver: "npm:^7.6.2" + shell-quote: "npm:^1.8.1" which: "npm:^4.0.0" bin: edgedb: dist/cli.mjs - checksum: 10c0/3bff45a41ac35a1b92a776caedb48d53d568c8e0b4a56f28405490b8e8570f5261b032fcb659914185def3c422c5ae944a5dcc3853d1efaf86b2eec7d60da98c + checksum: 10c0/9bdf05484f2bbffc398bd6ca75628b056baaa51114bbfecc72382fd593619510ae319adfe15879138a90f64ff52ab9b159a87bed0472e05046a0ab19493f257e languageName: node linkType: hard @@ -9407,15 +9409,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 - languageName: node - linkType: hard - "lru-cache@npm:^7.10.1, lru-cache@npm:^7.14.1, lru-cache@npm:^7.18.3": version: 7.18.3 resolution: "lru-cache@npm:7.18.3" @@ -11772,14 +11765,12 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": - version: 7.6.0 - resolution: "semver@npm:7.6.0" - dependencies: - lru-cache: "npm:^6.0.0" +"semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.2": + version: 7.6.3 + resolution: "semver@npm:7.6.3" bin: semver: bin/semver.js - checksum: 10c0/fbfe717094ace0aa8d6332d7ef5ce727259815bd8d8815700853f4faf23aacbd7192522f0dc5af6df52ef4fa85a355ebd2f5d39f554bd028200d6cf481ab9b53 + checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf languageName: node linkType: hard @@ -11881,6 +11872,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.8.1": + version: 1.8.1 + resolution: "shell-quote@npm:1.8.1" + checksum: 10c0/8cec6fd827bad74d0a49347057d40dfea1e01f12a6123bf82c4649f3ef152fc2bc6d6176e6376bffcd205d9d0ccb4f1f9acae889384d20baff92186f01ea455a + languageName: node + linkType: hard + "shelljs@npm:0.8.5": version: 0.8.5 resolution: "shelljs@npm:0.8.5" From 23f1a6e19ad30b8ef31b26299aac7a563ca45ebc Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 4 Sep 2024 10:30:50 -0500 Subject: [PATCH 06/11] Switch to resolve function w/ class comparison instead of strings --- .../create-engagement-default-ceremony.handler.ts | 3 ++- src/components/engagement/dto/engagement.dto.ts | 1 + src/components/engagement/engagement.resolver.ts | 2 +- .../engagement/events/engagement-created.event.ts | 4 ++-- .../engagement/events/engagement-updated.event.ts | 4 ++-- .../handlers/set-initial-end-date.handler.ts | 4 ++-- .../handlers/set-last-status-date.handler.ts | 11 ++--------- 7 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/ceremony/handlers/create-engagement-default-ceremony.handler.ts b/src/components/ceremony/handlers/create-engagement-default-ceremony.handler.ts index 86f38788bd..b0ed318546 100644 --- a/src/components/ceremony/handlers/create-engagement-default-ceremony.handler.ts +++ b/src/components/ceremony/handlers/create-engagement-default-ceremony.handler.ts @@ -2,6 +2,7 @@ import { node, relation } from 'cypher-query-builder'; import { DateTime } from 'luxon'; import { ConfigService, EventsHandler, IEventHandler } from '~/core'; import { DatabaseService } from '~/core/database'; +import { LanguageEngagement } from '../../engagement/dto'; import { EngagementCreatedEvent } from '../../engagement/events'; import { CeremonyService } from '../ceremony.service'; import { CeremonyType } from '../dto'; @@ -20,7 +21,7 @@ export class CreateEngagementDefaultCeremonyHandler const { engagement } = event; const input = { type: - engagement.__typename === 'LanguageEngagement' + LanguageEngagement.resolve(engagement) === LanguageEngagement ? CeremonyType.Dedication : CeremonyType.Certification, }; diff --git a/src/components/engagement/dto/engagement.dto.ts b/src/components/engagement/dto/engagement.dto.ts index 192f345a38..32c7f8a013 100644 --- a/src/components/engagement/dto/engagement.dto.ts +++ b/src/components/engagement/dto/engagement.dto.ts @@ -63,6 +63,7 @@ class Engagement extends Interfaces { static readonly Props: string[] = keysOf(); static readonly SecuredProps: string[] = keysOf>(); static readonly Parent = import('../../project/dto').then((m) => m.IProject); + static readonly resolve = resolveEngagementType; declare readonly __typename: 'LanguageEngagement' | 'InternshipEngagement'; diff --git a/src/components/engagement/engagement.resolver.ts b/src/components/engagement/engagement.resolver.ts index 1bd0dbf445..baa10a1f77 100644 --- a/src/components/engagement/engagement.resolver.ts +++ b/src/components/engagement/engagement.resolver.ts @@ -60,7 +60,7 @@ export class EngagementResolver { @Loader(EngagementLoader) engagements: LoaderOf, ): Promise { const engagement = await engagements.load(key); - if (engagement.__typename !== 'LanguageEngagement') { + if (LanguageEngagement.resolve(engagement) !== LanguageEngagement) { throw new InvalidIdForTypeException(); } return engagement; diff --git a/src/components/engagement/events/engagement-created.event.ts b/src/components/engagement/events/engagement-created.event.ts index 2bb6a841b8..a47958d3f7 100644 --- a/src/components/engagement/events/engagement-created.event.ts +++ b/src/components/engagement/events/engagement-created.event.ts @@ -18,13 +18,13 @@ export class EngagementCreatedEvent { engagement: UnsecuredDto; input: CreateLanguageEngagement; } { - return this.engagement.__typename === 'LanguageEngagement'; + return LanguageEngagement.resolve(this.engagement) === LanguageEngagement; } isInternshipEngagement(): this is EngagementCreatedEvent & { engagement: UnsecuredDto; input: CreateInternshipEngagement; } { - return this.engagement.__typename === 'InternshipEngagement'; + return LanguageEngagement.resolve(this.engagement) === InternshipEngagement; } } diff --git a/src/components/engagement/events/engagement-updated.event.ts b/src/components/engagement/events/engagement-updated.event.ts index 84b21cc860..19ccfce845 100644 --- a/src/components/engagement/events/engagement-updated.event.ts +++ b/src/components/engagement/events/engagement-updated.event.ts @@ -19,13 +19,13 @@ export class EngagementUpdatedEvent { updated: UnsecuredDto; input: UpdateLanguageEngagement; } { - return this.updated.__typename === 'LanguageEngagement'; + return LanguageEngagement.resolve(this.updated) === LanguageEngagement; } isInternshipEngagement(): this is EngagementUpdatedEvent & { updated: UnsecuredDto; input: UpdateInternshipEngagement; } { - return this.updated.__typename === 'InternshipEngagement'; + return LanguageEngagement.resolve(this.updated) === InternshipEngagement; } } diff --git a/src/components/engagement/handlers/set-initial-end-date.handler.ts b/src/components/engagement/handlers/set-initial-end-date.handler.ts index 0a2c023ea7..4ba0630abb 100644 --- a/src/components/engagement/handlers/set-initial-end-date.handler.ts +++ b/src/components/engagement/handlers/set-initial-end-date.handler.ts @@ -6,7 +6,7 @@ import { ILogger, Logger, } from '~/core'; -import { EngagementStatus } from '../dto'; +import { EngagementStatus, LanguageEngagement } from '../dto'; import { EngagementRepository } from '../engagement.repository'; import { EngagementService } from '../engagement.service'; import { EngagementCreatedEvent, EngagementUpdatedEvent } from '../events'; @@ -49,7 +49,7 @@ export class SetInitialEndDate implements IEventHandler { const initialEndDate = engagement.endDate; const type = - engagement.__typename === 'LanguageEngagement' + LanguageEngagement.resolve(engagement) === LanguageEngagement ? 'Language' : 'Internship'; await this.engagementRepo[`update${type}`]( diff --git a/src/components/engagement/handlers/set-last-status-date.handler.ts b/src/components/engagement/handlers/set-last-status-date.handler.ts index c2e63e9f64..efe99da1c5 100644 --- a/src/components/engagement/handlers/set-last-status-date.handler.ts +++ b/src/components/engagement/handlers/set-last-status-date.handler.ts @@ -7,11 +7,7 @@ import { Logger, } from '~/core'; import { DatabaseService } from '~/core/database'; -import { - EngagementStatus, - InternshipEngagement, - LanguageEngagement, -} from '../dto'; +import { EngagementStatus, IEngagement } from '../dto'; import { EngagementUpdatedEvent } from '../events'; @EventsHandler(EngagementUpdatedEvent) @@ -48,10 +44,7 @@ export class SetLastStatusDate } as const; event.updated = await this.db.updateProperties({ - type: - updated.__typename === 'LanguageEngagement' - ? LanguageEngagement - : InternshipEngagement, + type: IEngagement.resolve(updated), object: updated, changes, }); From ed0696f46d5c645b81bd560032927a360e451e8c Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 4 Sep 2024 11:01:33 -0500 Subject: [PATCH 07/11] Switch to EdgeDB typenames for Engagements --- src/components/engagement/dto/engagement.dto.ts | 9 +++++---- src/components/engagement/engagement.repository.ts | 10 +++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/engagement/dto/engagement.dto.ts b/src/components/engagement/dto/engagement.dto.ts index 32c7f8a013..3315802bff 100644 --- a/src/components/engagement/dto/engagement.dto.ts +++ b/src/components/engagement/dto/engagement.dto.ts @@ -7,6 +7,7 @@ import { DateInterval, DateTimeField, DbLabel, + DBNames, IntersectTypes, parentIdMiddleware, Resource, @@ -47,7 +48,7 @@ export type AnyEngagement = MergeExclusive< const Interfaces = IntersectTypes(Resource, ChangesetAware); export const resolveEngagementType = (val: Pick) => - val.__typename === 'LanguageEngagement' + val.__typename === 'default::LanguageEngagement' ? LanguageEngagement : InternshipEngagement; @@ -65,7 +66,7 @@ class Engagement extends Interfaces { static readonly Parent = import('../../project/dto').then((m) => m.IProject); static readonly resolve = resolveEngagementType; - declare readonly __typename: 'LanguageEngagement' | 'InternshipEngagement'; + declare readonly __typename: DBNames; readonly project: LinkTo<'Project'> & Pick; @@ -155,7 +156,7 @@ export class LanguageEngagement extends Engagement { (m) => m.TranslationProject, ); - declare readonly __typename: 'LanguageEngagement'; + declare readonly __typename: DBNames; @Field(() => TranslationProject) declare readonly parent: BaseNode; @@ -196,7 +197,7 @@ export class InternshipEngagement extends Engagement { (m) => m.InternshipProject, ); - declare readonly __typename: 'InternshipEngagement'; + declare readonly __typename: DBNames; @Field(() => InternshipProject) declare readonly parent: BaseNode; diff --git a/src/components/engagement/engagement.repository.ts b/src/components/engagement/engagement.repository.ts index cad1ae0723..9c4bb4cc04 100644 --- a/src/components/engagement/engagement.repository.ts +++ b/src/components/engagement/engagement.repository.ts @@ -36,6 +36,7 @@ import { filter, FullTextIndex, INACTIVE, + listConcat, matchChangesetAndChangedProps, matchProjectSens, matchPropsAndProjectSensAndScopedRoles, @@ -169,9 +170,12 @@ export class EngagementRepository extends CommonRepository { ]) .return<{ dto: UnsecuredDto }>( merge('props', 'changedProps', { - __typename: typenameForView( - ['LanguageEngagement', 'InternshipEngagement'], - view, + __typename: listConcat( + '"default::"', + typenameForView( + ['LanguageEngagement', 'InternshipEngagement'], + view, + ), ), parent: 'project', project: { From d4c7e57dbd7ebe97e9ca4683c45d8af0eaeb80ca Mon Sep 17 00:00:00 2001 From: Bryan Nelson Date: Tue, 11 Jun 2024 14:28:59 -0400 Subject: [PATCH 08/11] Add trigger assertions for `firstScripture` in `Engagement` schema --- dbschema/engagement.esdl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dbschema/engagement.esdl b/dbschema/engagement.esdl index ee2e13c016..58c9d0346b 100644 --- a/dbschema/engagement.esdl +++ b/dbschema/engagement.esdl @@ -51,7 +51,20 @@ module default { property firstScripture := ( exists .language.firstScriptureEngagement ); - + + trigger denyDuplicateFirstScriptureBasedOnExternal after insert, update for each do ( + assert( + not __new__.firstScripture or not exists __new__.language.hasExternalFirstScripture, + message := "First scripture has already been marked as having been done externally" + ) + ); + trigger denyDuplicateFirstScriptureBasedOnOtherEngagement after insert, update for each do ( + assert( + not exists (select __new__.language.engagements filter .firstScripture), + message := "Another engagement has already been marked as having done the first scripture" + ) + ); + required lukePartnership: bool { default := false; }; From ff509e4675acb12fbfb30ddaceb3a639082fd72e Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 5 Sep 2024 08:47:08 -0500 Subject: [PATCH 09/11] [EdgeDB] Engagement repo w/ polymorphic hydration --- .../engagement/dto/engagement.dto.ts | 5 + .../engagement.edgedb.repository.ts | 130 ++++++++++++++++++ .../engagement/engagement.module.ts | 4 +- 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/components/engagement/engagement.edgedb.repository.ts diff --git a/src/components/engagement/dto/engagement.dto.ts b/src/components/engagement/dto/engagement.dto.ts index 3315802bff..627cc340a8 100644 --- a/src/components/engagement/dto/engagement.dto.ts +++ b/src/components/engagement/dto/engagement.dto.ts @@ -222,6 +222,11 @@ export class InternshipEngagement extends Engagement { export const engagementRange = (engagement: UnsecuredDto) => DateInterval.tryFrom(engagement.startDate, engagement.endDate); +export const EngagementConcretes = { + LanguageEngagement, + InternshipEngagement, +}; + declare module '~/core/resources/map' { interface ResourceMap { Engagement: typeof Engagement; diff --git a/src/components/engagement/engagement.edgedb.repository.ts b/src/components/engagement/engagement.edgedb.repository.ts new file mode 100644 index 0000000000..f4235dc01f --- /dev/null +++ b/src/components/engagement/engagement.edgedb.repository.ts @@ -0,0 +1,130 @@ +import { Injectable, Type } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { LazyGetter } from 'lazy-get-decorator'; +import { PublicOf } from '~/common'; +import { grabInstances } from '~/common/instance-maps'; +import { e, RepoFor } from '~/core/edgedb'; +import { + EngagementConcretes as ConcreteTypes, + CreateInternshipEngagement, + CreateLanguageEngagement, + IEngagement, + UpdateInternshipEngagement, + UpdateLanguageEngagement, +} from './dto'; +import { EngagementRepository } from './engagement.repository'; + +const baseHydrate = e.shape(e.Engagement, (engagement) => ({ + ...engagement['*'], + __typename: engagement.__type__.name, + project: { + id: true, + status: true, + type: true, + }, + parent: e.tuple({ + identity: engagement.project.id, + labels: e.array_agg(e.set(engagement.project.__type__.name.slice(9, null))), + properties: e.tuple({ + id: engagement.project.id, + createdAt: engagement.project.createdAt, + }), + }), + ceremony: true, + completeDate: engagement.completedDate, // TODO fix in schema +})); + +const languageExtraHydrate = { + language: true, + firstScripture: true, + lukePartnership: true, + openToInvestorVisit: true, + sentPrintingDate: true, + paratextRegistryId: true, + pnp: true, + historicGoal: true, +} as const; + +const internshipExtraHydrate = { + countryOfOrigin: true, + intern: true, + mentor: true, + position: true, + methodologies: true, + growthPlan: true, +} as const; + +const languageHydrate = e.shape(e.LanguageEngagement, (le) => ({ + ...baseHydrate(le), + __typename: le.__type__.name, + ...languageExtraHydrate, +})); + +const internshipHydrate = e.shape(e.InternshipEngagement, (ie) => ({ + ...baseHydrate(ie), + __typename: ie.__type__.name, + ...internshipExtraHydrate, +})); + +const hydrate = e.shape(e.Engagement, (engagement) => ({ + ...baseHydrate(engagement), + ...e.is(e.LanguageEngagement, languageExtraHydrate), + ...e.is(e.InternshipEngagement, internshipExtraHydrate), +})); + +export const ConcreteRepos = { + LanguageEngagement: class LanguageEngagementRepository extends RepoFor( + ConcreteTypes.LanguageEngagement, + { + hydrate: languageHydrate, + }, + ) {}, + + InternshipEngagement: class InternshipEngagementRepository extends RepoFor( + ConcreteTypes.InternshipEngagement, + { + hydrate: internshipHydrate, + }, + ) {}, +} satisfies Record; + +@Injectable() +export class EngagementEdgeDBRepository + extends RepoFor(IEngagement, { + hydrate, + omit: ['create', 'update'], + }) + implements PublicOf +{ + constructor(private readonly moduleRef: ModuleRef) { + super(); + } + + @LazyGetter() protected get concretes() { + return grabInstances(this.moduleRef, ConcreteRepos); + } + + async createLanguageEngagement(input: CreateLanguageEngagement) { + return await this.concretes.LanguageEngagement.create(input); + } + + async createInternshipEngagement(input: CreateInternshipEngagement) { + return await this.concretes.InternshipEngagement.create(input); + } + + get getActualLanguageChanges() { + return this.concretes.LanguageEngagement.getActualChanges; + } + + get getActualInternshipChanges() { + return this.concretes.InternshipEngagement.getActualChanges; + } + + async updateLanguage(input: UpdateLanguageEngagement) { + return await this.concretes.LanguageEngagement.update(input); + } + + async updateInternship(input: UpdateInternshipEngagement) { + return await this.concretes.InternshipEngagement.update(input); + } +} diff --git a/src/components/engagement/engagement.module.ts b/src/components/engagement/engagement.module.ts index d38e7acb2e..a4a8aaca21 100644 --- a/src/components/engagement/engagement.module.ts +++ b/src/components/engagement/engagement.module.ts @@ -1,4 +1,5 @@ import { forwardRef, Module } from '@nestjs/common'; +import { splitDb } from '~/core'; import { AuthorizationModule } from '../authorization/authorization.module'; import { CeremonyModule } from '../ceremony/ceremony.module'; import { FileModule } from '../file/file.module'; @@ -7,6 +8,7 @@ import { LocationModule } from '../location/location.module'; import { ProductModule } from '../product/product.module'; import { ProjectModule } from '../project/project.module'; import { EngagementStatusResolver } from './engagement-status.resolver'; +import { EngagementEdgeDBRepository } from './engagement.edgedb.repository'; import { EngagementLoader } from './engagement.loader'; import { EngagementRepository } from './engagement.repository'; import { EngagementResolver } from './engagement.resolver'; @@ -38,7 +40,7 @@ import { EngagementProductConnectionResolver } from './product-connection.resolv EngagementProductConnectionResolver, EngagementRules, EngagementService, - EngagementRepository, + splitDb(EngagementRepository, EngagementEdgeDBRepository), EngagementLoader, ...Object.values(handlers), FixNullMethodologiesMigration, From 19e827042133e1ed2e79f8c115d800c9151c3c85 Mon Sep 17 00:00:00 2001 From: Bryan Nelson Date: Thu, 5 Sep 2024 08:49:11 -0500 Subject: [PATCH 10/11] [EdgeDB] Finish Engagement queries --- .../engagement.edgedb.repository.ts | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/components/engagement/engagement.edgedb.repository.ts b/src/components/engagement/engagement.edgedb.repository.ts index f4235dc01f..9e18865a5d 100644 --- a/src/components/engagement/engagement.edgedb.repository.ts +++ b/src/components/engagement/engagement.edgedb.repository.ts @@ -1,7 +1,8 @@ import { Injectable, Type } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { LazyGetter } from 'lazy-get-decorator'; -import { PublicOf } from '~/common'; +import { difference } from 'lodash'; +import { ID, PublicOf } from '~/common'; import { grabInstances } from '~/common/instance-maps'; import { e, RepoFor } from '~/core/edgedb'; import { @@ -9,6 +10,7 @@ import { CreateInternshipEngagement, CreateLanguageEngagement, IEngagement, + EngagementStatus as Status, UpdateInternshipEngagement, UpdateLanguageEngagement, } from './dto'; @@ -77,15 +79,33 @@ export const ConcreteRepos = { ConcreteTypes.LanguageEngagement, { hydrate: languageHydrate, + omit: ['create'], }, - ) {}, + ) { + async create(input: CreateLanguageEngagement) { + const project = e.cast(e.TranslationProject, e.uuid(input.projectId)); + return await this.defaults.create({ + ...input, + projectContext: project.projectContext, + }); + } + }, InternshipEngagement: class InternshipEngagementRepository extends RepoFor( ConcreteTypes.InternshipEngagement, { hydrate: internshipHydrate, + omit: ['create'], }, - ) {}, + ) { + async create(input: CreateInternshipEngagement) { + const project = e.cast(e.InternshipProject, e.uuid(input.projectId)); + return await this.defaults.create({ + ...input, + projectContext: project.projectContext, + }); + } + }, } satisfies Record; @Injectable() @@ -127,4 +147,33 @@ export class EngagementEdgeDBRepository async updateInternship(input: UpdateInternshipEngagement) { return await this.concretes.InternshipEngagement.update(input); } + + async listAllByProjectId(projectId: ID) { + const project = e.cast(e.Project, e.uuid(projectId)); + const query = e.select(e.Engagement, (eng) => ({ + filter: e.op(eng.project, '=', project), + ...hydrate(eng), + })); + + return await this.db.run(query); + } + + async getOngoingEngagementIds(projectId: ID, excludes: Status[] = []) { + const project = e.cast(e.Project, e.uuid(projectId)); + + const ongoingExceptExclusions = e.cast( + e.Engagement.Status, + e.set(...difference([...Status.Ongoing], excludes)), + ); + + const engagements = e.select(e.Engagement, (eng) => ({ + filter: e.op( + e.op(eng.project, '=', project), + 'and', + e.op(eng.status, 'in', ongoingExceptExclusions), + ), + })); + + return await this.db.run(engagements.id); + } } From 8919871e4651016fa5a7610b150d88a85de4700f Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 5 Sep 2024 09:01:36 -0500 Subject: [PATCH 11/11] [EdgeDB] Rename completeDate to match --- dbschema/engagement.esdl | 2 +- dbschema/migrations/00005-m1yeyjr.edgeql | 21 +++++++++++++++++++ .../engagement.edgedb.repository.ts | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 dbschema/migrations/00005-m1yeyjr.edgeql diff --git a/dbschema/engagement.esdl b/dbschema/engagement.esdl index 58c9d0346b..eb2f4657c7 100644 --- a/dbschema/engagement.esdl +++ b/dbschema/engagement.esdl @@ -24,7 +24,7 @@ module default { . ({ }), }), ceremony: true, - completeDate: engagement.completedDate, // TODO fix in schema + completeDate: true, })); const languageExtraHydrate = {