Add default relation to standard object on custom object in manifest (#18033)

add default relation fields when creating an object from an application

---------

Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
martmull
2026-02-20 11:49:06 +01:00
committed by GitHub
parent d060c843d1
commit 3bc887e12e
48 changed files with 1812 additions and 554 deletions
@@ -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:
@@ -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),
);
});
});
};
@@ -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',
},
],
},
],
@@ -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);
});
});
};
@@ -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 = <T extends JsonManifestInput>(
export const normalizeManifestForComparison = <T extends Manifest>(
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,
@@ -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',
),
}));
@@ -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<string[]> => {
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);
@@ -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);
@@ -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',
]);
@@ -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');
});
});
@@ -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',
]);
});
});
@@ -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 };
};
@@ -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,
];
};
@@ -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<typeof config.targetFieldType> => {
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 };
};
@@ -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;
};
@@ -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) {
@@ -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,