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
This commit is contained in:
martmull
2025-05-27 20:46:15 +02:00
committed by GitHub
parent 01b40e173b
commit 196d8c97a4
30 changed files with 568 additions and 302 deletions

View File

@ -321,8 +321,8 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
); );
this.apiEventEmitterService.emitUpdateEvents({ this.apiEventEmitterService.emitUpdateEvents({
existingRecords: [existingRecord], existingRecords: structuredClone([existingRecord]),
records: [record], records: structuredClone([record]),
updatedFields: Object.keys(formattedPartialRecordToUpdate), updatedFields: Object.keys(formattedPartialRecordToUpdate),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: objectMetadataItemWithFieldMaps,
@ -366,7 +366,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
} }
this.apiEventEmitterService.emitCreateEvents({ this.apiEventEmitterService.emitCreateEvents({
records: formattedInsertedRecords, records: structuredClone(formattedInsertedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: objectMetadataItemWithFieldMaps,
}); });

View File

@ -54,7 +54,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
); );
this.apiEventEmitterService.emitCreateEvents({ this.apiEventEmitterService.emitCreateEvents({
records: upsertedRecords, records: structuredClone(upsertedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: objectMetadataItemWithFieldMaps,
}); });

View File

@ -56,7 +56,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
); );
this.apiEventEmitterService.emitDeletedEvents({ this.apiEventEmitterService.emitDeletedEvents({
records: formattedDeletedRecords, records: structuredClone(formattedDeletedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: objectMetadataItemWithFieldMaps,
}); });

View File

@ -48,12 +48,6 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
objectMetadataMaps, objectMetadataMaps,
); );
this.apiEventEmitterService.emitDeletedEvents({
records: formattedDeletedRecords,
authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps,
});
if (formattedDeletedRecords.length === 0) { if (formattedDeletedRecords.length === 0) {
throw new GraphqlQueryRunnerException( throw new GraphqlQueryRunnerException(
'Record not found', 'Record not found',
@ -63,6 +57,12 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
const deletedRecord = formattedDeletedRecords[0]; const deletedRecord = formattedDeletedRecords[0];
this.apiEventEmitterService.emitDeletedEvents({
records: structuredClone(formattedDeletedRecords),
authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps,
});
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({ await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps, objectMetadataMaps,

View File

@ -54,7 +54,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
); );
this.apiEventEmitterService.emitDestroyEvents({ this.apiEventEmitterService.emitDestroyEvents({
records: deletedRecords, records: structuredClone(deletedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: objectMetadataItemWithFieldMaps,
}); });

View File

@ -54,7 +54,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
); );
this.apiEventEmitterService.emitDestroyEvents({ this.apiEventEmitterService.emitDestroyEvents({
records: deletedRecords, records: structuredClone(deletedRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: objectMetadataItemWithFieldMaps,
}); });

View File

@ -56,7 +56,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
); );
this.apiEventEmitterService.emitRestoreEvents({ this.apiEventEmitterService.emitRestoreEvents({
records: formattedRestoredRecords, records: structuredClone(formattedRestoredRecords),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: objectMetadataItemWithFieldMaps,
}); });

View File

@ -48,12 +48,6 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
objectMetadataMaps, objectMetadataMaps,
); );
this.apiEventEmitterService.emitRestoreEvents({
records: formattedRestoredRecords,
authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps,
});
if (formattedRestoredRecords.length === 0) { if (formattedRestoredRecords.length === 0) {
throw new GraphqlQueryRunnerException( throw new GraphqlQueryRunnerException(
'Record not found', 'Record not found',
@ -63,6 +57,12 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
const restoredRecord = formattedRestoredRecords[0]; const restoredRecord = formattedRestoredRecords[0];
this.apiEventEmitterService.emitRestoreEvents({
records: structuredClone(formattedRestoredRecords),
authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps,
});
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({ await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps, objectMetadataMaps,

View File

@ -90,8 +90,8 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
); );
this.apiEventEmitterService.emitUpdateEvents({ this.apiEventEmitterService.emitUpdateEvents({
existingRecords: formattedExistingRecords, existingRecords: structuredClone(formattedExistingRecords),
records: formattedUpdatedRecords, records: structuredClone(formattedUpdatedRecords),
updatedFields: Object.keys(executionArgs.args.data), updatedFields: Object.keys(executionArgs.args.data),
authContext, authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps, objectMetadataItem: objectMetadataItemWithFieldMaps,
@ -101,7 +101,10 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
await this.processNestedRelationsHelper.processNestedRelations({ await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps, objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: formattedUpdatedRecords, parentObjectRecords: [
...formattedExistingRecords,
...formattedUpdatedRecords,
],
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS, limit: QUERY_MAX_RECORDS,
authContext, authContext,

View File

@ -74,14 +74,6 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
objectMetadataMaps, objectMetadataMaps,
); );
this.apiEventEmitterService.emitUpdateEvents({
existingRecords: formattedExistingRecords,
records: formattedUpdatedRecords,
updatedFields: Object.keys(executionArgs.args.data),
authContext,
objectMetadataItem: objectMetadataItemWithFieldMaps,
});
if (formattedUpdatedRecords.length === 0) { if (formattedUpdatedRecords.length === 0) {
throw new GraphqlQueryRunnerException( throw new GraphqlQueryRunnerException(
'Record not found', 'Record not found',
@ -90,12 +82,21 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
} }
const updatedRecord = formattedUpdatedRecords[0]; 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) { if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({ await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps, objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: [updatedRecord], parentObjectRecords: [existingRecord, updatedRecord],
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS, limit: QUERY_MAX_RECORDS,
authContext, authContext,

View File

@ -12,6 +12,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelSingular: 'Person', labelSingular: 'Person',
labelPlural: 'People', labelPlural: 'People',
description: 'A person', description: 'A person',
icon: 'test-person-icon',
targetTableName: 'DEPRECATED', targetTableName: 'DEPRECATED',
isCustom: false, isCustom: false,
isRemote: false, isRemote: false,
@ -24,13 +25,32 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', workspaceId: '',
fields: [], fields: [
{
id: 'nameFieldMetadataId',
objectMetadataId: '',
type: FieldMetadataType.FULL_NAME,
icon: 'test-field-icon',
name: 'name',
label: 'Name',
defaultValue: {
lastName: "''",
firstName: "''",
},
description: 'Contacts name',
isCustom: false,
isNullable: true,
isUnique: false,
workspaceId: '',
},
],
indexMetadatas: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: {
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.FULL_NAME, type: FieldMetadataType.FULL_NAME,
icon: 'test-field-icon',
name: 'name', name: 'name',
label: 'Name', label: 'Name',
defaultValue: { defaultValue: {
@ -49,6 +69,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.FULL_NAME, type: FieldMetadataType.FULL_NAME,
icon: 'test-field-icon',
name: 'name', name: 'name',
label: 'Name', label: 'Name',
defaultValue: { defaultValue: {
@ -72,6 +93,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelSingular: 'Company', labelSingular: 'Company',
labelPlural: 'Companies', labelPlural: 'Companies',
description: 'A company', description: 'A company',
icon: 'test-company-icon',
targetTableName: 'DEPRECATED', targetTableName: 'DEPRECATED',
isCustom: false, isCustom: false,
isRemote: false, isRemote: false,
@ -84,13 +106,41 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', 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: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: {
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon',
name: 'name', name: 'name',
label: 'Name', label: 'Name',
defaultValue: '', defaultValue: '',
@ -103,6 +153,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
id: 'domainNameFieldMetadataId', id: 'domainNameFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.LINKS, type: FieldMetadataType.LINKS,
icon: 'test-field-icon',
name: 'domainName', name: 'domainName',
label: 'Domain Name', label: 'Domain Name',
defaultValue: '', defaultValue: '',
@ -117,6 +168,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon',
name: 'name', name: 'name',
label: 'Name', label: 'Name',
defaultValue: { defaultValue: {
@ -132,6 +184,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
id: 'domainNameFieldMetadataId', id: 'domainNameFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.LINKS, type: FieldMetadataType.LINKS,
icon: 'test-field-icon',
name: 'domainName', name: 'domainName',
label: 'Domain Name', label: 'Domain Name',
defaultValue: '', defaultValue: '',
@ -151,6 +204,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelSingular: 'Regular Custom Object', labelSingular: 'Regular Custom Object',
labelPlural: 'Regular Custom Objects', labelPlural: 'Regular Custom Objects',
description: 'A regular custom object', description: 'A regular custom object',
icon: 'test-regular-custom-object-icon',
targetTableName: 'DEPRECATED', targetTableName: 'DEPRECATED',
isCustom: true, isCustom: true,
isRemote: false, isRemote: false,
@ -163,13 +217,41 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId', imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId',
workspaceId: '', 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: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: {
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon',
name: 'name', name: 'name',
label: 'Name', label: 'Name',
defaultValue: '', defaultValue: '',
@ -182,6 +264,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
id: 'imageIdentifierFieldMetadataId', id: 'imageIdentifierFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon',
name: 'imageIdentifierFieldName', name: 'imageIdentifierFieldName',
label: 'Image Identifier Field Name', label: 'Image Identifier Field Name',
defaultValue: '', defaultValue: '',
@ -196,6 +279,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon',
name: 'name', name: 'name',
label: 'Name', label: 'Name',
defaultValue: { defaultValue: {
@ -211,6 +295,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
id: 'imageIdentifierFieldMetadataId', id: 'imageIdentifierFieldMetadataId',
objectMetadataId: '', objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon',
name: 'imageIdentifierFieldName', name: 'imageIdentifierFieldName',
label: 'Image Identifier Field Name', label: 'Image Identifier Field Name',
defaultValue: '', defaultValue: '',
@ -230,6 +315,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
labelSingular: '', labelSingular: '',
labelPlural: '', labelPlural: '',
description: '', description: '',
icon: 'test-non-searchable-object-icon',
targetTableName: 'DEPRECATED', targetTableName: 'DEPRECATED',
isCustom: false, isCustom: false,
isRemote: false, isRemote: false,

View File

@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { FileService } from 'src/engine/core-modules/file/services/file.service'; 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 { SearchService } from 'src/engine/core-modules/search/services/search.service';
import { encodeCursorData } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; 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'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';

View File

@ -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 { 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 { 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 { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-metadata.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
export interface FieldMetadataInterface< export interface FieldMetadataInterface<
T extends FieldMetadataType = FieldMetadataType, T extends FieldMetadataType = FieldMetadataType,
@ -21,14 +19,15 @@ export interface FieldMetadataInterface<
objectMetadataId: string; objectMetadataId: string;
workspaceId?: string; workspaceId?: string;
description?: string; description?: string;
icon?: string;
isNullable?: boolean; isNullable?: boolean;
isUnique?: boolean; isUnique?: boolean;
fromRelationMetadata?: RelationMetadataEntity; fromRelationMetadata?: RelationMetadataInterface;
toRelationMetadata?: RelationMetadataEntity; toRelationMetadata?: RelationMetadataInterface;
relationTargetFieldMetadataId?: string; relationTargetFieldMetadataId?: string;
relationTargetFieldMetadata?: FieldMetadataEntity; relationTargetFieldMetadata?: FieldMetadataInterface;
relationTargetObjectMetadataId?: string; relationTargetObjectMetadataId?: string;
relationTargetObjectMetadata?: ObjectMetadataEntity; relationTargetObjectMetadata?: ObjectMetadataInterface;
isCustom?: boolean; isCustom?: boolean;
isSystem?: boolean; isSystem?: boolean;
isActive?: boolean; isActive?: boolean;

View File

@ -14,6 +14,7 @@ export interface ObjectMetadataInterface {
labelSingular: string; labelSingular: string;
labelPlural: string; labelPlural: string;
description?: string; description?: string;
icon?: string;
targetTableName: string; targetTableName: string;
fromRelations: RelationMetadataInterface[]; fromRelations: RelationMetadataInterface[];
toRelations: RelationMetadataInterface[]; toRelations: RelationMetadataInterface[];

View File

@ -7,4 +7,8 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
export const removeFieldMapsFromObjectMetadata = ( export const removeFieldMapsFromObjectMetadata = (
objectMetadata: ObjectMetadataItemWithFieldMaps, objectMetadata: ObjectMetadataItemWithFieldMaps,
): ObjectMetadataInterface => ): ObjectMetadataInterface =>
omit(objectMetadata, ['fieldsById', 'fieldsByName']); omit(objectMetadata, [
'fieldsById',
'fieldsByName',
'fieldsByJoinColumnName',
]);

View File

@ -20,6 +20,11 @@ import {
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity'; import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
export type ObjectMetadataInfo = {
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
};
@Injectable() @Injectable()
export class WorkflowCommonWorkspaceService { export class WorkflowCommonWorkspaceService {
constructor( constructor(
@ -73,13 +78,9 @@ export class WorkflowCommonWorkspaceService {
return { ...workflowVersion, trigger: workflowVersion.trigger }; return { ...workflowVersion, trigger: workflowVersion.trigger };
} }
async getObjectMetadataItemWithFieldsMaps( async getObjectMetadataMaps(
objectNameSingular: string,
workspaceId: string, workspaceId: string,
): Promise<{ ): Promise<ObjectMetadataMaps> {
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
}> {
const currentCacheVersion = const currentCacheVersion =
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
@ -103,6 +104,15 @@ export class WorkflowCommonWorkspaceService {
); );
} }
return objectMetadataMaps;
}
async getObjectMetadataItemWithFieldsMaps(
objectNameSingular: string,
workspaceId: string,
): Promise<ObjectMetadataInfo> {
const objectMetadataMaps = await this.getObjectMetadataMaps(workspaceId);
const objectMetadataItemWithFieldsMaps = const objectMetadataItemWithFieldsMaps =
getObjectMetadataMapItemByNameSingular( getObjectMetadataMapItemByNameSingular(
objectMetadataMaps, objectMetadataMaps,

View File

@ -1,6 +1,6 @@
import { FieldMetadataType } from 'twenty-shared/types'; 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 { 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'; 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', () => { 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 () => { it('should generate fake responses for a form schema', async () => {
const schema: FormFieldMetadata[] = [ const schema: FormFieldMetadata[] = [
{ {
@ -50,11 +41,19 @@ describe('generateFakeFormResponse', () => {
}, },
]; ];
const mockObjectMetadataMaps = {
byId: {
[companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem,
},
idByNameSingular: {
[companyMockObjectMetadataItem.nameSingular]:
companyMockObjectMetadataItem.id,
},
};
const result = await generateFakeFormResponse({ const result = await generateFakeFormResponse({
formMetadata: schema, formMetadata: schema,
workspaceId: '1', objectMetadataMaps: mockObjectMetadataMaps,
// @ts-expect-error legacy noImplicitAny
objectMetadataRepository,
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -82,7 +81,7 @@ describe('generateFakeFormResponse', () => {
isLeaf: true, isLeaf: true,
label: 'Company', label: 'Company',
fieldIdName: 'id', fieldIdName: 'id',
icon: undefined, icon: 'test-company-icon',
nameSingular: 'company', nameSingular: 'company',
value: 'A company', value: 'A company',
}, },

View File

@ -1,7 +1,7 @@
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; 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 { 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 { 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( jest.mock(
'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields', 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields',
@ -12,35 +12,48 @@ describe('generateFakeObjectRecordEvent', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
const mockObjectMetadata = {
icon: 'test-icon',
labelSingular: 'Test Object',
description: 'Test Description',
nameSingular: 'testObject',
} as ObjectMetadataEntity;
const mockFields = { const mockFields = {
field1: { type: 'TEXT', value: 'test' }, field1: { type: 'TEXT', value: 'test' },
field2: { type: 'NUMBER', value: 123 }, 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(() => { beforeEach(() => {
(generateObjectRecordFields as jest.Mock).mockReturnValue(mockFields); (generateObjectRecordFields as jest.Mock).mockReturnValue(mockFields);
}); });
it('should generate record with "after" prefix for CREATED action', () => { it('should generate record with "after" prefix for CREATED action', () => {
const result = generateFakeObjectRecordEvent( const result = generateFakeObjectRecordEvent(
mockObjectMetadata, objectMetadataInfo,
DatabaseEventAction.CREATED, DatabaseEventAction.CREATED,
); );
expect(result).toEqual({ expect(result).toEqual({
object: { object: {
isLeaf: true, isLeaf: true,
icon: 'test-icon', icon: 'test-company-icon',
label: 'Test Object', label: 'Company',
value: 'Test Description', value: 'A company',
nameSingular: 'testObject', nameSingular: 'company',
fieldIdName: 'properties.after.id', fieldIdName: 'properties.after.id',
}, },
fields: { fields: {
@ -53,17 +66,17 @@ describe('generateFakeObjectRecordEvent', () => {
it('should generate record with "after" prefix for UPDATED action', () => { it('should generate record with "after" prefix for UPDATED action', () => {
const result = generateFakeObjectRecordEvent( const result = generateFakeObjectRecordEvent(
mockObjectMetadata, objectMetadataInfo,
DatabaseEventAction.UPDATED, DatabaseEventAction.UPDATED,
); );
expect(result).toEqual({ expect(result).toEqual({
object: { object: {
isLeaf: true, isLeaf: true,
icon: 'test-icon', icon: 'test-company-icon',
label: 'Test Object', label: 'Company',
value: 'Test Description', value: 'A company',
nameSingular: 'testObject', nameSingular: 'company',
fieldIdName: 'properties.after.id', fieldIdName: 'properties.after.id',
}, },
fields: { fields: {
@ -76,17 +89,17 @@ describe('generateFakeObjectRecordEvent', () => {
it('should generate record with "before" prefix for DELETED action', () => { it('should generate record with "before" prefix for DELETED action', () => {
const result = generateFakeObjectRecordEvent( const result = generateFakeObjectRecordEvent(
mockObjectMetadata, objectMetadataInfo,
DatabaseEventAction.DELETED, DatabaseEventAction.DELETED,
); );
expect(result).toEqual({ expect(result).toEqual({
object: { object: {
isLeaf: true, isLeaf: true,
icon: 'test-icon', icon: 'test-company-icon',
label: 'Test Object', label: 'Company',
value: 'Test Description', value: 'A company',
nameSingular: 'testObject', nameSingular: 'company',
fieldIdName: 'properties.before.id', fieldIdName: 'properties.before.id',
}, },
fields: { fields: {
@ -99,17 +112,17 @@ describe('generateFakeObjectRecordEvent', () => {
it('should generate record with "before" prefix for DESTROYED action', () => { it('should generate record with "before" prefix for DESTROYED action', () => {
const result = generateFakeObjectRecordEvent( const result = generateFakeObjectRecordEvent(
mockObjectMetadata, objectMetadataInfo,
DatabaseEventAction.DESTROYED, DatabaseEventAction.DESTROYED,
); );
expect(result).toEqual({ expect(result).toEqual({
object: { object: {
isLeaf: true, isLeaf: true,
icon: 'test-icon', icon: 'test-company-icon',
label: 'Test Object', label: 'Company',
value: 'Test Description', value: 'A company',
nameSingular: 'testObject', nameSingular: 'company',
fieldIdName: 'properties.before.id', fieldIdName: 'properties.before.id',
}, },
fields: { fields: {
@ -123,7 +136,7 @@ describe('generateFakeObjectRecordEvent', () => {
it('should throw error for unknown action', () => { it('should throw error for unknown action', () => {
expect(() => { expect(() => {
generateFakeObjectRecordEvent( generateFakeObjectRecordEvent(
mockObjectMetadata, objectMetadataInfo,
'UNKNOWN' as DatabaseEventAction, 'UNKNOWN' as DatabaseEventAction,
); );
}).toThrow("Unknown action 'UNKNOWN'"); }).toThrow("Unknown action 'UNKNOWN'");

View File

@ -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 { 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 { 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( jest.mock(
'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields', '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', () => { describe('generateFakeObjectRecord', () => {
it('should generate a record with correct object metadata', () => { it('should generate a record with correct object metadata', () => {
const mockObjectMetadata = { const result = generateFakeObjectRecord(objectMetadataInfo);
icon: 'test-icon',
labelSingular: 'Test Object',
description: 'Test Description',
nameSingular: 'testObject',
} as ObjectMetadataEntity;
const result = generateFakeObjectRecord(mockObjectMetadata);
expect(result).toEqual({ expect(result).toEqual({
object: { object: {
isLeaf: true, isLeaf: true,
icon: 'test-icon', icon: 'test-company-icon',
label: 'Test Object', label: 'Company',
value: 'Test Description', value: 'A company',
nameSingular: 'testObject', nameSingular: 'company',
fieldIdName: 'id', fieldIdName: 'id',
}, },
fields: { fields: {
@ -41,15 +53,10 @@ describe('generateFakeObjectRecord', () => {
}); });
it('should call generateObjectRecordFields with the object metadata', () => { it('should call generateObjectRecordFields with the object metadata', () => {
const mockObjectMetadata = { generateFakeObjectRecord(objectMetadataInfo);
icon: 'test-icon',
labelSingular: 'Test Object',
description: 'Test Description',
nameSingular: 'testObject',
} as ObjectMetadataEntity;
generateFakeObjectRecord(mockObjectMetadata); expect(generateObjectRecordFields).toHaveBeenCalledWith({
objectMetadataInfo,
expect(generateObjectRecordFields).toHaveBeenCalledWith(mockObjectMetadata); });
}); });
}); });

View File

@ -1,9 +1,9 @@
import { FieldMetadataType } from 'twenty-shared/types'; 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 { 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 { 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 { 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( jest.mock(
'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field', 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field',
@ -17,38 +17,27 @@ describe('generateObjectRecordFields', () => {
jest.clearAllMocks(); 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', () => { 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( (shouldGenerateFieldFakeValue as jest.Mock).mockImplementation(
(field) => field.type !== FieldMetadataType.RELATION, (field) => field.type !== FieldMetadataType.RELATION,
); );
@ -62,49 +51,34 @@ describe('generateObjectRecordFields', () => {
}), }),
); );
const result = generateObjectRecordFields(mockObjectMetadata); const result = generateObjectRecordFields({ objectMetadataInfo });
expect(result).toEqual({ expect(result).toEqual({
field1: { domainName: {
type: FieldMetadataType.TEXT, icon: 'test-field-icon',
label: 'Field 1', label: 'Domain Name',
icon: 'icon1', type: 'LINKS',
value: 'mock-TEXT', value: 'mock-LINKS',
}, },
field3: { name: {
type: FieldMetadataType.NUMBER, icon: 'test-field-icon',
label: 'Field 3', label: 'Name',
icon: 'icon3', type: 'TEXT',
value: 'mock-NUMBER', value: 'mock-TEXT',
}, },
}); });
expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(3); expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(2);
expect(generateFakeField).toHaveBeenCalledTimes(2); expect(generateFakeField).toHaveBeenCalledTimes(2);
}); });
it('should return empty object when no valid fields', () => { 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); (shouldGenerateFieldFakeValue as jest.Mock).mockReturnValue(false);
const result = generateObjectRecordFields(mockObjectMetadata); const result = generateObjectRecordFields({ objectMetadataInfo });
expect(result).toEqual({}); expect(result).toEqual({});
expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(1); expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(2);
expect(generateFakeField).not.toHaveBeenCalled(); expect(generateFakeField).not.toHaveBeenCalled();
}); });
}); });

View File

@ -19,15 +19,7 @@ export const generateFakeField = ({
}): Leaf | Node => { }): Leaf | Node => {
const compositeType = compositeTypeDefinitions.get(type); const compositeType = compositeTypeDefinitions.get(type);
if (!compositeType) { if (compositeType) {
return {
isLeaf: true,
type: type,
icon: icon,
label: label,
value: generateFakeValue(type, 'FieldMetadataType'),
};
} else {
return { return {
isLeaf: false, isLeaf: false,
icon: icon, icon: icon,
@ -45,4 +37,12 @@ export const generateFakeField = ({
}, {}), }, {}),
}; };
} }
return {
isLeaf: true,
type: type,
icon: icon,
label: label,
value: generateFakeValue(type, 'FieldMetadataType'),
};
}; };

View File

@ -1,7 +1,5 @@
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { import {
Leaf, Leaf,
Node, Node,
@ -9,15 +7,15 @@ import {
import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; 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 { 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 { 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 ({ export const generateFakeFormResponse = async ({
formMetadata, formMetadata,
workspaceId, objectMetadataMaps,
objectMetadataRepository,
}: { }: {
formMetadata: FormFieldMetadata[]; formMetadata: FormFieldMetadata[];
workspaceId: string; objectMetadataMaps: ObjectMetadataMaps;
objectMetadataRepository: Repository<ObjectMetadataEntity>;
}): Promise<Record<string, Leaf | Node>> => { }): Promise<Record<string, Leaf | Node>> => {
const result = await Promise.all( const result = await Promise.all(
formMetadata.map(async (formFieldMetadata) => { formMetadata.map(async (formFieldMetadata) => {
@ -26,19 +24,25 @@ export const generateFakeFormResponse = async ({
return undefined; return undefined;
} }
const objectMetadata = await objectMetadataRepository.findOneOrFail({ const objectMetadataItemWithFieldsMaps =
where: { getObjectMetadataMapItemByNameSingular(
nameSingular: formFieldMetadata?.settings?.objectName, objectMetadataMaps,
workspaceId, formFieldMetadata?.settings?.objectName,
}, );
relations: ['fields'],
}); if (!objectMetadataItemWithFieldsMaps)
throw new Error(
`Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`,
);
return { return {
[formFieldMetadata.name]: { [formFieldMetadata.name]: {
isLeaf: false, isLeaf: false,
label: formFieldMetadata.label, label: formFieldMetadata.label,
value: generateFakeObjectRecord(objectMetadata), value: generateFakeObjectRecord({
objectMetadataMaps,
objectMetadataItemWithFieldsMaps,
}),
}, },
}; };
} else { } else {

View File

@ -1,19 +1,19 @@
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; 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 { import {
BaseOutputSchema, BaseOutputSchema,
RecordOutputSchema, RecordOutputSchema,
} from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; } 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'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
const generateFakeObjectRecordEventWithPrefix = ({ const generateFakeObjectRecordEventWithPrefix = ({
objectMetadataEntity, objectMetadataInfo,
prefix, prefix,
}: { }: {
objectMetadataEntity: ObjectMetadataEntity; objectMetadataInfo: ObjectMetadataInfo;
prefix: string; prefix: string;
}): RecordOutputSchema => { }): RecordOutputSchema => {
const recordFields = generateObjectRecordFields(objectMetadataEntity); const recordFields = generateObjectRecordFields({ objectMetadataInfo });
const prefixedRecordFields = Object.entries(recordFields).reduce( const prefixedRecordFields = Object.entries(recordFields).reduce(
(acc, [key, value]) => { (acc, [key, value]) => {
acc[`${prefix}.${key}`] = value; acc[`${prefix}.${key}`] = value;
@ -26,10 +26,11 @@ const generateFakeObjectRecordEventWithPrefix = ({
return { return {
object: { object: {
isLeaf: true, isLeaf: true,
icon: objectMetadataEntity.icon, icon: objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon,
label: objectMetadataEntity.labelSingular, label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular,
value: objectMetadataEntity.description, value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description,
nameSingular: objectMetadataEntity.nameSingular, nameSingular:
objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular,
fieldIdName: `${prefix}.id`, fieldIdName: `${prefix}.id`,
}, },
fields: prefixedRecordFields, fields: prefixedRecordFields,
@ -38,20 +39,20 @@ const generateFakeObjectRecordEventWithPrefix = ({
}; };
export const generateFakeObjectRecordEvent = ( export const generateFakeObjectRecordEvent = (
objectMetadataEntity: ObjectMetadataEntity, objectMetadataInfo: ObjectMetadataInfo,
action: DatabaseEventAction, action: DatabaseEventAction,
): RecordOutputSchema => { ): RecordOutputSchema => {
switch (action) { switch (action) {
case DatabaseEventAction.CREATED: case DatabaseEventAction.CREATED:
case DatabaseEventAction.UPDATED: case DatabaseEventAction.UPDATED:
return generateFakeObjectRecordEventWithPrefix({ return generateFakeObjectRecordEventWithPrefix({
objectMetadataEntity, objectMetadataInfo,
prefix: 'properties.after', prefix: 'properties.after',
}); });
case DatabaseEventAction.DELETED: case DatabaseEventAction.DELETED:
case DatabaseEventAction.DESTROYED: case DatabaseEventAction.DESTROYED:
return generateFakeObjectRecordEventWithPrefix({ return generateFakeObjectRecordEventWithPrefix({
objectMetadataEntity, objectMetadataInfo,
prefix: 'properties.before', prefix: 'properties.before',
}); });
default: default:

View File

@ -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 { 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 { 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 = ( export const generateFakeObjectRecord = (
objectMetadataEntity: ObjectMetadataEntity, objectMetadataInfo: ObjectMetadataInfo,
): RecordOutputSchema => ({ ): RecordOutputSchema => {
object: { return {
isLeaf: true, object: {
icon: objectMetadataEntity.icon, isLeaf: true,
label: objectMetadataEntity.labelSingular, icon: objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon,
value: objectMetadataEntity.description, label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular,
nameSingular: objectMetadataEntity.nameSingular, value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description,
fieldIdName: 'id', nameSingular:
}, objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular,
fields: generateObjectRecordFields(objectMetadataEntity), fieldIdName: 'id',
_outputSchemaType: 'RECORD', },
}); fields: generateObjectRecordFields({ objectMetadataInfo }),
_outputSchemaType: 'RECORD',
};
};

View File

@ -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 { 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 { 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 { 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 = ( const MAXIMUM_DEPTH = 1;
objectMetadataEntity: ObjectMetadataEntity,
): BaseOutputSchema => export const generateObjectRecordFields = ({
objectMetadataEntity.fields.reduce((acc: BaseOutputSchema, field) => { objectMetadataInfo,
depth = 0,
}: {
objectMetadataInfo: ObjectMetadataInfo;
depth?: number;
}): BaseOutputSchema => {
const objectMetadata = objectMetadataInfo.objectMetadataItemWithFieldsMaps;
return objectMetadata.fields.reduce((acc: BaseOutputSchema, field) => {
if (!shouldGenerateFieldFakeValue(field)) { if (!shouldGenerateFieldFakeValue(field)) {
return acc; return acc;
} }
acc[field.name] = generateFakeField({ if (field.type !== FieldMetadataType.RELATION) {
type: field.type, acc[field.name] = generateFakeField({
label: field.label, type: field.type,
icon: field.icon, 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; return acc;
}, {} as BaseOutputSchema); }, {} as BaseOutputSchema);
};

View File

@ -1,11 +1,18 @@
import { FieldMetadataType } from 'twenty-shared/types'; 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'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const shouldGenerateFieldFakeValue = (field: FieldMetadataEntity) => { const isManyToOneRelationField = (field: FieldMetadataInterface) =>
(field as FieldMetadataEntity<FieldMetadataType.RELATION>).settings
?.relationType === 'MANY_TO_ONE';
export const shouldGenerateFieldFakeValue = (field: FieldMetadataInterface) => {
return ( return (
(!field.isSystem || field.name === 'id') &&
field.isActive && field.isActive &&
field.type !== FieldMetadataType.RELATION (!field.isSystem || field.name === 'id' || field.name === 'userEmail') &&
(field.type !== FieldMetadataType.RELATION ||
isManyToOneRelationField(field))
); );
}; };

View File

@ -1,11 +1,10 @@
import { Module } from '@nestjs/common'; 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 { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata')], imports: [WorkflowCommonModule],
providers: [WorkflowSchemaWorkspaceService], providers: [WorkflowSchemaWorkspaceService],
exports: [WorkflowSchemaWorkspaceService], exports: [WorkflowSchemaWorkspaceService],
}) })

View File

@ -1,12 +1,7 @@
import { Injectable } from '@nestjs/common'; 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 { 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 { 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 { generateFakeValue } from 'src/engine/utils/generate-fake-value';
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; 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'; import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response';
@ -21,12 +16,12 @@ import {
WorkflowTrigger, WorkflowTrigger,
WorkflowTriggerType, WorkflowTriggerType,
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@Injectable() @Injectable()
export class WorkflowSchemaWorkspaceService { export class WorkflowSchemaWorkspaceService {
constructor( constructor(
@InjectRepository(ObjectMetadataEntity, 'metadata') private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {} ) {}
async computeStepOutputSchema({ async computeStepOutputSchema({
@ -43,7 +38,6 @@ export class WorkflowSchemaWorkspaceService {
return this.computeDatabaseEventTriggerOutputSchema({ return this.computeDatabaseEventTriggerOutputSchema({
eventName: step.settings.eventName, eventName: step.settings.eventName,
workspaceId, workspaceId,
objectMetadataRepository: this.objectMetadataRepository,
}); });
} }
case WorkflowTriggerType.MANUAL: { case WorkflowTriggerType.MANUAL: {
@ -56,7 +50,6 @@ export class WorkflowSchemaWorkspaceService {
return this.computeRecordOutputSchema({ return this.computeRecordOutputSchema({
objectType, objectType,
workspaceId, workspaceId,
objectMetadataRepository: this.objectMetadataRepository,
}); });
} }
case WorkflowTriggerType.WEBHOOK: case WorkflowTriggerType.WEBHOOK:
@ -72,19 +65,16 @@ export class WorkflowSchemaWorkspaceService {
return this.computeRecordOutputSchema({ return this.computeRecordOutputSchema({
objectType: step.settings.input.objectName, objectType: step.settings.input.objectName,
workspaceId, workspaceId,
objectMetadataRepository: this.objectMetadataRepository,
}); });
case WorkflowActionType.FIND_RECORDS: case WorkflowActionType.FIND_RECORDS:
return this.computeFindRecordsOutputSchema({ return this.computeFindRecordsOutputSchema({
objectType: step.settings.input.objectName, objectType: step.settings.input.objectName,
workspaceId, workspaceId,
objectMetadataRepository: this.objectMetadataRepository,
}); });
case WorkflowActionType.FORM: case WorkflowActionType.FORM:
return this.computeFormActionOutputSchema({ return this.computeFormActionOutputSchema({
formMetadata: step.settings.input, formMetadata: step.settings.input,
workspaceId, workspaceId,
objectMetadataRepository: this.objectMetadataRepository,
}); });
case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution
default: default:
@ -95,11 +85,9 @@ export class WorkflowSchemaWorkspaceService {
private async computeDatabaseEventTriggerOutputSchema({ private async computeDatabaseEventTriggerOutputSchema({
eventName, eventName,
workspaceId, workspaceId,
objectMetadataRepository,
}: { }: {
eventName: string; eventName: string;
workspaceId: string; workspaceId: string;
objectMetadataRepository: Repository<ObjectMetadataEntity>;
}): Promise<OutputSchema> { }): Promise<OutputSchema> {
const [nameSingular, action] = eventName.split('.'); const [nameSingular, action] = eventName.split('.');
@ -107,20 +95,14 @@ export class WorkflowSchemaWorkspaceService {
return {}; return {};
} }
const objectMetadata = await objectMetadataRepository.findOneOrFail({ const objectMetadataInfo =
where: { await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
nameSingular, nameSingular,
workspaceId, workspaceId,
}, );
relations: ['fields'],
});
if (!isDefined(objectMetadata)) {
return {};
}
return generateFakeObjectRecordEvent( return generateFakeObjectRecordEvent(
objectMetadata, objectMetadataInfo,
action as DatabaseEventAction, action as DatabaseEventAction,
); );
} }
@ -128,16 +110,13 @@ export class WorkflowSchemaWorkspaceService {
private async computeFindRecordsOutputSchema({ private async computeFindRecordsOutputSchema({
objectType, objectType,
workspaceId, workspaceId,
objectMetadataRepository,
}: { }: {
objectType: string; objectType: string;
workspaceId: string; workspaceId: string;
objectMetadataRepository: Repository<ObjectMetadataEntity>;
}): Promise<OutputSchema> { }): Promise<OutputSchema> {
const recordOutputSchema = await this.computeRecordOutputSchema({ const recordOutputSchema = await this.computeRecordOutputSchema({
objectType, objectType,
workspaceId, workspaceId,
objectMetadataRepository,
}); });
return { return {
@ -159,25 +138,17 @@ export class WorkflowSchemaWorkspaceService {
private async computeRecordOutputSchema({ private async computeRecordOutputSchema({
objectType, objectType,
workspaceId, workspaceId,
objectMetadataRepository,
}: { }: {
objectType: string; objectType: string;
workspaceId: string; workspaceId: string;
objectMetadataRepository: Repository<ObjectMetadataEntity>;
}): Promise<OutputSchema> { }): Promise<OutputSchema> {
const objectMetadata = await objectMetadataRepository.findOneOrFail({ const objectMetadataInfo =
where: { await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
nameSingular: objectType, objectType,
workspaceId, workspaceId,
}, );
relations: ['fields'],
});
if (!isDefined(objectMetadata)) { return generateFakeObjectRecord(objectMetadataInfo);
return {};
}
return generateFakeObjectRecord(objectMetadata);
} }
private computeSendEmailActionOutputSchema(): OutputSchema { private computeSendEmailActionOutputSchema(): OutputSchema {
@ -187,16 +158,18 @@ export class WorkflowSchemaWorkspaceService {
private async computeFormActionOutputSchema({ private async computeFormActionOutputSchema({
formMetadata, formMetadata,
workspaceId, workspaceId,
objectMetadataRepository,
}: { }: {
formMetadata: FormFieldMetadata[]; formMetadata: FormFieldMetadata[];
workspaceId: string; workspaceId: string;
objectMetadataRepository: Repository<ObjectMetadataEntity>;
}): Promise<OutputSchema> { }): Promise<OutputSchema> {
const objectMetadataMaps =
await this.workflowCommonWorkspaceService.getObjectMetadataMaps(
workspaceId,
);
return generateFakeFormResponse({ return generateFakeFormResponse({
formMetadata, formMetadata,
workspaceId, objectMetadataMaps,
objectMetadataRepository,
}); });
} }
} }

View File

@ -7,9 +7,14 @@ import { DatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trig
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; 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 { 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 { 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({ @Module({
imports: [FeatureFlagModule, TypeOrmModule.forFeature([Workspace], 'core')], imports: [
FeatureFlagModule,
TypeOrmModule.forFeature([Workspace], 'core'),
WorkflowCommonModule,
],
providers: [ providers: [
AutomatedTriggerWorkspaceService, AutomatedTriggerWorkspaceService,
DatabaseEventTriggerListener, DatabaseEventTriggerListener,

View File

@ -1,5 +1,7 @@
import { Injectable, Logger } from '@nestjs/common'; 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 { 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 { 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'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
@ -17,6 +19,8 @@ import {
WorkflowTriggerJob, WorkflowTriggerJob,
WorkflowTriggerJobData, WorkflowTriggerJobData,
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job'; } 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 { import {
AutomatedTriggerType, AutomatedTriggerType,
WorkflowAutomatedTriggerWorkspaceEntity, WorkflowAutomatedTriggerWorkspaceEntity,
@ -31,43 +35,172 @@ export class DatabaseEventTriggerListener {
@InjectMessageQueue(MessageQueue.workflowQueue) @InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService, private readonly messageQueueService: MessageQueueService,
private readonly isFeatureFlagEnabledService: FeatureFlagService, private readonly isFeatureFlagEnabledService: FeatureFlagService,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {} ) {}
@OnDatabaseBatchEvent('*', DatabaseEventAction.CREATED) @OnDatabaseBatchEvent('*', DatabaseEventAction.CREATED)
async handleObjectRecordCreateEvent( async handleObjectRecordCreateEvent(
payload: WorkspaceEventBatch<ObjectRecordCreateEvent>, payload: WorkspaceEventBatch<ObjectRecordCreateEvent>,
) { ) {
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) @OnDatabaseBatchEvent('*', DatabaseEventAction.UPDATED)
async handleObjectRecordUpdateEvent( async handleObjectRecordUpdateEvent(
payload: WorkspaceEventBatch<ObjectRecordUpdateEvent>, payload: WorkspaceEventBatch<ObjectRecordUpdateEvent>,
) { ) {
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) @OnDatabaseBatchEvent('*', DatabaseEventAction.DELETED)
async handleObjectRecordDeleteEvent( async handleObjectRecordDeleteEvent(
payload: WorkspaceEventBatch<ObjectRecordDeleteEvent>, payload: WorkspaceEventBatch<ObjectRecordDeleteEvent>,
) { ) {
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) @OnDatabaseBatchEvent('*', DatabaseEventAction.DESTROYED)
async handleObjectRecordDestroyEvent( async handleObjectRecordDestroyEvent(
payload: WorkspaceEventBatch<ObjectRecordDestroyEvent>, payload: WorkspaceEventBatch<ObjectRecordDestroyEvent>,
) { ) {
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( private async enrichCreatedEvent(
payload: WorkspaceEventBatch< payload: WorkspaceEventBatch<ObjectRecordCreateEvent>,
| ObjectRecordCreateEvent ) {
| ObjectRecordUpdateEvent const workspaceId = payload.workspaceId;
| ObjectRecordDeleteEvent
| ObjectRecordDestroyEvent for (const event of payload.events) {
>, await this.enrichRecord({
event,
record: event.properties.after,
workspaceId,
});
}
}
private async enrichUpdatedEvent(
payload: WorkspaceEventBatch<ObjectRecordUpdateEvent>,
) {
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<ObjectRecordDeleteEvent>,
) {
const workspaceId = payload.workspaceId;
for (const event of payload.events) {
await this.enrichRecord({
event,
record: event.properties.before,
workspaceId,
});
}
}
private async enrichDestroyedEvent(
payload: WorkspaceEventBatch<ObjectRecordDestroyEvent>,
) {
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<string, any>;
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<ObjectRecordNonDestructiveEvent>,
) { ) {
const workspaceId = payload.workspaceId; const workspaceId = payload.workspaceId;
const databaseEventName = payload.name; const databaseEventName = payload.name;
@ -79,7 +212,7 @@ export class DatabaseEventTriggerListener {
)}`, )}`,
); );
return; return true;
} }
const isWorkflowEnabled = const isWorkflowEnabled =
@ -88,9 +221,14 @@ export class DatabaseEventTriggerListener {
workspaceId, workspaceId,
); );
if (!isWorkflowEnabled) { return !isWorkflowEnabled;
return; }
}
private async handleEvent(
payload: WorkspaceEventBatch<ObjectRecordNonDestructiveEvent>,
) {
const workspaceId = payload.workspaceId;
const databaseEventName = payload.name;
const workflowAutomatedTriggerRepository = const workflowAutomatedTriggerRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowAutomatedTriggerWorkspaceEntity>( await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowAutomatedTriggerWorkspaceEntity>(
@ -107,7 +245,7 @@ export class DatabaseEventTriggerListener {
for (const eventListener of eventListeners) { for (const eventListener of eventListeners) {
for (const eventPayload of payload.events) { for (const eventPayload of payload.events) {
this.messageQueueService.add<WorkflowTriggerJobData>( await this.messageQueueService.add<WorkflowTriggerJobData>(
WorkflowTriggerJob.name, WorkflowTriggerJob.name,
{ {
workspaceId, workspaceId,