diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 80e56c4f5..1aed46afe 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -5,7 +5,7 @@ on: - main pull_request: - + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/packages/twenty-front/.eslintrc.cjs b/packages/twenty-front/.eslintrc.cjs index 790724178..4d14adac1 100644 --- a/packages/twenty-front/.eslintrc.cjs +++ b/packages/twenty-front/.eslintrc.cjs @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { extends: ['../../.eslintrc.cjs', '../../.eslintrc.react.cjs'], ignorePatterns: [ @@ -23,8 +25,10 @@ module.exports = { }, plugins: ['project-structure'], settings: { - 'project-structure/folder-structure-config-path': - 'packages/twenty-front/folderStructure.json', + 'project-structure/folder-structure-config-path':path.resolve( + __dirname, + 'folderStructure.json' + ) }, rules: { 'project-structure/folder-structure': 'error', diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/findAvailableTimeZoneOption.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/findAvailableTimeZoneOption.test.ts index 2199b3a10..05e670b59 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/findAvailableTimeZoneOption.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/findAvailableTimeZoneOption.test.ts @@ -1,20 +1,14 @@ import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption'; +jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00.000Z')); describe('findAvailableTimeZoneOption', () => { it('should find the matching available IANA time zone select option from a given IANA time zone', () => { const ianaTimeZone = 'Europe/Paris'; - const expectedValue = 'Europe/Paris'; - const expectedLabelWinter = - '(GMT+01:00) Central European Standard Time - Paris'; - const expectedLabelSummer = - '(GMT+02:00) Central European Summer Time - Paris'; + const value = 'Europe/Paris'; + const label = '(GMT+01:00) Central European Standard Time - Paris'; const option = findAvailableTimeZoneOption(ianaTimeZone); - expect(option.value).toEqual(expectedValue); - expect( - expectedLabelWinter === option.label || - expectedLabelSummer === option.label, - ).toBeTruthy(); + expect(option).toEqual({ value, label }); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/formatTimeZoneLabel.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/formatTimeZoneLabel.test.ts index ae60e98db..50def87ed 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/formatTimeZoneLabel.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/formatTimeZoneLabel.test.ts @@ -1,19 +1,14 @@ import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel'; +jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00.000Z')); describe('formatTimeZoneLabel', () => { it('should format the time zone label correctly when location is included in the label', () => { const ianaTimeZone = 'Europe/Paris'; - const expectedLabelSummer = - '(GMT+02:00) Central European Summer Time - Paris'; - const expectedLabelWinter = - '(GMT+01:00) Central European Standard Time - Paris'; + const expectedLabel = '(GMT+01:00) Central European Standard Time - Paris'; const formattedLabel = formatTimeZoneLabel(ianaTimeZone); - expect( - expectedLabelSummer === formattedLabel || - expectedLabelWinter === formattedLabel, - ).toBeTruthy(); + expect(expectedLabel).toEqual(formattedLabel); }); it('should format the time zone label correctly when location is not included in the label', () => { diff --git a/packages/twenty-front/src/pages/settings/__stories__/SettingsAppearance.stories.tsx b/packages/twenty-front/src/pages/settings/__stories__/SettingsAppearance.stories.tsx index 2b7ab8f8e..2e2880015 100644 --- a/packages/twenty-front/src/pages/settings/__stories__/SettingsAppearance.stories.tsx +++ b/packages/twenty-front/src/pages/settings/__stories__/SettingsAppearance.stories.tsx @@ -63,7 +63,7 @@ export const DateTimeSettingsTimezone: Story = { await canvas.findByText('Date and time'); const timezoneSelect = await canvas.findByText( - '(GMT-04:00) Eastern Daylight Time - New York', + '(GMT-05:00) Eastern Standard Time - New York', ); userEvent.click(timezoneSelect); diff --git a/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksDetail.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksDetail.stories.tsx index a77e38d73..77627403b 100644 --- a/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksDetail.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksDetail.stories.tsx @@ -29,7 +29,7 @@ const meta: Meta = { targetUrl: 'https://example.com/webhook', description: 'A Sample Description', updatedAt: '2021-08-27T12:00:00Z', - operation: 'create', + operation: 'created', __typename: 'Webhook', }, }, diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx index d4031df59..e5655ad5e 100644 --- a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx +++ b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx @@ -121,9 +121,9 @@ export const SettingsDevelopersWebhooksDetail = () => { const actionOptions: SelectOption[] = [ { value: '*', label: 'All Actions', Icon: IconNorthStar }, - { value: 'create', label: 'Created', Icon: IconPlus }, - { value: 'update', label: 'Updated', Icon: IconRefresh }, - { value: 'delete', label: 'Deleted', Icon: IconTrash }, + { value: 'created', label: 'Created', Icon: IconPlus }, + { value: 'updated', label: 'Updated', Icon: IconRefresh }, + { value: 'deleted', label: 'Deleted', Icon: IconTrash }, ]; const { updateOneRecord } = useUpdateOneRecord({ diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command.ts index d2f2d4571..fbcb10849 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; -import chalk from 'chalk'; import { Command } from 'nest-commander'; +import chalk from 'chalk'; import { Repository } from 'typeorm'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; @@ -43,17 +43,26 @@ export class CopyWebhookOperationIntoOperationsCommand extends ActiveWorkspacesC for (const webhook of webhooks) { if ('operation' in webhook) { - let newOperation = webhook.operation; + let newOpe = webhook.operation; - const [firstWebhookPart, lastWebhookPart] = newOperation.split('.'); + newOpe = newOpe.replace(/\bcreate\b(?=\.|$)/g, 'created'); + newOpe = newOpe.replace(/\bupdate\b(?=\.|$)/g, 'updated'); + newOpe = newOpe.replace(/\bdelete\b(?=\.|$)/g, 'deleted'); + newOpe = newOpe.replace(/\bdestroy\b(?=\.|$)/g, 'destroyed'); - if (['created', 'updated', 'deleted'].includes(firstWebhookPart)) { - newOperation = `${lastWebhookPart}.${firstWebhookPart}`; + const [firstWebhookPart, lastWebhookPart] = newOpe.split('.'); + + if ( + ['created', 'updated', 'deleted', 'destroyed'].includes( + firstWebhookPart, + ) + ) { + newOpe = `${lastWebhookPart}.${firstWebhookPart}`; } await webhookRepository.update(webhook.id, { - operation: newOperation, - operations: [newOperation], + operation: newOpe, + operations: [newOpe], }); this.logger.log( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator.ts new file mode 100644 index 000000000..b874ac8f3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator.ts @@ -0,0 +1,18 @@ +import { OnEvent } from '@nestjs/event-emitter'; + +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; + +export function OnDatabaseEvent( + object: string, + action: DatabaseEventAction, +): MethodDecorator { + const event = `${object}.${action}`; + + return ( + target: object, + propertyKey: string, + descriptor: PropertyDescriptor, + ) => { + OnEvent(event)(target, propertyKey, descriptor); + }; +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/enums/database-event-action.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/enums/database-event-action.ts new file mode 100644 index 000000000..dfe45b33a --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/enums/database-event-action.ts @@ -0,0 +1,6 @@ +export enum DatabaseEventAction { + CREATED = 'created', + UPDATED = 'updated', + DELETED = 'deleted', + DESTROYED = 'destroyed', +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index 2330ec472..c3fe76e2e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -31,15 +31,7 @@ import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-quer import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; -import { - CallWebhookJobsJob, - CallWebhookJobsJobData, - CallWebhookJobsJobOperation, -} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job'; import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; -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 { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; import { capitalize } from 'src/utils/capitalize'; @@ -49,8 +41,6 @@ export class GraphqlQueryRunnerService { private readonly workspaceQueryHookService: WorkspaceQueryHookService, private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory, private readonly queryResultGettersFactory: QueryResultGettersFactory, - @InjectMessageQueue(MessageQueue.webhookQueue) - private readonly messageQueueService: MessageQueueService, private readonly graphqlQueryResolverFactory: GraphqlQueryResolverFactory, private readonly apiEventEmitterService: ApiEventEmitterService, ) {} @@ -312,7 +302,7 @@ export class GraphqlQueryRunnerService { args: RestoreManyResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - const result = await this.executeQuery< + return await this.executeQuery< UpdateManyResolverArgs>, ObjectRecord >( @@ -323,8 +313,6 @@ export class GraphqlQueryRunnerService { }, options, ); - - return result; } private async executeQuery( @@ -372,54 +360,6 @@ export class GraphqlQueryRunnerService { resultWithGettersArray, ); - const jobOperation = this.operationNameToJobOperation(operationName); - - if (jobOperation) { - await this.triggerWebhooks(resultWithGettersArray, jobOperation, options); - } - return resultWithGetters; } - - private operationNameToJobOperation( - operationName: WorkspaceResolverBuilderMethodNames, - ): CallWebhookJobsJobOperation | undefined { - switch (operationName) { - case 'createOne': - case 'createMany': - return CallWebhookJobsJobOperation.create; - case 'updateOne': - case 'updateMany': - case 'restoreMany': - return CallWebhookJobsJobOperation.update; - case 'deleteOne': - case 'deleteMany': - return CallWebhookJobsJobOperation.delete; - case 'destroyOne': - return CallWebhookJobsJobOperation.destroy; - default: - return undefined; - } - } - - private async triggerWebhooks( - jobsData: T[] | undefined, - operation: CallWebhookJobsJobOperation, - options: WorkspaceQueryRunnerOptions, - ): Promise { - if (!jobsData || !Array.isArray(jobsData)) return; - - jobsData.forEach((jobData) => { - this.messageQueueService.add( - CallWebhookJobsJob.name, - { - record: jobData, - workspaceId: options.authContext.workspace.id, - operation, - objectMetadataItem: options.objectMetadataItem, - }, - { retryLimit: 3 }, - ); - }); - } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts index 8cb2c9cc7..a96a52d99 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts @@ -5,6 +5,8 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class ApiEventEmitterService { @@ -16,7 +18,7 @@ export class ApiEventEmitterService { objectMetadataItem: ObjectMetadataInterface, ): void { this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.created`, + `${objectMetadataItem.nameSingular}.${DatabaseEventAction.CREATED}`, records.map((record) => ({ userId: authContext.user?.id, recordId: record.id, @@ -46,20 +48,28 @@ export class ApiEventEmitterService { ); this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.updated`, + `${objectMetadataItem.nameSingular}.${DatabaseEventAction.UPDATED}`, records.map((record) => { + const before = this.removeGraphQLAndNestedProperties( + mappedExistingRecords[record.id], + ); + const after = this.removeGraphQLAndNestedProperties(record); + const diff = objectRecordChangedValues( + before, + after, + updatedFields, + objectMetadataItem, + ); + return { userId: authContext.user?.id, recordId: record.id, objectMetadata: objectMetadataItem, properties: { - before: mappedExistingRecords[record.id] - ? this.removeGraphQLAndNestedProperties( - mappedExistingRecords[record.id], - ) - : undefined, - after: this.removeGraphQLAndNestedProperties(record), + before, + after, updatedFields, + diff, }, }; }), @@ -73,7 +83,7 @@ export class ApiEventEmitterService { objectMetadataItem: ObjectMetadataInterface, ): void { this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.deleted`, + `${objectMetadataItem.nameSingular}.${DatabaseEventAction.DELETED}`, records.map((record) => { return { userId: authContext.user?.id, @@ -95,7 +105,7 @@ export class ApiEventEmitterService { objectMetadataItem: ObjectMetadataInterface, ): void { this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.destroyed`, + `${objectMetadataItem.nameSingular}.${DatabaseEventAction.DESTROYED}`, records.map((record) => { return { userId: authContext.user?.id, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action.ts new file mode 100644 index 000000000..95a882a28 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action.ts @@ -0,0 +1,9 @@ +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; + +export const checkStringIsDatabaseEventAction = ( + value: string, +): value is DatabaseEventAction => { + return Object.values(DatabaseEventAction).includes( + value as DatabaseEventAction, + ); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module.ts index c7e93901b..e99954324 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module.ts @@ -1,8 +1,6 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { CallWebhookJobsJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job'; -import { CallWebhookJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job'; import { RecordPositionBackfillJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/record-position-backfill.job'; import { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module'; import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; @@ -14,9 +12,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works WorkspaceDataSourceModule, DataSourceModule, RecordPositionBackfillModule, - HttpModule, - AnalyticsModule, ], - providers: [CallWebhookJobsJob, CallWebhookJob, RecordPositionBackfillJob], + providers: [RecordPositionBackfillJob], }) export class WorkspaceQueryRunnerJobModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts index 8eb8be61f..f278e2565 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts @@ -1,55 +1,49 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; -import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; 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/workspace-event.type'; import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event'; import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { CallWebhookJobsJob } from 'src/modules/webhook/jobs/call-webhook-jobs.job'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class EntityEventsToDbListener { constructor( @InjectMessageQueue(MessageQueue.entityEventsToDbQueue) - private readonly messageQueueService: MessageQueueService, + private readonly entityEventsToDbQueueService: MessageQueueService, + @InjectMessageQueue(MessageQueue.webhookQueue) + private readonly webhookQueueService: MessageQueueService, ) {} - @OnEvent('*.created') + @OnDatabaseEvent('*', DatabaseEventAction.CREATED) async handleCreate( payload: WorkspaceEventBatch>, ) { return this.handle(payload); } - @OnEvent('*.updated') + @OnDatabaseEvent('*', DatabaseEventAction.UPDATED) async handleUpdate( payload: WorkspaceEventBatch>, ) { - for (const eventPayload of payload.events) { - eventPayload.properties.diff = objectRecordChangedValues( - eventPayload.properties.before, - eventPayload.properties.after, - eventPayload.properties.updatedFields, - eventPayload.objectMetadata, - ); - } - return this.handle(payload); } - @OnEvent('*.deleted') + @OnDatabaseEvent('*', DatabaseEventAction.DELETED) async handleDelete( payload: WorkspaceEventBatch>, ) { return this.handle(payload); } - @OnEvent('*.destroyed') + @OnDatabaseEvent('*', DatabaseEventAction.DESTROYED) async handleDestroy( payload: WorkspaceEventBatch>, ) { @@ -61,18 +55,22 @@ export class EntityEventsToDbListener { (event) => event.objectMetadata?.isAuditLogged, ); - await this.messageQueueService.add< + await this.entityEventsToDbQueueService.add< WorkspaceEventBatch >(CreateAuditLogFromInternalEvent.name, { ...payload, events: filteredEvents, }); - await this.messageQueueService.add< + await this.entityEventsToDbQueueService.add< WorkspaceEventBatch >(UpsertTimelineActivityFromInternalEvent.name, { ...payload, events: filteredEvents, }); + + await this.webhookQueueService.add< + WorkspaceEventBatch + >(CallWebhookJobsJob.name, payload, { retryLimit: 3 }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts index f627caf47..67c2f321f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts @@ -5,6 +5,8 @@ import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.se import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class TelemetryListener { @@ -13,7 +15,7 @@ export class TelemetryListener { private readonly telemetryService: TelemetryService, ) {} - @OnEvent('*.created') + @OnDatabaseEvent('*', DatabaseEventAction.CREATED) async handleAllCreate( payload: WorkspaceEventBatch>, ) { diff --git a/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts b/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts index 13d419850..4ca381bd2 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { UpdateSubscriptionJob, @@ -12,6 +11,8 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class BillingWorkspaceMemberListener { @@ -21,8 +22,8 @@ export class BillingWorkspaceMemberListener { private readonly environmentService: EnvironmentService, ) {} - @OnEvent('workspaceMember.created') - @OnEvent('workspaceMember.deleted') + @OnDatabaseEvent('workspaceMember', DatabaseEventAction.CREATED) + @OnDatabaseEvent('workspaceMember', DatabaseEventAction.DELETED) async handleCreateOrDeleteEvent( payload: WorkspaceEventBatch< ObjectRecordCreateEvent diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-update.event.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-update.event.ts index 7f2df5a1e..a6fb5938b 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-update.event.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-update.event.ts @@ -1,10 +1,14 @@ import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; +type Diff = { + [K in keyof T]: { before: T[K]; after: T[K] }; +}; + export class ObjectRecordUpdateEvent extends ObjectRecordBaseEvent { properties: { updatedFields?: string[]; before: T; after: T; - diff?: Partial; + diff?: Partial>; }; } diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record.base.event.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record.base.event.ts index e515c1309..0295724e8 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record.base.event.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record.base.event.ts @@ -7,8 +7,3 @@ export class ObjectRecordBaseEvent { objectMetadata: ObjectMetadataInterface; properties: any; } - -export class ObjectRecordBaseEventWithNameAndWorkspaceId extends ObjectRecordBaseEvent { - name: string; - workspaceId: string; -} diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts index b18400d08..2352ef8d9 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts @@ -45,76 +45,76 @@ describe('objectRecordChangedValues', () => { name: { before: 'Original Name', after: 'Updated Name' }, }); }); -}); - -it('ignores changes to the updatedAt field', () => { - const oldRecord = { - id: '74316f58-29b0-4a6a-b8fa-d2b506d5516d', - updatedAt: new Date('2020-01-01').toDateString(), - }; - const newRecord = { - id: '74316f58-29b0-4a6a-b8fa-d2b506d5516d', - updatedAt: new Date('2024-01-01').toDateString(), - }; - - const result = objectRecordChangedValues( - oldRecord, - newRecord, - [], - mockObjectMetadata, - ); - - expect(result).toEqual({}); -}); - -it('returns an empty object when there are no changes', () => { - const oldRecord = { - id: '74316f58-29b0-4a6a-b8fa-d2b506d5516k', - name: 'Name', - value: 100, - }; - const newRecord = { - id: '74316f58-29b0-4a6a-b8fa-d2b506d5516k', - name: 'Name', - value: 100, - }; - - const result = objectRecordChangedValues( - oldRecord, - newRecord, - ['name', 'value'], - mockObjectMetadata, - ); - - expect(result).toEqual({}); -}); - -it('correctly handles a mix of changed, unchanged, and special case values', () => { - const oldRecord = { - id: '74316f58-29b0-4a6a-b8fa-d2b506d5516l', - name: 'Original', - status: 'active', - updatedAt: new Date(2020, 1, 1).toDateString(), - config: { theme: 'dark' }, - }; - const newRecord = { - id: '74316f58-29b0-4a6a-b8fa-d2b506d5516l', - name: 'Updated', - status: 'active', - updatedAt: new Date(2021, 1, 1).toDateString(), - config: { theme: 'light' }, - }; - const expectedChanges = { - name: { before: 'Original', after: 'Updated' }, - config: { before: { theme: 'dark' }, after: { theme: 'light' } }, - }; - - const result = objectRecordChangedValues( - oldRecord, - newRecord, - ['name', 'config', 'status'], - mockObjectMetadata, - ); - - expect(result).toEqual(expectedChanges); + + it('ignores changes to the updatedAt field', () => { + const oldRecord = { + id: '74316f58-29b0-4a6a-b8fa-d2b506d5516d', + updatedAt: new Date('2020-01-01').toDateString(), + }; + const newRecord = { + id: '74316f58-29b0-4a6a-b8fa-d2b506d5516d', + updatedAt: new Date('2024-01-01').toDateString(), + }; + + const result = objectRecordChangedValues( + oldRecord, + newRecord, + [], + mockObjectMetadata, + ); + + expect(result).toEqual({}); + }); + + it('returns an empty object when there are no changes', () => { + const oldRecord = { + id: '74316f58-29b0-4a6a-b8fa-d2b506d5516k', + name: 'Name', + value: 100, + }; + const newRecord = { + id: '74316f58-29b0-4a6a-b8fa-d2b506d5516k', + name: 'Name', + value: 100, + }; + + const result = objectRecordChangedValues( + oldRecord, + newRecord, + ['name', 'value'], + mockObjectMetadata, + ); + + expect(result).toEqual({}); + }); + + it('correctly handles a mix of changed, unchanged, and special case values', () => { + const oldRecord = { + id: '74316f58-29b0-4a6a-b8fa-d2b506d5516l', + name: 'Original', + status: 'active', + updatedAt: new Date(2020, 1, 1).toDateString(), + config: { theme: 'dark' }, + }; + const newRecord = { + id: '74316f58-29b0-4a6a-b8fa-d2b506d5516l', + name: 'Updated', + status: 'active', + updatedAt: new Date(2021, 1, 1).toDateString(), + config: { theme: 'light' }, + }; + const expectedChanges = { + name: { before: 'Original', after: 'Updated' }, + config: { before: { theme: 'dark' }, after: { theme: 'light' } }, + }; + + const result = objectRecordChangedValues( + oldRecord, + newRecord, + ['name', 'config', 'status'], + mockObjectMetadata, + ); + + expect(result).toEqual(expectedChanges); + }); }); diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/generate-fake-object-record-event.ts deleted file mode 100644 index 66e8026c5..000000000 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/generate-fake-object-record-event.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { v4 } from 'uuid'; - -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; -import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; - -export const generateFakeObjectRecordEvent = ( - objectMetadataEntity: ObjectMetadataEntity, - action: 'created' | 'updated' | 'deleted' | 'destroyed', -): - | ObjectRecordCreateEvent - | ObjectRecordUpdateEvent - | ObjectRecordDeleteEvent - | ObjectRecordDestroyEvent => { - const recordId = v4(); - const userId = v4(); - const workspaceMemberId = v4(); - - const after = objectMetadataEntity.fields.reduce((acc, field) => { - acc[field.name] = generateFakeValue(field.type); - - return acc; - }, {} as Entity); - - if (action === 'created') { - return { - recordId, - userId, - workspaceMemberId, - objectMetadata: objectMetadataEntity, - properties: { - after, - }, - } satisfies ObjectRecordCreateEvent; - } - - const before = objectMetadataEntity.fields.reduce((acc, field) => { - acc[field.name] = generateFakeValue(field.type); - - return acc; - }, {} as Entity); - - if (action === 'updated') { - return { - recordId, - userId, - workspaceMemberId, - objectMetadata: objectMetadataEntity, - properties: { - before, - after, - diff: after, - }, - } satisfies ObjectRecordUpdateEvent; - } - - if (action === 'deleted') { - return { - recordId, - userId, - workspaceMemberId, - objectMetadata: objectMetadataEntity, - properties: { - before, - }, - } satisfies ObjectRecordDeleteEvent; - } - - if (action === 'destroyed') { - return { - recordId, - userId, - workspaceMemberId, - objectMetadata: objectMetadataEntity, - properties: { - before, - }, - } satisfies ObjectRecordDestroyEvent; - } - - throw new Error(`Unknown action '${action}'`); -}; diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts index 392fe732c..363c4059e 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts @@ -15,7 +15,7 @@ export const objectRecordChangedValues = ( objectMetadata.fields.map((field) => [field.name, field]), ); - const changedValues = Object.keys(newRecord).reduce( + return Object.keys(newRecord).reduce( (acc, key) => { const field = fieldsByKey.get(key); const oldRecordValue = oldRecord[key]; @@ -36,6 +36,4 @@ export const objectRecordChangedValues = ( }, {} as Record, ); - - return changedValues; }; diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts index 71fb7c279..7e624cc67 100644 --- a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts @@ -27,6 +27,7 @@ import { MessagingModule } from 'src/modules/messaging/messaging.module'; import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module'; import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module'; import { WorkflowModule } from 'src/modules/workflow/workflow.module'; +import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module'; @Module({ imports: [ @@ -49,6 +50,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module'; WorkspaceQueryRunnerJobModule, AutoCompaniesAndContactsCreationJobModule, TimelineJobModule, + WebhookJobModule, WorkflowModule, ], providers: [ diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts index d7168bca8..7a2828d0f 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts @@ -37,6 +37,7 @@ import { import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { capitalize } from 'src/utils/capitalize'; import { getServerUrl } from 'src/utils/get-server-url'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class OpenApiService { @@ -81,9 +82,18 @@ export class OpenApiService { schema.webhooks = objectMetadataItems.reduce( (paths, item) => { - paths[`Create ${item.nameSingular}`] = computeWebhooks('create', item); - paths[`Update ${item.nameSingular}`] = computeWebhooks('update', item); - paths[`Delete ${item.nameSingular}`] = computeWebhooks('delete', item); + paths[`Create ${item.nameSingular}`] = computeWebhooks( + DatabaseEventAction.CREATED, + item, + ); + paths[`Update ${item.nameSingular}`] = computeWebhooks( + DatabaseEventAction.UPDATED, + item, + ); + paths[`Delete ${item.nameSingular}`] = computeWebhooks( + DatabaseEventAction.DELETED, + item, + ); return paths; }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts index f6e7fe249..8f7dd0977 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts @@ -2,17 +2,24 @@ import { OpenAPIV3_1 } from 'openapi-types'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { capitalize } from 'src/utils/capitalize'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; export const computeWebhooks = ( - type: 'create' | 'update' | 'delete', + type: DatabaseEventAction, item: ObjectMetadataEntity, ): OpenAPIV3_1.PathItemObject => { + const updatedFields = { + type: 'array', + items: { + type: 'string', + }, + }; + return { post: { tags: [item.nameSingular], security: [], requestBody: { - description: `*${type}*.**${item.nameSingular}**, ***.**${item.nameSingular}**, ***.*****`, content: { 'application/json': { schema: { @@ -22,17 +29,9 @@ export const computeWebhooks = ( type: 'string', example: 'https://example.com/incomingWebhook', }, - description: { + eventName: { type: 'string', - example: 'A sample description', - }, - eventType: { - type: 'string', - enum: [ - '*.*', - '*.' + item.nameSingular, - type + '.' + item.nameSingular, - ], + example: `${item.nameSingular}.${type}`, }, objectMetadata: { type: 'object', @@ -60,8 +59,9 @@ export const computeWebhooks = ( example: '2024-02-14T11:27:01.779Z', }, record: { - $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, + $ref: `#/components/schemas/${capitalize(item.nameSingular)} for Response`, }, + ...(type === DatabaseEventAction.UPDATED && { updatedFields }), }, }, }, diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 3810176f3..0590033fb 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -18,6 +18,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { assert } from 'src/utils/assert'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; export class UserWorkspaceService extends TypeOrmQueryService { constructor( @@ -88,7 +89,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { payload.recordId = workspaceMember[0].id; this.workspaceEventEmitter.emit( - 'workspaceMember.created', + `workspaceMember.${DatabaseEventAction.CREATED}`, [payload], workspaceId, ); diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index 5a832e0eb..a27dc91f9 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -17,6 +17,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; // eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UserService extends TypeOrmQueryService { @@ -115,7 +116,7 @@ export class UserService extends TypeOrmQueryService { payload.recordId = workspaceMember.id; this.workspaceEventEmitter.emit( - 'workspaceMember.deleted', + `workspaceMember.${DatabaseEventAction.DELETED}`, [payload], workspaceId, ); 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 index ad3c2b7ee..6edfa356b 100644 --- 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 @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { @@ -13,6 +12,8 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class WorkspaceWorkspaceMemberListener { @@ -22,7 +23,7 @@ export class WorkspaceWorkspaceMemberListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('workspaceMember.updated') + @OnDatabaseEvent('workspaceMember', DatabaseEventAction.UPDATED) async handleUpdateEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent @@ -50,7 +51,7 @@ export class WorkspaceWorkspaceMemberListener { ); } - @OnEvent('workspaceMember.deleted') + @OnDatabaseEvent('workspaceMember', DatabaseEventAction.DELETED) async handleDeleteEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent diff --git a/packages/twenty-server/src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener.ts b/packages/twenty-server/src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener.ts index 886321725..665acb577 100644 --- a/packages/twenty-server/src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener.ts +++ b/packages/twenty-server/src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; @@ -17,6 +16,8 @@ import { BlocklistReimportCalendarEventsJob, BlocklistReimportCalendarEventsJobData, } from 'src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class CalendarBlocklistListener { @@ -25,7 +26,7 @@ export class CalendarBlocklistListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('blocklist.created') + @OnDatabaseEvent('blocklist', DatabaseEventAction.CREATED) async handleCreatedEvent( payload: WorkspaceEventBatch< ObjectRecordCreateEvent @@ -37,7 +38,7 @@ export class CalendarBlocklistListener { ); } - @OnEvent('blocklist.deleted') + @OnDatabaseEvent('blocklist', DatabaseEventAction.DELETED) async handleDeletedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent @@ -49,7 +50,7 @@ export class CalendarBlocklistListener { ); } - @OnEvent('blocklist.updated') + @OnDatabaseEvent('blocklist', DatabaseEventAction.UPDATED) async handleUpdatedEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts index 1406d442d..dac22ff35 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; @@ -11,6 +10,8 @@ import { DeleteConnectedAccountAssociatedCalendarDataJobData, } from 'src/modules/calendar/calendar-event-cleaner/jobs/delete-connected-account-associated-calendar-data.job'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class CalendarEventCleanerConnectedAccountListener { @@ -19,7 +20,7 @@ export class CalendarEventCleanerConnectedAccountListener { private readonly calendarQueueService: MessageQueueService, ) {} - @OnEvent('connectedAccount.destroyed') + @OnDatabaseEvent('connectedAccount', DatabaseEventAction.DESTROYED) async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts index 0bbd2cf5a..369de49c7 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; @@ -17,6 +16,8 @@ import { CalendarEventParticipantUnmatchParticipantJobData, } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class CalendarEventParticipantPersonListener { @@ -25,7 +26,7 @@ export class CalendarEventParticipantPersonListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('person.created') + @OnDatabaseEvent('person', DatabaseEventAction.CREATED) async handleCreatedEvent( payload: WorkspaceEventBatch< ObjectRecordCreateEvent @@ -48,7 +49,7 @@ export class CalendarEventParticipantPersonListener { } } - @OnEvent('person.updated') + @OnDatabaseEvent('person', DatabaseEventAction.UPDATED) async handleUpdatedEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener.ts index 31d292124..dabca70ef 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; @@ -17,6 +16,8 @@ import { CalendarEventParticipantUnmatchParticipantJobData, } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class CalendarEventParticipantWorkspaceMemberListener { @@ -25,7 +26,7 @@ export class CalendarEventParticipantWorkspaceMemberListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('workspaceMember.created') + @OnDatabaseEvent('workspaceMember', DatabaseEventAction.CREATED) async handleCreatedEvent( payload: WorkspaceEventBatch< ObjectRecordCreateEvent @@ -47,7 +48,7 @@ export class CalendarEventParticipantWorkspaceMemberListener { } } - @OnEvent('workspaceMember.updated') + @OnDatabaseEvent('workspaceMember', DatabaseEventAction.UPDATED) async handleUpdatedEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent diff --git a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts index 62ee28d1b..073735e7d 100644 --- a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts +++ b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -7,6 +6,8 @@ import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspac import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class ConnectedAccountListener { @@ -15,7 +16,7 @@ export class ConnectedAccountListener { private readonly accountsToReconnectService: AccountsToReconnectService, ) {} - @OnEvent('connectedAccount.destroyed') + @OnDatabaseEvent('connectedAccount', DatabaseEventAction.DESTROYED) async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts index f49db465d..037a43e8f 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts @@ -7,6 +7,7 @@ import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/t import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @WorkspaceQueryHook(`connectedAccount.destroyOne`) export class ConnectedAccountDeleteOnePreQueryHook @@ -34,7 +35,7 @@ export class ConnectedAccountDeleteOnePreQueryHook }); this.workspaceEventEmitter.emit( - 'messageChannel.destroyed', + `messageChannel.${DatabaseEventAction.DESTROYED}`, messageChannels.map( (messageChannel) => ({ diff --git a/packages/twenty-server/src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-calendar-channel.listener.ts b/packages/twenty-server/src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-calendar-channel.listener.ts index 50a24f1d1..076c3dadb 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-calendar-channel.listener.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-calendar-channel.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { objectRecordChangedProperties } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util'; @@ -12,6 +11,8 @@ import { CalendarCreateCompanyAndContactAfterSyncJobData, } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class AutoCompaniesAndContactsCreationCalendarChannelListener { @@ -20,7 +21,7 @@ export class AutoCompaniesAndContactsCreationCalendarChannelListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('calendarChannel.updated') + @OnDatabaseEvent('calendarChannel', DatabaseEventAction.UPDATED) async handleUpdatedEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent diff --git a/packages/twenty-server/src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-message-channel.listener.ts b/packages/twenty-server/src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-message-channel.listener.ts index 6ed36b4be..60a394190 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-message-channel.listener.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-message-channel.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { objectRecordChangedProperties } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util'; @@ -12,6 +11,8 @@ import { MessagingCreateCompanyAndContactAfterSyncJob, MessagingCreateCompanyAndContactAfterSyncJobData, } from 'src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class AutoCompaniesAndContactsCreationMessageChannelListener { @@ -20,7 +21,7 @@ export class AutoCompaniesAndContactsCreationMessageChannelListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('messageChannel.updated') + @OnDatabaseEvent('messageChannel', DatabaseEventAction.UPDATED) async handleUpdatedEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts index d9fd73740..712de3cc7 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts @@ -25,6 +25,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { isWorkEmail } from 'src/utils/is-work-email'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class CreateCompanyAndContactService { @@ -195,7 +196,7 @@ export class CreateCompanyAndContactService { ); this.workspaceEventEmitter.emit( - 'person.created', + `person.${DatabaseEventAction.CREATED}`, createdPeople.map( (createdPerson) => ({ diff --git a/packages/twenty-server/src/modules/messaging/blocklist-manager/listeners/messaging-blocklist.listener.ts b/packages/twenty-server/src/modules/messaging/blocklist-manager/listeners/messaging-blocklist.listener.ts index d232e9040..c038f62fb 100644 --- a/packages/twenty-server/src/modules/messaging/blocklist-manager/listeners/messaging-blocklist.listener.ts +++ b/packages/twenty-server/src/modules/messaging/blocklist-manager/listeners/messaging-blocklist.listener.ts @@ -1,5 +1,4 @@ import { Injectable, Scope } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; @@ -17,6 +16,8 @@ import { BlocklistReimportMessagesJob, BlocklistReimportMessagesJobData, } from 'src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable({ scope: Scope.REQUEST }) export class MessagingBlocklistListener { @@ -25,7 +26,7 @@ export class MessagingBlocklistListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('blocklist.created') + @OnDatabaseEvent('blocklist', DatabaseEventAction.CREATED) async handleCreatedEvent( payload: WorkspaceEventBatch< ObjectRecordCreateEvent @@ -37,7 +38,7 @@ export class MessagingBlocklistListener { ); } - @OnEvent('blocklist.deleted') + @OnDatabaseEvent('blocklist', DatabaseEventAction.CREATED) async handleDeletedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent @@ -49,7 +50,7 @@ export class MessagingBlocklistListener { ); } - @OnEvent('blocklist.updated') + @OnDatabaseEvent('blocklist', DatabaseEventAction.UPDATED) async handleUpdatedEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent diff --git a/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts b/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts index c5c3a033d..5127f2f66 100644 --- a/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; @@ -11,6 +10,8 @@ import { MessagingConnectedAccountDeletionCleanupJob, MessagingConnectedAccountDeletionCleanupJobData, } from 'src/modules/messaging/message-cleaner/jobs/messaging-connected-account-deletion-cleanup.job'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class MessagingMessageCleanerConnectedAccountListener { @@ -19,7 +20,7 @@ export class MessagingMessageCleanerConnectedAccountListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('connectedAccount.destroyed') + @OnDatabaseEvent('connectedAccount', DatabaseEventAction.DESTROYED) async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts index 80802c3fb..859ff9dd2 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; @@ -11,6 +10,8 @@ import { MessagingCleanCacheJob, MessagingCleanCacheJobData, } from 'src/modules/messaging/message-import-manager/jobs/messaging-clean-cache'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class MessagingMessageImportManagerMessageChannelListener { @@ -19,7 +20,7 @@ export class MessagingMessageImportManagerMessageChannelListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('messageChannel.destroyed') + @OnDatabaseEvent('messageChannel', DatabaseEventAction.DESTROYED) async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts index 53bf3329a..434d174d2 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; @@ -17,6 +16,8 @@ import { MessageParticipantUnmatchParticipantJobData, } from 'src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class MessageParticipantPersonListener { @@ -25,7 +26,7 @@ export class MessageParticipantPersonListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('person.created') + @OnDatabaseEvent('person', DatabaseEventAction.CREATED) async handleCreatedEvent( payload: WorkspaceEventBatch< ObjectRecordCreateEvent @@ -47,7 +48,7 @@ export class MessageParticipantPersonListener { } } - @OnEvent('person.updated') + @OnDatabaseEvent('person', DatabaseEventAction.UPDATED) async handleUpdatedEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts index 6d093a1cc..0b7f44886 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -24,6 +23,8 @@ import { MessageParticipantUnmatchParticipantJobData, } from 'src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class MessageParticipantWorkspaceMemberListener { @@ -34,7 +35,7 @@ export class MessageParticipantWorkspaceMemberListener { private readonly workspaceRepository: Repository, ) {} - @OnEvent('workspaceMember.created') + @OnDatabaseEvent('workspaceMember', DatabaseEventAction.CREATED) async handleCreatedEvent( payload: WorkspaceEventBatch< ObjectRecordCreateEvent @@ -67,7 +68,7 @@ export class MessageParticipantWorkspaceMemberListener { } } - @OnEvent('workspaceMember.updated') + @OnDatabaseEvent('workspaceMember', DatabaseEventAction.UPDATED) async handleUpdatedEvent( payload: WorkspaceEventBatch< ObjectRecordUpdateEvent diff --git a/packages/twenty-server/src/modules/timeline/jobs/create-audit-log-from-internal-event.ts b/packages/twenty-server/src/modules/timeline/jobs/create-audit-log-from-internal-event.ts index b831c0c38..cbe755b91 100644 --- a/packages/twenty-server/src/modules/timeline/jobs/create-audit-log-from-internal-event.ts +++ b/packages/twenty-server/src/modules/timeline/jobs/create-audit-log-from-internal-event.ts @@ -20,15 +20,15 @@ export class CreateAuditLogFromInternalEvent { @Process(CreateAuditLogFromInternalEvent.name) async handle( - data: WorkspaceEventBatch, + workspaceEventBatch: WorkspaceEventBatch, ): Promise { - for (const eventData of data.events) { + for (const eventData of workspaceEventBatch.events) { let workspaceMemberId: string | null = null; if (eventData.userId) { const workspaceMember = await this.workspaceMemberService.getByIdOrFail( eventData.userId, - data.workspaceId, + workspaceEventBatch.workspaceId, ); workspaceMemberId = workspaceMember.id; @@ -42,13 +42,13 @@ export class CreateAuditLogFromInternalEvent { } await this.auditLogRepository.insert( - data.name, + workspaceEventBatch.name, eventData.properties, workspaceMemberId, - data.name.split('.')[0], + workspaceEventBatch.name.split('.')[0], eventData.objectMetadata.id, eventData.recordId, - data.workspaceId, + workspaceEventBatch.workspaceId, ); } } diff --git a/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts b/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts index 89d372163..fb35fa903 100644 --- a/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts +++ b/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts @@ -18,13 +18,13 @@ export class UpsertTimelineActivityFromInternalEvent { @Process(UpsertTimelineActivityFromInternalEvent.name) async handle( - data: WorkspaceEventBatch, + workspaceEventBatch: WorkspaceEventBatch, ): Promise { - for (const eventData of data.events) { + for (const eventData of workspaceEventBatch.events) { if (eventData.userId) { const workspaceMember = await this.workspaceMemberService.getByIdOrFail( eventData.userId, - data.workspaceId, + workspaceEventBatch.workspaceId, ); eventData.workspaceMemberId = workspaceMember.id; @@ -48,9 +48,9 @@ export class UpsertTimelineActivityFromInternalEvent { } await this.timelineActivityService.upsertEvent({ - ...eventData, - workspaceId: data.workspaceId, - name: data.name, + event: eventData, + eventName: workspaceEventBatch.name, + workspaceId: workspaceEventBatch.workspaceId, }); } } diff --git a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts index 12dfe5a52..26d715180 100644 --- a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts +++ b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { ObjectRecordBaseEventWithNameAndWorkspaceId } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; +import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; -type TransformedEvent = ObjectRecordBaseEventWithNameAndWorkspaceId & { +type TransformedEvent = ObjectRecordBaseEvent & { objectName?: string; linkedRecordCachedName?: string; linkedRecordId?: string; @@ -26,18 +26,26 @@ export class TimelineActivityService { task: 'taskTarget', }; - async upsertEvent(event: ObjectRecordBaseEventWithNameAndWorkspaceId) { - const events = await this.transformEvent(event); + async upsertEvent({ + event, + eventName, + workspaceId, + }: { + event: ObjectRecordBaseEvent; + eventName: string; + workspaceId: string; + }) { + const events = await this.transformEvent({ event, workspaceId, eventName }); if (!events || events.length === 0) return; for (const event of events) { await this.timelineActivityRepository.upsertOne( - event.name, + eventName, event.properties, event.objectName ?? event.objectMetadata.nameSingular, event.recordId, - event.workspaceId, + workspaceId, event.workspaceMemberId, event.linkedRecordCachedName, event.linkedRecordId, @@ -46,11 +54,21 @@ export class TimelineActivityService { } } - private async transformEvent( - event: ObjectRecordBaseEventWithNameAndWorkspaceId, - ): Promise { + private async transformEvent({ + event, + workspaceId, + eventName, + }: { + event: ObjectRecordBaseEvent; + workspaceId: string; + eventName: string; + }): Promise { if (['note', 'task'].includes(event.objectMetadata.nameSingular)) { - const linkedObjects = await this.handleLinkedObjects(event); + const linkedObjects = await this.handleLinkedObjects({ + event, + workspaceId, + eventName, + }); // 2 timelines, one for the linked object and one for the task/note if (linkedObjects?.length > 0) return [...linkedObjects, event]; @@ -61,56 +79,81 @@ export class TimelineActivityService { event.objectMetadata.nameSingular, ) ) { - const linkedObjects = await this.handleLinkedObjects(event); - - return linkedObjects; + return await this.handleLinkedObjects({ event, workspaceId, eventName }); } return [event]; } - private async handleLinkedObjects( - event: ObjectRecordBaseEventWithNameAndWorkspaceId, - ) { - const dataSourceSchema = this.workspaceDataSourceService.getSchemaName( - event.workspaceId, - ); + private async handleLinkedObjects({ + event, + workspaceId, + eventName, + }: { + event: ObjectRecordBaseEvent; + workspaceId: string; + eventName: string; + }) { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); switch (event.objectMetadata.nameSingular) { case 'noteTarget': - return this.processActivityTarget(event, dataSourceSchema, 'note'); - case 'taskTarget': - return this.processActivityTarget(event, dataSourceSchema, 'task'); - case 'note': - case 'task': - return this.processActivity( + return this.processActivityTarget({ event, dataSourceSchema, - event.objectMetadata.nameSingular, - ); + activityType: 'note', + eventName, + workspaceId, + }); + case 'taskTarget': + return this.processActivityTarget({ + event, + dataSourceSchema, + activityType: 'task', + eventName, + workspaceId, + }); + case 'note': + case 'task': + return this.processActivity({ + event, + dataSourceSchema, + activityType: event.objectMetadata.nameSingular, + eventName, + workspaceId, + }); default: return []; } } - private async processActivity( - event: ObjectRecordBaseEventWithNameAndWorkspaceId, - dataSourceSchema: string, - activityType: string, - ) { + private async processActivity({ + event, + dataSourceSchema, + activityType, + eventName, + workspaceId, + }: { + event: ObjectRecordBaseEvent; + dataSourceSchema: string; + activityType: string; + eventName: string; + workspaceId: string; + }) { const activityTargets = await this.workspaceDataSourceService.executeRawQuery( `SELECT * FROM ${dataSourceSchema}."${this.targetObjects[activityType]}" WHERE "${activityType}Id" = $1`, [event.recordId], - event.workspaceId, + workspaceId, ); const activity = await this.workspaceDataSourceService.executeRawQuery( `SELECT * FROM ${dataSourceSchema}."${activityType}" WHERE "id" = $1`, [event.recordId], - event.workspaceId, + workspaceId, ); if (activityTargets.length === 0) return; @@ -135,7 +178,7 @@ export class TimelineActivityService { return { ...event, - name: 'linked-' + event.name, + name: 'linked-' + eventName, objectName: targetColumn[0].replace(/Id$/, ''), recordId: activityTarget[targetColumn[0]], linkedRecordCachedName: activity[0].title, @@ -146,17 +189,25 @@ export class TimelineActivityService { .filter((event): event is TransformedEvent => event !== undefined); } - private async processActivityTarget( - event: ObjectRecordBaseEventWithNameAndWorkspaceId, - dataSourceSchema: string, - activityType: string, - ) { + private async processActivityTarget({ + event, + dataSourceSchema, + activityType, + eventName, + workspaceId, + }: { + event: ObjectRecordBaseEvent; + dataSourceSchema: string; + activityType: string; + eventName: string; + workspaceId: string; + }) { const activityTarget = await this.workspaceDataSourceService.executeRawQuery( `SELECT * FROM ${dataSourceSchema}."${this.targetObjects[activityType]}" WHERE "id" = $1`, [event.recordId], - event.workspaceId, + workspaceId, ); if (activityTarget.length === 0) return; @@ -165,7 +216,7 @@ export class TimelineActivityService { `SELECT * FROM ${dataSourceSchema}."${activityType}" WHERE "id" = $1`, [activityTarget[0].activityId], - event.workspaceId, + workspaceId, ); if (activity.length === 0) return; @@ -189,14 +240,14 @@ export class TimelineActivityService { return [ { ...event, - name: 'linked-' + event.name, + name: 'linked-' + eventName, properties: {}, objectName: targetColumn[0].replace(/Id$/, ''), recordId: activityTarget[0][targetColumn[0]], linkedRecordCachedName: activity[0].title, linkedRecordId: activity[0].id, linkedObjectMetadataId: activityObjectMetadataId, - }, - ] as TransformedEvent[]; + } as TransformedEvent, + ]; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts b/packages/twenty-server/src/modules/webhook/jobs/call-webhook-jobs.job.ts similarity index 52% rename from packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts rename to packages/twenty-server/src/modules/webhook/jobs/call-webhook-jobs.job.ts index 2b3729b60..8efcec254 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts +++ b/packages/twenty-server/src/modules/webhook/jobs/call-webhook-jobs.job.ts @@ -2,12 +2,6 @@ import { Logger } from '@nestjs/common'; import { ArrayContains } from 'typeorm'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; - -import { - CallWebhookJob, - CallWebhookJobData, -} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job'; 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'; @@ -15,20 +9,12 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity'; - -export enum CallWebhookJobsJobOperation { - create = 'create', - update = 'update', - delete = 'delete', - destroy = 'destroy', -} - -export type CallWebhookJobsJobData = { - workspaceId: string; - objectMetadataItem: ObjectMetadataInterface; - record: any; - operation: CallWebhookJobsJobOperation; -}; +import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; +import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; +import { + CallWebhookJob, + CallWebhookJobData, +} from 'src/modules/webhook/jobs/call-webhook.job'; @Processor(MessageQueue.webhookQueue) export class CallWebhookJobsJob { @@ -41,51 +27,64 @@ export class CallWebhookJobsJob { ) {} @Process(CallWebhookJobsJob.name) - async handle(data: CallWebhookJobsJobData): Promise { + async handle( + workspaceEventBatch: WorkspaceEventBatch, + ): Promise { // If you change that function, double check it does not break Zapier // trigger in packages/twenty-zapier/src/triggers/trigger_record.ts + // Also change the openApi schema for webhooks + // packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts const webhookRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( - data.workspaceId, + workspaceEventBatch.workspaceId, 'webhook', ); - const nameSingular = data.objectMetadataItem.nameSingular; - const operation = data.operation; - const eventName = `${nameSingular}.${operation}`; + const [nameSingular, operation] = workspaceEventBatch.name.split('.'); const webhooks = await webhookRepository.find({ where: [ - { operations: ArrayContains([eventName]) }, + { operations: ArrayContains([`${nameSingular}.${operation}`]) }, { operations: ArrayContains([`*.${operation}`]) }, { operations: ArrayContains([`${nameSingular}.*`]) }, { operations: ArrayContains(['*.*']) }, ], }); - webhooks.forEach((webhook) => { - this.messageQueueService.add( - CallWebhookJob.name, - { + for (const eventData of workspaceEventBatch.events) { + const eventName = workspaceEventBatch.name; + const objectMetadata = { + id: eventData.objectMetadata.id, + nameSingular: eventData.objectMetadata.nameSingular, + }; + const workspaceId = workspaceEventBatch.workspaceId; + const record = eventData.properties.after || eventData.properties.before; + const updatedFields = eventData.properties.updatedFields; + + webhooks.forEach((webhook) => { + const webhookData = { targetUrl: webhook.targetUrl, eventName, - objectMetadata: { - id: data.objectMetadataItem.id, - nameSingular: data.objectMetadataItem.nameSingular, - }, - workspaceId: data.workspaceId, + objectMetadata, + workspaceId, webhookId: webhook.id, eventDate: new Date(), - record: data.record, - }, - { retryLimit: 3 }, - ); - }); + record, + ...(updatedFields && { updatedFields }), + }; - webhooks.length > 0 && - this.logger.log( - `CallWebhookJobsJob on eventName '${eventName}' triggered webhooks with ids [\n"${webhooks.map((webhook) => webhook.id).join('",\n"')}"\n]`, - ); + this.messageQueueService.add( + CallWebhookJob.name, + webhookData, + { retryLimit: 3 }, + ); + }); + + webhooks.length > 0 && + this.logger.log( + `CallWebhookJobsJob on eventName '${workspaceEventBatch.name}' triggered webhooks with ids [\n"${webhooks.map((webhook) => webhook.id).join('",\n"')}"\n]`, + ); + } } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job.ts b/packages/twenty-server/src/modules/webhook/jobs/call-webhook.job.ts similarity index 98% rename from packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job.ts rename to packages/twenty-server/src/modules/webhook/jobs/call-webhook.job.ts index f0ec8317a..31b72988d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job.ts +++ b/packages/twenty-server/src/modules/webhook/jobs/call-webhook.job.ts @@ -14,6 +14,7 @@ export type CallWebhookJobData = { webhookId: string; eventDate: Date; record: any; + updatedFields?: string[]; }; @Processor(MessageQueue.webhookQueue) diff --git a/packages/twenty-server/src/modules/webhook/jobs/webhook-job.module.ts b/packages/twenty-server/src/modules/webhook/jobs/webhook-job.module.ts new file mode 100644 index 000000000..a3606ce36 --- /dev/null +++ b/packages/twenty-server/src/modules/webhook/jobs/webhook-job.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; + +import { CallWebhookJobsJob } from 'src/modules/webhook/jobs/call-webhook-jobs.job'; +import { CallWebhookJob } from 'src/modules/webhook/jobs/call-webhook.job'; +import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; + +@Module({ + imports: [HttpModule, AnalyticsModule], + providers: [CallWebhookJobsJob, CallWebhookJob], +}) +export class WebhookJobModule {} diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts index d59b593d2..dbc7c63be 100644 --- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts +++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts @@ -16,6 +16,7 @@ import { WorkflowVersionWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @WorkspaceQueryHook({ key: `workflow.createMany`, @@ -62,7 +63,7 @@ export class WorkflowCreateManyPostQueryHook }); this.workspaceEventEmitter.emit( - `workflowVersion.created`, + `workflowVersion.${DatabaseEventAction.CREATED}`, workflowVersionsToCreate.map((workflowVersionToCreate) => { return { userId: authContext.user?.id, diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts index b9447a061..36df3f290 100644 --- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts +++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts @@ -16,6 +16,7 @@ import { WorkflowVersionWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @WorkspaceQueryHook({ key: `workflow.createOne`, @@ -58,7 +59,7 @@ export class WorkflowCreateOnePostQueryHook }); this.workspaceEventEmitter.emit( - `workflowVersion.created`, + `workflowVersion.${DatabaseEventAction.CREATED}`, [ { userId: authContext.user?.id, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts index d10b7fb37..84025618a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts @@ -6,10 +6,11 @@ import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/ import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; export const generateFakeObjectRecordEvent = ( objectMetadataEntity: ObjectMetadataEntity, - action: 'created' | 'updated' | 'deleted' | 'destroyed', + action: DatabaseEventAction, ): | ObjectRecordCreateEvent | ObjectRecordUpdateEvent @@ -21,7 +22,7 @@ export const generateFakeObjectRecordEvent = ( const after = generateFakeObjectRecord(objectMetadataEntity); - if (action === 'created') { + if (action === DatabaseEventAction.CREATED) { return { recordId, userId, @@ -35,7 +36,7 @@ export const generateFakeObjectRecordEvent = ( const before = generateFakeObjectRecord(objectMetadataEntity); - if (action === 'updated') { + if (action === DatabaseEventAction.UPDATED) { return { recordId, userId, @@ -44,12 +45,11 @@ export const generateFakeObjectRecordEvent = ( properties: { before, after, - diff: after, }, } satisfies ObjectRecordUpdateEvent; } - if (action === 'deleted') { + if (action === DatabaseEventAction.DELETED) { return { recordId, userId, @@ -61,7 +61,7 @@ export const generateFakeObjectRecordEvent = ( } satisfies ObjectRecordDeleteEvent; } - if (action === 'destroyed') { + if (action === DatabaseEventAction.DESTROYED) { return { recordId, userId, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.service.ts index e9c15add7..61ff08d89 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.service.ts @@ -5,7 +5,6 @@ import { join } from 'path'; import { Repository } from 'typeorm'; -import { generateFakeObjectRecordEvent } from 'src/engine/core-modules/event-emitter/utils/generate-fake-object-record-event'; import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; @@ -21,6 +20,9 @@ import { WorkflowTriggerType, } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; import { isDefined } from 'src/utils/is-defined'; +import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action'; +import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class WorkflowBuilderService { @@ -92,7 +94,7 @@ export class WorkflowBuilderService { }) { const [nameSingular, action] = eventName.split('.'); - if (!['created', 'updated', 'deleted', 'destroyed'].includes(action)) { + if (!checkStringIsDatabaseEventAction(action)) { return {}; } @@ -110,7 +112,7 @@ export class WorkflowBuilderService { return generateFakeObjectRecordEvent( objectMetadata, - action as 'created' | 'updated' | 'deleted' | 'destroyed', + action as DatabaseEventAction, ); } diff --git a/packages/twenty-server/src/modules/workflow/workflow-status/listeners/workflow-version-status.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-status/listeners/workflow-version-status.listener.ts index b8e3c9619..948e7f5f1 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-status/listeners/workflow-version-status.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-status/listeners/workflow-version-status.listener.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; @@ -17,6 +16,8 @@ import { WorkflowVersionEventType, WorkflowVersionStatusUpdate, } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class WorkflowVersionStatusListener { @@ -25,7 +26,7 @@ export class WorkflowVersionStatusListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('workflowVersion.created') + @OnDatabaseEvent('workflowVersion', DatabaseEventAction.CREATED) async handleWorkflowVersionCreated( payload: WorkspaceEventBatch< ObjectRecordCreateEvent @@ -53,7 +54,7 @@ export class WorkflowVersionStatusListener { ); } - @OnEvent('workflowVersion.statusUpdated') + @OnDatabaseEvent('workflowVersion', DatabaseEventAction.UPDATED) async handleWorkflowVersionUpdated( payload: WorkspaceEventBatch, ): Promise { @@ -67,7 +68,7 @@ export class WorkflowVersionStatusListener { ); } - @OnEvent('workflowVersion.deleted') + @OnDatabaseEvent('workflowVersion', DatabaseEventAction.DELETED) async handleWorkflowVersionDeleted( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts index 5ea3f82d4..b0faa2061 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts @@ -1,5 +1,4 @@ import { Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; @@ -17,6 +16,8 @@ import { WorkflowEventTriggerJob, WorkflowEventTriggerJobData, } from 'src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class DatabaseEventTriggerListener { @@ -29,28 +30,28 @@ export class DatabaseEventTriggerListener { private readonly isFeatureFlagEnabledService: FeatureFlagService, ) {} - @OnEvent('*.created') + @OnDatabaseEvent('*', DatabaseEventAction.CREATED) async handleObjectRecordCreateEvent( payload: WorkspaceEventBatch>, ) { await this.handleEvent(payload); } - @OnEvent('*.updated') + @OnDatabaseEvent('*', DatabaseEventAction.UPDATED) async handleObjectRecordUpdateEvent( payload: WorkspaceEventBatch>, ) { await this.handleEvent(payload); } - @OnEvent('*.deleted') + @OnDatabaseEvent('*', DatabaseEventAction.DELETED) async handleObjectRecordDeleteEvent( payload: WorkspaceEventBatch>, ) { await this.handleEvent(payload); } - @OnEvent('*.destroyed') + @OnDatabaseEvent('*', DatabaseEventAction.DESTROYED) async handleObjectRecordDestroyEvent( payload: WorkspaceEventBatch>, ) { 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 125e45780..eeea06b1a 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 @@ -25,6 +25,7 @@ import { 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 { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class WorkflowTriggerWorkspaceService { @@ -362,7 +363,7 @@ export class WorkflowTriggerWorkspaceService { } this.workspaceEventEmitter.emit( - 'workflowVersion.statusUpdated', + `workflowVersion.${DatabaseEventAction.UPDATED}`, [ { workflowId, diff --git a/packages/twenty-zapier/package.json b/packages/twenty-zapier/package.json index 307906787..4c57386e9 100644 --- a/packages/twenty-zapier/package.json +++ b/packages/twenty-zapier/package.json @@ -1,6 +1,6 @@ { "name": "twenty-zapier", - "version": "1.0.2", + "version": "2.0.0", "description": "Effortlessly sync Twenty with 3000+ apps. Automate tasks, boost productivity, and supercharge your customer relationships!", "main": "src/index.ts", "scripts": { diff --git a/packages/twenty-zapier/src/creates/crud_record.ts b/packages/twenty-zapier/src/creates/crud_record.ts index d0beb0ee4..43c34a3dc 100644 --- a/packages/twenty-zapier/src/creates/crud_record.ts +++ b/packages/twenty-zapier/src/creates/crud_record.ts @@ -7,7 +7,7 @@ import { computeInputFields } from '../utils/computeInputFields'; import { InputData } from '../utils/data.types'; import handleQueryParams from '../utils/handleQueryParams'; import requestDb, { requestSchema } from '../utils/requestDb'; -import { Operation } from '../utils/triggers/triggers.utils'; +import { DatabaseEventAction } from '../utils/triggers/triggers.utils'; export const recordInputFields = async ( z: ZObject, @@ -24,7 +24,7 @@ export const recordInputFields = async ( const computeFields = async (z: ZObject, bundle: Bundle) => { const operation = bundle.inputData.crudZapierOperation; switch (operation) { - case Operation.delete: + case DatabaseEventAction.DELETED: return [ { key: 'id', @@ -34,9 +34,9 @@ const computeFields = async (z: ZObject, bundle: Bundle) => { required: true, }, ]; - case Operation.update: + case DatabaseEventAction.UPDATED: return recordInputFields(z, bundle, true); - case Operation.create: + case DatabaseEventAction.CREATED: return recordInputFields(z, bundle, false); default: return []; @@ -44,18 +44,18 @@ const computeFields = async (z: ZObject, bundle: Bundle) => { }; const computeQueryParameters = ( - operation: Operation, + operation: DatabaseEventAction, data: InputData, ): string => { switch (operation) { - case Operation.create: + case DatabaseEventAction.CREATED: return `data:{${handleQueryParams(data)}}`; - case Operation.update: + case DatabaseEventAction.UPDATED: return ` data:{${handleQueryParams(data)}}, id: "${data.id}" `; - case Operation.delete: + case DatabaseEventAction.DELETED: return ` id: "${data.id}" `; @@ -104,9 +104,9 @@ export default { required: true, label: 'Operation', choices: { - [Operation.create]: Operation.create, - [Operation.update]: Operation.update, - [Operation.delete]: Operation.delete, + [DatabaseEventAction.CREATED]: DatabaseEventAction.CREATED, + [DatabaseEventAction.UPDATED]: DatabaseEventAction.UPDATED, + [DatabaseEventAction.DELETED]: DatabaseEventAction.DELETED, }, altersDynamicFields: true, }, diff --git a/packages/twenty-zapier/src/test/creates/crud_record.test.ts b/packages/twenty-zapier/src/test/creates/crud_record.test.ts index 0fe5a3f4a..5065c8778 100644 --- a/packages/twenty-zapier/src/test/creates/crud_record.test.ts +++ b/packages/twenty-zapier/src/test/creates/crud_record.test.ts @@ -4,7 +4,7 @@ import { crudRecordKey } from '../../creates/crud_record'; import App from '../../index'; import getBundle from '../../utils/getBundle'; import requestDb from '../../utils/requestDb'; -import { Operation } from '../../utils/triggers/triggers.utils'; +import { DatabaseEventAction } from '../../utils/triggers/triggers.utils'; const appTester = createAppTester(App); tools.env.inject(); @@ -12,7 +12,7 @@ describe('creates.create_company', () => { test('should run to create a Company Record', async () => { const bundle = getBundle({ nameSingular: 'Company', - crudZapierOperation: Operation.create, + crudZapierOperation: DatabaseEventAction.CREATED, name: 'Company Name', address: { addressCity: 'Paris' }, linkedinLink: { @@ -56,7 +56,7 @@ describe('creates.create_company', () => { test('should run to create a Person Record', async () => { const bundle = getBundle({ nameSingular: 'Person', - crudZapierOperation: Operation.create, + crudZapierOperation: DatabaseEventAction.CREATED, name: { firstName: 'John', lastName: 'Doe' }, phones: { primaryPhoneNumber: '610203040', @@ -90,7 +90,7 @@ describe('creates.update_company', () => { test('should run to update a Company record', async () => { const createBundle = getBundle({ nameSingular: 'Company', - crudZapierOperation: Operation.create, + crudZapierOperation: DatabaseEventAction.CREATED, name: 'Company Name', employees: 25, }); @@ -104,7 +104,7 @@ describe('creates.update_company', () => { const updateBundle = getBundle({ nameSingular: 'Company', - crudZapierOperation: Operation.update, + crudZapierOperation: DatabaseEventAction.UPDATED, id: companyId, name: 'Updated Company Name', }); @@ -133,7 +133,7 @@ describe('creates.delete_company', () => { test('should run to delete a Company record', async () => { const createBundle = getBundle({ nameSingular: 'Company', - crudZapierOperation: Operation.create, + crudZapierOperation: DatabaseEventAction.CREATED, name: 'Delete Company Name', employees: 25, }); @@ -147,7 +147,7 @@ describe('creates.delete_company', () => { const deleteBundle = getBundle({ nameSingular: 'Company', - crudZapierOperation: Operation.delete, + crudZapierOperation: DatabaseEventAction.DELETED, id: companyId, }); diff --git a/packages/twenty-zapier/src/test/triggers/trigger_record.test.ts b/packages/twenty-zapier/src/test/triggers/trigger_record.test.ts index 9139629da..14d75e368 100644 --- a/packages/twenty-zapier/src/test/triggers/trigger_record.test.ts +++ b/packages/twenty-zapier/src/test/triggers/trigger_record.test.ts @@ -4,13 +4,14 @@ import App from '../../index'; import { triggerRecordKey } from '../../triggers/trigger_record'; import getBundle from '../../utils/getBundle'; import requestDb from '../../utils/requestDb'; +import { DatabaseEventAction } from '../../utils/triggers/triggers.utils'; const appTester = createAppTester(App); describe('triggers.trigger_record.created', () => { test('should succeed to subscribe', async () => { const bundle = getBundle({}); bundle.inputData.nameSingular = 'company'; - bundle.inputData.operation = 'create'; + bundle.inputData.operation = DatabaseEventAction.CREATED; bundle.targetUrl = 'https://test.com'; const result = await appTester( App.triggers[triggerRecordKey].operation.performSubscribe, diff --git a/packages/twenty-zapier/src/triggers/trigger_record.ts b/packages/twenty-zapier/src/triggers/trigger_record.ts index f5b1b83f1..c61259dc9 100644 --- a/packages/twenty-zapier/src/triggers/trigger_record.ts +++ b/packages/twenty-zapier/src/triggers/trigger_record.ts @@ -1,20 +1,21 @@ -import { Bundle, ZObject } from 'zapier-platform-core'; - import { findObjectNamesSingularKey } from '../triggers/find_object_names_singular'; import { - listSample, - Operation, - perform, + performSubscribe, performUnsubscribe, - subscribe, + perform, + performList, + DatabaseEventAction, } from '../utils/triggers/triggers.utils'; export const triggerRecordKey = 'trigger_record'; -const performSubscribe = (z: ZObject, bundle: Bundle) => - subscribe(z, bundle, bundle.inputData.operation); -const performList = (z: ZObject, bundle: Bundle) => - listSample(z, bundle, bundle.inputData.operation === Operation.delete); +const choices = Object.values(DatabaseEventAction).reduce( + (acc, action) => { + acc[action] = action; + return acc; + }, + {} as Record, +); export default { key: triggerRecordKey, @@ -37,12 +38,7 @@ export default { key: 'operation', required: true, label: 'Operation', - choices: { - [Operation.create]: Operation.create, - [Operation.update]: Operation.update, - [Operation.delete]: Operation.delete, - [Operation.destroy]: Operation.destroy, - }, + choices, altersDynamicFields: true, }, ], diff --git a/packages/twenty-zapier/src/utils/requestDb.ts b/packages/twenty-zapier/src/utils/requestDb.ts index 6426e7c70..5e5582049 100644 --- a/packages/twenty-zapier/src/utils/requestDb.ts +++ b/packages/twenty-zapier/src/utils/requestDb.ts @@ -79,7 +79,7 @@ export const requestDbViaRestApi = ( z: ZObject, bundle: Bundle, objectNamePlural: string, -) => { +): Promise[]> => { const options = { url: `${ bundle.authData.apiUrl || process.env.SERVER_BASE_URL diff --git a/packages/twenty-zapier/src/utils/triggers/triggers.utils.ts b/packages/twenty-zapier/src/utils/triggers/triggers.utils.ts index 9fa1188a1..e757308f2 100644 --- a/packages/twenty-zapier/src/utils/triggers/triggers.utils.ts +++ b/packages/twenty-zapier/src/utils/triggers/triggers.utils.ts @@ -7,48 +7,28 @@ import requestDb, { requestSchema, } from '../../utils/requestDb'; -export enum Operation { - create = 'create', - update = 'update', - delete = 'delete', - destroy = 'destroy', +export enum DatabaseEventAction { + CREATED = 'created', + UPDATED = 'updated', + DELETED = 'deleted', + DESTROYED = 'destroyed', } -export const subscribe = async ( - z: ZObject, - bundle: Bundle, - operation: Operation, -) => { - try { - const data = { - targetUrl: bundle.targetUrl, - operations: [`${bundle.inputData.nameSingular}.${operation}`], - }; - const result = await requestDb( - z, - bundle, - `mutation createWebhook {createWebhook(data:{${handleQueryParams( - data, - )}}) {id}}`, - ); - return result.data.createWebhook; - } catch (e) { - // Remove that catch code when VERSION 0.32 is deployed - // probably removable after 01/11/2024 - // (ie: when operations column exists in all active workspace schemas) - const data = { - targetUrl: bundle.targetUrl, - operation: `${bundle.inputData.nameSingular}.${operation}`, - }; - const result = await requestDb( - z, - bundle, - `mutation createWebhook {createWebhook(data:{${handleQueryParams( - data, - )}}) {id}}`, - ); - return result.data.createWebhook; - } +export const performSubscribe = async (z: ZObject, bundle: Bundle) => { + const data = { + targetUrl: bundle.targetUrl, + operations: [ + `${bundle.inputData.nameSingular}.${bundle.inputData.operation}`, + ], + }; + const result = await requestDb( + z, + bundle, + `mutation createWebhook {createWebhook(data:{${handleQueryParams( + data, + )}}) {id}}`, + ); + return result.data.createWebhook; }; export const performUnsubscribe = async (z: ZObject, bundle: Bundle) => { @@ -62,20 +42,26 @@ export const performUnsubscribe = async (z: ZObject, bundle: Bundle) => { }; export const perform = (z: ZObject, bundle: Bundle) => { - const record = bundle.cleanedRequest.record; - if (record.createdAt) { - record.createdAt = record.createdAt + 'Z'; + const data = { + record: bundle.cleanedRequest.record, + ...(bundle.cleanedRequest.updatedFields && { + updatedFields: bundle.cleanedRequest.updatedFields, + }), + }; + if (data.record.createdAt) { + data.record.createdAt = data.record.createdAt + 'Z'; } - if (record.updatedAt) { - record.updatedAt = record.updatedAt + 'Z'; + if (data.record.updatedAt) { + data.record.updatedAt = data.record.updatedAt + 'Z'; } - if (record.revokedAt) { - record.revokedAt = record.revokedAt + 'Z'; + if (data.record.revokedAt) { + data.record.revokedAt = data.record.revokedAt + 'Z'; } - if (record.expiresAt) { - record.expiresAt = record.expiresAt + 'Z'; + if (data.record.expiresAt) { + data.record.expiresAt = data.record.expiresAt + 'Z'; } - return [record]; + + return [data]; }; const getNamePluralFromNameSingular = async ( @@ -92,10 +78,9 @@ const getNamePluralFromNameSingular = async ( throw new Error(`Unknown Object Name Singular ${nameSingular}`); }; -export const listSample = async ( +export const performList = async ( z: ZObject, bundle: Bundle, - onlyIds = false, ): Promise => { const nameSingular = bundle.inputData.nameSingular; const namePlural = await getNamePluralFromNameSingular( @@ -103,19 +88,13 @@ export const listSample = async ( bundle, nameSingular, ); - const result: { [key: string]: string }[] = await requestDbViaRestApi( - z, - bundle, - namePlural, - ); - - if (onlyIds) { - return result.map((res) => { - return { - id: res.id, - }; - }); - } - - return result; + const results = await requestDbViaRestApi(z, bundle, namePlural); + return results.map((result) => ({ + record: result, + ...(bundle.inputData.operation === DatabaseEventAction.UPDATED && { + updatedFields: Object.keys(result).filter((key) => key !== 'id')?.[0] || [ + 'updatedField', + ], + }), + })); };