From 196d8c97a428410d9080e0dc6cc23718cea28d5d Mon Sep 17 00:00:00 2001 From: martmull Date: Tue, 27 May 2025 20:46:15 +0200 Subject: [PATCH] Add relations in database event trigger output data (#11820) ## Done - add relations in dropdown variables - add relations in worklfow run inputs - use objectMetadataMaps in workflow folder ## To do - does not work with rest api calls, will be fixed after https://github.com/twentyhq/twenty/pull/11349 is merged - waiting for crud action relation fields https://github.com/twentyhq/core-team-issues/issues/509 --- ...phql-query-create-many-resolver.service.ts | 6 +- ...aphql-query-create-one-resolver.service.ts | 2 +- ...phql-query-delete-many-resolver.service.ts | 2 +- ...aphql-query-delete-one-resolver.service.ts | 12 +- ...hql-query-destroy-many-resolver.service.ts | 2 +- ...phql-query-destroy-one-resolver.service.ts | 2 +- ...hql-query-restore-many-resolver.service.ts | 2 +- ...phql-query-restore-one-resolver.service.ts | 12 +- ...phql-query-update-many-resolver.service.ts | 9 +- ...aphql-query-update-one-resolver.service.ts | 19 +- .../mockObjectMetadataItemsWithFieldMaps.ts | 92 +++++++++- .../search/__tests__/search.service.spec.ts | 2 +- .../interfaces/field-metadata.interface.ts | 15 +- .../interfaces/object-metadata.interface.ts | 1 + ...ve-field-maps-from-object-metadata.util.ts | 6 +- .../workflow-common.workspace-service.ts | 22 ++- .../generate-fake-form-response.spec.ts | 27 ++- .../generate-fake-object-record-event.spec.ts | 71 +++++--- .../generate-fake-object-record.spec.ts | 51 +++--- .../generate-object-record-fields.spec.ts | 96 ++++------ .../utils/generate-fake-field.ts | 18 +- .../utils/generate-fake-form-response.ts | 32 ++-- .../generate-fake-object-record-event.ts | 23 +-- .../utils/generate-fake-object-record.ts | 31 ++-- .../utils/generate-object-record-fields.ts | 59 ++++-- .../utils/should-generate-field-fake-value.ts | 13 +- .../workflow-schema/workflow-schema.module.ts | 5 +- .../workflow-schema.workspace-service.ts | 61 ++----- .../automated-trigger.module.ts | 7 +- .../database-event-trigger.listener.ts | 170 ++++++++++++++++-- 30 files changed, 568 insertions(+), 302 deletions(-) rename packages/twenty-server/src/engine/core-modules/{search => }/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts (73%) diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index df8a23567..921d15fe7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -321,8 +321,8 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol ); this.apiEventEmitterService.emitUpdateEvents({ - existingRecords: [existingRecord], - records: [record], + existingRecords: structuredClone([existingRecord]), + records: structuredClone([record]), updatedFields: Object.keys(formattedPartialRecordToUpdate), authContext, objectMetadataItem: objectMetadataItemWithFieldMaps, @@ -366,7 +366,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol } this.apiEventEmitterService.emitCreateEvents({ - records: formattedInsertedRecords, + records: structuredClone(formattedInsertedRecords), authContext, objectMetadataItem: objectMetadataItemWithFieldMaps, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts index a6cd7dc3b..a31a0d49a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts @@ -54,7 +54,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv ); this.apiEventEmitterService.emitCreateEvents({ - records: upsertedRecords, + records: structuredClone(upsertedRecords), authContext, objectMetadataItem: objectMetadataItemWithFieldMaps, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts index 6e12671e6..100007d42 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts @@ -56,7 +56,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol ); this.apiEventEmitterService.emitDeletedEvents({ - records: formattedDeletedRecords, + records: structuredClone(formattedDeletedRecords), authContext, objectMetadataItem: objectMetadataItemWithFieldMaps, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts index f5e9f117b..8538b3ca6 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts @@ -48,12 +48,6 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv objectMetadataMaps, ); - this.apiEventEmitterService.emitDeletedEvents({ - records: formattedDeletedRecords, - authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, - }); - if (formattedDeletedRecords.length === 0) { throw new GraphqlQueryRunnerException( 'Record not found', @@ -63,6 +57,12 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv const deletedRecord = formattedDeletedRecords[0]; + this.apiEventEmitterService.emitDeletedEvents({ + records: structuredClone(formattedDeletedRecords), + authContext, + objectMetadataItem: objectMetadataItemWithFieldMaps, + }); + if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts index a18b93647..0145ed9a2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts @@ -54,7 +54,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso ); this.apiEventEmitterService.emitDestroyEvents({ - records: deletedRecords, + records: structuredClone(deletedRecords), authContext, objectMetadataItem: objectMetadataItemWithFieldMaps, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts index 0c7d1fb97..1bd44395a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -54,7 +54,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol ); this.apiEventEmitterService.emitDestroyEvents({ - records: deletedRecords, + records: structuredClone(deletedRecords), authContext, objectMetadataItem: objectMetadataItemWithFieldMaps, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts index 4ffa99405..33280b482 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts @@ -56,7 +56,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso ); this.apiEventEmitterService.emitRestoreEvents({ - records: formattedRestoredRecords, + records: structuredClone(formattedRestoredRecords), authContext, objectMetadataItem: objectMetadataItemWithFieldMaps, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts index b9407a1cf..5875b441e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts @@ -48,12 +48,6 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol objectMetadataMaps, ); - this.apiEventEmitterService.emitRestoreEvents({ - records: formattedRestoredRecords, - authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, - }); - if (formattedRestoredRecords.length === 0) { throw new GraphqlQueryRunnerException( 'Record not found', @@ -63,6 +57,12 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol const restoredRecord = formattedRestoredRecords[0]; + this.apiEventEmitterService.emitRestoreEvents({ + records: structuredClone(formattedRestoredRecords), + authContext, + objectMetadataItem: objectMetadataItemWithFieldMaps, + }); + if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts index fa81216aa..261aa1195 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts @@ -90,8 +90,8 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol ); this.apiEventEmitterService.emitUpdateEvents({ - existingRecords: formattedExistingRecords, - records: formattedUpdatedRecords, + existingRecords: structuredClone(formattedExistingRecords), + records: structuredClone(formattedUpdatedRecords), updatedFields: Object.keys(executionArgs.args.data), authContext, objectMetadataItem: objectMetadataItemWithFieldMaps, @@ -101,7 +101,10 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: formattedUpdatedRecords, + parentObjectRecords: [ + ...formattedExistingRecords, + ...formattedUpdatedRecords, + ], relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, limit: QUERY_MAX_RECORDS, authContext, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts index 0ebe0b988..47b4d0923 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts @@ -74,14 +74,6 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv objectMetadataMaps, ); - this.apiEventEmitterService.emitUpdateEvents({ - existingRecords: formattedExistingRecords, - records: formattedUpdatedRecords, - updatedFields: Object.keys(executionArgs.args.data), - authContext, - objectMetadataItem: objectMetadataItemWithFieldMaps, - }); - if (formattedUpdatedRecords.length === 0) { throw new GraphqlQueryRunnerException( 'Record not found', @@ -90,12 +82,21 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv } const updatedRecord = formattedUpdatedRecords[0]; + const existingRecord = formattedExistingRecords[0]; + + this.apiEventEmitterService.emitUpdateEvents({ + existingRecords: structuredClone(formattedExistingRecords), + records: structuredClone(formattedUpdatedRecords), + updatedFields: Object.keys(executionArgs.args.data), + authContext, + objectMetadataItem: objectMetadataItemWithFieldMaps, + }); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: [updatedRecord], + parentObjectRecords: [existingRecord, updatedRecord], relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, limit: QUERY_MAX_RECORDS, authContext, diff --git a/packages/twenty-server/src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts similarity index 73% rename from packages/twenty-server/src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts rename to packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts index bb0b777f8..9120b1004 100644 --- a/packages/twenty-server/src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts +++ b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts @@ -12,6 +12,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelSingular: 'Person', labelPlural: 'People', description: 'A person', + icon: 'test-person-icon', targetTableName: 'DEPRECATED', isCustom: false, isRemote: false, @@ -24,13 +25,32 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: '', workspaceId: '', - fields: [], + fields: [ + { + id: 'nameFieldMetadataId', + objectMetadataId: '', + type: FieldMetadataType.FULL_NAME, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: { + lastName: "''", + firstName: "''", + }, + description: 'Contact’s name', + isCustom: false, + isNullable: true, + isUnique: false, + workspaceId: '', + }, + ], indexMetadatas: [], fieldsById: { nameFieldMetadataId: { id: 'nameFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.FULL_NAME, + icon: 'test-field-icon', name: 'name', label: 'Name', defaultValue: { @@ -49,6 +69,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa id: 'nameFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.FULL_NAME, + icon: 'test-field-icon', name: 'name', label: 'Name', defaultValue: { @@ -72,6 +93,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelSingular: 'Company', labelPlural: 'Companies', description: 'A company', + icon: 'test-company-icon', targetTableName: 'DEPRECATED', isCustom: false, isRemote: false, @@ -84,13 +106,41 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: '', workspaceId: '', - fields: [], + fields: [ + { + id: 'nameFieldMetadataId', + objectMetadataId: '', + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + workspaceId: '', + }, + { + id: 'domainNameFieldMetadataId', + objectMetadataId: '', + type: FieldMetadataType.LINKS, + icon: 'test-field-icon', + name: 'domainName', + label: 'Domain Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + workspaceId: '', + }, + ], indexMetadatas: [], fieldsById: { nameFieldMetadataId: { id: 'nameFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.TEXT, + icon: 'test-field-icon', name: 'name', label: 'Name', defaultValue: '', @@ -103,6 +153,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa id: 'domainNameFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.LINKS, + icon: 'test-field-icon', name: 'domainName', label: 'Domain Name', defaultValue: '', @@ -117,6 +168,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa id: 'nameFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.TEXT, + icon: 'test-field-icon', name: 'name', label: 'Name', defaultValue: { @@ -132,6 +184,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa id: 'domainNameFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.LINKS, + icon: 'test-field-icon', name: 'domainName', label: 'Domain Name', defaultValue: '', @@ -151,6 +204,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelSingular: 'Regular Custom Object', labelPlural: 'Regular Custom Objects', description: 'A regular custom object', + icon: 'test-regular-custom-object-icon', targetTableName: 'DEPRECATED', isCustom: true, isRemote: false, @@ -163,13 +217,41 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId', workspaceId: '', - fields: [], + fields: [ + { + id: 'nameFieldMetadataId', + objectMetadataId: '', + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + workspaceId: '', + }, + { + id: 'imageIdentifierFieldMetadataId', + objectMetadataId: '', + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'imageIdentifierFieldName', + label: 'Image Identifier Field Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + workspaceId: '', + }, + ], indexMetadatas: [], fieldsById: { nameFieldMetadataId: { id: 'nameFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.TEXT, + icon: 'test-field-icon', name: 'name', label: 'Name', defaultValue: '', @@ -182,6 +264,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa id: 'imageIdentifierFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.TEXT, + icon: 'test-field-icon', name: 'imageIdentifierFieldName', label: 'Image Identifier Field Name', defaultValue: '', @@ -196,6 +279,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa id: 'nameFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.TEXT, + icon: 'test-field-icon', name: 'name', label: 'Name', defaultValue: { @@ -211,6 +295,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa id: 'imageIdentifierFieldMetadataId', objectMetadataId: '', type: FieldMetadataType.TEXT, + icon: 'test-field-icon', name: 'imageIdentifierFieldName', label: 'Image Identifier Field Name', defaultValue: '', @@ -230,6 +315,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa labelSingular: '', labelPlural: '', description: '', + icon: 'test-non-searchable-object-icon', targetTableName: 'DEPRECATED', isCustom: false, isRemote: false, diff --git a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts index 1f84f37ad..20c8f8eec 100644 --- a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { SearchService } from 'src/engine/core-modules/search/services/search.service'; import { encodeCursorData } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts index 0568ecfcb..36397bbbe 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts @@ -3,10 +3,8 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; - -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { RelationMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-metadata.interface'; export interface FieldMetadataInterface< T extends FieldMetadataType = FieldMetadataType, @@ -21,14 +19,15 @@ export interface FieldMetadataInterface< objectMetadataId: string; workspaceId?: string; description?: string; + icon?: string; isNullable?: boolean; isUnique?: boolean; - fromRelationMetadata?: RelationMetadataEntity; - toRelationMetadata?: RelationMetadataEntity; + fromRelationMetadata?: RelationMetadataInterface; + toRelationMetadata?: RelationMetadataInterface; relationTargetFieldMetadataId?: string; - relationTargetFieldMetadata?: FieldMetadataEntity; + relationTargetFieldMetadata?: FieldMetadataInterface; relationTargetObjectMetadataId?: string; - relationTargetObjectMetadata?: ObjectMetadataEntity; + relationTargetObjectMetadata?: ObjectMetadataInterface; isCustom?: boolean; isSystem?: boolean; isActive?: boolean; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface.ts index 7122b400b..1c0f7799d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface.ts @@ -14,6 +14,7 @@ export interface ObjectMetadataInterface { labelSingular: string; labelPlural: string; description?: string; + icon?: string; targetTableName: string; fromRelations: RelationMetadataInterface[]; toRelations: RelationMetadataInterface[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts index dcba421ec..b84f0840a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts @@ -7,4 +7,8 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ export const removeFieldMapsFromObjectMetadata = ( objectMetadata: ObjectMetadataItemWithFieldMaps, ): ObjectMetadataInterface => - omit(objectMetadata, ['fieldsById', 'fieldsByName']); + omit(objectMetadata, [ + 'fieldsById', + 'fieldsByName', + 'fieldsByJoinColumnName', + ]); diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts index 3f4fef179..3ef0db64b 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts @@ -20,6 +20,11 @@ import { } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity'; +export type ObjectMetadataInfo = { + objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + objectMetadataMaps: ObjectMetadataMaps; +}; + @Injectable() export class WorkflowCommonWorkspaceService { constructor( @@ -73,13 +78,9 @@ export class WorkflowCommonWorkspaceService { return { ...workflowVersion, trigger: workflowVersion.trigger }; } - async getObjectMetadataItemWithFieldsMaps( - objectNameSingular: string, + async getObjectMetadataMaps( workspaceId: string, - ): Promise<{ - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; - }> { + ): Promise { const currentCacheVersion = await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); @@ -103,6 +104,15 @@ export class WorkflowCommonWorkspaceService { ); } + return objectMetadataMaps; + } + + async getObjectMetadataItemWithFieldsMaps( + objectNameSingular: string, + workspaceId: string, + ): Promise { + const objectMetadataMaps = await this.getObjectMetadataMaps(workspaceId); + const objectMetadataItemWithFieldsMaps = getObjectMetadataMapItemByNameSingular( objectMetadataMaps, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts index 1428e4213..0bf7bc826 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts @@ -1,6 +1,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response'; import { FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type'; @@ -9,15 +9,6 @@ const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find( )!; describe('generateFakeFormResponse', () => { - // @ts-expect-error legacy noImplicitAny - let objectMetadataRepository; - - beforeEach(() => { - objectMetadataRepository = { - findOneOrFail: jest.fn().mockResolvedValue(companyMockObjectMetadataItem), - }; - }); - it('should generate fake responses for a form schema', async () => { const schema: FormFieldMetadata[] = [ { @@ -50,11 +41,19 @@ describe('generateFakeFormResponse', () => { }, ]; + const mockObjectMetadataMaps = { + byId: { + [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, + }, + idByNameSingular: { + [companyMockObjectMetadataItem.nameSingular]: + companyMockObjectMetadataItem.id, + }, + }; + const result = await generateFakeFormResponse({ formMetadata: schema, - workspaceId: '1', - // @ts-expect-error legacy noImplicitAny - objectMetadataRepository, + objectMetadataMaps: mockObjectMetadataMaps, }); expect(result).toEqual({ @@ -82,7 +81,7 @@ describe('generateFakeFormResponse', () => { isLeaf: true, label: 'Company', fieldIdName: 'id', - icon: undefined, + icon: 'test-company-icon', nameSingular: 'company', value: 'A company', }, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts index bbb30e816..de304b555 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts @@ -1,7 +1,7 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; +import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; jest.mock( 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields', @@ -12,35 +12,48 @@ describe('generateFakeObjectRecordEvent', () => { jest.clearAllMocks(); }); - const mockObjectMetadata = { - icon: 'test-icon', - labelSingular: 'Test Object', - description: 'Test Description', - nameSingular: 'testObject', - } as ObjectMetadataEntity; - const mockFields = { field1: { type: 'TEXT', value: 'test' }, field2: { type: 'NUMBER', value: 123 }, }; + const companyMockObjectMetadataItem = + mockObjectMetadataItemsWithFieldMaps.find( + (item) => item.nameSingular === 'company', + )!; + + const mockObjectMetadataMaps = { + byId: { + [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, + }, + idByNameSingular: { + [companyMockObjectMetadataItem.nameSingular]: + companyMockObjectMetadataItem.id, + }, + }; + + const objectMetadataInfo = { + objectMetadataMaps: mockObjectMetadataMaps, + objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, + }; + beforeEach(() => { (generateObjectRecordFields as jest.Mock).mockReturnValue(mockFields); }); it('should generate record with "after" prefix for CREATED action', () => { const result = generateFakeObjectRecordEvent( - mockObjectMetadata, + objectMetadataInfo, DatabaseEventAction.CREATED, ); expect(result).toEqual({ object: { isLeaf: true, - icon: 'test-icon', - label: 'Test Object', - value: 'Test Description', - nameSingular: 'testObject', + icon: 'test-company-icon', + label: 'Company', + value: 'A company', + nameSingular: 'company', fieldIdName: 'properties.after.id', }, fields: { @@ -53,17 +66,17 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "after" prefix for UPDATED action', () => { const result = generateFakeObjectRecordEvent( - mockObjectMetadata, + objectMetadataInfo, DatabaseEventAction.UPDATED, ); expect(result).toEqual({ object: { isLeaf: true, - icon: 'test-icon', - label: 'Test Object', - value: 'Test Description', - nameSingular: 'testObject', + icon: 'test-company-icon', + label: 'Company', + value: 'A company', + nameSingular: 'company', fieldIdName: 'properties.after.id', }, fields: { @@ -76,17 +89,17 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "before" prefix for DELETED action', () => { const result = generateFakeObjectRecordEvent( - mockObjectMetadata, + objectMetadataInfo, DatabaseEventAction.DELETED, ); expect(result).toEqual({ object: { isLeaf: true, - icon: 'test-icon', - label: 'Test Object', - value: 'Test Description', - nameSingular: 'testObject', + icon: 'test-company-icon', + label: 'Company', + value: 'A company', + nameSingular: 'company', fieldIdName: 'properties.before.id', }, fields: { @@ -99,17 +112,17 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "before" prefix for DESTROYED action', () => { const result = generateFakeObjectRecordEvent( - mockObjectMetadata, + objectMetadataInfo, DatabaseEventAction.DESTROYED, ); expect(result).toEqual({ object: { isLeaf: true, - icon: 'test-icon', - label: 'Test Object', - value: 'Test Description', - nameSingular: 'testObject', + icon: 'test-company-icon', + label: 'Company', + value: 'A company', + nameSingular: 'company', fieldIdName: 'properties.before.id', }, fields: { @@ -123,7 +136,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should throw error for unknown action', () => { expect(() => { generateFakeObjectRecordEvent( - mockObjectMetadata, + objectMetadataInfo, 'UNKNOWN' as DatabaseEventAction, ); }).toThrow("Unknown action 'UNKNOWN'"); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts index 1108869f5..64b654e1f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts @@ -1,6 +1,6 @@ -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; +import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; jest.mock( 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields', @@ -12,24 +12,36 @@ jest.mock( }), ); +const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find( + (item) => item.nameSingular === 'company', +)!; + +const mockObjectMetadataMaps = { + byId: { + [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, + }, + idByNameSingular: { + [companyMockObjectMetadataItem.nameSingular]: + companyMockObjectMetadataItem.id, + }, +}; + +const objectMetadataInfo = { + objectMetadataMaps: mockObjectMetadataMaps, + objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, +}; + describe('generateFakeObjectRecord', () => { it('should generate a record with correct object metadata', () => { - const mockObjectMetadata = { - icon: 'test-icon', - labelSingular: 'Test Object', - description: 'Test Description', - nameSingular: 'testObject', - } as ObjectMetadataEntity; - - const result = generateFakeObjectRecord(mockObjectMetadata); + const result = generateFakeObjectRecord(objectMetadataInfo); expect(result).toEqual({ object: { isLeaf: true, - icon: 'test-icon', - label: 'Test Object', - value: 'Test Description', - nameSingular: 'testObject', + icon: 'test-company-icon', + label: 'Company', + value: 'A company', + nameSingular: 'company', fieldIdName: 'id', }, fields: { @@ -41,15 +53,10 @@ describe('generateFakeObjectRecord', () => { }); it('should call generateObjectRecordFields with the object metadata', () => { - const mockObjectMetadata = { - icon: 'test-icon', - labelSingular: 'Test Object', - description: 'Test Description', - nameSingular: 'testObject', - } as ObjectMetadataEntity; + generateFakeObjectRecord(objectMetadataInfo); - generateFakeObjectRecord(mockObjectMetadata); - - expect(generateObjectRecordFields).toHaveBeenCalledWith(mockObjectMetadata); + expect(generateObjectRecordFields).toHaveBeenCalledWith({ + objectMetadataInfo, + }); }); }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts index 5ecab2fde..1449d871a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts @@ -1,9 +1,9 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; +import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; jest.mock( 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field', @@ -17,38 +17,27 @@ describe('generateObjectRecordFields', () => { jest.clearAllMocks(); }); + const companyMockObjectMetadataItem = + mockObjectMetadataItemsWithFieldMaps.find( + (item) => item.nameSingular === 'company', + )!; + + const mockObjectMetadataMaps = { + byId: { + [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, + }, + idByNameSingular: { + [companyMockObjectMetadataItem.nameSingular]: + companyMockObjectMetadataItem.id, + }, + }; + + const objectMetadataInfo = { + objectMetadataMaps: mockObjectMetadataMaps, + objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, + }; + it('should generate fields for valid fields only', () => { - const mockFields = [ - { - name: 'field1', - type: FieldMetadataType.TEXT, - label: 'Field 1', - icon: 'icon1', - isSystem: false, - isActive: true, - }, - { - name: 'field2', - type: FieldMetadataType.RELATION, - label: 'Field 2', - icon: 'icon2', - isSystem: false, - isActive: true, - }, - { - name: 'field3', - type: FieldMetadataType.NUMBER, - label: 'Field 3', - icon: 'icon3', - isSystem: false, - isActive: true, - }, - ]; - - const mockObjectMetadata = { - fields: mockFields, - } as ObjectMetadataEntity; - (shouldGenerateFieldFakeValue as jest.Mock).mockImplementation( (field) => field.type !== FieldMetadataType.RELATION, ); @@ -62,49 +51,34 @@ describe('generateObjectRecordFields', () => { }), ); - const result = generateObjectRecordFields(mockObjectMetadata); + const result = generateObjectRecordFields({ objectMetadataInfo }); expect(result).toEqual({ - field1: { - type: FieldMetadataType.TEXT, - label: 'Field 1', - icon: 'icon1', - value: 'mock-TEXT', + domainName: { + icon: 'test-field-icon', + label: 'Domain Name', + type: 'LINKS', + value: 'mock-LINKS', }, - field3: { - type: FieldMetadataType.NUMBER, - label: 'Field 3', - icon: 'icon3', - value: 'mock-NUMBER', + name: { + icon: 'test-field-icon', + label: 'Name', + type: 'TEXT', + value: 'mock-TEXT', }, }); - expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(3); + expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(2); expect(generateFakeField).toHaveBeenCalledTimes(2); }); it('should return empty object when no valid fields', () => { - const mockFields = [ - { - name: 'field1', - type: FieldMetadataType.RELATION, - label: 'Field 1', - icon: 'icon1', - isSystem: false, - isActive: true, - }, - ]; - - const mockObjectMetadata = { - fields: mockFields, - } as ObjectMetadataEntity; - (shouldGenerateFieldFakeValue as jest.Mock).mockReturnValue(false); - const result = generateObjectRecordFields(mockObjectMetadata); + const result = generateObjectRecordFields({ objectMetadataInfo }); expect(result).toEqual({}); - expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(1); + expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(2); expect(generateFakeField).not.toHaveBeenCalled(); }); }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts index bacedc5c3..e345b3369 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts @@ -19,15 +19,7 @@ export const generateFakeField = ({ }): Leaf | Node => { const compositeType = compositeTypeDefinitions.get(type); - if (!compositeType) { - return { - isLeaf: true, - type: type, - icon: icon, - label: label, - value: generateFakeValue(type, 'FieldMetadataType'), - }; - } else { + if (compositeType) { return { isLeaf: false, icon: icon, @@ -45,4 +37,12 @@ export const generateFakeField = ({ }, {}), }; } + + return { + isLeaf: true, + type: type, + icon: icon, + label: label, + value: generateFakeValue(type, 'FieldMetadataType'), + }; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts index ff5e7f1c6..29bbc41d4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts @@ -1,7 +1,5 @@ import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { Leaf, Node, @@ -9,15 +7,15 @@ import { import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; import { FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; export const generateFakeFormResponse = async ({ formMetadata, - workspaceId, - objectMetadataRepository, + objectMetadataMaps, }: { formMetadata: FormFieldMetadata[]; - workspaceId: string; - objectMetadataRepository: Repository; + objectMetadataMaps: ObjectMetadataMaps; }): Promise> => { const result = await Promise.all( formMetadata.map(async (formFieldMetadata) => { @@ -26,19 +24,25 @@ export const generateFakeFormResponse = async ({ return undefined; } - const objectMetadata = await objectMetadataRepository.findOneOrFail({ - where: { - nameSingular: formFieldMetadata?.settings?.objectName, - workspaceId, - }, - relations: ['fields'], - }); + const objectMetadataItemWithFieldsMaps = + getObjectMetadataMapItemByNameSingular( + objectMetadataMaps, + formFieldMetadata?.settings?.objectName, + ); + + if (!objectMetadataItemWithFieldsMaps) + throw new Error( + `Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`, + ); return { [formFieldMetadata.name]: { isLeaf: false, label: formFieldMetadata.label, - value: generateFakeObjectRecord(objectMetadata), + value: generateFakeObjectRecord({ + objectMetadataMaps, + objectMetadataItemWithFieldsMaps, + }), }, }; } else { diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts index 9a5fcfe60..044dab8e0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts @@ -1,19 +1,19 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { BaseOutputSchema, RecordOutputSchema, } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; +import { ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; const generateFakeObjectRecordEventWithPrefix = ({ - objectMetadataEntity, + objectMetadataInfo, prefix, }: { - objectMetadataEntity: ObjectMetadataEntity; + objectMetadataInfo: ObjectMetadataInfo; prefix: string; }): RecordOutputSchema => { - const recordFields = generateObjectRecordFields(objectMetadataEntity); + const recordFields = generateObjectRecordFields({ objectMetadataInfo }); const prefixedRecordFields = Object.entries(recordFields).reduce( (acc, [key, value]) => { acc[`${prefix}.${key}`] = value; @@ -26,10 +26,11 @@ const generateFakeObjectRecordEventWithPrefix = ({ return { object: { isLeaf: true, - icon: objectMetadataEntity.icon, - label: objectMetadataEntity.labelSingular, - value: objectMetadataEntity.description, - nameSingular: objectMetadataEntity.nameSingular, + icon: objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon, + label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular, + value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description, + nameSingular: + objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular, fieldIdName: `${prefix}.id`, }, fields: prefixedRecordFields, @@ -38,20 +39,20 @@ const generateFakeObjectRecordEventWithPrefix = ({ }; export const generateFakeObjectRecordEvent = ( - objectMetadataEntity: ObjectMetadataEntity, + objectMetadataInfo: ObjectMetadataInfo, action: DatabaseEventAction, ): RecordOutputSchema => { switch (action) { case DatabaseEventAction.CREATED: case DatabaseEventAction.UPDATED: return generateFakeObjectRecordEventWithPrefix({ - objectMetadataEntity, + objectMetadataInfo, prefix: 'properties.after', }); case DatabaseEventAction.DELETED: case DatabaseEventAction.DESTROYED: return generateFakeObjectRecordEventWithPrefix({ - objectMetadataEntity, + objectMetadataInfo, prefix: 'properties.before', }); default: diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts index 33fe7af7a..e14ae581e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts @@ -1,18 +1,21 @@ -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RecordOutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; +import { ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; export const generateFakeObjectRecord = ( - objectMetadataEntity: ObjectMetadataEntity, -): RecordOutputSchema => ({ - object: { - isLeaf: true, - icon: objectMetadataEntity.icon, - label: objectMetadataEntity.labelSingular, - value: objectMetadataEntity.description, - nameSingular: objectMetadataEntity.nameSingular, - fieldIdName: 'id', - }, - fields: generateObjectRecordFields(objectMetadataEntity), - _outputSchemaType: 'RECORD', -}); + objectMetadataInfo: ObjectMetadataInfo, +): RecordOutputSchema => { + return { + object: { + isLeaf: true, + icon: objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon, + label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular, + value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description, + nameSingular: + objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular, + fieldIdName: 'id', + }, + fields: generateObjectRecordFields({ objectMetadataInfo }), + _outputSchemaType: 'RECORD', + }; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts index 4ec536b7a..a7dc58311 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts @@ -1,21 +1,60 @@ -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + import { BaseOutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; +import { ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; -export const generateObjectRecordFields = ( - objectMetadataEntity: ObjectMetadataEntity, -): BaseOutputSchema => - objectMetadataEntity.fields.reduce((acc: BaseOutputSchema, field) => { +const MAXIMUM_DEPTH = 1; + +export const generateObjectRecordFields = ({ + objectMetadataInfo, + depth = 0, +}: { + objectMetadataInfo: ObjectMetadataInfo; + depth?: number; +}): BaseOutputSchema => { + const objectMetadata = objectMetadataInfo.objectMetadataItemWithFieldsMaps; + + return objectMetadata.fields.reduce((acc: BaseOutputSchema, field) => { if (!shouldGenerateFieldFakeValue(field)) { return acc; } - acc[field.name] = generateFakeField({ - type: field.type, - label: field.label, - icon: field.icon, - }); + if (field.type !== FieldMetadataType.RELATION) { + acc[field.name] = generateFakeField({ + type: field.type, + label: field.label, + icon: field.icon, + }); + + return acc; + } + + if ( + depth < MAXIMUM_DEPTH && + isDefined(field.relationTargetObjectMetadataId) + ) { + const relationTargetObjectMetadata = + objectMetadataInfo.objectMetadataMaps.byId[ + field.relationTargetObjectMetadataId + ]; + + acc[field.name] = { + isLeaf: false, + icon: field.icon, + label: field.label, + value: generateObjectRecordFields({ + objectMetadataInfo: { + objectMetadataItemWithFieldsMaps: relationTargetObjectMetadata, + objectMetadataMaps: objectMetadataInfo.objectMetadataMaps, + }, + depth: depth + 1, + }), + }; + } return acc; }, {} as BaseOutputSchema); +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts index c1b678db3..aa61a1618 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts @@ -1,11 +1,18 @@ import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -export const shouldGenerateFieldFakeValue = (field: FieldMetadataEntity) => { +const isManyToOneRelationField = (field: FieldMetadataInterface) => + (field as FieldMetadataEntity).settings + ?.relationType === 'MANY_TO_ONE'; + +export const shouldGenerateFieldFakeValue = (field: FieldMetadataInterface) => { return ( - (!field.isSystem || field.name === 'id') && field.isActive && - field.type !== FieldMetadataType.RELATION + (!field.isSystem || field.name === 'id' || field.name === 'userEmail') && + (field.type !== FieldMetadataType.RELATION || + isManyToOneRelationField(field)) ); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module.ts index c3a4e0087..a2ceb12a3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module.ts @@ -1,11 +1,10 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; +import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; @Module({ - imports: [TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata')], + imports: [WorkflowCommonModule], providers: [WorkflowSchemaWorkspaceService], exports: [WorkflowSchemaWorkspaceService], }) diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index 3582d2bec..48d97e6b4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -1,12 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response'; @@ -21,12 +16,12 @@ import { WorkflowTrigger, WorkflowTriggerType, } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() export class WorkflowSchemaWorkspaceService { constructor( - @InjectRepository(ObjectMetadataEntity, 'metadata') - private readonly objectMetadataRepository: Repository, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async computeStepOutputSchema({ @@ -43,7 +38,6 @@ export class WorkflowSchemaWorkspaceService { return this.computeDatabaseEventTriggerOutputSchema({ eventName: step.settings.eventName, workspaceId, - objectMetadataRepository: this.objectMetadataRepository, }); } case WorkflowTriggerType.MANUAL: { @@ -56,7 +50,6 @@ export class WorkflowSchemaWorkspaceService { return this.computeRecordOutputSchema({ objectType, workspaceId, - objectMetadataRepository: this.objectMetadataRepository, }); } case WorkflowTriggerType.WEBHOOK: @@ -72,19 +65,16 @@ export class WorkflowSchemaWorkspaceService { return this.computeRecordOutputSchema({ objectType: step.settings.input.objectName, workspaceId, - objectMetadataRepository: this.objectMetadataRepository, }); case WorkflowActionType.FIND_RECORDS: return this.computeFindRecordsOutputSchema({ objectType: step.settings.input.objectName, workspaceId, - objectMetadataRepository: this.objectMetadataRepository, }); case WorkflowActionType.FORM: return this.computeFormActionOutputSchema({ formMetadata: step.settings.input, workspaceId, - objectMetadataRepository: this.objectMetadataRepository, }); case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution default: @@ -95,11 +85,9 @@ export class WorkflowSchemaWorkspaceService { private async computeDatabaseEventTriggerOutputSchema({ eventName, workspaceId, - objectMetadataRepository, }: { eventName: string; workspaceId: string; - objectMetadataRepository: Repository; }): Promise { const [nameSingular, action] = eventName.split('.'); @@ -107,20 +95,14 @@ export class WorkflowSchemaWorkspaceService { return {}; } - const objectMetadata = await objectMetadataRepository.findOneOrFail({ - where: { + const objectMetadataInfo = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( nameSingular, workspaceId, - }, - relations: ['fields'], - }); - - if (!isDefined(objectMetadata)) { - return {}; - } + ); return generateFakeObjectRecordEvent( - objectMetadata, + objectMetadataInfo, action as DatabaseEventAction, ); } @@ -128,16 +110,13 @@ export class WorkflowSchemaWorkspaceService { private async computeFindRecordsOutputSchema({ objectType, workspaceId, - objectMetadataRepository, }: { objectType: string; workspaceId: string; - objectMetadataRepository: Repository; }): Promise { const recordOutputSchema = await this.computeRecordOutputSchema({ objectType, workspaceId, - objectMetadataRepository, }); return { @@ -159,25 +138,17 @@ export class WorkflowSchemaWorkspaceService { private async computeRecordOutputSchema({ objectType, workspaceId, - objectMetadataRepository, }: { objectType: string; workspaceId: string; - objectMetadataRepository: Repository; }): Promise { - const objectMetadata = await objectMetadataRepository.findOneOrFail({ - where: { - nameSingular: objectType, + const objectMetadataInfo = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + objectType, workspaceId, - }, - relations: ['fields'], - }); + ); - if (!isDefined(objectMetadata)) { - return {}; - } - - return generateFakeObjectRecord(objectMetadata); + return generateFakeObjectRecord(objectMetadataInfo); } private computeSendEmailActionOutputSchema(): OutputSchema { @@ -187,16 +158,18 @@ export class WorkflowSchemaWorkspaceService { private async computeFormActionOutputSchema({ formMetadata, workspaceId, - objectMetadataRepository, }: { formMetadata: FormFieldMetadata[]; workspaceId: string; - objectMetadataRepository: Repository; }): Promise { + const objectMetadataMaps = + await this.workflowCommonWorkspaceService.getObjectMetadataMaps( + workspaceId, + ); + return generateFakeFormResponse({ formMetadata, - workspaceId, - objectMetadataRepository, + objectMetadataMaps, }); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.module.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.module.ts index ac4ffc5f4..130352611 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.module.ts @@ -7,9 +7,14 @@ import { DatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trig import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { CronTriggerCronCommand } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/commands/cron-trigger.cron.command'; import { CronTriggerCronJob } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/jobs/cron-trigger.cron.job'; +import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; @Module({ - imports: [FeatureFlagModule, TypeOrmModule.forFeature([Workspace], 'core')], + imports: [ + FeatureFlagModule, + TypeOrmModule.forFeature([Workspace], 'core'), + WorkflowCommonModule, + ], providers: [ AutomatedTriggerWorkspaceService, DatabaseEventTriggerListener, diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts index 5369961fe..e3ae63f16 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts @@ -1,5 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; +import { isDefined } from 'twenty-shared/utils'; + import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; @@ -17,6 +19,8 @@ import { WorkflowTriggerJob, WorkflowTriggerJobData, } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job'; +import { ObjectRecordNonDestructiveEvent } from 'src/engine/core-modules/event-emitter/types/object-record-non-destructive-event'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { AutomatedTriggerType, WorkflowAutomatedTriggerWorkspaceEntity, @@ -31,43 +35,172 @@ export class DatabaseEventTriggerListener { @InjectMessageQueue(MessageQueue.workflowQueue) private readonly messageQueueService: MessageQueueService, private readonly isFeatureFlagEnabledService: FeatureFlagService, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} @OnDatabaseBatchEvent('*', DatabaseEventAction.CREATED) async handleObjectRecordCreateEvent( payload: WorkspaceEventBatch, ) { - await this.handleEvent(payload); + if (await this.shouldIgnoreEvent(payload)) { + return; + } + + const clonedPayload = structuredClone(payload); + + await this.enrichCreatedEvent(clonedPayload); + await this.handleEvent(clonedPayload); } @OnDatabaseBatchEvent('*', DatabaseEventAction.UPDATED) async handleObjectRecordUpdateEvent( payload: WorkspaceEventBatch, ) { - await this.handleEvent(payload); + if (await this.shouldIgnoreEvent(payload)) { + return; + } + + const clonedPayload = structuredClone(payload); + + await this.enrichUpdatedEvent(clonedPayload); + await this.handleEvent(clonedPayload); } @OnDatabaseBatchEvent('*', DatabaseEventAction.DELETED) async handleObjectRecordDeleteEvent( payload: WorkspaceEventBatch, ) { - await this.handleEvent(payload); + if (await this.shouldIgnoreEvent(payload)) { + return; + } + + const clonedPayload = structuredClone(payload); + + await this.enrichDeletedEvent(clonedPayload); + await this.handleEvent(clonedPayload); } @OnDatabaseBatchEvent('*', DatabaseEventAction.DESTROYED) async handleObjectRecordDestroyEvent( payload: WorkspaceEventBatch, ) { - await this.handleEvent(payload); + if (await this.shouldIgnoreEvent(payload)) { + return; + } + + const clonedPayload = structuredClone(payload); + + await this.enrichDestroyedEvent(clonedPayload); + await this.handleEvent(clonedPayload); } - private async handleEvent( - payload: WorkspaceEventBatch< - | ObjectRecordCreateEvent - | ObjectRecordUpdateEvent - | ObjectRecordDeleteEvent - | ObjectRecordDestroyEvent - >, + private async enrichCreatedEvent( + payload: WorkspaceEventBatch, + ) { + const workspaceId = payload.workspaceId; + + for (const event of payload.events) { + await this.enrichRecord({ + event, + record: event.properties.after, + workspaceId, + }); + } + } + + private async enrichUpdatedEvent( + payload: WorkspaceEventBatch, + ) { + const workspaceId = payload.workspaceId; + + for (const event of payload.events) { + await this.enrichRecord({ + event, + record: event.properties.before, + workspaceId, + }); + await this.enrichRecord({ + event, + record: event.properties.after, + workspaceId, + }); + } + } + + private async enrichDeletedEvent( + payload: WorkspaceEventBatch, + ) { + const workspaceId = payload.workspaceId; + + for (const event of payload.events) { + await this.enrichRecord({ + event, + record: event.properties.before, + workspaceId, + }); + } + } + + private async enrichDestroyedEvent( + payload: WorkspaceEventBatch, + ) { + const workspaceId = payload.workspaceId; + + for (const event of payload.events) { + await this.enrichRecord({ + event, + record: event.properties.before, + workspaceId, + }); + } + } + + private async enrichRecord({ + event, + record, + workspaceId, + }: { + event: ObjectRecordNonDestructiveEvent; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + record: Record; + workspaceId: string; + }) { + const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + event.objectMetadata.nameSingular, + workspaceId, + ); + + const fieldsByJoinColumnName = + objectMetadataItemWithFieldsMaps.fieldsByJoinColumnName; + + for (const [joinColumn, joinField] of Object.entries( + fieldsByJoinColumnName, + )) { + const joinRecordId = record[joinColumn]; + const relatedObjectMetadataId = joinField.relationTargetObjectMetadataId; + + if (!isDefined(relatedObjectMetadataId)) { + continue; + } + + const relatedObjectMetadataNameSingular = + objectMetadataMaps.byId[relatedObjectMetadataId].nameSingular; + + const relatedObjectRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + relatedObjectMetadataNameSingular, + ); + + record[joinField.name] = await relatedObjectRepository.findOne({ + where: { id: joinRecordId }, + }); + } + } + + private async shouldIgnoreEvent( + payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; const databaseEventName = payload.name; @@ -79,7 +212,7 @@ export class DatabaseEventTriggerListener { )}`, ); - return; + return true; } const isWorkflowEnabled = @@ -88,9 +221,14 @@ export class DatabaseEventTriggerListener { workspaceId, ); - if (!isWorkflowEnabled) { - return; - } + return !isWorkflowEnabled; + } + + private async handleEvent( + payload: WorkspaceEventBatch, + ) { + const workspaceId = payload.workspaceId; + const databaseEventName = payload.name; const workflowAutomatedTriggerRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( @@ -107,7 +245,7 @@ export class DatabaseEventTriggerListener { for (const eventListener of eventListeners) { for (const eventPayload of payload.events) { - this.messageQueueService.add( + await this.messageQueueService.add( WorkflowTriggerJob.name, { workspaceId,