From 94c0d0f8d217a020845b315856dc9376b50863f2 Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Razak Wahab <60781022+mdrazak2001@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:35:26 +0530 Subject: [PATCH] [BUG] Refactor actor composite type (#10232) fixes #10200 The FieldActor Zod schema was updated to correctly handle null context. --------- Co-authored-by: prastoin --- .../record-field/types/FieldMetadata.ts | 39 ++--- .../types/guards/isFieldActorValue.ts | 23 +-- packages/twenty-server/jest.config.ts | 3 + ...ization-of-actor-composite-context-type.ts | 114 ++++++++++++++ .../0-42/0-42-upgrade-version.command.ts | 8 + .../0-42/0-42-upgrade-version.module.ts | 2 + .../0-43/0-43-upgrade-version.command.ts | 9 ++ .../0-43/0-43-upgrade-version.module.ts | 2 + .../engine/core-modules/actor/actor.module.ts | 14 +- .../created-by.create-many.pre-query-hook.ts | 59 +------ .../created-by.create-one.pre-query-hook.ts | 57 +------ ...eated-by-from-auth-context.service.spec.ts | 148 ++++++++++++++++++ .../created-by-from-auth-context.service.ts | 67 ++++++++ .../build-created-by-from-api-key.util.ts | 17 ++ ...created-by-from-full-name-metadata.util.ts | 19 +++ ...d-created-by-from-workspace-member.util.ts | 11 -- .../composite-types/actor.composite-type.ts | 4 +- .../jobs/workflow-trigger.job.ts | 6 +- .../workflow-trigger.workspace-service.ts | 21 +-- 19 files changed, 454 insertions(+), 169 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/actor/services/__tests__/created-by-from-auth-context.service.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-api-key.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index 84296ec13..b02d03fb6 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -1,13 +1,12 @@ -import { ConnectedAccountProvider } from 'twenty-shared'; import { ThemeColor } from 'twenty-ui'; import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues'; import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral'; import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect'; - +import { ConnectedAccountProvider } from 'twenty-shared'; +import * as z from 'zod'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { CurrencyCode } from './CurrencyCode'; - export type FieldUuidMetadata = { objectMetadataNameSingular?: string; fieldName: string; @@ -279,23 +278,25 @@ export type FieldRichTextV2Value = { export type FieldRichTextValue = null | string; -type FieldActorSource = - | 'API' - | 'IMPORT' - | 'EMAIL' - | 'CALENDAR' - | 'MANUAL' - | 'SYSTEM' - | 'WORKFLOW'; +const FieldActorSourceSchema = z.union([ + z.literal('API'), + z.literal('IMPORT'), + z.literal('EMAIL'), + z.literal('CALENDAR'), + z.literal('MANUAL'), + z.literal('SYSTEM'), + z.literal('WORKFLOW'), +]); -export type FieldActorValue = { - source: FieldActorSource; - workspaceMemberId: string | null; - name: string; - context: { - provider?: ConnectedAccountProvider; - } | null; -}; +export const FieldActorValueSchema = z.object({ + source: FieldActorSourceSchema, + workspaceMemberId: z.string().nullable(), + name: z.string(), + context: z.object({ + provider: z.nativeEnum(ConnectedAccountProvider).optional(), + }), +}); +export type FieldActorValue = z.infer; export type FieldActorForInputValue = Pick< FieldActorValue, diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldActorValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldActorValue.ts index 676765d3e..9a45f1d38 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldActorValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldActorValue.ts @@ -1,20 +1,9 @@ -import { ConnectedAccountProvider } from 'twenty-shared'; - -import { z } from 'zod'; - -import { FieldActorValue } from '../FieldMetadata'; - -const actorSchema = z.object({ - source: z.string(), - workspaceMemberId: z.optional(z.string().nullable()), - name: z.string(), - context: z.optional( - z.object({ - provider: z.optional(z.nativeEnum(ConnectedAccountProvider)), - }), - ), -}); +import { + FieldActorValue, + FieldActorValueSchema, +} from '@/object-record/record-field/types/FieldMetadata'; export const isFieldActorValue = ( fieldValue: unknown, -): fieldValue is FieldActorValue => actorSchema.safeParse(fieldValue).success; +): fieldValue is FieldActorValue => + FieldActorValueSchema.safeParse(fieldValue).success; diff --git a/packages/twenty-server/jest.config.ts b/packages/twenty-server/jest.config.ts index 707af8934..aef2c489e 100644 --- a/packages/twenty-server/jest.config.ts +++ b/packages/twenty-server/jest.config.ts @@ -1,4 +1,7 @@ const jestConfig = { + // For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string + // Prettier v3 will should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1 + prettierPath: null, // to enable logs, comment out the following line silent: true, clearMocks: true, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type.ts new file mode 100644 index 000000000..95d45cf6c --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type.ts @@ -0,0 +1,114 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared'; +import { IsNull, Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { CommandLogger } from 'src/database/commands/logger'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +@Command({ + name: 'upgrade-0.42:standardization-of-actor-composite-context-type', + description: 'Add context to actor composite type.', +}) +export class StandardizationOfActorCompositeContextTypeCommand extends ActiveWorkspacesCommandRunner { + protected readonly logger; + + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository); + this.logger = new CommandLogger({ + constructorName: this.constructor.name, + verbose: false, + }); + this.logger.setVerbose(false); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log(`Running add-context-to-actor-composite-type command`); + + if (options?.dryRun) { + this.logger.log(chalk.yellow('Dry run mode: No changes will be applied')); + } + + for (const [index, workspaceId] of workspaceIds.entries()) { + try { + await this.execute(workspaceId, options?.dryRun); + this.logger.verbose( + `[${index + 1}/${workspaceIds.length}] Added for workspace: ${workspaceId}`, + ); + } catch (error) { + this.logger.error(`Error for workspace: ${workspaceId}`, error); + } + } + } + + private async execute(workspaceId: string, dryRun = false): Promise { + this.logger.verbose(`Adding for workspace: ${workspaceId}`); + const actorFields = await this.fieldMetadataRepository.find({ + where: { + type: FieldMetadataType.ACTOR, + workspaceId, + }, + relations: ['object'], + }); + + for (const field of actorFields) { + if (!field || !field.object) { + this.logger.verbose( + 'field.objectMetadata is null', + workspaceId, + field.id, + ); + continue; + } + + const fieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + field.object.nameSingular, + ); + + if (!dryRun) { + const rowsToUpdate = await fieldRepository.update( + { + [field.name + 'Context']: IsNull(), + }, + { + [field.name + 'Context']: {}, + }, + ); + + this.logger.verbose( + `updated ${rowsToUpdate ? rowsToUpdate.affected : 0} rows`, + ); + } + } + + if (!dryRun) { + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + } + + this.twentyORMGlobalManager.destroyDataSourceForWorkspace(workspaceId); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command.ts index bf151112b..7e2136644 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command.ts @@ -8,6 +8,7 @@ import { BaseCommandOptions } from 'src/database/commands/base.command'; import { FixBodyV2ViewFieldPositionCommand } from 'src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command'; import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field'; import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command'; +import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; @@ -28,6 +29,7 @@ export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner { private readonly fixBodyV2ViewFieldPositionCommand: FixBodyV2ViewFieldPositionCommand, private readonly limitAmountOfViewFieldCommand: LimitAmountOfViewFieldCommand, private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, + private readonly standardizationOfActorCompositeContextType: StandardizationOfActorCompositeContextTypeCommand, ) { super(workspaceRepository); } @@ -75,5 +77,11 @@ export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner { }, workspaceIds, ); + + await this.standardizationOfActorCompositeContextType.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module.ts index dcc1b80c5..c923f83a5 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FixBodyV2ViewFieldPositionCommand } from 'src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command'; import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field'; import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command'; +import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type'; import { UpgradeTo0_42Command } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; @@ -35,6 +36,7 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage MigrateRichTextFieldCommand, FixBodyV2ViewFieldPositionCommand, LimitAmountOfViewFieldCommand, + StandardizationOfActorCompositeContextTypeCommand, ], }) export class UpgradeTo0_42CommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command.ts index ac2d59eb7..312087f39 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { BaseCommandOptions } from 'src/database/commands/base.command'; +import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type'; import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -17,6 +18,7 @@ export class UpgradeTo0_43Command extends ActiveWorkspacesCommandRunner { @InjectRepository(Workspace, 'core') protected readonly workspaceRepository: Repository, private readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand, + private readonly standardizationOfActorCompositeContextTypeCommand: StandardizationOfActorCompositeContextTypeCommand, ) { super(workspaceRepository); } @@ -33,5 +35,12 @@ export class UpgradeTo0_43Command extends ActiveWorkspacesCommandRunner { options, workspaceIds, ); + + // Note: Introduced in 0.42, ran manually on prod. Introduced to self-host globally on 0.43 + await this.standardizationOfActorCompositeContextTypeCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module.ts index 6621ec39d..6e30a6324 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type'; import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command'; import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command'; import { UpgradeTo0_43Command } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command'; @@ -29,6 +30,7 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor UpgradeTo0_43Command, AddTasksAssignedToMeViewCommand, MigrateSearchVectorOnNoteAndTaskEntitiesCommand, + StandardizationOfActorCompositeContextTypeCommand, ], }) export class UpgradeTo0_43CommandModule {} diff --git a/packages/twenty-server/src/engine/core-modules/actor/actor.module.ts b/packages/twenty-server/src/engine/core-modules/actor/actor.module.ts index 4e7b9006b..d29e56942 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/actor.module.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/actor.module.ts @@ -5,9 +5,19 @@ import { CreatedByCreateManyPreQueryHook } from 'src/engine/core-modules/actor/q import { CreatedByCreateOnePreQueryHook } from 'src/engine/core-modules/actor/query-hooks/created-by.create-one.pre-query-hook'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { CreatedByFromAuthContextService } from './services/created-by-from-auth-context.service'; + @Module({ imports: [TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata')], - providers: [CreatedByCreateManyPreQueryHook, CreatedByCreateOnePreQueryHook], - exports: [CreatedByCreateManyPreQueryHook, CreatedByCreateOnePreQueryHook], + providers: [ + CreatedByCreateManyPreQueryHook, + CreatedByCreateOnePreQueryHook, + CreatedByFromAuthContextService, + ], + exports: [ + CreatedByCreateManyPreQueryHook, + CreatedByCreateOnePreQueryHook, + CreatedByFromAuthContextService, + ], }) export class ActorModule {} diff --git a/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.create-many.pre-query-hook.ts b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.create-many.pre-query-hook.ts index f07f97077..3291e65cd 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.create-many.pre-query-hook.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.create-many.pre-query-hook.ts @@ -1,7 +1,6 @@ -import { Logger } from '@nestjs/common/services/logger.service'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined } from 'class-validator'; +import { isDefined } from 'twenty-shared'; import { Repository } from 'typeorm'; import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; @@ -12,16 +11,10 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; -import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util'; +import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { - ActorMetadata, - FieldActorSource, -} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; type CustomWorkspaceItem = Omit< CustomWorkspaceEntity, @@ -35,12 +28,10 @@ type CustomWorkspaceItem = Omit< export class CreatedByCreateManyPreQueryHook implements WorkspaceQueryHookInstance { - private readonly logger = new Logger(CreatedByCreateManyPreQueryHook.name); - constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, @InjectRepository(FieldMetadataEntity, 'metadata') private readonly fieldMetadataRepository: Repository, + private readonly createdByFromAuthContextService: CreatedByFromAuthContextService, ) {} async execute( @@ -48,8 +39,6 @@ export class CreatedByCreateManyPreQueryHook objectName: string, payload: CreateManyResolverArgs, ): Promise> { - let createdBy: ActorMetadata | null = null; - if (!isDefined(payload.data)) { throw new GraphqlQueryRunnerException( 'Payload data is required', @@ -72,46 +61,8 @@ export class CreatedByCreateManyPreQueryHook return payload; } - // If user is logged in, we use the workspace member - if (authContext.workspaceMemberId && authContext.user) { - createdBy = buildCreatedByFromWorkspaceMember( - authContext.workspaceMemberId, - authContext.user, - ); - // TODO: remove that code once we have the workspace member id in all tokens - } else if (authContext.user) { - this.logger.warn("User doesn't have a workspace member id in the token"); - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - authContext.workspace.id, - 'workspaceMember', - ); - - const workspaceMember = await workspaceMemberRepository.findOne({ - where: { - userId: authContext.user?.id, - }, - }); - - if (!workspaceMember) { - throw new Error( - `Workspace member can't be found for user ${authContext.user.id}`, - ); - } - - createdBy = { - source: FieldActorSource.MANUAL, - workspaceMemberId: workspaceMember.id, - name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, - }; - } - - if (authContext.apiKey) { - createdBy = { - source: FieldActorSource.API, - name: authContext.apiKey.name, - }; - } + const createdBy = + await this.createdByFromAuthContextService.buildCreatedBy(authContext); for (const datum of payload.data) { // Front-end can fill the source field diff --git a/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.create-one.pre-query-hook.ts b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.create-one.pre-query-hook.ts index 96e750936..e53a12ed5 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.create-one.pre-query-hook.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.create-one.pre-query-hook.ts @@ -1,4 +1,3 @@ -import { Logger } from '@nestjs/common/services/logger.service'; import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'class-validator'; @@ -12,16 +11,10 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; -import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util'; +import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { - ActorMetadata, - FieldActorSource, -} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; type CustomWorkspaceItem = Omit< CustomWorkspaceEntity, @@ -35,12 +28,10 @@ type CustomWorkspaceItem = Omit< export class CreatedByCreateOnePreQueryHook implements WorkspaceQueryHookInstance { - private readonly logger = new Logger(CreatedByCreateOnePreQueryHook.name); - constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, @InjectRepository(FieldMetadataEntity, 'metadata') private readonly fieldMetadataRepository: Repository, + private readonly createdByFromAuthContextService: CreatedByFromAuthContextService, ) {} async execute( @@ -48,8 +39,6 @@ export class CreatedByCreateOnePreQueryHook objectName: string, payload: CreateOneResolverArgs, ): Promise> { - let createdBy: ActorMetadata | null = null; - if (!isDefined(payload.data)) { throw new GraphqlQueryRunnerException( 'Payload data is required', @@ -72,46 +61,8 @@ export class CreatedByCreateOnePreQueryHook return payload; } - // If user is logged in, we use the workspace member - if (authContext.workspaceMemberId && authContext.user) { - createdBy = buildCreatedByFromWorkspaceMember( - authContext.workspaceMemberId, - authContext.user, - ); - // TODO: remove that code once we have the workspace member id in all tokens - } else if (authContext.user) { - this.logger.warn("User doesn't have a workspace member id in the token"); - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - authContext.workspace.id, - 'workspaceMember', - ); - - const workspaceMember = await workspaceMemberRepository.findOne({ - where: { - userId: authContext.user?.id, - }, - }); - - if (!workspaceMember) { - throw new Error( - `Workspace member can't be found for user ${authContext.user.id}`, - ); - } - - createdBy = { - source: FieldActorSource.MANUAL, - workspaceMemberId: workspaceMember.id, - name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, - }; - } - - if (authContext.apiKey) { - createdBy = { - source: FieldActorSource.API, - name: authContext.apiKey.name, - }; - } + const createdBy = + await this.createdByFromAuthContextService.buildCreatedBy(authContext); // Front-end can fill the source field if ( diff --git a/packages/twenty-server/src/engine/core-modules/actor/services/__tests__/created-by-from-auth-context.service.spec.ts b/packages/twenty-server/src/engine/core-modules/actor/services/__tests__/created-by-from-auth-context.service.spec.ts new file mode 100644 index 000000000..e60a59227 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/actor/services/__tests__/created-by-from-auth-context.service.spec.ts @@ -0,0 +1,148 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + ActorMetadata, + FieldActorSource, +} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +type TestingAuthContext = Omit & { + workspace: Partial; + apiKey?: Partial; + user?: Partial; +}; +// TODO create util +const fromFullNameMetadataToName = ({ + firstName, + lastName, +}: FullNameMetadata) => `${firstName} ${lastName}`; + +describe('CreatedByFromAuthContextService', () => { + let service: CreatedByFromAuthContextService; + const mockWorkspaceMemberRepository = { + findOneOrFail: jest.fn(), + }; + const twentyORMGlobalManager: jest.Mocked< + Pick + > = { + getRepositoryForWorkspace: jest + .fn() + .mockResolvedValue(mockWorkspaceMemberRepository), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CreatedByFromAuthContextService, + { + provide: TwentyORMGlobalManager, + useValue: twentyORMGlobalManager, + }, + ], + }).compile(); + + service = module.get( + CreatedByFromAuthContextService, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + describe('buildCreatedBy', () => { + it('should build metadata from workspaceMemberId and user when both are present', async () => { + const authContext = { + workspaceMemberId: '20202020-0b5c-4178-bed7-d371f6411eaa', + user: { + firstName: 'John', + lastName: 'Doe', + id: '20202020-9aae-49a8-bafc-ac44bae62d6d', + }, + workspace: { id: '20202020-bdec-497f-847a-1bb334fefe58' }, + } as const satisfies TestingAuthContext; + + const result = await service.buildCreatedBy(authContext as AuthContext); + + expect(result).toEqual({ + context: {}, + name: fromFullNameMetadataToName(authContext.user), + workspaceMemberId: authContext.workspaceMemberId, + source: FieldActorSource.MANUAL, + }); + }); + + it('should build metadata from user when workspaceMemberId is missing', async () => { + const authContext = { + user: { + firstName: 'John', + lastName: 'Doe', + id: '20202020-9aae-49a8-bafc-ac44bae62d6d', + }, + workspace: { id: '20202020-bdec-497f-847a-1bb334fefe58' }, + } as const satisfies TestingAuthContext; + + const mockedWorkspaceMember = { + id: '20202020-78a3-4520-ba74-b0e1b534a501', + name: { + firstName: 'Pepito', + lastName: 'Dubois', + }, + } as const satisfies Partial; + + mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValueOnce( + mockedWorkspaceMember, + ); + + const result = await service.buildCreatedBy(authContext as AuthContext); + + expect(result).toEqual({ + context: {}, + name: fromFullNameMetadataToName(mockedWorkspaceMember.name), + workspaceMemberId: mockedWorkspaceMember.id, + source: FieldActorSource.MANUAL, + }); + }); + + it('should build metadata from apiKey when only apiKey is present', async () => { + const authContext = { + apiKey: { + id: '20202020-56c2-471b-925d-31ed3ecd0951', + name: 'API Key Name', + }, + workspace: { id: '20202020-bdec-497f-847a-1bb334fefe58' }, + } as const satisfies TestingAuthContext; + + const result = await service.buildCreatedBy(authContext as AuthContext); + + expect(result).toEqual({ + source: FieldActorSource.API, + workspaceMemberId: null, + name: authContext.apiKey.name, + context: {}, + }); + }); + + it('should throw error when no valid actor information is found', async () => { + const authContext = { + workspace: { id: 'workspace-id' }, + } as const satisfies TestingAuthContext; + + await expect( + service.buildCreatedBy(authContext as AuthContext), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to build createdBy metadata - no valid actor information found in auth context"`, + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts new file mode 100644 index 000000000..74faa77d5 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts @@ -0,0 +1,67 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { isDefined } from 'twenty-shared'; + +import { buildCreatedByFromApiKey } from 'src/engine/core-modules/actor/utils/build-created-by-from-api-key.util'; +import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +@Injectable() +export class CreatedByFromAuthContextService { + private readonly logger = new Logger(CreatedByFromAuthContextService.name); + + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + public async buildCreatedBy( + authContext: AuthContext, + ): Promise { + const { workspace, workspaceMemberId, user, apiKey } = authContext; + + // TODO: remove that code once we have the workspace member id in all tokens + if (isDefined(workspaceMemberId) && isDefined(user)) { + return buildCreatedByFromFullNameMetadata({ + fullNameMetadata: { + firstName: user.firstName, + lastName: user.lastName, + }, + workspaceMemberId, + }); + } + + if (isDefined(user)) { + this.logger.warn("User doesn't have a workspace member id in the token"); + + const workspaceMemberRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspace.id, + 'workspaceMember', + ); + + const workspaceMember = await workspaceMemberRepository.findOneOrFail({ + where: { + userId: user.id, + }, + }); + + return buildCreatedByFromFullNameMetadata({ + fullNameMetadata: workspaceMember.name, + workspaceMemberId: workspaceMember.id, + }); + } + + if (isDefined(apiKey)) { + return buildCreatedByFromApiKey({ + apiKey, + }); + } + + throw new Error( + 'Unable to build createdBy metadata - no valid actor information found in auth context', + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-api-key.util.ts b/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-api-key.util.ts new file mode 100644 index 000000000..4df7ece99 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-api-key.util.ts @@ -0,0 +1,17 @@ +import { + ActorMetadata, + FieldActorSource, +} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity'; + +type BuildCreatedByFromApiKeyArgs = { + apiKey: ApiKeyWorkspaceEntity; +}; +export const buildCreatedByFromApiKey = ({ + apiKey, +}: BuildCreatedByFromApiKeyArgs): ActorMetadata => ({ + source: FieldActorSource.API, + name: apiKey.name, + workspaceMemberId: null, + context: {}, +}); diff --git a/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util.ts new file mode 100644 index 000000000..c150688c5 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util.ts @@ -0,0 +1,19 @@ +import { + ActorMetadata, + FieldActorSource, +} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; + +type BuildCreatedByFromFullNameMetadataArgs = { + workspaceMemberId: string; + fullNameMetadata: FullNameMetadata; +}; +export const buildCreatedByFromFullNameMetadata = ({ + fullNameMetadata, + workspaceMemberId, +}: BuildCreatedByFromFullNameMetadataArgs): ActorMetadata => ({ + workspaceMemberId, + source: FieldActorSource.MANUAL, + name: `${fullNameMetadata.firstName} ${fullNameMetadata.lastName}`, + context: {}, +}); diff --git a/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util.ts b/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util.ts deleted file mode 100644 index a553f9970..000000000 --- a/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { User } from 'src/engine/core-modules/user/user.entity'; -import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; - -export const buildCreatedByFromWorkspaceMember = ( - workspaceMemberId: string, - user: User, -) => ({ - workspaceMemberId, - source: FieldActorSource.MANUAL, - name: `${user.firstName} ${user.lastName}`, -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts index 91722d51b..e69e81ad9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts @@ -56,9 +56,9 @@ export const actorCompositeType: CompositeType = { export type ActorMetadata = { source: FieldActorSource; - workspaceMemberId?: string; + workspaceMemberId: string | null; name: string; - context?: { + context: { provider?: ConnectedAccountProvider; }; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts index bb2658e79..17ac71f99 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts @@ -1,8 +1,10 @@ import { Scope } from '@nestjs/common'; +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { @@ -15,8 +17,6 @@ import { WorkflowTriggerException, WorkflowTriggerExceptionCode, } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; -import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; -import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; export type WorkflowTriggerJobData = { workspaceId: string; @@ -75,6 +75,8 @@ export class WorkflowTriggerJob { { source: FieldActorSource.WORKFLOW, name: workflow.name, + context: {}, + workspaceMemberId: null, }, ); } catch (e) { diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index e5548d9a3..1298958aa 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -4,7 +4,10 @@ import { InjectRepository } from '@nestjs/typeorm'; import { EntityManager, Repository } from 'typeorm'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util'; +import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util'; +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; @@ -26,17 +29,14 @@ import { WorkflowTriggerException, WorkflowTriggerExceptionCode, } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; -import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; -import { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util'; -import { assertNever } from 'src/utils/assert'; -import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; -import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { WorkflowTriggerJob, WorkflowTriggerJobData, } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job'; +import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; +import { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util'; import { computeCronPatternFromSchedule } from 'src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule'; +import { assertNever } from 'src/utils/assert'; @Injectable() export class WorkflowTriggerWorkspaceService { @@ -70,7 +70,7 @@ export class WorkflowTriggerWorkspaceService { workflowVersionId: string, payload: object, workspaceMemberId: string, - user: User, + { firstName, lastName }: User, ) { await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail( workflowVersionId, @@ -80,7 +80,10 @@ export class WorkflowTriggerWorkspaceService { this.getWorkspaceId(), workflowVersionId, payload, - buildCreatedByFromWorkspaceMember(workspaceMemberId, user), + buildCreatedByFromFullNameMetadata({ + fullNameMetadata: { firstName, lastName }, + workspaceMemberId, + }), ); }