From 83fc434c5dc8dd273680fee9403dafd92c2748c9 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 9 Dec 2025 19:18:17 +0100 Subject: [PATCH] Fix workspace invite onboarding loop (#16444) - The issue is that the listener is not triggered if they are no change to the workspaceMember (which happens when name is already filled through GoogleSSO) Fixes https://github.com/twentyhq/twenty/issues/16440 --- .../workspace-workspace-member.listener.ts | 75 ------------------- .../workspace/workspace.module.ts | 2 - .../twenty-orm/utils/format-data.util.ts | 3 - ...workspace-member-pre-query-hook.service.ts | 38 +++++++++- .../workspace-member-query-hook.module.ts | 2 + ...kspace-member-update-one.pre-query.hook.ts | 13 +++- 6 files changed, 49 insertions(+), 84 deletions(-) delete mode 100644 packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts deleted file mode 100644 index 4e7f73fcce..0000000000 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; -import { - HandleWorkspaceMemberDeletedJob, - type HandleWorkspaceMemberDeletedJobData, -} from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job'; -import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -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 { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; - -@Injectable() -export class WorkspaceWorkspaceMemberListener { - constructor( - private readonly onboardingService: OnboardingService, - @InjectMessageQueue(MessageQueue.workspaceQueue) - private readonly messageQueueService: MessageQueueService, - ) {} - - @OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.UPDATED) - async handleUpdateEvent( - payload: WorkspaceEventBatch< - ObjectRecordUpdateEvent - >, - ) { - await Promise.all( - payload.events.map((eventPayload) => { - const { firstName: firstNameAfter, lastName: lastNameAfter } = - eventPayload.properties.after.name; - - if (firstNameAfter === '' && lastNameAfter === '') { - return; - } - - if (!eventPayload.userId) { - return; - } - - return this.onboardingService.setOnboardingCreateProfilePending({ - userId: eventPayload.userId, - workspaceId: payload.workspaceId, - value: false, - }); - }), - ); - } - - @OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.DELETED) - async handleDeleteEvent( - payload: WorkspaceEventBatch< - ObjectRecordDeleteEvent - >, - ) { - await Promise.all( - payload.events.map((eventPayload) => { - const userId = eventPayload.properties.before.userId; - - if (!userId) { - return; - } - - return this.messageQueueService.add( - HandleWorkspaceMemberDeletedJob.name, - { workspaceId: payload.workspaceId, userId }, - ); - }), - ); - } -} diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 6afe2a6797..72da403d1c 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -25,7 +25,6 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity'; import { CheckCustomDomainValidRecordsCronCommand } from 'src/engine/core-modules/workspace/crons/commands/check-custom-domain-valid-records.cron.command'; import { CheckCustomDomainValidRecordsCronJob } from 'src/engine/core-modules/workspace/crons/jobs/check-custom-domain-valid-records.cron.job'; import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; -import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener'; import { workspaceAutoResolverOpts } from 'src/engine/core-modules/workspace/workspace.auto-resolver-opts'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver'; @@ -81,7 +80,6 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m providers: [ WorkspaceResolver, WorkspaceService, - WorkspaceWorkspaceMemberListener, CheckCustomDomainValidRecordsCronCommand, CheckCustomDomainValidRecordsCronJob, ], diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts index c04a3715f2..156788a126 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -48,9 +48,6 @@ export function formatData( const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId]; if (!fieldMetadata) { - this.logger.warn( - `Field metadata for field "${key}" is missing in object metadata ${flatObjectMetadata.nameSingular} for data: ${JSON.stringify(data)}`, - ); throw new Error( `Field metadata for field "${key}" is missing in object metadata ${flatObjectMetadata.nameSingular}`, ); diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts index a88fd4453f..fb802bdbfd 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { isDefined } from 'twenty-shared/utils'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; +import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { PermissionsException, PermissionsExceptionCode, @@ -13,7 +14,10 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm @Injectable() export class WorkspaceMemberPreQueryHookService { - constructor(private readonly permissionsService: PermissionsService) {} + constructor( + private readonly permissionsService: PermissionsService, + private readonly onboardingService: OnboardingService, + ) {} async validateWorkspaceMemberUpdatePermissionOrThrow({ userWorkspaceId, @@ -62,4 +66,34 @@ export class WorkspaceMemberPreQueryHookService { PermissionsExceptionCode.PERMISSION_DENIED, ); } + + async completeOnboardingProfileStepIfNameProvided({ + userId, + workspaceId, + firstName, + lastName, + }: { + userId?: string; + workspaceId: string; + firstName?: string; + lastName?: string; + }) { + if (!userId) { + return; + } + + if (firstName === '' && lastName === '') { + return; + } + + if (!isDefined(firstName) && !isDefined(lastName)) { + return; + } + + await this.onboardingService.setOnboardingCreateProfilePending({ + userId, + workspaceId, + value: false, + }); + } } diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts index 97260aedca..2bfb8d5c8a 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { UserModule } from 'src/engine/core-modules/user/user.module'; @@ -36,6 +37,7 @@ import { WorkspaceMemberUpdateOnePreQueryHook } from 'src/modules/workspace-memb ], imports: [ FeatureFlagModule, + OnboardingModule, PermissionsModule, UserWorkspaceModule, TypeOrmModule.forFeature([UserWorkspaceEntity]), diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-update-one.pre-query.hook.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-update-one.pre-query.hook.ts index 4f5eb542be..2b3426ad79 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-update-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-update-one.pre-query.hook.ts @@ -1,8 +1,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'class-validator'; -import { Repository } from 'typeorm'; import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -14,8 +14,8 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service'; @WorkspaceQueryHook(`workspaceMember.updateOne`) export class WorkspaceMemberUpdateOnePreQueryHook @@ -67,6 +67,15 @@ export class WorkspaceMemberUpdateOnePreQueryHook }); } + await this.workspaceMemberPreQueryHookService.completeOnboardingProfileStepIfNameProvided( + { + userId: authContext.user?.id, + workspaceId: workspace.id, + firstName: payload.data.name?.firstName, + lastName: payload.data.name?.lastName, + }, + ); + return payload; } }