diff --git a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts index a614f5776c7..78bbb8115b4 100644 --- a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts +++ b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts @@ -1,4 +1,5 @@ import { type ExampleOptions } from '@/types/scaffolding-options'; +import { GENERATED_DIR } from 'twenty-shared/application'; import { copyBaseApplicationProject } from '@/utils/app-template'; import * as fs from 'fs-extra'; import { tmpdir } from 'os'; @@ -111,7 +112,7 @@ describe('copyBaseApplicationProject', () => { const gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); expect(gitignoreContent).toContain('/node_modules'); - expect(gitignoreContent).toContain('generated'); + expect(gitignoreContent).toContain(GENERATED_DIR); }); it('should create yarn.lock file', async () => { diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts index 9973bf2ae31..dcbba27124b 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts @@ -77,6 +77,107 @@ export const EXPECTED_MANIFEST: Manifest = { ], fields: [ + // Reverse relation fields for rootNote (5 fields) + { + description: 'RootNote Root note', + icon: 'IconTimelineEvent', + isNullable: true, + label: 'RootNote', + morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1', + name: 'targetRootNote', + objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5', + relationTargetFieldMetadataUniversalIdentifier: + '1cdb0b3a-0ae1-59f6-b3f0-59ff99e40525', + relationTargetObjectMetadataUniversalIdentifier: + 'b0b1b2b3-b4b5-4000-8000-000000000001', + type: FieldType.MORPH_RELATION, + universalIdentifier: 'a0f6ffdc-a570-58a6-973d-a9bc9a527de8', + universalSettings: { + joinColumnName: 'targetRootNoteId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'RootNote Root note', + icon: 'IconHeart', + isNullable: true, + label: 'RootNote', + name: 'targetRootNote', + objectUniversalIdentifier: '20202020-ab56-4e05-92a3-e2414a499860', + relationTargetFieldMetadataUniversalIdentifier: + 'bbc0dc11-9daa-5e3f-b939-e6f0a12bb277', + relationTargetObjectMetadataUniversalIdentifier: + 'b0b1b2b3-b4b5-4000-8000-000000000001', + type: FieldType.RELATION, + universalIdentifier: '13fb0eae-50b4-5e28-afbc-5b5a9c422ead', + universalSettings: { + joinColumnName: 'targetRootNoteId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'RootNote Root note', + icon: 'IconFileImport', + isNullable: true, + label: 'RootNote', + morphId: '20202020-f634-435d-ab8d-e1168b375c69', + name: 'targetRootNote', + objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a', + relationTargetFieldMetadataUniversalIdentifier: + 'aa05ee89-bd8a-5911-a4d5-3355926186bc', + relationTargetObjectMetadataUniversalIdentifier: + 'b0b1b2b3-b4b5-4000-8000-000000000001', + type: FieldType.MORPH_RELATION, + universalIdentifier: '95cad10a-bff6-524d-b6d8-dbc6db7cbf18', + universalSettings: { + joinColumnName: 'targetRootNoteId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'RootNote Root note', + icon: 'IconCheckbox', + isNullable: true, + label: 'RootNote', + morphId: '20202020-f635-435d-ab8d-e1168b375c70', + name: 'targetRootNote', + objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400', + relationTargetFieldMetadataUniversalIdentifier: + '762ad37e-d624-51d8-989c-3a119eef9e7f', + relationTargetObjectMetadataUniversalIdentifier: + 'b0b1b2b3-b4b5-4000-8000-000000000001', + type: FieldType.MORPH_RELATION, + universalIdentifier: 'c327faff-76eb-5baf-8b35-1c31e57306fb', + universalSettings: { + joinColumnName: 'targetRootNoteId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'RootNote Root note', + icon: 'IconCheckbox', + isNullable: true, + label: 'RootNote', + morphId: '20202020-f636-435d-ab8d-e1168b375c71', + name: 'targetRootNote', + objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1', + relationTargetFieldMetadataUniversalIdentifier: + 'd4f1b0c8-8cd9-581e-9bf9-917f8c918d76', + relationTargetObjectMetadataUniversalIdentifier: + 'b0b1b2b3-b4b5-4000-8000-000000000001', + type: FieldType.MORPH_RELATION, + universalIdentifier: '36843050-e6e3-5759-99b6-e975426bad73', + universalSettings: { + joinColumnName: 'targetRootNoteId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + // User-defined relation and field definitions { label: 'Post Card', name: 'postCard', @@ -176,6 +277,306 @@ export const EXPECTED_MANIFEST: Manifest = { relationType: RelationType.MANY_TO_ONE, }, }, + // Reverse relation fields for postCardRecipient (5 fields) + { + description: 'PostCardRecipient Post Card Recipient', + icon: 'IconTimelineEvent', + isNullable: true, + label: 'PostCardRecipient', + morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1', + name: 'targetPostCardRecipient', + objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5', + relationTargetFieldMetadataUniversalIdentifier: + '02f5efba-4f94-5cc3-9e17-ca7b1fe5f71b', + relationTargetObjectMetadataUniversalIdentifier: + 'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e', + type: FieldType.MORPH_RELATION, + universalIdentifier: '18b6354d-8b0f-5be6-9473-d635aed670a1', + universalSettings: { + joinColumnName: 'targetPostCardRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'PostCardRecipient Post Card Recipient', + icon: 'IconHeart', + isNullable: true, + label: 'PostCardRecipient', + name: 'targetPostCardRecipient', + objectUniversalIdentifier: '20202020-ab56-4e05-92a3-e2414a499860', + relationTargetFieldMetadataUniversalIdentifier: + 'a284914f-650c-54ad-84ef-d6229d3fa453', + relationTargetObjectMetadataUniversalIdentifier: + 'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e', + type: FieldType.RELATION, + universalIdentifier: 'f3247843-dd1f-5b15-8873-c0062a021180', + universalSettings: { + joinColumnName: 'targetPostCardRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'PostCardRecipient Post Card Recipient', + icon: 'IconFileImport', + isNullable: true, + label: 'PostCardRecipient', + morphId: '20202020-f634-435d-ab8d-e1168b375c69', + name: 'targetPostCardRecipient', + objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a', + relationTargetFieldMetadataUniversalIdentifier: + '82497ec7-0121-53d6-9518-ae7434ceaf8b', + relationTargetObjectMetadataUniversalIdentifier: + 'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e', + type: FieldType.MORPH_RELATION, + universalIdentifier: 'ba74dfd1-e7ee-502d-abad-82e14d595874', + universalSettings: { + joinColumnName: 'targetPostCardRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'PostCardRecipient Post Card Recipient', + icon: 'IconCheckbox', + isNullable: true, + label: 'PostCardRecipient', + morphId: '20202020-f635-435d-ab8d-e1168b375c70', + name: 'targetPostCardRecipient', + objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400', + relationTargetFieldMetadataUniversalIdentifier: + '6de8b57e-ed0a-5d35-bd39-bd8874445810', + relationTargetObjectMetadataUniversalIdentifier: + 'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e', + type: FieldType.MORPH_RELATION, + universalIdentifier: '9d5a9f4b-5431-5b90-810a-1e9d8873b302', + universalSettings: { + joinColumnName: 'targetPostCardRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'PostCardRecipient Post Card Recipient', + icon: 'IconCheckbox', + isNullable: true, + label: 'PostCardRecipient', + morphId: '20202020-f636-435d-ab8d-e1168b375c71', + name: 'targetPostCardRecipient', + objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1', + relationTargetFieldMetadataUniversalIdentifier: + '00459e7e-8ef1-5a9b-b180-26bbb54a2696', + relationTargetObjectMetadataUniversalIdentifier: + 'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e', + type: FieldType.MORPH_RELATION, + universalIdentifier: '85353e69-1e70-51ef-91c3-334d85013a4b', + universalSettings: { + joinColumnName: 'targetPostCardRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + // Reverse relation fields for postCard (5 fields) + { + description: 'PostCard Post card', + icon: 'IconTimelineEvent', + isNullable: true, + label: 'PostCard', + morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1', + name: 'targetPostCard', + objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5', + relationTargetFieldMetadataUniversalIdentifier: + '3f8c70b5-abb0-51ec-9109-0a94ce9c04a6', + relationTargetObjectMetadataUniversalIdentifier: + '54b589ca-eeed-4950-a176-358418b85c05', + type: FieldType.MORPH_RELATION, + universalIdentifier: '1bdc52a0-0339-5360-ba17-b615250ef730', + universalSettings: { + joinColumnName: 'targetPostCardId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'PostCard Post card', + icon: 'IconHeart', + isNullable: true, + label: 'PostCard', + name: 'targetPostCard', + objectUniversalIdentifier: '20202020-ab56-4e05-92a3-e2414a499860', + relationTargetFieldMetadataUniversalIdentifier: + '94f43ec4-d2d1-5a50-b414-78da27d24258', + relationTargetObjectMetadataUniversalIdentifier: + '54b589ca-eeed-4950-a176-358418b85c05', + type: FieldType.RELATION, + universalIdentifier: '9a5d4ae3-6f45-593f-923d-bcd16ede4345', + universalSettings: { + joinColumnName: 'targetPostCardId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'PostCard Post card', + icon: 'IconFileImport', + isNullable: true, + label: 'PostCard', + morphId: '20202020-f634-435d-ab8d-e1168b375c69', + name: 'targetPostCard', + objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a', + relationTargetFieldMetadataUniversalIdentifier: + '547c93e4-c9e7-5880-b251-46230fb59bfa', + relationTargetObjectMetadataUniversalIdentifier: + '54b589ca-eeed-4950-a176-358418b85c05', + type: FieldType.MORPH_RELATION, + universalIdentifier: 'fecb8484-7e3c-59f8-a5ef-30882c5c1904', + universalSettings: { + joinColumnName: 'targetPostCardId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'PostCard Post card', + icon: 'IconCheckbox', + isNullable: true, + label: 'PostCard', + morphId: '20202020-f635-435d-ab8d-e1168b375c70', + name: 'targetPostCard', + objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400', + relationTargetFieldMetadataUniversalIdentifier: + '47deea5c-8559-5ae7-991a-60c9f7102f86', + relationTargetObjectMetadataUniversalIdentifier: + '54b589ca-eeed-4950-a176-358418b85c05', + type: FieldType.MORPH_RELATION, + universalIdentifier: 'ca687361-ac2b-5722-9ad9-cd37427d087a', + universalSettings: { + joinColumnName: 'targetPostCardId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'PostCard Post card', + icon: 'IconCheckbox', + isNullable: true, + label: 'PostCard', + morphId: '20202020-f636-435d-ab8d-e1168b375c71', + name: 'targetPostCard', + objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1', + relationTargetFieldMetadataUniversalIdentifier: + 'ecdccc0d-7c31-5126-89d5-0bdf3372257c', + relationTargetObjectMetadataUniversalIdentifier: + '54b589ca-eeed-4950-a176-358418b85c05', + type: FieldType.MORPH_RELATION, + universalIdentifier: '17058c96-d5a4-53b1-96ca-786f8c5fc8bd', + universalSettings: { + joinColumnName: 'targetPostCardId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + // Reverse relation fields for recipient (5 fields) + { + description: 'Recipient Recipient', + icon: 'IconTimelineEvent', + isNullable: true, + label: 'Recipient', + morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1', + name: 'targetRecipient', + objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5', + relationTargetFieldMetadataUniversalIdentifier: + '51d6d2d7-2fa8-5f77-b6cf-a96dbb801fa5', + relationTargetObjectMetadataUniversalIdentifier: + 'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', + type: FieldType.MORPH_RELATION, + universalIdentifier: 'b01f6937-4859-587b-b3df-92e0a6f65e93', + universalSettings: { + joinColumnName: 'targetRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'Recipient Recipient', + icon: 'IconHeart', + isNullable: true, + label: 'Recipient', + name: 'targetRecipient', + objectUniversalIdentifier: '20202020-ab56-4e05-92a3-e2414a499860', + relationTargetFieldMetadataUniversalIdentifier: + 'd16a6387-273b-563c-a6cc-2cb4524b9b7b', + relationTargetObjectMetadataUniversalIdentifier: + 'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', + type: FieldType.RELATION, + universalIdentifier: 'f4e6fd66-83bb-5f2d-b1ec-a3db2e0ca32d', + universalSettings: { + joinColumnName: 'targetRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'Recipient Recipient', + icon: 'IconFileImport', + isNullable: true, + label: 'Recipient', + morphId: '20202020-f634-435d-ab8d-e1168b375c69', + name: 'targetRecipient', + objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a', + relationTargetFieldMetadataUniversalIdentifier: + '244a7841-9e45-5ed1-a830-83e73c1f13fa', + relationTargetObjectMetadataUniversalIdentifier: + 'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', + type: FieldType.MORPH_RELATION, + universalIdentifier: '9c87e1b8-74a2-5833-9237-281d561d2a7d', + universalSettings: { + joinColumnName: 'targetRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'Recipient Recipient', + icon: 'IconCheckbox', + isNullable: true, + label: 'Recipient', + morphId: '20202020-f635-435d-ab8d-e1168b375c70', + name: 'targetRecipient', + objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400', + relationTargetFieldMetadataUniversalIdentifier: + 'eab71eb1-b457-555a-8a2b-f292cef1671d', + relationTargetObjectMetadataUniversalIdentifier: + 'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', + type: FieldType.MORPH_RELATION, + universalIdentifier: '23508618-7ed8-519e-9fee-a57b94dc98e9', + universalSettings: { + joinColumnName: 'targetRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, + { + description: 'Recipient Recipient', + icon: 'IconCheckbox', + isNullable: true, + label: 'Recipient', + morphId: '20202020-f636-435d-ab8d-e1168b375c71', + name: 'targetRecipient', + objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1', + relationTargetFieldMetadataUniversalIdentifier: + '305f1890-04ba-5101-b18f-a1ca25a1d8a7', + relationTargetObjectMetadataUniversalIdentifier: + 'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', + type: FieldType.MORPH_RELATION, + universalIdentifier: '20500ff3-2410-5bfc-b2e7-804785c07770', + universalSettings: { + joinColumnName: 'targetRecipientId', + onDelete: RelationOnDeleteAction.SET_NULL, + relationType: RelationType.MANY_TO_ONE, + }, + }, ], objects: [ { @@ -269,6 +670,106 @@ export const EXPECTED_MANIFEST: Manifest = { type: FieldMetadataType.ACTOR, universalIdentifier: '38e2c671-cbf5-5773-a04a-e5a2d6c323c8', }, + { + defaultValue: 0, + description: 'Position', + icon: 'IconHierarchy2', + isNullable: false, + label: 'Position', + name: 'position', + type: FieldMetadataType.POSITION, + universalIdentifier: '3b56cdea-cf85-51d4-a7a0-cd701f76141a', + }, + { + defaultValue: null, + description: 'Search vector', + icon: 'IconSearch', + isNullable: true, + label: 'Search vector', + name: 'searchVector', + type: FieldMetadataType.TS_VECTOR, + universalIdentifier: '7c6d88f5-a348-51dd-8d0b-74a17e626754', + }, + { + description: 'Root notes tied to the RootNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Timeline Activities', + name: 'timelineActivities', + relationTargetFieldMetadataUniversalIdentifier: + 'a0f6ffdc-a570-58a6-973d-a9bc9a527de8', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-6736-4337-b5c4-8b39fae325a5', + type: FieldType.RELATION, + universalIdentifier: '1cdb0b3a-0ae1-59f6-b3f0-59ff99e40525', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Root notes tied to the RootNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Favorites', + name: 'favorites', + relationTargetFieldMetadataUniversalIdentifier: + '13fb0eae-50b4-5e28-afbc-5b5a9c422ead', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-ab56-4e05-92a3-e2414a499860', + type: FieldType.RELATION, + universalIdentifier: 'bbc0dc11-9daa-5e3f-b939-e6f0a12bb277', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Root notes tied to the RootNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Attachments', + name: 'attachments', + relationTargetFieldMetadataUniversalIdentifier: + '95cad10a-bff6-524d-b6d8-dbc6db7cbf18', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-bd3d-4c60-8dca-571c71d4447a', + type: FieldType.RELATION, + universalIdentifier: 'aa05ee89-bd8a-5911-a4d5-3355926186bc', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Root notes tied to the RootNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Note Targets', + name: 'noteTargets', + relationTargetFieldMetadataUniversalIdentifier: + 'c327faff-76eb-5baf-8b35-1c31e57306fb', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-fff0-4b44-be82-bda313884400', + type: FieldType.RELATION, + universalIdentifier: '762ad37e-d624-51d8-989c-3a119eef9e7f', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Root notes tied to the RootNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Task Targets', + name: 'taskTargets', + relationTargetFieldMetadataUniversalIdentifier: + '36843050-e6e3-5759-99b6-e975426bad73', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-5a9a-44e8-95df-771cd06d0fb1', + type: FieldType.RELATION, + universalIdentifier: 'd4f1b0c8-8cd9-581e-9bf9-917f8c918d76', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, ], icon: 'IconNote', labelIdentifierFieldMetadataUniversalIdentifier: @@ -367,6 +868,106 @@ export const EXPECTED_MANIFEST: Manifest = { type: FieldMetadataType.ACTOR, universalIdentifier: '1dd75ff5-5fd0-51df-b3a3-a12e3aff30df', }, + { + defaultValue: 0, + description: 'Position', + icon: 'IconHierarchy2', + isNullable: false, + label: 'Position', + name: 'position', + type: FieldMetadataType.POSITION, + universalIdentifier: '6c5e91f5-e323-555b-84e0-5a1dd7f36b1b', + }, + { + defaultValue: null, + description: 'Search vector', + icon: 'IconSearch', + isNullable: true, + label: 'Search vector', + name: 'searchVector', + type: FieldMetadataType.TS_VECTOR, + universalIdentifier: '9fa18b19-41b6-5082-b3e2-4e73f7461164', + }, + { + description: 'Post Card Recipients tied to the PostCardRecipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Timeline Activities', + name: 'timelineActivities', + relationTargetFieldMetadataUniversalIdentifier: + '18b6354d-8b0f-5be6-9473-d635aed670a1', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-6736-4337-b5c4-8b39fae325a5', + type: FieldType.RELATION, + universalIdentifier: '02f5efba-4f94-5cc3-9e17-ca7b1fe5f71b', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Post Card Recipients tied to the PostCardRecipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Favorites', + name: 'favorites', + relationTargetFieldMetadataUniversalIdentifier: + 'f3247843-dd1f-5b15-8873-c0062a021180', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-ab56-4e05-92a3-e2414a499860', + type: FieldType.RELATION, + universalIdentifier: 'a284914f-650c-54ad-84ef-d6229d3fa453', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Post Card Recipients tied to the PostCardRecipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Attachments', + name: 'attachments', + relationTargetFieldMetadataUniversalIdentifier: + 'ba74dfd1-e7ee-502d-abad-82e14d595874', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-bd3d-4c60-8dca-571c71d4447a', + type: FieldType.RELATION, + universalIdentifier: '82497ec7-0121-53d6-9518-ae7434ceaf8b', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Post Card Recipients tied to the PostCardRecipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Note Targets', + name: 'noteTargets', + relationTargetFieldMetadataUniversalIdentifier: + '9d5a9f4b-5431-5b90-810a-1e9d8873b302', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-fff0-4b44-be82-bda313884400', + type: FieldType.RELATION, + universalIdentifier: '6de8b57e-ed0a-5d35-bd39-bd8874445810', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Post Card Recipients tied to the PostCardRecipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Task Targets', + name: 'taskTargets', + relationTargetFieldMetadataUniversalIdentifier: + '85353e69-1e70-51ef-91c3-334d85013a4b', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-5a9a-44e8-95df-771cd06d0fb1', + type: FieldType.RELATION, + universalIdentifier: '00459e7e-8ef1-5a9b-b180-26bbb54a2696', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, ], icon: 'IconLink', labelIdentifierFieldMetadataUniversalIdentifier: @@ -525,6 +1126,106 @@ export const EXPECTED_MANIFEST: Manifest = { type: FieldMetadataType.ACTOR, universalIdentifier: 'dc5231a4-e128-57b0-ac9d-723678d9b239', }, + { + defaultValue: 0, + description: 'Position', + icon: 'IconHierarchy2', + isNullable: false, + label: 'Position', + name: 'position', + type: FieldMetadataType.POSITION, + universalIdentifier: '3ad26df7-6345-5f85-aac1-2306cf10eaed', + }, + { + defaultValue: null, + description: 'Search vector', + icon: 'IconSearch', + isNullable: true, + label: 'Search vector', + name: 'searchVector', + type: FieldMetadataType.TS_VECTOR, + universalIdentifier: 'b49b1a1c-2f6b-52a3-836b-ece89c698627', + }, + { + description: 'Post cards tied to the PostCard', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Timeline Activities', + name: 'timelineActivities', + relationTargetFieldMetadataUniversalIdentifier: + '1bdc52a0-0339-5360-ba17-b615250ef730', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-6736-4337-b5c4-8b39fae325a5', + type: FieldType.RELATION, + universalIdentifier: '3f8c70b5-abb0-51ec-9109-0a94ce9c04a6', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Post cards tied to the PostCard', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Favorites', + name: 'favorites', + relationTargetFieldMetadataUniversalIdentifier: + '9a5d4ae3-6f45-593f-923d-bcd16ede4345', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-ab56-4e05-92a3-e2414a499860', + type: FieldType.RELATION, + universalIdentifier: '94f43ec4-d2d1-5a50-b414-78da27d24258', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Post cards tied to the PostCard', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Attachments', + name: 'attachments', + relationTargetFieldMetadataUniversalIdentifier: + 'fecb8484-7e3c-59f8-a5ef-30882c5c1904', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-bd3d-4c60-8dca-571c71d4447a', + type: FieldType.RELATION, + universalIdentifier: '547c93e4-c9e7-5880-b251-46230fb59bfa', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Post cards tied to the PostCard', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Note Targets', + name: 'noteTargets', + relationTargetFieldMetadataUniversalIdentifier: + 'ca687361-ac2b-5722-9ad9-cd37427d087a', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-fff0-4b44-be82-bda313884400', + type: FieldType.RELATION, + universalIdentifier: '47deea5c-8559-5ae7-991a-60c9f7102f86', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Post cards tied to the PostCard', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Task Targets', + name: 'taskTargets', + relationTargetFieldMetadataUniversalIdentifier: + '17058c96-d5a4-53b1-96ca-786f8c5fc8bd', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-5a9a-44e8-95df-771cd06d0fb1', + type: FieldType.RELATION, + universalIdentifier: 'ecdccc0d-7c31-5126-89d5-0bdf3372257c', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, ], icon: 'IconMail', labelIdentifierFieldMetadataUniversalIdentifier: @@ -628,6 +1329,106 @@ export const EXPECTED_MANIFEST: Manifest = { type: FieldMetadataType.ACTOR, universalIdentifier: '8da8e149-b60e-52e2-878d-73d05c8312b2', }, + { + defaultValue: 0, + description: 'Position', + icon: 'IconHierarchy2', + isNullable: false, + label: 'Position', + name: 'position', + type: FieldMetadataType.POSITION, + universalIdentifier: '824e930c-ce9f-5267-bebd-f6db215c3c14', + }, + { + defaultValue: null, + description: 'Search vector', + icon: 'IconSearch', + isNullable: true, + label: 'Search vector', + name: 'searchVector', + type: FieldMetadataType.TS_VECTOR, + universalIdentifier: '1f5df24b-8248-59cf-8bb6-362de121d2e4', + }, + { + description: 'Recipients tied to the Recipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Timeline Activities', + name: 'timelineActivities', + relationTargetFieldMetadataUniversalIdentifier: + 'b01f6937-4859-587b-b3df-92e0a6f65e93', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-6736-4337-b5c4-8b39fae325a5', + type: FieldType.RELATION, + universalIdentifier: '51d6d2d7-2fa8-5f77-b6cf-a96dbb801fa5', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Recipients tied to the Recipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Favorites', + name: 'favorites', + relationTargetFieldMetadataUniversalIdentifier: + 'f4e6fd66-83bb-5f2d-b1ec-a3db2e0ca32d', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-ab56-4e05-92a3-e2414a499860', + type: FieldType.RELATION, + universalIdentifier: 'd16a6387-273b-563c-a6cc-2cb4524b9b7b', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Recipients tied to the Recipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Attachments', + name: 'attachments', + relationTargetFieldMetadataUniversalIdentifier: + '9c87e1b8-74a2-5833-9237-281d561d2a7d', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-bd3d-4c60-8dca-571c71d4447a', + type: FieldType.RELATION, + universalIdentifier: '244a7841-9e45-5ed1-a830-83e73c1f13fa', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Recipients tied to the Recipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Note Targets', + name: 'noteTargets', + relationTargetFieldMetadataUniversalIdentifier: + '23508618-7ed8-519e-9fee-a57b94dc98e9', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-fff0-4b44-be82-bda313884400', + type: FieldType.RELATION, + universalIdentifier: 'eab71eb1-b457-555a-8a2b-f292cef1671d', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, + { + description: 'Recipients tied to the Recipient', + icon: 'IconBuildingSkyscraper', + isNullable: false, + label: 'Task Targets', + name: 'taskTargets', + relationTargetFieldMetadataUniversalIdentifier: + '20500ff3-2410-5bfc-b2e7-804785c07770', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-5a9a-44e8-95df-771cd06d0fb1', + type: FieldType.RELATION, + universalIdentifier: '305f1890-04ba-5101-b18f-a1ca25a1d8a7', + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + }, ], icon: 'IconUser', labelIdentifierFieldMetadataUniversalIdentifier: diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts index 1b631963516..e1379f70a0e 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts @@ -13,42 +13,17 @@ export const defineManifestTests = (appPath: string): void => { expect(manifest).not.toBeNull(); - const { sources: _sources, ...sanitizedManifest } = manifest; - - expect(normalizeManifestForComparison(sanitizedManifest)).toEqual( - normalizeManifestForComparison(EXPECTED_MANIFEST), - ); - - for (const fn of manifest.logicFunctions) { - expect(fn.builtHandlerChecksum).toBeDefined(); - expect(fn.builtHandlerChecksum).not.toBeNull(); - expect(typeof fn.builtHandlerChecksum).toBe('string'); - } - - for (const component of manifest.frontComponents ?? []) { - expect(component.builtComponentChecksum).toBeDefined(); - expect(component.builtComponentChecksum).not.toBeNull(); - expect(typeof component.builtComponentChecksum).toBe('string'); - } - }); - - it('should have correct application config', async () => { - const manifest = await fs.readJson(manifestOutputPath); - - expect(manifest?.application.displayName).toBe('Rich App'); - expect(manifest?.application.description).toBe('A simple rich app'); - }); - - it('should load all entity types', async () => { - const manifest = await fs.readJson(manifestOutputPath); - expect(manifest.objects).toHaveLength(4); expect(manifest.logicFunctions).toHaveLength(6); expect(manifest.frontComponents).toHaveLength(4); expect(manifest.roles).toHaveLength(2); - expect(manifest.fields).toHaveLength(6); + expect(manifest.fields).toHaveLength(26); expect(manifest.views).toHaveLength(3); expect(manifest.navigationMenuItems).toHaveLength(3); + + expect(normalizeManifestForComparison(manifest)).toEqual( + normalizeManifestForComparison(EXPECTED_MANIFEST), + ); }); }); }; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts index dab295ebfa8..eb172411860 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts @@ -1,6 +1,10 @@ import { FieldType } from '@/sdk'; import type { Manifest } from 'twenty-shared/application'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { + FieldMetadataType, + RelationOnDeleteAction, + RelationType, +} from 'twenty-shared/types'; export const EXPECTED_MANIFEST: Manifest = { application: { @@ -9,13 +13,113 @@ export const EXPECTED_MANIFEST: Manifest = { description: 'An app with all entities at root level', icon: 'IconFolder', defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', - packageJsonChecksum: '93ae1e2eb3db18351d06f43550700dcc', - yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e', + packageJsonChecksum: '[checksum]', + yarnLockChecksum: '[checksum]', apiClientChecksum: null, }, skills: [], publicAssets: [], - fields: [], + fields: [ + { + name: 'targetMyNote', + label: 'MyNote', + description: 'MyNote My note', + icon: 'IconTimelineEvent', + isNullable: true, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: RelationOnDeleteAction.SET_NULL, + joinColumnName: 'targetMyNoteId', + }, + universalIdentifier: '7e28b1ea-9a94-5d36-bc03-3f12f476867a', + objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5', + relationTargetFieldMetadataUniversalIdentifier: + '12c84c1c-6e3f-54c5-a9a8-e93c7b46ac35', + relationTargetObjectMetadataUniversalIdentifier: + 'e1e2e3e4-e5e6-4000-8000-000000000030', + type: FieldMetadataType.MORPH_RELATION, + morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1', + }, + { + name: 'targetMyNote', + label: 'MyNote', + description: 'MyNote My note', + icon: 'IconHeart', + isNullable: true, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: RelationOnDeleteAction.SET_NULL, + joinColumnName: 'targetMyNoteId', + }, + universalIdentifier: '698f12fe-3feb-55e3-b6cc-73375f1a8ae6', + objectUniversalIdentifier: '20202020-ab56-4e05-92a3-e2414a499860', + relationTargetFieldMetadataUniversalIdentifier: + 'ed80c73b-25ff-5a3c-99c7-e27fa2f2611e', + relationTargetObjectMetadataUniversalIdentifier: + 'e1e2e3e4-e5e6-4000-8000-000000000030', + type: FieldType.RELATION, + }, + { + name: 'targetMyNote', + label: 'MyNote', + description: 'MyNote My note', + icon: 'IconFileImport', + isNullable: true, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: RelationOnDeleteAction.SET_NULL, + joinColumnName: 'targetMyNoteId', + }, + universalIdentifier: '721b12d8-7a3d-5e13-b8d5-62b3ad8cb8b5', + objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a', + relationTargetFieldMetadataUniversalIdentifier: + '56a872c8-7a1e-563d-8772-3978c2c5c220', + relationTargetObjectMetadataUniversalIdentifier: + 'e1e2e3e4-e5e6-4000-8000-000000000030', + type: FieldMetadataType.MORPH_RELATION, + morphId: '20202020-f634-435d-ab8d-e1168b375c69', + }, + { + name: 'targetMyNote', + label: 'MyNote', + description: 'MyNote My note', + icon: 'IconCheckbox', + isNullable: true, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: RelationOnDeleteAction.SET_NULL, + joinColumnName: 'targetMyNoteId', + }, + universalIdentifier: '94b6f269-fc9a-5bcd-af5b-2bdac69f276b', + objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400', + relationTargetFieldMetadataUniversalIdentifier: + '011595e8-7524-5ec7-9fbc-4bd3c42d7d24', + relationTargetObjectMetadataUniversalIdentifier: + 'e1e2e3e4-e5e6-4000-8000-000000000030', + type: FieldMetadataType.MORPH_RELATION, + morphId: '20202020-f635-435d-ab8d-e1168b375c70', + }, + { + name: 'targetMyNote', + label: 'MyNote', + description: 'MyNote My note', + icon: 'IconCheckbox', + isNullable: true, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: RelationOnDeleteAction.SET_NULL, + joinColumnName: 'targetMyNoteId', + }, + universalIdentifier: '50da3cfa-dea5-5c9b-b1b7-cb615e1df4c3', + objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1', + relationTargetFieldMetadataUniversalIdentifier: + 'dd734fa3-26c3-53dd-91aa-577c313e1fa4', + relationTargetObjectMetadataUniversalIdentifier: + 'e1e2e3e4-e5e6-4000-8000-000000000030', + type: FieldMetadataType.MORPH_RELATION, + morphId: '20202020-f636-435d-ab8d-e1168b375c71', + }, + ], objects: [ { universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000030', @@ -110,6 +214,106 @@ export const EXPECTED_MANIFEST: Manifest = { type: FieldMetadataType.ACTOR, universalIdentifier: 'fa60f87d-15c8-5ff7-9da0-191fa14a922f', }, + { + defaultValue: 0, + description: 'Position', + icon: 'IconHierarchy2', + isNullable: false, + label: 'Position', + name: 'position', + type: FieldMetadataType.POSITION, + universalIdentifier: '4a5d7791-ddc9-53ee-bc46-699da7373290', + }, + { + defaultValue: null, + description: 'Search vector', + icon: 'IconSearch', + isNullable: true, + label: 'Search vector', + name: 'searchVector', + type: FieldMetadataType.TS_VECTOR, + universalIdentifier: '0f00bc3b-f313-5206-8496-842af20a538a', + }, + { + name: 'timelineActivities', + label: 'Timeline Activities', + description: 'My notes tied to the MyNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + type: FieldType.RELATION, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + universalIdentifier: '12c84c1c-6e3f-54c5-a9a8-e93c7b46ac35', + relationTargetFieldMetadataUniversalIdentifier: + '7e28b1ea-9a94-5d36-bc03-3f12f476867a', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-6736-4337-b5c4-8b39fae325a5', + }, + { + name: 'favorites', + label: 'Favorites', + description: 'My notes tied to the MyNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + type: FieldType.RELATION, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + universalIdentifier: 'ed80c73b-25ff-5a3c-99c7-e27fa2f2611e', + relationTargetFieldMetadataUniversalIdentifier: + '698f12fe-3feb-55e3-b6cc-73375f1a8ae6', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-ab56-4e05-92a3-e2414a499860', + }, + { + name: 'attachments', + label: 'Attachments', + description: 'My notes tied to the MyNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + type: FieldType.RELATION, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + universalIdentifier: '56a872c8-7a1e-563d-8772-3978c2c5c220', + relationTargetFieldMetadataUniversalIdentifier: + '721b12d8-7a3d-5e13-b8d5-62b3ad8cb8b5', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-bd3d-4c60-8dca-571c71d4447a', + }, + { + name: 'noteTargets', + label: 'Note Targets', + description: 'My notes tied to the MyNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + type: FieldType.RELATION, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + universalIdentifier: '011595e8-7524-5ec7-9fbc-4bd3c42d7d24', + relationTargetFieldMetadataUniversalIdentifier: + '94b6f269-fc9a-5bcd-af5b-2bdac69f276b', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-fff0-4b44-be82-bda313884400', + }, + { + name: 'taskTargets', + label: 'Task Targets', + description: 'My notes tied to the MyNote', + icon: 'IconBuildingSkyscraper', + isNullable: false, + type: FieldType.RELATION, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, + universalIdentifier: 'dd734fa3-26c3-53dd-91aa-577c313e1fa4', + relationTargetFieldMetadataUniversalIdentifier: + '50da3cfa-dea5-5c9b-b1b7-cb615e1df4c3', + relationTargetObjectMetadataUniversalIdentifier: + '20202020-5a9a-44e8-95df-771cd06d0fb1', + }, ], }, ], diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts index 45e220ee0f7..dc3a1d960b1 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts @@ -16,43 +16,11 @@ export const defineManifestTests = (appPath: string): void => { it('should have correct manifest content', async () => { const manifestPath = join(appPath, '.twenty/output/manifest.json'); - const manifest: Manifest = await fs.readJSON(manifestPath); - - expect(manifest.application).toEqual(EXPECTED_MANIFEST.application); - expect(manifest.objects).toEqual(EXPECTED_MANIFEST.objects); - - expect( - normalizeManifestForComparison({ - logicFunctions: manifest.logicFunctions, - }).logicFunctions, - ).toEqual( - normalizeManifestForComparison({ - logicFunctions: EXPECTED_MANIFEST.logicFunctions, - }).logicFunctions, + const manifest: Manifest = normalizeManifestForComparison( + await fs.readJSON(manifestPath), ); - for (const fn of manifest.logicFunctions) { - expect(fn.builtHandlerChecksum).toBeDefined(); - expect(fn.builtHandlerChecksum).not.toBeNull(); - expect(typeof fn.builtHandlerChecksum).toBe('string'); - } - - expect( - normalizeManifestForComparison({ - frontComponents: manifest.frontComponents, - }).frontComponents, - ).toEqual( - normalizeManifestForComparison({ - frontComponents: EXPECTED_MANIFEST.frontComponents, - }).frontComponents, - ); - - for (const component of manifest.frontComponents ?? []) { - expect(component.builtComponentChecksum).toBeDefined(); - expect(component.builtComponentChecksum).not.toBeNull(); - expect(typeof component.builtComponentChecksum).toBe('string'); - } - expect(manifest.roles).toEqual(EXPECTED_MANIFEST.roles); + expect(manifest).toEqual(EXPECTED_MANIFEST); }); }); }; diff --git a/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts b/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts index 5959c58285c..1ea1796e994 100644 --- a/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts +++ b/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts @@ -1,21 +1,21 @@ -// Loose type for JSON manifest imports where enum values are inferred as strings -type JsonManifestInput = { - logicFunctions?: Array<{ - builtHandlerChecksum?: string | null; - [key: string]: unknown; - }>; - frontComponents?: Array<{ - builtComponentChecksum?: string; - [key: string]: unknown; - }>; - [key: string]: unknown; -}; +import { type Manifest } from 'twenty-shared/application'; -// Replace dynamic checksum values with a placeholder for consistent comparisons -export const normalizeManifestForComparison = ( +export const normalizeManifestForComparison = ( manifest: T, ): T => ({ ...manifest, + application: { + ...manifest.application, + yarnLockChecksum: manifest.application.yarnLockChecksum + ? '[checksum]' + : null, + packageJsonChecksum: manifest.application.packageJsonChecksum + ? '[checksum]' + : null, + apiClientChecksum: manifest.application.apiClientChecksum + ? '[checksum]' + : null, + }, logicFunctions: manifest.logicFunctions?.map((fn) => ({ ...fn, builtHandlerChecksum: fn.builtHandlerChecksum ? '[checksum]' : null, diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts index a03d0a934d0..457ab9e627c 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts @@ -11,7 +11,11 @@ import { import { createTypecheckPlugin } from '@/cli/utilities/build/common/typecheck-plugin'; import * as esbuild from 'esbuild'; import path from 'path'; -import { OUTPUT_DIR, NODE_ESM_CJS_BANNER } from 'twenty-shared/application'; +import { + OUTPUT_DIR, + NODE_ESM_CJS_BANNER, + GENERATED_DIR, +} from 'twenty-shared/application'; import { FileFolder } from 'twenty-shared/types'; export const LOGIC_FUNCTION_EXTERNAL_MODULES: string[] = [ @@ -199,7 +203,7 @@ const createSdkGeneratedResolverPlugin = (appPath: string): esbuild.Plugin => ({ appPath, 'node_modules', 'twenty-sdk', - 'generated', + GENERATED_DIR, 'index.ts', ), })); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts index 452f5791af9..5163115c829 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts @@ -5,7 +5,6 @@ import { TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING, } from '@/cli/utilities/build/manifest/manifest-extract-config'; import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file'; -import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields'; import { type ApplicationConfig, type FrontComponentConfig, @@ -34,6 +33,7 @@ import { } from 'twenty-shared/application'; import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function'; import { assertUnreachable } from 'twenty-shared/utils'; +import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields'; const loadSources = async (appPath: string): Promise => { return await glob(['**/*.ts', '**/*.tsx'], { @@ -120,13 +120,14 @@ export const buildManifest = async ( filePath, }); - const objectFieldsWithDefaultFields = injectDefaultFieldsInObjectFields( - extract.config, - ); + const { + objectFields: objectFieldsWithDefaults, + fields: reverseRelationFields, + } = getDefaultFieldsInObjectFields(extract.config); const labelIdentifierFieldMetadataUniversalIdentifier = extract.config.labelIdentifierFieldMetadataUniversalIdentifier ?? - objectFieldsWithDefaultFields.find((field) => field.name === 'name') + objectFieldsWithDefaults.find((field) => field.name === 'name') ?.universalIdentifier; if (!labelIdentifierFieldMetadataUniversalIdentifier) { @@ -138,11 +139,12 @@ export const buildManifest = async ( const objectManifest: ObjectManifest = { ...extract.config, - fields: objectFieldsWithDefaultFields, + fields: objectFieldsWithDefaults, labelIdentifierFieldMetadataUniversalIdentifier, }; objects.push(objectManifest); + fields.push(...reverseRelationFields); errors.push(...extract.errors); objectsFilePaths.push(relativePath); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-update-checksums.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-update-checksums.ts index 735e33d288b..47c915409c1 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-update-checksums.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-update-checksums.ts @@ -1,6 +1,10 @@ import crypto from 'crypto'; import { relative } from 'path'; -import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application'; +import { + type Manifest, + OUTPUT_DIR, + API_CLIENT_DIR, +} from 'twenty-shared/application'; import { FileFolder } from 'twenty-shared/types'; import type { EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config'; @@ -97,7 +101,7 @@ export const manifestUpdateChecksums = ({ if ( fileFolder === FileFolder.Dependencies && - rootBuiltPath.startsWith('api-client/') + rootBuiltPath.startsWith(`${API_CLIENT_DIR}/`) ) { const entry = builtFileInfos.get(builtPath); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts index c42653191af..0f76ff986c9 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts @@ -1,7 +1,7 @@ import path, { relative } from 'path'; import chokidar, { type FSWatcher } from 'chokidar'; import { type EventName } from 'chokidar/handler.js'; -import { ASSETS_DIR } from 'twenty-shared/application'; +import { ASSETS_DIR, GENERATED_DIR } from 'twenty-shared/application'; export type ManifestWatcherOptions = { appPath: string; @@ -10,7 +10,7 @@ export type ManifestWatcherOptions = { const IGNORED_DIRECTORY_NAMES = new Set([ 'node_modules', - 'generated', + GENERATED_DIR, 'dist', '.twenty', ]); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/inject-default-fields-in-object-fields.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts similarity index 56% rename from packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/inject-default-fields-in-object-fields.spec.ts rename to packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts index 02a7988931f..614eed0baa8 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/inject-default-fields-in-object-fields.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts @@ -1,7 +1,9 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields'; +import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields'; import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields'; import type { ObjectConfig } from '@/sdk/objects/object-config'; +import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields'; +import { type ObjectFieldManifest } from 'twenty-shared/application'; const baseObjectConfig: ObjectConfig = { universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', @@ -12,16 +14,25 @@ const baseObjectConfig: ObjectConfig = { fields: [], }; -describe('injectDefaultFieldsInObjectFields', () => { +describe('getDefaultFieldsInObjectFields', () => { it('should return all default fields when objectConfig has no fields', () => { - const result = injectDefaultFieldsInObjectFields(baseObjectConfig); + const { objectFields, fields } = + getDefaultFieldsInObjectFields(baseObjectConfig); const defaultFields = getDefaultObjectFields(baseObjectConfig); + const { + objectFields: defaultRelationObjectFields, + fields: expectedReverseFields, + } = getDefaultRelationObjectFields(baseObjectConfig); - expect(result).toEqual(defaultFields); + expect(objectFields).toEqual([ + ...defaultFields, + ...defaultRelationObjectFields, + ]); + expect(fields).toEqual(expectedReverseFields); }); it('should preserve custom fields and append missing default fields', () => { - const customField = { + const customField: ObjectFieldManifest = { universalIdentifier: '11111111-1111-1111-1111-111111111111', name: 'customField', label: 'Custom Field', @@ -33,15 +44,19 @@ describe('injectDefaultFieldsInObjectFields', () => { fields: [customField], }; - const result = injectDefaultFieldsInObjectFields(objectConfig); + const { objectFields } = getDefaultFieldsInObjectFields(objectConfig); const defaultFields = getDefaultObjectFields(objectConfig); + const { objectFields: defaultRelationObjectFields } = + getDefaultRelationObjectFields(objectConfig); - expect(result[0]).toEqual(customField); - expect(result).toHaveLength(1 + defaultFields.length); + expect(objectFields[0]).toEqual(customField); + expect(objectFields).toHaveLength( + 1 + defaultFields.length + defaultRelationObjectFields.length, + ); }); it('should not inject a default field when a field with the same name exists', () => { - const customIdField = { + const customIdField: ObjectFieldManifest = { universalIdentifier: '22222222-2222-2222-2222-222222222222', name: 'id', label: 'Custom Id', @@ -53,16 +68,16 @@ describe('injectDefaultFieldsInObjectFields', () => { fields: [customIdField], }; - const result = injectDefaultFieldsInObjectFields(objectConfig); + const { objectFields } = getDefaultFieldsInObjectFields(objectConfig); - const idFields = result.filter((f) => f.name === 'id'); + const idFields = objectFields.filter((f) => f.name === 'id'); expect(idFields).toHaveLength(1); expect(idFields[0]).toEqual(customIdField); }); it('should skip multiple default fields when overridden by custom fields', () => { - const customFields = [ + const customFields: ObjectFieldManifest[] = [ { universalIdentifier: '33333333-3333-3333-3333-333333333333', name: 'id', @@ -88,23 +103,28 @@ describe('injectDefaultFieldsInObjectFields', () => { fields: customFields, }; - const result = injectDefaultFieldsInObjectFields(objectConfig); + const { objectFields } = getDefaultFieldsInObjectFields(objectConfig); const defaultFields = getDefaultObjectFields(objectConfig); + const { objectFields: defaultRelationObjectFields } = + getDefaultRelationObjectFields(objectConfig); const overriddenCount = defaultFields.filter((df) => customFields.some((cf) => cf.name === df.name), ).length; - expect(result).toHaveLength( - customFields.length + defaultFields.length - overriddenCount, + expect(objectFields).toHaveLength( + customFields.length + + defaultFields.length + + defaultRelationObjectFields.length - + overriddenCount, ); - expect(result.filter((f) => f.name === 'id')).toHaveLength(1); - expect(result.filter((f) => f.name === 'name')).toHaveLength(1); - expect(result.filter((f) => f.name === 'position')).toHaveLength(1); + expect(objectFields.filter((f) => f.name === 'id')).toHaveLength(1); + expect(objectFields.filter((f) => f.name === 'name')).toHaveLength(1); + expect(objectFields.filter((f) => f.name === 'position')).toHaveLength(1); }); it('should place custom fields before default fields in the result', () => { - const customField = { + const customField: ObjectFieldManifest = { universalIdentifier: '66666666-6666-6666-6666-666666666666', name: 'customField', label: 'Custom Field', @@ -116,13 +136,13 @@ describe('injectDefaultFieldsInObjectFields', () => { fields: [customField], }; - const result = injectDefaultFieldsInObjectFields(objectConfig); + const { objectFields } = getDefaultFieldsInObjectFields(objectConfig); - expect(result[0]).toEqual(customField); + expect(objectFields[0]).toEqual(customField); }); it('should not mutate the original objectConfig fields array', () => { - const customField = { + const customField: ObjectFieldManifest = { universalIdentifier: '77777777-7777-7777-7777-777777777777', name: 'customField', label: 'Custom Field', @@ -136,8 +156,18 @@ describe('injectDefaultFieldsInObjectFields', () => { const originalLength = objectConfig.fields.length; - injectDefaultFieldsInObjectFields(objectConfig); + getDefaultFieldsInObjectFields(objectConfig); expect(objectConfig.fields).toHaveLength(originalLength); }); + + it('should return reverse relation fields for each default relation', () => { + const { fields } = getDefaultFieldsInObjectFields(baseObjectConfig); + + expect(fields).toHaveLength(5); + + const fieldNames = fields.map((f) => f.name); + + expect(fieldNames).toContain('targetTestObject'); + }); }); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-object-fields.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-object-fields.spec.ts index 6fcd9d3a3ad..ca468c60f15 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-object-fields.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-object-fields.spec.ts @@ -22,7 +22,7 @@ describe('getDefaultObjectFields', () => { it('should return an array of 9 default fields', () => { const fields = getDefaultObjectFields(mockObjectConfig); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(9); }); it('should include an id field with UUID type', () => { @@ -59,15 +59,26 @@ describe('getDefaultObjectFields', () => { }); }); - it('should include updatedAt and deletedAt fields with DATE_TIME type', () => { + it('should include createdAt, updatedAt and deletedAt fields with DATE_TIME type', () => { const fields = getDefaultObjectFields(mockObjectConfig); - const updatedAtFields = fields.filter( - (field) => field.name === 'updatedAt', - ); + const createdAtField = fields.find((field) => field.name === 'createdAt'); + const updatedAtField = fields.find((field) => field.name === 'updatedAt'); const deletedAtField = fields.find((field) => field.name === 'deletedAt'); - expect(updatedAtFields).toHaveLength(1); - expect(updatedAtFields[0]).toEqual({ + expect(createdAtField).toBeDefined(); + expect(createdAtField).toEqual({ + name: 'createdAt', + label: 'Creation date', + description: 'Creation date', + icon: 'IconCalendar', + isNullable: false, + defaultValue: 'now', + type: FieldMetadataType.DATE_TIME, + universalIdentifier: expectedUniversalId('createdAt'), + }); + + expect(updatedAtField).toBeDefined(); + expect(updatedAtField).toEqual({ name: 'updatedAt', label: 'Last update', description: 'Last time the record was changed', @@ -121,6 +132,42 @@ describe('getDefaultObjectFields', () => { }); }); + it('should include a position field with POSITION type', () => { + const fields = getDefaultObjectFields(mockObjectConfig); + const positionField = fields.find((field) => field.name === 'position'); + + expect(positionField).toBeDefined(); + expect(positionField).toEqual({ + name: 'position', + label: 'Position', + description: 'Position', + icon: 'IconHierarchy2', + isNullable: false, + defaultValue: 0, + type: FieldMetadataType.POSITION, + universalIdentifier: expectedUniversalId('position'), + }); + }); + + it('should include a searchVector field with TS_VECTOR type', () => { + const fields = getDefaultObjectFields(mockObjectConfig); + const searchVectorField = fields.find( + (field) => field.name === 'searchVector', + ); + + expect(searchVectorField).toBeDefined(); + expect(searchVectorField).toEqual({ + name: 'searchVector', + label: 'Search vector', + description: 'Search vector', + icon: 'IconSearch', + isNullable: true, + defaultValue: null, + type: FieldMetadataType.TS_VECTOR, + universalIdentifier: expectedUniversalId('searchVector'), + }); + }); + it('should generate deterministic universalIdentifiers based on objectConfig', () => { const firstResult = getDefaultObjectFields(mockObjectConfig); const secondResult = getDefaultObjectFields(mockObjectConfig); @@ -160,6 +207,8 @@ describe('getDefaultObjectFields', () => { 'deletedAt', 'createdBy', 'updatedBy', + 'position', + 'searchVector', ]); }); }); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields.ts new file mode 100644 index 00000000000..954f4692b51 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields.ts @@ -0,0 +1,33 @@ +import type { ObjectConfig } from '@/sdk/objects/object-config'; +import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields'; +import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields'; +import { + type FieldManifest, + type ObjectFieldManifest, +} from 'twenty-shared/application'; + +export const getDefaultFieldsInObjectFields = ( + objectConfig: ObjectConfig, +): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => { + const defaultObjectFields = getDefaultObjectFields(objectConfig); + const { objectFields: defaultRelationObjectFields, fields: reverseFields } = + getDefaultRelationObjectFields(objectConfig); + + const objectConfigFieldNames = (objectConfig.fields ?? []).map((f) => f.name); + + const objectFieldsWithDefaults = [...objectConfig.fields]; + + for (const defaultField of defaultObjectFields) { + if (!objectConfigFieldNames.includes(defaultField.name)) { + objectFieldsWithDefaults.push(defaultField); + } + } + + for (const defaultRelationField of defaultRelationObjectFields) { + if (!objectConfigFieldNames.includes(defaultRelationField.name)) { + objectFieldsWithDefaults.push(defaultRelationField); + } + } + + return { objectFields: objectFieldsWithDefaults, fields: reverseFields }; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-object-fields.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-object-fields.ts index bea2c5be866..74fada0dec2 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-object-fields.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-object-fields.ts @@ -1,12 +1,12 @@ -import { type ObjectManifest } from 'twenty-shared/application'; import type { ObjectConfig } from '@/sdk/objects/object-config'; import { FieldMetadataType } from 'twenty-shared/types'; import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier'; +import { type ObjectFieldManifest } from 'twenty-shared/application'; export const getDefaultObjectFields = ( objectConfig: ObjectConfig, -): ObjectManifest['fields'] => { - const idField = { +): ObjectFieldManifest[] => { + const idField: ObjectFieldManifest = { name: 'id', label: 'Id', description: 'Id', @@ -20,7 +20,7 @@ export const getDefaultObjectFields = ( }), }; - const nameField = { + const nameField: ObjectFieldManifest = { name: 'name', label: 'Name', description: 'Name', @@ -34,7 +34,7 @@ export const getDefaultObjectFields = ( }), }; - const createdAtField = { + const createdAtField: ObjectFieldManifest = { name: 'createdAt', label: 'Creation date', description: 'Creation date', @@ -48,7 +48,7 @@ export const getDefaultObjectFields = ( }), }; - const updatedAtField = { + const updatedAtField: ObjectFieldManifest = { name: 'updatedAt', label: 'Last update', description: 'Last time the record was changed', @@ -62,7 +62,7 @@ export const getDefaultObjectFields = ( }), }; - const deletedAtField = { + const deletedAtField: ObjectFieldManifest = { name: 'deletedAt', label: 'Deleted at', description: 'Deletion date', @@ -76,7 +76,7 @@ export const getDefaultObjectFields = ( }), }; - const createdByField = { + const createdByField: ObjectFieldManifest = { name: 'createdBy', label: 'Created by', description: 'The creator of the record', @@ -90,7 +90,7 @@ export const getDefaultObjectFields = ( }), }; - const updatedByField = { + const updatedByField: ObjectFieldManifest = { name: 'updatedBy', label: 'Updated by', description: 'The workspace member who last updated the record', @@ -104,6 +104,34 @@ export const getDefaultObjectFields = ( }), }; + const positionField: ObjectFieldManifest = { + name: 'position', + label: 'Position', + description: 'Position', + icon: 'IconHierarchy2', + isNullable: false, + defaultValue: 0, + type: FieldMetadataType.POSITION, + universalIdentifier: generateDefaultFieldUniversalIdentifier({ + objectConfig, + fieldName: 'position', + }), + }; + + const searchVectorField: ObjectFieldManifest = { + name: 'searchVector', + label: 'Search vector', + icon: 'IconSearch', + description: 'Search vector', + isNullable: true, + defaultValue: null, + type: FieldMetadataType.TS_VECTOR, + universalIdentifier: generateDefaultFieldUniversalIdentifier({ + objectConfig, + fieldName: 'searchVector', + }), + }; + return [ idField, nameField, @@ -112,5 +140,7 @@ export const getDefaultObjectFields = ( deletedAtField, createdByField, updatedByField, + positionField, + searchVectorField, ]; }; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-relation-object-fields.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-relation-object-fields.ts new file mode 100644 index 00000000000..6bd5fa662e4 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-relation-object-fields.ts @@ -0,0 +1,178 @@ +import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier'; +import { RelationType } from '@/sdk'; +import type { ObjectConfig } from '@/sdk/objects/object-config'; +import { + type FieldManifest, + type ObjectFieldManifest, +} from 'twenty-shared/application'; +import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; +import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; + +type DefaultRelationConfig = { + fieldName: string; + label: string; + targetFieldName: (objectConfig: ObjectConfig) => string; + targetLabel: (objectConfig: ObjectConfig) => string; + icon: string; + targetIcon: string; + standardObjectKey: keyof typeof STANDARD_OBJECTS; +} & ( + | { targetFieldType: FieldMetadataType.RELATION } + | { targetFieldType: FieldMetadataType.MORPH_RELATION; morphId: string } +); + +const DEFAULT_DEFAULT_RELATION = { + targetFieldName: (objectConfig: ObjectConfig) => + `target${capitalize(objectConfig.nameSingular)}`, + targetLabel: (objectConfig: ObjectConfig) => + capitalize(objectConfig.nameSingular), + icon: 'IconBuildingSkyscraper', +}; + +const DEFAULT_RELATION_CONFIGS = [ + { + ...DEFAULT_DEFAULT_RELATION, + fieldName: 'timelineActivities', + label: 'Timeline Activities', + targetIcon: 'IconTimelineEvent', + targetFieldType: FieldMetadataType.MORPH_RELATION, + standardObjectKey: 'timelineActivity', + morphId: STANDARD_OBJECTS.timelineActivity.morphIds.targetMorphId.morphId, + }, + { + ...DEFAULT_DEFAULT_RELATION, + fieldName: 'favorites', + label: 'Favorites', + icon: 'IconBuildingSkyscraper', + targetIcon: 'IconHeart', + targetFieldType: FieldMetadataType.RELATION, + standardObjectKey: 'favorite', + }, + { + ...DEFAULT_DEFAULT_RELATION, + fieldName: 'attachments', + label: 'Attachments', + icon: 'IconBuildingSkyscraper', + targetIcon: 'IconFileImport', + targetFieldType: FieldMetadataType.MORPH_RELATION, + standardObjectKey: 'attachment', + morphId: STANDARD_OBJECTS.attachment.morphIds.targetMorphId.morphId, + }, + { + ...DEFAULT_DEFAULT_RELATION, + fieldName: 'noteTargets', + label: 'Note Targets', + icon: 'IconBuildingSkyscraper', + targetIcon: 'IconCheckbox', + targetFieldType: FieldMetadataType.MORPH_RELATION, + standardObjectKey: 'noteTarget', + morphId: STANDARD_OBJECTS.noteTarget.morphIds.targetMorphId.morphId, + }, + { + ...DEFAULT_DEFAULT_RELATION, + fieldName: 'taskTargets', + label: 'Task Targets', + icon: 'IconBuildingSkyscraper', + targetIcon: 'IconCheckbox', + targetFieldType: FieldMetadataType.MORPH_RELATION, + standardObjectKey: 'taskTarget', + morphId: STANDARD_OBJECTS.taskTarget.morphIds.targetMorphId.morphId, + }, +] as const satisfies DefaultRelationConfig[]; + +const buildReverseField = ({ + config, + forwardFieldUniversalIdentifier, + objectConfig, + universalIdentifier, +}: { + config: DefaultRelationConfig; + objectConfig: ObjectConfig; + universalIdentifier: string; + forwardFieldUniversalIdentifier: string; +}): FieldManifest => { + const standardObject = STANDARD_OBJECTS[config.standardObjectKey]; + + const commonFields = { + name: config.targetFieldName(objectConfig), + label: config.targetLabel(objectConfig), + description: `${config.targetLabel(objectConfig)} ${objectConfig.labelSingular}`, + icon: config.targetIcon, + isNullable: true, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: RelationOnDeleteAction.SET_NULL, + joinColumnName: `target${capitalize(objectConfig.nameSingular)}Id`, + }, + universalIdentifier, + objectUniversalIdentifier: standardObject.universalIdentifier, + relationTargetFieldMetadataUniversalIdentifier: + forwardFieldUniversalIdentifier, + relationTargetObjectMetadataUniversalIdentifier: + objectConfig.universalIdentifier, + }; + + if (config.targetFieldType === FieldMetadataType.MORPH_RELATION) { + return { + ...commonFields, + type: config.targetFieldType, + morphId: config.morphId, + }; + } + + return { + ...commonFields, + type: config.targetFieldType, + }; +}; + +export const getDefaultRelationObjectFields = ( + objectConfig: ObjectConfig, +): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => { + const objectFields: ObjectFieldManifest[] = []; + const fields: FieldManifest[] = []; + + for (const config of DEFAULT_RELATION_CONFIGS) { + const standardObject = STANDARD_OBJECTS[config.standardObjectKey]; + + const forwardFieldUniversalIdentifier = + generateDefaultFieldUniversalIdentifier({ + objectConfig, + fieldName: config.fieldName, + }); + + const reverseFieldUniversalIdentifier = + generateDefaultFieldUniversalIdentifier({ + objectConfig, + fieldName: `${config.fieldName}Inverse`, + }); + + const forwardField: ObjectFieldManifest = { + name: config.fieldName, + label: config.label, + description: `${objectConfig.labelPlural} tied to the ${config.targetLabel(objectConfig)}`, + icon: config.icon, + isNullable: false, + type: FieldMetadataType.RELATION, + universalSettings: { relationType: RelationType.ONE_TO_MANY }, + universalIdentifier: forwardFieldUniversalIdentifier, + relationTargetFieldMetadataUniversalIdentifier: + reverseFieldUniversalIdentifier, + relationTargetObjectMetadataUniversalIdentifier: + standardObject.universalIdentifier, + }; + + const reverseField = buildReverseField({ + config, + objectConfig, + universalIdentifier: reverseFieldUniversalIdentifier, + forwardFieldUniversalIdentifier, + }); + + objectFields.push(forwardField); + fields.push(reverseField); + } + + return { objectFields, fields }; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields.ts deleted file mode 100644 index 515fd7fdcf5..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ObjectConfig } from '@/sdk/objects/object-config'; -import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields'; - -export const injectDefaultFieldsInObjectFields = ( - objectConfig: ObjectConfig, -) => { - const defaultObjectFields = getDefaultObjectFields(objectConfig); - - const objectConfigFieldNames = (objectConfig.fields ?? []).map((f) => f.name); - - const fieldsWithDefaultFields = [...objectConfig.fields]; - - for (const defaultField of defaultObjectFields) { - if (!objectConfigFieldNames.includes(defaultField.name)) { - fieldsWithDefaultFields.push(defaultField); - } - } - - return fieldsWithDefaultFields; -}; diff --git a/packages/twenty-sdk/src/cli/utilities/client/client-service.ts b/packages/twenty-sdk/src/cli/utilities/client/client-service.ts index 4c9eaec3a30..dc1241d046f 100644 --- a/packages/twenty-sdk/src/cli/utilities/client/client-service.ts +++ b/packages/twenty-sdk/src/cli/utilities/client/client-service.ts @@ -5,6 +5,7 @@ import { join } from 'path'; import { DEFAULT_API_KEY_NAME, DEFAULT_API_URL_NAME, + GENERATED_DIR, } from 'twenty-shared/application'; export class ClientService { @@ -54,7 +55,7 @@ export class ClientService { } private resolveGeneratedPath(appPath: string): string { - return join(appPath, 'node_modules', 'twenty-sdk', 'generated'); + return join(appPath, 'node_modules', 'twenty-sdk', GENERATED_DIR); } private async injectTwentyClient(output: string) { diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts index d1cc0446ad9..2982e8ae60e 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts @@ -6,7 +6,11 @@ import { FileUploader } from '@/cli/utilities/file/file-uploader'; import crypto from 'crypto'; import * as fs from 'fs-extra'; import { join } from 'path'; -import { OUTPUT_DIR } from 'twenty-shared/application'; +import { + OUTPUT_DIR, + GENERATED_DIR, + API_CLIENT_DIR, +} from 'twenty-shared/application'; import { FileFolder } from 'twenty-shared/types'; const API_CLIENT_FILES = ['types.ts', 'schema.ts']; @@ -114,14 +118,14 @@ export class UploadFilesOrchestratorStep { appPath, 'node_modules', 'twenty-sdk', - 'generated', + GENERATED_DIR, ); if (!(await fs.pathExists(generatedDir))) { return; } - const outputDir = join(appPath, OUTPUT_DIR, 'api-client'); + const outputDir = join(appPath, OUTPUT_DIR, API_CLIENT_DIR); await fs.ensureDir(outputDir); @@ -137,8 +141,8 @@ export class UploadFilesOrchestratorStep { const content = await fs.readFile(absoluteSourcePath); const checksum = crypto.createHash('md5').update(content).digest('hex'); - const builtPath = join(OUTPUT_DIR, 'api-client', fileName); - const sourcePath = join('api-client', fileName); + const builtPath = join(OUTPUT_DIR, API_CLIENT_DIR, fileName); + const sourcePath = join(API_CLIENT_DIR, fileName); this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, { checksum, diff --git a/packages/twenty-sdk/vitest.config.ts b/packages/twenty-sdk/vitest.config.ts index e9464ac860d..ec2117b65a2 100644 --- a/packages/twenty-sdk/vitest.config.ts +++ b/packages/twenty-sdk/vitest.config.ts @@ -16,11 +16,7 @@ export default defineConfig({ 'src/**/__integration__/**/*.{test,spec}.{ts,tsx}', 'src/**/*.{test,spec}.{ts,tsx}', ], - exclude: [ - '**/node_modules/**', - '**/.git/**', - '**/__e2e__/**', - ], + exclude: ['**/node_modules/**', '**/.git/**', '**/__e2e__/**'], coverage: { provider: 'v8', include: ['src/**/*.{ts,tsx}'], diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts index e2840e69b81..820f95579f2 100644 --- a/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts @@ -1,23 +1,25 @@ import { Injectable, Logger } from '@nestjs/common'; import { type Manifest, type RoleManifest } from 'twenty-shared/application'; +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; import { isDefined } from 'twenty-shared/utils'; import { ApplicationException, ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; -import { APPLICATION_MANIFEST_METADATA_NAMES } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/build-from-to-all-universal-flat-entity-maps.util'; import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util'; -import { getSubApplicationFromToAllFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util'; +import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-application-sub-all-flat-entity-maps.util'; import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service'; import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type'; @@ -48,32 +50,42 @@ export class ApplicationManifestMigrationService { }): Promise { const now = new Date().toISOString(); - const toAllUniversalFlatEntityMaps = - computeApplicationManifestAllUniversalFlatEntityMaps({ - manifest, - applicationUniversalIdentifier: - ownerFlatApplication.universalIdentifier, - now, - }); + const { twentyStandardFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); const cacheResult = await this.workspaceCacheService.getOrRecompute( workspaceId, [ - ...APPLICATION_MANIFEST_METADATA_NAMES.map( - getMetadataFlatEntityMapsKey, - ), + ...Object.values(ALL_METADATA_NAME).map(getMetadataFlatEntityMapsKey), 'featureFlagsMap', ], ); - const { featureFlagsMap, ...fromAllFlatEntityMaps } = cacheResult; + const { featureFlagsMap, ...existingAllFlatEntityMaps } = cacheResult; - const fromToAllFlatEntityMaps = getSubApplicationFromToAllFlatEntityMaps({ - applicationId: ownerFlatApplication.id, - fromAllFlatEntityMaps, - toAllUniversalFlatEntityMaps, + const toAllUniversalFlatEntityMaps = + computeApplicationManifestAllUniversalFlatEntityMaps({ + manifest, + ownerFlatApplication, + now, + }); + + const fromAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({ + applicationIds: [ownerFlatApplication.id], + fromAllFlatEntityMaps: existingAllFlatEntityMaps, }); + const dependencyAllFlatEntityMaps = + ownerFlatApplication.universalIdentifier === + TWENTY_STANDARD_APPLICATION.universalIdentifier + ? undefined + : getApplicationSubAllFlatEntityMaps({ + applicationIds: [twentyStandardFlatApplication.id], + fromAllFlatEntityMaps: existingAllFlatEntityMaps, + }); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo( { @@ -83,8 +95,12 @@ export class ApplicationManifestMigrationService { applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, }, - fromToAllFlatEntityMaps, + fromToAllFlatEntityMaps: buildFromToAllUniversalFlatEntityMaps({ + fromAllFlatEntityMaps, + toAllUniversalFlatEntityMaps, + }), workspaceId, + dependencyAllFlatEntityMaps, additionalCacheDataMaps: { featureFlagsMap }, }, ); diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts index 1eec8ecf890..e25814e8aa7 100644 --- a/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts @@ -3,22 +3,23 @@ import { Injectable, Logger } from '@nestjs/common'; import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { PackageJson } from 'type-fest'; +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationException, ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; -import { APPLICATION_MANIFEST_METADATA_NAMES } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input'; import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/services/application-manifest-migration.service'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/build-from-to-all-universal-flat-entity-maps.util'; +import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-application-sub-all-flat-entity-maps.util'; import { getDefaultApplicationPackageFields } from 'src/engine/core-modules/application/utils/get-default-application-package-fields.util'; -import { getEmptyApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util'; -import { getSubApplicationFromToAllFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util'; import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant'; import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; @@ -152,7 +153,7 @@ export class ApplicationSyncService { ); } - const flatEntityMapsCacheKeys = APPLICATION_MANIFEST_METADATA_NAMES.map( + const flatEntityMapsCacheKeys = Object.values(ALL_METADATA_NAME).map( getMetadataFlatEntityMapsKey, ); @@ -163,11 +164,16 @@ export class ApplicationSyncService { const { featureFlagsMap, ...fromAllFlatEntityMaps } = cacheResult; - const fromToAllFlatEntityMaps = getSubApplicationFromToAllFlatEntityMaps({ - applicationId: application.id, - fromAllFlatEntityMaps, - toAllUniversalFlatEntityMaps: - getEmptyApplicationManifestAllUniversalFlatEntityMaps(), + const applicationFromAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps( + { + applicationIds: [application.id], + fromAllFlatEntityMaps, + }, + ); + + const fromToAllFlatEntityMaps = buildFromToAllUniversalFlatEntityMaps({ + fromAllFlatEntityMaps: applicationFromAllFlatEntityMaps, + toAllUniversalFlatEntityMaps: createEmptyAllFlatEntityMaps(), }); const validateAndBuildResult = diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/build-from-to-all-universal-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/build-from-to-all-universal-flat-entity-maps.util.ts new file mode 100644 index 00000000000..ccc1f69e1e6 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/build-from-to-all-universal-flat-entity-maps.util.ts @@ -0,0 +1,31 @@ +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +import { type FromToAllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; + +export const buildFromToAllUniversalFlatEntityMaps = ({ + fromAllFlatEntityMaps, + toAllUniversalFlatEntityMaps, +}: { + fromAllFlatEntityMaps: AllFlatEntityMaps; + toAllUniversalFlatEntityMaps: AllFlatEntityMaps; +}): FromToAllUniversalFlatEntityMaps => { + const fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps = {}; + + for (const metadataName of Object.values(ALL_METADATA_NAME)) { + const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName); + const fromFlatEntityMaps = fromAllFlatEntityMaps[flatEntityMapsKey]; + const toFlatEntityMaps = toAllUniversalFlatEntityMaps[flatEntityMapsKey]; + + const fromTo = { + from: fromFlatEntityMaps, + to: toFlatEntityMaps, + }; + + // @ts-expect-error Metadata flat entity maps cache key and metadataName colliding + fromToAllFlatEntityMaps[flatEntityMapsKey] = fromTo; + } + + return fromToAllFlatEntityMaps; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts index 87eb4636c3c..ae706004fdc 100644 --- a/packages/twenty-server/src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts @@ -1,6 +1,6 @@ import { type Manifest } from 'twenty-shared/application'; -import { type ApplicationManifestMetadataName } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/utils/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util'; import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util'; import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/utils/from-front-component-manifest-to-universal-flat-front-component.util'; @@ -18,140 +18,115 @@ import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/e import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/utils/from-view-filter-manifest-to-universal-flat-view-filter.util'; import { fromViewGroupManifestToUniversalFlatViewGroup } from 'src/engine/core-modules/application/utils/from-view-group-manifest-to-universal-flat-view-group.util'; import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/utils/from-view-manifest-to-universal-flat-view.util'; -import { getEmptyApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util'; +import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; -import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key'; -import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type'; -import { addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util'; - -export type ApplicationManifestAllUniversalFlatEntityMaps = Pick< - AllUniversalFlatEntityMaps, - MetadataToFlatEntityMapsKey ->; - -export type ApplicationManifestAllFlatEntityMaps = Pick< - AllFlatEntityMaps, - MetadataToFlatEntityMapsKey ->; +import { addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-maps-through-mutation-or-throw.util'; export const computeApplicationManifestAllUniversalFlatEntityMaps = ({ manifest, - applicationUniversalIdentifier, + ownerFlatApplication, now, }: { manifest: Manifest; - applicationUniversalIdentifier: string; + ownerFlatApplication: FlatApplication; now: string; -}): ApplicationManifestAllUniversalFlatEntityMaps => { - const allUniversalFlatEntityMaps = - getEmptyApplicationManifestAllUniversalFlatEntityMaps(); +}): AllFlatEntityMaps => { + const allUniversalFlatEntityMaps = createEmptyAllFlatEntityMaps(); + + const { universalIdentifier: applicationUniversalIdentifier } = + ownerFlatApplication; for (const objectManifest of manifest.objects) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'objectMetadata', - universalFlatEntity: fromObjectManifestToUniversalFlatObjectMetadata({ - objectManifest, + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromObjectManifestToUniversalFlatObjectMetadata({ + objectManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatObjectMetadataMaps, + }); + + for (const fieldManifest of objectManifest.fields) { + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({ + fieldManifest: { + ...fieldManifest, + objectUniversalIdentifier: objectManifest.universalIdentifier, + }, applicationUniversalIdentifier, now, }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); - - for (const fieldManifest of objectManifest.fields) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'fieldMetadata', - universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({ - fieldManifest: { - ...fieldManifest, - objectUniversalIdentifier: objectManifest.universalIdentifier, - }, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatFieldMetadataMaps, + }); } } for (const fieldManifest of manifest.fields) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'fieldMetadata', - universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({ - fieldManifest: fieldManifest, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({ + fieldManifest: fieldManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatFieldMetadataMaps, + }); } for (const logicFunctionManifest of manifest.logicFunctions) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'logicFunction', - universalFlatEntity: - fromLogicFunctionManifestToUniversalFlatLogicFunction({ - logicFunctionManifest, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: + fromLogicFunctionManifestToUniversalFlatLogicFunction({ + logicFunctionManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatLogicFunctionMaps, + }); } for (const frontComponentManifest of manifest.frontComponents) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'frontComponent', + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: + fromFrontComponentManifestToUniversalFlatFrontComponent({ + frontComponentManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatFrontComponentMaps, + }); + + if (frontComponentManifest.command) { + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ universalFlatEntity: - fromFrontComponentManifestToUniversalFlatFrontComponent({ - frontComponentManifest, + fromCommandMenuItemManifestToUniversalFlatCommandMenuItem({ + commandMenuItemManifest: { + ...frontComponentManifest.command, + frontComponentUniversalIdentifier: + frontComponentManifest.universalIdentifier, + }, applicationUniversalIdentifier, now, }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); - - if (frontComponentManifest.command) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'commandMenuItem', - universalFlatEntity: - fromCommandMenuItemManifestToUniversalFlatCommandMenuItem({ - commandMenuItemManifest: { - ...frontComponentManifest.command, - frontComponentUniversalIdentifier: - frontComponentManifest.universalIdentifier, - }, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatCommandMenuItemMaps, + }); } } for (const roleManifest of manifest.roles) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'role', - universalFlatEntity: fromRoleManifestToUniversalFlatRole({ - roleManifest, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromRoleManifestToUniversalFlatRole({ + roleManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatRoleMaps, + }); } for (const skillManifest of manifest.skills ?? []) { @@ -169,157 +144,135 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({ } for (const viewManifest of manifest.views ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'view', - universalFlatEntity: fromViewManifestToUniversalFlatView({ - viewManifest, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromViewManifestToUniversalFlatView({ + viewManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatViewMaps, + }); for (const viewFieldGroupManifest of viewManifest.fieldGroups ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'viewFieldGroup', - universalFlatEntity: - fromViewFieldGroupManifestToUniversalFlatViewFieldGroup({ - viewFieldGroupManifest, - viewUniversalIdentifier: viewManifest.universalIdentifier, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: + fromViewFieldGroupManifestToUniversalFlatViewFieldGroup({ + viewFieldGroupManifest, + viewUniversalIdentifier: viewManifest.universalIdentifier, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatViewFieldGroupMaps, + }); } for (const viewFieldManifest of viewManifest.fields ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'viewField', - universalFlatEntity: fromViewFieldManifestToUniversalFlatViewField({ - viewFieldManifest, - viewUniversalIdentifier: viewManifest.universalIdentifier, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromViewFieldManifestToUniversalFlatViewField({ + viewFieldManifest, + viewUniversalIdentifier: viewManifest.universalIdentifier, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatViewFieldMaps, + }); } for (const viewFilterGroupManifest of viewManifest.filterGroups ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'viewFilterGroup', - universalFlatEntity: - fromViewFilterGroupManifestToUniversalFlatViewFilterGroup({ - viewFilterGroupManifest, - viewUniversalIdentifier: viewManifest.universalIdentifier, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: + fromViewFilterGroupManifestToUniversalFlatViewFilterGroup({ + viewFilterGroupManifest, + viewUniversalIdentifier: viewManifest.universalIdentifier, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatViewFilterGroupMaps, + }); } for (const viewFilterManifest of viewManifest.filters ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'viewFilter', - universalFlatEntity: fromViewFilterManifestToUniversalFlatViewFilter({ - viewFilterManifest, - viewUniversalIdentifier: viewManifest.universalIdentifier, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromViewFilterManifestToUniversalFlatViewFilter({ + viewFilterManifest, + viewUniversalIdentifier: viewManifest.universalIdentifier, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatViewFilterMaps, + }); } for (const viewGroupManifest of viewManifest.groups ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'viewGroup', - universalFlatEntity: fromViewGroupManifestToUniversalFlatViewGroup({ - viewGroupManifest, - viewUniversalIdentifier: viewManifest.universalIdentifier, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromViewGroupManifestToUniversalFlatViewGroup({ + viewGroupManifest, + viewUniversalIdentifier: viewManifest.universalIdentifier, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatViewGroupMaps, + }); } } for (const navigationMenuItemManifest of manifest.navigationMenuItems ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'navigationMenuItem', - universalFlatEntity: - fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem({ - navigationMenuItemManifest, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); - } - - for (const pageLayoutManifest of manifest.pageLayouts ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'pageLayout', - universalFlatEntity: fromPageLayoutManifestToUniversalFlatPageLayout({ - pageLayoutManifest, + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: + fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem({ + navigationMenuItemManifest, applicationUniversalIdentifier, now, }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatNavigationMenuItemMaps, + }); + } + + for (const pageLayoutManifest of manifest.pageLayouts ?? []) { + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: fromPageLayoutManifestToUniversalFlatPageLayout({ + pageLayoutManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatPageLayoutMaps, + }); for (const pageLayoutTabManifest of pageLayoutManifest.tabs ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'pageLayoutTab', - universalFlatEntity: - fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({ - pageLayoutTabManifest, - pageLayoutUniversalIdentifier: - pageLayoutManifest.universalIdentifier, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: + fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({ + pageLayoutTabManifest, + pageLayoutUniversalIdentifier: + pageLayoutManifest.universalIdentifier, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatPageLayoutTabMaps, + }); for (const pageLayoutWidgetManifest of pageLayoutTabManifest.widgets ?? []) { - addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( - { - metadataName: 'pageLayoutWidget', - universalFlatEntity: - fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({ - pageLayoutWidgetManifest, - pageLayoutTabUniversalIdentifier: - pageLayoutTabManifest.universalIdentifier, - applicationUniversalIdentifier, - now, - }), - universalFlatEntityAndRelatedMapsToMutate: - allUniversalFlatEntityMaps, - }, - ); + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: + fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({ + pageLayoutWidgetManifest, + pageLayoutTabUniversalIdentifier: + pageLayoutTabManifest.universalIdentifier, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatPageLayoutWidgetMaps, + }); } } } diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts index 9fc6ad9f141..a6b1f7d21dd 100644 --- a/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts @@ -1,8 +1,8 @@ +import { type FieldManifest } from 'twenty-shared/application'; import { - type FieldManifest, - type RelationFieldManifest, -} from 'twenty-shared/application'; -import { type FieldMetadataType } from 'twenty-shared/types'; + FieldMetadataType, + type RelationAndMorphRelationFieldMetadataType, +} from 'twenty-shared/types'; import { ApplicationException, @@ -14,12 +14,12 @@ import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; const isRelationFieldManifest = ( - fieldManifest: FieldManifest, -): fieldManifest is RelationFieldManifest => + fieldManifest: FieldManifest, +): fieldManifest is FieldManifest => isMorphOrRelationFieldMetadataType(fieldManifest.type); const getRelationTargetUniversalIdentifiers = ( - fieldManifest: FieldManifest, + fieldManifest: FieldManifest, ): { relationTargetFieldMetadataUniversalIdentifier: string | null; relationTargetObjectMetadataUniversalIdentifier: string | null; @@ -54,7 +54,7 @@ export const fromFieldManifestToUniversalFlatFieldMetadata = ({ applicationUniversalIdentifier, now, }: { - fieldManifest: FieldManifest & { + fieldManifest: FieldManifest & { objectUniversalIdentifier: string; }; applicationUniversalIdentifier: string; @@ -85,7 +85,10 @@ export const fromFieldManifestToUniversalFlatFieldMetadata = ({ isNullable: fieldManifest.isNullable ?? true, isUnique: false, isLabelSyncedWithName: false, - morphId: null, + morphId: + fieldManifest.type === FieldMetadataType.MORPH_RELATION + ? (fieldManifest.morphId ?? null) + : null, objectMetadataUniversalIdentifier: fieldManifest.objectUniversalIdentifier, relationTargetFieldMetadataUniversalIdentifier, relationTargetObjectMetadataUniversalIdentifier, diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/get-application-sub-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/get-application-sub-all-flat-entity-maps.util.ts new file mode 100644 index 00000000000..b9696e45e77 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/get-application-sub-all-flat-entity-maps.util.ts @@ -0,0 +1,35 @@ +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +import { getSubFlatEntityMapsByApplicationIdsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-ids-or-throw.util'; + +export const getApplicationSubAllFlatEntityMaps = ({ + applicationIds, + fromAllFlatEntityMaps, +}: { + applicationIds: string[]; + fromAllFlatEntityMaps: AllFlatEntityMaps; +}): AllFlatEntityMaps => { + const emptyAllFlatEntityMaps = createEmptyAllFlatEntityMaps(); + + for (const metadataName of Object.values(ALL_METADATA_NAME)) { + const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName); + const fromFlatEntityMaps = fromAllFlatEntityMaps[flatEntityMapsKey]; + + const applicationSubFlatEntityMaps = + getSubFlatEntityMapsByApplicationIdsOrThrow< + MetadataFlatEntity + >({ + applicationIds, + flatEntityMaps: fromFlatEntityMaps, + }); + + // @ts-expect-error Metadata flat entity maps cache key and metadataName colliding + emptyAllFlatEntityMaps[flatEntityMapsKey] = applicationSubFlatEntityMaps; + } + + return emptyAllFlatEntityMaps; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util.ts deleted file mode 100644 index 12dde95c7d2..00000000000 --- a/packages/twenty-server/src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { APPLICATION_MANIFEST_METADATA_NAMES } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; -import { type ApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util'; -import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; - -export const getEmptyApplicationManifestAllUniversalFlatEntityMaps = - (): ApplicationManifestAllUniversalFlatEntityMaps => { - const result = {} as ApplicationManifestAllUniversalFlatEntityMaps; - - for (const metadataName of APPLICATION_MANIFEST_METADATA_NAMES) { - const key = getMetadataFlatEntityMapsKey(metadataName); - - result[key] = { byUniversalIdentifier: {} }; - } - - return result; - }; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util.ts deleted file mode 100644 index 26548def2fd..00000000000 --- a/packages/twenty-server/src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { type FromTo } from 'twenty-shared/types'; - -import { APPLICATION_MANIFEST_METADATA_NAMES } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; -import { - type ApplicationManifestAllFlatEntityMaps, - type ApplicationManifestAllUniversalFlatEntityMaps, -} from 'src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util'; -import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; -import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { getSubFlatEntityMapsByApplicationIdsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-ids-or-throw.util'; -import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type'; - -export type FromToApplicationManifestAllUniversalFlatEntityMaps = { - [P in keyof ApplicationManifestAllFlatEntityMaps]?: FromTo< - AllUniversalFlatEntityMaps[P] - >; -}; - -export const getSubApplicationFromToAllFlatEntityMaps = ({ - applicationId, - fromAllFlatEntityMaps, - toAllUniversalFlatEntityMaps, -}: { - applicationId: string; - fromAllFlatEntityMaps: ApplicationManifestAllFlatEntityMaps; - toAllUniversalFlatEntityMaps: ApplicationManifestAllUniversalFlatEntityMaps; -}): FromToApplicationManifestAllUniversalFlatEntityMaps => { - const fromToAllFlatEntityMaps: FromToApplicationManifestAllUniversalFlatEntityMaps = - {}; - - for (const metadataName of APPLICATION_MANIFEST_METADATA_NAMES) { - const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName); - const fromFlatEntityMaps = fromAllFlatEntityMaps[flatEntityMapsKey]; - const toFlatEntityMaps = toAllUniversalFlatEntityMaps[flatEntityMapsKey]; - - const fromTo = { - from: getSubFlatEntityMapsByApplicationIdsOrThrow< - MetadataFlatEntity - >({ - applicationIds: [applicationId], - flatEntityMaps: fromFlatEntityMaps, - }), - to: toFlatEntityMaps, - }; - - // @ts-expect-error Metadata flat entity maps cache key and metadataName colliding - fromToAllFlatEntityMaps[flatEntityMapsKey] = fromTo; - } - - return fromToAllFlatEntityMaps; -}; diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/local.driver.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/local.driver.ts index bce2968edd5..ac5a17fca99 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/local.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/local.driver.ts @@ -12,6 +12,7 @@ import { type FlatApplication } from 'src/engine/core-modules/application/types/ import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-executor-tmpdir-folder'; import { ConsoleListener } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/intercept-console'; import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager'; +import { HANDLER_NAME_REGEX } from 'src/engine/metadata-modules/logic-function/constants/handler.contant'; import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto'; import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/application/utils/copy-yarn-engine-and-build-dependencies'; import type { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service'; @@ -215,9 +216,9 @@ export class LocalDriver implements LogicFunctionDriver { builtFileAbsPath: string; handlerName: string; }) { - if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(handlerName)) { + if (!HANDLER_NAME_REGEX.test(handlerName)) { throw new Error( - `Invalid handlerName "${handlerName}": must be a valid JavaScript identifier`, + `Invalid handlerName "${handlerName}": must be a valid JavaScript identifier or dotted path`, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/constants/handler.contant.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/constants/handler.contant.ts index 66c52b98972..d10673e25ca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/constants/handler.contant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/constants/handler.contant.ts @@ -1,3 +1,8 @@ export const DEFAULT_BUILT_HANDLER_PATH = 'src/index.mjs'; export const DEFAULT_SOURCE_HANDLER_PATH = 'src/index.ts'; export const DEFAULT_HANDLER_NAME = 'main'; + +// Matches a valid JS identifier or dotted member expression +// e.g. "main", "handler", "default.config.handler", "exports.run" +export const HANDLER_NAME_REGEX = + /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/; diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/dtos/logic-function-source.input.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/dtos/logic-function-source.input.ts index 1973ca38d1e..6a53bb9ef79 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/dtos/logic-function-source.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/dtos/logic-function-source.input.ts @@ -3,6 +3,8 @@ import { Field, InputType } from '@nestjs/graphql'; import { IsObject, IsString, Matches } from 'class-validator'; import graphqlTypeJson from 'graphql-type-json'; +import { HANDLER_NAME_REGEX } from 'src/engine/metadata-modules/logic-function/constants/handler.contant'; + @InputType() export class LogicFunctionSourceInput { @IsString() @@ -14,8 +16,8 @@ export class LogicFunctionSourceInput { toolInputSchema: object; @IsString() - @Matches(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/, { - message: 'handlerName must be a valid JavaScript identifier', + @Matches(HANDLER_NAME_REGEX, { + message: 'handlerName must be a valid JavaScript identifier or dotted path', }) @Field({ nullable: false }) handlerName: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input.ts index ee7cc84c5cb..7ccbbf1e015 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input.ts @@ -22,6 +22,7 @@ import { } from 'twenty-shared/application'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { HANDLER_NAME_REGEX } from 'src/engine/metadata-modules/logic-function/constants/handler.contant'; import type { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; @InputType() @@ -54,8 +55,8 @@ class UpdateLogicFunctionFromSourceInputUpdates { toolInputSchema?: object; @IsString() - @Matches(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/, { - message: 'handlerName must be a valid JavaScript identifier', + @Matches(HANDLER_NAME_REGEX, { + message: 'handlerName must be a valid JavaScript identifier or dotted path', }) @Field({ nullable: true }) @IsOptional() diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts index 106fafa44cd..d44fc1cc2d0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts @@ -1,4 +1,7 @@ -import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; +import { + STANDARD_OBJECTS, + DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS, +} from 'twenty-shared/metadata'; import { FieldMetadataType } from 'twenty-shared/types'; import { capitalize, isDefined } from 'twenty-shared/utils'; @@ -20,14 +23,6 @@ import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-mi import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; -const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [ - 'timelineActivity', - 'favorite', - 'attachment', - 'noteTarget', - 'taskTarget', -] as const satisfies (keyof typeof STANDARD_OBJECTS)[]; - const morphIdByRelationObjectNameSingular = { timelineActivity: STANDARD_OBJECTS.timelineActivity.morphIds.targetMorphId.morphId, diff --git a/packages/twenty-shared/src/application/constants/ApiClientDirectory.ts b/packages/twenty-shared/src/application/constants/ApiClientDirectory.ts new file mode 100644 index 00000000000..4282f67dd70 --- /dev/null +++ b/packages/twenty-shared/src/application/constants/ApiClientDirectory.ts @@ -0,0 +1 @@ +export const API_CLIENT_DIR = 'api-client'; diff --git a/packages/twenty-shared/src/application/fieldManifestType.ts b/packages/twenty-shared/src/application/fieldManifestType.ts index d3d54614347..dad00c7e544 100644 --- a/packages/twenty-shared/src/application/fieldManifestType.ts +++ b/packages/twenty-shared/src/application/fieldManifestType.ts @@ -1,10 +1,10 @@ import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; import { - type FieldMetadataDefaultValue, - type FieldMetadataOptions, - type FieldMetadataType, - type FieldMetadataUniversalSettings, - type RelationAndMorphRelationFieldMetadataType, + FieldMetadataDefaultValue, + FieldMetadataOptions, + FieldMetadataType, + FieldMetadataUniversalSettings, + RelationAndMorphRelationFieldMetadataType } from '@/types'; export type RegularFieldManifest< @@ -25,20 +25,27 @@ export type RegularFieldManifest< objectUniversalIdentifier: string; }; -// Both sides of a relation must be declared explicitly in the manifest. -// Relation-specific universal settings (relationType, onDelete, joinColumnName) -// are provided through the `universalSettings` field. export type RelationFieldManifest< T extends RelationAndMorphRelationFieldMetadataType = RelationAndMorphRelationFieldMetadataType, -> = RegularFieldManifest & { +> = Omit, 'universalSettings' | 'type'> & { + type: T; relationTargetFieldMetadataUniversalIdentifier: string; relationTargetObjectMetadataUniversalIdentifier: string; -}; + universalSettings: FieldMetadataUniversalSettings; +} & ([T] extends [FieldMetadataType.MORPH_RELATION] + ? { + morphId: string; + } + : { + morphId?: undefined; + }); -export type FieldManifest< - T extends FieldMetadataType = Exclude< - FieldMetadataType, - RelationAndMorphRelationFieldMetadataType - >, -> = RegularFieldManifest | RelationFieldManifest; +export type FieldManifest = + T extends RelationAndMorphRelationFieldMetadataType + ? RelationFieldManifest< + Extract + > + : RegularFieldManifest< + Exclude + >; diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts index 0346cce14a0..97715199c7d 100644 --- a/packages/twenty-shared/src/application/index.ts +++ b/packages/twenty-shared/src/application/index.ts @@ -13,6 +13,7 @@ export type { } from './applicationType'; export type { ApplicationVariables } from './applicationVariablesType'; export type { AssetManifest } from './assetManifestType'; +export { API_CLIENT_DIR } from './constants/ApiClientDirectory'; export { ASSETS_DIR } from './constants/AssetDirectory'; export { DEFAULT_API_KEY_NAME } from './constants/DefaultApiKeyName'; export { DEFAULT_API_URL_NAME } from './constants/DefaultApiUrlName'; diff --git a/packages/twenty-shared/src/application/objectFieldManifest.type.ts b/packages/twenty-shared/src/application/objectFieldManifest.type.ts index 1d8c69ee1c7..558b7765f12 100644 --- a/packages/twenty-shared/src/application/objectFieldManifest.type.ts +++ b/packages/twenty-shared/src/application/objectFieldManifest.type.ts @@ -1,9 +1,10 @@ import { type FieldManifest } from '@/application/fieldManifestType'; import type { FieldMetadataType } from '@/types'; +type DistributiveOmit = T extends unknown + ? Omit + : never; + export type ObjectFieldManifest< - T extends FieldMetadataType = Exclude< - FieldMetadataType, - FieldMetadataType.RELATION - >, -> = Omit, 'objectUniversalIdentifier'>; + T extends FieldMetadataType = FieldMetadataType, +> = DistributiveOmit, 'objectUniversalIdentifier'>; diff --git a/packages/twenty-shared/src/metadata/__tests__/addCustomSuffixIfReserved.test.ts b/packages/twenty-shared/src/metadata/__tests__/addCustomSuffixIfReserved.test.ts index 090d1f0d3d6..e3c22c39ed3 100644 --- a/packages/twenty-shared/src/metadata/__tests__/addCustomSuffixIfReserved.test.ts +++ b/packages/twenty-shared/src/metadata/__tests__/addCustomSuffixIfReserved.test.ts @@ -1,4 +1,4 @@ -import { addCustomSuffixIfIsReserved } from '@/metadata/add-custom-suffix-if-reserved.util'; +import { addCustomSuffixIfIsReserved } from '@/metadata/utils/add-custom-suffix-if-reserved.util'; describe('addCustomSuffixIfIsReserved', () => { it('should add Custom suffix for reserved name "event"', () => { diff --git a/packages/twenty-shared/src/metadata/__tests__/computeMetadataNameFromLabel.test.ts b/packages/twenty-shared/src/metadata/__tests__/computeMetadataNameFromLabel.test.ts index db84d1c35a2..30c959cd5c5 100644 --- a/packages/twenty-shared/src/metadata/__tests__/computeMetadataNameFromLabel.test.ts +++ b/packages/twenty-shared/src/metadata/__tests__/computeMetadataNameFromLabel.test.ts @@ -1,4 +1,4 @@ -import { computeMetadataNameFromLabel } from '@/metadata/compute-metadata-name-from-label.util'; +import { computeMetadataNameFromLabel } from '@/metadata/utils/compute-metadata-name-from-label.util'; describe('computeMetadataNameFromLabel', () => { it('should convert a label to camelCase', () => { diff --git a/packages/twenty-shared/src/metadata/all-metadata-name.type.ts b/packages/twenty-shared/src/metadata/all-metadata-name.type.ts deleted file mode 100644 index 5243cb289a5..00000000000 --- a/packages/twenty-shared/src/metadata/all-metadata-name.type.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type ALL_METADATA_NAME } from '@/metadata/all-metadata-name.constant'; - -export type AllMetadataName = keyof typeof ALL_METADATA_NAME; diff --git a/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts b/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts similarity index 100% rename from packages/twenty-shared/src/metadata/all-metadata-name.constant.ts rename to packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts diff --git a/packages/twenty-shared/src/metadata/constants/default-relations-object-standard-ids.constant.ts b/packages/twenty-shared/src/metadata/constants/default-relations-object-standard-ids.constant.ts new file mode 100644 index 00000000000..ba52a6da590 --- /dev/null +++ b/packages/twenty-shared/src/metadata/constants/default-relations-object-standard-ids.constant.ts @@ -0,0 +1,9 @@ +import { type STANDARD_OBJECTS } from '@/metadata/constants/standard-object.constant'; + +export const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [ + 'timelineActivity', + 'favorite', + 'attachment', + 'noteTarget', + 'taskTarget', +] as const satisfies (keyof typeof STANDARD_OBJECTS)[]; diff --git a/packages/twenty-shared/src/metadata/reserved-metadata-name-keywords.constant.ts b/packages/twenty-shared/src/metadata/constants/reserved-metadata-name-keywords.constant.ts similarity index 97% rename from packages/twenty-shared/src/metadata/reserved-metadata-name-keywords.constant.ts rename to packages/twenty-shared/src/metadata/constants/reserved-metadata-name-keywords.constant.ts index bebc340ebc2..24ea5cff54d 100644 --- a/packages/twenty-shared/src/metadata/reserved-metadata-name-keywords.constant.ts +++ b/packages/twenty-shared/src/metadata/constants/reserved-metadata-name-keywords.constant.ts @@ -1,4 +1,4 @@ -export const CORE_OBJECT_NAMES = [ +const CORE_OBJECT_NAMES = [ 'approvedAccessDomain', 'approvedAccessDomains', 'appToken', diff --git a/packages/twenty-shared/src/metadata/standard-object.constant.ts b/packages/twenty-shared/src/metadata/constants/standard-object.constant.ts similarity index 100% rename from packages/twenty-shared/src/metadata/standard-object.constant.ts rename to packages/twenty-shared/src/metadata/constants/standard-object.constant.ts diff --git a/packages/twenty-shared/src/metadata/index.ts b/packages/twenty-shared/src/metadata/index.ts index 980edc99ef4..933edeedbf0 100644 --- a/packages/twenty-shared/src/metadata/index.ts +++ b/packages/twenty-shared/src/metadata/index.ts @@ -7,23 +7,21 @@ * |___/ */ -export { addCustomSuffixIfIsReserved } from './add-custom-suffix-if-reserved.util'; -export { ALL_METADATA_NAME } from './all-metadata-name.constant'; -export type { AllMetadataName } from './all-metadata-name.type'; export { checkIfFieldIsImageIdentifier } from './check-if-field-is-image-identifier.util'; export { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, checkIfFieldIsLabelIdentifier, } from './check-if-field-is-label-identifier.util'; -export { computeMetadataNameFromLabel } from './compute-metadata-name-from-label.util'; +export { ALL_METADATA_NAME } from './constants/all-metadata-name.constant'; +export { DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS } from './constants/default-relations-object-standard-ids.constant'; +export { RESERVED_METADATA_NAME_KEYWORDS } from './constants/reserved-metadata-name-keywords.constant'; +export { STANDARD_OBJECTS } from './constants/standard-object.constant'; +export type { AllMetadataName } from './types/all-metadata-name.type'; export type { FailedMetadataValidationError, FailedMetadataValidation, MetadataValidationErrorResponse, -} from './MetadataValidationError'; -export { WorkspaceMigrationV2ExceptionCode } from './MetadataValidationError'; -export { - CORE_OBJECT_NAMES, - RESERVED_METADATA_NAME_KEYWORDS, -} from './reserved-metadata-name-keywords.constant'; -export { STANDARD_OBJECTS } from './standard-object.constant'; +} from './types/MetadataValidationError'; +export { WorkspaceMigrationV2ExceptionCode } from './types/MetadataValidationError'; +export { addCustomSuffixIfIsReserved } from './utils/add-custom-suffix-if-reserved.util'; +export { computeMetadataNameFromLabel } from './utils/compute-metadata-name-from-label.util'; diff --git a/packages/twenty-shared/src/metadata/MetadataValidationError.ts b/packages/twenty-shared/src/metadata/types/MetadataValidationError.ts similarity index 89% rename from packages/twenty-shared/src/metadata/MetadataValidationError.ts rename to packages/twenty-shared/src/metadata/types/MetadataValidationError.ts index 2cce75aa0a7..0e2335747bb 100644 --- a/packages/twenty-shared/src/metadata/MetadataValidationError.ts +++ b/packages/twenty-shared/src/metadata/types/MetadataValidationError.ts @@ -1,4 +1,4 @@ -import { type AllMetadataName } from './all-metadata-name.type'; +import { type AllMetadataName } from '@/metadata/types/all-metadata-name.type'; export type FailedMetadataValidationError = { code: string; diff --git a/packages/twenty-shared/src/metadata/types/all-metadata-name.type.ts b/packages/twenty-shared/src/metadata/types/all-metadata-name.type.ts new file mode 100644 index 00000000000..2becf394e7a --- /dev/null +++ b/packages/twenty-shared/src/metadata/types/all-metadata-name.type.ts @@ -0,0 +1,3 @@ +import { type ALL_METADATA_NAME } from '@/metadata/constants/all-metadata-name.constant'; + +export type AllMetadataName = keyof typeof ALL_METADATA_NAME; diff --git a/packages/twenty-shared/src/metadata/add-custom-suffix-if-reserved.util.ts b/packages/twenty-shared/src/metadata/utils/add-custom-suffix-if-reserved.util.ts similarity index 67% rename from packages/twenty-shared/src/metadata/add-custom-suffix-if-reserved.util.ts rename to packages/twenty-shared/src/metadata/utils/add-custom-suffix-if-reserved.util.ts index c3e4b8534ed..1a21479878a 100644 --- a/packages/twenty-shared/src/metadata/add-custom-suffix-if-reserved.util.ts +++ b/packages/twenty-shared/src/metadata/utils/add-custom-suffix-if-reserved.util.ts @@ -1,4 +1,4 @@ -import { RESERVED_METADATA_NAME_KEYWORDS } from './reserved-metadata-name-keywords.constant'; +import { RESERVED_METADATA_NAME_KEYWORDS } from '@/metadata'; export const addCustomSuffixIfIsReserved = (name: string): string => { if (!name) return name; diff --git a/packages/twenty-shared/src/metadata/compute-metadata-name-from-label.util.ts b/packages/twenty-shared/src/metadata/utils/compute-metadata-name-from-label.util.ts similarity index 88% rename from packages/twenty-shared/src/metadata/compute-metadata-name-from-label.util.ts rename to packages/twenty-shared/src/metadata/utils/compute-metadata-name-from-label.util.ts index 0b935b59179..aa490861e23 100644 --- a/packages/twenty-shared/src/metadata/compute-metadata-name-from-label.util.ts +++ b/packages/twenty-shared/src/metadata/utils/compute-metadata-name-from-label.util.ts @@ -1,4 +1,4 @@ -import { addCustomSuffixIfIsReserved } from '@/metadata/add-custom-suffix-if-reserved.util'; +import { addCustomSuffixIfIsReserved } from '@/metadata/utils/add-custom-suffix-if-reserved.util'; import camelCase from 'lodash.camelcase'; import { slugify } from 'transliteration';