From f5c01ef0cd9dc4a534280ab181c3cafbcfe4687e Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Mon, 25 Nov 2024 21:19:46 +0100 Subject: [PATCH 01/22] Expose visuals constraints through the API --- src/domain/common/visual/avatar.constants.ts | 2 - .../common/visual/visual.constraints.ts | 106 ++++++++++++++++++ src/domain/common/visual/visual.entity.ts | 13 +-- src/domain/common/visual/visual.service.ts | 35 ++---- .../configuration/config/config.interface.ts | 7 ++ .../configuration/config/config.service.ts | 3 + 6 files changed, 128 insertions(+), 38 deletions(-) delete mode 100644 src/domain/common/visual/avatar.constants.ts create mode 100644 src/domain/common/visual/visual.constraints.ts diff --git a/src/domain/common/visual/avatar.constants.ts b/src/domain/common/visual/avatar.constants.ts deleted file mode 100644 index 6d35cf84ff..0000000000 --- a/src/domain/common/visual/avatar.constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const avatarMinImageSize = 190; -export const avatarMaxImageSize = 410; diff --git a/src/domain/common/visual/visual.constraints.ts b/src/domain/common/visual/visual.constraints.ts new file mode 100644 index 0000000000..a5a4556ee4 --- /dev/null +++ b/src/domain/common/visual/visual.constraints.ts @@ -0,0 +1,106 @@ +import { VisualType } from '@common/enums/visual.type'; +import { Field, ObjectType } from '@nestjs/graphql'; + +export const VISUAL_ALLOWED_TYPES = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/svg+xml', + 'image/webp', +] as const; + +export const VISUAL_CONSTRAINTS = { + [VisualType.AVATAR]: { + minWidth: 190, + maxWidth: 410, + minHeight: 190, + maxHeight: 410, + aspectRatio: 1, + allowedTypes: VISUAL_ALLOWED_TYPES, + }, + [VisualType.BANNER]: { + minWidth: 384, + maxWidth: 1536, + minHeight: 64, + maxHeight: 256, + aspectRatio: 6, + allowedTypes: VISUAL_ALLOWED_TYPES, + }, + [VisualType.CARD]: { + minWidth: 307, + maxWidth: 410, + minHeight: 192, + maxHeight: 256, + aspectRatio: 1.6, + allowedTypes: VISUAL_ALLOWED_TYPES, + }, + [VisualType.BANNER_WIDE]: { + minWidth: 640, + maxWidth: 2560, + minHeight: 64, + maxHeight: 256, + aspectRatio: 10, + allowedTypes: VISUAL_ALLOWED_TYPES, + }, +} as const; + +@ObjectType('VisualConstraints') +export class VisualConstraints { + @Field(() => Number, { + description: 'Minimum width resolution.', + }) + minWidth!: number; + + @Field(() => Number, { + description: 'Maximum width resolution.', + }) + maxWidth!: number; + + @Field(() => Number, { + description: 'Minimum height resolution.', + }) + minHeight!: number; + + @Field(() => Number, { + description: 'Maximum height resolution.', + }) + maxHeight!: number; + + @Field(() => Number, { + description: 'Dimensions ratio width / height.', + }) + aspectRatio!: number; + + @Field(() => [String], { + description: 'Allowed file types.', + }) + allowedTypes!: typeof VISUAL_ALLOWED_TYPES; +} + +@ObjectType('VisualTypeContraints') +export class VisualTypeConstraints { + @Field(() => VisualConstraints, { + nullable: false, + description: 'Avatar visual dimensions', + }) + public Avatar: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.AVATAR]; + + @Field(() => VisualConstraints, { + nullable: false, + description: 'Banner visual dimensions', + }) + public Banner: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.BANNER]; + + @Field(() => VisualConstraints, { + nullable: false, + description: 'Card visual dimensions', + }) + public Card: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.CARD]; + + @Field(() => VisualConstraints, { + nullable: false, + description: 'BannerWide visual dimensions', + }) + public BannerWide: VisualConstraints = + VISUAL_CONSTRAINTS[VisualType.BANNER_WIDE]; +} diff --git a/src/domain/common/visual/visual.entity.ts b/src/domain/common/visual/visual.entity.ts index 4970fad7f5..59a2ed274c 100644 --- a/src/domain/common/visual/visual.entity.ts +++ b/src/domain/common/visual/visual.entity.ts @@ -3,6 +3,7 @@ import { AuthorizableEntity } from '@domain/common/entity/authorizable-entity'; import { IVisual } from './visual.interface'; import { Profile } from '@domain/common/profile/profile.entity'; import { ALT_TEXT_LENGTH, URI_LENGTH } from '@common/constants'; +import { VISUAL_ALLOWED_TYPES } from './visual.constraints'; @Entity() export class Visual extends AuthorizableEntity implements IVisual { @@ -42,21 +43,11 @@ export class Visual extends AuthorizableEntity implements IVisual { constructor() { super(); - this.allowedTypes = this.createDefaultAllowedTypes(); + this.allowedTypes = [...VISUAL_ALLOWED_TYPES]; this.minHeight = 0; this.maxHeight = 0; this.minWidth = 0; this.maxWidth = 0; this.aspectRatio = 1; } - - private createDefaultAllowedTypes(): string[] { - return [ - 'image/png', - 'image/jpeg', - 'image/jpg', - 'image/svg+xml', - 'image/webp', - ]; - } } diff --git a/src/domain/common/visual/visual.service.ts b/src/domain/common/visual/visual.service.ts index 9f0d14b229..0afa530fa2 100644 --- a/src/domain/common/visual/visual.service.ts +++ b/src/domain/common/visual/visual.service.ts @@ -14,7 +14,6 @@ import { getImageDimensions, streamToBuffer } from '@common/utils'; import { Visual } from './visual.entity'; import { IVisual } from './visual.interface'; import { DeleteVisualInput } from './dto/visual.dto.delete'; -import { avatarMinImageSize, avatarMaxImageSize } from './avatar.constants'; import { IStorageBucket } from '@domain/storage/storage-bucket/storage.bucket.interface'; import { ReadStream } from 'fs'; import { IDocument } from '@domain/storage/document/document.interface'; @@ -22,6 +21,8 @@ import { DocumentService } from '@domain/storage/document/document.service'; import { StorageBucketService } from '@domain/storage/storage-bucket/storage.bucket.service'; import { StorageUploadFailedException } from '@common/exceptions/storage/storage.upload.failed.exception'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; +import { VisualType } from '@common/enums/visual.type'; +import { VISUAL_CONSTRAINTS } from './visual.constraints'; @Injectable() export class VisualService { @@ -188,12 +189,8 @@ export class VisualService { public createVisualBanner(uri?: string): IVisual { return this.createVisual( { - name: 'banner', - minWidth: 384, - maxWidth: 1536, - minHeight: 64, - maxHeight: 256, - aspectRatio: 6, + name: VisualType.BANNER, + ...VISUAL_CONSTRAINTS[VisualType.BANNER], }, uri ); @@ -202,12 +199,8 @@ export class VisualService { public createVisualCard(uri?: string): IVisual { return this.createVisual( { - name: 'card', - minWidth: 307, - maxWidth: 410, - minHeight: 192, - maxHeight: 256, - aspectRatio: 1.6, + name: VisualType.CARD, + ...VISUAL_CONSTRAINTS[VisualType.CARD], }, uri ); @@ -216,12 +209,8 @@ export class VisualService { public createVisualBannerWide(uri?: string): IVisual { return this.createVisual( { - name: 'bannerWide', - minWidth: 640, - maxWidth: 2560, - minHeight: 64, - maxHeight: 256, - aspectRatio: 10, + name: VisualType.BANNER_WIDE, + ...VISUAL_CONSTRAINTS[VisualType.BANNER_WIDE], }, uri ); @@ -229,12 +218,8 @@ export class VisualService { public createVisualAvatar(): IVisual { return this.createVisual({ - name: 'avatar', - minWidth: avatarMinImageSize, - maxWidth: avatarMaxImageSize, - minHeight: avatarMinImageSize, - maxHeight: avatarMaxImageSize, - aspectRatio: 1, + name: VisualType.AVATAR, + ...VISUAL_CONSTRAINTS[VisualType.AVATAR], }); } } diff --git a/src/platform/configuration/config/config.interface.ts b/src/platform/configuration/config/config.interface.ts index a307779477..f48a00aece 100644 --- a/src/platform/configuration/config/config.interface.ts +++ b/src/platform/configuration/config/config.interface.ts @@ -6,6 +6,7 @@ import { IApmConfig } from './apm'; import { IGeoConfig } from './integrations'; import { IStorageConfig } from './storage'; import { IPlatformFeatureFlag } from '../feature-flag/platform.feature.flag.interface'; +import { VisualTypeConstraints } from '@domain/common/visual/visual.constraints'; @ObjectType('Config') export abstract class IConfig { @@ -40,6 +41,12 @@ export abstract class IConfig { }) apm?: IApmConfig; + @Field(() => VisualTypeConstraints, { + nullable: false, + description: 'Visual constraints for different visual types', + }) + visualTypeConstraints?: VisualTypeConstraints; + @Field(() => IStorageConfig, { nullable: false, description: 'Configuration for storage providers, e.g. file', diff --git a/src/platform/configuration/config/config.service.ts b/src/platform/configuration/config/config.service.ts index b3d62c3f29..db0540f2db 100644 --- a/src/platform/configuration/config/config.service.ts +++ b/src/platform/configuration/config/config.service.ts @@ -5,6 +5,7 @@ import { IAuthenticationProviderConfig } from './authentication/providers/authen import { IOryConfig } from './authentication/providers/ory/ory.config.interface'; import { PlatformFeatureFlagName } from '@common/enums/platform.feature.flag.name'; import { AlkemioConfig } from '@src/types'; +import { VisualTypeConstraints } from '@domain/common/visual/visual.constraints'; @Injectable() export class KonfigService { @@ -31,6 +32,7 @@ export class KonfigService { const fileConfig = this.configService.get('storage.file', { infer: true, }); + const visualTypeConstraints = new VisualTypeConstraints(); return { authentication: { providers: await this.getAuthenticationProvidersConfig(), @@ -119,6 +121,7 @@ export class KonfigService { submitPII: sentry?.submit_pii, environment: sentry?.environment, }, + visualTypeConstraints, apm: { rumEnabled: apm?.rumEnabled, endpoint: apm?.endpoint, From a90ce567d791e2cb1943286820ada33cc79da88b Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Tue, 26 Nov 2024 17:41:19 +0200 Subject: [PATCH 02/22] save storage bucket auth separately --- package-lock.json | 4 ++-- package.json | 2 +- src/domain/common/profile/profile.service.authorization.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2cff6fc9ef..1e634fe09c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "alkemio-server", - "version": "0.96.0", + "version": "0.96.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "alkemio-server", - "version": "0.96.0", + "version": "0.96.1", "license": "EUPL-1.2", "dependencies": { "@alkemio/matrix-adapter-lib": "^0.4.1", diff --git a/package.json b/package.json index e5e7352dbf..ad2a48078c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alkemio-server", - "version": "0.96.0", + "version": "0.96.1", "description": "Alkemio server, responsible for managing the shared Alkemio platform", "author": "Alkemio Foundation", "private": false, diff --git a/src/domain/common/profile/profile.service.authorization.ts b/src/domain/common/profile/profile.service.authorization.ts index a5c07433ba..a88003402f 100644 --- a/src/domain/common/profile/profile.service.authorization.ts +++ b/src/domain/common/profile/profile.service.authorization.ts @@ -126,7 +126,7 @@ export class ProfileAuthorizationService { profile.authorization ); updatedAuthorizations.push(...storageBucketAuthorizations); - - return updatedAuthorizations; + await this.authorizationPolicyService.saveAll(updatedAuthorizations); + return []; } } From 5c38e758068bc2f96d96c2f188316ae2207a1049 Mon Sep 17 00:00:00 2001 From: Valentin Date: Wed, 27 Nov 2024 14:22:03 +0200 Subject: [PATCH 03/22] coderabbit config --- .coderabbit.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 8f4dfd5d51..ad7787cc5c 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -11,7 +11,19 @@ reviews: - "!**/dist/**" # Exclude build files path_instructions: - path: "src/**/*.{ts,js}" - instructions: "Review the TypeScript/JavaScript code for NestJS best practices, dependency injection, module structure, and potential bugs. Ensure that the code adheres to TypeScript's typing system and modern standards." + instructions: | + Review the TypeScript/JavaScript code for NestJS best practices, dependency injection, module structure, and potential bugs. + + **Context Files (Do Not Review):** + - `docs/design.md` + - `src/core/error-handling/graphql.exception.filter.ts` + - `src/core/error-handling/http.exception.filter.ts` + - `src/core/error-handling/rest.error.response.ts` + - `src/core/error-handling/unhandled.exception.filter.ts` + + **Guidelines:** + - Our project uses global exception handlers (`UnhandledExceptionFilter`), so avoid suggesting additional `try/catch` blocks unless handling specific cases. + - Refer to the design overview in the context files for better understanding. - path: "src/**/*.spec.ts" instructions: "Review the unit tests, ensuring proper NestJS testing techniques (using TestingModule, mocks, etc.). Check for completeness and coverage." - path: "manifests/**/*.{yaml,yml}" From 99e1ec5f513c198e77cbd2920aff19b61589e961 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Tue, 26 Nov 2024 23:37:34 +0200 Subject: [PATCH 04/22] More stable auth --- package-lock.json | 4 ++-- package.json | 2 +- .../link/link.resolver.mutations.ts | 2 +- .../authorization.policy.service.ts | 16 ++++++++++++---- .../profile/profile.service.authorization.ts | 6 +++--- .../reference/reference.resolver.mutations.ts | 2 +- .../common/visual/visual.resolver.mutations.ts | 2 +- .../profile.documents.service.ts | 2 +- .../account/account.service.authorization.ts | 1 - .../document/document.service.authorization.ts | 7 ++++--- .../storage.aggregator.service.authorization.ts | 2 +- .../storage.bucket.service.authorization.ts | 9 +++++---- .../avatars/admin.avatarresolver.mutations.ts | 2 +- .../whiteboards/admin.whiteboard.service.ts | 2 +- 14 files changed, 34 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e634fe09c..52f309e933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "alkemio-server", - "version": "0.96.1", + "version": "0.96.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "alkemio-server", - "version": "0.96.1", + "version": "0.96.2", "license": "EUPL-1.2", "dependencies": { "@alkemio/matrix-adapter-lib": "^0.4.1", diff --git a/package.json b/package.json index ad2a48078c..c1b86d0edd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alkemio-server", - "version": "0.96.1", + "version": "0.96.2", "description": "Alkemio server, responsible for managing the shared Alkemio platform", "author": "Alkemio Foundation", "private": false, diff --git a/src/domain/collaboration/link/link.resolver.mutations.ts b/src/domain/collaboration/link/link.resolver.mutations.ts index ccf505fc7a..924347fc09 100644 --- a/src/domain/collaboration/link/link.resolver.mutations.ts +++ b/src/domain/collaboration/link/link.resolver.mutations.ts @@ -123,7 +123,7 @@ export class LinkResolverMutations { document = await this.documentService.saveDocument(document); const documentAuthorizations = - this.documentAuthorizationService.applyAuthorizationPolicy( + await this.documentAuthorizationService.applyAuthorizationPolicy( document, storageBucket.authorization ); diff --git a/src/domain/common/authorization-policy/authorization.policy.service.ts b/src/domain/common/authorization-policy/authorization.policy.service.ts index 09186ab547..53416f4c86 100644 --- a/src/domain/common/authorization-policy/authorization.policy.service.ts +++ b/src/domain/common/authorization-policy/authorization.policy.service.ts @@ -196,10 +196,18 @@ export class AuthorizationPolicyService { } async saveAll(authorizationPolicies: IAuthorizationPolicy[]): Promise { - this.logger.verbose?.( - `Saving ${authorizationPolicies.length} authorization policies`, - LogContext.AUTH - ); + if (authorizationPolicies.length > 500) + this.logger.warn?.( + `Saving ${authorizationPolicies.length} authorization policies of type ${authorizationPolicies[0].type}`, + LogContext.AUTH + ); + else { + this.logger.verbose?.( + `Saving ${authorizationPolicies.length} authorization policies`, + LogContext.AUTH + ); + } + await this.authorizationPolicyRepository.save(authorizationPolicies, { chunk: this.authChunkSize, }); diff --git a/src/domain/common/profile/profile.service.authorization.ts b/src/domain/common/profile/profile.service.authorization.ts index a88003402f..2f7f616deb 100644 --- a/src/domain/common/profile/profile.service.authorization.ts +++ b/src/domain/common/profile/profile.service.authorization.ts @@ -121,12 +121,12 @@ export class ProfileAuthorizationService { } const storageBucketAuthorizations = - this.storageBucketAuthorizationService.applyAuthorizationPolicy( + await this.storageBucketAuthorizationService.applyAuthorizationPolicy( profile.storageBucket, profile.authorization ); updatedAuthorizations.push(...storageBucketAuthorizations); - await this.authorizationPolicyService.saveAll(updatedAuthorizations); - return []; + + return updatedAuthorizations; } } diff --git a/src/domain/common/reference/reference.resolver.mutations.ts b/src/domain/common/reference/reference.resolver.mutations.ts index 0af9342d34..c60215177b 100644 --- a/src/domain/common/reference/reference.resolver.mutations.ts +++ b/src/domain/common/reference/reference.resolver.mutations.ts @@ -133,7 +133,7 @@ export class ReferenceResolverMutations { document = await this.documentService.saveDocument(document); const documentAuthorizations = - this.documentAuthorizationService.applyAuthorizationPolicy( + await this.documentAuthorizationService.applyAuthorizationPolicy( document, storageBucket.authorization ); diff --git a/src/domain/common/visual/visual.resolver.mutations.ts b/src/domain/common/visual/visual.resolver.mutations.ts index f38fd178a5..0517afec1a 100644 --- a/src/domain/common/visual/visual.resolver.mutations.ts +++ b/src/domain/common/visual/visual.resolver.mutations.ts @@ -104,7 +104,7 @@ export class VisualResolverMutations { await this.documentService.saveDocument(visualDocument); // Ensure authorization is updated const documentAuthorizations = - this.documentAuthorizationService.applyAuthorizationPolicy( + await this.documentAuthorizationService.applyAuthorizationPolicy( visualDocument, storageBucket.authorization ); diff --git a/src/domain/profile-documents/profile.documents.service.ts b/src/domain/profile-documents/profile.documents.service.ts index 77fac5f11c..f0be2775ba 100644 --- a/src/domain/profile-documents/profile.documents.service.ts +++ b/src/domain/profile-documents/profile.documents.service.ts @@ -81,7 +81,7 @@ export class ProfileDocumentsService { await this.documentService.saveDocument(newDoc); const authorizations = - this.documentAuthorizationService.applyAuthorizationPolicy( + await this.documentAuthorizationService.applyAuthorizationPolicy( newDoc, storageBucketToCheck.authorization ); diff --git a/src/domain/space/account/account.service.authorization.ts b/src/domain/space/account/account.service.authorization.ts index 72f7844e54..53de8d949e 100644 --- a/src/domain/space/account/account.service.authorization.ts +++ b/src/domain/space/account/account.service.authorization.ts @@ -99,7 +99,6 @@ export class AccountAuthorizationService { account.authorization = await this.authorizationPolicyService.save( account.authorization ); - updatedAuthorizations.push(account.authorization); const childUpdatedAuthorizations = await this.applyAuthorizationPolicyForChildEntities(account); diff --git a/src/domain/storage/document/document.service.authorization.ts b/src/domain/storage/document/document.service.authorization.ts index dc040a695d..01672c62db 100644 --- a/src/domain/storage/document/document.service.authorization.ts +++ b/src/domain/storage/document/document.service.authorization.ts @@ -15,10 +15,10 @@ import { RelationshipNotFoundException } from '@common/exceptions/relationship.n export class DocumentAuthorizationService { constructor(private authorizationPolicyService: AuthorizationPolicyService) {} - applyAuthorizationPolicy( + public async applyAuthorizationPolicy( document: IDocument, parentAuthorization: IAuthorizationPolicy | undefined - ): IAuthorizationPolicy[] { + ): Promise { if (!document.tagset || !document.tagset.authorization) { throw new RelationshipNotFoundException( `Unable to find entities required to reset auth for Document ${document.id} `, @@ -44,7 +44,8 @@ export class DocumentAuthorizationService { ); updatedAuthorizations.push(document.tagset.authorization); - return updatedAuthorizations; + await this.authorizationPolicyService.saveAll(updatedAuthorizations); + return []; } private appendCredentialRules(document: IDocument): IAuthorizationPolicy { diff --git a/src/domain/storage/storage-aggregator/storage.aggregator.service.authorization.ts b/src/domain/storage/storage-aggregator/storage.aggregator.service.authorization.ts index 8ed98839fb..718bb21ab3 100644 --- a/src/domain/storage/storage-aggregator/storage.aggregator.service.authorization.ts +++ b/src/domain/storage/storage-aggregator/storage.aggregator.service.authorization.ts @@ -52,7 +52,7 @@ export class StorageAggregatorAuthorizationService { updatedAuthorizations.push(storageAggregator.authorization); const bucketAuthorizations = - this.storageBucketAuthorizationService.applyAuthorizationPolicy( + await this.storageBucketAuthorizationService.applyAuthorizationPolicy( storageAggregator.directStorage, storageAggregator.authorization ); diff --git a/src/domain/storage/storage-bucket/storage.bucket.service.authorization.ts b/src/domain/storage/storage-bucket/storage.bucket.service.authorization.ts index 45e5198479..35d2fcebeb 100644 --- a/src/domain/storage/storage-bucket/storage.bucket.service.authorization.ts +++ b/src/domain/storage/storage-bucket/storage.bucket.service.authorization.ts @@ -19,10 +19,10 @@ export class StorageBucketAuthorizationService { private documentAuthorizationService: DocumentAuthorizationService ) {} - applyAuthorizationPolicy( + public async applyAuthorizationPolicy( storageBucket: IStorageBucket, parentAuthorization: IAuthorizationPolicy | undefined - ): IAuthorizationPolicy[] { + ): Promise { if (!storageBucket.documents) { throw new RelationshipNotFoundException( `Unable to load entities to reset auth for StorageBucket ${storageBucket.id} `, @@ -49,14 +49,15 @@ export class StorageBucketAuthorizationService { // Cascade down for (const document of storageBucket.documents) { const documentAuthorizations = - this.documentAuthorizationService.applyAuthorizationPolicy( + await this.documentAuthorizationService.applyAuthorizationPolicy( document, storageBucket.authorization ); updatedAuthorizations.push(...documentAuthorizations); } - return updatedAuthorizations; + await this.authorizationPolicyService.saveAll(updatedAuthorizations); + return []; } private appendPrivilegeRules( diff --git a/src/platform/admin/avatars/admin.avatarresolver.mutations.ts b/src/platform/admin/avatars/admin.avatarresolver.mutations.ts index b13351f296..21b62d7716 100644 --- a/src/platform/admin/avatars/admin.avatarresolver.mutations.ts +++ b/src/platform/admin/avatars/admin.avatarresolver.mutations.ts @@ -78,7 +78,7 @@ export class AdminSearchContributorsMutations { } const authorizations = - this.storageBucketAuthorizationService.applyAuthorizationPolicy( + await this.storageBucketAuthorizationService.applyAuthorizationPolicy( profile.storageBucket, profile.authorization ); diff --git a/src/platform/admin/whiteboards/admin.whiteboard.service.ts b/src/platform/admin/whiteboards/admin.whiteboard.service.ts index 1017333331..d939a297ba 100644 --- a/src/platform/admin/whiteboards/admin.whiteboard.service.ts +++ b/src/platform/admin/whiteboards/admin.whiteboard.service.ts @@ -118,7 +118,7 @@ export class AdminWhiteboardService { ); document = await this.documentService.saveDocument(document); const documentAuthorizations = - this.documentAuthorizationService.applyAuthorizationPolicy( + await this.documentAuthorizationService.applyAuthorizationPolicy( document, profile.storageBucket.authorization ); From 24624f9d99dc1c2f41fc32bc0effe2ff38e0daaa Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Mon, 2 Dec 2024 12:43:29 +0200 Subject: [PATCH 05/22] address coderabbit comments --- src/domain/common/visual/visual.constraints.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/domain/common/visual/visual.constraints.ts b/src/domain/common/visual/visual.constraints.ts index a5a4556ee4..f5fa2e840c 100644 --- a/src/domain/common/visual/visual.constraints.ts +++ b/src/domain/common/visual/visual.constraints.ts @@ -77,30 +77,30 @@ export class VisualConstraints { allowedTypes!: typeof VISUAL_ALLOWED_TYPES; } -@ObjectType('VisualTypeContraints') +@ObjectType('VisualTypeConstraints') export class VisualTypeConstraints { @Field(() => VisualConstraints, { nullable: false, description: 'Avatar visual dimensions', }) - public Avatar: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.AVATAR]; + public avatar: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.AVATAR]; @Field(() => VisualConstraints, { nullable: false, description: 'Banner visual dimensions', }) - public Banner: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.BANNER]; + public banner: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.BANNER]; @Field(() => VisualConstraints, { nullable: false, description: 'Card visual dimensions', }) - public Card: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.CARD]; + public card: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.CARD]; @Field(() => VisualConstraints, { nullable: false, description: 'BannerWide visual dimensions', }) - public BannerWide: VisualConstraints = + public bannerWide: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.BANNER_WIDE]; } From 2d32d6a5d2a455cd6bf8e7686e8b7adf8009c3fb Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 2 Dec 2024 14:57:12 +0200 Subject: [PATCH 06/22] added args input --- .../configuration/config/config.interface.ts | 6 +++--- .../configuration/config/config.module.ts | 3 ++- .../config/config.resolver.fields.ts | 21 +++++++++++++++++++ .../configuration/config/config.service.ts | 3 --- 4 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 src/platform/configuration/config/config.resolver.fields.ts diff --git a/src/platform/configuration/config/config.interface.ts b/src/platform/configuration/config/config.interface.ts index f48a00aece..bf912ad07e 100644 --- a/src/platform/configuration/config/config.interface.ts +++ b/src/platform/configuration/config/config.interface.ts @@ -6,7 +6,7 @@ import { IApmConfig } from './apm'; import { IGeoConfig } from './integrations'; import { IStorageConfig } from './storage'; import { IPlatformFeatureFlag } from '../feature-flag/platform.feature.flag.interface'; -import { VisualTypeConstraints } from '@domain/common/visual/visual.constraints'; +import { VisualConstraints } from '@domain/common/visual/visual.constraints'; @ObjectType('Config') export abstract class IConfig { @@ -41,11 +41,11 @@ export abstract class IConfig { }) apm?: IApmConfig; - @Field(() => VisualTypeConstraints, { + @Field(() => VisualConstraints, { nullable: false, description: 'Visual constraints for different visual types', }) - visualTypeConstraints?: VisualTypeConstraints; + visualConstraints?: VisualConstraints; @Field(() => IStorageConfig, { nullable: false, diff --git a/src/platform/configuration/config/config.module.ts b/src/platform/configuration/config/config.module.ts index 8806dea36f..5f8faade25 100644 --- a/src/platform/configuration/config/config.module.ts +++ b/src/platform/configuration/config/config.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { KonfigService } from './config.service'; +import { ConfigurationResolverFields } from './config.resolver.fields'; @Module({ - providers: [KonfigService], + providers: [KonfigService, ConfigurationResolverFields], exports: [KonfigService], }) export class KonfigModule {} diff --git a/src/platform/configuration/config/config.resolver.fields.ts b/src/platform/configuration/config/config.resolver.fields.ts new file mode 100644 index 0000000000..10d5a3d1f9 --- /dev/null +++ b/src/platform/configuration/config/config.resolver.fields.ts @@ -0,0 +1,21 @@ +import { + VisualConstraints, + VISUAL_CONSTRAINTS, +} from '@domain/common/visual/visual.constraints'; +import { Args, ResolveField } from '@nestjs/graphql'; +import { Resolver } from '@nestjs/graphql'; +import { IConfig } from './config.interface'; +import { VisualType } from '@common/enums/visual.type'; + +@Resolver(() => IConfig) +export class ConfigurationResolverFields { + @ResolveField(() => VisualConstraints, { + nullable: false, + description: 'Visual constraints for the given type', + }) + visualConstraints( + @Args('type', { type: () => VisualType }) visualTypeInput: VisualType + ): VisualConstraints { + return VISUAL_CONSTRAINTS[visualTypeInput]; + } +} diff --git a/src/platform/configuration/config/config.service.ts b/src/platform/configuration/config/config.service.ts index db0540f2db..b3d62c3f29 100644 --- a/src/platform/configuration/config/config.service.ts +++ b/src/platform/configuration/config/config.service.ts @@ -5,7 +5,6 @@ import { IAuthenticationProviderConfig } from './authentication/providers/authen import { IOryConfig } from './authentication/providers/ory/ory.config.interface'; import { PlatformFeatureFlagName } from '@common/enums/platform.feature.flag.name'; import { AlkemioConfig } from '@src/types'; -import { VisualTypeConstraints } from '@domain/common/visual/visual.constraints'; @Injectable() export class KonfigService { @@ -32,7 +31,6 @@ export class KonfigService { const fileConfig = this.configService.get('storage.file', { infer: true, }); - const visualTypeConstraints = new VisualTypeConstraints(); return { authentication: { providers: await this.getAuthenticationProvidersConfig(), @@ -121,7 +119,6 @@ export class KonfigService { submitPII: sentry?.submit_pii, environment: sentry?.environment, }, - visualTypeConstraints, apm: { rumEnabled: apm?.rumEnabled, endpoint: apm?.endpoint, From d81e723a62da631d2766dcfc33ff887131f423f9 Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Mon, 2 Dec 2024 14:29:39 +0100 Subject: [PATCH 07/22] Address CR comments, cleaned up and renamed to defaultVisualConstraints --- .../common/visual/visual.constraints.ts | 30 +------------------ src/domain/common/visual/visual.service.ts | 10 +++---- .../configuration/config/config.interface.ts | 7 ----- .../config/config.resolver.fields.ts | 6 ++-- 4 files changed, 9 insertions(+), 44 deletions(-) diff --git a/src/domain/common/visual/visual.constraints.ts b/src/domain/common/visual/visual.constraints.ts index f5fa2e840c..4052d391a9 100644 --- a/src/domain/common/visual/visual.constraints.ts +++ b/src/domain/common/visual/visual.constraints.ts @@ -9,7 +9,7 @@ export const VISUAL_ALLOWED_TYPES = [ 'image/webp', ] as const; -export const VISUAL_CONSTRAINTS = { +export const DEFAULT_VISUAL_CONSTRAINTS = { [VisualType.AVATAR]: { minWidth: 190, maxWidth: 410, @@ -76,31 +76,3 @@ export class VisualConstraints { }) allowedTypes!: typeof VISUAL_ALLOWED_TYPES; } - -@ObjectType('VisualTypeConstraints') -export class VisualTypeConstraints { - @Field(() => VisualConstraints, { - nullable: false, - description: 'Avatar visual dimensions', - }) - public avatar: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.AVATAR]; - - @Field(() => VisualConstraints, { - nullable: false, - description: 'Banner visual dimensions', - }) - public banner: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.BANNER]; - - @Field(() => VisualConstraints, { - nullable: false, - description: 'Card visual dimensions', - }) - public card: VisualConstraints = VISUAL_CONSTRAINTS[VisualType.CARD]; - - @Field(() => VisualConstraints, { - nullable: false, - description: 'BannerWide visual dimensions', - }) - public bannerWide: VisualConstraints = - VISUAL_CONSTRAINTS[VisualType.BANNER_WIDE]; -} diff --git a/src/domain/common/visual/visual.service.ts b/src/domain/common/visual/visual.service.ts index 0afa530fa2..bdcee49254 100644 --- a/src/domain/common/visual/visual.service.ts +++ b/src/domain/common/visual/visual.service.ts @@ -22,7 +22,7 @@ import { StorageBucketService } from '@domain/storage/storage-bucket/storage.buc import { StorageUploadFailedException } from '@common/exceptions/storage/storage.upload.failed.exception'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { VisualType } from '@common/enums/visual.type'; -import { VISUAL_CONSTRAINTS } from './visual.constraints'; +import { DEFAULT_VISUAL_CONSTRAINTS } from './visual.constraints'; @Injectable() export class VisualService { @@ -190,7 +190,7 @@ export class VisualService { return this.createVisual( { name: VisualType.BANNER, - ...VISUAL_CONSTRAINTS[VisualType.BANNER], + ...DEFAULT_VISUAL_CONSTRAINTS[VisualType.BANNER], }, uri ); @@ -200,7 +200,7 @@ export class VisualService { return this.createVisual( { name: VisualType.CARD, - ...VISUAL_CONSTRAINTS[VisualType.CARD], + ...DEFAULT_VISUAL_CONSTRAINTS[VisualType.CARD], }, uri ); @@ -210,7 +210,7 @@ export class VisualService { return this.createVisual( { name: VisualType.BANNER_WIDE, - ...VISUAL_CONSTRAINTS[VisualType.BANNER_WIDE], + ...DEFAULT_VISUAL_CONSTRAINTS[VisualType.BANNER_WIDE], }, uri ); @@ -219,7 +219,7 @@ export class VisualService { public createVisualAvatar(): IVisual { return this.createVisual({ name: VisualType.AVATAR, - ...VISUAL_CONSTRAINTS[VisualType.AVATAR], + ...DEFAULT_VISUAL_CONSTRAINTS[VisualType.AVATAR], }); } } diff --git a/src/platform/configuration/config/config.interface.ts b/src/platform/configuration/config/config.interface.ts index bf912ad07e..a307779477 100644 --- a/src/platform/configuration/config/config.interface.ts +++ b/src/platform/configuration/config/config.interface.ts @@ -6,7 +6,6 @@ import { IApmConfig } from './apm'; import { IGeoConfig } from './integrations'; import { IStorageConfig } from './storage'; import { IPlatformFeatureFlag } from '../feature-flag/platform.feature.flag.interface'; -import { VisualConstraints } from '@domain/common/visual/visual.constraints'; @ObjectType('Config') export abstract class IConfig { @@ -41,12 +40,6 @@ export abstract class IConfig { }) apm?: IApmConfig; - @Field(() => VisualConstraints, { - nullable: false, - description: 'Visual constraints for different visual types', - }) - visualConstraints?: VisualConstraints; - @Field(() => IStorageConfig, { nullable: false, description: 'Configuration for storage providers, e.g. file', diff --git a/src/platform/configuration/config/config.resolver.fields.ts b/src/platform/configuration/config/config.resolver.fields.ts index 10d5a3d1f9..89419c7ffc 100644 --- a/src/platform/configuration/config/config.resolver.fields.ts +++ b/src/platform/configuration/config/config.resolver.fields.ts @@ -1,6 +1,6 @@ import { VisualConstraints, - VISUAL_CONSTRAINTS, + DEFAULT_VISUAL_CONSTRAINTS, } from '@domain/common/visual/visual.constraints'; import { Args, ResolveField } from '@nestjs/graphql'; import { Resolver } from '@nestjs/graphql'; @@ -13,9 +13,9 @@ export class ConfigurationResolverFields { nullable: false, description: 'Visual constraints for the given type', }) - visualConstraints( + defaultVisualTypeConstraints( @Args('type', { type: () => VisualType }) visualTypeInput: VisualType ): VisualConstraints { - return VISUAL_CONSTRAINTS[visualTypeInput]; + return DEFAULT_VISUAL_CONSTRAINTS[visualTypeInput]; } } From 7340096a2d46fe76215e14acbf6019e77225f5f4 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 2 Dec 2024 15:38:41 +0200 Subject: [PATCH 08/22] Expose visuals constraints through the API (with args) (#4743) * Expose visuals constraints through the API * address coderabbit comments * added args input * Address CR comments, cleaned up and renamed to defaultVisualConstraints --------- Co-authored-by: Carlos Cano Co-authored-by: Bobby Kolev --- src/domain/common/visual/avatar.constants.ts | 2 - .../common/visual/visual.constraints.ts | 78 +++++++++++++++++++ src/domain/common/visual/visual.entity.ts | 13 +--- src/domain/common/visual/visual.service.ts | 35 +++------ .../configuration/config/config.module.ts | 3 +- .../config/config.resolver.fields.ts | 21 +++++ 6 files changed, 113 insertions(+), 39 deletions(-) delete mode 100644 src/domain/common/visual/avatar.constants.ts create mode 100644 src/domain/common/visual/visual.constraints.ts create mode 100644 src/platform/configuration/config/config.resolver.fields.ts diff --git a/src/domain/common/visual/avatar.constants.ts b/src/domain/common/visual/avatar.constants.ts deleted file mode 100644 index 6d35cf84ff..0000000000 --- a/src/domain/common/visual/avatar.constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const avatarMinImageSize = 190; -export const avatarMaxImageSize = 410; diff --git a/src/domain/common/visual/visual.constraints.ts b/src/domain/common/visual/visual.constraints.ts new file mode 100644 index 0000000000..4052d391a9 --- /dev/null +++ b/src/domain/common/visual/visual.constraints.ts @@ -0,0 +1,78 @@ +import { VisualType } from '@common/enums/visual.type'; +import { Field, ObjectType } from '@nestjs/graphql'; + +export const VISUAL_ALLOWED_TYPES = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/svg+xml', + 'image/webp', +] as const; + +export const DEFAULT_VISUAL_CONSTRAINTS = { + [VisualType.AVATAR]: { + minWidth: 190, + maxWidth: 410, + minHeight: 190, + maxHeight: 410, + aspectRatio: 1, + allowedTypes: VISUAL_ALLOWED_TYPES, + }, + [VisualType.BANNER]: { + minWidth: 384, + maxWidth: 1536, + minHeight: 64, + maxHeight: 256, + aspectRatio: 6, + allowedTypes: VISUAL_ALLOWED_TYPES, + }, + [VisualType.CARD]: { + minWidth: 307, + maxWidth: 410, + minHeight: 192, + maxHeight: 256, + aspectRatio: 1.6, + allowedTypes: VISUAL_ALLOWED_TYPES, + }, + [VisualType.BANNER_WIDE]: { + minWidth: 640, + maxWidth: 2560, + minHeight: 64, + maxHeight: 256, + aspectRatio: 10, + allowedTypes: VISUAL_ALLOWED_TYPES, + }, +} as const; + +@ObjectType('VisualConstraints') +export class VisualConstraints { + @Field(() => Number, { + description: 'Minimum width resolution.', + }) + minWidth!: number; + + @Field(() => Number, { + description: 'Maximum width resolution.', + }) + maxWidth!: number; + + @Field(() => Number, { + description: 'Minimum height resolution.', + }) + minHeight!: number; + + @Field(() => Number, { + description: 'Maximum height resolution.', + }) + maxHeight!: number; + + @Field(() => Number, { + description: 'Dimensions ratio width / height.', + }) + aspectRatio!: number; + + @Field(() => [String], { + description: 'Allowed file types.', + }) + allowedTypes!: typeof VISUAL_ALLOWED_TYPES; +} diff --git a/src/domain/common/visual/visual.entity.ts b/src/domain/common/visual/visual.entity.ts index 4970fad7f5..59a2ed274c 100644 --- a/src/domain/common/visual/visual.entity.ts +++ b/src/domain/common/visual/visual.entity.ts @@ -3,6 +3,7 @@ import { AuthorizableEntity } from '@domain/common/entity/authorizable-entity'; import { IVisual } from './visual.interface'; import { Profile } from '@domain/common/profile/profile.entity'; import { ALT_TEXT_LENGTH, URI_LENGTH } from '@common/constants'; +import { VISUAL_ALLOWED_TYPES } from './visual.constraints'; @Entity() export class Visual extends AuthorizableEntity implements IVisual { @@ -42,21 +43,11 @@ export class Visual extends AuthorizableEntity implements IVisual { constructor() { super(); - this.allowedTypes = this.createDefaultAllowedTypes(); + this.allowedTypes = [...VISUAL_ALLOWED_TYPES]; this.minHeight = 0; this.maxHeight = 0; this.minWidth = 0; this.maxWidth = 0; this.aspectRatio = 1; } - - private createDefaultAllowedTypes(): string[] { - return [ - 'image/png', - 'image/jpeg', - 'image/jpg', - 'image/svg+xml', - 'image/webp', - ]; - } } diff --git a/src/domain/common/visual/visual.service.ts b/src/domain/common/visual/visual.service.ts index 9f0d14b229..bdcee49254 100644 --- a/src/domain/common/visual/visual.service.ts +++ b/src/domain/common/visual/visual.service.ts @@ -14,7 +14,6 @@ import { getImageDimensions, streamToBuffer } from '@common/utils'; import { Visual } from './visual.entity'; import { IVisual } from './visual.interface'; import { DeleteVisualInput } from './dto/visual.dto.delete'; -import { avatarMinImageSize, avatarMaxImageSize } from './avatar.constants'; import { IStorageBucket } from '@domain/storage/storage-bucket/storage.bucket.interface'; import { ReadStream } from 'fs'; import { IDocument } from '@domain/storage/document/document.interface'; @@ -22,6 +21,8 @@ import { DocumentService } from '@domain/storage/document/document.service'; import { StorageBucketService } from '@domain/storage/storage-bucket/storage.bucket.service'; import { StorageUploadFailedException } from '@common/exceptions/storage/storage.upload.failed.exception'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; +import { VisualType } from '@common/enums/visual.type'; +import { DEFAULT_VISUAL_CONSTRAINTS } from './visual.constraints'; @Injectable() export class VisualService { @@ -188,12 +189,8 @@ export class VisualService { public createVisualBanner(uri?: string): IVisual { return this.createVisual( { - name: 'banner', - minWidth: 384, - maxWidth: 1536, - minHeight: 64, - maxHeight: 256, - aspectRatio: 6, + name: VisualType.BANNER, + ...DEFAULT_VISUAL_CONSTRAINTS[VisualType.BANNER], }, uri ); @@ -202,12 +199,8 @@ export class VisualService { public createVisualCard(uri?: string): IVisual { return this.createVisual( { - name: 'card', - minWidth: 307, - maxWidth: 410, - minHeight: 192, - maxHeight: 256, - aspectRatio: 1.6, + name: VisualType.CARD, + ...DEFAULT_VISUAL_CONSTRAINTS[VisualType.CARD], }, uri ); @@ -216,12 +209,8 @@ export class VisualService { public createVisualBannerWide(uri?: string): IVisual { return this.createVisual( { - name: 'bannerWide', - minWidth: 640, - maxWidth: 2560, - minHeight: 64, - maxHeight: 256, - aspectRatio: 10, + name: VisualType.BANNER_WIDE, + ...DEFAULT_VISUAL_CONSTRAINTS[VisualType.BANNER_WIDE], }, uri ); @@ -229,12 +218,8 @@ export class VisualService { public createVisualAvatar(): IVisual { return this.createVisual({ - name: 'avatar', - minWidth: avatarMinImageSize, - maxWidth: avatarMaxImageSize, - minHeight: avatarMinImageSize, - maxHeight: avatarMaxImageSize, - aspectRatio: 1, + name: VisualType.AVATAR, + ...DEFAULT_VISUAL_CONSTRAINTS[VisualType.AVATAR], }); } } diff --git a/src/platform/configuration/config/config.module.ts b/src/platform/configuration/config/config.module.ts index 8806dea36f..5f8faade25 100644 --- a/src/platform/configuration/config/config.module.ts +++ b/src/platform/configuration/config/config.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { KonfigService } from './config.service'; +import { ConfigurationResolverFields } from './config.resolver.fields'; @Module({ - providers: [KonfigService], + providers: [KonfigService, ConfigurationResolverFields], exports: [KonfigService], }) export class KonfigModule {} diff --git a/src/platform/configuration/config/config.resolver.fields.ts b/src/platform/configuration/config/config.resolver.fields.ts new file mode 100644 index 0000000000..89419c7ffc --- /dev/null +++ b/src/platform/configuration/config/config.resolver.fields.ts @@ -0,0 +1,21 @@ +import { + VisualConstraints, + DEFAULT_VISUAL_CONSTRAINTS, +} from '@domain/common/visual/visual.constraints'; +import { Args, ResolveField } from '@nestjs/graphql'; +import { Resolver } from '@nestjs/graphql'; +import { IConfig } from './config.interface'; +import { VisualType } from '@common/enums/visual.type'; + +@Resolver(() => IConfig) +export class ConfigurationResolverFields { + @ResolveField(() => VisualConstraints, { + nullable: false, + description: 'Visual constraints for the given type', + }) + defaultVisualTypeConstraints( + @Args('type', { type: () => VisualType }) visualTypeInput: VisualType + ): VisualConstraints { + return DEFAULT_VISUAL_CONSTRAINTS[visualTypeInput]; + } +} From e6d34ce8861bdce15fdad36cd4a82cd2d092c981 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 2 Dec 2024 16:36:52 +0200 Subject: [PATCH 09/22] Updated coderabbit configuration --- .coderabbit.yaml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index ad7787cc5c..58907af1a9 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -15,15 +15,22 @@ reviews: Review the TypeScript/JavaScript code for NestJS best practices, dependency injection, module structure, and potential bugs. **Context Files (Do Not Review):** - - `docs/design.md` - - `src/core/error-handling/graphql.exception.filter.ts` - - `src/core/error-handling/http.exception.filter.ts` - - `src/core/error-handling/rest.error.response.ts` - - `src/core/error-handling/unhandled.exception.filter.ts` + - `docs/Design.md` - Design overview of the project + - `docs/Pagination.md` - Pagination design overview + - `docs/Developing.md` - Development setup overview + - `docs/graphql-typeorm-usage.md` - overview of GraphQL and TypeORM usage and how they are used together with NestJS in the project + - `docs/database-definitions.md` - guidelines for creating TypeORM entity defnitions + - `src/core/error-handling/graphql.exception.filter.ts` - GraphQL error handling + - `src/core/error-handling/http.exception.filter.ts` - HTTP error handling + - `src/core/error-handling/rest.error.response.ts` - REST error response + - `src/core/error-handling/unhandled.exception.filter.ts` - Global exception handler **Guidelines:** - Our project uses global exception handlers (`UnhandledExceptionFilter`), so avoid suggesting additional `try/catch` blocks unless handling specific cases. + - Use NestJS latest documentation from `https://docs.nestjs.com/` for reference on NestJS best practices. + - Use TypeORM latest documentation from `https://typeorm.io/` for reference on TypeORM best practices. - Refer to the design overview in the context files for better understanding. + - path: "src/**/*.spec.ts" instructions: "Review the unit tests, ensuring proper NestJS testing techniques (using TestingModule, mocks, etc.). Check for completeness and coverage." - path: "manifests/**/*.{yaml,yml}" From 1122914ba0a3205e66d0d78880aeb4f69f0fada4 Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Tue, 3 Dec 2024 08:05:36 +0100 Subject: [PATCH 10/22] pseudocode --- .../template.applier.service.ts | 1 + .../dto/collaboration.template.dto.update.ts | 36 ++++++++++++++++++ .../template/template.resolver.mutations.ts | 38 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/domain/template/template/dto/collaboration.template.dto.update.ts diff --git a/src/domain/template/template-applier/template.applier.service.ts b/src/domain/template/template-applier/template.applier.service.ts index 60730bbf0c..bf169107da 100644 --- a/src/domain/template/template-applier/template.applier.service.ts +++ b/src/domain/template/template-applier/template.applier.service.ts @@ -82,6 +82,7 @@ export class TemplateApplierService { return await this.collaborationService.save(targetCollaboration); } } + private ensureCalloutsInValidGroupsAndStates( targetCollaboration: ICollaboration ) { diff --git a/src/domain/template/template/dto/collaboration.template.dto.update.ts b/src/domain/template/template/dto/collaboration.template.dto.update.ts new file mode 100644 index 0000000000..70a10e0a4c --- /dev/null +++ b/src/domain/template/template/dto/collaboration.template.dto.update.ts @@ -0,0 +1,36 @@ +import { VERY_LONG_TEXT_LENGTH } from '@common/constants/entity.field.length.constants'; +import { UpdateBaseAlkemioInput } from '@domain/common/entity/base-entity/dto/base.alkemio.dto.update'; +import { UpdateProfileInput } from '@domain/common/profile/dto/profile.dto.update'; +import { Markdown } from '@domain/common/scalars/scalar.markdown'; +import { WhiteboardContent } from '@domain/common/scalars/scalar.whiteboard.content'; +import { Field, InputType } from '@nestjs/graphql'; +import { Type } from 'class-transformer'; +import { IsOptional, MaxLength, ValidateNested } from 'class-validator'; + +@InputType() +export class UpdateTemplateInput extends UpdateBaseAlkemioInput { + @Field(() => UpdateProfileInput, { + nullable: true, + description: 'The Profile of the Template.', + }) + @IsOptional() + @ValidateNested() + @Type(() => UpdateProfileInput) + profile?: UpdateProfileInput; + + @Field(() => Markdown, { + nullable: true, + description: + 'The default description to be pre-filled when users create Posts based on this template.', + }) + @IsOptional() + @MaxLength(VERY_LONG_TEXT_LENGTH) + postDefaultDescription!: string; + + @Field(() => WhiteboardContent, { + nullable: true, + description: 'The new content to be used.', + }) + @IsOptional() + whiteboardContent?: string; +} diff --git a/src/domain/template/template/template.resolver.mutations.ts b/src/domain/template/template/template.resolver.mutations.ts index fea6fc848d..11f2992694 100644 --- a/src/domain/template/template/template.resolver.mutations.ts +++ b/src/domain/template/template/template.resolver.mutations.ts @@ -12,6 +12,7 @@ import { DeleteTemplateInput } from './dto/template.dto.delete'; import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; import { LogContext } from '@common/enums/logging.context'; import { ValidationException } from '@common/exceptions/validation.exception'; +import { template } from 'lodash'; @Resolver() export class TemplateResolverMutations { @@ -45,6 +46,43 @@ export class TemplateResolverMutations { return await this.templateService.updateTemplate(template, updateData); } + @UseGuards(GraphqlGuard) + @Mutation(() => ITemplate, { + description: 'Updates the specified Template.', + }) + async updateCollaborationTemplate( + @CurrentUser() agentInfo: AgentInfo, + @Args('updateData') + updateData: UpdateCollaborationTemplateInput + ): Promise { + const template = await this.templateService.getTemplateOrFail( + updateData.ID, + { + relations: { profile: true }, + } + ); + await this.authorizationService.grantAccessOrFail( + agentInfo, + template.authorization, + AuthorizationPrivilege.UPDATE, + `update template: ${template.id}` + ); + return await this.templateService.updateTemplate(template, updateData); + } + /* + updateCOllaborationTemplate{ + update privilege on the template + read privilege on the source + + check that it is collab template + delete all the callouts on the template + repla + clone callouts from the source collaboration + + logic in template updateCollaborationFromTemplate + } + */ + @UseGuards(GraphqlGuard) @Mutation(() => ITemplate, { description: 'Deletes the specified Template.', From 4e6c89bfccc2cbc24c84ae1c64d1862cd80c8a7a Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Tue, 3 Dec 2024 14:00:36 +0100 Subject: [PATCH 11/22] Coding with Neil --- .../template.applier.module.ts | 10 +- .../template.applier.service.ts | 84 +------------ .../dto/collaboration.template.dto.update.ts | 36 ------ .../template.dto.update.from.collaboration.ts | 18 +++ .../template/template/template.module.ts | 4 + .../template/template.resolver.mutations.ts | 43 ++++--- .../template/template/template.service.ts | 110 ++++++++++++++++++ 7 files changed, 163 insertions(+), 142 deletions(-) delete mode 100644 src/domain/template/template/dto/collaboration.template.dto.update.ts create mode 100644 src/domain/template/template/dto/template.dto.update.from.collaboration.ts diff --git a/src/domain/template/template-applier/template.applier.module.ts b/src/domain/template/template-applier/template.applier.module.ts index 7304a8995d..bf3b06cfc0 100644 --- a/src/domain/template/template-applier/template.applier.module.ts +++ b/src/domain/template/template-applier/template.applier.module.ts @@ -4,12 +4,8 @@ import { TemplateApplierResolverMutations } from './template.applier.resolver.mu import { TemplateModule } from '../template/template.module'; import { AuthorizationModule } from '@core/authorization/authorization.module'; import { CollaborationModule } from '@domain/collaboration/collaboration/collaboration.module'; -import { InnovationFlowModule } from '@domain/collaboration/innovation-flow/innovation.flow.module'; -import { InputCreatorModule } from '@services/api/input-creator/input.creator.module'; -import { StorageAggregatorResolverModule } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.module'; -import { NamingModule } from '@services/infrastructure/naming/naming.module'; -import { CalloutModule } from '@domain/collaboration/callout/callout.module'; import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; +import { CalloutModule } from '@domain/collaboration/callout/callout.module'; @Module({ imports: [ @@ -17,11 +13,7 @@ import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/a AuthorizationModule, TemplateModule, CollaborationModule, - InnovationFlowModule, - InputCreatorModule, - StorageAggregatorResolverModule, CalloutModule, - NamingModule, ], providers: [TemplateApplierService, TemplateApplierResolverMutations], exports: [], diff --git a/src/domain/template/template-applier/template.applier.service.ts b/src/domain/template/template-applier/template.applier.service.ts index bf169107da..70cba62457 100644 --- a/src/domain/template/template-applier/template.applier.service.ts +++ b/src/domain/template/template-applier/template.applier.service.ts @@ -1,23 +1,14 @@ import { Injectable } from '@nestjs/common'; import { UpdateCollaborationFromTemplateInput } from './dto/template.applier.dto.update.collaboration'; import { TemplateService } from '../template/template.service'; -import { InnovationFlowService } from '@domain/collaboration/innovation-flow/innovation.flow.service'; import { ICollaboration } from '@domain/collaboration/collaboration/collaboration.interface'; import { CollaborationService } from '@domain/collaboration/collaboration/collaboration.service'; -import { RelationshipNotFoundException } from '@common/exceptions'; -import { LogContext } from '@common/enums/logging.context'; -import { InputCreatorService } from '@services/api/input-creator/input.creator.service'; -import { TagsetReservedName } from '@common/enums/tagset.reserved.name'; -import { StorageAggregatorResolverService } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service'; @Injectable() export class TemplateApplierService { constructor( private templateService: TemplateService, - private innovationFlowService: InnovationFlowService, - private collaborationService: CollaborationService, - private inputCreatorService: InputCreatorService, - private storageAggregatorResolverService: StorageAggregatorResolverService + private collaborationService: CollaborationService ) {} async updateCollaborationFromTemplate( @@ -38,74 +29,11 @@ export class TemplateApplierService { }, } ); - if ( - !collaborationFromTemplate.innovationFlow || - !targetCollaboration.innovationFlow || - !targetCollaboration.callouts || - !targetCollaboration.authorization - ) { - throw new RelationshipNotFoundException( - `Template cannot be applied on uninitialized collaboration templateId:'${collaborationTemplate.id}' TargetCollaboration.id='${targetCollaboration.id}'`, - LogContext.TEMPLATES - ); - } - const newStatesStr = collaborationFromTemplate.innovationFlow.states; - targetCollaboration.innovationFlow = - await this.innovationFlowService.updateInnovationFlowStates( - targetCollaboration.innovationFlow.id, - newStatesStr - ); - - const storageAggregator = - await this.storageAggregatorResolverService.getStorageAggregatorForCollaboration( - targetCollaboration.id - ); - if (updateData.addCallouts) { - const calloutsFromTemplate = - await this.inputCreatorService.buildCreateCalloutInputsFromCallouts( - collaborationFromTemplate.callouts ?? [] - ); - - const newCallouts = await this.collaborationService.addCallouts( - targetCollaboration, - calloutsFromTemplate, - storageAggregator, - userID - ); - targetCollaboration.callouts?.push(...newCallouts); - this.ensureCalloutsInValidGroupsAndStates(targetCollaboration); - - // Need to save before applying authorization policy to get the callout ids - return await this.collaborationService.save(targetCollaboration); - } else { - this.ensureCalloutsInValidGroupsAndStates(targetCollaboration); - return await this.collaborationService.save(targetCollaboration); - } - } - - private ensureCalloutsInValidGroupsAndStates( - targetCollaboration: ICollaboration - ) { - // We don't have callouts or we don't have innovationFlow, can't do anything - if (!targetCollaboration.innovationFlow || !targetCollaboration.callouts) { - throw new RelationshipNotFoundException( - `Unable to load Callouts or InnovationFlow ${targetCollaboration.id} `, - LogContext.TEMPLATES - ); - } - - const validGroupNames = - targetCollaboration.tagsetTemplateSet?.tagsetTemplates.find( - tagset => tagset.name === TagsetReservedName.CALLOUT_GROUP - )?.allowedValues; - const validFlowStates = this.innovationFlowService - .getStates(targetCollaboration.innovationFlow) - ?.map(state => state.displayName); - - this.collaborationService.moveCalloutsToDefaultGroupAndState( - validGroupNames ?? [], - validFlowStates ?? [], - targetCollaboration.callouts + return this.templateService.updateCollaborationFromCollaboration( + collaborationFromTemplate, + targetCollaboration, + updateData.addCallouts, + userID ); } } diff --git a/src/domain/template/template/dto/collaboration.template.dto.update.ts b/src/domain/template/template/dto/collaboration.template.dto.update.ts deleted file mode 100644 index 70a10e0a4c..0000000000 --- a/src/domain/template/template/dto/collaboration.template.dto.update.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { VERY_LONG_TEXT_LENGTH } from '@common/constants/entity.field.length.constants'; -import { UpdateBaseAlkemioInput } from '@domain/common/entity/base-entity/dto/base.alkemio.dto.update'; -import { UpdateProfileInput } from '@domain/common/profile/dto/profile.dto.update'; -import { Markdown } from '@domain/common/scalars/scalar.markdown'; -import { WhiteboardContent } from '@domain/common/scalars/scalar.whiteboard.content'; -import { Field, InputType } from '@nestjs/graphql'; -import { Type } from 'class-transformer'; -import { IsOptional, MaxLength, ValidateNested } from 'class-validator'; - -@InputType() -export class UpdateTemplateInput extends UpdateBaseAlkemioInput { - @Field(() => UpdateProfileInput, { - nullable: true, - description: 'The Profile of the Template.', - }) - @IsOptional() - @ValidateNested() - @Type(() => UpdateProfileInput) - profile?: UpdateProfileInput; - - @Field(() => Markdown, { - nullable: true, - description: - 'The default description to be pre-filled when users create Posts based on this template.', - }) - @IsOptional() - @MaxLength(VERY_LONG_TEXT_LENGTH) - postDefaultDescription!: string; - - @Field(() => WhiteboardContent, { - nullable: true, - description: 'The new content to be used.', - }) - @IsOptional() - whiteboardContent?: string; -} diff --git a/src/domain/template/template/dto/template.dto.update.from.collaboration.ts b/src/domain/template/template/dto/template.dto.update.from.collaboration.ts new file mode 100644 index 0000000000..8cf11788e3 --- /dev/null +++ b/src/domain/template/template/dto/template.dto.update.from.collaboration.ts @@ -0,0 +1,18 @@ +import { UUID } from '@domain/common/scalars'; +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class UpdateTemplateFromCollaborationInput { + @Field(() => UUID, { + nullable: false, + description: 'The ID of the Template.', + }) + templateID!: string; + + @Field(() => UUID, { + nullable: false, + description: + 'The Collaboration whose content should be copied to this Template.', + }) + collaborationID!: string; +} diff --git a/src/domain/template/template/template.module.ts b/src/domain/template/template/template.module.ts index 6ac72e1d06..f918cc5dc9 100644 --- a/src/domain/template/template/template.module.ts +++ b/src/domain/template/template/template.module.ts @@ -13,6 +13,8 @@ import { WhiteboardModule } from '@domain/common/whiteboard'; import { TemplateResolverFields } from './template.resolver.fields'; import { InnovationFlowModule } from '@domain/collaboration/innovation-flow/innovation.flow.module'; import { CollaborationModule } from '@domain/collaboration/collaboration/collaboration.module'; +import { InputCreatorModule } from '@services/api/input-creator/input.creator.module'; +import { StorageAggregatorResolverModule } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.module'; @Module({ imports: [ @@ -23,6 +25,8 @@ import { CollaborationModule } from '@domain/collaboration/collaboration/collabo CalloutModule, WhiteboardModule, InnovationFlowModule, + InputCreatorModule, + StorageAggregatorResolverModule, CollaborationModule, TypeOrmModule.forFeature([Template]), ], diff --git a/src/domain/template/template/template.resolver.mutations.ts b/src/domain/template/template/template.resolver.mutations.ts index 11f2992694..abcaccb365 100644 --- a/src/domain/template/template/template.resolver.mutations.ts +++ b/src/domain/template/template/template.resolver.mutations.ts @@ -12,12 +12,14 @@ import { DeleteTemplateInput } from './dto/template.dto.delete'; import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; import { LogContext } from '@common/enums/logging.context'; import { ValidationException } from '@common/exceptions/validation.exception'; -import { template } from 'lodash'; +import { UpdateTemplateFromCollaborationInput } from './dto/template.dto.update.from.collaboration'; +import { CollaborationService } from '@domain/collaboration/collaboration/collaboration.service'; @Resolver() export class TemplateResolverMutations { constructor( private authorizationService: AuthorizationService, + private collaborationService: CollaborationService, private templateService: TemplateService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} @@ -48,17 +50,18 @@ export class TemplateResolverMutations { @UseGuards(GraphqlGuard) @Mutation(() => ITemplate, { - description: 'Updates the specified Template.', + description: + 'Updates the specified Collaboration Template using the provided Collaboration.', }) - async updateCollaborationTemplate( + async updateTemplateFromCollaboration( @CurrentUser() agentInfo: AgentInfo, @Args('updateData') - updateData: UpdateCollaborationTemplateInput + updateData: UpdateTemplateFromCollaborationInput ): Promise { const template = await this.templateService.getTemplateOrFail( - updateData.ID, + updateData.templateID, { - relations: { profile: true }, + relations: { collaboration: true }, } ); await this.authorizationService.grantAccessOrFail( @@ -67,21 +70,23 @@ export class TemplateResolverMutations { AuthorizationPrivilege.UPDATE, `update template: ${template.id}` ); - return await this.templateService.updateTemplate(template, updateData); - } - /* - updateCOllaborationTemplate{ - update privilege on the template - read privilege on the source - - check that it is collab template - delete all the callouts on the template - repla - clone callouts from the source collaboration - logic in template updateCollaborationFromTemplate + const sourceCollaboration = + await this.collaborationService.getCollaborationOrFail( + updateData.collaborationID + ); + await this.authorizationService.grantAccessOrFail( + agentInfo, + sourceCollaboration.authorization, + AuthorizationPrivilege.READ, + `read source collaboration for template: ${sourceCollaboration.id}` + ); + return await this.templateService.updateTemplateFromCollaboration( + template, + updateData, + agentInfo.userID + ); } - */ @UseGuards(GraphqlGuard) @Mutation(() => ITemplate, { diff --git a/src/domain/template/template/template.service.ts b/src/domain/template/template/template.service.ts index 3fee69b61a..13f1e43ca8 100644 --- a/src/domain/template/template/template.service.ts +++ b/src/domain/template/template/template.service.ts @@ -34,6 +34,9 @@ import { CollaborationService } from '@domain/collaboration/collaboration/collab import { CalloutVisibility } from '@common/enums/callout.visibility'; import { TemplateDefault } from '../template-default/template.default.entity'; import { CalloutGroupName } from '@common/enums/callout.group.name'; +import { UpdateTemplateFromCollaborationInput } from './dto/template.dto.update.from.collaboration'; +import { StorageAggregatorResolverService } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service'; +import { InputCreatorService } from '@services/api/input-creator/input.creator.service'; @Injectable() export class TemplateService { @@ -41,6 +44,8 @@ export class TemplateService { private profileService: ProfileService, private innovationFlowService: InnovationFlowService, private communityGuidelinesService: CommunityGuidelinesService, + private storageAggregatorResolverService: StorageAggregatorResolverService, + private inputCreatorService: InputCreatorService, private calloutService: CalloutService, private whiteboardService: WhiteboardService, private collaborationService: CollaborationService, @@ -276,6 +281,111 @@ export class TemplateService { return await this.templateRepository.save(template); } + public async updateTemplateFromCollaboration( + templateInput: ITemplate, + templateData: UpdateTemplateFromCollaborationInput, + userID: string + ): Promise { + if (!templateInput.collaboration) { + throw new RelationshipNotFoundException( + `Unable to load Collaboration on Template: ${templateInput.id} `, + LogContext.TEMPLATES + ); + } + const sourceCollaboration = + await this.collaborationService.getCollaborationOrFail( + templateData.collaborationID + ); + + templateInput.collaboration = + await this.updateCollaborationFromCollaboration( + sourceCollaboration, + templateInput.collaboration, + true, + userID + ); + + await this.collaborationService.save(templateInput.collaboration); + return await this.getTemplateOrFail(templateInput.id); + } + + public async updateCollaborationFromCollaboration( + sourceCollaboration: ICollaboration, + targetCollaboration: ICollaboration, + addCallouts: boolean, + userID: string + ): Promise { + if ( + !sourceCollaboration.innovationFlow || + !targetCollaboration.innovationFlow || + !sourceCollaboration.callouts || + !targetCollaboration.callouts + ) { + throw new RelationshipNotFoundException( + `Template cannot be applied on uninitialized collaboration sourceCollaboration.i:'${sourceCollaboration.id}' TargetCollaboration.id='${targetCollaboration.id}'`, + LogContext.TEMPLATES + ); + } + const newStatesStr = sourceCollaboration.innovationFlow.states; + targetCollaboration.innovationFlow = + await this.innovationFlowService.updateInnovationFlowStates( + targetCollaboration.innovationFlow.id, + newStatesStr + ); + + const storageAggregator = + await this.storageAggregatorResolverService.getStorageAggregatorForCollaboration( + targetCollaboration.id + ); + if (addCallouts) { + const calloutsFromSourceCollaboration = + await this.inputCreatorService.buildCreateCalloutInputsFromCallouts( + sourceCollaboration.callouts ?? [] + ); + + const newCallouts = await this.collaborationService.addCallouts( + targetCollaboration, + calloutsFromSourceCollaboration, + storageAggregator, + userID + ); + targetCollaboration.callouts?.push(...newCallouts); + this.ensureCalloutsInValidGroupsAndStates(targetCollaboration); + + // Need to save before applying authorization policy to get the callout ids + return await this.collaborationService.save(targetCollaboration); + } else { + this.ensureCalloutsInValidGroupsAndStates(targetCollaboration); + return await this.collaborationService.save(targetCollaboration); + } + } + + private ensureCalloutsInValidGroupsAndStates( + targetCollaboration: ICollaboration + ) { + // We don't have callouts or we don't have innovationFlow, can't do anything + if (!targetCollaboration.innovationFlow || !targetCollaboration.callouts) { + throw new RelationshipNotFoundException( + `Unable to load Callouts or InnovationFlow ${targetCollaboration.id} `, + LogContext.TEMPLATES + ); + } + + const validGroupNames = + targetCollaboration.tagsetTemplateSet?.tagsetTemplates.find( + tagset => tagset.name === TagsetReservedName.CALLOUT_GROUP + )?.allowedValues; + const validFlowStates = this.innovationFlowService + .getStates(targetCollaboration.innovationFlow) + ?.map(state => state.displayName); + + this.collaborationService.moveCalloutsToDefaultGroupAndState( + validGroupNames ?? [], + validFlowStates ?? [], + targetCollaboration.callouts + ); + } + async delete(templateInput: ITemplate): Promise { const template = await this.getTemplateOrFail(templateInput.id, { relations: { From cfd2e475fd3fcddc578c9a727b6891da7c159209 Mon Sep 17 00:00:00 2001 From: Neil Smyth <30729240+techsmyth@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:28:16 +0100 Subject: [PATCH 12/22] remove innovation flow from template (#4746) --- src/common/enums/template.type.ts | 1 - .../template/template/template.entity.ts | 9 -- .../template/template/template.interface.ts | 2 - .../template/template/template.module.ts | 2 - .../template/template.resolver.fields.ts | 17 +- .../template.service.authorization.ts | 39 +---- .../template/template/template.service.ts | 30 ---- .../templates.set.resolver.fields.ts | 28 ---- .../templates-set/templates.set.service.ts | 30 +--- .../1733155972372-innovationFlowTemplate.ts | 148 ++++++++++++++++++ .../url-generator/url.generator.service.ts | 13 -- 11 files changed, 153 insertions(+), 166 deletions(-) create mode 100644 src/migrations/1733155972372-innovationFlowTemplate.ts diff --git a/src/common/enums/template.type.ts b/src/common/enums/template.type.ts index b33cee520f..b35f0c701d 100644 --- a/src/common/enums/template.type.ts +++ b/src/common/enums/template.type.ts @@ -5,7 +5,6 @@ export enum TemplateType { POST = 'post', WHITEBOARD = 'whiteboard', COMMUNITY_GUIDELINES = 'community-guidelines', - INNOVATION_FLOW = 'innovation-flow', COLLABORATION = 'collaboration', } diff --git a/src/domain/template/template/template.entity.ts b/src/domain/template/template/template.entity.ts index e6466c5b03..d1befab5c4 100644 --- a/src/domain/template/template/template.entity.ts +++ b/src/domain/template/template/template.entity.ts @@ -6,7 +6,6 @@ import { Profile } from '@domain/common/profile/profile.entity'; import { CommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.entity'; import { Callout } from '@domain/collaboration/callout'; import { Whiteboard } from '@domain/common/whiteboard/whiteboard.entity'; -import { InnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.entity'; import { Collaboration } from '@domain/collaboration/collaboration'; import { NameableEntity } from '@domain/common/entity/nameable-entity'; @@ -33,14 +32,6 @@ export class Template extends NameableEntity implements ITemplate { @Column('text', { nullable: true }) postDefaultDescription?: string; - @OneToOne(() => InnovationFlow, { - eager: false, - cascade: true, - onDelete: 'SET NULL', - }) - @JoinColumn() - innovationFlow?: InnovationFlow; - @OneToOne(() => CommunityGuidelines, { eager: false, cascade: true, diff --git a/src/domain/template/template/template.interface.ts b/src/domain/template/template/template.interface.ts index 27c082d540..4344aaa579 100644 --- a/src/domain/template/template/template.interface.ts +++ b/src/domain/template/template/template.interface.ts @@ -5,7 +5,6 @@ import { TemplateType } from '@common/enums/template.type'; import { ICommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.interface'; import { ICallout } from '@domain/collaboration/callout'; import { IWhiteboard } from '@domain/common/whiteboard/whiteboard.interface'; -import { IInnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.interface'; import { ICollaboration } from '@domain/collaboration/collaboration/collaboration.interface'; import { INameable } from '@domain/common/entity/nameable-entity'; @@ -29,6 +28,5 @@ export abstract class ITemplate extends INameable { communityGuidelines?: ICommunityGuidelines; callout?: ICallout; whiteboard?: IWhiteboard; - innovationFlow?: IInnovationFlow; collaboration?: ICollaboration; } diff --git a/src/domain/template/template/template.module.ts b/src/domain/template/template/template.module.ts index 6ac72e1d06..a9f36f4869 100644 --- a/src/domain/template/template/template.module.ts +++ b/src/domain/template/template/template.module.ts @@ -11,7 +11,6 @@ import { CommunityGuidelinesModule } from '@domain/community/community-guideline import { CalloutModule } from '@domain/collaboration/callout/callout.module'; import { WhiteboardModule } from '@domain/common/whiteboard'; import { TemplateResolverFields } from './template.resolver.fields'; -import { InnovationFlowModule } from '@domain/collaboration/innovation-flow/innovation.flow.module'; import { CollaborationModule } from '@domain/collaboration/collaboration/collaboration.module'; @Module({ @@ -22,7 +21,6 @@ import { CollaborationModule } from '@domain/collaboration/collaboration/collabo CommunityGuidelinesModule, CalloutModule, WhiteboardModule, - InnovationFlowModule, CollaborationModule, TypeOrmModule.forFeature([Template]), ], diff --git a/src/domain/template/template/template.resolver.fields.ts b/src/domain/template/template/template.resolver.fields.ts index 8729652b00..156d1eec9a 100644 --- a/src/domain/template/template/template.resolver.fields.ts +++ b/src/domain/template/template/template.resolver.fields.ts @@ -4,7 +4,6 @@ import { TemplateService } from './template.service'; import { TemplateType } from '@common/enums/template.type'; import { ICommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.interface'; import { IWhiteboard } from '@domain/common/whiteboard/whiteboard.interface'; -import { IInnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.interface'; import { ICallout } from '@domain/collaboration/callout/callout.interface'; import { ICollaboration } from '@domain/collaboration/collaboration'; import { IProfile } from '@domain/common/profile'; @@ -22,7 +21,7 @@ export class TemplateResolverFields { @UseGuards(GraphqlGuard) @ResolveField('profile', () => IProfile, { nullable: false, - description: 'The Profile for this InnovationFlow.', + description: 'The Profile for this Template.', }) async profile( @Parent() template: ITemplate, @@ -32,20 +31,6 @@ export class TemplateResolverFields { return loader.load(template.id); } - @UseGuards(GraphqlGuard) - @ResolveField('innovationFlow', () => IInnovationFlow, { - nullable: true, - description: 'The Innovation Flow.', - }) - async innovationFlow( - @Parent() template: ITemplate - ): Promise { - if (template.type !== TemplateType.INNOVATION_FLOW) { - return undefined; - } - return this.templateService.getInnovationFlow(template.id); - } - @UseGuards(GraphqlGuard) @ResolveField('communityGuidelines', () => ICommunityGuidelines, { nullable: true, diff --git a/src/domain/template/template/template.service.authorization.ts b/src/domain/template/template/template.service.authorization.ts index fc7d567822..f5fd72c3f1 100644 --- a/src/domain/template/template/template.service.authorization.ts +++ b/src/domain/template/template/template.service.authorization.ts @@ -12,7 +12,6 @@ import { CalloutAuthorizationService } from '@domain/collaboration/callout/callo import { WhiteboardAuthorizationService } from '@domain/common/whiteboard/whiteboard.service.authorization'; import { CollaborationAuthorizationService } from '@domain/collaboration/collaboration/collaboration.service.authorization'; import { EntityNotFoundException } from '@common/exceptions/entity.not.found.exception'; -import { InnovationFlowAuthorizationService } from '@domain/collaboration/innovation-flow/innovation.flow.service.authorization'; @Injectable() export class TemplateAuthorizationService { @@ -23,8 +22,7 @@ export class TemplateAuthorizationService { private communityGuidelinesAuthorizationService: CommunityGuidelinesAuthorizationService, private calloutAuthorizationService: CalloutAuthorizationService, private whiteboardAuthorizationService: WhiteboardAuthorizationService, - private collaborationAuthorizationService: CollaborationAuthorizationService, - private innovationFlowAuthorizationService: InnovationFlowAuthorizationService + private collaborationAuthorizationService: CollaborationAuthorizationService ) {} async applyAuthorizationPolicy( @@ -53,10 +51,6 @@ export class TemplateAuthorizationService { collaboration: { authorization: true, }, - innovationFlow: { - profile: true, - authorization: true, - }, }, } ); @@ -146,21 +140,6 @@ export class TemplateAuthorizationService { updatedAuthorizations.push(...collaborationAuthorizations); break; } - case TemplateType.INNOVATION_FLOW: { - if (!template.innovationFlow) { - throw new RelationshipNotFoundException( - `Unable to load InnovationFlow on Template of that type: ${template.id} `, - LogContext.TEMPLATES - ); - } - const innovationFlowAuthorizations = - await this.innovationFlowAuthorizationService.applyAuthorizationPolicy( - template.innovationFlow, - template.authorization - ); - updatedAuthorizations.push(...innovationFlowAuthorizations); - break; - } case TemplateType.POST: { break; } @@ -172,22 +151,6 @@ export class TemplateAuthorizationService { } } - if (template.type == TemplateType.INNOVATION_FLOW) { - if (!template.innovationFlow) { - throw new RelationshipNotFoundException( - `Unable to load InnovationFlow on Template of that type: ${template.id} `, - LogContext.TEMPLATES - ); - } - // Cascade - const innovationFlowAuthorizations = - await this.innovationFlowAuthorizationService.applyAuthorizationPolicy( - template.innovationFlow, - template.authorization - ); - updatedAuthorizations.push(...innovationFlowAuthorizations); - } - return updatedAuthorizations; } } diff --git a/src/domain/template/template/template.service.ts b/src/domain/template/template/template.service.ts index 3fee69b61a..932a42f8cf 100644 --- a/src/domain/template/template/template.service.ts +++ b/src/domain/template/template/template.service.ts @@ -19,7 +19,6 @@ import { ProfileService } from '@domain/common/profile/profile.service'; import { AuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.entity'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { TemplateType } from '@common/enums/template.type'; -import { InnovationFlowService } from '@domain/collaboration/innovation-flow/innovation.flow.service'; import { CommunityGuidelinesService } from '@domain/community/community-guidelines/community.guidelines.service'; import { CreateCommunityGuidelinesInput } from '@domain/community/community-guidelines/dto/community.guidelines.dto.create'; import { ICommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.interface'; @@ -27,7 +26,6 @@ import { ICallout } from '@domain/collaboration/callout'; import { CalloutService } from '@domain/collaboration/callout/callout.service'; import { WhiteboardService } from '@domain/common/whiteboard'; import { IWhiteboard } from '@domain/common/whiteboard/whiteboard.interface'; -import { IInnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.interface'; import { randomUUID } from 'crypto'; import { ICollaboration } from '@domain/collaboration/collaboration'; import { CollaborationService } from '@domain/collaboration/collaboration/collaboration.service'; @@ -39,7 +37,6 @@ import { CalloutGroupName } from '@common/enums/callout.group.name'; export class TemplateService { constructor( private profileService: ProfileService, - private innovationFlowService: InnovationFlowService, private communityGuidelinesService: CommunityGuidelinesService, private calloutService: CalloutService, private whiteboardService: WhiteboardService, @@ -283,7 +280,6 @@ export class TemplateService { communityGuidelines: true, callout: true, whiteboard: true, - innovationFlow: true, collaboration: true, }, }); @@ -339,18 +335,6 @@ export class TemplateService { ); break; } - case TemplateType.INNOVATION_FLOW: { - if (!template.innovationFlow) { - throw new RelationshipNotFoundException( - `Unable to load InnovationFlow on Template: ${templateInput.id} `, - LogContext.TEMPLATES - ); - } - await this.innovationFlowService.deleteInnovationFlow( - template.innovationFlow.id - ); - break; - } case TemplateType.POST: { // Nothing to do break; @@ -502,20 +486,6 @@ export class TemplateService { return template.collaboration; } - public async getInnovationFlow(templateID: string): Promise { - const template = await this.getTemplateOrFail(templateID, { - relations: { - innovationFlow: true, - }, - }); - if (!template.innovationFlow) { - throw new RelationshipNotFoundException( - `Unable to load Template with InnovationFlow: ${template.id} `, - LogContext.TEMPLATES - ); - } - return template.innovationFlow; - } public async isTemplateInUseInTemplateDefault( templateID: string ): Promise { diff --git a/src/domain/template/templates-set/templates.set.resolver.fields.ts b/src/domain/template/templates-set/templates.set.resolver.fields.ts index 8e534c4910..c9ed65c283 100644 --- a/src/domain/template/templates-set/templates.set.resolver.fields.ts +++ b/src/domain/template/templates-set/templates.set.resolver.fields.ts @@ -85,34 +85,6 @@ export class TemplatesSetResolverFields { ); } - @UseGuards(GraphqlGuard) - @ResolveField('innovationFlowTemplates', () => [ITemplate], { - nullable: false, - description: 'The InnovationFlowTemplates in this TemplatesSet.', - }) - async innovationFlowTemplates( - @Parent() templatesSet: ITemplatesSet - ): Promise { - return this.templatesSetService.getTemplatesOfType( - templatesSet, - TemplateType.INNOVATION_FLOW - ); - } - @UseGuards(GraphqlGuard) - @ResolveField('innovationFlowTemplatesCount', () => Float, { - nullable: false, - description: - 'The total number of InnovationFlowTemplates in this TemplatesSet.', - }) - async innovationFlowTemplatesCount( - @Parent() templatesSet: ITemplatesSet - ): Promise { - return this.templatesSetService.getTemplatesCountForType( - templatesSet.id, - TemplateType.INNOVATION_FLOW - ); - } - @UseGuards(GraphqlGuard) @ResolveField('communityGuidelinesTemplates', () => [ITemplate], { nullable: false, diff --git a/src/domain/template/templates-set/templates.set.service.ts b/src/domain/template/templates-set/templates.set.service.ts index 711c3d4a5a..7a8aa67227 100644 --- a/src/domain/template/templates-set/templates.set.service.ts +++ b/src/domain/template/templates-set/templates.set.service.ts @@ -160,30 +160,6 @@ export class TemplatesSetService { return await this.createTemplate(templatesSet, templateInput); } - async addTemplates( - templatesSet: ITemplatesSet, - templateInputs: CreateTemplateInput[], - innovationFlowTemplateInputs: CreateTemplateInput[], - storageAggregator: IStorageAggregator - ): Promise { - for (const templateDefault of templateInputs) { - const template = await this.templateService.createTemplate( - templateDefault, - storageAggregator - ); - templatesSet.templates.push(template); - } - - for (const innovationFlowTemplateDefault of innovationFlowTemplateInputs) { - const innovationFlowTemplate = await this.templateService.createTemplate( - innovationFlowTemplateDefault, - storageAggregator - ); - templatesSet.templates.push(innovationFlowTemplate); - } - return await this.save(templatesSet); - } - private async getStorageAggregator( templatesSet: ITemplatesSet ): Promise { @@ -219,10 +195,10 @@ export class TemplatesSetService { TemplateType.POST ); - const innovationFlowsCount = + const collaborationTemplatesCount = await this.templateService.getCountInTemplatesSet( templatesSetID, - TemplateType.INNOVATION_FLOW + TemplateType.COLLABORATION ); const calloutTemplatesCount = @@ -240,7 +216,7 @@ export class TemplatesSetService { return ( whiteboardTemplatesCount + postTemplatesCount + - innovationFlowsCount + + collaborationTemplatesCount + calloutTemplatesCount + communityGuidelinesTemplatesCount ); diff --git a/src/migrations/1733155972372-innovationFlowTemplate.ts b/src/migrations/1733155972372-innovationFlowTemplate.ts new file mode 100644 index 0000000000..dfebd8a488 --- /dev/null +++ b/src/migrations/1733155972372-innovationFlowTemplate.ts @@ -0,0 +1,148 @@ +import { of } from 'rxjs'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InnovationFlowTemplate1733155972372 implements MigrationInterface { + name = 'InnovationFlowTemplate1733155972372'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE `template` DROP FOREIGN KEY `FK_45cf273f30c1fa509456b6b0ddf`' + ); + await queryRunner.query( + 'DROP INDEX `REL_45cf273f30c1fa509456b6b0dd` ON `template`' + ); + + // delete the data from the template table + const innovationFlowTemplates: { + id: string; + innovationFlowId: string; + profileId: string; + authorizationId: string; + }[] = await queryRunner.query( + `SELECT id, innovationFlowId, profileId, authorizationId FROM template WHERE innovationFlowId IS NOT NULL;` + ); + for (const template of innovationFlowTemplates) { + // delete the innovation flow + await this.deleteInnovationFlow(queryRunner, template.innovationFlowId); + // delete the profile + await this.deleteProfile(queryRunner, template.profileId); + // delete the template + await queryRunner.query(`DELETE FROM template WHERE id = ?`, [ + template.id, + ]); + } + + await queryRunner.query( + 'ALTER TABLE `template` DROP COLUMN `innovationFlowId`' + ); + } + + private async deleteInnovationFlow( + queryRunner: QueryRunner, + innovationFlowId: string + ) { + const [innovationFlow]: { + id: string; + profileId: string; + authorizationId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId, profileId FROM innovation_flow WHERE id = ?`, + [innovationFlowId] + ); + if (innovationFlow) { + // delete the innovation flow + await queryRunner.query(`DELETE FROM innovation_flow WHERE id = ?`, [ + innovationFlowId, + ]); + // delete the authorization policy + await queryRunner.query(`DELETE FROM authorization_policy WHERE id = ?`, [ + innovationFlow.authorizationId, + ]); + // delete the profile + await this.deleteProfile(queryRunner, innovationFlow.profileId); + } + } + + private async deleteProfile(queryRunner: QueryRunner, profileId: string) { + const [profile]: { + id: string; + authorizationId: string; + locationId: string; + storageBucketId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId, locationId, storageBucketId FROM profile WHERE id = ?`, + [profileId] + ); + if (profile) { + await queryRunner.query(`DELETE FROM profile WHERE id = ?`, [profileId]); + // delete the authorization policy + await queryRunner.query(`DELETE FROM authorization_policy WHERE id = ?`, [ + profile.authorizationId, + ]); + // Delete location + await queryRunner.query(`DELETE FROM location WHERE id = ?`, [ + profile.locationId, + ]); + // Delete storage bucket + await this.deleteStorageBucket(queryRunner, profile.storageBucketId); + } + } + + private async deleteStorageBucket( + queryRunner: QueryRunner, + storageBucketId: string + ) { + const [storageBucket]: { + id: string; + authorizationId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId FROM storage_bucket WHERE id = ?`, + [storageBucketId] + ); + if (storageBucket) { + await queryRunner.query(`DELETE FROM storage_bucket WHERE id = ?`, [ + storageBucketId, + ]); + // delete the authorization policy + await queryRunner.query(`DELETE FROM authorization_policy WHERE id = ?`, [ + storageBucket.authorizationId, + ]); + // Delete documents + const documents: { + id: string; + authorizationId: string; + tagsetId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId, tagsetId FROM document where storageBucketId = ?`, + [storageBucketId] + ); + for (const document of documents) { + // delete the tagset + await queryRunner.query(`DELETE FROM tagset WHERE id = ?`, [ + document.tagsetId, + ]); + // delete the authorization policy + await queryRunner.query( + `DELETE FROM authorization_policy WHERE id = ?`, + [document.authorizationId] + ); + // delete the document + await queryRunner.query(`DELETE FROM document WHERE id = ?`, [ + document.id, + ]); + } + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE `template` ADD `innovationFlowId` char(36) NULL' + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `REL_45cf273f30c1fa509456b6b0dd` ON `template` (`innovationFlowId`)' + ); + await queryRunner.query( + 'ALTER TABLE `template` ADD CONSTRAINT `FK_45cf273f30c1fa509456b6b0ddf` FOREIGN KEY (`innovationFlowId`) REFERENCES `innovation_flow`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION' + ); + } +} diff --git a/src/services/infrastructure/url-generator/url.generator.service.ts b/src/services/infrastructure/url-generator/url.generator.service.ts index 27182321ac..6ad9aab707 100644 --- a/src/services/infrastructure/url-generator/url.generator.service.ts +++ b/src/services/infrastructure/url-generator/url.generator.service.ts @@ -549,19 +549,6 @@ export class UrlGeneratorService { return await this.getJourneyUrlPath('collaborationId', collaboration.id); } - const template = await this.entityManager.findOne(Template, { - where: { - innovationFlow: { - id: innovationFlow.id, - }, - }, - relations: { - profile: true, - }, - }); - if (template) { - return await this.getTemplateUrlPathOrFail(template.profile.id); - } throw new EntityNotFoundException( `Unable to find innovationFlow for profile: ${profileID}`, LogContext.URL_GENERATOR From 58672643fec7e51339c9eeb7e62c944464082f41 Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Tue, 3 Dec 2024 16:37:57 +0100 Subject: [PATCH 13/22] Erase CommunityGuidelines content mutation (#4740) * Erase CommunityGuidelines content mutation * Address coderabbit comments * Update community.guidelines.resolver.mutations.ts --------- Co-authored-by: Valentin Yanakiev --- src/domain/common/profile/profile.service.ts | 18 +++++++++++++ ...community.guidelines.resolver.mutations.ts | 26 +++++++++++++++++++ .../community.guidelines.service.ts | 19 ++++++++++++++ ...community.guidelines.dto.remove.content.ts | 10 +++++++ 4 files changed, 73 insertions(+) create mode 100644 src/domain/community/community-guidelines/dto/community.guidelines.dto.remove.content.ts diff --git a/src/domain/common/profile/profile.service.ts b/src/domain/common/profile/profile.service.ts index 220e92cf15..1a5e2d9bad 100644 --- a/src/domain/common/profile/profile.service.ts +++ b/src/domain/common/profile/profile.service.ts @@ -273,6 +273,24 @@ export class ProfileService { return await this.referenceService.save(newReference); } + async deleteAllReferencesFromProfile(profileId: string): Promise { + const profile = await this.getProfileOrFail(profileId, { + relations: { references: true }, + }); + + if (!profile.references) + throw new EntityNotInitializedException( + 'References not defined', + LogContext.COMMUNITY + ); + + for (const reference of profile.references) { + await this.referenceService.deleteReference({ + ID: reference.id, + }); + } + } + async getProfileOrFail( profileID: string, options?: FindOneOptions diff --git a/src/domain/community/community-guidelines/community.guidelines.resolver.mutations.ts b/src/domain/community/community-guidelines/community.guidelines.resolver.mutations.ts index ac2fcfc6ec..9396147e81 100644 --- a/src/domain/community/community-guidelines/community.guidelines.resolver.mutations.ts +++ b/src/domain/community/community-guidelines/community.guidelines.resolver.mutations.ts @@ -8,6 +8,7 @@ import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; import { ICommunityGuidelines } from './community.guidelines.interface'; import { CommunityGuidelinesService } from './community.guidelines.service'; import { UpdateCommunityGuidelinesEntityInput } from './dto/community.guidelines.dto.update.entity'; +import { RemoveCommunityGuidelinesContentInput as RemoveCommunityGuidelinesContentInput } from './dto/community.guidelines.dto.remove.content'; @Resolver() export class CommunityGuidelinesResolverMutations { @@ -42,4 +43,29 @@ export class CommunityGuidelinesResolverMutations { communityGuidelinesData ); } + @UseGuards(GraphqlGuard) + @Mutation(() => ICommunityGuidelines, { + description: 'Empties the CommunityGuidelines.', // Update mutation doesn't allow empty values. And we cannot really delete the entity, but this will leave it empty. + }) + @Profiling.api + async removeCommunityGuidelinesContent( + @CurrentUser() agentInfo: AgentInfo, + @Args('communityGuidelinesData') + communityGuidelinesData: RemoveCommunityGuidelinesContentInput + ): Promise { + const communityGuidelines = + await this.communityGuidelinesService.getCommunityGuidelinesOrFail( + communityGuidelinesData.communityGuidelinesID + ); + await this.authorizationService.grantAccessOrFail( + agentInfo, + communityGuidelines.authorization, + AuthorizationPrivilege.UPDATE, + `removeCommunityGuidelinesContent: ${communityGuidelines.id}` + ); + + return await this.communityGuidelinesService.eraseContent( + communityGuidelines + ); + } } diff --git a/src/domain/community/community-guidelines/community.guidelines.service.ts b/src/domain/community/community-guidelines/community.guidelines.service.ts index 5b9da2951e..f9595ee2b7 100644 --- a/src/domain/community/community-guidelines/community.guidelines.service.ts +++ b/src/domain/community/community-guidelines/community.guidelines.service.ts @@ -78,6 +78,25 @@ export class CommunityGuidelinesService { return await this.communityGuidelinesRepository.save(communityGuidelines); } + async eraseContent( + communityGuidelines: ICommunityGuidelines + ): Promise { + communityGuidelines.profile = await this.profileService.updateProfile( + communityGuidelines.profile, + { + displayName: '', + description: '', + } + ); + + await this.profileService.deleteAllReferencesFromProfile( + communityGuidelines.profile.id + ); + communityGuidelines.profile.references = []; + + return await this.communityGuidelinesRepository.save(communityGuidelines); + } + async deleteCommunityGuidelines( communityGuidelinesID: string ): Promise { diff --git a/src/domain/community/community-guidelines/dto/community.guidelines.dto.remove.content.ts b/src/domain/community/community-guidelines/dto/community.guidelines.dto.remove.content.ts new file mode 100644 index 0000000000..7dec07187f --- /dev/null +++ b/src/domain/community/community-guidelines/dto/community.guidelines.dto.remove.content.ts @@ -0,0 +1,10 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { UUID } from '@domain/common/scalars/scalar.uuid'; + +@InputType() +export class RemoveCommunityGuidelinesContentInput { + @Field(() => UUID, { + description: 'ID of the CommunityGuidelines that will be emptied', + }) + communityGuidelinesID!: string; +} From b6de5efccc7d252c144634aa816e9ff1e75d5b69 Mon Sep 17 00:00:00 2001 From: Neil Smyth <30729240+techsmyth@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:28:16 +0100 Subject: [PATCH 14/22] remove innovation flow from template (#4746) --- src/common/enums/template.type.ts | 1 - .../template/template/template.entity.ts | 9 -- .../template/template/template.interface.ts | 2 - .../template/template/template.module.ts | 2 - .../template/template.resolver.fields.ts | 17 +- .../template.service.authorization.ts | 39 +---- .../template/template/template.service.ts | 30 ---- .../templates.set.resolver.fields.ts | 28 ---- .../templates-set/templates.set.service.ts | 30 +--- .../1733155972372-innovationFlowTemplate.ts | 148 ++++++++++++++++++ .../url-generator/url.generator.service.ts | 13 -- 11 files changed, 153 insertions(+), 166 deletions(-) create mode 100644 src/migrations/1733155972372-innovationFlowTemplate.ts diff --git a/src/common/enums/template.type.ts b/src/common/enums/template.type.ts index b33cee520f..b35f0c701d 100644 --- a/src/common/enums/template.type.ts +++ b/src/common/enums/template.type.ts @@ -5,7 +5,6 @@ export enum TemplateType { POST = 'post', WHITEBOARD = 'whiteboard', COMMUNITY_GUIDELINES = 'community-guidelines', - INNOVATION_FLOW = 'innovation-flow', COLLABORATION = 'collaboration', } diff --git a/src/domain/template/template/template.entity.ts b/src/domain/template/template/template.entity.ts index e6466c5b03..d1befab5c4 100644 --- a/src/domain/template/template/template.entity.ts +++ b/src/domain/template/template/template.entity.ts @@ -6,7 +6,6 @@ import { Profile } from '@domain/common/profile/profile.entity'; import { CommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.entity'; import { Callout } from '@domain/collaboration/callout'; import { Whiteboard } from '@domain/common/whiteboard/whiteboard.entity'; -import { InnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.entity'; import { Collaboration } from '@domain/collaboration/collaboration'; import { NameableEntity } from '@domain/common/entity/nameable-entity'; @@ -33,14 +32,6 @@ export class Template extends NameableEntity implements ITemplate { @Column('text', { nullable: true }) postDefaultDescription?: string; - @OneToOne(() => InnovationFlow, { - eager: false, - cascade: true, - onDelete: 'SET NULL', - }) - @JoinColumn() - innovationFlow?: InnovationFlow; - @OneToOne(() => CommunityGuidelines, { eager: false, cascade: true, diff --git a/src/domain/template/template/template.interface.ts b/src/domain/template/template/template.interface.ts index 27c082d540..4344aaa579 100644 --- a/src/domain/template/template/template.interface.ts +++ b/src/domain/template/template/template.interface.ts @@ -5,7 +5,6 @@ import { TemplateType } from '@common/enums/template.type'; import { ICommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.interface'; import { ICallout } from '@domain/collaboration/callout'; import { IWhiteboard } from '@domain/common/whiteboard/whiteboard.interface'; -import { IInnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.interface'; import { ICollaboration } from '@domain/collaboration/collaboration/collaboration.interface'; import { INameable } from '@domain/common/entity/nameable-entity'; @@ -29,6 +28,5 @@ export abstract class ITemplate extends INameable { communityGuidelines?: ICommunityGuidelines; callout?: ICallout; whiteboard?: IWhiteboard; - innovationFlow?: IInnovationFlow; collaboration?: ICollaboration; } diff --git a/src/domain/template/template/template.module.ts b/src/domain/template/template/template.module.ts index f918cc5dc9..02ee018c38 100644 --- a/src/domain/template/template/template.module.ts +++ b/src/domain/template/template/template.module.ts @@ -11,7 +11,6 @@ import { CommunityGuidelinesModule } from '@domain/community/community-guideline import { CalloutModule } from '@domain/collaboration/callout/callout.module'; import { WhiteboardModule } from '@domain/common/whiteboard'; import { TemplateResolverFields } from './template.resolver.fields'; -import { InnovationFlowModule } from '@domain/collaboration/innovation-flow/innovation.flow.module'; import { CollaborationModule } from '@domain/collaboration/collaboration/collaboration.module'; import { InputCreatorModule } from '@services/api/input-creator/input.creator.module'; import { StorageAggregatorResolverModule } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.module'; @@ -24,7 +23,6 @@ import { StorageAggregatorResolverModule } from '@services/infrastructure/storag CommunityGuidelinesModule, CalloutModule, WhiteboardModule, - InnovationFlowModule, InputCreatorModule, StorageAggregatorResolverModule, CollaborationModule, diff --git a/src/domain/template/template/template.resolver.fields.ts b/src/domain/template/template/template.resolver.fields.ts index 8729652b00..156d1eec9a 100644 --- a/src/domain/template/template/template.resolver.fields.ts +++ b/src/domain/template/template/template.resolver.fields.ts @@ -4,7 +4,6 @@ import { TemplateService } from './template.service'; import { TemplateType } from '@common/enums/template.type'; import { ICommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.interface'; import { IWhiteboard } from '@domain/common/whiteboard/whiteboard.interface'; -import { IInnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.interface'; import { ICallout } from '@domain/collaboration/callout/callout.interface'; import { ICollaboration } from '@domain/collaboration/collaboration'; import { IProfile } from '@domain/common/profile'; @@ -22,7 +21,7 @@ export class TemplateResolverFields { @UseGuards(GraphqlGuard) @ResolveField('profile', () => IProfile, { nullable: false, - description: 'The Profile for this InnovationFlow.', + description: 'The Profile for this Template.', }) async profile( @Parent() template: ITemplate, @@ -32,20 +31,6 @@ export class TemplateResolverFields { return loader.load(template.id); } - @UseGuards(GraphqlGuard) - @ResolveField('innovationFlow', () => IInnovationFlow, { - nullable: true, - description: 'The Innovation Flow.', - }) - async innovationFlow( - @Parent() template: ITemplate - ): Promise { - if (template.type !== TemplateType.INNOVATION_FLOW) { - return undefined; - } - return this.templateService.getInnovationFlow(template.id); - } - @UseGuards(GraphqlGuard) @ResolveField('communityGuidelines', () => ICommunityGuidelines, { nullable: true, diff --git a/src/domain/template/template/template.service.authorization.ts b/src/domain/template/template/template.service.authorization.ts index fc7d567822..f5fd72c3f1 100644 --- a/src/domain/template/template/template.service.authorization.ts +++ b/src/domain/template/template/template.service.authorization.ts @@ -12,7 +12,6 @@ import { CalloutAuthorizationService } from '@domain/collaboration/callout/callo import { WhiteboardAuthorizationService } from '@domain/common/whiteboard/whiteboard.service.authorization'; import { CollaborationAuthorizationService } from '@domain/collaboration/collaboration/collaboration.service.authorization'; import { EntityNotFoundException } from '@common/exceptions/entity.not.found.exception'; -import { InnovationFlowAuthorizationService } from '@domain/collaboration/innovation-flow/innovation.flow.service.authorization'; @Injectable() export class TemplateAuthorizationService { @@ -23,8 +22,7 @@ export class TemplateAuthorizationService { private communityGuidelinesAuthorizationService: CommunityGuidelinesAuthorizationService, private calloutAuthorizationService: CalloutAuthorizationService, private whiteboardAuthorizationService: WhiteboardAuthorizationService, - private collaborationAuthorizationService: CollaborationAuthorizationService, - private innovationFlowAuthorizationService: InnovationFlowAuthorizationService + private collaborationAuthorizationService: CollaborationAuthorizationService ) {} async applyAuthorizationPolicy( @@ -53,10 +51,6 @@ export class TemplateAuthorizationService { collaboration: { authorization: true, }, - innovationFlow: { - profile: true, - authorization: true, - }, }, } ); @@ -146,21 +140,6 @@ export class TemplateAuthorizationService { updatedAuthorizations.push(...collaborationAuthorizations); break; } - case TemplateType.INNOVATION_FLOW: { - if (!template.innovationFlow) { - throw new RelationshipNotFoundException( - `Unable to load InnovationFlow on Template of that type: ${template.id} `, - LogContext.TEMPLATES - ); - } - const innovationFlowAuthorizations = - await this.innovationFlowAuthorizationService.applyAuthorizationPolicy( - template.innovationFlow, - template.authorization - ); - updatedAuthorizations.push(...innovationFlowAuthorizations); - break; - } case TemplateType.POST: { break; } @@ -172,22 +151,6 @@ export class TemplateAuthorizationService { } } - if (template.type == TemplateType.INNOVATION_FLOW) { - if (!template.innovationFlow) { - throw new RelationshipNotFoundException( - `Unable to load InnovationFlow on Template of that type: ${template.id} `, - LogContext.TEMPLATES - ); - } - // Cascade - const innovationFlowAuthorizations = - await this.innovationFlowAuthorizationService.applyAuthorizationPolicy( - template.innovationFlow, - template.authorization - ); - updatedAuthorizations.push(...innovationFlowAuthorizations); - } - return updatedAuthorizations; } } diff --git a/src/domain/template/template/template.service.ts b/src/domain/template/template/template.service.ts index 13f1e43ca8..03f391a46a 100644 --- a/src/domain/template/template/template.service.ts +++ b/src/domain/template/template/template.service.ts @@ -19,7 +19,6 @@ import { ProfileService } from '@domain/common/profile/profile.service'; import { AuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.entity'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { TemplateType } from '@common/enums/template.type'; -import { InnovationFlowService } from '@domain/collaboration/innovation-flow/innovation.flow.service'; import { CommunityGuidelinesService } from '@domain/community/community-guidelines/community.guidelines.service'; import { CreateCommunityGuidelinesInput } from '@domain/community/community-guidelines/dto/community.guidelines.dto.create'; import { ICommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.interface'; @@ -27,7 +26,6 @@ import { ICallout } from '@domain/collaboration/callout'; import { CalloutService } from '@domain/collaboration/callout/callout.service'; import { WhiteboardService } from '@domain/common/whiteboard'; import { IWhiteboard } from '@domain/common/whiteboard/whiteboard.interface'; -import { IInnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.interface'; import { randomUUID } from 'crypto'; import { ICollaboration } from '@domain/collaboration/collaboration'; import { CollaborationService } from '@domain/collaboration/collaboration/collaboration.service'; @@ -42,7 +40,6 @@ import { InputCreatorService } from '@services/api/input-creator/input.creator.s export class TemplateService { constructor( private profileService: ProfileService, - private innovationFlowService: InnovationFlowService, private communityGuidelinesService: CommunityGuidelinesService, private storageAggregatorResolverService: StorageAggregatorResolverService, private inputCreatorService: InputCreatorService, @@ -393,7 +390,6 @@ export class TemplateService { communityGuidelines: true, callout: true, whiteboard: true, - innovationFlow: true, collaboration: true, }, }); @@ -449,18 +445,6 @@ export class TemplateService { ); break; } - case TemplateType.INNOVATION_FLOW: { - if (!template.innovationFlow) { - throw new RelationshipNotFoundException( - `Unable to load InnovationFlow on Template: ${templateInput.id} `, - LogContext.TEMPLATES - ); - } - await this.innovationFlowService.deleteInnovationFlow( - template.innovationFlow.id - ); - break; - } case TemplateType.POST: { // Nothing to do break; @@ -612,20 +596,6 @@ export class TemplateService { return template.collaboration; } - public async getInnovationFlow(templateID: string): Promise { - const template = await this.getTemplateOrFail(templateID, { - relations: { - innovationFlow: true, - }, - }); - if (!template.innovationFlow) { - throw new RelationshipNotFoundException( - `Unable to load Template with InnovationFlow: ${template.id} `, - LogContext.TEMPLATES - ); - } - return template.innovationFlow; - } public async isTemplateInUseInTemplateDefault( templateID: string ): Promise { diff --git a/src/domain/template/templates-set/templates.set.resolver.fields.ts b/src/domain/template/templates-set/templates.set.resolver.fields.ts index 8e534c4910..c9ed65c283 100644 --- a/src/domain/template/templates-set/templates.set.resolver.fields.ts +++ b/src/domain/template/templates-set/templates.set.resolver.fields.ts @@ -85,34 +85,6 @@ export class TemplatesSetResolverFields { ); } - @UseGuards(GraphqlGuard) - @ResolveField('innovationFlowTemplates', () => [ITemplate], { - nullable: false, - description: 'The InnovationFlowTemplates in this TemplatesSet.', - }) - async innovationFlowTemplates( - @Parent() templatesSet: ITemplatesSet - ): Promise { - return this.templatesSetService.getTemplatesOfType( - templatesSet, - TemplateType.INNOVATION_FLOW - ); - } - @UseGuards(GraphqlGuard) - @ResolveField('innovationFlowTemplatesCount', () => Float, { - nullable: false, - description: - 'The total number of InnovationFlowTemplates in this TemplatesSet.', - }) - async innovationFlowTemplatesCount( - @Parent() templatesSet: ITemplatesSet - ): Promise { - return this.templatesSetService.getTemplatesCountForType( - templatesSet.id, - TemplateType.INNOVATION_FLOW - ); - } - @UseGuards(GraphqlGuard) @ResolveField('communityGuidelinesTemplates', () => [ITemplate], { nullable: false, diff --git a/src/domain/template/templates-set/templates.set.service.ts b/src/domain/template/templates-set/templates.set.service.ts index 711c3d4a5a..7a8aa67227 100644 --- a/src/domain/template/templates-set/templates.set.service.ts +++ b/src/domain/template/templates-set/templates.set.service.ts @@ -160,30 +160,6 @@ export class TemplatesSetService { return await this.createTemplate(templatesSet, templateInput); } - async addTemplates( - templatesSet: ITemplatesSet, - templateInputs: CreateTemplateInput[], - innovationFlowTemplateInputs: CreateTemplateInput[], - storageAggregator: IStorageAggregator - ): Promise { - for (const templateDefault of templateInputs) { - const template = await this.templateService.createTemplate( - templateDefault, - storageAggregator - ); - templatesSet.templates.push(template); - } - - for (const innovationFlowTemplateDefault of innovationFlowTemplateInputs) { - const innovationFlowTemplate = await this.templateService.createTemplate( - innovationFlowTemplateDefault, - storageAggregator - ); - templatesSet.templates.push(innovationFlowTemplate); - } - return await this.save(templatesSet); - } - private async getStorageAggregator( templatesSet: ITemplatesSet ): Promise { @@ -219,10 +195,10 @@ export class TemplatesSetService { TemplateType.POST ); - const innovationFlowsCount = + const collaborationTemplatesCount = await this.templateService.getCountInTemplatesSet( templatesSetID, - TemplateType.INNOVATION_FLOW + TemplateType.COLLABORATION ); const calloutTemplatesCount = @@ -240,7 +216,7 @@ export class TemplatesSetService { return ( whiteboardTemplatesCount + postTemplatesCount + - innovationFlowsCount + + collaborationTemplatesCount + calloutTemplatesCount + communityGuidelinesTemplatesCount ); diff --git a/src/migrations/1733155972372-innovationFlowTemplate.ts b/src/migrations/1733155972372-innovationFlowTemplate.ts new file mode 100644 index 0000000000..dfebd8a488 --- /dev/null +++ b/src/migrations/1733155972372-innovationFlowTemplate.ts @@ -0,0 +1,148 @@ +import { of } from 'rxjs'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InnovationFlowTemplate1733155972372 implements MigrationInterface { + name = 'InnovationFlowTemplate1733155972372'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE `template` DROP FOREIGN KEY `FK_45cf273f30c1fa509456b6b0ddf`' + ); + await queryRunner.query( + 'DROP INDEX `REL_45cf273f30c1fa509456b6b0dd` ON `template`' + ); + + // delete the data from the template table + const innovationFlowTemplates: { + id: string; + innovationFlowId: string; + profileId: string; + authorizationId: string; + }[] = await queryRunner.query( + `SELECT id, innovationFlowId, profileId, authorizationId FROM template WHERE innovationFlowId IS NOT NULL;` + ); + for (const template of innovationFlowTemplates) { + // delete the innovation flow + await this.deleteInnovationFlow(queryRunner, template.innovationFlowId); + // delete the profile + await this.deleteProfile(queryRunner, template.profileId); + // delete the template + await queryRunner.query(`DELETE FROM template WHERE id = ?`, [ + template.id, + ]); + } + + await queryRunner.query( + 'ALTER TABLE `template` DROP COLUMN `innovationFlowId`' + ); + } + + private async deleteInnovationFlow( + queryRunner: QueryRunner, + innovationFlowId: string + ) { + const [innovationFlow]: { + id: string; + profileId: string; + authorizationId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId, profileId FROM innovation_flow WHERE id = ?`, + [innovationFlowId] + ); + if (innovationFlow) { + // delete the innovation flow + await queryRunner.query(`DELETE FROM innovation_flow WHERE id = ?`, [ + innovationFlowId, + ]); + // delete the authorization policy + await queryRunner.query(`DELETE FROM authorization_policy WHERE id = ?`, [ + innovationFlow.authorizationId, + ]); + // delete the profile + await this.deleteProfile(queryRunner, innovationFlow.profileId); + } + } + + private async deleteProfile(queryRunner: QueryRunner, profileId: string) { + const [profile]: { + id: string; + authorizationId: string; + locationId: string; + storageBucketId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId, locationId, storageBucketId FROM profile WHERE id = ?`, + [profileId] + ); + if (profile) { + await queryRunner.query(`DELETE FROM profile WHERE id = ?`, [profileId]); + // delete the authorization policy + await queryRunner.query(`DELETE FROM authorization_policy WHERE id = ?`, [ + profile.authorizationId, + ]); + // Delete location + await queryRunner.query(`DELETE FROM location WHERE id = ?`, [ + profile.locationId, + ]); + // Delete storage bucket + await this.deleteStorageBucket(queryRunner, profile.storageBucketId); + } + } + + private async deleteStorageBucket( + queryRunner: QueryRunner, + storageBucketId: string + ) { + const [storageBucket]: { + id: string; + authorizationId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId FROM storage_bucket WHERE id = ?`, + [storageBucketId] + ); + if (storageBucket) { + await queryRunner.query(`DELETE FROM storage_bucket WHERE id = ?`, [ + storageBucketId, + ]); + // delete the authorization policy + await queryRunner.query(`DELETE FROM authorization_policy WHERE id = ?`, [ + storageBucket.authorizationId, + ]); + // Delete documents + const documents: { + id: string; + authorizationId: string; + tagsetId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId, tagsetId FROM document where storageBucketId = ?`, + [storageBucketId] + ); + for (const document of documents) { + // delete the tagset + await queryRunner.query(`DELETE FROM tagset WHERE id = ?`, [ + document.tagsetId, + ]); + // delete the authorization policy + await queryRunner.query( + `DELETE FROM authorization_policy WHERE id = ?`, + [document.authorizationId] + ); + // delete the document + await queryRunner.query(`DELETE FROM document WHERE id = ?`, [ + document.id, + ]); + } + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE `template` ADD `innovationFlowId` char(36) NULL' + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `REL_45cf273f30c1fa509456b6b0dd` ON `template` (`innovationFlowId`)' + ); + await queryRunner.query( + 'ALTER TABLE `template` ADD CONSTRAINT `FK_45cf273f30c1fa509456b6b0ddf` FOREIGN KEY (`innovationFlowId`) REFERENCES `innovation_flow`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION' + ); + } +} diff --git a/src/services/infrastructure/url-generator/url.generator.service.ts b/src/services/infrastructure/url-generator/url.generator.service.ts index 27182321ac..6ad9aab707 100644 --- a/src/services/infrastructure/url-generator/url.generator.service.ts +++ b/src/services/infrastructure/url-generator/url.generator.service.ts @@ -549,19 +549,6 @@ export class UrlGeneratorService { return await this.getJourneyUrlPath('collaborationId', collaboration.id); } - const template = await this.entityManager.findOne(Template, { - where: { - innovationFlow: { - id: innovationFlow.id, - }, - }, - relations: { - profile: true, - }, - }); - if (template) { - return await this.getTemplateUrlPathOrFail(template.profile.id); - } throw new EntityNotFoundException( `Unable to find innovationFlow for profile: ${profileID}`, LogContext.URL_GENERATOR From 2cfa6f9ae128c3894aad80b24017c30382c20328 Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Tue, 3 Dec 2024 16:37:57 +0100 Subject: [PATCH 15/22] Erase CommunityGuidelines content mutation (#4740) * Erase CommunityGuidelines content mutation * Address coderabbit comments * Update community.guidelines.resolver.mutations.ts --------- Co-authored-by: Valentin Yanakiev --- src/domain/common/profile/profile.service.ts | 18 +++++++++++++ ...community.guidelines.resolver.mutations.ts | 26 +++++++++++++++++++ .../community.guidelines.service.ts | 19 ++++++++++++++ ...community.guidelines.dto.remove.content.ts | 10 +++++++ 4 files changed, 73 insertions(+) create mode 100644 src/domain/community/community-guidelines/dto/community.guidelines.dto.remove.content.ts diff --git a/src/domain/common/profile/profile.service.ts b/src/domain/common/profile/profile.service.ts index 220e92cf15..1a5e2d9bad 100644 --- a/src/domain/common/profile/profile.service.ts +++ b/src/domain/common/profile/profile.service.ts @@ -273,6 +273,24 @@ export class ProfileService { return await this.referenceService.save(newReference); } + async deleteAllReferencesFromProfile(profileId: string): Promise { + const profile = await this.getProfileOrFail(profileId, { + relations: { references: true }, + }); + + if (!profile.references) + throw new EntityNotInitializedException( + 'References not defined', + LogContext.COMMUNITY + ); + + for (const reference of profile.references) { + await this.referenceService.deleteReference({ + ID: reference.id, + }); + } + } + async getProfileOrFail( profileID: string, options?: FindOneOptions diff --git a/src/domain/community/community-guidelines/community.guidelines.resolver.mutations.ts b/src/domain/community/community-guidelines/community.guidelines.resolver.mutations.ts index ac2fcfc6ec..9396147e81 100644 --- a/src/domain/community/community-guidelines/community.guidelines.resolver.mutations.ts +++ b/src/domain/community/community-guidelines/community.guidelines.resolver.mutations.ts @@ -8,6 +8,7 @@ import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; import { ICommunityGuidelines } from './community.guidelines.interface'; import { CommunityGuidelinesService } from './community.guidelines.service'; import { UpdateCommunityGuidelinesEntityInput } from './dto/community.guidelines.dto.update.entity'; +import { RemoveCommunityGuidelinesContentInput as RemoveCommunityGuidelinesContentInput } from './dto/community.guidelines.dto.remove.content'; @Resolver() export class CommunityGuidelinesResolverMutations { @@ -42,4 +43,29 @@ export class CommunityGuidelinesResolverMutations { communityGuidelinesData ); } + @UseGuards(GraphqlGuard) + @Mutation(() => ICommunityGuidelines, { + description: 'Empties the CommunityGuidelines.', // Update mutation doesn't allow empty values. And we cannot really delete the entity, but this will leave it empty. + }) + @Profiling.api + async removeCommunityGuidelinesContent( + @CurrentUser() agentInfo: AgentInfo, + @Args('communityGuidelinesData') + communityGuidelinesData: RemoveCommunityGuidelinesContentInput + ): Promise { + const communityGuidelines = + await this.communityGuidelinesService.getCommunityGuidelinesOrFail( + communityGuidelinesData.communityGuidelinesID + ); + await this.authorizationService.grantAccessOrFail( + agentInfo, + communityGuidelines.authorization, + AuthorizationPrivilege.UPDATE, + `removeCommunityGuidelinesContent: ${communityGuidelines.id}` + ); + + return await this.communityGuidelinesService.eraseContent( + communityGuidelines + ); + } } diff --git a/src/domain/community/community-guidelines/community.guidelines.service.ts b/src/domain/community/community-guidelines/community.guidelines.service.ts index 5b9da2951e..f9595ee2b7 100644 --- a/src/domain/community/community-guidelines/community.guidelines.service.ts +++ b/src/domain/community/community-guidelines/community.guidelines.service.ts @@ -78,6 +78,25 @@ export class CommunityGuidelinesService { return await this.communityGuidelinesRepository.save(communityGuidelines); } + async eraseContent( + communityGuidelines: ICommunityGuidelines + ): Promise { + communityGuidelines.profile = await this.profileService.updateProfile( + communityGuidelines.profile, + { + displayName: '', + description: '', + } + ); + + await this.profileService.deleteAllReferencesFromProfile( + communityGuidelines.profile.id + ); + communityGuidelines.profile.references = []; + + return await this.communityGuidelinesRepository.save(communityGuidelines); + } + async deleteCommunityGuidelines( communityGuidelinesID: string ): Promise { diff --git a/src/domain/community/community-guidelines/dto/community.guidelines.dto.remove.content.ts b/src/domain/community/community-guidelines/dto/community.guidelines.dto.remove.content.ts new file mode 100644 index 0000000000..7dec07187f --- /dev/null +++ b/src/domain/community/community-guidelines/dto/community.guidelines.dto.remove.content.ts @@ -0,0 +1,10 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { UUID } from '@domain/common/scalars/scalar.uuid'; + +@InputType() +export class RemoveCommunityGuidelinesContentInput { + @Field(() => UUID, { + description: 'ID of the CommunityGuidelines that will be emptied', + }) + communityGuidelinesID!: string; +} From c9ffd4802d4e0df1d46db1290d2b06d14f653c25 Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Wed, 4 Dec 2024 12:08:54 +0100 Subject: [PATCH 16/22] Fix --- src/domain/template/template/template.module.ts | 2 ++ src/domain/template/template/template.service.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/domain/template/template/template.module.ts b/src/domain/template/template/template.module.ts index 02ee018c38..7c8aea6ec4 100644 --- a/src/domain/template/template/template.module.ts +++ b/src/domain/template/template/template.module.ts @@ -14,6 +14,7 @@ import { TemplateResolverFields } from './template.resolver.fields'; import { CollaborationModule } from '@domain/collaboration/collaboration/collaboration.module'; import { InputCreatorModule } from '@services/api/input-creator/input.creator.module'; import { StorageAggregatorResolverModule } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.module'; +import { InnovationFlowModule } from '@domain/collaboration/innovation-flow/innovation.flow.module'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { StorageAggregatorResolverModule } from '@services/infrastructure/storag CommunityGuidelinesModule, CalloutModule, WhiteboardModule, + InnovationFlowModule, InputCreatorModule, StorageAggregatorResolverModule, CollaborationModule, diff --git a/src/domain/template/template/template.service.ts b/src/domain/template/template/template.service.ts index 03f391a46a..375d8c4974 100644 --- a/src/domain/template/template/template.service.ts +++ b/src/domain/template/template/template.service.ts @@ -35,6 +35,7 @@ import { CalloutGroupName } from '@common/enums/callout.group.name'; import { UpdateTemplateFromCollaborationInput } from './dto/template.dto.update.from.collaboration'; import { StorageAggregatorResolverService } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service'; import { InputCreatorService } from '@services/api/input-creator/input.creator.service'; +import { InnovationFlowService } from '@domain/collaboration/innovation-flow/innovation.flow.service'; @Injectable() export class TemplateService { @@ -43,6 +44,7 @@ export class TemplateService { private communityGuidelinesService: CommunityGuidelinesService, private storageAggregatorResolverService: StorageAggregatorResolverService, private inputCreatorService: InputCreatorService, + private innovationFlowService: InnovationFlowService, private calloutService: CalloutService, private whiteboardService: WhiteboardService, private collaborationService: CollaborationService, From 8309b2465e6f439c3c902c37a6554a3e1e20f665 Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Wed, 4 Dec 2024 17:51:33 +0100 Subject: [PATCH 17/22] Callout Authorizations after updating CollabTemplate --- .../template/template.resolver.mutations.ts | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/domain/template/template/template.resolver.mutations.ts b/src/domain/template/template/template.resolver.mutations.ts index abcaccb365..1e6bd1c6d0 100644 --- a/src/domain/template/template/template.resolver.mutations.ts +++ b/src/domain/template/template/template.resolver.mutations.ts @@ -14,12 +14,16 @@ import { LogContext } from '@common/enums/logging.context'; import { ValidationException } from '@common/exceptions/validation.exception'; import { UpdateTemplateFromCollaborationInput } from './dto/template.dto.update.from.collaboration'; import { CollaborationService } from '@domain/collaboration/collaboration/collaboration.service'; +import { TemplateAuthorizationService } from './template.service.authorization'; +import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; @Resolver() export class TemplateResolverMutations { constructor( private authorizationService: AuthorizationService, + private authorizationPolicyService: AuthorizationPolicyService, private collaborationService: CollaborationService, + private templateAuthorizationService: TemplateAuthorizationService, private templateService: TemplateService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} @@ -61,7 +65,14 @@ export class TemplateResolverMutations { const template = await this.templateService.getTemplateOrFail( updateData.templateID, { - relations: { collaboration: true }, + relations: { + templatesSet: true, + collaboration: { + innovationFlow: true, + callouts: true, + tagsetTemplateSet: true, + }, + }, } ); await this.authorizationService.grantAccessOrFail( @@ -81,11 +92,21 @@ export class TemplateResolverMutations { AuthorizationPrivilege.READ, `read source collaboration for template: ${sourceCollaboration.id}` ); - return await this.templateService.updateTemplateFromCollaboration( - template, - updateData, - agentInfo.userID - ); + const templateUpdated = + await this.templateService.updateTemplateFromCollaboration( + template, + updateData, + agentInfo.userID + ); + + const authorizations = + await this.templateAuthorizationService.applyAuthorizationPolicy( + templateUpdated, + template.templatesSet?.authorization + ); + + await this.authorizationPolicyService.saveAll(authorizations); + return this.templateService.getTemplateOrFail(template.id); } @UseGuards(GraphqlGuard) From afbde69c18afce3aa48b87ba14b7f1e4479a995a Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Wed, 4 Dec 2024 17:51:54 +0100 Subject: [PATCH 18/22] Delete callouts on update collabTemplate from collab --- .../template/template/template.service.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/domain/template/template/template.service.ts b/src/domain/template/template/template.service.ts index 375d8c4974..58b6062fdb 100644 --- a/src/domain/template/template/template.service.ts +++ b/src/domain/template/template/template.service.ts @@ -293,9 +293,25 @@ export class TemplateService { } const sourceCollaboration = await this.collaborationService.getCollaborationOrFail( - templateData.collaborationID + templateData.collaborationID, + { + relations: { + innovationFlow: true, + callouts: true, + }, + } ); + if ( + templateInput.collaboration.callouts && + templateInput.collaboration.callouts.length > 0 + ) { + for (const callout of templateInput.collaboration.callouts) { + await this.calloutService.deleteCallout(callout.id); + } + templateInput.collaboration.callouts = []; + } + templateInput.collaboration = await this.updateCollaborationFromCollaboration( sourceCollaboration, @@ -304,7 +320,6 @@ export class TemplateService { userID ); - await this.collaborationService.save(templateInput.collaboration); return await this.getTemplateOrFail(templateInput.id); } From 0ae48fdfa52f5bd3ef3c0a41ea30c6ced58b7285 Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Wed, 4 Dec 2024 17:52:10 +0100 Subject: [PATCH 19/22] Find storageAggregator of CollaborationTemplate --- .../storage.aggregator.resolver.service.ts | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts b/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts index 6292e6aeee..cd4a040602 100644 --- a/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts +++ b/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts @@ -24,6 +24,7 @@ import { IAccount } from '@domain/space/account/account.interface'; import { User } from '@domain/community/user/user.entity'; import { Platform } from '@platform/platform/platform.entity'; import { TemplatesManager } from '@domain/template/templates-manager'; +import { Template } from '@domain/template/template/template.entity'; @Injectable() export class StorageAggregatorResolverService { @@ -283,13 +284,35 @@ export class StorageAggregatorResolverService { storageAggregator: true, }, }); - if (!space || !space.storageAggregator) { - throw new EntityNotFoundException( - `Unable to retrieve storage aggregator for collaborationID: ${collaborationID}`, - LogContext.STORAGE_AGGREGATOR - ); + if (space) { + if (!space.storageAggregator) { + throw new EntityNotFoundException( + `Unable to retrieve storage aggregator for space through collaborationID: ${collaborationID}`, + LogContext.STORAGE_AGGREGATOR + ); + } + return space.storageAggregator.id; } - return space.storageAggregator.id; + // If not found on Space, try with Collaboration templates + const template = await this.entityManager.findOne(Template, { + where: { + collaboration: { + id: collaborationID, + }, + }, + relations: { + templatesSet: true, + }, + }); + if (template && template.templatesSet) { + return ( + await this.getStorageAggregatorForTemplatesSet(template.templatesSet.id) + ).id; + } + throw new EntityNotFoundException( + `Unable to retrieve storage aggregator for collaborationID: ${collaborationID}`, + LogContext.STORAGE_AGGREGATOR + ); } private async getStorageAggregatorIdForCalendar( From 0c47edfeb7f7489e4e444965db9b84e3b830208c Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Thu, 5 Dec 2024 15:31:10 +0200 Subject: [PATCH 20/22] GraphiQL config at /graphiql (#4741) Co-authored-by: Neil Smyth <30729240+techsmyth@users.noreply.github.com> --- .build/ory/oathkeeper/access-rules.yml | 24 +++++++++++++++++ .build/traefik/http.yml | 6 +++++ package-lock.json | 36 ++++++++++++++++++++++++++ package.json | 2 ++ src/main.ts | 12 +++++++++ 5 files changed, 80 insertions(+) diff --git a/.build/ory/oathkeeper/access-rules.yml b/.build/ory/oathkeeper/access-rules.yml index 5ec1711141..9b55374222 100644 --- a/.build/ory/oathkeeper/access-rules.yml +++ b/.build/ory/oathkeeper/access-rules.yml @@ -23,6 +23,30 @@ - unauthorized - forbidden +- id: 'alkemio:graphiql:protected' + upstream: + preserve_host: true + url: 'http://host.docker.internal:4000' + match: + url: 'http://localhost:3000/graphiql' + methods: + - POST + - GET + authenticators: + - handler: cookie_session + - handler: noop + authorizer: + handler: allow + mutators: + - handler: id_token + errors: + - handler: redirect + config: + to: http://localhost:3000/login + when: + - error: + - unauthorized + - forbidden - id: 'alkemio:api:private:rest:storage' upstream: diff --git a/.build/traefik/http.yml b/.build/traefik/http.yml index 3c53599656..a80d3907bc 100644 --- a/.build/traefik/http.yml +++ b/.build/traefik/http.yml @@ -115,6 +115,12 @@ http: entryPoints: - 'web' + graphiql: + rule: 'PathPrefix(`/graphiql`)' + service: 'oathkeeper-proxy' + entryPoints: + - 'web' + api-public-graphql: rule: 'PathPrefix(`/api/public/graphql`)' service: 'alkemio-server' diff --git a/package-lock.json b/package-lock.json index 52f309e933..9f14a98d82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,8 @@ "dataloader": "^2.2.2", "graphql": "^16.9.0", "graphql-amqp-subscriptions": "^2.0.0", + "graphql-helix": "^1.13.0", + "graphql-http": "^1.22.3", "graphql-subscriptions": "^2.0.0", "graphql-type-json": "^0.3.2", "graphql-upload": "^13.0.0", @@ -8048,6 +8050,28 @@ "graphql-subscriptions": "2.x" } }, + "node_modules/graphql-helix": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/graphql-helix/-/graphql-helix-1.13.0.tgz", + "integrity": "sha512-cqDKMoRywKjnL0ZWCTB0GOiBgsH6d3nU4JGDF6RuzAyd35tmalzKpSxkx3NNp4H5RvnKWnrukWzR51wUq277ng==", + "peerDependencies": { + "graphql": "^15.3.0 || ^16.0.0" + } + }, + "node_modules/graphql-http": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.22.3.tgz", + "integrity": "sha512-sgUz/2DZt+QvY6WrpAsAXUvhnIkp2eX9jN78V8DAtFcpZi/nfDrzDt2byYjyoJzRcWuqhE0K63g1QMewt73U6A==", + "workspaces": [ + "implementations/**/*" + ], + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/graphql-request": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-3.7.0.tgz", @@ -20193,6 +20217,18 @@ "uuid": "9.0.1" } }, + "graphql-helix": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/graphql-helix/-/graphql-helix-1.13.0.tgz", + "integrity": "sha512-cqDKMoRywKjnL0ZWCTB0GOiBgsH6d3nU4JGDF6RuzAyd35tmalzKpSxkx3NNp4H5RvnKWnrukWzR51wUq277ng==", + "requires": {} + }, + "graphql-http": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.22.3.tgz", + "integrity": "sha512-sgUz/2DZt+QvY6WrpAsAXUvhnIkp2eX9jN78V8DAtFcpZi/nfDrzDt2byYjyoJzRcWuqhE0K63g1QMewt73U6A==", + "requires": {} + }, "graphql-request": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-3.7.0.tgz", diff --git a/package.json b/package.json index c1b86d0edd..99381a3c59 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,8 @@ "dataloader": "^2.2.2", "graphql": "^16.9.0", "graphql-amqp-subscriptions": "^2.0.0", + "graphql-helix": "^1.13.0", + "graphql-http": "^1.22.3", "graphql-subscriptions": "^2.0.0", "graphql-type-json": "^0.3.2", "graphql-upload": "^13.0.0", diff --git a/src/main.ts b/src/main.ts index 9829ff55e9..96144cbf60 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,8 @@ import cookieParser from 'cookie-parser'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { INestApplication } from '@nestjs/common'; import { AlkemioConfig } from '@src/types'; +import { renderGraphiQL } from 'graphql-helix'; +import { Request, Response } from 'express'; const bootstrap = async () => { const app = await NestFactory.create(AppModule, { @@ -71,6 +73,16 @@ const bootstrap = async () => { }) ); + // Serve the GraphiQL interface at '/graphiql' + app.use('/graphiql', (_req: Request, res: Response) => { + res.send( + renderGraphiQL({ + endpoint: '/graphql', + //subscriptionsEndpoint: '/graphql', + }) + ); + }); + await app.listen(port); const connectionOptions = configService.get( From faba13c725ee3b7aaa17865bc83e401b609feec0 Mon Sep 17 00:00:00 2001 From: Vladimir Aleksiev Date: Fri, 6 Dec 2024 11:35:15 +0200 Subject: [PATCH 21/22] Async vc engine invocation (#4683) * initial setup of invoking engines and receiving reponses trough CQRS * adding the new mesages/handlers * rename question to input and ask to invoke related to VCs adds room controller to be used by the server post replies to rooms and threads * complete implementation for the Expert engine * fix build * address comments and minor cleanups * change ask guidence question query to mutation back the guidance chat with a room implement guidence communication thorugh the event bus * update quickstart services * handle guidance room creaton trough the room service * Generate room on demand * Chat guidance room created only when needed * pending reset chat * it was already done * address comments * address some comments * Entitlements + license services (#4593) * first pass at two new modules for TemplatesManager, TemplateDefault * added templates manager to space; removed the SpaceDefaults entity (module still present until move all data to be via defaults * added templatesManager to platform * moved creating of default innovatin flow input to space defaults * back out space type on Template; tidy up Template module to use switch statements * created template applier module * tidy up naming * updated set of default template types * fixed circular dependency; moved logic for creating collaboration input to space defaults * removed loading of defaults from files for collaboration content * removed code based addition of callouts, innovation flow states * tidy up naming * added loading of default templates at platform level in to bootstrap * removed option to create new innovation flow template * added in migration: * loading in templates on bootstrap * added field for collaboration templates on templatesSet; added lookup for templatesManager * added mutation to create template from collaboration; added logic to prevent template used as default to be deleted; fixed removal of template set on template manager * initial creation of license + entitlements modules * add license into account * updated account to have license service + use that in mutations checking limits, removing notion of soft limits * ensure data is loaded properly on account for license checking * added mutation to reset the license calculations on account, including new auth privilege to be able to do so * renamed Licensing module to LicensingFramework module; trigger license reset on Account after assigning / removing license * removed usage of LicenseEngine outside of license services on space or account * renamed entitlement to licenseEntitlement as entity; first pass at migration * fixed issues in migration * fixed issues related to auth reset; tidied up loader creator imports * fixed auth cascade for templates of type post * license reset running * reset licenses on space after adding / removing license plans * removed need for license check in community; added entitlement check in roleset when adding a VC * remove auth reset when assigning / removing license plans * added License to RoleSet * added license to collaboration * tidied up retrieval of license for whiteboard; added license to collaboration in migration * fix typo; fix space spec file * fix additional tests * moved tempaltesManager to last migration in the list * fixed retrieval of template when creating collaboration * added logging * fixed bootstrap setting of templates * refactored inputCreator to do the data loading closer to usage; fixed picking up of templates; fixed bootstrap usage of templates * added ability to retrieve limits on entitlements + current usage * updated field names on entitlements * updated field names on entitlements * fixed account mutaiton logic bug * ensure that licenses are reset when assigning beta tester or vc campaign role to a user * added reset all account licenses mutation * fixed bug on space entitlements; refactored code to reduce duplication * fixed url generation for templates inside of TempaltesManager * fixed bootstrap order to create forum earlier * ensure collaboration creation on template provides some defaults for callouts * fix deletion of templates of type post * ensure more data is defaulted inside of template service for collaboration; add setting of isTemplate field on Collaboration, and also on contained Callouts * ensure isTempalte is passed to Collaboration entity * fixed groups in bootstrap space template; updated signature for creating callout from collaboration * fixed missing field * fixed type on mutation to create from collaboration * fixed typo * fixed groups in bootstrap space template; updated signature for creating callout from collaboration * fixed missing field * fixed type on mutation to create from collaboration * fixed typo * reworked applying collaboraiton template to collaboration * improved error message in wrong type of ID passed in * fixed build * made migration last in the list * rename migration to be last * removed read check when looking up collaboration * track free / plus / premium space entitlements separately * updated migration order * removed duplicate migration * moved auth reset to mutation for applying the template to another collaboration * extend lookup of entitlement usage to cover new types * updaed license policy to reflect new entitlements; made license engine work with entitlements, not license privileges; removed license privilege (no longer relevant) * updated migration to not drop indexes already removed * fix for license reset on space * added license policy rule for free space credential * ensure license entitlements are reset as part of the bootstrap * fixed typo * extended reset all to include resetting licenses on accounts + AI server; moved migration to be last * Address pr comment * Address PR feedback * Address PR comment * Address PR comments * Address PR comments * Address PR comment Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Improved types & naming * Address PR comments * Fixed switch-case logic in entitlements * Converge entitlements schema * Remove unused AuthorizationPrivilege --------- Co-authored-by: Carlos Cano Co-authored-by: Valentin Yanakiev Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Major version bump * Add license lookup * removed redundant checks * Fixed proper L0 space agent selection in recursive license assignment (#4708) * Fixed proper L0 account agent selection in recursive license assignment * provide root space agent for sub-sub space * Look up correct entitlement for VC creation (#4707) Co-authored-by: Evgeni Dimitrov * filter out demo spaces for unauthenticated users search in elastic (#4712) * filter out demo spaces for unauthenticated users search in elastic * improvements --------- Co-authored-by: Svetoslav * get guidance room from user and not from me * Finish * wrapping up * fix roomId missing * Put the GuidanceVC into the platform * Guidance VC created on bootstrap * add support for guidance ingest * cleanup and handle sources label * fix build * address comments and update engine images --------- Co-authored-by: Valentin Yanakiev Co-authored-by: Carlos Cano Co-authored-by: Neil Smyth <30729240+techsmyth@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Evgeni Dimitrov Co-authored-by: Svetoslav --- .env.docker | 10 +- quickstart-services-ai.yml | 14 +- .../enums/ai.persona.invocation.operation.ts | 4 + src/common/enums/room.type.ts | 1 + src/core/bootstrap/bootstrap.module.ts | 2 + src/core/bootstrap/bootstrap.service.ts | 46 +++++ .../microservices/client.proxy.factory.ts | 6 - .../microservices/microservices.module.ts | 2 +- ...age.answer.to.question.source.interface.ts | 18 -- ...age.guidance.question.result.interface.ts} | 18 +- ...essage.guidance.question.result.module.ts} | 2 +- .../communication/message/message.module.ts | 5 +- .../communication/message/message.service.ts | 31 ---- .../room/room.resolver.mutations.ts | 4 +- .../room/room.service.mentions.ts | 64 +++---- src/domain/communication/room/room.service.ts | 7 + .../ai-persona/ai.persona.service.ts | 21 ++- src/domain/community/user/user.entity.ts | 9 + src/domain/community/user/user.interface.ts | 3 + .../community/user/user.resolver.fields.ts | 26 +++ src/domain/community/user/user.service.ts | 42 +++++ .../virtual-contributor/dto/index.ts | 5 + .../virtual-contributor/dto/utils.ts | 16 ++ ...irtual.contributor.dto.invocation.input.ts | 81 +++++++++ .../virtual.contributor.dto.question.input.ts | 44 ----- ...tributor.dto.refresh.body.of.knowledge.ts} | 0 .../virtual.contributor.resolver.mutations.ts | 2 +- .../virtual.contributor.resolver.queries.ts | 29 --- .../virtual.contributor.service.ts | 60 ++++--- .../1731937383422-userGuidanceRoom.ts | 51 ++++++ src/platform/platform/platform.entity.ts | 9 + src/platform/platform/platform.interface.ts | 2 + .../platform.service.authorization.ts | 10 +- src/platform/platform/platform.service.ts | 19 ++ .../adapters/activity-adapter/index.ts | 0 .../ai-server-adapter/ai.server.adapter.ts | 15 +- .../dto/ai.server.adapter.dto.ask.question.ts | 13 -- .../dto/ai.server.adapter.dto.invocation.ts | 37 ++++ .../guidance.engine.adapter.ts | 20 +-- .../communication.adapter.ts | 2 +- .../ai.persona.engine.adapter.ts | 169 +----------------- .../dto/ai.persona.engine.adapter.dto.base.ts | 2 + ...na.engine.adapter.dto.invocation.input.ts} | 23 ++- .../ai.persona.service.service.ts | 41 +++-- ...te.ts => ai.persona.service.dto.delete.ts} | 0 ...ai.persona.service.invocation.dto.input.ts | 134 ++++++++++++++ .../ai.persona.service.question.dto.input.ts | 61 ------- .../ai-server/ai-persona-service/dto/index.ts | 7 +- .../ai-server/ai-persona-service/dto/utils.ts | 25 +++ .../ai-server/ai-server/ai.server.module.ts | 6 +- .../ai-server/ai.server.resolver.fields.ts | 35 +--- .../ai-server/ai-server/ai.server.service.ts | 155 +++++++++------- .../api/chat-guidance/chat.guidance.module.ts | 21 ++- .../chat.guidance.resolver.mutations.ts | 82 ++++++++- .../chat.guidance.resolver.queries.ts | 45 ----- .../chat-guidance/chat.guidance.service.ts | 117 ++++++++++-- .../dto/chat.guidance.relevance.dto.ts | 4 +- .../event-bus/event.bus.module.ts | 12 ++ .../event-bus/handlers/index.ts | 3 +- .../handlers/invoke.engine.result.handler.ts | 14 ++ .../event-bus/messages/index.ts | 8 +- .../messages/invoke.engine.result.ts | 39 ++++ .../event-bus/messages/invoke.engine.ts | 6 + .../infrastructure/event-bus/publisher.ts | 8 +- .../room.controller.service.ts | 102 +++++++++++ .../room.integration.module.ts | 15 ++ 66 files changed, 1195 insertions(+), 689 deletions(-) create mode 100644 src/common/enums/ai.persona.invocation.operation.ts delete mode 100644 src/domain/communication/message.answer.to.question/message.answer.to.question.source.interface.ts rename src/domain/communication/{message.answer.to.question/message.answer.to.question.interface.ts => message.guidance.question.result/message.guidance.question.result.interface.ts} (58%) rename src/domain/communication/{message.answer.to.question/message.answer.to.question.module.ts => message.guidance.question.result/message.guidance.question.result.module.ts} (89%) delete mode 100644 src/domain/communication/message/message.service.ts create mode 100644 src/domain/community/virtual-contributor/dto/utils.ts create mode 100644 src/domain/community/virtual-contributor/dto/virtual.contributor.dto.invocation.input.ts delete mode 100644 src/domain/community/virtual-contributor/dto/virtual.contributor.dto.question.input.ts rename src/domain/community/virtual-contributor/dto/{virtual.contributor.dto.refresh.body.of.knowlege.ts => virtual.contributor.dto.refresh.body.of.knowledge.ts} (100%) create mode 100644 src/migrations/1731937383422-userGuidanceRoom.ts delete mode 100644 src/services/adapters/activity-adapter/index.ts delete mode 100644 src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question.ts create mode 100644 src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.invocation.ts rename src/services/ai-server/ai-persona-engine-adapter/dto/{ai.persona.engine.adapter.dto.question.input.ts => ai.persona.engine.adapter.dto.invocation.input.ts} (58%) rename src/services/ai-server/ai-persona-service/dto/{ai.persona..service.dto.delete.ts => ai.persona.service.dto.delete.ts} (100%) create mode 100644 src/services/ai-server/ai-persona-service/dto/ai.persona.service.invocation.dto.input.ts delete mode 100644 src/services/ai-server/ai-persona-service/dto/ai.persona.service.question.dto.input.ts create mode 100644 src/services/ai-server/ai-persona-service/dto/utils.ts delete mode 100644 src/services/api/chat-guidance/chat.guidance.resolver.queries.ts create mode 100644 src/services/infrastructure/event-bus/handlers/invoke.engine.result.handler.ts create mode 100644 src/services/infrastructure/event-bus/messages/invoke.engine.result.ts create mode 100644 src/services/infrastructure/event-bus/messages/invoke.engine.ts create mode 100644 src/services/room-integration/room.controller.service.ts create mode 100644 src/services/room-integration/room.integration.module.ts diff --git a/.env.docker b/.env.docker index 09ff373fb3..3d7cabfb88 100644 --- a/.env.docker +++ b/.env.docker @@ -30,6 +30,9 @@ RABBITMQ_INGEST_SPACE_QUEUE=virtual-contributor-ingest-space RABBITMQ_INGEST_SPACE_RESULT_QUEUE=virtual-contributor-ingest-space-result RABBITMQ_EVENT_BUS_EXCHANGE=event-bus +RABBITMQ_RESULT_QUEUE=virtual-contributor-invoke-engine-result +RABBITMQ_RESULT_ROUTING_KEY=invoke-engine-result + ALKEMIO_SERVER_ENDPOINT=http://host.docker.internal:4000/graphql KRATOS_API_PUBLIC_ENDPOINT=http://kratos:4433/ SERVICE_ACCOUNT_USERNAME=notifications@alkem.io @@ -46,16 +49,21 @@ AI_LOCAL_PATH=/home/alkemio/data OPENAI_API_VERSION=2023-05-15 AZURE_OPENAI_ENDPOINT=https://alkemio-gpt.openai.azure.com AZURE_OPENAI_API_KEY=your-openai-key +AZURE_MISTRAL_ENDPOINT=https://Mistral-small-alkemio-serverless.swedencentral.inference.ai.azure.com +AZURE_MISTRAL_API_KEY=mistral-api-key LLM_DEPLOYMENT_NAME=deploy-gpt-35-turbo EMBEDDINGS_DEPLOYMENT_NAME=embedding AI_MODEL_TEMPERATURE=0.3 AI_SOURCE_WEBSITE=https://www.alkemio.org AI_SOURCE_WEBSITE2=https://welcome.alkem.io + AI_WEBSITE_REPO=github.com/alkem-io/website.git AI_WEBSITE_REPO2=github.com/alkem-io/welcome-site.git + AI_GITHUB_USER=your-github-user AI_GITHUB_PAT=your-github-pat -LANGCHAIN_TRACING_V2=true + +LANGCHAIN_TRACING_V2=false LANGCHAIN_ENDPOINT=https://api.smith.langchain.com LANGCHAIN_API_KEY=your-langchain-key LANGCHAIN_PROJECT=guidance-engine diff --git a/quickstart-services-ai.yml b/quickstart-services-ai.yml index 185d97a025..a78b0422d0 100644 --- a/quickstart-services-ai.yml +++ b/quickstart-services-ai.yml @@ -323,7 +323,7 @@ services: virtual_contributor_engine_guidance: container_name: alkemio_dev_virtual_contributor_engine_guidance hostname: virtual-contributor-engine-guidance - image: alkemio/virtual-contributor-engine-guidance:v0.7.1 + image: alkemio/virtual-contributor-engine-guidance:v0.8.2 platform: linux/x86_64 restart: always volumes: @@ -339,6 +339,11 @@ services: - RABBITMQ_USER - RABBITMQ_PASSWORD - RABBITMQ_QUEUE=virtual-contributor-engine-guidance + - RABBITMQ_EVENT_BUS_EXCHANGE + - RABBITMQ_RESULT_QUEUE + - RABBITMQ_RESULT_ROUTING_KEY + - AZURE_MISTRAL_ENDPOINT + - AZURE_MISTRAL_API_KEY - ENVIRONMENT=dev - EMBEDDINGS_DEPLOYMENT_NAME - AZURE_OPENAI_API_KEY @@ -396,7 +401,7 @@ services: - 'host.docker.internal:host-gateway' container_name: alkemio_dev_virtual_contributor_engine_expert hostname: virtual-contributor-engine-expert - image: alkemio/virtual-contributor-engine-expert:v0.5.0 + image: alkemio/virtual-contributor-engine-expert:v0.10.0 platform: linux/x86_64 restart: always volumes: @@ -412,6 +417,11 @@ services: - RABBITMQ_PASSWORD - RABBITMQ_PORT - RABBITMQ_QUEUE=virtual-contributor-engine-expert + - RABBITMQ_EVENT_BUS_EXCHANGE + - RABBITMQ_RESULT_QUEUE + - RABBITMQ_RESULT_ROUTING_KEY + - AZURE_MISTRAL_ENDPOINT + - AZURE_MISTRAL_API_KEY - API_ENDPOINT_PRIVATE_GRAPHQL=http://host.docker.internal:3000/api/private/non-interactive/graphql - AUTH_ORY_KRATOS_PUBLIC_BASE_URL=http://host.docker.internal:3000/ory/kratos/public - AUTH_ADMIN_EMAIL diff --git a/src/common/enums/ai.persona.invocation.operation.ts b/src/common/enums/ai.persona.invocation.operation.ts new file mode 100644 index 0000000000..78b12db46c --- /dev/null +++ b/src/common/enums/ai.persona.invocation.operation.ts @@ -0,0 +1,4 @@ +export enum InvocationOperation { + QUERY = 'query', + INGEST = 'ingest', +} diff --git a/src/common/enums/room.type.ts b/src/common/enums/room.type.ts index 8390263dc7..e3e18e5faf 100644 --- a/src/common/enums/room.type.ts +++ b/src/common/enums/room.type.ts @@ -5,4 +5,5 @@ export enum RoomType { DISCUSSION_FORUM = 'discussion_forum', UPDATES = 'updates', CALLOUT = 'callout', + GUIDANCE = 'guidance', } diff --git a/src/core/bootstrap/bootstrap.module.ts b/src/core/bootstrap/bootstrap.module.ts index 1e2fb9036f..238e7b0faa 100644 --- a/src/core/bootstrap/bootstrap.module.ts +++ b/src/core/bootstrap/bootstrap.module.ts @@ -22,10 +22,12 @@ import { TemplateDefaultModule } from '@domain/template/template-default/templat import { LicenseModule } from '@domain/common/license/license.module'; import { LicensePlanModule } from '@platform/license-plan/license.plan.module'; import { LicensingFrameworkModule } from '@platform/licensing-framework/licensing.framework.module'; +import { AiPersonaServiceModule } from '@services/ai-server/ai-persona-service/ai.persona.service.module'; @Module({ imports: [ AiServerModule, + AiPersonaServiceModule, AgentModule, AuthorizationPolicyModule, LicenseModule, diff --git a/src/core/bootstrap/bootstrap.service.ts b/src/core/bootstrap/bootstrap.service.ts index b5aca6edde..4ea3e2c2ae 100644 --- a/src/core/bootstrap/bootstrap.service.ts +++ b/src/core/bootstrap/bootstrap.service.ts @@ -59,6 +59,10 @@ import { LicenseService } from '@domain/common/license/license.service'; import { AccountLicenseService } from '@domain/space/account/account.service.license'; import { LicensePlanService } from '@platform/license-plan/license.plan.service'; import { LicensingFrameworkService } from '@platform/licensing-framework/licensing.framework.service'; +import { AiPersonaServiceService } from '@services/ai-server/ai-persona-service/ai.persona.service.service'; +import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; +import { AiPersonaBodyOfKnowledgeType } from '@common/enums/ai.persona.body.of.knowledge.type'; +import { AiPersonaDataAccessMode } from '@common/enums/ai.persona.data.access.mode'; @Injectable() export class BootstrapService { @@ -82,6 +86,7 @@ export class BootstrapService { @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService, private aiServer: AiServerService, + private aiPersonaServiceService: AiPersonaServiceService, private aiServerAuthorizationService: AiServerAuthorizationService, private templatesManagerService: TemplatesManagerService, private templatesSetService: TemplatesSetService, @@ -113,6 +118,7 @@ export class BootstrapService { await this.ensurePlatformTemplatesArePresent(); await this.ensureOrganizationSingleton(); await this.ensureSpaceSingleton(); + await this.ensureGuidanceChat(); await this.ensureSsiPopulated(); // reset auth as last in the actions // await this.ensureSpaceNamesInElastic(); @@ -524,4 +530,44 @@ export class BootstrapService { return this.spaceService.getSpaceOrFail(space.id); } } + + private async ensureGuidanceChat() { + const platform = await this.platformService.getPlatformOrFail({ + relations: { guidanceVirtualContributor: true }, + }); + if (!platform.guidanceVirtualContributor?.id) { + const aiPersonaService = + await this.aiPersonaServiceService.createAiPersonaService({ + bodyOfKnowledgeID: '', + bodyOfKnowledgeType: AiPersonaBodyOfKnowledgeType.NONE, + engine: AiPersonaEngine.GUIDANCE, + dataAccessMode: AiPersonaDataAccessMode.NONE, + prompt: [], + externalConfig: undefined, + }); + + // Get admin account: + const hostOrganization = + await this.organizationService.getOrganizationOrFail( + DEFAULT_HOST_ORG_NAMEID + ); + const account = + await this.organizationService.getAccount(hostOrganization); + + // Create the VC + const vc = await this.accountService.createVirtualContributorOnAccount({ + accountID: account.id, + aiPersona: { + aiPersonaServiceID: aiPersonaService.id, + }, + profileData: { + displayName: 'Guidance', + description: 'Guidance Virtual Contributor', + }, + }); + + platform.guidanceVirtualContributor = vc; + await this.platformService.savePlatform(platform); + } + } } diff --git a/src/core/microservices/client.proxy.factory.ts b/src/core/microservices/client.proxy.factory.ts index aaec24cc62..1f50817672 100644 --- a/src/core/microservices/client.proxy.factory.ts +++ b/src/core/microservices/client.proxy.factory.ts @@ -6,12 +6,6 @@ import { AlkemioConfig } from '@src/types'; const QUEUE_CONTEXT_MAP: { [key in MessagingQueue]?: LogContext } = { [MessagingQueue.AUTH_RESET]: LogContext.AUTH, - [MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT]: - LogContext.VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT, - [MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC]: - LogContext.VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC, - [MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT]: - LogContext.VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT, }; export const clientProxyFactory = (queue: MessagingQueue, durable = true) => { diff --git a/src/core/microservices/microservices.module.ts b/src/core/microservices/microservices.module.ts index 826a31df49..35b26063d4 100644 --- a/src/core/microservices/microservices.module.ts +++ b/src/core/microservices/microservices.module.ts @@ -99,7 +99,7 @@ const excalidrawPubSubFactoryProvider = subscriptionFactoryProvider( provide: VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, useFactory: clientProxyFactory( MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, - false + true ), inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], diff --git a/src/domain/communication/message.answer.to.question/message.answer.to.question.source.interface.ts b/src/domain/communication/message.answer.to.question/message.answer.to.question.source.interface.ts deleted file mode 100644 index 17c37cdaac..0000000000 --- a/src/domain/communication/message.answer.to.question/message.answer.to.question.source.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; - -@ObjectType('MessageAnswerToQuestionSource', { - description: 'A source used in a detailed answer to a question.', -}) -export class IAnswerToQuestionSource { - @Field(() => String, { - nullable: true, - description: 'The URI of the source', - }) - uri!: string; - - @Field(() => String, { - nullable: true, - description: 'The title of the source', - }) - title!: string; -} diff --git a/src/domain/communication/message.answer.to.question/message.answer.to.question.interface.ts b/src/domain/communication/message.guidance.question.result/message.guidance.question.result.interface.ts similarity index 58% rename from src/domain/communication/message.answer.to.question/message.answer.to.question.interface.ts rename to src/domain/communication/message.guidance.question.result/message.guidance.question.result.interface.ts index d0c1a5ba06..373d363919 100644 --- a/src/domain/communication/message.answer.to.question/message.answer.to.question.interface.ts +++ b/src/domain/communication/message.guidance.question.result/message.guidance.question.result.interface.ts @@ -1,10 +1,9 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { IAnswerToQuestionSource } from './message.answer.to.question.source.interface'; @ObjectType('MessageAnswerQuestion', { description: 'A detailed answer to a question, typically from an AI service.', }) -export class IMessageAnswerToQuestion { +export class IMessageGuidanceQuestionResult { @Field(() => String, { nullable: true, description: 'The id of the answer; null if an error was returned', @@ -17,17 +16,16 @@ export class IMessageAnswerToQuestion { }) question!: string; - @Field(() => [IAnswerToQuestionSource], { + @Field(() => String, { nullable: true, - description: 'The sources used to answer the question', + description: 'Error message if an error occurred', }) - sources?: IAnswerToQuestionSource[]; + error?: string; - @Field(() => String, { + @Field(() => Boolean, { nullable: false, - description: 'The answer to the question', + description: + 'Message successfully sent. If false, error will have the reason.', }) - answer!: string; - - threadId?: string; + success!: boolean; } diff --git a/src/domain/communication/message.answer.to.question/message.answer.to.question.module.ts b/src/domain/communication/message.guidance.question.result/message.guidance.question.result.module.ts similarity index 89% rename from src/domain/communication/message.answer.to.question/message.answer.to.question.module.ts rename to src/domain/communication/message.guidance.question.result/message.guidance.question.result.module.ts index bb9e8f1b35..0871562b01 100644 --- a/src/domain/communication/message.answer.to.question/message.answer.to.question.module.ts +++ b/src/domain/communication/message.guidance.question.result/message.guidance.question.result.module.ts @@ -11,4 +11,4 @@ import { VirtualContributor } from '@domain/community/virtual-contributor/virtua providers: [], exports: [], }) -export class MessageAnswerToQuestionModule {} +export class MessageGuidanceQuestionResultModule {} diff --git a/src/domain/communication/message/message.module.ts b/src/domain/communication/message/message.module.ts index b2210cffe0..58b02a0b30 100644 --- a/src/domain/communication/message/message.module.ts +++ b/src/domain/communication/message/message.module.ts @@ -3,14 +3,13 @@ import { MessageResolverFields } from './message.resolver.fields'; import { ContributorLookupModule } from '@services/infrastructure/contributor-lookup/contributor.lookup.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity'; -import { MessageService } from './message.service'; @Module({ imports: [ ContributorLookupModule, TypeOrmModule.forFeature([VirtualContributor]), ], - providers: [MessageResolverFields, MessageService], - exports: [MessageResolverFields, MessageService], + providers: [MessageResolverFields], + exports: [MessageResolverFields], }) export class MessageModule {} diff --git a/src/domain/communication/message/message.service.ts b/src/domain/communication/message/message.service.ts deleted file mode 100644 index 02acbac135..0000000000 --- a/src/domain/communication/message/message.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { LogContext } from '@common/enums/logging.context'; -import { IMessageAnswerToQuestion } from '../message.answer.to.question/message.answer.to.question.interface'; -import { LoggerService } from '@nestjs/common/services/logger.service'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { Inject, Injectable } from '@nestjs/common'; - -@Injectable() -export class MessageService { - constructor( - @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService - ) {} - - public convertAnswerToSimpleMessage( - answerToQuestion: IMessageAnswerToQuestion - ): string { - this.logger.verbose?.( - `Converting answer to simple message: ${JSON.stringify( - answerToQuestion - )}`, - LogContext.COMMUNICATION - ); - let answer = answerToQuestion.answer; - - if (answerToQuestion.sources) { - answer = `${answer}\n${answerToQuestion.sources - .map(({ title, uri }) => `- [${title}](${uri})`) - .join('\n')}`; - } - return answer; - } -} diff --git a/src/domain/communication/room/room.resolver.mutations.ts b/src/domain/communication/room/room.resolver.mutations.ts index 0d79758dca..1ef2887f4b 100644 --- a/src/domain/communication/room/room.resolver.mutations.ts +++ b/src/domain/communication/room/room.resolver.mutations.ts @@ -344,7 +344,7 @@ export class RoomResolverMutations { const contextSpaceID = await this.roomServiceMentions.getSpaceIdForRoom(room); - await this.roomServiceMentions.askQuestionToVirtualContributor( + await this.roomServiceMentions.invokeVirtualContributor( vcMentioned?.nameID, messageData.message, threadID, @@ -435,7 +435,7 @@ export class RoomResolverMutations { const contextSpaceID = await this.roomServiceMentions.getSpaceIdForRoom(room); - await this.roomServiceMentions.askQuestionToVirtualContributor( + await this.roomServiceMentions.invokeVirtualContributor( vcMentioned?.nameID, messageData.message, threadID, diff --git a/src/domain/communication/room/room.service.mentions.ts b/src/domain/communication/room/room.service.mentions.ts index 113205b303..3feba428e1 100644 --- a/src/domain/communication/room/room.service.mentions.ts +++ b/src/domain/communication/room/room.service.mentions.ts @@ -9,18 +9,17 @@ import { CommunityResolverService } from '@services/infrastructure/entity-resolv import { IProfile } from '@domain/common/profile'; import { Mention, MentionedEntityType } from '../messaging/mention.interface'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { MutationType } from '@common/enums/subscriptions/mutation.type'; import { LogContext } from '@common/enums/logging.context'; -import { RoomSendMessageReplyInput } from '@domain/communication/room/dto/room.dto.send.message.reply'; -import { SubscriptionPublishService } from '@services/subscriptions/subscription-service/subscription.publish.service'; import { RoomService } from './room.service'; import { VirtualContributorService } from '@domain/community/virtual-contributor/virtual.contributor.service'; import { EntityNotFoundException, EntityNotInitializedException, } from '@common/exceptions'; -import { VirtualContributorQuestionInput } from '@domain/community/virtual-contributor/dto/virtual.contributor.dto.question.input'; -import { MessageService } from '../message/message.service'; +import { + InvocationResultAction, + VirtualContributorInvocationInput, +} from '@domain/community/virtual-contributor/dto'; import { IVcInteraction } from '../vc-interaction/vc.interaction.interface'; import { ContributorLookupService } from '@services/infrastructure/contributor-lookup/contributor.lookup.service'; @@ -35,8 +34,6 @@ export class RoomServiceMentions { private notificationAdapter: NotificationAdapter, private communityResolverService: CommunityResolverService, private roomService: RoomService, - private messageService: MessageService, - private subscriptionPublishService: SubscriptionPublishService, private virtualContributorService: VirtualContributorService, private contributorLookupService: ContributorLookupService, @Inject(WINSTON_MODULE_NEST_PROVIDER) @@ -49,7 +46,7 @@ export class RoomServiceMentions { room.type as RoomType ); - // The ID of the actual community where the question is being asked + // The ID of the actual community where the vc is being invoced const space = await this.communityResolverService.getSpaceForRoleSetOrFail( community.roleSet.id ); @@ -58,7 +55,7 @@ export class RoomServiceMentions { public async processVirtualContributorMentions( mentions: Mention[], - question: string, + message: string, threadID: string, agentInfo: AgentInfo, room: IRoom @@ -87,9 +84,9 @@ export class RoomServiceMentions { }); } - await this.askQuestionToVirtualContributor( + await this.invokeVirtualContributor( vcMention.id, - question, + message, threadID, agentInfo, contextSpaceID, @@ -99,9 +96,9 @@ export class RoomServiceMentions { } } - public async askQuestionToVirtualContributor( + public async invokeVirtualContributor( uuid: string, - question: string, + message: string, threadID: string, agentInfo: AgentInfo, contextSpaceID: string, @@ -124,40 +121,23 @@ export class RoomServiceMentions { ); } - const vcQuestion: VirtualContributorQuestionInput = { + const vcInput: VirtualContributorInvocationInput = { virtualContributorID: virtualContributor.id, - question: question, + message, contextSpaceID, userID: agentInfo.userID, - threadID, - }; - - if (vcInteraction) { - vcQuestion.vcInteractionID = vcInteraction.id; - } - - const result = await this.virtualContributorService.askQuestion(vcQuestion); - - const simpleAnswer = - this.messageService.convertAnswerToSimpleMessage(result); - - const answerData: RoomSendMessageReplyInput = { - message: simpleAnswer, - roomID: room.id, - threadID: threadID, + resultHandler: { + action: InvocationResultAction.POST_REPLY, + roomDetails: { + roomID: room.id, + threadID, + communicationID: virtualContributor.communicationID, + vcInteractionID: vcInteraction?.id, + }, + }, }; - const answerMessage = await this.roomService.sendMessageReply( - room, - virtualContributor.communicationID, - answerData, - 'virtualContributor' - ); - this.subscriptionPublishService.publishRoomEvent( - room, - MutationType.CREATE, - answerMessage - ); + await this.virtualContributorService.invoke(vcInput); } public processNotificationMentions( diff --git a/src/domain/communication/room/room.service.ts b/src/domain/communication/room/room.service.ts index 28ec9cecfa..0364b3ff4e 100644 --- a/src/domain/communication/room/room.service.ts +++ b/src/domain/communication/room/room.service.ts @@ -64,6 +64,13 @@ export class RoomService { return room; } + async findRoom(options?: FindOneOptions): Promise { + const room = await this.roomRepository.findOne({ + ...options, + }); + return room; + } + async deleteRoom(roomInput: IRoom): Promise { const room = await this.getRoomOrFail(roomInput.id, { relations: { diff --git a/src/domain/community/ai-persona/ai.persona.service.ts b/src/domain/community/ai-persona/ai.persona.service.ts index 9823b5d586..663dbacfb0 100644 --- a/src/domain/community/ai-persona/ai.persona.service.ts +++ b/src/domain/community/ai-persona/ai.persona.service.ts @@ -13,10 +13,12 @@ import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { LogContext } from '@common/enums/logging.context'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; import { AiServerAdapter } from '@services/adapters/ai-server-adapter/ai.server.adapter'; -import { AiServerAdapterAskQuestionInput } from '@services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question'; +import { + AiServerAdapterInvocationInput, + InvocationResultAction, +} from '@services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.invocation'; import { AiPersonaDataAccessMode } from '@common/enums/ai.persona.data.access.mode'; import { AiPersonaInteractionMode } from '@common/enums/ai.persona.interaction.mode'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; @Injectable() @@ -127,23 +129,26 @@ export class AiPersonaService { return await this.aiPersonaRepository.save(aiPersona); } - public async askQuestion( + public invoke( aiPersona: IAiPersona, - question: string, + message: string, agentInfo: AgentInfo, contextSpaceID: string - ): Promise { + ): Promise { this.logger.verbose?.( `Asking question to AI Persona from user ${agentInfo.userID} + with context ${contextSpaceID}`, LogContext.PLATFORM ); - const input: AiServerAdapterAskQuestionInput = { - question: question, + const input: AiServerAdapterInvocationInput = { + message, displayName: '', aiPersonaServiceID: aiPersona.aiPersonaServiceID, + resultHandler: { + action: InvocationResultAction.POST_REPLY, + }, }; - return await this.aiServerAdapter.askQuestion(input); + return this.aiServerAdapter.invoke(input); } } diff --git a/src/domain/community/user/user.entity.ts b/src/domain/community/user/user.entity.ts index 35e136fa8a..a69cb5e5d8 100644 --- a/src/domain/community/user/user.entity.ts +++ b/src/domain/community/user/user.entity.ts @@ -12,6 +12,7 @@ import { Application } from '@domain/access/application/application.entity'; import { PreferenceSet } from '@domain/common/preference-set/preference.set.entity'; import { ContributorBase } from '../contributor/contributor.base.entity'; import { StorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.entity'; +import { Room } from '@domain/communication/room/room.entity'; import { MID_TEXT_LENGTH, SMALL_TEXT_LENGTH, @@ -68,4 +69,12 @@ export class User extends ContributorBase implements IUser { }) @JoinColumn() storageAggregator?: StorageAggregator; + + @OneToOne(() => Room, { + eager: false, + cascade: true, + onDelete: 'SET NULL', + }) + @JoinColumn() + guidanceRoom?: Room; } diff --git a/src/domain/community/user/user.interface.ts b/src/domain/community/user/user.interface.ts index 4f2c5e7406..2c023ccecf 100644 --- a/src/domain/community/user/user.interface.ts +++ b/src/domain/community/user/user.interface.ts @@ -3,6 +3,7 @@ import { IPreferenceSet } from '@domain/common/preference-set'; import { IContributorBase } from '../contributor/contributor.base.interface'; import { IContributor } from '../contributor/contributor.interface'; import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; +import { IRoom } from '@domain/communication/room/room.interface'; @ObjectType('User', { implements: () => [IContributor], @@ -27,6 +28,8 @@ export class IUser extends IContributorBase implements IContributor { storageAggregator?: IStorageAggregator; + guidanceRoom?: IRoom; + // Indicates if this profile is a service profile that is only used for service account style access // to the platform. Temporary measure, full service account support for later. serviceProfile!: boolean; diff --git a/src/domain/community/user/user.resolver.fields.ts b/src/domain/community/user/user.resolver.fields.ts index 83fd5314aa..e05f0c95c5 100644 --- a/src/domain/community/user/user.resolver.fields.ts +++ b/src/domain/community/user/user.resolver.fields.ts @@ -32,6 +32,7 @@ import { AuthenticationType } from '@common/enums/authentication.type'; import { UserAuthenticationResult } from './dto/roles.dto.authentication.result'; import { KratosService } from '@services/infrastructure/kratos/kratos.service'; import { Identity } from '@ory/kratos-client'; +import { IRoom } from '@domain/communication/room/room.interface'; @Resolver(() => IUser) export class UserResolverFields { @@ -270,6 +271,31 @@ export class UserResolverFields { return result; } + @UseGuards(GraphqlGuard) + @ResolveField(() => IRoom, { + nullable: true, + description: 'Guidance Chat Room for this user', + }) + async guidanceRoom( + @Parent() user: User, + @CurrentUser() agentInfo: AgentInfo + ): Promise { + const { guidanceRoom } = await this.userService.getUserOrFail(user.id, { + relations: { guidanceRoom: true }, + }); + if (!guidanceRoom) { + return undefined; + } + + this.authorizationService.grantAccessOrFail( + agentInfo, + guidanceRoom.authorization, + AuthorizationPrivilege.READ, + `guidance Room: ${guidanceRoom.id}` + ); + return guidanceRoom; + } + /** * Retrieves the authentication type associated with a given email. * diff --git a/src/domain/community/user/user.service.ts b/src/domain/community/user/user.service.ts index d43fa8918b..eecd2ad192 100644 --- a/src/domain/community/user/user.service.ts +++ b/src/domain/community/user/user.service.ts @@ -64,6 +64,8 @@ import { ContributorService } from '../contributor/contributor.service'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { AccountType } from '@common/enums/account.type'; import { KratosService } from '@services/infrastructure/kratos/kratos.service'; +import { IRoom } from '@domain/communication/room/room.interface'; +import { RoomType } from '@common/enums/room.type'; @Injectable() export class UserService { @@ -136,6 +138,8 @@ export class UserService { await this.storageAggregatorService.createStorageAggregator( StorageAggregatorType.USER ); + // Do not create the guidance room here, it will be created on demand + user.profile = await this.profileService.createProfile( profileData, ProfileType.USER, @@ -372,6 +376,7 @@ export class UserService { agent: true, preferenceSet: true, storageAggregator: true, + guidanceRoom: true, }, }); @@ -410,6 +415,10 @@ export class UserService { await this.storageAggregatorService.delete(user.storageAggregator.id); } + if (user.guidanceRoom) { + await this.roomService.deleteRoom(user.guidanceRoom); + } + if (deleteData.deleteIdentity) { await this.kratosService.deleteIdentityByEmail(user.email); } @@ -834,6 +843,39 @@ export class UserService { return storageAggregator; } + async getGuidanceRoom(userID: string): Promise { + const userWithGuidanceRoom = await this.getUserOrFail(userID, { + relations: { + guidanceRoom: true, + }, + }); + return userWithGuidanceRoom.guidanceRoom; + } + + public async createGuidanceRoom(userId: string): Promise { + const user = await this.getUserOrFail(userId, { + relations: { + guidanceRoom: true, + }, + }); + + if (user.guidanceRoom) { + throw new Error( + `Guidance room already exists for user with ID: ${userId}` + ); + } + + const room = await this.roomService.createRoom( + `${user.communicationID}-guidance`, + RoomType.GUIDANCE + ); + + user.guidanceRoom = room; + await this.save(user); + + return room; + } + private tryRegisterUserCommunication( user: IUser ): Promise { diff --git a/src/domain/community/virtual-contributor/dto/index.ts b/src/domain/community/virtual-contributor/dto/index.ts index 450b1e47d5..ca65a97c79 100644 --- a/src/domain/community/virtual-contributor/dto/index.ts +++ b/src/domain/community/virtual-contributor/dto/index.ts @@ -1,3 +1,8 @@ export * from './virtual.contributor.dto.create'; export * from './virtual.contributor.dto.update'; export * from './virtual.contributor.dto.delete'; +export * from './virtual.contributor.dto.invocation.input'; +export * from './virtual.contributor.dto.refresh.body.of.knowledge'; +export * from './virtual.contributor.updated.subscription.args'; +export * from './virtual.contributor.updated.subscription.result'; +export * from './utils'; diff --git a/src/domain/community/virtual-contributor/dto/utils.ts b/src/domain/community/virtual-contributor/dto/utils.ts new file mode 100644 index 0000000000..67e11fae27 --- /dev/null +++ b/src/domain/community/virtual-contributor/dto/utils.ts @@ -0,0 +1,16 @@ +import { + InvocationResultAction, + VirtualContributorInvocationInput, +} from './virtual.contributor.dto.invocation.input'; + +export const isInputValidForAction = ( + input: VirtualContributorInvocationInput, + action: InvocationResultAction +) => { + if (action === InvocationResultAction.POST_REPLY) { + return ( + input.resultHandler.action === action && input.resultHandler.roomDetails + ); + } + return true; +}; diff --git a/src/domain/community/virtual-contributor/dto/virtual.contributor.dto.invocation.input.ts b/src/domain/community/virtual-contributor/dto/virtual.contributor.dto.invocation.input.ts new file mode 100644 index 0000000000..4f4985aa73 --- /dev/null +++ b/src/domain/community/virtual-contributor/dto/virtual.contributor.dto.invocation.input.ts @@ -0,0 +1,81 @@ +import { UUID } from '@domain/common/scalars'; +import { Field, InputType } from '@nestjs/graphql'; + +export enum InvocationResultAction { + POST_REPLY = 'postReply', + POST_MESSAGE = 'postMessage', + NONE = 'none', +} + +@InputType() +export class RoomDetails { + @Field(() => String, { + nullable: false, + description: 'The room to which the reply shold be posted.', + }) + roomID!: string; + @Field(() => String, { + nullable: false, + description: 'The thread to which the reply shold be posted.', + }) + threadID!: string; + @Field(() => String, { + nullable: false, + description: 'The communicationID for the VC', + }) + communicationID!: string; + @Field(() => String, { + nullable: true, + description: + 'The Virtual Contributor interaciton part of which is this question', + }) + vcInteractionID?: string | undefined = undefined; +} + +@InputType() +export class ResultHandler { + @Field(() => InvocationResultAction, { + nullable: false, + description: + 'The action that should be taken with the result of the invocation', + }) + action!: InvocationResultAction; + @Field(() => RoomDetails, { + nullable: true, + description: 'The context needed for the result handler', + }) + roomDetails?: RoomDetails = undefined; +} + +@InputType() +export class VirtualContributorInvocationInput { + @Field(() => UUID, { + nullable: false, + description: 'Virtual Contributor to be asked.', + }) + virtualContributorID!: string; + + @Field(() => String, { + nullable: false, + description: 'The message for the virtual contributor invocation.', + }) + message!: string; + + @Field(() => String, { + nullable: true, + description: 'The context space for the Virtual Contributor invocation', + }) + contextSpaceID: string | undefined = undefined; + + @Field(() => String, { + nullable: true, + description: 'User identifier used internaly by the engine', + }) + userID: string | undefined = undefined; + + @Field(() => ResultHandler, { + nullable: false, + description: 'What should happen with the result of the VC invocation', + }) + resultHandler!: ResultHandler; +} diff --git a/src/domain/community/virtual-contributor/dto/virtual.contributor.dto.question.input.ts b/src/domain/community/virtual-contributor/dto/virtual.contributor.dto.question.input.ts deleted file mode 100644 index bbb2e4b839..0000000000 --- a/src/domain/community/virtual-contributor/dto/virtual.contributor.dto.question.input.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { UUID } from '@domain/common/scalars'; -import { Field, InputType } from '@nestjs/graphql'; - -@InputType() -export class VirtualContributorQuestionInput { - @Field(() => UUID, { - nullable: false, - description: 'Virtual Contributor to be asked.', - }) - virtualContributorID!: string; - - @Field(() => String, { - nullable: false, - description: 'The question that is being asked.', - }) - question!: string; - - @Field(() => String, { - nullable: true, - description: - 'The space in which context the Virtual Contributor is asked a question', - }) - contextSpaceID: string | undefined = undefined; - - @Field(() => String, { - nullable: true, - description: 'User identifier used internaly by the engine', - }) - userID: string | undefined = undefined; - - @Field(() => String, { - nullable: true, - description: - 'The ID of the message thread where the Virtual Contributor is asked a question', - }) - threadID?: string | undefined = undefined; - - @Field(() => String, { - nullable: true, - description: - 'The Virtual Contributor interaciton part of which is this question', - }) - vcInteractionID?: string | undefined = undefined; -} diff --git a/src/domain/community/virtual-contributor/dto/virtual.contributor.dto.refresh.body.of.knowlege.ts b/src/domain/community/virtual-contributor/dto/virtual.contributor.dto.refresh.body.of.knowledge.ts similarity index 100% rename from src/domain/community/virtual-contributor/dto/virtual.contributor.dto.refresh.body.of.knowlege.ts rename to src/domain/community/virtual-contributor/dto/virtual.contributor.dto.refresh.body.of.knowledge.ts diff --git a/src/domain/community/virtual-contributor/virtual.contributor.resolver.mutations.ts b/src/domain/community/virtual-contributor/virtual.contributor.resolver.mutations.ts index 81a568a5b0..ba6db40e19 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.resolver.mutations.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.resolver.mutations.ts @@ -11,7 +11,7 @@ import { DeleteVirtualContributorInput, UpdateVirtualContributorInput, } from './dto'; -import { RefreshVirtualContributorBodyOfKnowledgeInput } from './dto/virtual.contributor.dto.refresh.body.of.knowlege'; +import { RefreshVirtualContributorBodyOfKnowledgeInput } from './dto/virtual.contributor.dto.refresh.body.of.knowledge'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; @ObjectType('MigrateEmbeddings') diff --git a/src/domain/community/virtual-contributor/virtual.contributor.resolver.queries.ts b/src/domain/community/virtual-contributor/virtual.contributor.resolver.queries.ts index 8fb935b23b..00be930864 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.resolver.queries.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.resolver.queries.ts @@ -10,8 +10,6 @@ import { AuthorizationPrivilege } from '@common/enums'; import { PlatformAuthorizationPolicyService } from '@platform/authorization/platform.authorization.policy.service'; import { UseGuards } from '@nestjs/common'; import { GraphqlGuard } from '@core/authorization'; -import { VirtualContributorQuestionInput } from './dto/virtual.contributor.dto.question.input'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; @Resolver() export class VirtualContributorResolverQueries { @@ -55,31 +53,4 @@ export class VirtualContributorResolverQueries { ): Promise { return await this.virtualContributorService.getVirtualContributorOrFail(id); } - - @UseGuards(GraphqlGuard) - @Query(() => IMessageAnswerToQuestion, { - nullable: false, - description: 'Ask the virtual contributor a question directly.', - }) - async askVirtualContributorQuestion( - @CurrentUser() agentInfo: AgentInfo, - @Args('virtualContributorQuestionInput') - virtualContributorQuestionInput: VirtualContributorQuestionInput - ): Promise { - const virtualContributor = - await this.virtualContributorService.getVirtualContributorOrFail( - virtualContributorQuestionInput.virtualContributorID - ); - this.authorizationService.grantAccessOrFail( - agentInfo, - virtualContributor.authorization, - AuthorizationPrivilege.READ, - `asking a question to virtual contributor (${virtualContributor.id}): $chatData.question` - ); - virtualContributorQuestionInput.userID = - virtualContributorQuestionInput.userID ?? agentInfo.userID; - return this.virtualContributorService.askQuestion( - virtualContributorQuestionInput - ); - } } diff --git a/src/domain/community/virtual-contributor/virtual.contributor.service.ts b/src/domain/community/virtual-contributor/virtual.contributor.service.ts index 978679f7c5..b5b7bac53e 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.service.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.service.ts @@ -28,12 +28,15 @@ import { CommunicationAdapter } from '@services/adapters/communication-adapter/c import { NamingService } from '@services/infrastructure/naming/naming.service'; import { AiPersonaService } from '../ai-persona/ai.persona.service'; import { CreateAiPersonaInput } from '../ai-persona/dto'; -import { VirtualContributorQuestionInput } from './dto/virtual.contributor.dto.question.input'; +import { + InvocationResultAction, + VirtualContributorInvocationInput, + isInputValidForAction, +} from './dto'; import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { AiServerAdapter } from '@services/adapters/ai-server-adapter/ai.server.adapter'; -import { AiServerAdapterAskQuestionInput } from '@services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question'; +import { AiServerAdapterInvocationInput } from '@services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.invocation'; import { SearchVisibility } from '@common/enums/search.visibility'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; import { IAiPersona } from '../ai-persona'; import { IContributor } from '../contributor/contributor.interface'; import { AccountHostService } from '@domain/space/account.host/account.host.service'; @@ -371,11 +374,11 @@ export class VirtualContributorService { ); } - public async askQuestion( - vcQuestionInput: VirtualContributorQuestionInput - ): Promise { + public async invoke( + invocationInput: VirtualContributorInvocationInput + ): Promise { const virtualContributor = await this.getVirtualContributorOrFail( - vcQuestionInput.virtualContributorID, + invocationInput.virtualContributorID, { relations: { authorization: true, @@ -387,41 +390,42 @@ export class VirtualContributorService { ); if (!virtualContributor.agent) { throw new EntityNotInitializedException( - `Virtual Contributor Agent not initialized: ${vcQuestionInput.virtualContributorID}`, + `Virtual Contributor Agent not initialized: ${invocationInput.virtualContributorID}`, LogContext.AUTH ); } + this.logger.verbose?.( - `still need to use the context ${vcQuestionInput.contextSpaceID}, ${vcQuestionInput.userID}`, + `still need to use the context ${invocationInput.contextSpaceID}, ${invocationInput.userID}`, LogContext.AI_PERSONA_SERVICE_ENGINE ); - const vcInteraction = - await this.vcInteractionService.getVcInteractionOrFail( - vcQuestionInput.vcInteractionID! - ); - - const aiServerAdapterQuestionInput: AiServerAdapterAskQuestionInput = { + const aiServerAdapterInvocationInput: AiServerAdapterInvocationInput = { aiPersonaServiceID: virtualContributor.aiPersona.aiPersonaServiceID, - question: vcQuestionInput.question, - contextID: vcQuestionInput.contextSpaceID, - userID: vcQuestionInput.userID, - threadID: vcQuestionInput.threadID, - vcInteractionID: vcInteraction.id, - externalMetadata: vcInteraction.externalMetadata, + message: invocationInput.message, + contextID: invocationInput.contextSpaceID, + userID: invocationInput.userID, description: virtualContributor.profile.description, displayName: virtualContributor.profile.displayName, + resultHandler: invocationInput.resultHandler, }; - const response = await this.aiServerAdapter.askQuestion( - aiServerAdapterQuestionInput - ); - - if (!vcInteraction.externalMetadata.threadId && response.threadId) { - vcInteraction.externalMetadata.threadId = response.threadId; - await this.vcInteractionService.save(vcInteraction); + if ( + isInputValidForAction(invocationInput, InvocationResultAction.POST_REPLY) + ) { + const vcInteraction = + await this.vcInteractionService.getVcInteractionOrFail( + invocationInput.resultHandler.roomDetails!.vcInteractionID! + ); + + aiServerAdapterInvocationInput.vcInteractionID = vcInteraction.id; + aiServerAdapterInvocationInput.externalMetadata = + vcInteraction.externalMetadata; } + const response = await this.aiServerAdapter.invoke( + aiServerAdapterInvocationInput + ); return response; } diff --git a/src/migrations/1731937383422-userGuidanceRoom.ts b/src/migrations/1731937383422-userGuidanceRoom.ts new file mode 100644 index 0000000000..4b3bbbaf29 --- /dev/null +++ b/src/migrations/1731937383422-userGuidanceRoom.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UserGuidanceRoom1731937383422 implements MigrationInterface { + name = 'UserGuidanceRoom1731937383422'; + + public async up(queryRunner: QueryRunner): Promise { + // Add Guidance Room to user + await queryRunner.query( + `ALTER TABLE \`user\` ADD \`guidanceRoomId\` char(36) NULL` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_67c9d8c51a7033bbe9355f7609\` ON \`user\` (\`guidanceRoomId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`user\` ADD CONSTRAINT \`FK_67c9d8c51a7033bbe9355f76095\` FOREIGN KEY (\`guidanceRoomId\`) REFERENCES \`room\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + + // Add Guidance VC to platform + await queryRunner.query( + `ALTER TABLE \`platform\` ADD \`guidanceVirtualContributorId\` char(36) NULL` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_8e78677ceea32e003ff23d463d\` ON \`platform\` (\`guidanceVirtualContributorId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` ADD CONSTRAINT \`FK_8e78677ceea32e003ff23d463dd\` FOREIGN KEY (\`guidanceVirtualContributorId\`) REFERENCES \`virtual_contributor\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`user\` DROP FOREIGN KEY \`FK_67c9d8c51a7033bbe9355f76095\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_67c9d8c51a7033bbe9355f7609\` ON \`user\`` + ); + await queryRunner.query( + `ALTER TABLE \`user\` DROP COLUMN \`guidanceRoomId\`` + ); + + await queryRunner.query( + `ALTER TABLE \`platform\` DROP FOREIGN KEY \`FK_8e78677ceea32e003ff23d463dd\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_8e78677ceea32e003ff23d463d\` ON \`platform\`` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` DROP COLUMN \`guidanceVirtualContributorId\`` + ); + } +} diff --git a/src/platform/platform/platform.entity.ts b/src/platform/platform/platform.entity.ts index 5ac831e1ff..82a656cf26 100644 --- a/src/platform/platform/platform.entity.ts +++ b/src/platform/platform/platform.entity.ts @@ -7,6 +7,7 @@ import { Forum } from '@platform/forum/forum.entity'; import { PlatformInvitation } from '@platform/invitation/platform.invitation.entity'; import { TemplatesManager } from '@domain/template/templates-manager/templates.manager.entity'; import { LicensingFramework } from '@platform/licensing-framework/licensing.framework.entity'; +import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity'; @Entity() export class Platform extends AuthorizableEntity implements IPlatform { @@ -59,4 +60,12 @@ export class Platform extends AuthorizableEntity implements IPlatform { } ) platformInvitations!: PlatformInvitation[]; + + @OneToOne(() => VirtualContributor, { + eager: false, + cascade: true, + onDelete: 'SET NULL', + }) + @JoinColumn() + guidanceVirtualContributor?: VirtualContributor; } diff --git a/src/platform/platform/platform.interface.ts b/src/platform/platform/platform.interface.ts index 0b8b70dcc2..e493146767 100644 --- a/src/platform/platform/platform.interface.ts +++ b/src/platform/platform/platform.interface.ts @@ -1,4 +1,5 @@ import { IAuthorizable } from '@domain/common/entity/authorizable-entity'; +import { IVirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.interface'; import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; import { ITemplatesManager } from '@domain/template/templates-manager/templates.manager.interface'; import { ILibrary } from '@library/library/library.interface'; @@ -16,6 +17,7 @@ export abstract class IPlatform extends IAuthorizable { configuration?: IConfig; metadata?: IMetadata; storageAggregator!: IStorageAggregator; + guidanceVirtualContributor?: IVirtualContributor; licensingFramework?: ILicensingFramework; platformInvitations!: IPlatformInvitation[]; templatesManager?: ITemplatesManager; diff --git a/src/platform/platform/platform.service.authorization.ts b/src/platform/platform/platform.service.authorization.ts index 4c205f90dc..c3e7d56051 100644 --- a/src/platform/platform/platform.service.authorization.ts +++ b/src/platform/platform/platform.service.authorization.ts @@ -221,20 +221,20 @@ export class PlatformAuthorizationService { } private async createCredentialRuleInteractiveGuidance(): Promise { - const userGuidanceChatAccessCredential = { + const userChatGuidanceAccessCredential = { type: AuthorizationCredential.GLOBAL_REGISTERED, resourceID: '', }; - const userGuidanceChatAccessPrivilegeRule = + const userChatGuidanceAccessPrivilegeRule = this.authorizationPolicyService.createCredentialRule( [AuthorizationPrivilege.ACCESS_INTERACTIVE_GUIDANCE], - [userGuidanceChatAccessCredential], + [userChatGuidanceAccessCredential], CREDENTIAL_RULE_TYPES_PLATFORM_ACCESS_GUIDANCE ); - userGuidanceChatAccessPrivilegeRule.cascade = false; + userChatGuidanceAccessPrivilegeRule.cascade = false; - return userGuidanceChatAccessPrivilegeRule; + return userChatGuidanceAccessPrivilegeRule; } private createPlatformCredentialRules(): IAuthorizationPolicyRuleCredential[] { diff --git a/src/platform/platform/platform.service.ts b/src/platform/platform/platform.service.ts index 15e407982a..f4af3f7006 100644 --- a/src/platform/platform/platform.service.ts +++ b/src/platform/platform/platform.service.ts @@ -21,6 +21,7 @@ import { ForumDiscussionCategory } from '@common/enums/forum.discussion.category import { Discussion } from '@platform/forum-discussion/discussion.entity'; import { ITemplatesManager } from '@domain/template/templates-manager/templates.manager.interface'; import { ILicensingFramework } from '@platform/licensing-framework/licensing.framework.interface'; +import { IVirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.interface'; @Injectable() export class PlatformService { @@ -132,6 +133,24 @@ export class PlatformService { return storageAggregator; } + async getGuidanceVirtualContributorOrFail(): Promise { + const platform = await this.getPlatformOrFail({ + relations: { + guidanceVirtualContributor: { + aiPersona: true, + }, + }, + }); + const guidanceVC = platform.guidanceVirtualContributor; + if (!guidanceVC) { + throw new EntityNotFoundException( + 'Unable to find Virtual Contributor for Guidance on Platform', + LogContext.PLATFORM + ); + } + return guidanceVC; + } + async getLicensingFramework( platformInput: IPlatform ): Promise { diff --git a/src/services/adapters/activity-adapter/index.ts b/src/services/adapters/activity-adapter/index.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/services/adapters/ai-server-adapter/ai.server.adapter.ts b/src/services/adapters/ai-server-adapter/ai.server.adapter.ts index 8a8ce73e3b..c05567cd85 100644 --- a/src/services/adapters/ai-server-adapter/ai.server.adapter.ts +++ b/src/services/adapters/ai-server-adapter/ai.server.adapter.ts @@ -1,10 +1,9 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { AiServerAdapterAskQuestionInput } from './dto/ai.server.adapter.dto.ask.question'; +import { AiServerAdapterInvocationInput } from './dto/ai.server.adapter.dto.invocation'; import { AiServerService } from '@services/ai-server/ai-server/ai.server.service'; import { CreateAiPersonaServiceInput } from '@services/ai-server/ai-persona-service/dto'; import { IAiPersonaService } from '@services/ai-server/ai-persona-service'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; import { AiPersonaBodyOfKnowledgeType } from '@common/enums/ai.persona.body.of.knowledge.type'; import { LogContext } from '@common/enums'; @@ -62,14 +61,10 @@ export class AiServerAdapter { return this.aiServer.createAiPersonaService(personaServiceData); } - async askQuestion( - questionInput: AiServerAdapterAskQuestionInput - ): Promise { - const vcInteractionID = questionInput.vcInteractionID; - return this.aiServer.askQuestion({ - ...questionInput, - externalMetadata: questionInput.externalMetadata || {}, - interactionID: vcInteractionID, + invoke(invocationInput: AiServerAdapterInvocationInput): Promise { + return this.aiServer.invoke({ + ...invocationInput, + externalMetadata: invocationInput.externalMetadata || {}, }); } } diff --git a/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question.ts b/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question.ts deleted file mode 100644 index 4cb3061f4e..0000000000 --- a/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ExternalMetadata } from '@domain/communication/vc-interaction/vc.interaction.entity'; - -export class AiServerAdapterAskQuestionInput { - question!: string; - aiPersonaServiceID!: string; - contextID?: string; - userID?: string; - threadID?: string; - vcInteractionID?: string; - description?: string; - displayName!: string; - externalMetadata?: ExternalMetadata = {}; -} diff --git a/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.invocation.ts b/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.invocation.ts new file mode 100644 index 0000000000..a1a3c49f1c --- /dev/null +++ b/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.invocation.ts @@ -0,0 +1,37 @@ +import { ExternalMetadata } from '@domain/communication/vc-interaction/vc.interaction.entity'; + +export enum InvocationOperation { + QUERY = 'query', + INGEST = 'ingest', +} + +export enum InvocationResultAction { + POST_REPLY = 'postReply', + POST_MESSAGE = 'postMessage', + NONE = 'none', +} +export class RoomDetails { + roomID!: string; + threadID?: string; + communicationID!: string; + vcInteractionID?: string; +} + +export class ResultHandler { + action!: InvocationResultAction; + roomDetails?: RoomDetails = undefined; +} + +export class AiServerAdapterInvocationInput { + operation?: InvocationOperation = InvocationOperation.QUERY; + message!: string; + aiPersonaServiceID!: string; + contextID?: string; + userID?: string; + vcInteractionID?: string; + description?: string; + displayName!: string; + externalMetadata?: ExternalMetadata = {}; + resultHandler!: ResultHandler; + language?: string; +} diff --git a/src/services/adapters/chat-guidance-adapter/guidance.engine.adapter.ts b/src/services/adapters/chat-guidance-adapter/guidance.engine.adapter.ts index e7bb31b9eb..76668540a0 100644 --- a/src/services/adapters/chat-guidance-adapter/guidance.engine.adapter.ts +++ b/src/services/adapters/chat-guidance-adapter/guidance.engine.adapter.ts @@ -10,7 +10,7 @@ import { GuidanceEngineQueryResponse } from './dto/guidance.engine.dto.question. import { Source } from './source.type'; import { GuidanceReporterService } from '@services/external/elasticsearch/guidance-reporter'; import { VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE } from '@common/constants'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; +import { IMessageGuidanceQuestionResult } from '@domain/communication/message.guidance.question.result/message.guidance.question.result.interface'; enum GuidanceEngineEventType { QUERY = 'query', @@ -33,7 +33,7 @@ export class GuidanceEngineAdapter { public async sendQuery( eventData: GuidanceEngineQueryInput - ): Promise { + ): Promise { let responseData: GuidanceEngineQueryResponse | undefined; try { @@ -47,7 +47,8 @@ export class GuidanceEngineAdapter { this.logger.error(errorMessage, undefined, LogContext.CHAT_GUIDANCE); // not a real answer; just return an error return { - answer: errorMessage, + success: false, + error: errorMessage, question: eventData.question, }; } @@ -79,7 +80,8 @@ export class GuidanceEngineAdapter { this.logger.error(errorMessage, err?.stack, LogContext.CHAT_GUIDANCE); // not a real answer; just return an error return { - answer: errorMessage, + success: false, + error: errorMessage, question: eventData.question, }; } @@ -92,9 +94,8 @@ export class GuidanceEngineAdapter { ); try { - const responseData = await firstValueFrom( - response - ); + const responseData = + await firstValueFrom(response); return responseData.result === successfulResetResponse; } catch (err: any) { @@ -116,9 +117,8 @@ export class GuidanceEngineAdapter { ); try { - const responseData = await firstValueFrom( - response - ); + const responseData = + await firstValueFrom(response); return responseData.result === successfulIngestionResponse; } catch (err: any) { this.logger.error( diff --git a/src/services/adapters/communication-adapter/communication.adapter.ts b/src/services/adapters/communication-adapter/communication.adapter.ts index 00f6f50ad1..1c031ddab5 100644 --- a/src/services/adapters/communication-adapter/communication.adapter.ts +++ b/src/services/adapters/communication-adapter/communication.adapter.ts @@ -551,7 +551,7 @@ export class CommunicationAdapter { return responseData.rooms.map(room => { return { ...room, - messages: room.messages.map(message => { + messages: (room.messages || []).map(message => { return { ...message, senderType: 'user', diff --git a/src/services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter.ts b/src/services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter.ts index fa940fce5e..fefa06888c 100644 --- a/src/services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter.ts +++ b/src/services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter.ts @@ -2,23 +2,13 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { ClientProxy } from '@nestjs/microservices'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { firstValueFrom } from 'rxjs'; -import { - VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT, - VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, - VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER, - VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC, - VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT, -} from '@common/constants'; -import { Source } from '../../adapters/chat-guidance-adapter/source.type'; -import { AiPersonaEngineAdapterQueryInput } from './dto/ai.persona.engine.adapter.dto.question.input'; -import { AiPersonaEngineAdapterQueryResponse } from './dto/ai.persona.engine.adapter.dto.question.response'; +import { VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER } from '@common/constants'; +import { AiPersonaEngineAdapterInvocationInput } from './dto/ai.persona.engine.adapter.dto.invocation.input'; import { LogContext } from '@common/enums/logging.context'; import { AiPersonaEngineAdapterInputBase } from './dto/ai.persona.engine.adapter.dto.base'; import { AiPersonaEngineAdapterBaseResponse } from './dto/ai.persona.engine.adapter.dto.base.response'; -import { ChatGuidanceInput } from '@services/api/chat-guidance/dto/chat.guidance.dto.input'; -import { ValidationException } from '@common/exceptions'; -import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; +import { EventBus } from '@nestjs/cqrs'; +import { InvokeEngine } from '@services/infrastructure/event-bus/messages/invoke.engine'; enum AiPersonaEngineEventType { QUERY = 'query', @@ -34,133 +24,13 @@ export class AiPersonaEngineAdapter { constructor( @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER) private virtualContributorEngineCommunityManager: ClientProxy, - @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT) - private virtualContributorEngineExpert: ClientProxy, - @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC) - private virtualContributorEngineGeneric: ClientProxy, - @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT) - private virtualContributorEngineOpenaiAssistant: ClientProxy, - @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE) - private virtualContributorEngineGuidance: ClientProxy, @Inject(WINSTON_MODULE_NEST_PROVIDER) - private readonly logger: LoggerService + private readonly logger: LoggerService, + private eventBus: EventBus ) {} - public async sendQuery( - eventData: AiPersonaEngineAdapterQueryInput - ): Promise { - let responseData: AiPersonaEngineAdapterQueryResponse | undefined; - try { - switch (eventData.engine) { - case AiPersonaEngine.COMMUNITY_MANAGER: - if (!eventData.prompt) - throw new ValidationException( - 'Prompt property is required for community manager engine!', - LogContext.AI_PERSONA_SERVICE_ENGINE - ); - const responseCommunityManager = - this.virtualContributorEngineCommunityManager.send< - AiPersonaEngineAdapterQueryResponse, - AiPersonaEngineAdapterQueryInput - >({ cmd: AiPersonaEngineEventType.QUERY }, eventData); - responseData = await firstValueFrom(responseCommunityManager); - break; - case AiPersonaEngine.GENERIC_OPENAI: - const responseGeneric = this.virtualContributorEngineGeneric.send< - AiPersonaEngineAdapterQueryResponse, - AiPersonaEngineAdapterQueryInput - >({ cmd: AiPersonaEngineEventType.QUERY }, eventData); - responseData = await firstValueFrom(responseGeneric); - break; - case AiPersonaEngine.OPENAI_ASSISTANT: - const responseOpenaiAssistant = - this.virtualContributorEngineOpenaiAssistant.send< - AiPersonaEngineAdapterQueryResponse, - AiPersonaEngineAdapterQueryInput - >({ cmd: AiPersonaEngineEventType.QUERY }, eventData); - responseData = await firstValueFrom(responseOpenaiAssistant); - break; - case AiPersonaEngine.EXPERT: - if (!eventData.contextID || !eventData.bodyOfKnowledgeID) - throw new ValidationException( - 'ContextSpaceNameID and knowledgeSpaceNameID properties are required for expert engine!', - LogContext.AI_PERSONA_SERVICE_ENGINE - ); - const responseExpert = this.virtualContributorEngineExpert.send< - AiPersonaEngineAdapterQueryResponse, - AiPersonaEngineAdapterQueryInput - >({ cmd: AiPersonaEngineEventType.QUERY }, eventData); - responseData = await firstValueFrom(responseExpert); - break; - case AiPersonaEngine.GUIDANCE: - const responseGuidance = this.virtualContributorEngineGuidance.send< - AiPersonaEngineAdapterQueryResponse, - ChatGuidanceInput - >({ cmd: AiPersonaEngineEventType.QUERY }, { - ...eventData, - language: 'EN', - } as ChatGuidanceInput); - responseData = await firstValueFrom(responseGuidance); - break; - } - } catch (e) { - const errorMessage = `Error received from guidance chat server! ${e}`; - this.logger.error( - errorMessage, - undefined, - LogContext.AI_PERSONA_SERVICE_ENGINE - ); - // not a real answer; just return an error - return { - answer: errorMessage, - question: eventData.question, - }; - } - - if (!responseData) { - const errorMessage = `Unable to get a response from virtual persona engine ('${eventData.engine}') server!`; - this.logger.error( - errorMessage, - undefined, - LogContext.AI_PERSONA_SERVICE_ENGINE - ); - // not a real answer; just return an error - return { - answer: errorMessage, - question: eventData.question, - }; - } - - const message = responseData.result; - let formattedString = message; - // Check if response is a string containing stringified JSON - if (typeof message === 'string' && message.startsWith('{')) { - formattedString = message - .replace(/\\\\n/g, ' ') - .replace(/\\\\"/g, '') - .replace(/<\|im_end\|>/g, ''); - } - - try { - const jsonObject = JSON.parse(formattedString); - - return { - ...jsonObject, - sources: jsonObject.sources, - }; - } catch (err: any) { - const errorMessage = `Could not send query to chat guidance adapter! ${err}`; - this.logger.error( - errorMessage, - err?.stack, - LogContext.AI_PERSONA_SERVICE_ENGINE - ); - // not a real answer; just return an error - return { - answer: errorMessage, - question: eventData.question, - }; - } + public invoke(eventData: AiPersonaEngineAdapterInvocationInput): void { + this.eventBus.publish(new InvokeEngine(eventData)); } public async sendReset( @@ -207,27 +77,4 @@ export class AiPersonaEngineAdapter { return false; } } - - private extractMetadata(metadata: string): Source[] { - // deduplicate sources - const sourceSet = new Set(); - const metadataObjects: Source[] = []; - - // Loop through metadata matches and extract source and title - for (const metadataMatch of metadata) { - const uri = metadataMatch; - const title = metadataMatch; - - const sourceKey = uri ?? title; - - if (sourceSet.has(sourceKey)) { - continue; - } - - sourceSet.add(sourceKey); - metadataObjects.push({ uri, title }); - } - - return metadataObjects; - } } diff --git a/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.base.ts b/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.base.ts index 43b8cce0de..ee56929553 100644 --- a/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.base.ts +++ b/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.base.ts @@ -1,6 +1,8 @@ import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; +import { InvocationOperation } from '@common/enums/ai.persona.invocation.operation'; export interface AiPersonaEngineAdapterInputBase { userID?: string | undefined; engine: AiPersonaEngine; + operation?: InvocationOperation; } diff --git a/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.question.input.ts b/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.invocation.input.ts similarity index 58% rename from src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.question.input.ts rename to src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.invocation.input.ts index 69dce2bf0c..2f8f05a4e5 100644 --- a/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.question.input.ts +++ b/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.invocation.input.ts @@ -3,9 +3,25 @@ import { AiPersonaEngineAdapterInputBase } from './ai.persona.engine.adapter.dto import { InteractionMessage } from '@services/ai-server/ai-persona-service/dto/interaction.message'; import { IExternalConfig } from '@services/ai-server/ai-persona-service/dto/external.config'; -export interface AiPersonaEngineAdapterQueryInput +export enum InvocationResultAction { + POST_REPLY = 'postReply', + POST_MESSAGE = 'postMessage', + NONE = 'none', +} +export class RoomDetails { + roomID!: string; + threadID?: string; + communicationID!: string; +} + +export class ResultHandler { + action!: InvocationResultAction; + roomDetails?: RoomDetails = undefined; +} + +export interface AiPersonaEngineAdapterInvocationInput extends AiPersonaEngineAdapterInputBase { - question: string; + message: string; prompt?: string[]; contextID?: string; bodyOfKnowledgeID: string; @@ -15,4 +31,7 @@ export interface AiPersonaEngineAdapterQueryInput displayName: string; externalConfig: IExternalConfig; externalMetadata: ExternalMetadata; + resultHandler: ResultHandler; + personaServiceID: string; + language?: string; } diff --git a/src/services/ai-server/ai-persona-service/ai.persona.service.service.ts b/src/services/ai-server/ai-persona-service/ai.persona.service.service.ts index 2deb20a0ce..ad2f0cd820 100644 --- a/src/services/ai-server/ai-persona-service/ai.persona.service.service.ts +++ b/src/services/ai-server/ai-persona-service/ai.persona.service.service.ts @@ -6,17 +6,17 @@ import { EntityNotFoundException } from '@common/exceptions'; import { AuthorizationPolicy } from '@domain/common/authorization-policy'; import { AiPersonaService } from './ai.persona.service.entity'; import { IAiPersonaService } from './ai.persona.service.interface'; -import { CreateAiPersonaServiceInput as CreateAiPersonaServiceInput } from './dto/ai.persona.service.dto.create'; -import { DeleteAiPersonaServiceInput as DeleteAiPersonaServiceInput } from './dto/ai.persona..service.dto.delete'; +import { + CreateAiPersonaServiceInput, + DeleteAiPersonaServiceInput, +} from './dto'; import { UpdateAiPersonaServiceInput } from './dto/ai.persona.service.dto.update'; -import { AiPersonaServiceQuestionInput } from './dto/ai.persona.service.question.dto.input'; +import { AiPersonaServiceInvocationInput } from './dto/ai.persona.service.invocation.dto.input'; import { LogContext } from '@common/enums/logging.context'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; -import { AiPersonaEngineAdapterQueryInput } from '@services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.question.input'; import { AiPersonaEngineAdapter } from '@services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter'; import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; import { EventBus } from '@nestjs/cqrs'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; import { InteractionMessage } from './dto/interaction.message'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { @@ -25,6 +25,7 @@ import { } from '@services/infrastructure/event-bus/messages/ingest.space.command'; import { IExternalConfig } from './dto/external.config'; import { EncryptionService } from '@hedger/nestjs-encryption'; +import { AiPersonaEngineAdapterInvocationInput } from '../ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.invocation.input'; @Injectable() export class AiPersonaServiceService { @@ -156,32 +157,36 @@ export class AiPersonaServiceService { return await this.aiPersonaServiceRepository.save(aiPersonaService); } - public async askQuestion( - personaQuestionInput: AiPersonaServiceQuestionInput, + public async invoke( + invocationInput: AiPersonaServiceInvocationInput, history: InteractionMessage[] - ): Promise { + ): Promise { const aiPersonaService = await this.getAiPersonaServiceOrFail( - personaQuestionInput.aiPersonaServiceID + invocationInput.aiPersonaServiceID ); - const input: AiPersonaEngineAdapterQueryInput = { + const input: AiPersonaEngineAdapterInvocationInput = { + operation: invocationInput.operation, engine: aiPersonaService.engine, prompt: aiPersonaService.prompt, - userID: personaQuestionInput.userID, - question: personaQuestionInput.question, + userID: invocationInput.userID, + message: invocationInput.message, bodyOfKnowledgeID: aiPersonaService.bodyOfKnowledgeID, - contextID: personaQuestionInput.contextID, + contextID: invocationInput.contextID, history, - interactionID: personaQuestionInput.interactionID, - externalMetadata: personaQuestionInput.externalMetadata, - displayName: personaQuestionInput.displayName, - description: personaQuestionInput.description, + interactionID: invocationInput.interactionID, + externalMetadata: invocationInput.externalMetadata, + displayName: invocationInput.displayName, + description: invocationInput.description, externalConfig: this.decryptExternalConfig( aiPersonaService.externalConfig ), + resultHandler: invocationInput.resultHandler, + personaServiceID: invocationInput.aiPersonaServiceID, + language: invocationInput.language, }; - return this.aiPersonaEngineAdapter.sendQuery(input); + return this.aiPersonaEngineAdapter.invoke(input); } public async ingest(aiPersonaService: IAiPersonaService): Promise { diff --git a/src/services/ai-server/ai-persona-service/dto/ai.persona..service.dto.delete.ts b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.delete.ts similarity index 100% rename from src/services/ai-server/ai-persona-service/dto/ai.persona..service.dto.delete.ts rename to src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.delete.ts diff --git a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.invocation.dto.input.ts b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.invocation.dto.input.ts new file mode 100644 index 0000000000..154961c8cb --- /dev/null +++ b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.invocation.dto.input.ts @@ -0,0 +1,134 @@ +import { UUID } from '@domain/common/scalars'; +import { ExternalMetadata } from '@domain/communication/vc-interaction/vc.interaction.entity'; +import { Field, InputType, registerEnumType } from '@nestjs/graphql'; + +export enum InvocationOperation { + QUERY = 'query', + INGEST = 'ingest', +} +registerEnumType(InvocationOperation, { + name: 'InvocationOperation', + description: 'Available operations for the engine to execute.', +}); + +export enum InvocationResultAction { + POST_REPLY = 'postReply', + POST_MESSAGE = 'postMessage', + NONE = 'none', +} + +registerEnumType(InvocationResultAction, { + name: 'InvocationResultAction', + description: 'Available actions for handling AI engines invocation results.', +}); + +@InputType() +export class RoomDetails { + @Field(() => String, { + nullable: false, + description: 'The room to which the reply shold be posted.', + }) + roomID!: string; + @Field(() => String, { + nullable: true, + description: 'The thread to which the reply shold be posted.', + }) + threadID?: string; + @Field(() => String, { + nullable: false, + description: 'The communicationID for the VC', + }) + communicationID!: string; + @Field(() => String, { + nullable: true, + description: + 'The Virtual Contributor interaciton part of which is this question', + }) + vcInteractionID?: string | undefined = undefined; +} + +@InputType() +export class ResultHandler { + @Field(() => InvocationResultAction, { + nullable: false, + description: + 'The action that should be taken with the result of the invocation', + }) + action!: InvocationResultAction; + + @Field(() => RoomDetails, { + nullable: true, + description: 'The context needed for the result handler', + }) + roomDetails?: RoomDetails; +} + +@InputType() +export class AiPersonaServiceInvocationInput { + @Field(() => UUID, { + nullable: false, + description: 'Virtual Persona Type.', + }) + aiPersonaServiceID!: string; + + @Field(() => String, { + nullable: false, + description: 'The message being sent to the engine.', + }) + message!: string; + + @Field(() => String, { + nullable: true, + description: + 'The ID of the context, the Virtual Persona is asked a question.', + }) + contextID?: string = undefined; + + @Field(() => String, { + nullable: true, + description: 'User identifier used internaly by the engine.', + }) + userID?: string = undefined; + + @Field(() => String, { + nullable: true, + description: + 'The ID of the message thread where the Virtual Contributor is asked a question if applicable.', + }) + threadID?: string = undefined; + + @Field(() => String, { + nullable: true, + description: + 'The Virtual Contributor interaciton part of which is this question.', + }) + interactionID?: string = undefined; + + @Field(() => String, { + nullable: true, + description: 'The Virtual Contributor description.', + }) + description?: string = undefined; + + @Field(() => String, { + nullable: false, + description: 'The Virtual Contributor displayName.', + }) + displayName!: string; + + // intentially skippuing the Field decorator as we are not sure we want to expose this data + // through the API + externalMetadata: ExternalMetadata = {}; + @Field(() => ResultHandler, { + nullable: false, + description: 'What should happen with the result of the VC invocation', + }) + resultHandler!: ResultHandler; + + @Field(() => InvocationOperation, { + nullable: true, + description: 'Operation we want the engine to execute - defaults to Query', + }) + operation?: InvocationOperation = InvocationOperation.QUERY; + language?: string; +} diff --git a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.question.dto.input.ts b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.question.dto.input.ts deleted file mode 100644 index c7418b8b2e..0000000000 --- a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.question.dto.input.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { UUID } from '@domain/common/scalars'; -import { ExternalMetadata } from '@domain/communication/vc-interaction/vc.interaction.entity'; -import { Field, InputType } from '@nestjs/graphql'; - -@InputType() -export class AiPersonaServiceQuestionInput { - @Field(() => UUID, { - nullable: false, - description: 'Virtual Persona Type.', - }) - aiPersonaServiceID!: string; - - @Field(() => String, { - nullable: false, - description: 'The question that is being asked.', - }) - question!: string; - - @Field(() => String, { - nullable: true, - description: - 'The ID of the context, the Virtual Persona is asked a question.', - }) - contextID?: string = undefined; - - @Field(() => String, { - nullable: true, - description: 'User identifier used internaly by the engine.', - }) - userID?: string = undefined; - - @Field(() => String, { - nullable: true, - description: - 'The ID of the message thread where the Virtual Contributor is asked a question if applicable.', - }) - threadID?: string = undefined; - - @Field(() => String, { - nullable: true, - description: - 'The Virtual Contributor interaciton part of which is this question.', - }) - interactionID?: string = undefined; - - @Field(() => String, { - nullable: true, - description: 'The Virtual Contributor description.', - }) - description?: string = undefined; - - @Field(() => String, { - nullable: false, - description: 'The Virtual Contributor displayName.', - }) - displayName!: string; - - // intentially skippuing the Field decorator as we are not sure we want to expose this data - // through the API - externalMetadata: ExternalMetadata = {}; -} diff --git a/src/services/ai-server/ai-persona-service/dto/index.ts b/src/services/ai-server/ai-persona-service/dto/index.ts index 6caa722ecb..244f2e780f 100644 --- a/src/services/ai-server/ai-persona-service/dto/index.ts +++ b/src/services/ai-server/ai-persona-service/dto/index.ts @@ -1,3 +1,8 @@ export * from './ai.persona.service.dto.create'; export * from './ai.persona.service.dto.update'; -export * from './ai.persona..service.dto.delete'; +export * from './ai.persona.service.dto.delete'; +export * from './ai.persona.service.dto.ingest'; +export * from './interaction.message'; +export * from './external.config'; +export * from './ai.persona.service.invocation.dto.input'; +export * from './utils'; diff --git a/src/services/ai-server/ai-persona-service/dto/utils.ts b/src/services/ai-server/ai-persona-service/dto/utils.ts new file mode 100644 index 0000000000..5dc1521266 --- /dev/null +++ b/src/services/ai-server/ai-persona-service/dto/utils.ts @@ -0,0 +1,25 @@ +import { + AiPersonaServiceInvocationInput, + InvocationResultAction, +} from './ai.persona.service.invocation.dto.input'; + +export const isInputValidForAction = ( + input: AiPersonaServiceInvocationInput, + action: InvocationResultAction +) => { + if (action === InvocationResultAction.POST_REPLY) { + return ( + input.resultHandler.action === action && + input.resultHandler.roomDetails && + input.resultHandler.roomDetails.threadID + ); + } + if (action === InvocationResultAction.POST_MESSAGE) { + return ( + input.resultHandler.action === action && input.resultHandler.roomDetails + ); + } + + // better safe than sorry + return false; +}; diff --git a/src/services/ai-server/ai-server/ai.server.module.ts b/src/services/ai-server/ai-server/ai.server.module.ts index 2eb61f190e..f03e78125e 100644 --- a/src/services/ai-server/ai-server/ai.server.module.ts +++ b/src/services/ai-server/ai-server/ai.server.module.ts @@ -1,6 +1,6 @@ import { AuthorizationModule } from '@core/authorization/authorization.module'; import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AiServer } from './ai.server.entity'; import { AiServerResolverFields } from './ai.server.resolver.fields'; @@ -15,6 +15,8 @@ import { VcInteractionModule } from '@domain/communication/vc-interaction/vc.int import { CommunicationAdapterModule } from '@services/adapters/communication-adapter/communication-adapter.module'; import { SubscriptionServiceModule } from '@services/subscriptions/subscription-service'; import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity'; +import { RoomIntegrationModule } from '@services/room-integration/room.integration.module'; +import { RoomModule } from '@domain/communication/room/room.module'; @Module({ imports: [ @@ -27,6 +29,8 @@ import { VirtualContributor } from '@domain/community/virtual-contributor/virtua VcInteractionModule, CommunicationAdapterModule, SubscriptionServiceModule, + RoomIntegrationModule, + forwardRef(() => RoomModule), ], providers: [ AiServerResolverQueries, diff --git a/src/services/ai-server/ai-server/ai.server.resolver.fields.ts b/src/services/ai-server/ai-server/ai.server.resolver.fields.ts index dfd0238a12..eae16741af 100644 --- a/src/services/ai-server/ai-server/ai.server.resolver.fields.ts +++ b/src/services/ai-server/ai-server/ai.server.resolver.fields.ts @@ -1,8 +1,5 @@ import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql'; -import { - AuthorizationAgentPrivilege, - CurrentUser, -} from '@src/common/decorators'; +import { AuthorizationAgentPrivilege } from '@src/common/decorators'; import { IAiServer } from './ai.server.interface'; import { AiServerService } from './ai.server.service'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.interface'; @@ -11,18 +8,10 @@ import { UseGuards } from '@nestjs/common'; import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; import { IAiPersonaService } from '@services/ai-server/ai-persona-service'; import { UUID } from '@domain/common/scalars/scalar.uuid'; -import { AiPersonaServiceQuestionInput } from '../ai-persona-service/dto/ai.persona.service.question.dto.input'; -import { AgentInfo } from '@core/authentication.agent.info/agent.info'; -import { AiPersonaServiceService } from '../ai-persona-service/ai.persona.service.service'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; -import { InteractionMessage } from '../ai-persona-service/dto/interaction.message'; @Resolver(() => IAiServer) export class AiServerResolverFields { - constructor( - private aiServerService: AiServerService, - private aiPersonaServiceService: AiPersonaServiceService - ) {} + constructor(private aiServerService: AiServerService) {} @ResolveField('authorization', () => IAuthorizationPolicy, { description: 'The authorization policy for the aiServer', @@ -59,24 +48,4 @@ export class AiServerResolverFields { ): Promise { return await this.aiServerService.getAiPersonaServiceOrFail(id); } - - @UseGuards(GraphqlGuard) - @ResolveField(() => IMessageAnswerToQuestion, { - nullable: false, - description: 'Ask the virtual persona engine for guidance.', - }) - async askAiPersonaServiceQuestion( - @CurrentUser() agentInfo: AgentInfo, - @Args('aiPersonaQuestionInput') - aiPersonaQuestionInput: AiPersonaServiceQuestionInput - ): Promise { - aiPersonaQuestionInput.userID = - aiPersonaQuestionInput.userID ?? agentInfo.userID; - // hardcode empty history for now; read it from the interaction - const history: InteractionMessage[] = []; - return this.aiPersonaServiceService.askQuestion( - aiPersonaQuestionInput, - history - ); - } } diff --git a/src/services/ai-server/ai-server/ai.server.service.ts b/src/services/ai-server/ai-server/ai.server.service.ts index 06bad9bdef..227ae62253 100644 --- a/src/services/ai-server/ai-server/ai.server.service.ts +++ b/src/services/ai-server/ai-server/ai.server.service.ts @@ -15,8 +15,11 @@ import { AiPersonaServiceService } from '../ai-persona-service/ai.persona.servic import { AiPersonaEngineAdapter } from '../ai-persona-engine-adapter/ai.persona.engine.adapter'; import { AiServerIngestAiPersonaServiceInput } from './dto/ai.server.dto.ingest.ai.persona.service'; import { AiPersonaEngineAdapterInputBase } from '../ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.base'; -import { CreateAiPersonaServiceInput } from '../ai-persona-service/dto'; -import { AiPersonaServiceQuestionInput } from '../ai-persona-service/dto/ai.persona.service.question.dto.input'; +import { + CreateAiPersonaServiceInput, + isInputValidForAction, +} from '../ai-persona-service/dto'; +import { AiPersonaServiceInvocationInput } from '../ai-persona-service/dto/ai.persona.service.invocation.dto.input'; import { IngestSpace, SpaceIngestionPurpose, @@ -24,7 +27,6 @@ import { import { EventBus } from '@nestjs/cqrs'; import { ConfigService } from '@nestjs/config'; import { ChromaClient } from 'chromadb'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; import { VcInteractionService } from '@domain/communication/vc-interaction/vc.interaction.service'; import { CommunicationAdapter } from '@services/adapters/communication-adapter/communication.adapter'; import { @@ -37,9 +39,44 @@ import { AuthorizationPolicyService } from '@domain/common/authorization-policy/ import { SubscriptionPublishService } from '@services/subscriptions/subscription-service'; import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity'; import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; +import { InvokeEngineResult } from '@services/infrastructure/event-bus/messages/invoke.engine.result'; +import { + InvocationResultAction, + RoomDetails, +} from '@services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.invocation'; +import { RoomControllerService } from '@services/room-integration/room.controller.service'; +import { RoomService } from '@domain/communication/room/room.service'; +import { IMessage } from '@domain/communication/message/message.interface'; @Injectable() export class AiServerService { + private INVOKE_ENGINE_RESULT_HANDLERS = { + [InvocationResultAction.NONE]: () => {}, + [InvocationResultAction.POST_REPLY]: (event: InvokeEngineResult) => { + if ( + isInputValidForAction(event.original, InvocationResultAction.POST_REPLY) + ) { + this.roomControllerService.postReply( + event.original.resultHandler.roomDetails!, + event.response + ); + } + }, + [InvocationResultAction.POST_MESSAGE]: (event: InvokeEngineResult) => { + if ( + isInputValidForAction( + event.original, + InvocationResultAction.POST_MESSAGE + ) + ) { + this.roomControllerService.postMessage( + event.original.resultHandler.roomDetails!, + event.response + ); + } + }, + }; + constructor( private authorizationPolicyService: AuthorizationPolicyService, private aiPersonaServiceService: AiPersonaServiceService, @@ -47,8 +84,10 @@ export class AiServerService { private aiPersonaEngineAdapter: AiPersonaEngineAdapter, private vcInteractionService: VcInteractionService, private communicationAdapter: CommunicationAdapter, + private roomService: RoomService, private subscriptionPublishService: SubscriptionPublishService, private config: ConfigService, + private roomControllerService: RoomControllerService, @InjectRepository(AiServer) private aiServerRepository: Repository, @InjectRepository(VirtualContributor) @@ -155,9 +194,9 @@ export class AiServerService { ); } - public async askQuestion( - questionInput: AiPersonaServiceQuestionInput - ): Promise { + public async invoke( + invocationInput: AiPersonaServiceInvocationInput + ): Promise { // the context is currently not used so no point in keeping this // commenting it out for now to save some work // if ( @@ -171,17 +210,21 @@ export class AiServerService { const personaService = await this.aiPersonaServiceService.getAiPersonaServiceOrFail( - questionInput.aiPersonaServiceID + invocationInput.aiPersonaServiceID ); const HISTORY_ENABLED_ENGINES = new Set([ AiPersonaEngine.EXPERT, + AiPersonaEngine.GUIDANCE, ]); - const loadHistory = HISTORY_ENABLED_ENGINES.has(personaService.engine); // history should be loaded trough the GQL API of the collaboration server let history: InteractionMessage[] = []; - if (loadHistory) { + + if ( + HISTORY_ENABLED_ENGINES.has(personaService.engine) && + invocationInput.resultHandler.roomDetails + ) { const historyLimit = parseInt( this.config.get( 'platform.virtual_contributors.history_length', @@ -192,80 +235,52 @@ export class AiServerService { ); history = await this.getLastNInteractionMessages( - questionInput.interactionID, + invocationInput.resultHandler.roomDetails, historyLimit ); } - return await this.aiPersonaServiceService.askQuestion( - questionInput, - history - ); + return this.aiPersonaServiceService.invoke(invocationInput, history); } + async getLastNInteractionMessages( - interactionID: string | undefined, + roomDetails: RoomDetails, + // interactionID: string | undefined, limit: number = 10 ): Promise { - if (!interactionID) { - return []; + let roomMessages: IMessage[] = []; + const room = await this.roomService.getRoomOrFail(roomDetails.roomID); + if (roomDetails.threadID) { + roomMessages = await this.roomService.getMessagesInThread( + room, + roomDetails.threadID + ); + } else { + roomMessages = await this.roomService.getMessages(room); } - const interaction = await this.vcInteractionService.getVcInteractionOrFail( - interactionID, - { - relations: { - room: true, - }, - } - ); - - const room = await this.communicationAdapter.getCommunityRoom( - interaction.room.externalRoomID - ); const messages: InteractionMessage[] = []; - for (let i = room.messages.length - 1; i >= 0; i--) { - const message = room.messages[i]; - // try to skip this check and use Matrix to filter by Room and Thread - if ( - message.threadID === interaction.threadID || - message.id === interaction.threadID - ) { - let role = MessageSenderRole.HUMAN; - - // try to set the assistant role for the replies of the specific persona/vc - if (message.sender.startsWith('@virtualcontributor')) { - role = MessageSenderRole.ASSISTANT; - } - - messages.unshift({ - content: message.message, - role, - }); - if (messages.length === limit) { - break; - } + for (let i = roomMessages.length - 1; i >= 0; i--) { + const message = roomMessages[i]; + let role = MessageSenderRole.HUMAN; + + // try to set the assistant role for the replies of the specific persona/vc + if (message.sender.startsWith('@virtualcontributor')) { + role = MessageSenderRole.ASSISTANT; + } + + messages.unshift({ + content: message.message, + role, + }); + if (messages.length === limit) { + break; } } return messages; } - // TODO: send over the original question / answer? Send over the whole thread? - // public async askFollowUpQuestion( - // questionInput: AiPersonaServiceQuestionInput - // ): Promise { - // if ( - // questionInput.contextID && - // !(await this.isContextLoaded(questionInput.contextID)) - // ) { - // this.eventBus.publish( - // new IngestSpace(questionInput.contextID, SpaceIngestionPurpose.CONTEXT) - // ); - // } - - // return this.aiPersonaServiceService.askQuestion(questionInput); - // } - private getContextCollectionID(contextID: string): string { return `${contextID}-${SpaceIngestionPurpose.CONTEXT}`; } @@ -411,4 +426,12 @@ export class AiServerService { await this.aiPersonaEngineAdapter.sendIngest(ingestAdapterInput); return result; } + + public async handleInvokeEngineResult(event: InvokeEngineResult) { + const resultHandler = event.original.resultHandler; + const handler = this.INVOKE_ENGINE_RESULT_HANDLERS[resultHandler.action]; + if (handler) { + handler(event); + } + } } diff --git a/src/services/api/chat-guidance/chat.guidance.module.ts b/src/services/api/chat-guidance/chat.guidance.module.ts index 8fb3c77901..70cd64b287 100644 --- a/src/services/api/chat-guidance/chat.guidance.module.ts +++ b/src/services/api/chat-guidance/chat.guidance.module.ts @@ -1,24 +1,29 @@ import { Module } from '@nestjs/common'; import { AuthorizationModule } from '@core/authorization/authorization.module'; import { PlatformAuthorizationPolicyModule } from '@platform/authorization/platform.authorization.policy.module'; -import { GuidanceEngineAdapterModule } from '@services/adapters/chat-guidance-adapter/guidance.engine.adapter.module'; import { ChatGuidanceService } from './chat.guidance.service'; -import { ChatGuidanceResolverQueries } from './chat.guidance.resolver.queries'; import { ChatGuidanceResolverMutations } from './chat.guidance.resolver.mutations'; import { GuidanceReporterModule } from '@services/external/elasticsearch/guidance-reporter'; +import { AiServerAdapterModule } from '@services/adapters/ai-server-adapter/ai.server.adapter.module'; +import { CommunicationAdapterModule } from '@services/adapters/communication-adapter/communication-adapter.module'; +import { RoomModule } from '@domain/communication/room/room.module'; +import { UserModule } from '@domain/community/user/user.module'; +import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; +import { PlatformModule } from '@platform/platform/platform.module'; @Module({ imports: [ AuthorizationModule, + AuthorizationPolicyModule, PlatformAuthorizationPolicyModule, - GuidanceEngineAdapterModule, GuidanceReporterModule, + AiServerAdapterModule, + CommunicationAdapterModule, + RoomModule, + UserModule, + PlatformModule, ], - providers: [ - ChatGuidanceService, - ChatGuidanceResolverMutations, - ChatGuidanceResolverQueries, - ], + providers: [ChatGuidanceService, ChatGuidanceResolverMutations], exports: [ChatGuidanceService, ChatGuidanceResolverMutations], }) export class ChatGuidanceModule {} diff --git a/src/services/api/chat-guidance/chat.guidance.resolver.mutations.ts b/src/services/api/chat-guidance/chat.guidance.resolver.mutations.ts index bc34e604be..3125151afb 100644 --- a/src/services/api/chat-guidance/chat.guidance.resolver.mutations.ts +++ b/src/services/api/chat-guidance/chat.guidance.resolver.mutations.ts @@ -10,6 +10,12 @@ import { PlatformAuthorizationPolicyService } from '@platform/authorization/plat import { ChatGuidanceService } from './chat.guidance.service'; import { ChatGuidanceAnswerRelevanceInput } from './dto/chat.guidance.relevance.dto'; import { GuidanceReporterService } from '@services/external/elasticsearch/guidance-reporter'; +import { ChatGuidanceInput } from './dto/chat.guidance.dto.input'; +import { IMessageGuidanceQuestionResult } from '@domain/communication/message.guidance.question.result/message.guidance.question.result.interface'; +import { RoomAuthorizationService } from '@domain/communication/room/room.service.authorization'; +import { UserService } from '@domain/community/user/user.service'; +import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; +import { IRoom } from '@domain/communication/room/room.interface'; @Resolver() export class ChatGuidanceResolverMutations { @@ -18,10 +24,80 @@ export class ChatGuidanceResolverMutations { private readonly logger: LoggerService, private chatGuidanceService: ChatGuidanceService, private authorizationService: AuthorizationService, + private authorizationPolicyService: AuthorizationPolicyService, private platformAuthorizationService: PlatformAuthorizationPolicyService, + private roomAuthorizationService: RoomAuthorizationService, + private userService: UserService, private guidanceReporterService: GuidanceReporterService ) {} + @UseGuards(GraphqlGuard) + @Mutation(() => IRoom, { + nullable: true, + description: 'Create a guidance chat room.', + }) + async createChatGuidanceRoom( + @CurrentUser() agentInfo: AgentInfo + ): Promise { + this.authorizationService.grantAccessOrFail( + agentInfo, + await this.platformAuthorizationService.getPlatformAuthorizationPolicy(), + AuthorizationPrivilege.ACCESS_INTERACTIVE_GUIDANCE, + `Access interactive guidance: ${agentInfo.email}` + ); + + if (!this.chatGuidanceService.isGuidanceEngineEnabled()) { + return undefined; + } + + const user = await this.userService.getUserOrFail(agentInfo.userID, { + relations: { authorization: true, guidanceRoom: true }, + }); + if (user.guidanceRoom) { + // Return current room if it exists + return user.guidanceRoom; + } + + const roomCreated = + await this.chatGuidanceService.createGuidanceRoom(agentInfo); + + if (roomCreated) { + const roomAuthorization = + this.roomAuthorizationService.applyAuthorizationPolicy( + roomCreated, + user.authorization + ); + await this.authorizationPolicyService.saveAll([roomAuthorization]); + } + return roomCreated; + } + + @UseGuards(GraphqlGuard) + @Mutation(() => IMessageGuidanceQuestionResult, { + nullable: false, + description: 'Ask the chat engine for guidance.', + }) + async askChatGuidanceQuestion( + @CurrentUser() agentInfo: AgentInfo, + @Args('chatData') chatData: ChatGuidanceInput + ): Promise { + this.authorizationService.grantAccessOrFail( + agentInfo, + await this.platformAuthorizationService.getPlatformAuthorizationPolicy(), + AuthorizationPrivilege.ACCESS_INTERACTIVE_GUIDANCE, + `Access interactive guidance: ${agentInfo.email}` + ); + + if (!this.chatGuidanceService.isGuidanceEngineEnabled()) { + return { + success: false, + error: 'Guidance Engine not enabled', + question: chatData.question, + }; + } + return this.chatGuidanceService.askQuestion(chatData, agentInfo); + } + @UseGuards(GraphqlGuard) @Mutation(() => Boolean, { description: 'Resets the interaction with the chat engine.', @@ -30,7 +106,7 @@ export class ChatGuidanceResolverMutations { async resetChatGuidance( @CurrentUser() agentInfo: AgentInfo ): Promise { - await this.authorizationService.grantAccessOrFail( + this.authorizationService.grantAccessOrFail( agentInfo, await this.platformAuthorizationService.getPlatformAuthorizationPolicy(), AuthorizationPrivilege.ACCESS_INTERACTIVE_GUIDANCE, @@ -48,7 +124,7 @@ export class ChatGuidanceResolverMutations { }) @Profiling.api async ingest(@CurrentUser() agentInfo: AgentInfo): Promise { - await this.authorizationService.grantAccessOrFail( + this.authorizationService.grantAccessOrFail( agentInfo, await this.platformAuthorizationService.getPlatformAuthorizationPolicy(), AuthorizationPrivilege.PLATFORM_ADMIN, @@ -57,7 +133,7 @@ export class ChatGuidanceResolverMutations { if (!this.chatGuidanceService.isGuidanceEngineEnabled()) { return false; } - return this.chatGuidanceService.ingest(agentInfo); + return this.chatGuidanceService.ingest(); } @UseGuards(GraphqlGuard) diff --git a/src/services/api/chat-guidance/chat.guidance.resolver.queries.ts b/src/services/api/chat-guidance/chat.guidance.resolver.queries.ts deleted file mode 100644 index 6d78227746..0000000000 --- a/src/services/api/chat-guidance/chat.guidance.resolver.queries.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { UseGuards } from '@nestjs/common'; -import { Args, Resolver, Query } from '@nestjs/graphql'; -import { CurrentUser } from '@src/common/decorators'; -import { GraphqlGuard } from '@core/authorization'; -import { AgentInfo } from '@core/authentication.agent.info/agent.info'; -import { ChatGuidanceInput } from './dto/chat.guidance.dto.input'; -import { ChatGuidanceService } from './chat.guidance.service'; -import { AuthorizationService } from '@core/authorization/authorization.service'; -import { PlatformAuthorizationPolicyService } from '@platform/authorization/platform.authorization.policy.service'; -import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; -@Resolver() -export class ChatGuidanceResolverQueries { - constructor( - private chatGuidanceService: ChatGuidanceService, - private authorizationService: AuthorizationService, - private platformAuthorizationService: PlatformAuthorizationPolicyService - ) {} - - @UseGuards(GraphqlGuard) - @Query(() => IMessageAnswerToQuestion, { - nullable: false, - description: 'Ask the chat engine for guidance.', - }) - async askChatGuidanceQuestion( - @CurrentUser() agentInfo: AgentInfo, - @Args('chatData') chatData: ChatGuidanceInput - ): Promise { - await this.authorizationService.grantAccessOrFail( - agentInfo, - await this.platformAuthorizationService.getPlatformAuthorizationPolicy(), - AuthorizationPrivilege.ACCESS_INTERACTIVE_GUIDANCE, - `Access interactive guidance: ${agentInfo.email}` - ); - - if (!this.chatGuidanceService.isGuidanceEngineEnabled()) { - return { - answer: 'guidance engine not enabled', - question: chatData.question, - sources: [], - }; - } - return this.chatGuidanceService.askQuestion(chatData, agentInfo); - } -} diff --git a/src/services/api/chat-guidance/chat.guidance.service.ts b/src/services/api/chat-guidance/chat.guidance.service.ts index 4d9126b71e..8d9123865b 100644 --- a/src/services/api/chat-guidance/chat.guidance.service.ts +++ b/src/services/api/chat-guidance/chat.guidance.service.ts @@ -2,41 +2,126 @@ import { Inject, LoggerService } from '@nestjs/common'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { ConfigService } from '@nestjs/config'; -import { GuidanceEngineAdapter } from '@services/adapters/chat-guidance-adapter/guidance.engine.adapter'; import { ChatGuidanceInput } from './dto/chat.guidance.dto.input'; -import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; import { AlkemioConfig } from '@src/types'; +import { AiServerAdapter } from '@services/adapters/ai-server-adapter/ai.server.adapter'; +import { InvocationResultAction } from '@services/ai-server/ai-persona-service/dto'; +import { CommunicationAdapter } from '@services/adapters/communication-adapter/communication.adapter'; +import { IRoom } from '@domain/communication/room/room.interface'; +import { RoomService } from '@domain/communication/room/room.service'; +import { UserService } from '@domain/community/user/user.service'; +import { IMessageGuidanceQuestionResult } from '@domain/communication/message.guidance.question.result/message.guidance.question.result.interface'; +import { PlatformService } from '@platform/platform/platform.service'; +import { InvocationOperation } from '@common/enums/ai.persona.invocation.operation'; export class ChatGuidanceService { constructor( - private guidanceEngineAdapter: GuidanceEngineAdapter, private configService: ConfigService, - @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService + @Inject(WINSTON_MODULE_NEST_PROVIDER) + private readonly logger: LoggerService, + private aiServerAdapter: AiServerAdapter, + private communicationAdapter: CommunicationAdapter, + private roomService: RoomService, + private userService: UserService, + private platformService: PlatformService ) {} + public async createGuidanceRoom(agentInfo: AgentInfo): Promise { + const guidanceVc = + await this.platformService.getGuidanceVirtualContributorOrFail(); + const room = await this.userService.createGuidanceRoom(agentInfo.userID); + + await this.communicationAdapter.addUserToRoom( + room.externalRoomID, + agentInfo.communicationID + ); + await this.communicationAdapter.addUserToRoom( + room.externalRoomID, + guidanceVc.communicationID + ); + return room; + } + + /** + * + * @param chatData + * @param agentInfo + * @returns { + * room: IRoom; + * roomCreated: boolean; Indicates that the room has just been created with this request + * } + */ public async askQuestion( chatData: ChatGuidanceInput, agentInfo: AgentInfo - ): Promise { - const response = await this.guidanceEngineAdapter.sendQuery({ - userId: agentInfo.userID, - question: chatData.question, - language: chatData.language ?? 'EN', + ): Promise { + const room = await this.userService.getGuidanceRoom(agentInfo.userID); + if (!room) { + return { + success: false, + error: 'No guidance room found', + question: chatData.question, + }; + } + const guidanceVc = + await this.platformService.getGuidanceVirtualContributorOrFail(); + + const message = await this.communicationAdapter.sendMessage({ + roomID: room.externalRoomID, + senderCommunicationsID: agentInfo.communicationID, + message: chatData.question, }); - return response; + this.aiServerAdapter.invoke({ + operation: InvocationOperation.QUERY, + message: chatData.question, + aiPersonaServiceID: guidanceVc.aiPersona.aiPersonaServiceID, + userID: agentInfo.userID, + displayName: 'Guidance', + language: chatData.language, + resultHandler: { + action: InvocationResultAction.POST_MESSAGE, + roomDetails: { + roomID: room.id, + communicationID: guidanceVc.communicationID, + }, + }, + }); + + return { + id: message.id, + success: true, + question: chatData.question, + }; } public async resetUserHistory(agentInfo: AgentInfo): Promise { - return this.guidanceEngineAdapter.sendReset({ - userId: agentInfo.userID, - }); + const { guidanceRoom } = await this.userService.getUserOrFail( + agentInfo.userID, + { + relations: { guidanceRoom: true }, + } + ); + + if (guidanceRoom) { + await this.roomService.deleteRoom(guidanceRoom); + } + return true; } - public async ingest(agentInfo: AgentInfo): Promise { - return this.guidanceEngineAdapter.sendIngest({ - userId: agentInfo.userID, + public async ingest() { + const guidanceVc = + await this.platformService.getGuidanceVirtualContributorOrFail(); + this.aiServerAdapter.invoke({ + operation: InvocationOperation.INGEST, + message: 'ingest', + aiPersonaServiceID: guidanceVc.aiPersona.aiPersonaServiceID, + displayName: '', + resultHandler: { + action: InvocationResultAction.NONE, + }, }); + return true; } public isGuidanceEngineEnabled(): boolean { diff --git a/src/services/api/chat-guidance/dto/chat.guidance.relevance.dto.ts b/src/services/api/chat-guidance/dto/chat.guidance.relevance.dto.ts index 7af7f33256..c925a33cd6 100644 --- a/src/services/api/chat-guidance/dto/chat.guidance.relevance.dto.ts +++ b/src/services/api/chat-guidance/dto/chat.guidance.relevance.dto.ts @@ -1,9 +1,9 @@ import { InputType, Field } from '@nestjs/graphql'; -import { UUID } from '@domain/common/scalars'; @InputType() export class ChatGuidanceAnswerRelevanceInput { - @Field(() => UUID, { + // Message id is not a UUID, it's the id of the message in Matrix + @Field(() => String, { nullable: false, description: 'The answer id.', }) diff --git a/src/services/infrastructure/event-bus/event.bus.module.ts b/src/services/infrastructure/event-bus/event.bus.module.ts index 8f19345b19..111950b8e9 100644 --- a/src/services/infrastructure/event-bus/event.bus.module.ts +++ b/src/services/infrastructure/event-bus/event.bus.module.ts @@ -74,6 +74,18 @@ import amqplib from 'amqplib'; exchange: eventBusConfig.exchange, routingKey: 'IngestSpaceResult', }, + { + name: 'virtual-contributor-engine-expert', + exchange: eventBusConfig.exchange, + routingKey: 'expert', + durable: true, + }, + { + name: 'virtual-contributor-engine-guidance', + exchange: eventBusConfig.exchange, + routingKey: 'guidance', + durable: true, + }, ], }; }, diff --git a/src/services/infrastructure/event-bus/handlers/index.ts b/src/services/infrastructure/event-bus/handlers/index.ts index 0925721540..5a2672d070 100644 --- a/src/services/infrastructure/event-bus/handlers/index.ts +++ b/src/services/infrastructure/event-bus/handlers/index.ts @@ -1,3 +1,4 @@ import { IngestSpaceResultHandler } from './ingest.space.result.handler'; +import { InvokeEngineResultHandler } from './invoke.engine.result.handler'; -export const Handlers = [IngestSpaceResultHandler]; +export const Handlers = [IngestSpaceResultHandler, InvokeEngineResultHandler]; diff --git a/src/services/infrastructure/event-bus/handlers/invoke.engine.result.handler.ts b/src/services/infrastructure/event-bus/handlers/invoke.engine.result.handler.ts new file mode 100644 index 0000000000..ec02bfeaae --- /dev/null +++ b/src/services/infrastructure/event-bus/handlers/invoke.engine.result.handler.ts @@ -0,0 +1,14 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { AiServerService } from '@services/ai-server/ai-server/ai.server.service'; +import { InvokeEngineResult } from '../messages/invoke.engine.result'; + +@EventsHandler(InvokeEngineResult) +export class InvokeEngineResultHandler + implements IEventHandler +{ + constructor(private readonly aiServerService: AiServerService) {} + + async handle(event: InvokeEngineResult) { + await this.aiServerService.handleInvokeEngineResult(event); + } +} diff --git a/src/services/infrastructure/event-bus/messages/index.ts b/src/services/infrastructure/event-bus/messages/index.ts index fcf69637f9..9c30dc05ce 100644 --- a/src/services/infrastructure/event-bus/messages/index.ts +++ b/src/services/infrastructure/event-bus/messages/index.ts @@ -1,7 +1,9 @@ import { IngestSpaceResult } from './ingest.space.result.event'; import { IngestSpace } from './ingest.space.command'; +import { InvokeEngineResult } from './invoke.engine.result'; +import { InvokeEngine } from './invoke.engine'; export { IngestSpace, SpaceIngestionPurpose } from './ingest.space.command'; -export const Messages = [IngestSpace, IngestSpaceResult]; -export const HandleMessages = [IngestSpaceResult]; -export const SendMessages = [IngestSpace]; +export const Messages = [IngestSpace, IngestSpaceResult, InvokeEngine]; +export const HandleMessages = [IngestSpaceResult, InvokeEngineResult]; +export const SendMessages = [IngestSpace, InvokeEngine]; diff --git a/src/services/infrastructure/event-bus/messages/invoke.engine.result.ts b/src/services/infrastructure/event-bus/messages/invoke.engine.result.ts new file mode 100644 index 0000000000..d3953fc383 --- /dev/null +++ b/src/services/infrastructure/event-bus/messages/invoke.engine.result.ts @@ -0,0 +1,39 @@ +import { IEvent } from '@nestjs/cqrs'; +import { AiPersonaServiceInvocationInput } from '@services/ai-server/ai-persona-service/dto'; + +export class Source { + chunkIndex?: number; + documentId!: string; + embeddingType!: string; + source!: string; + title!: string; + type!: string; + score!: number; + uri!: string; + + constructor(data: Partial) { + Object.assign(this, data); + } +} + +export class InvokeEngineResponse { + message!: string; + result!: string; + humanLanguage!: string; + resultLanguage!: string; + knowledgeLanguage!: string; + originalResult!: string; + sources!: Source[]; + + constructor(data: Partial) { + Object.assign(this, data); + this.sources = (data.sources || []).map(source => new Source(source)); + } +} + +export class InvokeEngineResult implements IEvent { + constructor( + public original: AiPersonaServiceInvocationInput, + public response: InvokeEngineResponse + ) {} +} diff --git a/src/services/infrastructure/event-bus/messages/invoke.engine.ts b/src/services/infrastructure/event-bus/messages/invoke.engine.ts new file mode 100644 index 0000000000..2e38b9738f --- /dev/null +++ b/src/services/infrastructure/event-bus/messages/invoke.engine.ts @@ -0,0 +1,6 @@ +import { IEvent } from '@nestjs/cqrs'; +import { AiPersonaEngineAdapterInvocationInput } from '@services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.invocation.input'; + +export class InvokeEngine implements IEvent { + constructor(public input: AiPersonaEngineAdapterInvocationInput) {} +} diff --git a/src/services/infrastructure/event-bus/publisher.ts b/src/services/infrastructure/event-bus/publisher.ts index 9aab7a9491..0b5278311a 100644 --- a/src/services/infrastructure/event-bus/publisher.ts +++ b/src/services/infrastructure/event-bus/publisher.ts @@ -1,6 +1,7 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { Injectable } from '@nestjs/common'; import { IEventPublisher } from '@nestjs/cqrs'; +import { InvokeEngine } from './messages/invoke.engine'; @Injectable() export class Publisher implements IEventPublisher { @@ -11,10 +12,13 @@ export class Publisher implements IEventPublisher { } publish(event: T): any { - // throw new Error(JSON.stringify(event)); + let routingKey = event.constructor.name; + if (event instanceof InvokeEngine) { + routingKey = event.input.engine; + } this.amqpConnection.publish( 'event-bus', - event.constructor.name, + routingKey, JSON.stringify({ type: event.constructor.name, ...event }) ); } diff --git a/src/services/room-integration/room.controller.service.ts b/src/services/room-integration/room.controller.service.ts new file mode 100644 index 0000000000..3cc7ab0eb6 --- /dev/null +++ b/src/services/room-integration/room.controller.service.ts @@ -0,0 +1,102 @@ +import { LogContext } from '@common/enums'; +import { MutationType } from '@common/enums/subscriptions'; +import { RoomService } from '@domain/communication/room/room.service'; +// import { VcInteractionService } from '@domain/communication/vc-interaction/vc.interaction.service'; +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { RoomDetails } from '@services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.invocation'; +import { InvokeEngineResponse } from '@services/infrastructure/event-bus/messages/invoke.engine.result'; +import { SubscriptionPublishService } from '@services/subscriptions/subscription-service'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; + +@Injectable() +export class RoomControllerService { + constructor( + private roomService: RoomService, + private subscriptionPublishService: SubscriptionPublishService, + // private vcInteractionService: VcInteractionService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService + ) {} + + public async postReply( + { roomID, threadID, communicationID }: RoomDetails, + message: any //TODO type this properly with the implementation of the rest of the engines + // vcInteractionID?: string + ) { + if (!threadID) { + return; + } + const room = await this.roomService.getRoomOrFail(roomID); + const answerMessage = await this.roomService.sendMessageReply( + room, + communicationID, + { + roomID: room.externalRoomID, + message: this.convertResultToMessage(message), + threadID, + }, + 'virtualContributor' + ); + + //TODO fix me with the openai assistant engine + // if (vcInteractionID) { + // const vcInteraction = + // await this.vcInteractionService.getVcInteractionOrFail(vcInteractionID); + // if (!vcInteraction.externalMetadata.threadId && response.threadId) { + // vcInteraction.externalMetadata.threadId = response.threadId; + // await this.vcInteractionService.save(vcInteraction); + // } + // } + + this.subscriptionPublishService.publishRoomEvent( + room, + MutationType.CREATE, + answerMessage + ); + } + + public async postMessage( + { roomID, communicationID }: RoomDetails, + response: InvokeEngineResponse + ) { + const room = await this.roomService.getRoomOrFail(roomID); + const answerMessage = await this.roomService.sendMessage( + room, + communicationID, + { + roomID: room.externalRoomID, + // this second argument (sourcesLable = true) should be part of the resultHandler and not hardcoded here + message: this.convertResultToMessage(response, true), + } + ); + + this.subscriptionPublishService.publishRoomEvent( + room, + MutationType.CREATE, + answerMessage + ); + } + + private convertResultToMessage( + result: InvokeEngineResponse, + sourcesLabel = false + ): string { + this.logger.verbose?.( + `Converting result to room message: ${JSON.stringify(result)}`, + LogContext.COMMUNICATION + ); + let answer = result.result; + + if (result.sources) { + answer += sourcesLabel ? '\n##### Sources:' : ''; + answer += + '\n' + + result.sources + .map( + ({ title, uri }: { title: string; uri: string }) => + `- [${title || uri}](${uri})` + ) + .join('\n'); + } + return answer; + } +} diff --git a/src/services/room-integration/room.integration.module.ts b/src/services/room-integration/room.integration.module.ts new file mode 100644 index 0000000000..39c83e9b27 --- /dev/null +++ b/src/services/room-integration/room.integration.module.ts @@ -0,0 +1,15 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { RoomControllerService } from './room.controller.service'; +import { RoomModule } from '@domain/communication/room/room.module'; +import { SubscriptionServiceModule } from '@services/subscriptions/subscription-service'; + +@Module({ + // No way araund the forward ref unfortunately + // Scope [AppModule -> AuthenticationModule -> UserModule -> RoomModule -> VirtualContributorModule -> AiPersonaModule -> AiServerAdapterModule -> AiServerModule] + // so the room needs the VC in order to invoke the engine which needs the AI server which needs this module to handle the result, and this module needs the Room in order to post + // a reply/message; once the hard link between the collaboration and AI server is broken this will automatically go away + imports: [forwardRef(() => RoomModule), SubscriptionServiceModule], + providers: [RoomControllerService], + exports: [RoomControllerService], +}) +export class RoomIntegrationModule {} From eb309d9b2d013f94ad8829a2bd276ac4bd69bc29 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Fri, 6 Dec 2024 12:44:54 +0200 Subject: [PATCH 22/22] Major version bump --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f14a98d82..c21a5616e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "alkemio-server", - "version": "0.96.2", + "version": "0.97.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "alkemio-server", - "version": "0.96.2", + "version": "0.97.0", "license": "EUPL-1.2", "dependencies": { "@alkemio/matrix-adapter-lib": "^0.4.1", diff --git a/package.json b/package.json index 99381a3c59..9a81d2c33e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alkemio-server", - "version": "0.96.2", + "version": "0.97.0", "description": "Alkemio server, responsible for managing the shared Alkemio platform", "author": "Alkemio Foundation", "private": false,