diff --git a/package.json b/package.json index 626d8c44c..364f56192 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.2", "@nestjs/core": "^9.0.0", + "@nestjs/event-emitter": "^2.0.3", "@nestjs/jwt": "^10.0.3", "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.0.0", diff --git a/packages/twenty-server/src/integrations/event-emitter/types/object-record-create.event.ts b/packages/twenty-server/src/integrations/event-emitter/types/object-record-create.event.ts new file mode 100644 index 000000000..d9dbbeb23 --- /dev/null +++ b/packages/twenty-server/src/integrations/event-emitter/types/object-record-create.event.ts @@ -0,0 +1,6 @@ +import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; + +export class ObjectRecordCreateEvent { + workspaceId: string; + createdRecord: T; +} diff --git a/packages/twenty-server/src/integrations/event-emitter/types/object-record-delete.event.ts b/packages/twenty-server/src/integrations/event-emitter/types/object-record-delete.event.ts new file mode 100644 index 000000000..82b93ebbd --- /dev/null +++ b/packages/twenty-server/src/integrations/event-emitter/types/object-record-delete.event.ts @@ -0,0 +1,6 @@ +import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; + +export declare class ObjectRecordDeleteEvent { + workspaceId: string; + deletedRecord: T; +} diff --git a/packages/twenty-server/src/integrations/event-emitter/types/object-record-update.event.ts b/packages/twenty-server/src/integrations/event-emitter/types/object-record-update.event.ts new file mode 100644 index 000000000..d577f246c --- /dev/null +++ b/packages/twenty-server/src/integrations/event-emitter/types/object-record-update.event.ts @@ -0,0 +1,7 @@ +import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; + +export class ObjectRecordUpdateEvent { + workspaceId: string; + previousRecord: T; + updatedRecord: T; +} diff --git a/packages/twenty-server/src/integrations/event-emitter/utils/object-record-changed-properties.util.ts b/packages/twenty-server/src/integrations/event-emitter/utils/object-record-changed-properties.util.ts new file mode 100644 index 000000000..53e2c7658 --- /dev/null +++ b/packages/twenty-server/src/integrations/event-emitter/utils/object-record-changed-properties.util.ts @@ -0,0 +1,12 @@ +import deepEqual from 'deep-equal'; + +export const objectRecordChangedProperties = ( + oldRecord: Record, + newRecord: Record, +) => { + const changedProperties = Object.keys(newRecord).filter( + (key) => !deepEqual(oldRecord[key], newRecord[key]), + ); + + return changedProperties; +}; diff --git a/packages/twenty-server/src/integrations/integrations.module.ts b/packages/twenty-server/src/integrations/integrations.module.ts index 7c63cfe56..febbbb716 100644 --- a/packages/twenty-server/src/integrations/integrations.module.ts +++ b/packages/twenty-server/src/integrations/integrations.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; +import { EventEmitterModule } from '@nestjs/event-emitter'; import { ExceptionHandlerModule } from 'src/integrations/exception-handler/exception-handler.module'; import { exceptionHandlerModuleFactory } from 'src/integrations/exception-handler/exception-handler.module-factory'; @@ -38,6 +39,7 @@ import { MessageQueueModule } from './message-queue/message-queue.module'; useFactory: emailModuleFactory, inject: [EnvironmentService], }), + EventEmitterModule.forRoot(), ], exports: [], providers: [], diff --git a/packages/twenty-server/src/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/integrations/message-queue/jobs.module.ts index 9c1b4d2ce..e29b4579b 100644 --- a/packages/twenty-server/src/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/integrations/message-queue/jobs.module.ts @@ -11,7 +11,7 @@ import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metada import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; import { CleanInactiveWorkspaceJob } from 'src/workspace/workspace-cleaner/crons/clean-inactive-workspace.job'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; -import { FetchWorkspaceMessagesModule } from 'src/workspace/messaging/services/fetch-workspace-messages.module'; +import { MessagingModule } from 'src/workspace/messaging/messaging.module'; import { GmailPartialSyncJob } from 'src/workspace/messaging/jobs/gmail-partial-sync.job'; import { EmailSenderJob } from 'src/integrations/email/email-sender.job'; import { UserModule } from 'src/core/user/user.module'; @@ -19,6 +19,8 @@ import { EnvironmentModule } from 'src/integrations/environment/environment.modu import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; import { FetchAllWorkspacesMessagesJob } from 'src/workspace/messaging/crons/fetch-all-workspaces-messages.job'; import { ConnectedAccountModule } from 'src/workspace/messaging/connected-account/connected-account.module'; +import { MatchMessageParticipantJob } from 'src/workspace/messaging/jobs/match-message-participant.job'; +import { MessageParticipantModule } from 'src/workspace/messaging/message-participant/message-participant.module'; @Module({ imports: [ @@ -27,12 +29,13 @@ import { ConnectedAccountModule } from 'src/workspace/messaging/connected-accoun DataSourceModule, HttpModule, TypeORMModule, - FetchWorkspaceMessagesModule, + MessagingModule, UserModule, EnvironmentModule, TypeORMModule, TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), ConnectedAccountModule, + MessageParticipantModule, ], providers: [ { @@ -60,6 +63,10 @@ import { ConnectedAccountModule } from 'src/workspace/messaging/connected-accoun provide: FetchAllWorkspacesMessagesJob.name, useClass: FetchAllWorkspacesMessagesJob, }, + { + provide: MatchMessageParticipantJob.name, + useClass: MatchMessageParticipantJob, + }, ], }) export class JobsModule { diff --git a/packages/twenty-server/src/workspace/messaging/jobs/match-message-participant.job.ts b/packages/twenty-server/src/workspace/messaging/jobs/match-message-participant.job.ts new file mode 100644 index 000000000..29df24412 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/jobs/match-message-participant.job.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; + +import { MessageQueueJob } from 'src/integrations/message-queue/interfaces/message-queue-job.interface'; + +import { MessageParticipantService } from 'src/workspace/messaging/message-participant/message-participant.service'; + +export type MatchMessageParticipantsJobData = { + workspaceId: string; + email: string; + personId?: string; + workspaceMemberId?: string; +}; + +@Injectable() +export class MatchMessageParticipantJob + implements MessageQueueJob +{ + constructor( + private readonly messageParticipantService: MessageParticipantService, + ) {} + + async handle(data: MatchMessageParticipantsJobData): Promise { + const { workspaceId, personId, workspaceMemberId, email } = data; + + const messageParticipantsToUpdate = + await this.messageParticipantService.getByHandles([email], workspaceId); + + const messageParticipantIdsToUpdate = messageParticipantsToUpdate.map( + (participant) => participant.id, + ); + + if (personId) { + await this.messageParticipantService.updateParticipantsPersonId( + messageParticipantIdsToUpdate, + personId, + workspaceId, + ); + } + if (workspaceMemberId) { + await this.messageParticipantService.updateParticipantsWorkspaceMemberId( + messageParticipantIdsToUpdate, + workspaceMemberId, + workspaceId, + ); + } + } +} diff --git a/packages/twenty-server/src/workspace/messaging/listeners/messaging-person.listener.ts b/packages/twenty-server/src/workspace/messaging/listeners/messaging-person.listener.ts new file mode 100644 index 000000000..383124964 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/listeners/messaging-person.listener.ts @@ -0,0 +1,63 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +import { ObjectRecordCreateEvent } from 'src/integrations/event-emitter/types/object-record-create.event'; +import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event'; +import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/integrations/event-emitter/utils/object-record-changed-properties.util'; +import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; +import { + MatchMessageParticipantJob, + MatchMessageParticipantsJobData, +} from 'src/workspace/messaging/jobs/match-message-participant.job'; +import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata'; + +@Injectable() +export class MessagingPersonListener { + constructor( + @Inject(MessageQueue.messagingQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + @OnEvent('person.created') + handleCreatedEvent(payload: ObjectRecordCreateEvent) { + if (payload.createdRecord.email === null) { + return; + } + + this.messageQueueService.add( + MatchMessageParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.createdRecord.email, + personId: payload.createdRecord.id, + }, + ); + } + + @OnEvent('person.updated') + handleUpdatedEvent(payload: ObjectRecordUpdateEvent) { + console.log( + objectRecordUpdateEventChangedProperties( + payload.previousRecord, + payload.updatedRecord, + ), + ); + + if ( + objectRecordUpdateEventChangedProperties( + payload.previousRecord, + payload.updatedRecord, + ).includes('email') + ) { + this.messageQueueService.add( + MatchMessageParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.updatedRecord.email, + personId: payload.updatedRecord.id, + }, + ); + } + } +} diff --git a/packages/twenty-server/src/workspace/messaging/listeners/messaging-workspace-member.listener.ts b/packages/twenty-server/src/workspace/messaging/listeners/messaging-workspace-member.listener.ts new file mode 100644 index 000000000..839da94b4 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/listeners/messaging-workspace-member.listener.ts @@ -0,0 +1,60 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +import { ObjectRecordCreateEvent } from 'src/integrations/event-emitter/types/object-record-create.event'; +import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event'; +import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/integrations/event-emitter/utils/object-record-changed-properties.util'; +import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; +import { + MatchMessageParticipantJob, + MatchMessageParticipantsJobData, +} from 'src/workspace/messaging/jobs/match-message-participant.job'; +import { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata'; + +@Injectable() +export class MessagingWorkspaceMemberListener { + constructor( + @Inject(MessageQueue.messagingQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + @OnEvent('workspaceMember.created') + handleCreatedEvent( + payload: ObjectRecordCreateEvent, + ) { + if (payload.createdRecord.userEmail === null) { + return; + } + + this.messageQueueService.add( + MatchMessageParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.createdRecord.userEmail, + workspaceMemberId: payload.createdRecord.id, + }, + ); + } + + @OnEvent('workspaceMember.updated') + handleUpdatedEvent( + payload: ObjectRecordUpdateEvent, + ) { + if ( + objectRecordUpdateEventChangedProperties( + payload.previousRecord, + payload.updatedRecord, + ).includes('userEmail') + ) { + this.messageQueueService.add( + MatchMessageParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.updatedRecord.userEmail, + workspaceMemberId: payload.updatedRecord.id, + }, + ); + } + } +} diff --git a/packages/twenty-server/src/workspace/messaging/message-participant/message-participant.module.ts b/packages/twenty-server/src/workspace/messaging/message-participant/message-participant.module.ts new file mode 100644 index 000000000..ae44e5411 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/message-participant/message-participant.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { MessageParticipantService } from 'src/workspace/messaging/message-participant/message-participant.service'; +import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; + +@Module({ + imports: [WorkspaceDataSourceModule], + providers: [MessageParticipantService], + exports: [MessageParticipantService], +}) +export class MessageParticipantModule {} diff --git a/packages/twenty-server/src/workspace/messaging/message-participant/message-participant.service.ts b/packages/twenty-server/src/workspace/messaging/message-participant/message-participant.service.ts new file mode 100644 index 000000000..cec95aa06 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/message-participant/message-participant.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; +import { MessageParticipantObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-participant.object-metadata'; +import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record'; + +@Injectable() +export class MessageParticipantService { + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + ) {} + + public async getByHandles( + handles: string[], + workspaceId: string, + transactionManager?: EntityManager, + ): Promise[]> { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + return await this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."messageParticipant" WHERE "handle" = ANY($1)`, + [handles], + workspaceId, + transactionManager, + ); + } + + public async updateParticipantsPersonId( + participantIds: string[], + personId: string, + workspaceId: string, + transactionManager?: EntityManager, + ) { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + await this.workspaceDataSourceService.executeRawQuery( + `UPDATE ${dataSourceSchema}."messageParticipant" SET "personId" = $1 WHERE "id" = ANY($2)`, + [personId, participantIds], + workspaceId, + transactionManager, + ); + } + + public async updateParticipantsWorkspaceMemberId( + participantIds: string[], + workspaceMemberId: string, + workspaceId: string, + transactionManager?: EntityManager, + ) { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + await this.workspaceDataSourceService.executeRawQuery( + `UPDATE ${dataSourceSchema}."messageParticipant" SET "workspaceMemberId" = $1 WHERE "id" = ANY($2)`, + [workspaceMemberId, participantIds], + workspaceId, + transactionManager, + ); + } +} diff --git a/packages/twenty-server/src/workspace/messaging/services/fetch-workspace-messages.module.ts b/packages/twenty-server/src/workspace/messaging/messaging.module.ts similarity index 80% rename from packages/twenty-server/src/workspace/messaging/services/fetch-workspace-messages.module.ts rename to packages/twenty-server/src/workspace/messaging/messaging.module.ts index 65ed671db..f4079f866 100644 --- a/packages/twenty-server/src/workspace/messaging/services/fetch-workspace-messages.module.ts +++ b/packages/twenty-server/src/workspace/messaging/messaging.module.ts @@ -1,18 +1,21 @@ import { Module } from '@nestjs/common'; -import { EnvironmentModule } from 'src/integrations/environment/environment.module'; import { ConnectedAccountModule } from 'src/workspace/messaging/connected-account/connected-account.module'; import { MessageChannelMessageAssociationModule } from 'src/workspace/messaging/message-channel-message-association/message-channel-message-assocation.module'; import { MessageChannelModule } from 'src/workspace/messaging/message-channel/message-channel.module'; import { MessageThreadModule } from 'src/workspace/messaging/message-thread/message-thread.module'; +import { MessagingUtilsService } from 'src/workspace/messaging/services/messaging-utils.service'; +import { EnvironmentModule } from 'src/integrations/environment/environment.module'; +import { MessagingPersonListener } from 'src/workspace/messaging/listeners/messaging-person.listener'; import { MessageModule } from 'src/workspace/messaging/message/message.module'; import { GmailClientProvider } from 'src/workspace/messaging/providers/gmail/gmail-client.provider'; import { FetchMessagesByBatchesService } from 'src/workspace/messaging/services/fetch-messages-by-batches.service'; import { GmailFullSyncService } from 'src/workspace/messaging/services/gmail-full-sync.service'; import { GmailPartialSyncService } from 'src/workspace/messaging/services/gmail-partial-sync.service'; import { GmailRefreshAccessTokenService } from 'src/workspace/messaging/services/gmail-refresh-access-token.service'; -import { MessagingUtilsService } from 'src/workspace/messaging/services/messaging-utils.service'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; +import { MessageParticipantModule } from 'src/workspace/messaging/message-participant/message-participant.module'; +import { MessagingWorkspaceMemberListener } from 'src/workspace/messaging/listeners/messaging-workspace-member.listener'; @Module({ imports: [ @@ -23,6 +26,7 @@ import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/wo MessageChannelMessageAssociationModule, MessageModule, MessageThreadModule, + MessageParticipantModule, ], providers: [ GmailFullSyncService, @@ -31,6 +35,8 @@ import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/wo GmailRefreshAccessTokenService, MessagingUtilsService, GmailClientProvider, + MessagingPersonListener, + MessagingWorkspaceMemberListener, ], exports: [ GmailPartialSyncService, @@ -39,4 +45,4 @@ import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/wo MessagingUtilsService, ], }) -export class FetchWorkspaceMessagesModule {} +export class MessagingModule {} diff --git a/packages/twenty-server/src/workspace/messaging/services/gmail-partial-sync.service.ts b/packages/twenty-server/src/workspace/messaging/services/gmail-partial-sync.service.ts index aadc430b4..7f835f33a 100644 --- a/packages/twenty-server/src/workspace/messaging/services/gmail-partial-sync.service.ts +++ b/packages/twenty-server/src/workspace/messaging/services/gmail-partial-sync.service.ts @@ -4,7 +4,6 @@ import { gmail_v1 } from 'googleapis'; import { FetchMessagesByBatchesService } from 'src/workspace/messaging/services/fetch-messages-by-batches.service'; import { GmailClientProvider } from 'src/workspace/messaging/providers/gmail/gmail-client.provider'; -import { MessagingUtilsService } from 'src/workspace/messaging/services/messaging-utils.service'; import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; import { @@ -12,8 +11,9 @@ import { GmailFullSyncJobData, } from 'src/workspace/messaging/jobs/gmail-full-sync.job'; import { ConnectedAccountService } from 'src/workspace/messaging/connected-account/connected-account.service'; -import { MessageChannelService } from 'src/workspace/messaging/message-channel/message-channel.service'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; +import { MessageChannelService } from 'src/workspace/messaging/message-channel/message-channel.service'; +import { MessagingUtilsService } from 'src/workspace/messaging/services/messaging-utils.service'; @Injectable() export class GmailPartialSyncService { diff --git a/packages/twenty-server/src/workspace/workspace-query-runner/interfaces/query-runner-optionts.interface.ts b/packages/twenty-server/src/workspace/workspace-query-runner/interfaces/query-runner-option.interface.ts similarity index 100% rename from packages/twenty-server/src/workspace/workspace-query-runner/interfaces/query-runner-optionts.interface.ts rename to packages/twenty-server/src/workspace/workspace-query-runner/interfaces/query-runner-option.interface.ts diff --git a/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts index 2cb012fa1..12a82ea8d 100644 --- a/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts @@ -3,8 +3,8 @@ import { Inject, Injectable, InternalServerErrorException, - Logger, } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { IConnection } from 'src/utils/pagination/interfaces/connection.interface'; import { @@ -34,10 +34,12 @@ import { CallWebhookJobsJobOperation, } from 'src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job'; import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util'; -import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util'; +import { ObjectRecordDeleteEvent } from 'src/integrations/event-emitter/types/object-record-delete.event'; +import { ObjectRecordCreateEvent } from 'src/integrations/event-emitter/types/object-record-create.event'; +import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event'; -import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface'; +import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface'; import { PGGraphQLMutation, PGGraphQLResult, @@ -45,14 +47,12 @@ import { @Injectable() export class WorkspaceQueryRunnerService { - private readonly logger = new Logger(WorkspaceQueryRunnerService.name); - constructor( private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory, private readonly workspaceDataSourceService: WorkspaceDataSourceService, @Inject(MessageQueue.webhookQueue) private readonly messageQueueService: MessageQueueService, - private readonly exceptionHandlerService: ExceptionHandlerService, + private readonly eventEmitter: EventEmitter2, ) {} async findMany< @@ -136,6 +136,13 @@ export class WorkspaceQueryRunnerService { options, ); + parsedResults.forEach((record) => { + this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, { + workspaceId, + createdRecord: [this.removeNestedProperties(record)], + } satisfies ObjectRecordCreateEvent); + }); + return parsedResults; } @@ -153,6 +160,12 @@ export class WorkspaceQueryRunnerService { options: WorkspaceQueryRunnerOptions, ): Promise { const { workspaceId, objectMetadataItem } = options; + + const existingRecord = await this.findOne( + { filter: { id: { eq: args.id } } } as FindOneResolverArgs, + options, + ); + const query = await this.workspaceQueryBuilderFactory.updateOne( args, options, @@ -171,31 +184,11 @@ export class WorkspaceQueryRunnerService { options, ); - return parsedResults?.[0]; - } - - async deleteOne( - args: DeleteOneResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const { workspaceId, objectMetadataItem } = options; - const query = await this.workspaceQueryBuilderFactory.deleteOne( - args, - options, - ); - const result = await this.execute(query, workspaceId); - - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'deleteFrom', - )?.records; - - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.delete, - options, - ); + this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, { + workspaceId, + previousRecord: this.removeNestedProperties(existingRecord as Record), + updatedRecord: this.removeNestedProperties(parsedResults?.[0]), + } satisfies ObjectRecordUpdateEvent); return parsedResults?.[0]; } @@ -209,6 +202,7 @@ export class WorkspaceQueryRunnerService { args, options, ); + const result = await this.execute(query, workspaceId); const parsedResults = this.parseResult>( @@ -252,9 +246,63 @@ export class WorkspaceQueryRunnerService { options, ); + parsedResults.forEach((record) => { + this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, { + workspaceId, + deletedRecord: [this.removeNestedProperties(record)], + } satisfies ObjectRecordDeleteEvent); + }); + return parsedResults; } + async deleteOne( + args: DeleteOneResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise { + const { workspaceId, objectMetadataItem } = options; + const query = await this.workspaceQueryBuilderFactory.deleteOne( + args, + options, + ); + const result = await this.execute(query, workspaceId); + + const parsedResults = this.parseResult>( + result, + objectMetadataItem, + 'deleteFrom', + )?.records; + + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.delete, + options, + ); + + this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, { + workspaceId, + deletedRecord: this.removeNestedProperties(parsedResults?.[0]), + } satisfies ObjectRecordDeleteEvent); + + return parsedResults?.[0]; + } + + private removeNestedProperties( + record: Record, + ) { + const sanitizedRecord = {}; + + for (const [key, value] of Object.entries(record)) { + if (value && typeof value === 'object' && value['edges']) { + continue; + } + + sanitizedRecord[key] = value; + } + + return sanitizedRecord; + } + async execute( query: string, workspaceId: string, diff --git a/packages/twenty-server/src/workspace/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory.ts b/packages/twenty-server/src/workspace/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory.ts index 7fda6465e..ca2dd9069 100644 --- a/packages/twenty-server/src/workspace/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory.ts +++ b/packages/twenty-server/src/workspace/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory.ts @@ -9,7 +9,7 @@ import { import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface'; import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; -import { WorkspaceQueryRunnerOptions } from 'src/workspace/workspace-query-runner/interfaces/query-runner-optionts.interface'; +import { WorkspaceQueryRunnerOptions } from 'src/workspace/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service'; import { QuickActionsService } from 'src/core/quick-actions/quick-actions.service'; diff --git a/packages/twenty-server/src/workspace/workspace.module.ts b/packages/twenty-server/src/workspace/workspace.module.ts index ed3c35312..f6bbe9705 100644 --- a/packages/twenty-server/src/workspace/workspace.module.ts +++ b/packages/twenty-server/src/workspace/workspace.module.ts @@ -5,6 +5,7 @@ import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; import { WorkspaceSchemaStorageModule } from 'src/workspace/workspace-schema-storage/workspace-schema-storage.module'; import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; import { ScalarsExplorerService } from 'src/workspace/services/scalars-explorer.service'; +import { MessagingModule } from 'src/workspace/messaging/messaging.module'; import { WorkspaceFactory } from './workspace.factory'; @@ -19,6 +20,7 @@ import { WorkspaceResolverBuilderModule } from './workspace-resolver-builder/wor WorkspaceSchemaBuilderModule, WorkspaceResolverBuilderModule, WorkspaceSchemaStorageModule, + MessagingModule, ], providers: [WorkspaceFactory, ScalarsExplorerService], exports: [WorkspaceFactory], diff --git a/yarn.lock b/yarn.lock index c530d727e..97f244fe5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7338,6 +7338,19 @@ __metadata: languageName: node linkType: hard +"@nestjs/event-emitter@npm:^2.0.3": + version: 2.0.3 + resolution: "@nestjs/event-emitter@npm:2.0.3" + dependencies: + eventemitter2: "npm:6.4.9" + peerDependencies: + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 + "@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.12 + checksum: 22fdbe4b68f9a4f25be8b2cc72afa5f4956b07f3b646befa263a0c112eee200de553a0ed1804090430002dcc6139b4cb20451dabe5c9dde140dc12469489c547 + languageName: node + linkType: hard + "@nestjs/graphql@npm:12.0.8": version: 12.0.8 resolution: "@nestjs/graphql@npm:12.0.8" @@ -24366,6 +24379,13 @@ __metadata: languageName: node linkType: hard +"eventemitter2@npm:6.4.9": + version: 6.4.9 + resolution: "eventemitter2@npm:6.4.9" + checksum: b2adf7d9f1544aa2d95ee271b0621acaf1e309d85ebcef1244fb0ebc7ab0afa6ffd5e371535d0981bc46195ad67fd6ff57a8d1db030584dee69aa5e371a27ea7 + languageName: node + linkType: hard + "eventemitter3@npm:^3.1.0": version: 3.1.2 resolution: "eventemitter3@npm:3.1.2" @@ -43324,6 +43344,7 @@ __metadata: "@nestjs/common": "npm:^9.0.0" "@nestjs/config": "npm:^2.3.2" "@nestjs/core": "npm:^9.0.0" + "@nestjs/event-emitter": "npm:^2.0.3" "@nestjs/jwt": "npm:^10.0.3" "@nestjs/passport": "npm:^9.0.3" "@nestjs/platform-express": "npm:^9.0.0"