From 4b25aabfa276d555b910b915a7a63f285c1b907a Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 27 May 2025 15:57:28 +0200 Subject: [PATCH] Update schema and add tests (#12314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For database event triggers, we remove the before / after logic. We go directly with the properties Capture d’écran 2025-05-27 à 11 40 36 To achieve this without changing the shape of events, we need to handle keys using dots, such: ``` 'properties.after.name': { icon: 'IconBuildingSkyscraper', type: FieldMetadataType.TEXT, label: 'Name', value: 'My text', isLeaf: true, }, ``` This PR: - adds logic to handle the case where the key has dot included - adds tests --- .../searchVariableThroughOutputSchema.test.ts | 486 +++++++++++------- .../searchVariableThroughOutputSchema.ts | 42 +- .../generate-fake-object-record-event.spec.ts | 131 +++++ .../generate-fake-object-record.spec.ts | 55 ++ .../generate-object-record-fields.spec.ts | 110 ++++ .../should-generate-field-fake-value.spec.ts | 61 +++ .../generate-fake-object-record-event.ts | 141 ++--- .../utils/generate-fake-object-record.ts | 29 +- .../utils/generate-object-record-fields.ts | 21 + 9 files changed, 761 insertions(+), 315 deletions(-) create mode 100644 packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughOutputSchema.test.ts index 39b83e81e..6ea3f5912 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughOutputSchema.test.ts @@ -1,191 +1,64 @@ import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema'; import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema'; - -const mockStep = { - id: 'step-1', - name: 'Step 1', - outputSchema: { - company: { - isLeaf: false, - icon: 'company', - label: 'Company', - value: { - object: { - nameSingular: 'company', - fieldIdName: 'id', - label: 'Company', - value: 'John', - isLeaf: true, - }, - fields: { - name: { label: 'Name', value: 'Twenty', isLeaf: true }, - address: { label: 'Address', value: '123 Main St', isLeaf: true }, - }, - _outputSchemaType: 'RECORD', - }, - }, - person: { - isLeaf: false, - icon: 'person', - label: 'Person', - value: { - object: { - nameSingular: 'person', - fieldIdName: 'id', - label: 'Person', - value: 'Jane', - isLeaf: true, - }, - fields: { - firstName: { label: 'First Name', value: 'Jane', isLeaf: true }, - lastName: { label: 'Last Name', value: 'Doe', isLeaf: true }, - email: { label: 'Email', value: 'jane@example.com', isLeaf: true }, - }, - _outputSchemaType: 'RECORD', - }, - }, - simpleData: { - isLeaf: true, - label: 'Simple Data', - value: 'Simple value', - }, - nestedData: { - isLeaf: false, - label: 'Nested Data', - value: { - field1: { label: 'Field 1', value: 'Value 1', isLeaf: true }, - field2: { label: 'Field 2', value: 'Value 2', isLeaf: true }, - }, - }, - }, -} satisfies StepOutputSchema; +import { FieldMetadataType } from '~/generated/graphql'; describe('searchVariableThroughOutputSchema', () => { - it('should not break with wrong path', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{step-1.wrong.wrong.wrong}}', - isFullRecord: false, - }); - - expect(result).toEqual({ - variableLabel: undefined, - variablePathLabel: 'Step 1 > undefined', - }); - }); - - it('should find a company field variable', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{step-1.company.name}}', - isFullRecord: false, - }); - - expect(result).toEqual({ - variableLabel: 'Name', - variablePathLabel: 'Step 1 > Company > Name', - }); - }); - - it('should find a person field variable', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{step-1.person.email}}', - isFullRecord: false, - }); - - expect(result).toEqual({ - variableLabel: 'Email', - variablePathLabel: 'Step 1 > Person > Email', - }); - }); - - it('should find a company object variable', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{step-1.company.id}}', - isFullRecord: true, - }); - - expect(result).toEqual({ - variableLabel: 'Company', - variablePathLabel: 'Step 1 > Company > Company', - }); - }); - - it('should find a person object variable', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{step-1.person.id}}', - isFullRecord: true, - }); - - expect(result).toEqual({ - variableLabel: 'Person', - variablePathLabel: 'Step 1 > Person > Person', - }); - }); - - it('should handle simple data fields', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{step-1.simpleData}}', - isFullRecord: false, - }); - - expect(result).toEqual({ - variableLabel: 'Simple Data', - variablePathLabel: 'Step 1 > Simple Data', - }); - }); - - it('should handle nested data fields', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{step-1.nestedData.field1}}', - isFullRecord: false, - }); - - expect(result).toEqual({ - variableLabel: 'Field 1', - variablePathLabel: 'Step 1 > Nested Data > Field 1', - }); - }); - - it('should handle invalid variable names', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{invalid}}', - isFullRecord: false, - }); - - expect(result).toEqual({ - variableLabel: undefined, - variablePathLabel: 'Step 1 > undefined', - }); - }); - - it('should handle non-existent paths', () => { - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStep, - rawVariableName: '{{step-1.nonExistent.field}}', - isFullRecord: false, - }); - - expect(result).toEqual({ - variableLabel: undefined, - variablePathLabel: 'Step 1 > undefined', - }); - }); - - it('should handle the case where the path has dots in field names', () => { - const mockStepWithDotInField = { + describe('step tests', () => { + const mockStep = { id: 'step-1', name: 'Step 1', outputSchema: { - 'complex.field': { + company: { isLeaf: false, - label: 'Complex Field', + icon: 'company', + label: 'Company', + value: { + object: { + nameSingular: 'company', + fieldIdName: 'id', + label: 'Company', + value: 'John', + isLeaf: true, + }, + fields: { + name: { label: 'Name', value: 'Twenty', isLeaf: true }, + address: { label: 'Address', value: '123 Main St', isLeaf: true }, + }, + _outputSchemaType: 'RECORD', + }, + }, + person: { + isLeaf: false, + icon: 'person', + label: 'Person', + value: { + object: { + nameSingular: 'person', + fieldIdName: 'id', + label: 'Person', + value: 'Jane', + isLeaf: true, + }, + fields: { + firstName: { label: 'First Name', value: 'Jane', isLeaf: true }, + lastName: { label: 'Last Name', value: 'Doe', isLeaf: true }, + email: { + label: 'Email', + value: 'jane@example.com', + isLeaf: true, + }, + }, + _outputSchemaType: 'RECORD', + }, + }, + simpleData: { + isLeaf: true, + label: 'Simple Data', + value: 'Simple value', + }, + nestedData: { + isLeaf: false, + label: 'Nested Data', value: { field1: { label: 'Field 1', value: 'Value 1', isLeaf: true }, field2: { label: 'Field 2', value: 'Value 2', isLeaf: true }, @@ -193,16 +66,255 @@ describe('searchVariableThroughOutputSchema', () => { }, }, } satisfies StepOutputSchema; + it('should not break with wrong path', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{step-1.wrong.wrong.wrong}}', + isFullRecord: false, + }); - const result = searchVariableThroughOutputSchema({ - stepOutputSchema: mockStepWithDotInField, - rawVariableName: '{{step-1.complex.field.field1}}', - isFullRecord: false, + expect(result).toEqual({ + variableLabel: undefined, + variablePathLabel: 'Step 1 > undefined', + }); }); - expect(result).toEqual({ - variableLabel: 'Field 1', - variablePathLabel: 'Step 1 > Complex Field > Field 1', + it('should find a company field variable', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{step-1.company.name}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: 'Name', + variablePathLabel: 'Step 1 > Company > Name', + }); + }); + + it('should find a person field variable', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{step-1.person.email}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: 'Email', + variablePathLabel: 'Step 1 > Person > Email', + }); + }); + + it('should find a company object variable', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{step-1.company.id}}', + isFullRecord: true, + }); + + expect(result).toEqual({ + variableLabel: 'Company', + variablePathLabel: 'Step 1 > Company > Company', + }); + }); + + it('should find a person object variable', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{step-1.person.id}}', + isFullRecord: true, + }); + + expect(result).toEqual({ + variableLabel: 'Person', + variablePathLabel: 'Step 1 > Person > Person', + }); + }); + + it('should handle simple data fields', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{step-1.simpleData}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: 'Simple Data', + variablePathLabel: 'Step 1 > Simple Data', + }); + }); + + it('should handle nested data fields', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{step-1.nestedData.field1}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: 'Field 1', + variablePathLabel: 'Step 1 > Nested Data > Field 1', + }); + }); + + it('should handle invalid variable names', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{invalid}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: undefined, + variablePathLabel: 'Step 1 > undefined', + }); + }); + + it('should handle non-existent paths', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStep, + rawVariableName: '{{step-1.nonExistent.field}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: undefined, + variablePathLabel: 'Step 1 > undefined', + }); + }); + + it('should handle the case where the path has dots in field names', () => { + const mockStepWithDotInField = { + id: 'step-1', + name: 'Step 1', + outputSchema: { + 'complex.field': { + isLeaf: false, + label: 'Complex Field', + value: { + field1: { label: 'Field 1', value: 'Value 1', isLeaf: true }, + field2: { label: 'Field 2', value: 'Value 2', isLeaf: true }, + }, + }, + }, + } satisfies StepOutputSchema; + + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockStepWithDotInField, + rawVariableName: '{{step-1.complex.field.field1}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: 'Field 1', + variablePathLabel: 'Step 1 > Complex Field > Field 1', + }); + }); + }); + + describe('trigger tests', () => { + const mockTrigger = { + id: 'trigger', + name: 'Record is Created', + icon: 'IconPlaylistAdd', + outputSchema: { + fields: { + 'properties.after.id': { + icon: 'Icon123', + type: FieldMetadataType.UUID, + label: 'Id', + value: '123e4567-e89b-12d3-a456-426614174000', + isLeaf: true, + }, + 'properties.after.name': { + icon: 'IconBuildingSkyscraper', + type: FieldMetadataType.TEXT, + label: 'Name', + value: 'My text', + isLeaf: true, + }, + 'properties.after.annualRecurringRevenue': { + icon: 'IconMoneybag', + label: 'ARR', + value: { + amountMicros: { + type: FieldMetadataType.NUMERIC, + label: ' Amount Micros', + value: null, + isLeaf: true, + }, + currencyCode: { + type: FieldMetadataType.TEXT, + label: ' Currency Code', + value: 'My text', + isLeaf: true, + }, + }, + isLeaf: false, + }, + }, + object: { + icon: 'IconBuildingSkyscraper', + label: 'Company', + value: 'A company', + isLeaf: true, + fieldIdName: 'properties.after.id', + nameSingular: 'company', + }, + _outputSchemaType: 'RECORD', + }, + } satisfies StepOutputSchema; + it('should find a simple field from trigger', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockTrigger, + rawVariableName: '{{trigger.properties.after.name}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: 'Name', + variablePathLabel: 'Record is Created > Name', + }); + }); + + it('should find a nested field from trigger', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockTrigger, + rawVariableName: + '{{trigger.properties.after.annualRecurringRevenue.amountMicros}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: ' Amount Micros', + variablePathLabel: 'Record is Created > ARR > Amount Micros', + }); + }); + + it('should find the object field from trigger', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockTrigger, + rawVariableName: '{{trigger.object}}', + isFullRecord: true, + }); + + expect(result).toEqual({ + variableLabel: 'Company', + variablePathLabel: 'Record is Created > Company', + }); + }); + + it('should handle invalid trigger field path', () => { + const result = searchVariableThroughOutputSchema({ + stepOutputSchema: mockTrigger, + rawVariableName: '{{trigger.nonExistent}}', + isFullRecord: false, + }); + + expect(result).toEqual({ + variableLabel: undefined, + variablePathLabel: 'Record is Created > undefined', + }); }); }); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchema.ts index 899bf8813..7f4385d91 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchema.ts @@ -45,27 +45,42 @@ const searchCurrentStepOutputSchema = ({ let nextKeyIndex = 0; let nextKey = path[nextKeyIndex]; let variablePathLabel = stepOutputSchema.name; + let isSelectedFieldInNextKey = false; + + const handleFieldNotFound = () => { + if (nextKeyIndex + 1 < path.length) { + // If the key is not found in the step, we handle the case where the path has been wrongly split + // For example, if there is a dot in the field name + nextKey = `${nextKey}.${path[nextKeyIndex + 1]}`; + } else { + // If we already reached the end of the path, we add the selected field to the next key + nextKey = `${nextKey}.${selectedField}`; + isSelectedFieldInNextKey = true; + } + }; while (nextKeyIndex < path.length) { if (!isDefined(currentSubStep)) { break; - } else if (isRecordOutputSchema(currentSubStep)) { + } + + if (isRecordOutputSchema(currentSubStep)) { const currentField = currentSubStep.fields[nextKey]; - currentSubStep = currentField?.value; - nextKey = path[nextKeyIndex + 1]; - variablePathLabel = `${variablePathLabel} > ${currentField?.label}`; + if (isDefined(currentField)) { + currentSubStep = currentField.value; + nextKey = path[nextKeyIndex + 1]; + variablePathLabel = `${variablePathLabel} > ${currentField.label}`; + } else { + handleFieldNotFound(); + } } else if (isBaseOutputSchema(currentSubStep)) { if (isDefined(currentSubStep[nextKey])) { const currentField = currentSubStep[nextKey]; - currentSubStep = currentField?.value; + currentSubStep = currentField.value; nextKey = path[nextKeyIndex + 1]; - variablePathLabel = `${variablePathLabel} > ${currentField?.label}`; + variablePathLabel = `${variablePathLabel} > ${currentField.label}`; } else { - // If the key is not found in the step, we handle the case where the path has been wrongly split - // For example, if there is a dot in the field name - if (nextKeyIndex + 1 < path.length) { - nextKey = `${nextKey}.${path[nextKeyIndex + 1]}`; - } + handleFieldNotFound(); } } nextKeyIndex++; @@ -81,7 +96,10 @@ const searchCurrentStepOutputSchema = ({ return { variableLabel: isFullRecord ? getDisplayedSubStepObjectLabel(currentSubStep) - : getDisplayedSubStepFieldLabel(selectedField, currentSubStep), + : getDisplayedSubStepFieldLabel( + isSelectedFieldInNextKey ? nextKey : selectedField, + currentSubStep, + ), variablePathLabel, }; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts new file mode 100644 index 000000000..bbb30e816 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts @@ -0,0 +1,131 @@ +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event'; +import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; + +jest.mock( + 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields', +); + +describe('generateFakeObjectRecordEvent', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockObjectMetadata = { + icon: 'test-icon', + labelSingular: 'Test Object', + description: 'Test Description', + nameSingular: 'testObject', + } as ObjectMetadataEntity; + + const mockFields = { + field1: { type: 'TEXT', value: 'test' }, + field2: { type: 'NUMBER', value: 123 }, + }; + + beforeEach(() => { + (generateObjectRecordFields as jest.Mock).mockReturnValue(mockFields); + }); + + it('should generate record with "after" prefix for CREATED action', () => { + const result = generateFakeObjectRecordEvent( + mockObjectMetadata, + DatabaseEventAction.CREATED, + ); + + expect(result).toEqual({ + object: { + isLeaf: true, + icon: 'test-icon', + label: 'Test Object', + value: 'Test Description', + nameSingular: 'testObject', + fieldIdName: 'properties.after.id', + }, + fields: { + 'properties.after.field1': { type: 'TEXT', value: 'test' }, + 'properties.after.field2': { type: 'NUMBER', value: 123 }, + }, + _outputSchemaType: 'RECORD', + }); + }); + + it('should generate record with "after" prefix for UPDATED action', () => { + const result = generateFakeObjectRecordEvent( + mockObjectMetadata, + DatabaseEventAction.UPDATED, + ); + + expect(result).toEqual({ + object: { + isLeaf: true, + icon: 'test-icon', + label: 'Test Object', + value: 'Test Description', + nameSingular: 'testObject', + fieldIdName: 'properties.after.id', + }, + fields: { + 'properties.after.field1': { type: 'TEXT', value: 'test' }, + 'properties.after.field2': { type: 'NUMBER', value: 123 }, + }, + _outputSchemaType: 'RECORD', + }); + }); + + it('should generate record with "before" prefix for DELETED action', () => { + const result = generateFakeObjectRecordEvent( + mockObjectMetadata, + DatabaseEventAction.DELETED, + ); + + expect(result).toEqual({ + object: { + isLeaf: true, + icon: 'test-icon', + label: 'Test Object', + value: 'Test Description', + nameSingular: 'testObject', + fieldIdName: 'properties.before.id', + }, + fields: { + 'properties.before.field1': { type: 'TEXT', value: 'test' }, + 'properties.before.field2': { type: 'NUMBER', value: 123 }, + }, + _outputSchemaType: 'RECORD', + }); + }); + + it('should generate record with "before" prefix for DESTROYED action', () => { + const result = generateFakeObjectRecordEvent( + mockObjectMetadata, + DatabaseEventAction.DESTROYED, + ); + + expect(result).toEqual({ + object: { + isLeaf: true, + icon: 'test-icon', + label: 'Test Object', + value: 'Test Description', + nameSingular: 'testObject', + fieldIdName: 'properties.before.id', + }, + fields: { + 'properties.before.field1': { type: 'TEXT', value: 'test' }, + 'properties.before.field2': { type: 'NUMBER', value: 123 }, + }, + _outputSchemaType: 'RECORD', + }); + }); + + it('should throw error for unknown action', () => { + expect(() => { + generateFakeObjectRecordEvent( + mockObjectMetadata, + 'UNKNOWN' as DatabaseEventAction, + ); + }).toThrow("Unknown action 'UNKNOWN'"); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts new file mode 100644 index 000000000..1108869f5 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts @@ -0,0 +1,55 @@ +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; +import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; + +jest.mock( + 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields', + () => ({ + generateObjectRecordFields: jest.fn().mockReturnValue({ + field1: { type: 'TEXT', value: 'test' }, + field2: { type: 'NUMBER', value: 123 }, + }), + }), +); + +describe('generateFakeObjectRecord', () => { + it('should generate a record with correct object metadata', () => { + const mockObjectMetadata = { + icon: 'test-icon', + labelSingular: 'Test Object', + description: 'Test Description', + nameSingular: 'testObject', + } as ObjectMetadataEntity; + + const result = generateFakeObjectRecord(mockObjectMetadata); + + expect(result).toEqual({ + object: { + isLeaf: true, + icon: 'test-icon', + label: 'Test Object', + value: 'Test Description', + nameSingular: 'testObject', + fieldIdName: 'id', + }, + fields: { + field1: { type: 'TEXT', value: 'test' }, + field2: { type: 'NUMBER', value: 123 }, + }, + _outputSchemaType: 'RECORD', + }); + }); + + it('should call generateObjectRecordFields with the object metadata', () => { + const mockObjectMetadata = { + icon: 'test-icon', + labelSingular: 'Test Object', + description: 'Test Description', + nameSingular: 'testObject', + } as ObjectMetadataEntity; + + generateFakeObjectRecord(mockObjectMetadata); + + expect(generateObjectRecordFields).toHaveBeenCalledWith(mockObjectMetadata); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts new file mode 100644 index 000000000..5ecab2fde --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts @@ -0,0 +1,110 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; +import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; +import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; + +jest.mock( + 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field', +); +jest.mock( + 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value', +); + +describe('generateObjectRecordFields', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should generate fields for valid fields only', () => { + const mockFields = [ + { + name: 'field1', + type: FieldMetadataType.TEXT, + label: 'Field 1', + icon: 'icon1', + isSystem: false, + isActive: true, + }, + { + name: 'field2', + type: FieldMetadataType.RELATION, + label: 'Field 2', + icon: 'icon2', + isSystem: false, + isActive: true, + }, + { + name: 'field3', + type: FieldMetadataType.NUMBER, + label: 'Field 3', + icon: 'icon3', + isSystem: false, + isActive: true, + }, + ]; + + const mockObjectMetadata = { + fields: mockFields, + } as ObjectMetadataEntity; + + (shouldGenerateFieldFakeValue as jest.Mock).mockImplementation( + (field) => field.type !== FieldMetadataType.RELATION, + ); + + (generateFakeField as jest.Mock).mockImplementation( + ({ type, label, icon }) => ({ + type, + label, + icon, + value: `mock-${type}`, + }), + ); + + const result = generateObjectRecordFields(mockObjectMetadata); + + expect(result).toEqual({ + field1: { + type: FieldMetadataType.TEXT, + label: 'Field 1', + icon: 'icon1', + value: 'mock-TEXT', + }, + field3: { + type: FieldMetadataType.NUMBER, + label: 'Field 3', + icon: 'icon3', + value: 'mock-NUMBER', + }, + }); + + expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(3); + expect(generateFakeField).toHaveBeenCalledTimes(2); + }); + + it('should return empty object when no valid fields', () => { + const mockFields = [ + { + name: 'field1', + type: FieldMetadataType.RELATION, + label: 'Field 1', + icon: 'icon1', + isSystem: false, + isActive: true, + }, + ]; + + const mockObjectMetadata = { + fields: mockFields, + } as ObjectMetadataEntity; + + (shouldGenerateFieldFakeValue as jest.Mock).mockReturnValue(false); + + const result = generateObjectRecordFields(mockObjectMetadata); + + expect(result).toEqual({}); + expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(1); + expect(generateFakeField).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts new file mode 100644 index 000000000..3b48549e0 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts @@ -0,0 +1,61 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; + +describe('shouldGenerateFieldFakeValue', () => { + it('should return true for active non-system fields', () => { + const field = { + isSystem: false, + isActive: true, + type: FieldMetadataType.TEXT, + name: 'testField', + } as FieldMetadataEntity; + + expect(shouldGenerateFieldFakeValue(field)).toBe(true); + }); + + it('should return true for system id field', () => { + const field = { + isSystem: true, + isActive: true, + type: FieldMetadataType.UUID, + name: 'id', + } as FieldMetadataEntity; + + expect(shouldGenerateFieldFakeValue(field)).toBe(true); + }); + + it('should return false for inactive fields', () => { + const field = { + isSystem: false, + isActive: false, + type: FieldMetadataType.TEXT, + name: 'testField', + } as FieldMetadataEntity; + + expect(shouldGenerateFieldFakeValue(field)).toBe(false); + }); + + it('should return false for system fields (except id)', () => { + const field = { + isSystem: true, + isActive: true, + type: FieldMetadataType.TEXT, + name: 'testField', + } as FieldMetadataEntity; + + expect(shouldGenerateFieldFakeValue(field)).toBe(false); + }); + + it('should return false for relation fields', () => { + const field = { + isSystem: false, + isActive: true, + type: FieldMetadataType.RELATION, + name: 'testField', + } as FieldMetadataEntity; + + expect(shouldGenerateFieldFakeValue(field)).toBe(false); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts index a98e6f1e8..9a5fcfe60 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts @@ -1,97 +1,60 @@ -import { v4 } from 'uuid'; - import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { BaseOutputSchema } 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'; -import { camelToTitleCase } from 'src/utils/camel-to-title-case'; +import { + BaseOutputSchema, + 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'; + +const generateFakeObjectRecordEventWithPrefix = ({ + objectMetadataEntity, + prefix, +}: { + objectMetadataEntity: ObjectMetadataEntity; + prefix: string; +}): RecordOutputSchema => { + const recordFields = generateObjectRecordFields(objectMetadataEntity); + const prefixedRecordFields = Object.entries(recordFields).reduce( + (acc, [key, value]) => { + acc[`${prefix}.${key}`] = value; + + return acc; + }, + {} as BaseOutputSchema, + ); + + return { + object: { + isLeaf: true, + icon: objectMetadataEntity.icon, + label: objectMetadataEntity.labelSingular, + value: objectMetadataEntity.description, + nameSingular: objectMetadataEntity.nameSingular, + fieldIdName: `${prefix}.id`, + }, + fields: prefixedRecordFields, + _outputSchemaType: 'RECORD', + }; +}; export const generateFakeObjectRecordEvent = ( objectMetadataEntity: ObjectMetadataEntity, action: DatabaseEventAction, -): BaseOutputSchema => { - const recordId = v4(); - const userId = v4(); - const workspaceMemberId = v4(); - - const after = generateFakeObjectRecord(objectMetadataEntity); - const formattedObjectMetadataEntity = Object.entries( - objectMetadataEntity, - ).reduce((acc: BaseOutputSchema, [key, value]) => { - acc[key] = { isLeaf: true, value, label: camelToTitleCase(key) }; - - return acc; - }, {}); - - const baseResult: BaseOutputSchema = { - recordId: { - isLeaf: true, - type: 'string', - value: recordId, - label: 'Record ID', - }, - userId: { isLeaf: true, type: 'string', value: userId, label: 'User ID' }, - workspaceMemberId: { - isLeaf: true, - type: 'string', - value: workspaceMemberId, - label: 'Workspace Member ID', - }, - objectMetadata: { - isLeaf: false, - value: formattedObjectMetadataEntity, - label: 'Object Metadata', - }, - }; - - if (action === DatabaseEventAction.CREATED) { - return { - ...baseResult, - 'properties.after': { - isLeaf: false, - value: after, - label: 'Record Fields', - }, - }; +): RecordOutputSchema => { + switch (action) { + case DatabaseEventAction.CREATED: + case DatabaseEventAction.UPDATED: + return generateFakeObjectRecordEventWithPrefix({ + objectMetadataEntity, + prefix: 'properties.after', + }); + case DatabaseEventAction.DELETED: + case DatabaseEventAction.DESTROYED: + return generateFakeObjectRecordEventWithPrefix({ + objectMetadataEntity, + prefix: 'properties.before', + }); + default: + throw new Error(`Unknown action '${action}'`); } - - const before = generateFakeObjectRecord(objectMetadataEntity); - - if (action === DatabaseEventAction.UPDATED) { - return { - ...baseResult, - properties: { - isLeaf: false, - value: { - before: { isLeaf: false, value: before, label: 'Before Update' }, - after: { isLeaf: false, value: after, label: 'After Update' }, - }, - label: 'Record Fields', - }, - }; - } - - if (action === DatabaseEventAction.DELETED) { - return { - ...baseResult, - 'properties.before': { - isLeaf: false, - value: before, - label: 'Record Fields', - }, - }; - } - - if (action === DatabaseEventAction.DESTROYED) { - return { - ...baseResult, - 'properties.before': { - isLeaf: false, - value: before, - label: 'Record Fields', - }, - }; - } - - throw new Error(`Unknown action '${action}'`); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts index 3fd213011..33fe7af7a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts @@ -1,31 +1,6 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { - Leaf, - Node, - RecordOutputSchema, -} from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; -import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; -import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; - -const generateObjectRecordFields = ( - objectMetadataEntity: ObjectMetadataEntity, -) => - objectMetadataEntity.fields.reduce( - (acc: Record, field) => { - if (!shouldGenerateFieldFakeValue(field)) { - return acc; - } - - acc[field.name] = generateFakeField({ - type: field.type, - label: field.label, - icon: field.icon, - }); - - return acc; - }, - {}, - ); +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'; export const generateFakeObjectRecord = ( objectMetadataEntity: ObjectMetadataEntity, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts new file mode 100644 index 000000000..4ec536b7a --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts @@ -0,0 +1,21 @@ +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { BaseOutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; +import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; +import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; + +export const generateObjectRecordFields = ( + objectMetadataEntity: ObjectMetadataEntity, +): BaseOutputSchema => + objectMetadataEntity.fields.reduce((acc: BaseOutputSchema, field) => { + if (!shouldGenerateFieldFakeValue(field)) { + return acc; + } + + acc[field.name] = generateFakeField({ + type: field.type, + label: field.label, + icon: field.icon, + }); + + return acc; + }, {} as BaseOutputSchema);