mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 04:37:56 +00:00 
			
		
		
		
	6071 return only updated fields of records in zapier update trigger (#8193)
- move webhook triggers into `entity-events-to-db.listener.ts` - refactor event management - add a `@OnDatabaseEvent` decorator to manage database events - add updatedFields in updated events - update openApi webhooks docs - update zapier integration
This commit is contained in:
		| @@ -1,3 +1,5 @@ | |||||||
|  | const path = require('path'); | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   extends: ['../../.eslintrc.cjs', '../../.eslintrc.react.cjs'], |   extends: ['../../.eslintrc.cjs', '../../.eslintrc.react.cjs'], | ||||||
|   ignorePatterns: [ |   ignorePatterns: [ | ||||||
| @@ -23,8 +25,10 @@ module.exports = { | |||||||
|       }, |       }, | ||||||
|       plugins: ['project-structure'], |       plugins: ['project-structure'], | ||||||
|       settings: { |       settings: { | ||||||
|         'project-structure/folder-structure-config-path': |         'project-structure/folder-structure-config-path':path.resolve( | ||||||
|           'packages/twenty-front/folderStructure.json', |           __dirname, | ||||||
|  |           'folderStructure.json' | ||||||
|  |         ) | ||||||
|       }, |       }, | ||||||
|       rules: { |       rules: { | ||||||
|         'project-structure/folder-structure': 'error', |         'project-structure/folder-structure': 'error', | ||||||
|   | |||||||
| @@ -1,20 +1,14 @@ | |||||||
| import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption'; | import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption'; | ||||||
|  | jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00.000Z')); | ||||||
|  |  | ||||||
| describe('findAvailableTimeZoneOption', () => { | describe('findAvailableTimeZoneOption', () => { | ||||||
|   it('should find the matching available IANA time zone select option from a given IANA time zone', () => { |   it('should find the matching available IANA time zone select option from a given IANA time zone', () => { | ||||||
|     const ianaTimeZone = 'Europe/Paris'; |     const ianaTimeZone = 'Europe/Paris'; | ||||||
|     const expectedValue = 'Europe/Paris'; |     const value = 'Europe/Paris'; | ||||||
|     const expectedLabelWinter = |     const label = '(GMT+01:00) Central European Standard Time - Paris'; | ||||||
|       '(GMT+01:00) Central European Standard Time - Paris'; |  | ||||||
|     const expectedLabelSummer = |  | ||||||
|       '(GMT+02:00) Central European Summer Time - Paris'; |  | ||||||
|  |  | ||||||
|     const option = findAvailableTimeZoneOption(ianaTimeZone); |     const option = findAvailableTimeZoneOption(ianaTimeZone); | ||||||
|  |  | ||||||
|     expect(option.value).toEqual(expectedValue); |     expect(option).toEqual({ value, label }); | ||||||
|     expect( |  | ||||||
|       expectedLabelWinter === option.label || |  | ||||||
|         expectedLabelSummer === option.label, |  | ||||||
|     ).toBeTruthy(); |  | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,19 +1,14 @@ | |||||||
| import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel'; | import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel'; | ||||||
|  | jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00.000Z')); | ||||||
|  |  | ||||||
| describe('formatTimeZoneLabel', () => { | describe('formatTimeZoneLabel', () => { | ||||||
|   it('should format the time zone label correctly when location is included in the label', () => { |   it('should format the time zone label correctly when location is included in the label', () => { | ||||||
|     const ianaTimeZone = 'Europe/Paris'; |     const ianaTimeZone = 'Europe/Paris'; | ||||||
|     const expectedLabelSummer = |     const expectedLabel = '(GMT+01:00) Central European Standard Time - Paris'; | ||||||
|       '(GMT+02:00) Central European Summer Time - Paris'; |  | ||||||
|     const expectedLabelWinter = |  | ||||||
|       '(GMT+01:00) Central European Standard Time - Paris'; |  | ||||||
|  |  | ||||||
|     const formattedLabel = formatTimeZoneLabel(ianaTimeZone); |     const formattedLabel = formatTimeZoneLabel(ianaTimeZone); | ||||||
|  |  | ||||||
|     expect( |     expect(expectedLabel).toEqual(formattedLabel); | ||||||
|       expectedLabelSummer === formattedLabel || |  | ||||||
|         expectedLabelWinter === formattedLabel, |  | ||||||
|     ).toBeTruthy(); |  | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   it('should format the time zone label correctly when location is not included in the label', () => { |   it('should format the time zone label correctly when location is not included in the label', () => { | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ export const DateTimeSettingsTimezone: Story = { | |||||||
|     await canvas.findByText('Date and time'); |     await canvas.findByText('Date and time'); | ||||||
|  |  | ||||||
|     const timezoneSelect = await canvas.findByText( |     const timezoneSelect = await canvas.findByText( | ||||||
|       '(GMT-04:00) Eastern Daylight Time - New York', |       '(GMT-05:00) Eastern Standard Time - New York', | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     userEvent.click(timezoneSelect); |     userEvent.click(timezoneSelect); | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ const meta: Meta<PageDecoratorArgs> = { | |||||||
|                 targetUrl: 'https://example.com/webhook', |                 targetUrl: 'https://example.com/webhook', | ||||||
|                 description: 'A Sample Description', |                 description: 'A Sample Description', | ||||||
|                 updatedAt: '2021-08-27T12:00:00Z', |                 updatedAt: '2021-08-27T12:00:00Z', | ||||||
|                 operation: 'create', |                 operation: 'created', | ||||||
|                 __typename: 'Webhook', |                 __typename: 'Webhook', | ||||||
|               }, |               }, | ||||||
|             }, |             }, | ||||||
|   | |||||||
| @@ -121,9 +121,9 @@ export const SettingsDevelopersWebhooksDetail = () => { | |||||||
|  |  | ||||||
|   const actionOptions: SelectOption<string>[] = [ |   const actionOptions: SelectOption<string>[] = [ | ||||||
|     { value: '*', label: 'All Actions', Icon: IconNorthStar }, |     { value: '*', label: 'All Actions', Icon: IconNorthStar }, | ||||||
|     { value: 'create', label: 'Created', Icon: IconPlus }, |     { value: 'created', label: 'Created', Icon: IconPlus }, | ||||||
|     { value: 'update', label: 'Updated', Icon: IconRefresh }, |     { value: 'updated', label: 'Updated', Icon: IconRefresh }, | ||||||
|     { value: 'delete', label: 'Deleted', Icon: IconTrash }, |     { value: 'deleted', label: 'Deleted', Icon: IconTrash }, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   const { updateOneRecord } = useUpdateOneRecord<Webhook>({ |   const { updateOneRecord } = useUpdateOneRecord<Webhook>({ | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
|  |  | ||||||
| import chalk from 'chalk'; |  | ||||||
| import { Command } from 'nest-commander'; | import { Command } from 'nest-commander'; | ||||||
|  | import chalk from 'chalk'; | ||||||
| import { Repository } from 'typeorm'; | import { Repository } from 'typeorm'; | ||||||
|  |  | ||||||
| import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; | import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; | ||||||
| @@ -43,17 +43,26 @@ export class CopyWebhookOperationIntoOperationsCommand extends ActiveWorkspacesC | |||||||
|  |  | ||||||
|       for (const webhook of webhooks) { |       for (const webhook of webhooks) { | ||||||
|         if ('operation' in webhook) { |         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)) { |           const [firstWebhookPart, lastWebhookPart] = newOpe.split('.'); | ||||||
|             newOperation = `${lastWebhookPart}.${firstWebhookPart}`; |  | ||||||
|  |           if ( | ||||||
|  |             ['created', 'updated', 'deleted', 'destroyed'].includes( | ||||||
|  |               firstWebhookPart, | ||||||
|  |             ) | ||||||
|  |           ) { | ||||||
|  |             newOpe = `${lastWebhookPart}.${firstWebhookPart}`; | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           await webhookRepository.update(webhook.id, { |           await webhookRepository.update(webhook.id, { | ||||||
|             operation: newOperation, |             operation: newOpe, | ||||||
|             operations: [newOperation], |             operations: [newOpe], | ||||||
|           }); |           }); | ||||||
|  |  | ||||||
|           this.logger.log( |           this.logger.log( | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |   }; | ||||||
|  | } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | export enum DatabaseEventAction { | ||||||
|  |   CREATED = 'created', | ||||||
|  |   UPDATED = 'updated', | ||||||
|  |   DELETED = 'deleted', | ||||||
|  |   DESTROYED = 'destroyed', | ||||||
|  | } | ||||||
| @@ -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 { 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 { 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 { 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 { 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 { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; | ||||||
| import { capitalize } from 'src/utils/capitalize'; | import { capitalize } from 'src/utils/capitalize'; | ||||||
|  |  | ||||||
| @@ -49,8 +41,6 @@ export class GraphqlQueryRunnerService { | |||||||
|     private readonly workspaceQueryHookService: WorkspaceQueryHookService, |     private readonly workspaceQueryHookService: WorkspaceQueryHookService, | ||||||
|     private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory, |     private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory, | ||||||
|     private readonly queryResultGettersFactory: QueryResultGettersFactory, |     private readonly queryResultGettersFactory: QueryResultGettersFactory, | ||||||
|     @InjectMessageQueue(MessageQueue.webhookQueue) |  | ||||||
|     private readonly messageQueueService: MessageQueueService, |  | ||||||
|     private readonly graphqlQueryResolverFactory: GraphqlQueryResolverFactory, |     private readonly graphqlQueryResolverFactory: GraphqlQueryResolverFactory, | ||||||
|     private readonly apiEventEmitterService: ApiEventEmitterService, |     private readonly apiEventEmitterService: ApiEventEmitterService, | ||||||
|   ) {} |   ) {} | ||||||
| @@ -312,7 +302,7 @@ export class GraphqlQueryRunnerService { | |||||||
|     args: RestoreManyResolverArgs, |     args: RestoreManyResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<ObjectRecord> { | ||||||
|     const result = await this.executeQuery< |     return await this.executeQuery< | ||||||
|       UpdateManyResolverArgs<Partial<ObjectRecord>>, |       UpdateManyResolverArgs<Partial<ObjectRecord>>, | ||||||
|       ObjectRecord |       ObjectRecord | ||||||
|     >( |     >( | ||||||
| @@ -323,8 +313,6 @@ export class GraphqlQueryRunnerService { | |||||||
|       }, |       }, | ||||||
|       options, |       options, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async executeQuery<Input extends ResolverArgs, Response>( |   private async executeQuery<Input extends ResolverArgs, Response>( | ||||||
| @@ -372,54 +360,6 @@ export class GraphqlQueryRunnerService { | |||||||
|       resultWithGettersArray, |       resultWithGettersArray, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const jobOperation = this.operationNameToJobOperation(operationName); |  | ||||||
|  |  | ||||||
|     if (jobOperation) { |  | ||||||
|       await this.triggerWebhooks(resultWithGettersArray, jobOperation, options); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return resultWithGetters; |     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<T>( |  | ||||||
|     jobsData: T[] | undefined, |  | ||||||
|     operation: CallWebhookJobsJobOperation, |  | ||||||
|     options: WorkspaceQueryRunnerOptions, |  | ||||||
|   ): Promise<void> { |  | ||||||
|     if (!jobsData || !Array.isArray(jobsData)) return; |  | ||||||
|  |  | ||||||
|     jobsData.forEach((jobData) => { |  | ||||||
|       this.messageQueueService.add<CallWebhookJobsJobData>( |  | ||||||
|         CallWebhookJobsJob.name, |  | ||||||
|         { |  | ||||||
|           record: jobData, |  | ||||||
|           workspaceId: options.authContext.workspace.id, |  | ||||||
|           operation, |  | ||||||
|           objectMetadataItem: options.objectMetadataItem, |  | ||||||
|         }, |  | ||||||
|         { retryLimit: 3 }, |  | ||||||
|       ); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; | ||||||
| import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; | 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() | @Injectable() | ||||||
| export class ApiEventEmitterService { | export class ApiEventEmitterService { | ||||||
| @@ -16,7 +18,7 @@ export class ApiEventEmitterService { | |||||||
|     objectMetadataItem: ObjectMetadataInterface, |     objectMetadataItem: ObjectMetadataInterface, | ||||||
|   ): void { |   ): void { | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       `${objectMetadataItem.nameSingular}.created`, |       `${objectMetadataItem.nameSingular}.${DatabaseEventAction.CREATED}`, | ||||||
|       records.map((record) => ({ |       records.map((record) => ({ | ||||||
|         userId: authContext.user?.id, |         userId: authContext.user?.id, | ||||||
|         recordId: record.id, |         recordId: record.id, | ||||||
| @@ -46,20 +48,28 @@ export class ApiEventEmitterService { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       `${objectMetadataItem.nameSingular}.updated`, |       `${objectMetadataItem.nameSingular}.${DatabaseEventAction.UPDATED}`, | ||||||
|       records.map((record) => { |       records.map((record) => { | ||||||
|  |         const before = this.removeGraphQLAndNestedProperties( | ||||||
|  |           mappedExistingRecords[record.id], | ||||||
|  |         ); | ||||||
|  |         const after = this.removeGraphQLAndNestedProperties(record); | ||||||
|  |         const diff = objectRecordChangedValues( | ||||||
|  |           before, | ||||||
|  |           after, | ||||||
|  |           updatedFields, | ||||||
|  |           objectMetadataItem, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         return { |         return { | ||||||
|           userId: authContext.user?.id, |           userId: authContext.user?.id, | ||||||
|           recordId: record.id, |           recordId: record.id, | ||||||
|           objectMetadata: objectMetadataItem, |           objectMetadata: objectMetadataItem, | ||||||
|           properties: { |           properties: { | ||||||
|             before: mappedExistingRecords[record.id] |             before, | ||||||
|               ? this.removeGraphQLAndNestedProperties( |             after, | ||||||
|                   mappedExistingRecords[record.id], |  | ||||||
|                 ) |  | ||||||
|               : undefined, |  | ||||||
|             after: this.removeGraphQLAndNestedProperties(record), |  | ||||||
|             updatedFields, |             updatedFields, | ||||||
|  |             diff, | ||||||
|           }, |           }, | ||||||
|         }; |         }; | ||||||
|       }), |       }), | ||||||
| @@ -73,7 +83,7 @@ export class ApiEventEmitterService { | |||||||
|     objectMetadataItem: ObjectMetadataInterface, |     objectMetadataItem: ObjectMetadataInterface, | ||||||
|   ): void { |   ): void { | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       `${objectMetadataItem.nameSingular}.deleted`, |       `${objectMetadataItem.nameSingular}.${DatabaseEventAction.DELETED}`, | ||||||
|       records.map((record) => { |       records.map((record) => { | ||||||
|         return { |         return { | ||||||
|           userId: authContext.user?.id, |           userId: authContext.user?.id, | ||||||
| @@ -95,7 +105,7 @@ export class ApiEventEmitterService { | |||||||
|     objectMetadataItem: ObjectMetadataInterface, |     objectMetadataItem: ObjectMetadataInterface, | ||||||
|   ): void { |   ): void { | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       `${objectMetadataItem.nameSingular}.destroyed`, |       `${objectMetadataItem.nameSingular}.${DatabaseEventAction.DESTROYED}`, | ||||||
|       records.map((record) => { |       records.map((record) => { | ||||||
|         return { |         return { | ||||||
|           userId: authContext.user?.id, |           userId: authContext.user?.id, | ||||||
|   | |||||||
| @@ -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, | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -1,8 +1,6 @@ | |||||||
| import { HttpModule } from '@nestjs/axios'; | import { HttpModule } from '@nestjs/axios'; | ||||||
| import { Module } from '@nestjs/common'; | 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 { 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 { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module'; | ||||||
| import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; | import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; | ||||||
| @@ -14,9 +12,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works | |||||||
|     WorkspaceDataSourceModule, |     WorkspaceDataSourceModule, | ||||||
|     DataSourceModule, |     DataSourceModule, | ||||||
|     RecordPositionBackfillModule, |     RecordPositionBackfillModule, | ||||||
|     HttpModule, |  | ||||||
|     AnalyticsModule, |  | ||||||
|   ], |   ], | ||||||
|   providers: [CallWebhookJobsJob, CallWebhookJob, RecordPositionBackfillJob], |   providers: [RecordPositionBackfillJob], | ||||||
| }) | }) | ||||||
| export class WorkspaceQueryRunnerJobModule {} | export class WorkspaceQueryRunnerJobModule {} | ||||||
|   | |||||||
| @@ -1,55 +1,49 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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 { 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 { 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 { 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 { 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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; | ||||||
| import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; | 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 { 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 { 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() | @Injectable() | ||||||
| export class EntityEventsToDbListener { | export class EntityEventsToDbListener { | ||||||
|   constructor( |   constructor( | ||||||
|     @InjectMessageQueue(MessageQueue.entityEventsToDbQueue) |     @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( |   async handleCreate( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordCreateEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordCreateEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
|     return this.handle(payload); |     return this.handle(payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('*.updated') |   @OnDatabaseEvent('*', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdate( |   async handleUpdate( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
|     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); |     return this.handle(payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('*.deleted') |   @OnDatabaseEvent('*', DatabaseEventAction.DELETED) | ||||||
|   async handleDelete( |   async handleDelete( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
|     return this.handle(payload); |     return this.handle(payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('*.destroyed') |   @OnDatabaseEvent('*', DatabaseEventAction.DESTROYED) | ||||||
|   async handleDestroy( |   async handleDestroy( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
| @@ -61,18 +55,22 @@ export class EntityEventsToDbListener { | |||||||
|       (event) => event.objectMetadata?.isAuditLogged, |       (event) => event.objectMetadata?.isAuditLogged, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     await this.messageQueueService.add< |     await this.entityEventsToDbQueueService.add< | ||||||
|       WorkspaceEventBatch<ObjectRecordBaseEvent> |       WorkspaceEventBatch<ObjectRecordBaseEvent> | ||||||
|     >(CreateAuditLogFromInternalEvent.name, { |     >(CreateAuditLogFromInternalEvent.name, { | ||||||
|       ...payload, |       ...payload, | ||||||
|       events: filteredEvents, |       events: filteredEvents, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     await this.messageQueueService.add< |     await this.entityEventsToDbQueueService.add< | ||||||
|       WorkspaceEventBatch<ObjectRecordBaseEvent> |       WorkspaceEventBatch<ObjectRecordBaseEvent> | ||||||
|     >(UpsertTimelineActivityFromInternalEvent.name, { |     >(UpsertTimelineActivityFromInternalEvent.name, { | ||||||
|       ...payload, |       ...payload, | ||||||
|       events: filteredEvents, |       events: filteredEvents, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     await this.webhookQueueService.add< | ||||||
|  |       WorkspaceEventBatch<ObjectRecordBaseEvent> | ||||||
|  |     >(CallWebhookJobsJob.name, payload, { retryLimit: 3 }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; | ||||||
| import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service'; | import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service'; | ||||||
| import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; | 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() | @Injectable() | ||||||
| export class TelemetryListener { | export class TelemetryListener { | ||||||
| @@ -13,7 +15,7 @@ export class TelemetryListener { | |||||||
|     private readonly telemetryService: TelemetryService, |     private readonly telemetryService: TelemetryService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('*.created') |   @OnDatabaseEvent('*', DatabaseEventAction.CREATED) | ||||||
|   async handleAllCreate( |   async handleAllCreate( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordCreateEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordCreateEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
| import { OnEvent } from '@nestjs/event-emitter'; |  | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   UpdateSubscriptionJob, |   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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; | ||||||
| import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; | 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 { 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() | @Injectable() | ||||||
| export class BillingWorkspaceMemberListener { | export class BillingWorkspaceMemberListener { | ||||||
| @@ -21,8 +22,8 @@ export class BillingWorkspaceMemberListener { | |||||||
|     private readonly environmentService: EnvironmentService, |     private readonly environmentService: EnvironmentService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('workspaceMember.created') |   @OnDatabaseEvent('workspaceMember', DatabaseEventAction.CREATED) | ||||||
|   @OnEvent('workspaceMember.deleted') |   @OnDatabaseEvent('workspaceMember', DatabaseEventAction.DELETED) | ||||||
|   async handleCreateOrDeleteEvent( |   async handleCreateOrDeleteEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity> |       ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| import { ObjectRecordBaseEvent } 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'; | ||||||
|  |  | ||||||
|  | type Diff<T> = { | ||||||
|  |   [K in keyof T]: { before: T[K]; after: T[K] }; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent { | export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent { | ||||||
|   properties: { |   properties: { | ||||||
|     updatedFields?: string[]; |     updatedFields?: string[]; | ||||||
|     before: T; |     before: T; | ||||||
|     after: T; |     after: T; | ||||||
|     diff?: Partial<T>; |     diff?: Partial<Diff<T>>; | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,8 +7,3 @@ export class ObjectRecordBaseEvent { | |||||||
|   objectMetadata: ObjectMetadataInterface; |   objectMetadata: ObjectMetadataInterface; | ||||||
|   properties: any; |   properties: any; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class ObjectRecordBaseEventWithNameAndWorkspaceId extends ObjectRecordBaseEvent { |  | ||||||
|   name: string; |  | ||||||
|   workspaceId: string; |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -45,7 +45,6 @@ describe('objectRecordChangedValues', () => { | |||||||
|       name: { before: 'Original Name', after: 'Updated Name' }, |       name: { before: 'Original Name', after: 'Updated Name' }, | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); |  | ||||||
|  |  | ||||||
|   it('ignores changes to the updatedAt field', () => { |   it('ignores changes to the updatedAt field', () => { | ||||||
|     const oldRecord = { |     const oldRecord = { | ||||||
| @@ -118,3 +117,4 @@ it('correctly handles a mix of changed, unchanged, and special case values', () | |||||||
|  |  | ||||||
|     expect(result).toEqual(expectedChanges); |     expect(result).toEqual(expectedChanges); | ||||||
|   }); |   }); | ||||||
|  | }); | ||||||
|   | |||||||
| @@ -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 = <Entity>( |  | ||||||
|   objectMetadataEntity: ObjectMetadataEntity, |  | ||||||
|   action: 'created' | 'updated' | 'deleted' | 'destroyed', |  | ||||||
| ): |  | ||||||
|   | ObjectRecordCreateEvent<Entity> |  | ||||||
|   | ObjectRecordUpdateEvent<Entity> |  | ||||||
|   | ObjectRecordDeleteEvent<Entity> |  | ||||||
|   | ObjectRecordDestroyEvent<Entity> => { |  | ||||||
|   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<Entity>; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   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<Entity>; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (action === 'deleted') { |  | ||||||
|     return { |  | ||||||
|       recordId, |  | ||||||
|       userId, |  | ||||||
|       workspaceMemberId, |  | ||||||
|       objectMetadata: objectMetadataEntity, |  | ||||||
|       properties: { |  | ||||||
|         before, |  | ||||||
|       }, |  | ||||||
|     } satisfies ObjectRecordDeleteEvent<Entity>; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (action === 'destroyed') { |  | ||||||
|     return { |  | ||||||
|       recordId, |  | ||||||
|       userId, |  | ||||||
|       workspaceMemberId, |  | ||||||
|       objectMetadata: objectMetadataEntity, |  | ||||||
|       properties: { |  | ||||||
|         before, |  | ||||||
|       }, |  | ||||||
|     } satisfies ObjectRecordDestroyEvent<Entity>; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   throw new Error(`Unknown action '${action}'`); |  | ||||||
| }; |  | ||||||
| @@ -15,7 +15,7 @@ export const objectRecordChangedValues = ( | |||||||
|     objectMetadata.fields.map((field) => [field.name, field]), |     objectMetadata.fields.map((field) => [field.name, field]), | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const changedValues = Object.keys(newRecord).reduce( |   return Object.keys(newRecord).reduce( | ||||||
|     (acc, key) => { |     (acc, key) => { | ||||||
|       const field = fieldsByKey.get(key); |       const field = fieldsByKey.get(key); | ||||||
|       const oldRecordValue = oldRecord[key]; |       const oldRecordValue = oldRecord[key]; | ||||||
| @@ -36,6 +36,4 @@ export const objectRecordChangedValues = ( | |||||||
|     }, |     }, | ||||||
|     {} as Record<string, { before: any; after: any }>, |     {} as Record<string, { before: any; after: any }>, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   return changedValues; |  | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ import { MessagingModule } from 'src/modules/messaging/messaging.module'; | |||||||
| import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module'; | import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module'; | ||||||
| import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module'; | import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module'; | ||||||
| import { WorkflowModule } from 'src/modules/workflow/workflow.module'; | import { WorkflowModule } from 'src/modules/workflow/workflow.module'; | ||||||
|  | import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module'; | ||||||
|  |  | ||||||
| @Module({ | @Module({ | ||||||
|   imports: [ |   imports: [ | ||||||
| @@ -49,6 +50,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module'; | |||||||
|     WorkspaceQueryRunnerJobModule, |     WorkspaceQueryRunnerJobModule, | ||||||
|     AutoCompaniesAndContactsCreationJobModule, |     AutoCompaniesAndContactsCreationJobModule, | ||||||
|     TimelineJobModule, |     TimelineJobModule, | ||||||
|  |     WebhookJobModule, | ||||||
|     WorkflowModule, |     WorkflowModule, | ||||||
|   ], |   ], | ||||||
|   providers: [ |   providers: [ | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ import { | |||||||
| import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; | import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; | ||||||
| import { capitalize } from 'src/utils/capitalize'; | import { capitalize } from 'src/utils/capitalize'; | ||||||
| import { getServerUrl } from 'src/utils/get-server-url'; | import { getServerUrl } from 'src/utils/get-server-url'; | ||||||
|  | import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class OpenApiService { | export class OpenApiService { | ||||||
| @@ -81,9 +82,18 @@ export class OpenApiService { | |||||||
|  |  | ||||||
|     schema.webhooks = objectMetadataItems.reduce( |     schema.webhooks = objectMetadataItems.reduce( | ||||||
|       (paths, item) => { |       (paths, item) => { | ||||||
|         paths[`Create ${item.nameSingular}`] = computeWebhooks('create', item); |         paths[`Create ${item.nameSingular}`] = computeWebhooks( | ||||||
|         paths[`Update ${item.nameSingular}`] = computeWebhooks('update', item); |           DatabaseEventAction.CREATED, | ||||||
|         paths[`Delete ${item.nameSingular}`] = computeWebhooks('delete', item); |           item, | ||||||
|  |         ); | ||||||
|  |         paths[`Update ${item.nameSingular}`] = computeWebhooks( | ||||||
|  |           DatabaseEventAction.UPDATED, | ||||||
|  |           item, | ||||||
|  |         ); | ||||||
|  |         paths[`Delete ${item.nameSingular}`] = computeWebhooks( | ||||||
|  |           DatabaseEventAction.DELETED, | ||||||
|  |           item, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         return paths; |         return paths; | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -2,17 +2,24 @@ import { OpenAPIV3_1 } from 'openapi-types'; | |||||||
|  |  | ||||||
| import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | ||||||
| import { capitalize } from 'src/utils/capitalize'; | import { capitalize } from 'src/utils/capitalize'; | ||||||
|  | import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; | ||||||
|  |  | ||||||
| export const computeWebhooks = ( | export const computeWebhooks = ( | ||||||
|   type: 'create' | 'update' | 'delete', |   type: DatabaseEventAction, | ||||||
|   item: ObjectMetadataEntity, |   item: ObjectMetadataEntity, | ||||||
| ): OpenAPIV3_1.PathItemObject => { | ): OpenAPIV3_1.PathItemObject => { | ||||||
|  |   const updatedFields = { | ||||||
|  |     type: 'array', | ||||||
|  |     items: { | ||||||
|  |       type: 'string', | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     post: { |     post: { | ||||||
|       tags: [item.nameSingular], |       tags: [item.nameSingular], | ||||||
|       security: [], |       security: [], | ||||||
|       requestBody: { |       requestBody: { | ||||||
|         description: `*${type}*.**${item.nameSingular}**, ***.**${item.nameSingular}**, ***.*****`, |  | ||||||
|         content: { |         content: { | ||||||
|           'application/json': { |           'application/json': { | ||||||
|             schema: { |             schema: { | ||||||
| @@ -22,17 +29,9 @@ export const computeWebhooks = ( | |||||||
|                   type: 'string', |                   type: 'string', | ||||||
|                   example: 'https://example.com/incomingWebhook', |                   example: 'https://example.com/incomingWebhook', | ||||||
|                 }, |                 }, | ||||||
|                 description: { |                 eventName: { | ||||||
|                   type: 'string', |                   type: 'string', | ||||||
|                   example: 'A sample description', |                   example: `${item.nameSingular}.${type}`, | ||||||
|                 }, |  | ||||||
|                 eventType: { |  | ||||||
|                   type: 'string', |  | ||||||
|                   enum: [ |  | ||||||
|                     '*.*', |  | ||||||
|                     '*.' + item.nameSingular, |  | ||||||
|                     type + '.' + item.nameSingular, |  | ||||||
|                   ], |  | ||||||
|                 }, |                 }, | ||||||
|                 objectMetadata: { |                 objectMetadata: { | ||||||
|                   type: 'object', |                   type: 'object', | ||||||
| @@ -60,8 +59,9 @@ export const computeWebhooks = ( | |||||||
|                   example: '2024-02-14T11:27:01.779Z', |                   example: '2024-02-14T11:27:01.779Z', | ||||||
|                 }, |                 }, | ||||||
|                 record: { |                 record: { | ||||||
|                   $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, |                   $ref: `#/components/schemas/${capitalize(item.nameSingular)} for Response`, | ||||||
|                 }, |                 }, | ||||||
|  |                 ...(type === DatabaseEventAction.UPDATED && { updatedFields }), | ||||||
|               }, |               }, | ||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|   | |||||||
| @@ -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 { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; | ||||||
| import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; | import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; | ||||||
| import { assert } from 'src/utils/assert'; | 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<UserWorkspace> { | export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> { | ||||||
|   constructor( |   constructor( | ||||||
| @@ -88,7 +89,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> { | |||||||
|     payload.recordId = workspaceMember[0].id; |     payload.recordId = workspaceMember[0].id; | ||||||
|  |  | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       'workspaceMember.created', |       `workspaceMember.${DatabaseEventAction.CREATED}`, | ||||||
|       [payload], |       [payload], | ||||||
|       workspaceId, |       workspaceId, | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||||
| import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; | 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 { 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 | // eslint-disable-next-line @nx/workspace-inject-workspace-repository | ||||||
| export class UserService extends TypeOrmQueryService<User> { | export class UserService extends TypeOrmQueryService<User> { | ||||||
| @@ -115,7 +116,7 @@ export class UserService extends TypeOrmQueryService<User> { | |||||||
|     payload.recordId = workspaceMember.id; |     payload.recordId = workspaceMember.id; | ||||||
|  |  | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       'workspaceMember.deleted', |       `workspaceMember.${DatabaseEventAction.DELETED}`, | ||||||
|       [payload], |       [payload], | ||||||
|       workspaceId, |       workspaceId, | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
| import { OnEvent } from '@nestjs/event-emitter'; |  | ||||||
|  |  | ||||||
| import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; | import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; | ||||||
| import { | 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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; | ||||||
| import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; | 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 { 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() | @Injectable() | ||||||
| export class WorkspaceWorkspaceMemberListener { | export class WorkspaceWorkspaceMemberListener { | ||||||
| @@ -22,7 +23,7 @@ export class WorkspaceWorkspaceMemberListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('workspaceMember.updated') |   @OnDatabaseEvent('workspaceMember', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdateEvent( |   async handleUpdateEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<WorkspaceMemberWorkspaceEntity> |       ObjectRecordUpdateEvent<WorkspaceMemberWorkspaceEntity> | ||||||
| @@ -50,7 +51,7 @@ export class WorkspaceWorkspaceMemberListener { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('workspaceMember.deleted') |   @OnDatabaseEvent('workspaceMember', DatabaseEventAction.DELETED) | ||||||
|   async handleDeleteEvent( |   async handleDeleteEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordDeleteEvent<WorkspaceMemberWorkspaceEntity> |       ObjectRecordDeleteEvent<WorkspaceMemberWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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'; | import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; | ||||||
| @@ -17,6 +16,8 @@ import { | |||||||
|   BlocklistReimportCalendarEventsJob, |   BlocklistReimportCalendarEventsJob, | ||||||
|   BlocklistReimportCalendarEventsJobData, |   BlocklistReimportCalendarEventsJobData, | ||||||
| } from 'src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job'; | } 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() | @Injectable() | ||||||
| export class CalendarBlocklistListener { | export class CalendarBlocklistListener { | ||||||
| @@ -25,7 +26,7 @@ export class CalendarBlocklistListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('blocklist.created') |   @OnDatabaseEvent('blocklist', DatabaseEventAction.CREATED) | ||||||
|   async handleCreatedEvent( |   async handleCreatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordCreateEvent<BlocklistWorkspaceEntity> |       ObjectRecordCreateEvent<BlocklistWorkspaceEntity> | ||||||
| @@ -37,7 +38,7 @@ export class CalendarBlocklistListener { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('blocklist.deleted') |   @OnDatabaseEvent('blocklist', DatabaseEventAction.DELETED) | ||||||
|   async handleDeletedEvent( |   async handleDeletedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordDeleteEvent<BlocklistWorkspaceEntity> |       ObjectRecordDeleteEvent<BlocklistWorkspaceEntity> | ||||||
| @@ -49,7 +50,7 @@ export class CalendarBlocklistListener { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('blocklist.updated') |   @OnDatabaseEvent('blocklist', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdatedEvent( |   async handleUpdatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<BlocklistWorkspaceEntity> |       ObjectRecordUpdateEvent<BlocklistWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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'; | import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; | ||||||
| @@ -11,6 +10,8 @@ import { | |||||||
|   DeleteConnectedAccountAssociatedCalendarDataJobData, |   DeleteConnectedAccountAssociatedCalendarDataJobData, | ||||||
| } from 'src/modules/calendar/calendar-event-cleaner/jobs/delete-connected-account-associated-calendar-data.job'; | } 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 { 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() | @Injectable() | ||||||
| export class CalendarEventCleanerConnectedAccountListener { | export class CalendarEventCleanerConnectedAccountListener { | ||||||
| @@ -19,7 +20,7 @@ export class CalendarEventCleanerConnectedAccountListener { | |||||||
|     private readonly calendarQueueService: MessageQueueService, |     private readonly calendarQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('connectedAccount.destroyed') |   @OnDatabaseEvent('connectedAccount', DatabaseEventAction.DESTROYED) | ||||||
|   async handleDestroyedEvent( |   async handleDestroyedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> |       ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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 { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; | ||||||
| @@ -17,6 +16,8 @@ import { | |||||||
|   CalendarEventParticipantUnmatchParticipantJobData, |   CalendarEventParticipantUnmatchParticipantJobData, | ||||||
| } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job'; | } 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 { 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() | @Injectable() | ||||||
| export class CalendarEventParticipantPersonListener { | export class CalendarEventParticipantPersonListener { | ||||||
| @@ -25,7 +26,7 @@ export class CalendarEventParticipantPersonListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('person.created') |   @OnDatabaseEvent('person', DatabaseEventAction.CREATED) | ||||||
|   async handleCreatedEvent( |   async handleCreatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordCreateEvent<PersonWorkspaceEntity> |       ObjectRecordCreateEvent<PersonWorkspaceEntity> | ||||||
| @@ -48,7 +49,7 @@ export class CalendarEventParticipantPersonListener { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('person.updated') |   @OnDatabaseEvent('person', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdatedEvent( |   async handleUpdatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<PersonWorkspaceEntity> |       ObjectRecordUpdateEvent<PersonWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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 { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; | ||||||
| @@ -17,6 +16,8 @@ import { | |||||||
|   CalendarEventParticipantUnmatchParticipantJobData, |   CalendarEventParticipantUnmatchParticipantJobData, | ||||||
| } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job'; | } 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 { 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() | @Injectable() | ||||||
| export class CalendarEventParticipantWorkspaceMemberListener { | export class CalendarEventParticipantWorkspaceMemberListener { | ||||||
| @@ -25,7 +26,7 @@ export class CalendarEventParticipantWorkspaceMemberListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('workspaceMember.created') |   @OnDatabaseEvent('workspaceMember', DatabaseEventAction.CREATED) | ||||||
|   async handleCreatedEvent( |   async handleCreatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity> |       ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity> | ||||||
| @@ -47,7 +48,7 @@ export class CalendarEventParticipantWorkspaceMemberListener { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('workspaceMember.updated') |   @OnDatabaseEvent('workspaceMember', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdatedEvent( |   async handleUpdatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<WorkspaceMemberWorkspaceEntity> |       ObjectRecordUpdateEvent<WorkspaceMemberWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; | ||||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | 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 { 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 { 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 { 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() | @Injectable() | ||||||
| export class ConnectedAccountListener { | export class ConnectedAccountListener { | ||||||
| @@ -15,7 +16,7 @@ export class ConnectedAccountListener { | |||||||
|     private readonly accountsToReconnectService: AccountsToReconnectService, |     private readonly accountsToReconnectService: AccountsToReconnectService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('connectedAccount.destroyed') |   @OnDatabaseEvent('connectedAccount', DatabaseEventAction.DESTROYED) | ||||||
|   async handleDestroyedEvent( |   async handleDestroyedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> |       ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> | ||||||
|   | |||||||
| @@ -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 { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; | ||||||
| import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; | 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 { 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`) | @WorkspaceQueryHook(`connectedAccount.destroyOne`) | ||||||
| export class ConnectedAccountDeleteOnePreQueryHook | export class ConnectedAccountDeleteOnePreQueryHook | ||||||
| @@ -34,7 +35,7 @@ export class ConnectedAccountDeleteOnePreQueryHook | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       'messageChannel.destroyed', |       `messageChannel.${DatabaseEventAction.DESTROYED}`, | ||||||
|       messageChannels.map( |       messageChannels.map( | ||||||
|         (messageChannel) => |         (messageChannel) => | ||||||
|           ({ |           ({ | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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'; | import { objectRecordChangedProperties } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util'; | ||||||
| @@ -12,6 +11,8 @@ import { | |||||||
|   CalendarCreateCompanyAndContactAfterSyncJobData, |   CalendarCreateCompanyAndContactAfterSyncJobData, | ||||||
| } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job'; | } 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 { 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() | @Injectable() | ||||||
| export class AutoCompaniesAndContactsCreationCalendarChannelListener { | export class AutoCompaniesAndContactsCreationCalendarChannelListener { | ||||||
| @@ -20,7 +21,7 @@ export class AutoCompaniesAndContactsCreationCalendarChannelListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('calendarChannel.updated') |   @OnDatabaseEvent('calendarChannel', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdatedEvent( |   async handleUpdatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<MessageChannelWorkspaceEntity> |       ObjectRecordUpdateEvent<MessageChannelWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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'; | import { objectRecordChangedProperties } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util'; | ||||||
| @@ -12,6 +11,8 @@ import { | |||||||
|   MessagingCreateCompanyAndContactAfterSyncJob, |   MessagingCreateCompanyAndContactAfterSyncJob, | ||||||
|   MessagingCreateCompanyAndContactAfterSyncJobData, |   MessagingCreateCompanyAndContactAfterSyncJobData, | ||||||
| } from 'src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job'; | } 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() | @Injectable() | ||||||
| export class AutoCompaniesAndContactsCreationMessageChannelListener { | export class AutoCompaniesAndContactsCreationMessageChannelListener { | ||||||
| @@ -20,7 +21,7 @@ export class AutoCompaniesAndContactsCreationMessageChannelListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('messageChannel.updated') |   @OnDatabaseEvent('messageChannel', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdatedEvent( |   async handleUpdatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<MessageChannelWorkspaceEntity> |       ObjectRecordUpdateEvent<MessageChannelWorkspaceEntity> | ||||||
|   | |||||||
| @@ -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 { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; | ||||||
| import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; | import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; | ||||||
| import { isWorkEmail } from 'src/utils/is-work-email'; | import { isWorkEmail } from 'src/utils/is-work-email'; | ||||||
|  | import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class CreateCompanyAndContactService { | export class CreateCompanyAndContactService { | ||||||
| @@ -195,7 +196,7 @@ export class CreateCompanyAndContactService { | |||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       this.workspaceEventEmitter.emit( |       this.workspaceEventEmitter.emit( | ||||||
|         'person.created', |         `person.${DatabaseEventAction.CREATED}`, | ||||||
|         createdPeople.map( |         createdPeople.map( | ||||||
|           (createdPerson) => |           (createdPerson) => | ||||||
|             ({ |             ({ | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable, Scope } from '@nestjs/common'; | 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 { 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'; | import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; | ||||||
| @@ -17,6 +16,8 @@ import { | |||||||
|   BlocklistReimportMessagesJob, |   BlocklistReimportMessagesJob, | ||||||
|   BlocklistReimportMessagesJobData, |   BlocklistReimportMessagesJobData, | ||||||
| } from 'src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job'; | } 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 }) | @Injectable({ scope: Scope.REQUEST }) | ||||||
| export class MessagingBlocklistListener { | export class MessagingBlocklistListener { | ||||||
| @@ -25,7 +26,7 @@ export class MessagingBlocklistListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('blocklist.created') |   @OnDatabaseEvent('blocklist', DatabaseEventAction.CREATED) | ||||||
|   async handleCreatedEvent( |   async handleCreatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordCreateEvent<BlocklistWorkspaceEntity> |       ObjectRecordCreateEvent<BlocklistWorkspaceEntity> | ||||||
| @@ -37,7 +38,7 @@ export class MessagingBlocklistListener { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('blocklist.deleted') |   @OnDatabaseEvent('blocklist', DatabaseEventAction.CREATED) | ||||||
|   async handleDeletedEvent( |   async handleDeletedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordDeleteEvent<BlocklistWorkspaceEntity> |       ObjectRecordDeleteEvent<BlocklistWorkspaceEntity> | ||||||
| @@ -49,7 +50,7 @@ export class MessagingBlocklistListener { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('blocklist.updated') |   @OnDatabaseEvent('blocklist', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdatedEvent( |   async handleUpdatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<BlocklistWorkspaceEntity> |       ObjectRecordUpdateEvent<BlocklistWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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'; | import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; | ||||||
| @@ -11,6 +10,8 @@ import { | |||||||
|   MessagingConnectedAccountDeletionCleanupJob, |   MessagingConnectedAccountDeletionCleanupJob, | ||||||
|   MessagingConnectedAccountDeletionCleanupJobData, |   MessagingConnectedAccountDeletionCleanupJobData, | ||||||
| } from 'src/modules/messaging/message-cleaner/jobs/messaging-connected-account-deletion-cleanup.job'; | } 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() | @Injectable() | ||||||
| export class MessagingMessageCleanerConnectedAccountListener { | export class MessagingMessageCleanerConnectedAccountListener { | ||||||
| @@ -19,7 +20,7 @@ export class MessagingMessageCleanerConnectedAccountListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('connectedAccount.destroyed') |   @OnDatabaseEvent('connectedAccount', DatabaseEventAction.DESTROYED) | ||||||
|   async handleDestroyedEvent( |   async handleDestroyedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> |       ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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'; | import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; | ||||||
| @@ -11,6 +10,8 @@ import { | |||||||
|   MessagingCleanCacheJob, |   MessagingCleanCacheJob, | ||||||
|   MessagingCleanCacheJobData, |   MessagingCleanCacheJobData, | ||||||
| } from 'src/modules/messaging/message-import-manager/jobs/messaging-clean-cache'; | } 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() | @Injectable() | ||||||
| export class MessagingMessageImportManagerMessageChannelListener { | export class MessagingMessageImportManagerMessageChannelListener { | ||||||
| @@ -19,7 +20,7 @@ export class MessagingMessageImportManagerMessageChannelListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('messageChannel.destroyed') |   @OnDatabaseEvent('messageChannel', DatabaseEventAction.DESTROYED) | ||||||
|   async handleDestroyedEvent( |   async handleDestroyedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordDeleteEvent<MessageChannelWorkspaceEntity> |       ObjectRecordDeleteEvent<MessageChannelWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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 { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; | ||||||
| @@ -17,6 +16,8 @@ import { | |||||||
|   MessageParticipantUnmatchParticipantJobData, |   MessageParticipantUnmatchParticipantJobData, | ||||||
| } from 'src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job'; | } 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 { 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() | @Injectable() | ||||||
| export class MessageParticipantPersonListener { | export class MessageParticipantPersonListener { | ||||||
| @@ -25,7 +26,7 @@ export class MessageParticipantPersonListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('person.created') |   @OnDatabaseEvent('person', DatabaseEventAction.CREATED) | ||||||
|   async handleCreatedEvent( |   async handleCreatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordCreateEvent<PersonWorkspaceEntity> |       ObjectRecordCreateEvent<PersonWorkspaceEntity> | ||||||
| @@ -47,7 +48,7 @@ export class MessageParticipantPersonListener { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('person.updated') |   @OnDatabaseEvent('person', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdatedEvent( |   async handleUpdatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<PersonWorkspaceEntity> |       ObjectRecordUpdateEvent<PersonWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
| import { OnEvent } from '@nestjs/event-emitter'; |  | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
|  |  | ||||||
| import { Repository } from 'typeorm'; | import { Repository } from 'typeorm'; | ||||||
| @@ -24,6 +23,8 @@ import { | |||||||
|   MessageParticipantUnmatchParticipantJobData, |   MessageParticipantUnmatchParticipantJobData, | ||||||
| } from 'src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job'; | } 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 { 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() | @Injectable() | ||||||
| export class MessageParticipantWorkspaceMemberListener { | export class MessageParticipantWorkspaceMemberListener { | ||||||
| @@ -34,7 +35,7 @@ export class MessageParticipantWorkspaceMemberListener { | |||||||
|     private readonly workspaceRepository: Repository<Workspace>, |     private readonly workspaceRepository: Repository<Workspace>, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('workspaceMember.created') |   @OnDatabaseEvent('workspaceMember', DatabaseEventAction.CREATED) | ||||||
|   async handleCreatedEvent( |   async handleCreatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity> |       ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity> | ||||||
| @@ -67,7 +68,7 @@ export class MessageParticipantWorkspaceMemberListener { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('workspaceMember.updated') |   @OnDatabaseEvent('workspaceMember', DatabaseEventAction.UPDATED) | ||||||
|   async handleUpdatedEvent( |   async handleUpdatedEvent( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordUpdateEvent<WorkspaceMemberWorkspaceEntity> |       ObjectRecordUpdateEvent<WorkspaceMemberWorkspaceEntity> | ||||||
|   | |||||||
| @@ -20,15 +20,15 @@ export class CreateAuditLogFromInternalEvent { | |||||||
|  |  | ||||||
|   @Process(CreateAuditLogFromInternalEvent.name) |   @Process(CreateAuditLogFromInternalEvent.name) | ||||||
|   async handle( |   async handle( | ||||||
|     data: WorkspaceEventBatch<ObjectRecordBaseEvent>, |     workspaceEventBatch: WorkspaceEventBatch<ObjectRecordBaseEvent>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     for (const eventData of data.events) { |     for (const eventData of workspaceEventBatch.events) { | ||||||
|       let workspaceMemberId: string | null = null; |       let workspaceMemberId: string | null = null; | ||||||
|  |  | ||||||
|       if (eventData.userId) { |       if (eventData.userId) { | ||||||
|         const workspaceMember = await this.workspaceMemberService.getByIdOrFail( |         const workspaceMember = await this.workspaceMemberService.getByIdOrFail( | ||||||
|           eventData.userId, |           eventData.userId, | ||||||
|           data.workspaceId, |           workspaceEventBatch.workspaceId, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         workspaceMemberId = workspaceMember.id; |         workspaceMemberId = workspaceMember.id; | ||||||
| @@ -42,13 +42,13 @@ export class CreateAuditLogFromInternalEvent { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       await this.auditLogRepository.insert( |       await this.auditLogRepository.insert( | ||||||
|         data.name, |         workspaceEventBatch.name, | ||||||
|         eventData.properties, |         eventData.properties, | ||||||
|         workspaceMemberId, |         workspaceMemberId, | ||||||
|         data.name.split('.')[0], |         workspaceEventBatch.name.split('.')[0], | ||||||
|         eventData.objectMetadata.id, |         eventData.objectMetadata.id, | ||||||
|         eventData.recordId, |         eventData.recordId, | ||||||
|         data.workspaceId, |         workspaceEventBatch.workspaceId, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -18,13 +18,13 @@ export class UpsertTimelineActivityFromInternalEvent { | |||||||
|  |  | ||||||
|   @Process(UpsertTimelineActivityFromInternalEvent.name) |   @Process(UpsertTimelineActivityFromInternalEvent.name) | ||||||
|   async handle( |   async handle( | ||||||
|     data: WorkspaceEventBatch<ObjectRecordBaseEvent>, |     workspaceEventBatch: WorkspaceEventBatch<ObjectRecordBaseEvent>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     for (const eventData of data.events) { |     for (const eventData of workspaceEventBatch.events) { | ||||||
|       if (eventData.userId) { |       if (eventData.userId) { | ||||||
|         const workspaceMember = await this.workspaceMemberService.getByIdOrFail( |         const workspaceMember = await this.workspaceMemberService.getByIdOrFail( | ||||||
|           eventData.userId, |           eventData.userId, | ||||||
|           data.workspaceId, |           workspaceEventBatch.workspaceId, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         eventData.workspaceMemberId = workspaceMember.id; |         eventData.workspaceMemberId = workspaceMember.id; | ||||||
| @@ -48,9 +48,9 @@ export class UpsertTimelineActivityFromInternalEvent { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       await this.timelineActivityService.upsertEvent({ |       await this.timelineActivityService.upsertEvent({ | ||||||
|         ...eventData, |         event: eventData, | ||||||
|         workspaceId: data.workspaceId, |         eventName: workspaceEventBatch.name, | ||||||
|         name: data.name, |         workspaceId: workspaceEventBatch.workspaceId, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; | ||||||
| import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; | import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; | ||||||
| import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository'; | import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository'; | ||||||
| import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; | import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; | ||||||
|  |  | ||||||
| type TransformedEvent = ObjectRecordBaseEventWithNameAndWorkspaceId & { | type TransformedEvent = ObjectRecordBaseEvent & { | ||||||
|   objectName?: string; |   objectName?: string; | ||||||
|   linkedRecordCachedName?: string; |   linkedRecordCachedName?: string; | ||||||
|   linkedRecordId?: string; |   linkedRecordId?: string; | ||||||
| @@ -26,18 +26,26 @@ export class TimelineActivityService { | |||||||
|     task: 'taskTarget', |     task: 'taskTarget', | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   async upsertEvent(event: ObjectRecordBaseEventWithNameAndWorkspaceId) { |   async upsertEvent({ | ||||||
|     const events = await this.transformEvent(event); |     event, | ||||||
|  |     eventName, | ||||||
|  |     workspaceId, | ||||||
|  |   }: { | ||||||
|  |     event: ObjectRecordBaseEvent; | ||||||
|  |     eventName: string; | ||||||
|  |     workspaceId: string; | ||||||
|  |   }) { | ||||||
|  |     const events = await this.transformEvent({ event, workspaceId, eventName }); | ||||||
|  |  | ||||||
|     if (!events || events.length === 0) return; |     if (!events || events.length === 0) return; | ||||||
|  |  | ||||||
|     for (const event of events) { |     for (const event of events) { | ||||||
|       await this.timelineActivityRepository.upsertOne( |       await this.timelineActivityRepository.upsertOne( | ||||||
|         event.name, |         eventName, | ||||||
|         event.properties, |         event.properties, | ||||||
|         event.objectName ?? event.objectMetadata.nameSingular, |         event.objectName ?? event.objectMetadata.nameSingular, | ||||||
|         event.recordId, |         event.recordId, | ||||||
|         event.workspaceId, |         workspaceId, | ||||||
|         event.workspaceMemberId, |         event.workspaceMemberId, | ||||||
|         event.linkedRecordCachedName, |         event.linkedRecordCachedName, | ||||||
|         event.linkedRecordId, |         event.linkedRecordId, | ||||||
| @@ -46,11 +54,21 @@ export class TimelineActivityService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async transformEvent( |   private async transformEvent({ | ||||||
|     event: ObjectRecordBaseEventWithNameAndWorkspaceId, |     event, | ||||||
|   ): Promise<TransformedEvent[]> { |     workspaceId, | ||||||
|  |     eventName, | ||||||
|  |   }: { | ||||||
|  |     event: ObjectRecordBaseEvent; | ||||||
|  |     workspaceId: string; | ||||||
|  |     eventName: string; | ||||||
|  |   }): Promise<TransformedEvent[]> { | ||||||
|     if (['note', 'task'].includes(event.objectMetadata.nameSingular)) { |     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 |       // 2 timelines, one for the linked object and one for the task/note | ||||||
|       if (linkedObjects?.length > 0) return [...linkedObjects, event]; |       if (linkedObjects?.length > 0) return [...linkedObjects, event]; | ||||||
| @@ -61,56 +79,81 @@ export class TimelineActivityService { | |||||||
|         event.objectMetadata.nameSingular, |         event.objectMetadata.nameSingular, | ||||||
|       ) |       ) | ||||||
|     ) { |     ) { | ||||||
|       const linkedObjects = await this.handleLinkedObjects(event); |       return await this.handleLinkedObjects({ event, workspaceId, eventName }); | ||||||
|  |  | ||||||
|       return linkedObjects; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return [event]; |     return [event]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async handleLinkedObjects( |   private async handleLinkedObjects({ | ||||||
|     event: ObjectRecordBaseEventWithNameAndWorkspaceId, |     event, | ||||||
|   ) { |     workspaceId, | ||||||
|     const dataSourceSchema = this.workspaceDataSourceService.getSchemaName( |     eventName, | ||||||
|       event.workspaceId, |   }: { | ||||||
|     ); |     event: ObjectRecordBaseEvent; | ||||||
|  |     workspaceId: string; | ||||||
|  |     eventName: string; | ||||||
|  |   }) { | ||||||
|  |     const dataSourceSchema = | ||||||
|  |       this.workspaceDataSourceService.getSchemaName(workspaceId); | ||||||
|  |  | ||||||
|     switch (event.objectMetadata.nameSingular) { |     switch (event.objectMetadata.nameSingular) { | ||||||
|       case 'noteTarget': |       case 'noteTarget': | ||||||
|         return this.processActivityTarget(event, dataSourceSchema, 'note'); |         return this.processActivityTarget({ | ||||||
|       case 'taskTarget': |  | ||||||
|         return this.processActivityTarget(event, dataSourceSchema, 'task'); |  | ||||||
|       case 'note': |  | ||||||
|       case 'task': |  | ||||||
|         return this.processActivity( |  | ||||||
|           event, |           event, | ||||||
|           dataSourceSchema, |           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: |       default: | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async processActivity( |   private async processActivity({ | ||||||
|     event: ObjectRecordBaseEventWithNameAndWorkspaceId, |     event, | ||||||
|     dataSourceSchema: string, |     dataSourceSchema, | ||||||
|     activityType: string, |     activityType, | ||||||
|   ) { |     eventName, | ||||||
|  |     workspaceId, | ||||||
|  |   }: { | ||||||
|  |     event: ObjectRecordBaseEvent; | ||||||
|  |     dataSourceSchema: string; | ||||||
|  |     activityType: string; | ||||||
|  |     eventName: string; | ||||||
|  |     workspaceId: string; | ||||||
|  |   }) { | ||||||
|     const activityTargets = |     const activityTargets = | ||||||
|       await this.workspaceDataSourceService.executeRawQuery( |       await this.workspaceDataSourceService.executeRawQuery( | ||||||
|         `SELECT * FROM ${dataSourceSchema}."${this.targetObjects[activityType]}" |         `SELECT * FROM ${dataSourceSchema}."${this.targetObjects[activityType]}" | ||||||
|           WHERE "${activityType}Id" = $1`, |           WHERE "${activityType}Id" = $1`, | ||||||
|         [event.recordId], |         [event.recordId], | ||||||
|         event.workspaceId, |         workspaceId, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const activity = await this.workspaceDataSourceService.executeRawQuery( |     const activity = await this.workspaceDataSourceService.executeRawQuery( | ||||||
|       `SELECT * FROM ${dataSourceSchema}."${activityType}" |       `SELECT * FROM ${dataSourceSchema}."${activityType}" | ||||||
|           WHERE "id" = $1`, |           WHERE "id" = $1`, | ||||||
|       [event.recordId], |       [event.recordId], | ||||||
|       event.workspaceId, |       workspaceId, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (activityTargets.length === 0) return; |     if (activityTargets.length === 0) return; | ||||||
| @@ -135,7 +178,7 @@ export class TimelineActivityService { | |||||||
|  |  | ||||||
|         return { |         return { | ||||||
|           ...event, |           ...event, | ||||||
|           name: 'linked-' + event.name, |           name: 'linked-' + eventName, | ||||||
|           objectName: targetColumn[0].replace(/Id$/, ''), |           objectName: targetColumn[0].replace(/Id$/, ''), | ||||||
|           recordId: activityTarget[targetColumn[0]], |           recordId: activityTarget[targetColumn[0]], | ||||||
|           linkedRecordCachedName: activity[0].title, |           linkedRecordCachedName: activity[0].title, | ||||||
| @@ -146,17 +189,25 @@ export class TimelineActivityService { | |||||||
|       .filter((event): event is TransformedEvent => event !== undefined); |       .filter((event): event is TransformedEvent => event !== undefined); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async processActivityTarget( |   private async processActivityTarget({ | ||||||
|     event: ObjectRecordBaseEventWithNameAndWorkspaceId, |     event, | ||||||
|     dataSourceSchema: string, |     dataSourceSchema, | ||||||
|     activityType: string, |     activityType, | ||||||
|   ) { |     eventName, | ||||||
|  |     workspaceId, | ||||||
|  |   }: { | ||||||
|  |     event: ObjectRecordBaseEvent; | ||||||
|  |     dataSourceSchema: string; | ||||||
|  |     activityType: string; | ||||||
|  |     eventName: string; | ||||||
|  |     workspaceId: string; | ||||||
|  |   }) { | ||||||
|     const activityTarget = |     const activityTarget = | ||||||
|       await this.workspaceDataSourceService.executeRawQuery( |       await this.workspaceDataSourceService.executeRawQuery( | ||||||
|         `SELECT * FROM ${dataSourceSchema}."${this.targetObjects[activityType]}" |         `SELECT * FROM ${dataSourceSchema}."${this.targetObjects[activityType]}" | ||||||
|             WHERE "id" = $1`, |             WHERE "id" = $1`, | ||||||
|         [event.recordId], |         [event.recordId], | ||||||
|         event.workspaceId, |         workspaceId, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     if (activityTarget.length === 0) return; |     if (activityTarget.length === 0) return; | ||||||
| @@ -165,7 +216,7 @@ export class TimelineActivityService { | |||||||
|       `SELECT * FROM ${dataSourceSchema}."${activityType}" |       `SELECT * FROM ${dataSourceSchema}."${activityType}" | ||||||
|           WHERE "id" = $1`, |           WHERE "id" = $1`, | ||||||
|       [activityTarget[0].activityId], |       [activityTarget[0].activityId], | ||||||
|       event.workspaceId, |       workspaceId, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (activity.length === 0) return; |     if (activity.length === 0) return; | ||||||
| @@ -189,14 +240,14 @@ export class TimelineActivityService { | |||||||
|     return [ |     return [ | ||||||
|       { |       { | ||||||
|         ...event, |         ...event, | ||||||
|         name: 'linked-' + event.name, |         name: 'linked-' + eventName, | ||||||
|         properties: {}, |         properties: {}, | ||||||
|         objectName: targetColumn[0].replace(/Id$/, ''), |         objectName: targetColumn[0].replace(/Id$/, ''), | ||||||
|         recordId: activityTarget[0][targetColumn[0]], |         recordId: activityTarget[0][targetColumn[0]], | ||||||
|         linkedRecordCachedName: activity[0].title, |         linkedRecordCachedName: activity[0].title, | ||||||
|         linkedRecordId: activity[0].id, |         linkedRecordId: activity[0].id, | ||||||
|         linkedObjectMetadataId: activityObjectMetadataId, |         linkedObjectMetadataId: activityObjectMetadataId, | ||||||
|       }, |       } as TransformedEvent, | ||||||
|     ] as TransformedEvent[]; |     ]; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,12 +2,6 @@ import { Logger } from '@nestjs/common'; | |||||||
| 
 | 
 | ||||||
| import { ArrayContains } from 'typeorm'; | 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 { 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 { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; | ||||||
| import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; | ||||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||||
| import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity'; | import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity'; | ||||||
| 
 | import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; | ||||||
| export enum CallWebhookJobsJobOperation { | import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; | ||||||
|   create = 'create', | import { | ||||||
|   update = 'update', |   CallWebhookJob, | ||||||
|   delete = 'delete', |   CallWebhookJobData, | ||||||
|   destroy = 'destroy', | } from 'src/modules/webhook/jobs/call-webhook.job'; | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export type CallWebhookJobsJobData = { |  | ||||||
|   workspaceId: string; |  | ||||||
|   objectMetadataItem: ObjectMetadataInterface; |  | ||||||
|   record: any; |  | ||||||
|   operation: CallWebhookJobsJobOperation; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| @Processor(MessageQueue.webhookQueue) | @Processor(MessageQueue.webhookQueue) | ||||||
| export class CallWebhookJobsJob { | export class CallWebhookJobsJob { | ||||||
| @@ -41,51 +27,64 @@ export class CallWebhookJobsJob { | |||||||
|   ) {} |   ) {} | ||||||
| 
 | 
 | ||||||
|   @Process(CallWebhookJobsJob.name) |   @Process(CallWebhookJobsJob.name) | ||||||
|   async handle(data: CallWebhookJobsJobData): Promise<void> { |   async handle( | ||||||
|  |     workspaceEventBatch: WorkspaceEventBatch<ObjectRecordBaseEvent>, | ||||||
|  |   ): Promise<void> { | ||||||
|     // If you change that function, double check it does not break Zapier
 |     // If you change that function, double check it does not break Zapier
 | ||||||
|     // trigger in packages/twenty-zapier/src/triggers/trigger_record.ts
 |     // 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 = |     const webhookRepository = | ||||||
|       await this.twentyORMGlobalManager.getRepositoryForWorkspace<WebhookWorkspaceEntity>( |       await this.twentyORMGlobalManager.getRepositoryForWorkspace<WebhookWorkspaceEntity>( | ||||||
|         data.workspaceId, |         workspaceEventBatch.workspaceId, | ||||||
|         'webhook', |         'webhook', | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|     const nameSingular = data.objectMetadataItem.nameSingular; |     const [nameSingular, operation] = workspaceEventBatch.name.split('.'); | ||||||
|     const operation = data.operation; |  | ||||||
|     const eventName = `${nameSingular}.${operation}`; |  | ||||||
| 
 | 
 | ||||||
|     const webhooks = await webhookRepository.find({ |     const webhooks = await webhookRepository.find({ | ||||||
|       where: [ |       where: [ | ||||||
|         { operations: ArrayContains([eventName]) }, |         { operations: ArrayContains([`${nameSingular}.${operation}`]) }, | ||||||
|         { operations: ArrayContains([`*.${operation}`]) }, |         { operations: ArrayContains([`*.${operation}`]) }, | ||||||
|         { operations: ArrayContains([`${nameSingular}.*`]) }, |         { operations: ArrayContains([`${nameSingular}.*`]) }, | ||||||
|         { operations: ArrayContains(['*.*']) }, |         { operations: ArrayContains(['*.*']) }, | ||||||
|       ], |       ], | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     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) => { |       webhooks.forEach((webhook) => { | ||||||
|       this.messageQueueService.add<CallWebhookJobData>( |         const webhookData = { | ||||||
|         CallWebhookJob.name, |  | ||||||
|         { |  | ||||||
|           targetUrl: webhook.targetUrl, |           targetUrl: webhook.targetUrl, | ||||||
|           eventName, |           eventName, | ||||||
|           objectMetadata: { |           objectMetadata, | ||||||
|             id: data.objectMetadataItem.id, |           workspaceId, | ||||||
|             nameSingular: data.objectMetadataItem.nameSingular, |  | ||||||
|           }, |  | ||||||
|           workspaceId: data.workspaceId, |  | ||||||
|           webhookId: webhook.id, |           webhookId: webhook.id, | ||||||
|           eventDate: new Date(), |           eventDate: new Date(), | ||||||
|           record: data.record, |           record, | ||||||
|         }, |           ...(updatedFields && { updatedFields }), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         this.messageQueueService.add<CallWebhookJobData>( | ||||||
|  |           CallWebhookJob.name, | ||||||
|  |           webhookData, | ||||||
|           { retryLimit: 3 }, |           { retryLimit: 3 }, | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       webhooks.length > 0 && |       webhooks.length > 0 && | ||||||
|         this.logger.log( |         this.logger.log( | ||||||
|         `CallWebhookJobsJob on eventName '${eventName}' triggered webhooks with ids [\n"${webhooks.map((webhook) => webhook.id).join('",\n"')}"\n]`, |           `CallWebhookJobsJob on eventName '${workspaceEventBatch.name}' triggered webhooks with ids [\n"${webhooks.map((webhook) => webhook.id).join('",\n"')}"\n]`, | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | } | ||||||
| @@ -14,6 +14,7 @@ export type CallWebhookJobData = { | |||||||
|   webhookId: string; |   webhookId: string; | ||||||
|   eventDate: Date; |   eventDate: Date; | ||||||
|   record: any; |   record: any; | ||||||
|  |   updatedFields?: string[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @Processor(MessageQueue.webhookQueue) | @Processor(MessageQueue.webhookQueue) | ||||||
| @@ -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 {} | ||||||
| @@ -16,6 +16,7 @@ import { | |||||||
|   WorkflowVersionWorkspaceEntity, |   WorkflowVersionWorkspaceEntity, | ||||||
| } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; | } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; | ||||||
| import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.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({ | @WorkspaceQueryHook({ | ||||||
|   key: `workflow.createMany`, |   key: `workflow.createMany`, | ||||||
| @@ -62,7 +63,7 @@ export class WorkflowCreateManyPostQueryHook | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       `workflowVersion.created`, |       `workflowVersion.${DatabaseEventAction.CREATED}`, | ||||||
|       workflowVersionsToCreate.map((workflowVersionToCreate) => { |       workflowVersionsToCreate.map((workflowVersionToCreate) => { | ||||||
|         return { |         return { | ||||||
|           userId: authContext.user?.id, |           userId: authContext.user?.id, | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import { | |||||||
|   WorkflowVersionWorkspaceEntity, |   WorkflowVersionWorkspaceEntity, | ||||||
| } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; | } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; | ||||||
| import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.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({ | @WorkspaceQueryHook({ | ||||||
|   key: `workflow.createOne`, |   key: `workflow.createOne`, | ||||||
| @@ -58,7 +59,7 @@ export class WorkflowCreateOnePostQueryHook | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       `workflowVersion.created`, |       `workflowVersion.${DatabaseEventAction.CREATED}`, | ||||||
|       [ |       [ | ||||||
|         { |         { | ||||||
|           userId: authContext.user?.id, |           userId: authContext.user?.id, | ||||||
|   | |||||||
| @@ -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 { 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 { 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 { 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 = <Entity>( | export const generateFakeObjectRecordEvent = <Entity>( | ||||||
|   objectMetadataEntity: ObjectMetadataEntity, |   objectMetadataEntity: ObjectMetadataEntity, | ||||||
|   action: 'created' | 'updated' | 'deleted' | 'destroyed', |   action: DatabaseEventAction, | ||||||
| ): | ): | ||||||
|   | ObjectRecordCreateEvent<Entity> |   | ObjectRecordCreateEvent<Entity> | ||||||
|   | ObjectRecordUpdateEvent<Entity> |   | ObjectRecordUpdateEvent<Entity> | ||||||
| @@ -21,7 +22,7 @@ export const generateFakeObjectRecordEvent = <Entity>( | |||||||
|  |  | ||||||
|   const after = generateFakeObjectRecord<Entity>(objectMetadataEntity); |   const after = generateFakeObjectRecord<Entity>(objectMetadataEntity); | ||||||
|  |  | ||||||
|   if (action === 'created') { |   if (action === DatabaseEventAction.CREATED) { | ||||||
|     return { |     return { | ||||||
|       recordId, |       recordId, | ||||||
|       userId, |       userId, | ||||||
| @@ -35,7 +36,7 @@ export const generateFakeObjectRecordEvent = <Entity>( | |||||||
|  |  | ||||||
|   const before = generateFakeObjectRecord<Entity>(objectMetadataEntity); |   const before = generateFakeObjectRecord<Entity>(objectMetadataEntity); | ||||||
|  |  | ||||||
|   if (action === 'updated') { |   if (action === DatabaseEventAction.UPDATED) { | ||||||
|     return { |     return { | ||||||
|       recordId, |       recordId, | ||||||
|       userId, |       userId, | ||||||
| @@ -44,12 +45,11 @@ export const generateFakeObjectRecordEvent = <Entity>( | |||||||
|       properties: { |       properties: { | ||||||
|         before, |         before, | ||||||
|         after, |         after, | ||||||
|         diff: after, |  | ||||||
|       }, |       }, | ||||||
|     } satisfies ObjectRecordUpdateEvent<Entity>; |     } satisfies ObjectRecordUpdateEvent<Entity>; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (action === 'deleted') { |   if (action === DatabaseEventAction.DELETED) { | ||||||
|     return { |     return { | ||||||
|       recordId, |       recordId, | ||||||
|       userId, |       userId, | ||||||
| @@ -61,7 +61,7 @@ export const generateFakeObjectRecordEvent = <Entity>( | |||||||
|     } satisfies ObjectRecordDeleteEvent<Entity>; |     } satisfies ObjectRecordDeleteEvent<Entity>; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (action === 'destroyed') { |   if (action === DatabaseEventAction.DESTROYED) { | ||||||
|     return { |     return { | ||||||
|       recordId, |       recordId, | ||||||
|       userId, |       userId, | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import { join } from 'path'; | |||||||
|  |  | ||||||
| import { Repository } from 'typeorm'; | 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 { 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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | ||||||
| import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; | import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; | ||||||
| @@ -21,6 +20,9 @@ import { | |||||||
|   WorkflowTriggerType, |   WorkflowTriggerType, | ||||||
| } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; | } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; | ||||||
| import { isDefined } from 'src/utils/is-defined'; | 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() | @Injectable() | ||||||
| export class WorkflowBuilderService { | export class WorkflowBuilderService { | ||||||
| @@ -92,7 +94,7 @@ export class WorkflowBuilderService { | |||||||
|   }) { |   }) { | ||||||
|     const [nameSingular, action] = eventName.split('.'); |     const [nameSingular, action] = eventName.split('.'); | ||||||
|  |  | ||||||
|     if (!['created', 'updated', 'deleted', 'destroyed'].includes(action)) { |     if (!checkStringIsDatabaseEventAction(action)) { | ||||||
|       return {}; |       return {}; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -110,7 +112,7 @@ export class WorkflowBuilderService { | |||||||
|  |  | ||||||
|     return generateFakeObjectRecordEvent( |     return generateFakeObjectRecordEvent( | ||||||
|       objectMetadata, |       objectMetadata, | ||||||
|       action as 'created' | 'updated' | 'deleted' | 'destroyed', |       action as DatabaseEventAction, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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'; | import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; | ||||||
| @@ -17,6 +16,8 @@ import { | |||||||
|   WorkflowVersionEventType, |   WorkflowVersionEventType, | ||||||
|   WorkflowVersionStatusUpdate, |   WorkflowVersionStatusUpdate, | ||||||
| } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job'; | } 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() | @Injectable() | ||||||
| export class WorkflowVersionStatusListener { | export class WorkflowVersionStatusListener { | ||||||
| @@ -25,7 +26,7 @@ export class WorkflowVersionStatusListener { | |||||||
|     private readonly messageQueueService: MessageQueueService, |     private readonly messageQueueService: MessageQueueService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('workflowVersion.created') |   @OnDatabaseEvent('workflowVersion', DatabaseEventAction.CREATED) | ||||||
|   async handleWorkflowVersionCreated( |   async handleWorkflowVersionCreated( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordCreateEvent<WorkflowVersionWorkspaceEntity> |       ObjectRecordCreateEvent<WorkflowVersionWorkspaceEntity> | ||||||
| @@ -53,7 +54,7 @@ export class WorkflowVersionStatusListener { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('workflowVersion.statusUpdated') |   @OnDatabaseEvent('workflowVersion', DatabaseEventAction.UPDATED) | ||||||
|   async handleWorkflowVersionUpdated( |   async handleWorkflowVersionUpdated( | ||||||
|     payload: WorkspaceEventBatch<WorkflowVersionStatusUpdate>, |     payload: WorkspaceEventBatch<WorkflowVersionStatusUpdate>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
| @@ -67,7 +68,7 @@ export class WorkflowVersionStatusListener { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('workflowVersion.deleted') |   @OnDatabaseEvent('workflowVersion', DatabaseEventAction.DELETED) | ||||||
|   async handleWorkflowVersionDeleted( |   async handleWorkflowVersionDeleted( | ||||||
|     payload: WorkspaceEventBatch< |     payload: WorkspaceEventBatch< | ||||||
|       ObjectRecordDeleteEvent<WorkflowVersionWorkspaceEntity> |       ObjectRecordDeleteEvent<WorkflowVersionWorkspaceEntity> | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { Injectable, Logger } from '@nestjs/common'; | 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 { 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'; | import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; | ||||||
| @@ -17,6 +16,8 @@ import { | |||||||
|   WorkflowEventTriggerJob, |   WorkflowEventTriggerJob, | ||||||
|   WorkflowEventTriggerJobData, |   WorkflowEventTriggerJobData, | ||||||
| } from 'src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job'; | } 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() | @Injectable() | ||||||
| export class DatabaseEventTriggerListener { | export class DatabaseEventTriggerListener { | ||||||
| @@ -29,28 +30,28 @@ export class DatabaseEventTriggerListener { | |||||||
|     private readonly isFeatureFlagEnabledService: FeatureFlagService, |     private readonly isFeatureFlagEnabledService: FeatureFlagService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @OnEvent('*.created') |   @OnDatabaseEvent('*', DatabaseEventAction.CREATED) | ||||||
|   async handleObjectRecordCreateEvent( |   async handleObjectRecordCreateEvent( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordCreateEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordCreateEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
|     await this.handleEvent(payload); |     await this.handleEvent(payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('*.updated') |   @OnDatabaseEvent('*', DatabaseEventAction.UPDATED) | ||||||
|   async handleObjectRecordUpdateEvent( |   async handleObjectRecordUpdateEvent( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
|     await this.handleEvent(payload); |     await this.handleEvent(payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('*.deleted') |   @OnDatabaseEvent('*', DatabaseEventAction.DELETED) | ||||||
|   async handleObjectRecordDeleteEvent( |   async handleObjectRecordDeleteEvent( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordDeleteEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordDeleteEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
|     await this.handleEvent(payload); |     await this.handleEvent(payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent('*.destroyed') |   @OnDatabaseEvent('*', DatabaseEventAction.DESTROYED) | ||||||
|   async handleObjectRecordDestroyEvent( |   async handleObjectRecordDestroyEvent( | ||||||
|     payload: WorkspaceEventBatch<ObjectRecordDestroyEvent<any>>, |     payload: WorkspaceEventBatch<ObjectRecordDestroyEvent<any>>, | ||||||
|   ) { |   ) { | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ import { | |||||||
| import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; | 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 { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util'; | ||||||
| import { assertNever } from 'src/utils/assert'; | import { assertNever } from 'src/utils/assert'; | ||||||
|  | import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class WorkflowTriggerWorkspaceService { | export class WorkflowTriggerWorkspaceService { | ||||||
| @@ -362,7 +363,7 @@ export class WorkflowTriggerWorkspaceService { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.workspaceEventEmitter.emit( |     this.workspaceEventEmitter.emit( | ||||||
|       'workflowVersion.statusUpdated', |       `workflowVersion.${DatabaseEventAction.UPDATED}`, | ||||||
|       [ |       [ | ||||||
|         { |         { | ||||||
|           workflowId, |           workflowId, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "twenty-zapier", |   "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!", |   "description": "Effortlessly sync Twenty with 3000+ apps. Automate tasks, boost productivity, and supercharge your customer relationships!", | ||||||
|   "main": "src/index.ts", |   "main": "src/index.ts", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import { computeInputFields } from '../utils/computeInputFields'; | |||||||
| import { InputData } from '../utils/data.types'; | import { InputData } from '../utils/data.types'; | ||||||
| import handleQueryParams from '../utils/handleQueryParams'; | import handleQueryParams from '../utils/handleQueryParams'; | ||||||
| import requestDb, { requestSchema } from '../utils/requestDb'; | import requestDb, { requestSchema } from '../utils/requestDb'; | ||||||
| import { Operation } from '../utils/triggers/triggers.utils'; | import { DatabaseEventAction } from '../utils/triggers/triggers.utils'; | ||||||
|  |  | ||||||
| export const recordInputFields = async ( | export const recordInputFields = async ( | ||||||
|   z: ZObject, |   z: ZObject, | ||||||
| @@ -24,7 +24,7 @@ export const recordInputFields = async ( | |||||||
| const computeFields = async (z: ZObject, bundle: Bundle) => { | const computeFields = async (z: ZObject, bundle: Bundle) => { | ||||||
|   const operation = bundle.inputData.crudZapierOperation; |   const operation = bundle.inputData.crudZapierOperation; | ||||||
|   switch (operation) { |   switch (operation) { | ||||||
|     case Operation.delete: |     case DatabaseEventAction.DELETED: | ||||||
|       return [ |       return [ | ||||||
|         { |         { | ||||||
|           key: 'id', |           key: 'id', | ||||||
| @@ -34,9 +34,9 @@ const computeFields = async (z: ZObject, bundle: Bundle) => { | |||||||
|           required: true, |           required: true, | ||||||
|         }, |         }, | ||||||
|       ]; |       ]; | ||||||
|     case Operation.update: |     case DatabaseEventAction.UPDATED: | ||||||
|       return recordInputFields(z, bundle, true); |       return recordInputFields(z, bundle, true); | ||||||
|     case Operation.create: |     case DatabaseEventAction.CREATED: | ||||||
|       return recordInputFields(z, bundle, false); |       return recordInputFields(z, bundle, false); | ||||||
|     default: |     default: | ||||||
|       return []; |       return []; | ||||||
| @@ -44,18 +44,18 @@ const computeFields = async (z: ZObject, bundle: Bundle) => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const computeQueryParameters = ( | const computeQueryParameters = ( | ||||||
|   operation: Operation, |   operation: DatabaseEventAction, | ||||||
|   data: InputData, |   data: InputData, | ||||||
| ): string => { | ): string => { | ||||||
|   switch (operation) { |   switch (operation) { | ||||||
|     case Operation.create: |     case DatabaseEventAction.CREATED: | ||||||
|       return `data:{${handleQueryParams(data)}}`; |       return `data:{${handleQueryParams(data)}}`; | ||||||
|     case Operation.update: |     case DatabaseEventAction.UPDATED: | ||||||
|       return ` |       return ` | ||||||
|       data:{${handleQueryParams(data)}}, |       data:{${handleQueryParams(data)}}, | ||||||
|       id: "${data.id}" |       id: "${data.id}" | ||||||
|       `; |       `; | ||||||
|     case Operation.delete: |     case DatabaseEventAction.DELETED: | ||||||
|       return ` |       return ` | ||||||
|       id: "${data.id}" |       id: "${data.id}" | ||||||
|       `; |       `; | ||||||
| @@ -104,9 +104,9 @@ export default { | |||||||
|         required: true, |         required: true, | ||||||
|         label: 'Operation', |         label: 'Operation', | ||||||
|         choices: { |         choices: { | ||||||
|           [Operation.create]: Operation.create, |           [DatabaseEventAction.CREATED]: DatabaseEventAction.CREATED, | ||||||
|           [Operation.update]: Operation.update, |           [DatabaseEventAction.UPDATED]: DatabaseEventAction.UPDATED, | ||||||
|           [Operation.delete]: Operation.delete, |           [DatabaseEventAction.DELETED]: DatabaseEventAction.DELETED, | ||||||
|         }, |         }, | ||||||
|         altersDynamicFields: true, |         altersDynamicFields: true, | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { crudRecordKey } from '../../creates/crud_record'; | |||||||
| import App from '../../index'; | import App from '../../index'; | ||||||
| import getBundle from '../../utils/getBundle'; | import getBundle from '../../utils/getBundle'; | ||||||
| import requestDb from '../../utils/requestDb'; | import requestDb from '../../utils/requestDb'; | ||||||
| import { Operation } from '../../utils/triggers/triggers.utils'; | import { DatabaseEventAction } from '../../utils/triggers/triggers.utils'; | ||||||
| const appTester = createAppTester(App); | const appTester = createAppTester(App); | ||||||
| tools.env.inject(); | tools.env.inject(); | ||||||
|  |  | ||||||
| @@ -12,7 +12,7 @@ describe('creates.create_company', () => { | |||||||
|   test('should run to create a Company Record', async () => { |   test('should run to create a Company Record', async () => { | ||||||
|     const bundle = getBundle({ |     const bundle = getBundle({ | ||||||
|       nameSingular: 'Company', |       nameSingular: 'Company', | ||||||
|       crudZapierOperation: Operation.create, |       crudZapierOperation: DatabaseEventAction.CREATED, | ||||||
|       name: 'Company Name', |       name: 'Company Name', | ||||||
|       address: { addressCity: 'Paris' }, |       address: { addressCity: 'Paris' }, | ||||||
|       linkedinLink: { |       linkedinLink: { | ||||||
| @@ -56,7 +56,7 @@ describe('creates.create_company', () => { | |||||||
|   test('should run to create a Person Record', async () => { |   test('should run to create a Person Record', async () => { | ||||||
|     const bundle = getBundle({ |     const bundle = getBundle({ | ||||||
|       nameSingular: 'Person', |       nameSingular: 'Person', | ||||||
|       crudZapierOperation: Operation.create, |       crudZapierOperation: DatabaseEventAction.CREATED, | ||||||
|       name: { firstName: 'John', lastName: 'Doe' }, |       name: { firstName: 'John', lastName: 'Doe' }, | ||||||
|       phones: { |       phones: { | ||||||
|         primaryPhoneNumber: '610203040', |         primaryPhoneNumber: '610203040', | ||||||
| @@ -90,7 +90,7 @@ describe('creates.update_company', () => { | |||||||
|   test('should run to update a Company record', async () => { |   test('should run to update a Company record', async () => { | ||||||
|     const createBundle = getBundle({ |     const createBundle = getBundle({ | ||||||
|       nameSingular: 'Company', |       nameSingular: 'Company', | ||||||
|       crudZapierOperation: Operation.create, |       crudZapierOperation: DatabaseEventAction.CREATED, | ||||||
|       name: 'Company Name', |       name: 'Company Name', | ||||||
|       employees: 25, |       employees: 25, | ||||||
|     }); |     }); | ||||||
| @@ -104,7 +104,7 @@ describe('creates.update_company', () => { | |||||||
|  |  | ||||||
|     const updateBundle = getBundle({ |     const updateBundle = getBundle({ | ||||||
|       nameSingular: 'Company', |       nameSingular: 'Company', | ||||||
|       crudZapierOperation: Operation.update, |       crudZapierOperation: DatabaseEventAction.UPDATED, | ||||||
|       id: companyId, |       id: companyId, | ||||||
|       name: 'Updated Company Name', |       name: 'Updated Company Name', | ||||||
|     }); |     }); | ||||||
| @@ -133,7 +133,7 @@ describe('creates.delete_company', () => { | |||||||
|   test('should run to delete a Company record', async () => { |   test('should run to delete a Company record', async () => { | ||||||
|     const createBundle = getBundle({ |     const createBundle = getBundle({ | ||||||
|       nameSingular: 'Company', |       nameSingular: 'Company', | ||||||
|       crudZapierOperation: Operation.create, |       crudZapierOperation: DatabaseEventAction.CREATED, | ||||||
|       name: 'Delete Company Name', |       name: 'Delete Company Name', | ||||||
|       employees: 25, |       employees: 25, | ||||||
|     }); |     }); | ||||||
| @@ -147,7 +147,7 @@ describe('creates.delete_company', () => { | |||||||
|  |  | ||||||
|     const deleteBundle = getBundle({ |     const deleteBundle = getBundle({ | ||||||
|       nameSingular: 'Company', |       nameSingular: 'Company', | ||||||
|       crudZapierOperation: Operation.delete, |       crudZapierOperation: DatabaseEventAction.DELETED, | ||||||
|       id: companyId, |       id: companyId, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,13 +4,14 @@ import App from '../../index'; | |||||||
| import { triggerRecordKey } from '../../triggers/trigger_record'; | import { triggerRecordKey } from '../../triggers/trigger_record'; | ||||||
| import getBundle from '../../utils/getBundle'; | import getBundle from '../../utils/getBundle'; | ||||||
| import requestDb from '../../utils/requestDb'; | import requestDb from '../../utils/requestDb'; | ||||||
|  | import { DatabaseEventAction } from '../../utils/triggers/triggers.utils'; | ||||||
| const appTester = createAppTester(App); | const appTester = createAppTester(App); | ||||||
|  |  | ||||||
| describe('triggers.trigger_record.created', () => { | describe('triggers.trigger_record.created', () => { | ||||||
|   test('should succeed to subscribe', async () => { |   test('should succeed to subscribe', async () => { | ||||||
|     const bundle = getBundle({}); |     const bundle = getBundle({}); | ||||||
|     bundle.inputData.nameSingular = 'company'; |     bundle.inputData.nameSingular = 'company'; | ||||||
|     bundle.inputData.operation = 'create'; |     bundle.inputData.operation = DatabaseEventAction.CREATED; | ||||||
|     bundle.targetUrl = 'https://test.com'; |     bundle.targetUrl = 'https://test.com'; | ||||||
|     const result = await appTester( |     const result = await appTester( | ||||||
|       App.triggers[triggerRecordKey].operation.performSubscribe, |       App.triggers[triggerRecordKey].operation.performSubscribe, | ||||||
|   | |||||||
| @@ -1,20 +1,21 @@ | |||||||
| import { Bundle, ZObject } from 'zapier-platform-core'; |  | ||||||
|  |  | ||||||
| import { findObjectNamesSingularKey } from '../triggers/find_object_names_singular'; | import { findObjectNamesSingularKey } from '../triggers/find_object_names_singular'; | ||||||
| import { | import { | ||||||
|   listSample, |   performSubscribe, | ||||||
|   Operation, |  | ||||||
|   perform, |  | ||||||
|   performUnsubscribe, |   performUnsubscribe, | ||||||
|   subscribe, |   perform, | ||||||
|  |   performList, | ||||||
|  |   DatabaseEventAction, | ||||||
| } from '../utils/triggers/triggers.utils'; | } from '../utils/triggers/triggers.utils'; | ||||||
|  |  | ||||||
| export const triggerRecordKey = 'trigger_record'; | export const triggerRecordKey = 'trigger_record'; | ||||||
|  |  | ||||||
| const performSubscribe = (z: ZObject, bundle: Bundle) => | const choices = Object.values(DatabaseEventAction).reduce( | ||||||
|   subscribe(z, bundle, bundle.inputData.operation); |   (acc, action) => { | ||||||
| const performList = (z: ZObject, bundle: Bundle) => |     acc[action] = action; | ||||||
|   listSample(z, bundle, bundle.inputData.operation === Operation.delete); |     return acc; | ||||||
|  |   }, | ||||||
|  |   {} as Record<DatabaseEventAction, DatabaseEventAction>, | ||||||
|  | ); | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   key: triggerRecordKey, |   key: triggerRecordKey, | ||||||
| @@ -37,12 +38,7 @@ export default { | |||||||
|         key: 'operation', |         key: 'operation', | ||||||
|         required: true, |         required: true, | ||||||
|         label: 'Operation', |         label: 'Operation', | ||||||
|         choices: { |         choices, | ||||||
|           [Operation.create]: Operation.create, |  | ||||||
|           [Operation.update]: Operation.update, |  | ||||||
|           [Operation.delete]: Operation.delete, |  | ||||||
|           [Operation.destroy]: Operation.destroy, |  | ||||||
|         }, |  | ||||||
|         altersDynamicFields: true, |         altersDynamicFields: true, | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   | |||||||
| @@ -79,7 +79,7 @@ export const requestDbViaRestApi = ( | |||||||
|   z: ZObject, |   z: ZObject, | ||||||
|   bundle: Bundle, |   bundle: Bundle, | ||||||
|   objectNamePlural: string, |   objectNamePlural: string, | ||||||
| ) => { | ): Promise<Record<string, any>[]> => { | ||||||
|   const options = { |   const options = { | ||||||
|     url: `${ |     url: `${ | ||||||
|       bundle.authData.apiUrl || process.env.SERVER_BASE_URL |       bundle.authData.apiUrl || process.env.SERVER_BASE_URL | ||||||
|   | |||||||
| @@ -7,22 +7,19 @@ import requestDb, { | |||||||
|   requestSchema, |   requestSchema, | ||||||
| } from '../../utils/requestDb'; | } from '../../utils/requestDb'; | ||||||
|  |  | ||||||
| export enum Operation { | export enum DatabaseEventAction { | ||||||
|   create = 'create', |   CREATED = 'created', | ||||||
|   update = 'update', |   UPDATED = 'updated', | ||||||
|   delete = 'delete', |   DELETED = 'deleted', | ||||||
|   destroy = 'destroy', |   DESTROYED = 'destroyed', | ||||||
| } | } | ||||||
|  |  | ||||||
| export const subscribe = async ( | export const performSubscribe = async (z: ZObject, bundle: Bundle) => { | ||||||
|   z: ZObject, |  | ||||||
|   bundle: Bundle, |  | ||||||
|   operation: Operation, |  | ||||||
| ) => { |  | ||||||
|   try { |  | ||||||
|   const data = { |   const data = { | ||||||
|     targetUrl: bundle.targetUrl, |     targetUrl: bundle.targetUrl, | ||||||
|       operations: [`${bundle.inputData.nameSingular}.${operation}`], |     operations: [ | ||||||
|  |       `${bundle.inputData.nameSingular}.${bundle.inputData.operation}`, | ||||||
|  |     ], | ||||||
|   }; |   }; | ||||||
|   const result = await requestDb( |   const result = await requestDb( | ||||||
|     z, |     z, | ||||||
| @@ -32,23 +29,6 @@ export const subscribe = async ( | |||||||
|     )}}) {id}}`, |     )}}) {id}}`, | ||||||
|   ); |   ); | ||||||
|   return result.data.createWebhook; |   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 performUnsubscribe = async (z: ZObject, bundle: Bundle) => { | 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) => { | export const perform = (z: ZObject, bundle: Bundle) => { | ||||||
|   const record = bundle.cleanedRequest.record; |   const data = { | ||||||
|   if (record.createdAt) { |     record: bundle.cleanedRequest.record, | ||||||
|     record.createdAt = record.createdAt + 'Z'; |     ...(bundle.cleanedRequest.updatedFields && { | ||||||
|  |       updatedFields: bundle.cleanedRequest.updatedFields, | ||||||
|  |     }), | ||||||
|  |   }; | ||||||
|  |   if (data.record.createdAt) { | ||||||
|  |     data.record.createdAt = data.record.createdAt + 'Z'; | ||||||
|   } |   } | ||||||
|   if (record.updatedAt) { |   if (data.record.updatedAt) { | ||||||
|     record.updatedAt = record.updatedAt + 'Z'; |     data.record.updatedAt = data.record.updatedAt + 'Z'; | ||||||
|   } |   } | ||||||
|   if (record.revokedAt) { |   if (data.record.revokedAt) { | ||||||
|     record.revokedAt = record.revokedAt + 'Z'; |     data.record.revokedAt = data.record.revokedAt + 'Z'; | ||||||
|   } |   } | ||||||
|   if (record.expiresAt) { |   if (data.record.expiresAt) { | ||||||
|     record.expiresAt = record.expiresAt + 'Z'; |     data.record.expiresAt = data.record.expiresAt + 'Z'; | ||||||
|   } |   } | ||||||
|   return [record]; |  | ||||||
|  |   return [data]; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getNamePluralFromNameSingular = async ( | const getNamePluralFromNameSingular = async ( | ||||||
| @@ -92,10 +78,9 @@ const getNamePluralFromNameSingular = async ( | |||||||
|   throw new Error(`Unknown Object Name Singular ${nameSingular}`); |   throw new Error(`Unknown Object Name Singular ${nameSingular}`); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const listSample = async ( | export const performList = async ( | ||||||
|   z: ZObject, |   z: ZObject, | ||||||
|   bundle: Bundle, |   bundle: Bundle, | ||||||
|   onlyIds = false, |  | ||||||
| ): Promise<ObjectData[]> => { | ): Promise<ObjectData[]> => { | ||||||
|   const nameSingular = bundle.inputData.nameSingular; |   const nameSingular = bundle.inputData.nameSingular; | ||||||
|   const namePlural = await getNamePluralFromNameSingular( |   const namePlural = await getNamePluralFromNameSingular( | ||||||
| @@ -103,19 +88,13 @@ export const listSample = async ( | |||||||
|     bundle, |     bundle, | ||||||
|     nameSingular, |     nameSingular, | ||||||
|   ); |   ); | ||||||
|   const result: { [key: string]: string }[] = await requestDbViaRestApi( |   const results = await requestDbViaRestApi(z, bundle, namePlural); | ||||||
|     z, |   return results.map((result) => ({ | ||||||
|     bundle, |     record: result, | ||||||
|     namePlural, |     ...(bundle.inputData.operation === DatabaseEventAction.UPDATED && { | ||||||
|   ); |       updatedFields: Object.keys(result).filter((key) => key !== 'id')?.[0] || [ | ||||||
|  |         'updatedField', | ||||||
|   if (onlyIds) { |       ], | ||||||
|     return result.map((res) => { |     }), | ||||||
|       return { |   })); | ||||||
|         id: res.id, |  | ||||||
|       }; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return result; |  | ||||||
| }; | }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 martmull
					martmull