Deprecate object metadata maps in favor of flat entities (#16080)
## Context Deprecating the old objectMetadataMap type in favour of split flat entities to match with our new caching. In the long run, trying to achieve: - Better performance through caching - Consistent data access patterns across the codebase - Reduced database queries Now that everything is based on flat entities, which are cached, we can finish the refactoring of workspace context cache which should already improve performances. Then the last step will be to consume that new cache in the new global datasource to get rid of the many workspace datasources stored in the server
This commit is contained in:
+37
-39
@@ -2,13 +2,12 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
|
||||
import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type 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';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import {
|
||||
@@ -32,8 +31,9 @@ import {
|
||||
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
|
||||
|
||||
export type ObjectMetadataInfo = {
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -95,55 +95,52 @@ export class WorkflowCommonWorkspaceService {
|
||||
return { ...workflowVersion, trigger: workflowVersion.trigger };
|
||||
}
|
||||
|
||||
async getObjectMetadataMaps(
|
||||
workspaceId: string,
|
||||
): Promise<ObjectMetadataMaps> {
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } =
|
||||
async getFlatEntityMaps(workspaceId: string): Promise<{
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
objectIdByNameSingular: Record<string, string>;
|
||||
}> {
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
],
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const { idByNameSingular } = buildObjectIdByNameMaps(
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
const objectMetadataMaps: ObjectMetadataMaps = {
|
||||
byId: {},
|
||||
idByNameSingular,
|
||||
|
||||
return {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular: idByNameSingular,
|
||||
};
|
||||
|
||||
for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) {
|
||||
if (isDefined(flatObj)) {
|
||||
objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps(
|
||||
flatObj,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return objectMetadataMaps;
|
||||
}
|
||||
|
||||
async getObjectMetadataItemWithFieldsMaps(
|
||||
async getObjectMetadataInfo(
|
||||
objectNameSingular: string,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectMetadataInfo> {
|
||||
const objectMetadataMaps = await this.getObjectMetadataMaps(workspaceId);
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
} = await this.getFlatEntityMaps(workspaceId);
|
||||
|
||||
const objectMetadataItemWithFieldsMaps =
|
||||
getObjectMetadataMapItemByNameSingular(
|
||||
objectMetadataMaps,
|
||||
objectNameSingular,
|
||||
const objectId = objectIdByNameSingular[objectNameSingular];
|
||||
|
||||
if (!isDefined(objectId)) {
|
||||
throw new WorkflowCommonException(
|
||||
`Failed to read: Object ${objectNameSingular} not found`,
|
||||
WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!objectMetadataItemWithFieldsMaps) {
|
||||
const flatObjectMetadata = flatObjectMetadataMaps.byId[objectId];
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
throw new WorkflowCommonException(
|
||||
`Failed to read: Object ${objectNameSingular} not found`,
|
||||
WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
@@ -151,8 +148,9 @@ export class WorkflowCommonWorkspaceService {
|
||||
}
|
||||
|
||||
return {
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+9
-17
@@ -1,13 +1,13 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||
import {
|
||||
mockFlatFieldMetadataMaps,
|
||||
mockFlatObjectMetadataMaps,
|
||||
mockObjectIdByNameSingular,
|
||||
} from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||
import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response';
|
||||
import { type FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||
|
||||
const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
)!;
|
||||
|
||||
describe('generateFakeFormResponse', () => {
|
||||
it('should generate fake responses for a form schema', async () => {
|
||||
const formFieldMetadataItems: FormFieldMetadata[] = [
|
||||
@@ -41,19 +41,11 @@ describe('generateFakeFormResponse', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const mockObjectMetadataMaps = {
|
||||
byId: {
|
||||
[companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem,
|
||||
},
|
||||
idByNameSingular: {
|
||||
[companyMockObjectMetadataItem.nameSingular]:
|
||||
companyMockObjectMetadataItem.id,
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateFakeFormResponse({
|
||||
formFieldMetadataItems,
|
||||
objectMetadataMaps: mockObjectMetadataMaps,
|
||||
flatObjectMetadataMaps: mockFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps: mockFlatFieldMetadataMaps,
|
||||
objectIdByNameSingular: mockObjectIdByNameSingular,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -103,7 +95,7 @@ describe('generateFakeFormResponse', () => {
|
||||
},
|
||||
},
|
||||
name: {
|
||||
fieldMetadataId: 'nameFieldMetadataId',
|
||||
fieldMetadataId: 'nameFieldMetadataId-company',
|
||||
icon: 'test-field-icon',
|
||||
isLeaf: true,
|
||||
label: 'Name',
|
||||
|
||||
+7
-27
@@ -1,5 +1,5 @@
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||
import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||
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';
|
||||
|
||||
@@ -25,33 +25,13 @@ describe('generateFakeObjectRecordEvent', () => {
|
||||
},
|
||||
};
|
||||
|
||||
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(
|
||||
objectMetadataInfo,
|
||||
mockCompanyObjectMetadataInfo,
|
||||
DatabaseEventAction.CREATED,
|
||||
);
|
||||
|
||||
@@ -82,7 +62,7 @@ describe('generateFakeObjectRecordEvent', () => {
|
||||
|
||||
it('should generate record with "after" prefix for UPDATED action', () => {
|
||||
const result = generateFakeObjectRecordEvent(
|
||||
objectMetadataInfo,
|
||||
mockCompanyObjectMetadataInfo,
|
||||
DatabaseEventAction.UPDATED,
|
||||
);
|
||||
|
||||
@@ -113,7 +93,7 @@ describe('generateFakeObjectRecordEvent', () => {
|
||||
|
||||
it('should generate record with "before" prefix for DELETED action', () => {
|
||||
const result = generateFakeObjectRecordEvent(
|
||||
objectMetadataInfo,
|
||||
mockCompanyObjectMetadataInfo,
|
||||
DatabaseEventAction.DELETED,
|
||||
);
|
||||
|
||||
@@ -144,7 +124,7 @@ describe('generateFakeObjectRecordEvent', () => {
|
||||
|
||||
it('should generate record with "before" prefix for DESTROYED action', () => {
|
||||
const result = generateFakeObjectRecordEvent(
|
||||
objectMetadataInfo,
|
||||
mockCompanyObjectMetadataInfo,
|
||||
DatabaseEventAction.DESTROYED,
|
||||
);
|
||||
|
||||
@@ -175,7 +155,7 @@ describe('generateFakeObjectRecordEvent', () => {
|
||||
|
||||
it('should generate record with "after" prefix for UPSERTED action', () => {
|
||||
const result = generateFakeObjectRecordEvent(
|
||||
objectMetadataInfo,
|
||||
mockCompanyObjectMetadataInfo,
|
||||
DatabaseEventAction.UPSERTED,
|
||||
);
|
||||
|
||||
@@ -207,7 +187,7 @@ describe('generateFakeObjectRecordEvent', () => {
|
||||
it('should throw error for unknown action', () => {
|
||||
expect(() => {
|
||||
generateFakeObjectRecordEvent(
|
||||
objectMetadataInfo,
|
||||
mockCompanyObjectMetadataInfo,
|
||||
'UNKNOWN' as DatabaseEventAction,
|
||||
);
|
||||
}).toThrow("Unknown action 'UNKNOWN'");
|
||||
|
||||
+8
-23
@@ -1,4 +1,4 @@
|
||||
import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||
import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||
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';
|
||||
|
||||
@@ -12,28 +12,11 @@ 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 result = generateFakeObjectRecord({ objectMetadataInfo });
|
||||
const result = generateFakeObjectRecord({
|
||||
objectMetadataInfo: mockCompanyObjectMetadataInfo,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
object: {
|
||||
@@ -53,10 +36,12 @@ describe('generateFakeObjectRecord', () => {
|
||||
});
|
||||
|
||||
it('should call generateObjectRecordFields with the object metadata', () => {
|
||||
generateFakeObjectRecord({ objectMetadataInfo });
|
||||
generateFakeObjectRecord({
|
||||
objectMetadataInfo: mockCompanyObjectMetadataInfo,
|
||||
});
|
||||
|
||||
expect(generateObjectRecordFields).toHaveBeenCalledWith({
|
||||
objectMetadataInfo,
|
||||
objectMetadataInfo: mockCompanyObjectMetadataInfo,
|
||||
depth: 0,
|
||||
maxDepth: 1,
|
||||
});
|
||||
|
||||
+7
-23
@@ -1,6 +1,6 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||
import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||
import { generateFakeRecordField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-record-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';
|
||||
@@ -17,26 +17,6 @@ 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', () => {
|
||||
(shouldGenerateFieldFakeValue as jest.Mock).mockImplementation(
|
||||
(field) => field.type !== FieldMetadataType.RELATION,
|
||||
@@ -51,7 +31,9 @@ describe('generateObjectRecordFields', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const result = generateObjectRecordFields({ objectMetadataInfo });
|
||||
const result = generateObjectRecordFields({
|
||||
objectMetadataInfo: mockCompanyObjectMetadataInfo,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
domainName: {
|
||||
@@ -75,7 +57,9 @@ describe('generateObjectRecordFields', () => {
|
||||
it('should return empty object when no valid fields', () => {
|
||||
(shouldGenerateFieldFakeValue as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = generateObjectRecordFields({ objectMetadataInfo });
|
||||
const result = generateObjectRecordFields({
|
||||
objectMetadataInfo: mockCompanyObjectMetadataInfo,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(2);
|
||||
|
||||
+16
-43
@@ -1,99 +1,72 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
|
||||
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
|
||||
import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
describe('shouldGenerateFieldFakeValue', () => {
|
||||
it('should return true for active non-system fields', () => {
|
||||
const field = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
const field = getFlatFieldMetadataMock({
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000002',
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isNullable: true,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
universalIdentifier: 'test-field-universal-id',
|
||||
});
|
||||
|
||||
expect(shouldGenerateFieldFakeValue(field)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for system id field', () => {
|
||||
const field = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
const field = getFlatFieldMetadataMock({
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000003',
|
||||
isSystem: true,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'id',
|
||||
label: 'ID',
|
||||
isNullable: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
universalIdentifier: 'id-field-universal-id',
|
||||
});
|
||||
|
||||
expect(shouldGenerateFieldFakeValue(field)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for inactive fields', () => {
|
||||
const field = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
const field = getFlatFieldMetadataMock({
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000004',
|
||||
isSystem: false,
|
||||
isActive: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isNullable: true,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
universalIdentifier: 'inactive-field-universal-id',
|
||||
});
|
||||
|
||||
expect(shouldGenerateFieldFakeValue(field)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for system fields', () => {
|
||||
const field = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
const field = getFlatFieldMetadataMock({
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000005',
|
||||
isSystem: true,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isNullable: true,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
universalIdentifier: 'system-field-universal-id',
|
||||
});
|
||||
|
||||
expect(shouldGenerateFieldFakeValue(field)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for relation fields', () => {
|
||||
const field = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
it('should return false for many-to-one relation fields', () => {
|
||||
const field = getFlatFieldMetadataMock({
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000006',
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isNullable: true,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
universalIdentifier: 'relation-field-universal-id',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
},
|
||||
});
|
||||
|
||||
expect(shouldGenerateFieldFakeValue(field)).toBe(false);
|
||||
|
||||
+23
-12
@@ -1,7 +1,8 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type 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';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import {
|
||||
type Leaf,
|
||||
type Node,
|
||||
@@ -12,12 +13,16 @@ import { type FormFieldMetadata } from 'src/modules/workflow/workflow-executor/w
|
||||
|
||||
type GenerateFakeFormResponseArgs = {
|
||||
formFieldMetadataItems: FormFieldMetadata[];
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
objectIdByNameSingular: Record<string, string>;
|
||||
};
|
||||
|
||||
export const generateFakeFormResponse = ({
|
||||
formFieldMetadataItems,
|
||||
objectMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
}: GenerateFakeFormResponseArgs): Record<string, Leaf | Node> => {
|
||||
const result = formFieldMetadataItems.map((formFieldMetadata) => {
|
||||
if (formFieldMetadata.type === 'RECORD') {
|
||||
@@ -25,13 +30,18 @@ export const generateFakeFormResponse = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const objectMetadataItemWithFieldsMaps =
|
||||
getObjectMetadataMapItemByNameSingular(
|
||||
objectMetadataMaps,
|
||||
formFieldMetadata?.settings?.objectName,
|
||||
);
|
||||
const objectId =
|
||||
objectIdByNameSingular[formFieldMetadata?.settings?.objectName];
|
||||
|
||||
if (!isDefined(objectMetadataItemWithFieldsMaps)) {
|
||||
if (!isDefined(objectId)) {
|
||||
throw new Error(
|
||||
`Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const flatObjectMetadata = flatObjectMetadataMaps.byId[objectId];
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
throw new Error(
|
||||
`Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`,
|
||||
);
|
||||
@@ -43,8 +53,9 @@ export const generateFakeFormResponse = ({
|
||||
label: formFieldMetadata.label,
|
||||
value: generateFakeObjectRecord({
|
||||
objectMetadataInfo: {
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
+5
-5
@@ -13,6 +13,7 @@ const generateFakeObjectRecordEventWithPrefix = ({
|
||||
objectMetadataInfo: ObjectMetadataInfo;
|
||||
prefix: string;
|
||||
}): RecordOutputSchema => {
|
||||
const { flatObjectMetadata } = objectMetadataInfo;
|
||||
const recordFields = generateObjectRecordFields({ objectMetadataInfo });
|
||||
const prefixedRecordFields = Object.entries(recordFields).reduce(
|
||||
(acc, [key, value]) => {
|
||||
@@ -26,12 +27,11 @@ const generateFakeObjectRecordEventWithPrefix = ({
|
||||
return {
|
||||
object: {
|
||||
isLeaf: true,
|
||||
icon:
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon ?? undefined,
|
||||
label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular,
|
||||
value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description,
|
||||
icon: flatObjectMetadata.icon ?? undefined,
|
||||
label: flatObjectMetadata.labelSingular,
|
||||
value: flatObjectMetadata.description,
|
||||
fieldIdName: `${prefix}.id`,
|
||||
objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id,
|
||||
objectMetadataId: flatObjectMetadata.id,
|
||||
},
|
||||
fields: prefixedRecordFields,
|
||||
_outputSchemaType: 'RECORD',
|
||||
|
||||
+6
-5
@@ -11,15 +11,16 @@ export const generateFakeObjectRecord = ({
|
||||
depth?: number;
|
||||
maxDepth?: number;
|
||||
}): RecordOutputSchema => {
|
||||
const { flatObjectMetadata } = objectMetadataInfo;
|
||||
|
||||
return {
|
||||
object: {
|
||||
isLeaf: true,
|
||||
icon:
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon ?? undefined,
|
||||
label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular,
|
||||
value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description,
|
||||
icon: flatObjectMetadata.icon ?? undefined,
|
||||
label: flatObjectMetadata.labelSingular,
|
||||
value: flatObjectMetadata.description,
|
||||
fieldIdName: 'id',
|
||||
objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id,
|
||||
objectMetadataId: flatObjectMetadata.id,
|
||||
},
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo,
|
||||
|
||||
+63
-58
@@ -1,6 +1,7 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { type FieldOutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record';
|
||||
@@ -17,67 +18,71 @@ export const generateObjectRecordFields = ({
|
||||
depth?: number;
|
||||
maxDepth?: number;
|
||||
}): Record<string, FieldOutputSchema> => {
|
||||
const objectMetadata = objectMetadataInfo.objectMetadataItemWithFieldsMaps;
|
||||
const { flatObjectMetadata, flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
objectMetadataInfo;
|
||||
|
||||
return Object.values(objectMetadata.fieldsById).reduce(
|
||||
(acc: Record<string, FieldOutputSchema>, field) => {
|
||||
if (!shouldGenerateFieldFakeValue(field)) {
|
||||
return acc;
|
||||
const result: Record<string, FieldOutputSchema> = {};
|
||||
|
||||
for (const fieldId of flatObjectMetadata.fieldMetadataIds) {
|
||||
const field = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: fieldId,
|
||||
});
|
||||
|
||||
if (!shouldGenerateFieldFakeValue(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.type !== FieldMetadataType.RELATION) {
|
||||
result[field.name] = generateFakeRecordField({
|
||||
type: field.type,
|
||||
label: field.label,
|
||||
icon: field.icon ?? undefined,
|
||||
fieldMetadataId: field.id,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDefined(field.relationTargetObjectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth < maxDepth) {
|
||||
const relationTargetObjectMetadata =
|
||||
flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId];
|
||||
|
||||
if (!isDefined(relationTargetObjectMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.type !== FieldMetadataType.RELATION) {
|
||||
acc[field.name] = generateFakeRecordField({
|
||||
type: field.type,
|
||||
label: field.label,
|
||||
icon: field.icon ?? undefined,
|
||||
fieldMetadataId: field.id,
|
||||
});
|
||||
result[field.name] = {
|
||||
isLeaf: false,
|
||||
icon: field.icon ?? undefined,
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
fieldMetadataId: field.id,
|
||||
value: generateFakeObjectRecord({
|
||||
objectMetadataInfo: {
|
||||
flatObjectMetadata: relationTargetObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
depth: depth + 1,
|
||||
}),
|
||||
};
|
||||
} else if (depth === maxDepth) {
|
||||
const relationIdFieldName = `${field.name}Id`;
|
||||
const relationIdFieldLabel = camelToTitleCase(relationIdFieldName);
|
||||
|
||||
return acc;
|
||||
}
|
||||
result[relationIdFieldName] = generateFakeRecordField({
|
||||
type: FieldMetadataType.UUID,
|
||||
label: relationIdFieldLabel,
|
||||
icon: field.icon ?? undefined,
|
||||
fieldMetadataId: field.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(field.relationTargetObjectMetadataId)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (depth < maxDepth) {
|
||||
const relationTargetObjectMetadata =
|
||||
objectMetadataInfo.objectMetadataMaps.byId[
|
||||
field.relationTargetObjectMetadataId
|
||||
];
|
||||
|
||||
if (!isDefined(relationTargetObjectMetadata)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[field.name] = {
|
||||
isLeaf: false,
|
||||
icon: field.icon ?? undefined,
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
fieldMetadataId: field.id,
|
||||
value: generateFakeObjectRecord({
|
||||
objectMetadataInfo: {
|
||||
objectMetadataItemWithFieldsMaps: relationTargetObjectMetadata,
|
||||
objectMetadataMaps: objectMetadataInfo.objectMetadataMaps,
|
||||
},
|
||||
depth: depth + 1,
|
||||
}),
|
||||
};
|
||||
} else if (depth === maxDepth) {
|
||||
const relationIdFieldName = `${field.name}Id`;
|
||||
const relationIdFieldLabel = camelToTitleCase(relationIdFieldName);
|
||||
|
||||
acc[relationIdFieldName] = generateFakeRecordField({
|
||||
type: FieldMetadataType.UUID,
|
||||
label: relationIdFieldLabel,
|
||||
icon: field.icon ?? undefined,
|
||||
fieldMetadataId: field.id,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, FieldOutputSchema>,
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
+12
-11
@@ -1,21 +1,22 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
|
||||
const isManyToOneRelationField = (field: FieldMetadataEntity) =>
|
||||
(field as FieldMetadataEntity<FieldMetadataType.RELATION>).settings
|
||||
?.relationType === 'MANY_TO_ONE';
|
||||
const isManyToOneRelationField = (field: FlatFieldMetadata) => {
|
||||
if (isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION)) {
|
||||
return field.settings?.relationType === RelationType.MANY_TO_ONE;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const EXCLUDED_SYSTEM_FIELDS = ['searchVector', 'position'];
|
||||
|
||||
// TODO refactor
|
||||
export const shouldGenerateFieldFakeValue = <T extends FieldMetadataType>(
|
||||
field: FieldMetadataEntity<T>,
|
||||
) => {
|
||||
export const shouldGenerateFieldFakeValue = (field: FlatFieldMetadata) => {
|
||||
return (
|
||||
field.isActive &&
|
||||
!(EXCLUDED_SYSTEM_FIELDS.includes(field.name) && field.isSystem) &&
|
||||
(field.type !== FieldMetadataType.RELATION ||
|
||||
isManyToOneRelationField(field as unknown as FieldMetadataEntity))
|
||||
!isManyToOneRelationField(field)
|
||||
);
|
||||
};
|
||||
|
||||
+22
-21
@@ -216,7 +216,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
}
|
||||
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
@@ -241,14 +241,14 @@ export class WorkflowSchemaWorkspaceService {
|
||||
});
|
||||
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
objectType,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const first: Node = {
|
||||
isLeaf: false,
|
||||
label: `First ${objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular ?? 'Record'}`,
|
||||
label: `First ${objectMetadataInfo.flatObjectMetadata.labelSingular ?? 'Record'}`,
|
||||
icon: 'IconAlpha',
|
||||
type: 'object',
|
||||
value: recordOutputSchema,
|
||||
@@ -256,7 +256,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
|
||||
const all: Leaf = {
|
||||
isLeaf: true,
|
||||
label: `All ${objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural ?? 'Records'}`,
|
||||
label: `All ${objectMetadataInfo.flatObjectMetadata.labelPlural ?? 'Records'}`,
|
||||
type: 'array',
|
||||
icon: 'IconListDetails',
|
||||
value: 'Returns an array of records',
|
||||
@@ -283,7 +283,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
maxDepth?: number;
|
||||
}): Promise<OutputSchema> {
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
objectType,
|
||||
workspaceId,
|
||||
);
|
||||
@@ -302,14 +302,18 @@ export class WorkflowSchemaWorkspaceService {
|
||||
formFieldMetadataItems: FormFieldMetadata[];
|
||||
workspaceId: string;
|
||||
}): Promise<OutputSchema> {
|
||||
const objectMetadataMaps =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataMaps(
|
||||
workspaceId,
|
||||
);
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
} =
|
||||
await this.workflowCommonWorkspaceService.getFlatEntityMaps(workspaceId);
|
||||
|
||||
return generateFakeFormResponse({
|
||||
formFieldMetadataItems,
|
||||
objectMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -337,20 +341,18 @@ export class WorkflowSchemaWorkspaceService {
|
||||
|
||||
if (availability.type === 'BULK_RECORDS') {
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
availability.objectNameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
[objectMetadataInfo.objectMetadataItemWithFieldsMaps.namePlural]: {
|
||||
label:
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural,
|
||||
[objectMetadataInfo.flatObjectMetadata.namePlural]: {
|
||||
label: objectMetadataInfo.flatObjectMetadata.labelPlural,
|
||||
isLeaf: true,
|
||||
type: 'array',
|
||||
value:
|
||||
'Array of ' +
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural,
|
||||
'Array of ' + objectMetadataInfo.flatObjectMetadata.labelPlural,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -417,7 +419,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
case WorkflowTriggerType.MANUAL: {
|
||||
if (trigger.settings.availability?.type === 'BULK_RECORDS') {
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
trigger.settings.availability.objectNameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
@@ -425,8 +427,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
return {
|
||||
label:
|
||||
'Current Item (' +
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps
|
||||
.labelSingular +
|
||||
objectMetadataInfo.flatObjectMetadata.labelSingular +
|
||||
')',
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
@@ -468,7 +469,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
switch (step.type) {
|
||||
case WorkflowActionType.FIND_RECORDS: {
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
step.settings.input.objectName,
|
||||
workspaceId,
|
||||
);
|
||||
@@ -476,7 +477,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
return {
|
||||
label:
|
||||
'Current Item (' +
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular +
|
||||
objectMetadataInfo.flatObjectMetadata.labelSingular +
|
||||
')',
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
|
||||
+6
-5
@@ -6,6 +6,7 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
||||
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
@@ -26,7 +27,6 @@ import {
|
||||
type WorkflowEmptyAction,
|
||||
type WorkflowFormAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = {
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
@@ -460,14 +460,15 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
isValidUuid(response[key].id)
|
||||
) {
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
const { flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
field.settings.objectName,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const relationFieldsNames = Object.values(
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.fieldsById,
|
||||
const relationFieldsNames = getFlatFieldsFromFlatObjectMetadata(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
)
|
||||
.filter((field) => field.type === FieldMetadataType.RELATION)
|
||||
.map((field) => field.name);
|
||||
|
||||
+41
-59
@@ -1,13 +1,13 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { WorkflowDatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener';
|
||||
import { WorkflowTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
|
||||
import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock';
|
||||
import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock';
|
||||
|
||||
describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
let listener: WorkflowDatabaseEventTriggerListener;
|
||||
@@ -18,6 +18,35 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const createMockFlatObjectMetadata = (
|
||||
overrides: Partial<FlatObjectMetadata>,
|
||||
): FlatObjectMetadata =>
|
||||
({
|
||||
id: 'test-object-metadata',
|
||||
workspaceId: 'test-workspace',
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
labelSingular: 'Test Object',
|
||||
labelPlural: 'Test Objects',
|
||||
description: 'Test object for testing',
|
||||
targetTableName: 'test_objects',
|
||||
isSystem: false,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isRemote: false,
|
||||
isAuditLogged: true,
|
||||
isSearchable: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
icon: 'Icon123',
|
||||
universalIdentifier: 'test-object-metadata',
|
||||
fieldMetadataIds: [],
|
||||
indexMetadataIds: [],
|
||||
viewIds: [],
|
||||
applicationId: null,
|
||||
...overrides,
|
||||
}) as FlatObjectMetadata;
|
||||
|
||||
beforeEach(async () => {
|
||||
twentyORMGlobalManager = {
|
||||
getRepositoryForWorkspace: jest.fn().mockResolvedValue(mockRepository),
|
||||
@@ -46,37 +75,10 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
provide: WorkflowCommonWorkspaceService,
|
||||
useValue: {
|
||||
getWorkflowById: jest.fn(),
|
||||
getObjectMetadataItemWithFieldsMaps: jest.fn().mockResolvedValue({
|
||||
objectMetadataMaps: {
|
||||
byId: {
|
||||
'test-object-metadata': {
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
},
|
||||
},
|
||||
},
|
||||
objectMetadataItemWithFieldsMaps:
|
||||
getMockObjectMetadataItemWithFieldsMaps({
|
||||
id: 'test-object-metadata',
|
||||
workspaceId: 'test-workspace',
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
labelSingular: 'Test Object',
|
||||
labelPlural: 'Test Objects',
|
||||
description: 'Test object for testing',
|
||||
indexMetadatas: [],
|
||||
targetTableName: 'test_objects',
|
||||
isSystem: false,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isRemote: false,
|
||||
isAuditLogged: true,
|
||||
isSearchable: true,
|
||||
icon: 'Icon123',
|
||||
fieldIdByJoinColumnName: {},
|
||||
fieldsById: {},
|
||||
fieldIdByName: {},
|
||||
}),
|
||||
getObjectMetadataInfo: jest.fn().mockResolvedValue({
|
||||
flatObjectMetadata: createMockFlatObjectMetadata({}),
|
||||
flatObjectMetadataMaps: { byId: {}, byName: {} },
|
||||
flatFieldMetadataMaps: { byId: {}, byName: {} },
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -93,30 +95,10 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
const databaseEventName = 'testEvent';
|
||||
const workflowId = 'test-workflow';
|
||||
|
||||
const mockPayload = {
|
||||
const mockPayload: WorkspaceEventBatch<any> = {
|
||||
workspaceId,
|
||||
name: databaseEventName,
|
||||
objectMetadata: getMockObjectMetadataEntity({
|
||||
id: 'test-object-metadata',
|
||||
workspaceId,
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
labelSingular: 'Test Object',
|
||||
labelPlural: 'Test Objects',
|
||||
description: 'Test object for testing',
|
||||
targetTableName: 'test_objects',
|
||||
isSystem: false,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isRemote: false,
|
||||
isAuditLogged: true,
|
||||
isSearchable: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
fields: [],
|
||||
indexMetadatas: [],
|
||||
icon: 'Icon123',
|
||||
}),
|
||||
objectMetadata: createMockFlatObjectMetadata({}),
|
||||
events: [
|
||||
{
|
||||
recordId: 'test-record',
|
||||
@@ -205,7 +187,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
});
|
||||
|
||||
it('should handle create events correctly', async () => {
|
||||
const createPayload = {
|
||||
const createPayload: WorkspaceEventBatch<any> = {
|
||||
...mockPayload,
|
||||
name: 'createEvent',
|
||||
events: [
|
||||
@@ -242,7 +224,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
});
|
||||
|
||||
it('should handle delete events correctly', async () => {
|
||||
const deletePayload = {
|
||||
const deletePayload: WorkspaceEventBatch<any> = {
|
||||
...mockPayload,
|
||||
name: 'deleteEvent',
|
||||
events: [
|
||||
@@ -279,7 +261,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
});
|
||||
|
||||
it('should handle destroy events correctly', async () => {
|
||||
const destroyPayload = {
|
||||
const destroyPayload: WorkspaceEventBatch<any> = {
|
||||
...mockPayload,
|
||||
name: 'destroyEvent',
|
||||
events: [
|
||||
@@ -316,7 +298,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
});
|
||||
|
||||
it('should handle multiple events in a batch', async () => {
|
||||
const batchPayload = {
|
||||
const batchPayload: WorkspaceEventBatch<any> = {
|
||||
...mockPayload,
|
||||
events: [
|
||||
mockPayload.events[0],
|
||||
|
||||
+70
-40
@@ -15,8 +15,11 @@ import { type ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emit
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import {
|
||||
@@ -135,17 +138,21 @@ export class WorkflowDatabaseEventTriggerListener {
|
||||
payload: WorkspaceEventBatch<ObjectRecordCreateEvent>,
|
||||
) {
|
||||
const workspaceId = payload.workspaceId;
|
||||
const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
payload.objectMetadata.nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
} = await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
payload.objectMetadata.nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.enrichRecordsWithRelations({
|
||||
records: payload.events.map((event) => event.properties.after),
|
||||
workspaceId,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,23 +160,28 @@ export class WorkflowDatabaseEventTriggerListener {
|
||||
payload: WorkspaceEventBatch<ObjectRecordUpdateEvent>,
|
||||
) {
|
||||
const workspaceId = payload.workspaceId;
|
||||
const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
payload.objectMetadata.nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
} = await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
payload.objectMetadata.nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.enrichRecordsWithRelations({
|
||||
records: payload.events.map((event) => event.properties.before),
|
||||
workspaceId,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
await this.enrichRecordsWithRelations({
|
||||
records: payload.events.map((event) => event.properties.after),
|
||||
workspaceId,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -177,17 +189,21 @@ export class WorkflowDatabaseEventTriggerListener {
|
||||
payload: WorkspaceEventBatch<ObjectRecordDeleteEvent>,
|
||||
) {
|
||||
const workspaceId = payload.workspaceId;
|
||||
const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
payload.objectMetadata.nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
} = await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
payload.objectMetadata.nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.enrichRecordsWithRelations({
|
||||
records: payload.events.map((event) => event.properties.before),
|
||||
workspaceId,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,36 +211,50 @@ export class WorkflowDatabaseEventTriggerListener {
|
||||
payload: WorkspaceEventBatch<ObjectRecordDestroyEvent>,
|
||||
) {
|
||||
const workspaceId = payload.workspaceId;
|
||||
const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
payload.objectMetadata.nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
} = await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
|
||||
payload.objectMetadata.nameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.enrichRecordsWithRelations({
|
||||
records: payload.events.map((event) => event.properties.before),
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
private async enrichRecordsWithRelations({
|
||||
records,
|
||||
workspaceId,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
records: Partial<ObjectRecord>[];
|
||||
workspaceId: string;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}) {
|
||||
const { fieldIdByJoinColumnName } = buildFieldMapsFromFlatObjectMetadata(
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
);
|
||||
|
||||
for (const [joinColumnName, joinFieldId] of Object.entries(
|
||||
objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName,
|
||||
fieldIdByJoinColumnName,
|
||||
)) {
|
||||
const joinField =
|
||||
objectMetadataItemWithFieldsMaps.fieldsById[joinFieldId];
|
||||
const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: joinFieldId,
|
||||
});
|
||||
|
||||
const joinRecordIds = records
|
||||
.map((record) => record[joinColumnName])
|
||||
.filter(isDefined);
|
||||
@@ -240,7 +270,7 @@ export class WorkflowDatabaseEventTriggerListener {
|
||||
}
|
||||
|
||||
const relatedObjectMetadataNameSingular =
|
||||
objectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular;
|
||||
flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular;
|
||||
|
||||
if (!isDefined(relatedObjectMetadataNameSingular)) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user