From 0d6fe7b2b4716d957719761222742e704347c15e Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 23 May 2024 14:59:34 +0200 Subject: [PATCH] Handle relations separately for remotes (#5538) Remote object id columns are not removed anymore when a remote object is unsynced. This is because we do not use relations anymore. We only created the id field. So the current behavior that was implemented for custom objects, to retrieve the fields to deleted, does not work. Since remote object relations are really different, I extracted the logic from `objectMetadataService`. It now handles only the relations for custom objects creation and deletion (this part should be extracted as well). I create a new remote table relation service that will: - fetch objects metadata linked to remotes (favorites, activityTargets...) - look for columns based on remote object name - delete the fields and columns --- .../object-metadata/object-metadata.module.ts | 2 + .../object-metadata.service.ts | 730 +++++++++--------- ...oreign-key-comment-alteration.util.spec.ts | 76 -- ...tions-for-custom-object-relations.util.ts} | 2 +- ...for-foreign-key-comment-alteration.util.ts | 97 --- ...ations-for-remote-object-relations.util.ts | 77 -- .../remote-table-relations.module.ts | 20 + .../remote-table-relations.service.ts | 327 ++++++++ ...s-to-create-remote-table-relations.util.ts | 29 + ...s-to-remove-remote-table-relations.util.ts | 26 + 10 files changed, 762 insertions(+), 624 deletions(-) delete mode 100644 packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/create-migration-for-foreign-key-comment-alteration.util.spec.ts rename packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/{create-migrations-for-custom-object-relations.util.ts => build-migrations-for-custom-object-relations.util.ts} (98%) delete mode 100644 packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-migration-for-foreign-key-comment-alteration.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object-relations.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-create-remote-table-relations.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-remove-remote-table-relations.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 9637f0522..997440362 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -20,6 +20,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; +import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module'; import { ObjectMetadataService } from './object-metadata.service'; import { ObjectMetadataEntity } from './object-metadata.entity'; @@ -43,6 +44,7 @@ import { ObjectMetadataDTO } from './dtos/object-metadata.dto'; WorkspaceMigrationRunnerModule, WorkspaceCacheVersionModule, FeatureFlagModule, + RemoteTableRelationsModule, ], services: [ObjectMetadataService], resolvers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index af23476fd..90c9b8fa1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -49,14 +49,13 @@ import { createForeignKeyDeterministicUuid, createRelationDeterministicUuid, } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; -import { createWorkspaceMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/create-migrations-for-custom-object-relations.util'; -import { createWorkspaceMigrationsForRemoteObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object-relations.util'; +import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { validateObjectMetadataInputOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { createMigrationToAlterCommentOnForeignKeyDeletion } from 'src/engine/metadata-modules/object-metadata/utils/create-migration-for-foreign-key-comment-alteration.util'; +import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; import { ObjectMetadataEntity } from './object-metadata.entity'; @@ -74,6 +73,8 @@ export class ObjectMetadataService extends TypeOrmQueryService, + private readonly remoteTableRelationsService: RemoteTableRelationsService, + private readonly dataSourceService: DataSourceService, private readonly typeORMService: TypeORMService, private readonly workspaceMigrationService: WorkspaceMigrationService, @@ -123,113 +124,18 @@ export class ObjectMetadataService extends TypeOrmQueryService 0) { - await this.relationMetadataRepository.delete( - relationsToDelete.map((relation) => relation.id), + // DELETE RELATIONS + if (objectMetadata.isRemote) { + await this.remoteTableRelationsService.deleteForeignKeysMetadataAndCreateMigrations( + objectMetadata.workspaceId, + objectMetadata, ); - } - - for (const relationToDelete of relationsToDelete) { - const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({ - where: { - name: `${relationToDelete.toFieldMetadataName}Id`, - objectMetadataId: relationToDelete.toObjectMetadataId, - workspaceId, - }, - }); - - const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map( - (field) => field.id, - ); - - await this.fieldMetadataRepository.delete([ - ...foreignKeyFieldsToDeleteIds, - relationToDelete.fromFieldMetadataId, - relationToDelete.toFieldMetadataId, - ]); - - if (relationToDelete.direction === 'from') { - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `delete-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`, - ), - workspaceId, - [ - { - name: computeTableName( - relationToDelete.toObjectName, - relationToDelete.toObjectMetadataIsCustom, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.DROP, - columnName: computeColumnName( - relationToDelete.toFieldMetadataName, - { isForeignKey: true }, - ), - } satisfies WorkspaceMigrationColumnDrop, - ], - }, - ], - ); - - // for remote objects, we need to update the comment of the foreign key column - if (objectMetadata.isRemote) { - await createMigrationToAlterCommentOnForeignKeyDeletion( - this.dataSourceService, - this.typeORMService, - this.workspaceMigrationService, - workspaceId, - relationToDelete, - ); - } - } + } else { + await this.deleteAllRelationsAndDropTable(objectMetadata, workspaceId); } await this.objectMetadataRepository.delete(objectMetadata.id); - if (!objectMetadata.isRemote) { - // DROP TABLE - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`delete-${objectMetadata.nameSingular}`), - workspaceId, - [ - { - name: computeObjectTargetTable(objectMetadata), - action: WorkspaceMigrationTableActionType.DROP, - }, - ], - ); - } - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( workspaceId, ); @@ -371,10 +277,19 @@ export class ObjectMetadataService extends TypeOrmQueryService | undefined, - isRemoteObject: boolean, ) { const activityTargetObjectMetadata = await this.objectMetadataRepository.findOneByOrFail({ @@ -607,70 +506,68 @@ export class ObjectMetadataService extends TypeOrmQueryService { - if (fieldMetadata.type === FieldMetadataType.RELATION) { - acc[fieldMetadata.objectMetadataId] = fieldMetadata; - } - - return acc; - }, - {}, - ); - - await this.relationMetadataRepository.save([ + const activityTargetRelationFieldMetadata = + await this.fieldMetadataRepository.save([ + // FROM { + standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.activityTargets, + objectMetadataId: createdObjectMetadata.id, workspaceId: workspaceId, - relationType: RelationMetadataType.ONE_TO_MANY, - fromObjectMetadataId: createdObjectMetadata.id, - toObjectMetadataId: activityTargetObjectMetadata.id, - fromFieldMetadataId: - activityTargetRelationFieldMetadataMap[createdObjectMetadata.id].id, - toFieldMetadataId: - activityTargetRelationFieldMetadataMap[ - activityTargetObjectMetadata.id - ].id, - onDeleteAction: RelationOnDeleteAction.CASCADE, + isCustom: false, + isActive: true, + type: FieldMetadataType.RELATION, + name: 'activityTargets', + label: 'Activities', + description: `Activities tied to the ${createdObjectMetadata.labelSingular}`, + icon: 'IconCheckbox', + isNullable: true, + }, + // TO + { + standardId: createRelationDeterministicUuid({ + objectId: createdObjectMetadata.id, + standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.custom, + }), + objectMetadataId: activityTargetObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: FieldMetadataType.RELATION, + name: createdObjectMetadata.nameSingular, + label: createdObjectMetadata.labelSingular, + description: `ActivityTarget ${createdObjectMetadata.labelSingular}`, + icon: 'IconBuildingSkyscraper', + isNullable: true, }, ]); - } + + const activityTargetRelationFieldMetadataMap = + activityTargetRelationFieldMetadata.reduce( + (acc, fieldMetadata: FieldMetadataEntity) => { + if (fieldMetadata.type === FieldMetadataType.RELATION) { + acc[fieldMetadata.objectMetadataId] = fieldMetadata; + } + + return acc; + }, + {}, + ); + + await this.relationMetadataRepository.save([ + { + workspaceId: workspaceId, + relationType: RelationMetadataType.ONE_TO_MANY, + fromObjectMetadataId: createdObjectMetadata.id, + toObjectMetadataId: activityTargetObjectMetadata.id, + fromFieldMetadataId: + activityTargetRelationFieldMetadataMap[createdObjectMetadata.id].id, + toFieldMetadataId: + activityTargetRelationFieldMetadataMap[ + activityTargetObjectMetadata.id + ].id, + onDeleteAction: RelationOnDeleteAction.CASCADE, + }, + ]); return { activityTargetObjectMetadata }; } @@ -682,7 +579,6 @@ export class ObjectMetadataService extends TypeOrmQueryService | undefined, - isRemoteObject: boolean, ) { const attachmentObjectMetadata = await this.objectMetadataRepository.findOneByOrFail({ @@ -713,68 +609,66 @@ export class ObjectMetadataService extends TypeOrmQueryService { - if (fieldMetadata.type === FieldMetadataType.RELATION) { - acc[fieldMetadata.objectMetadataId] = fieldMetadata; - } - - return acc; - }, - {}, - ); - - await this.relationMetadataRepository.save([ + const attachmentRelationFieldMetadata = + await this.fieldMetadataRepository.save([ + // FROM { + standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.attachments, + objectMetadataId: createdObjectMetadata.id, workspaceId: workspaceId, - relationType: RelationMetadataType.ONE_TO_MANY, - fromObjectMetadataId: createdObjectMetadata.id, - toObjectMetadataId: attachmentObjectMetadata.id, - fromFieldMetadataId: - attachmentRelationFieldMetadataMap[createdObjectMetadata.id].id, - toFieldMetadataId: - attachmentRelationFieldMetadataMap[attachmentObjectMetadata.id].id, - onDeleteAction: RelationOnDeleteAction.CASCADE, + isCustom: false, + isActive: true, + type: FieldMetadataType.RELATION, + name: 'attachments', + label: 'Attachments', + description: `Attachments tied to the ${createdObjectMetadata.labelSingular}`, + icon: 'IconFileImport', + isNullable: true, + }, + // TO + { + standardId: createRelationDeterministicUuid({ + objectId: createdObjectMetadata.id, + standardId: ATTACHMENT_STANDARD_FIELD_IDS.custom, + }), + objectMetadataId: attachmentObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: FieldMetadataType.RELATION, + name: createdObjectMetadata.nameSingular, + label: createdObjectMetadata.labelSingular, + description: `Attachment ${createdObjectMetadata.labelSingular}`, + icon: 'IconBuildingSkyscraper', + isNullable: true, }, ]); - } + + const attachmentRelationFieldMetadataMap = + attachmentRelationFieldMetadata.reduce( + (acc, fieldMetadata: FieldMetadataEntity) => { + if (fieldMetadata.type === FieldMetadataType.RELATION) { + acc[fieldMetadata.objectMetadataId] = fieldMetadata; + } + + return acc; + }, + {}, + ); + + await this.relationMetadataRepository.save([ + { + workspaceId: workspaceId, + relationType: RelationMetadataType.ONE_TO_MANY, + fromObjectMetadataId: createdObjectMetadata.id, + toObjectMetadataId: attachmentObjectMetadata.id, + fromFieldMetadataId: + attachmentRelationFieldMetadataMap[createdObjectMetadata.id].id, + toFieldMetadataId: + attachmentRelationFieldMetadataMap[attachmentObjectMetadata.id].id, + onDeleteAction: RelationOnDeleteAction.CASCADE, + }, + ]); return { attachmentObjectMetadata }; } @@ -786,7 +680,6 @@ export class ObjectMetadataService extends TypeOrmQueryService | undefined, - isRemoteObject: boolean, ) { const timelineActivityObjectMetadata = await this.objectMetadataRepository.findOneByOrFail({ @@ -817,71 +710,68 @@ export class ObjectMetadataService extends TypeOrmQueryService { - if (fieldMetadata.type === FieldMetadataType.RELATION) { - acc[fieldMetadata.objectMetadataId] = fieldMetadata; - } - - return acc; - }, - {}, - ); - - await this.relationMetadataRepository.save([ + const timelineActivityRelationFieldMetadata = + await this.fieldMetadataRepository.save([ + // FROM { + standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities, + objectMetadataId: createdObjectMetadata.id, workspaceId: workspaceId, - relationType: RelationMetadataType.ONE_TO_MANY, - fromObjectMetadataId: createdObjectMetadata.id, - toObjectMetadataId: timelineActivityObjectMetadata.id, - fromFieldMetadataId: - timelineActivityRelationFieldMetadataMap[createdObjectMetadata.id] - .id, - toFieldMetadataId: - timelineActivityRelationFieldMetadataMap[ - timelineActivityObjectMetadata.id - ].id, - onDeleteAction: RelationOnDeleteAction.CASCADE, + isCustom: false, + isActive: true, + type: FieldMetadataType.RELATION, + name: 'timelineActivities', + label: 'Timeline Activities', + description: `Timeline Activities tied to the ${createdObjectMetadata.labelSingular}`, + icon: 'IconTimeline', + isNullable: true, + }, + // TO + { + standardId: createRelationDeterministicUuid({ + objectId: createdObjectMetadata.id, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom, + }), + objectMetadataId: timelineActivityObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: FieldMetadataType.RELATION, + name: createdObjectMetadata.nameSingular, + label: createdObjectMetadata.labelSingular, + description: `Timeline Activity ${createdObjectMetadata.labelSingular}`, + icon: 'IconBuildingSkyscraper', + isNullable: true, }, ]); - } + + const timelineActivityRelationFieldMetadataMap = + timelineActivityRelationFieldMetadata.reduce( + (acc, fieldMetadata: FieldMetadataEntity) => { + if (fieldMetadata.type === FieldMetadataType.RELATION) { + acc[fieldMetadata.objectMetadataId] = fieldMetadata; + } + + return acc; + }, + {}, + ); + + await this.relationMetadataRepository.save([ + { + workspaceId: workspaceId, + relationType: RelationMetadataType.ONE_TO_MANY, + fromObjectMetadataId: createdObjectMetadata.id, + toObjectMetadataId: timelineActivityObjectMetadata.id, + fromFieldMetadataId: + timelineActivityRelationFieldMetadataMap[createdObjectMetadata.id].id, + toFieldMetadataId: + timelineActivityRelationFieldMetadataMap[ + timelineActivityObjectMetadata.id + ].id, + onDeleteAction: RelationOnDeleteAction.CASCADE, + }, + ]); return { timelineActivityObjectMetadata }; } @@ -893,7 +783,6 @@ export class ObjectMetadataService extends TypeOrmQueryService | undefined, - isRemoteObject: boolean, ) { const favoriteObjectMetadata = await this.objectMetadataRepository.findOneByOrFail({ @@ -924,70 +813,165 @@ export class ObjectMetadataService extends TypeOrmQueryService { - if (fieldMetadata.type === FieldMetadataType.RELATION) { - acc[fieldMetadata.objectMetadataId] = fieldMetadata; - } - - return acc; - }, - {}, - ); - - await this.relationMetadataRepository.save([ + const favoriteRelationFieldMetadata = + await this.fieldMetadataRepository.save([ + // FROM { + standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.favorites, + objectMetadataId: createdObjectMetadata.id, workspaceId: workspaceId, - relationType: RelationMetadataType.ONE_TO_MANY, - fromObjectMetadataId: createdObjectMetadata.id, - toObjectMetadataId: favoriteObjectMetadata.id, - fromFieldMetadataId: - favoriteRelationFieldMetadataMap[createdObjectMetadata.id].id, - toFieldMetadataId: - favoriteRelationFieldMetadataMap[favoriteObjectMetadata.id].id, - onDeleteAction: RelationOnDeleteAction.CASCADE, + isCustom: false, + isActive: true, + isSystem: true, + type: FieldMetadataType.RELATION, + name: 'favorites', + label: 'Favorites', + description: `Favorites tied to the ${createdObjectMetadata.labelSingular}`, + icon: 'IconHeart', + isNullable: true, + }, + // TO + { + standardId: createRelationDeterministicUuid({ + objectId: createdObjectMetadata.id, + standardId: FAVORITE_STANDARD_FIELD_IDS.custom, + }), + objectMetadataId: favoriteObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: FieldMetadataType.RELATION, + name: createdObjectMetadata.nameSingular, + label: createdObjectMetadata.labelSingular, + description: `Favorite ${createdObjectMetadata.labelSingular}`, + icon: 'IconBuildingSkyscraper', + isNullable: true, }, ]); - } + + const favoriteRelationFieldMetadataMap = + favoriteRelationFieldMetadata.reduce( + (acc, fieldMetadata: FieldMetadataEntity) => { + if (fieldMetadata.type === FieldMetadataType.RELATION) { + acc[fieldMetadata.objectMetadataId] = fieldMetadata; + } + + return acc; + }, + {}, + ); + + await this.relationMetadataRepository.save([ + { + workspaceId: workspaceId, + relationType: RelationMetadataType.ONE_TO_MANY, + fromObjectMetadataId: createdObjectMetadata.id, + toObjectMetadataId: favoriteObjectMetadata.id, + fromFieldMetadataId: + favoriteRelationFieldMetadataMap[createdObjectMetadata.id].id, + toFieldMetadataId: + favoriteRelationFieldMetadataMap[favoriteObjectMetadata.id].id, + onDeleteAction: RelationOnDeleteAction.CASCADE, + }, + ]); return { favoriteObjectMetadata }; } + + private async deleteAllRelationsAndDropTable( + objectMetadata: ObjectMetadataEntity, + workspaceId: string, + ) { + const relationsToDelete: RelationToDelete[] = []; + + // TODO: Most of this logic should be moved to relation-metadata.service.ts + for (const relation of [ + ...objectMetadata.fromRelations, + ...objectMetadata.toRelations, + ]) { + relationsToDelete.push({ + id: relation.id, + fromFieldMetadataId: relation.fromFieldMetadata.id, + toFieldMetadataId: relation.toFieldMetadata.id, + fromFieldMetadataName: relation.fromFieldMetadata.name, + toFieldMetadataName: relation.toFieldMetadata.name, + fromObjectMetadataId: relation.fromObjectMetadata.id, + toObjectMetadataId: relation.toObjectMetadata.id, + fromObjectName: relation.fromObjectMetadata.nameSingular, + toObjectName: relation.toObjectMetadata.nameSingular, + toFieldMetadataIsCustom: relation.toFieldMetadata.isCustom, + toObjectMetadataIsCustom: relation.toObjectMetadata.isCustom, + direction: + relation.fromObjectMetadata.nameSingular === + objectMetadata.nameSingular + ? 'from' + : 'to', + }); + } + + if (relationsToDelete.length > 0) { + await this.relationMetadataRepository.delete( + relationsToDelete.map((relation) => relation.id), + ); + } + + for (const relationToDelete of relationsToDelete) { + const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({ + where: { + name: `${relationToDelete.toFieldMetadataName}Id`, + objectMetadataId: relationToDelete.toObjectMetadataId, + workspaceId, + }, + }); + + const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map( + (field) => field.id, + ); + + await this.fieldMetadataRepository.delete([ + ...foreignKeyFieldsToDeleteIds, + relationToDelete.fromFieldMetadataId, + relationToDelete.toFieldMetadataId, + ]); + + if (relationToDelete.direction === 'from') { + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `delete-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`, + ), + workspaceId, + [ + { + name: computeTableName( + relationToDelete.toObjectName, + relationToDelete.toObjectMetadataIsCustom, + ), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.DROP, + columnName: computeColumnName( + relationToDelete.toFieldMetadataName, + { isForeignKey: true }, + ), + } satisfies WorkspaceMigrationColumnDrop, + ], + }, + ], + ); + } + } + + // DROP TABLE + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`delete-${objectMetadata.nameSingular}`), + workspaceId, + [ + { + name: computeObjectTargetTable(objectMetadata), + action: WorkspaceMigrationTableActionType.DROP, + }, + ], + ); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/create-migration-for-foreign-key-comment-alteration.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/create-migration-for-foreign-key-comment-alteration.util.spec.ts deleted file mode 100644 index 5c5a5a626..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/create-migration-for-foreign-key-comment-alteration.util.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { buildAlteredCommentOnForeignKeyDeletion } from 'src/engine/metadata-modules/object-metadata/utils/create-migration-for-foreign-key-comment-alteration.util'; - -describe('buildAlteredCommentOnForeignKeyDeletion', () => { - const localObjectMetadataName = 'favorite'; - const remoteObjectMetadataName = 'blog'; - const schema = 'schema'; - const workspaceDataSource = { - query: jest.fn(), - }; - - it('should return null if no comment ', async () => { - workspaceDataSource.query.mockResolvedValueOnce([]); - - const result = await buildAlteredCommentOnForeignKeyDeletion( - localObjectMetadataName, - remoteObjectMetadataName, - schema, - workspaceDataSource as any, - ); - - expect(result).toBeNull(); - }); - - it('should return null if the existing comment does not contain foreign keys', async () => { - workspaceDataSource.query.mockResolvedValueOnce([ - { col_description: '@graphql({"totalCount":{"enabled":true}})' }, - ]); - - const result = await buildAlteredCommentOnForeignKeyDeletion( - localObjectMetadataName, - remoteObjectMetadataName, - schema, - workspaceDataSource as any, - ); - - expect(result).toBeNull(); - }); - - it('should return altered comment without foreign key', async () => { - const existingComment = { - col_description: `@graphql({"totalCount":{"enabled":true},"foreign_keys":[{"local_name":"favoriteCollection","local_columns":["${remoteObjectMetadataName}Id"],"foreign_name":"${remoteObjectMetadataName}","foreign_schema":"schema","foreign_table":"${remoteObjectMetadataName}","foreign_columns":["id"]}]})`, - }; - - workspaceDataSource.query.mockResolvedValueOnce([existingComment]); - - const result = await buildAlteredCommentOnForeignKeyDeletion( - localObjectMetadataName, - remoteObjectMetadataName, - schema, - workspaceDataSource as any, - ); - - expect(result).toBe( - '@graphql({"totalCount":{"enabled":true},"foreign_keys":[]})', - ); - }); - - it('should return altered comment without the input foreign key', async () => { - const existingComment = { - col_description: `@graphql({"totalCount":{"enabled":true},"foreign_keys":[{"local_name":"favoriteCollection","local_columns":["${remoteObjectMetadataName}Id"],"foreign_name":"${remoteObjectMetadataName}","foreign_schema":"schema","foreign_table":"${remoteObjectMetadataName}","foreign_columns":["id"]}, {"local_name":"favoriteCollection","local_columns":["testId"],"foreign_name":"test","foreign_schema":"schema","foreign_table":"test","foreign_columns":["id"]}]})`, - }; - - workspaceDataSource.query.mockResolvedValueOnce([existingComment]); - - const result = await buildAlteredCommentOnForeignKeyDeletion( - localObjectMetadataName, - remoteObjectMetadataName, - schema, - workspaceDataSource as any, - ); - - expect(result).toBe( - '@graphql({"totalCount":{"enabled":true},"foreign_keys":[{"local_name":"favoriteCollection","local_columns":["testId"],"foreign_name":"test","foreign_schema":"schema","foreign_table":"test","foreign_columns":["id"]}]})', - ); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-migrations-for-custom-object-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts similarity index 98% rename from packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-migrations-for-custom-object-relations.util.ts rename to packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts index 8c05bcfe4..653a53178 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-migrations-for-custom-object-relations.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts @@ -9,7 +9,7 @@ import { } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -export const createWorkspaceMigrationsForCustomObjectRelations = ( +export const buildMigrationsForCustomObjectRelations = ( createdObjectMetadata: ObjectMetadataEntity, activityTargetObjectMetadata: ObjectMetadataEntity, attachmentObjectMetadata: ObjectMetadataEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-migration-for-foreign-key-comment-alteration.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-migration-for-foreign-key-comment-alteration.util.ts deleted file mode 100644 index f42dcbff6..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-migration-for-foreign-key-comment-alteration.util.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { DataSource } from 'typeorm'; - -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete'; -import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; -import { - WorkspaceMigrationTableActionType, - WorkspaceMigrationColumnActionType, - WorkspaceMigrationCreateComment, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; -import { computeTableName } from 'src/engine/utils/compute-table-name.util'; - -export const buildAlteredCommentOnForeignKeyDeletion = async ( - localObjectMetadataName: string, - remoteObjectMetadataName: string, - schema: string, - workspaceDataSource: DataSource | undefined, -): Promise => { - const existingComment = await workspaceDataSource?.query( - `SELECT col_description('${schema}."${localObjectMetadataName}"'::regclass, 0)`, - ); - - if (!existingComment[0]?.col_description) { - return null; - } - - const commentWithoutGraphQL = existingComment[0].col_description - .replace('@graphql(', '') - .replace(')', ''); - - const parsedComment = JSON.parse(commentWithoutGraphQL); - - const currentForeignKeys = parsedComment.foreign_keys; - - if (!currentForeignKeys) { - return null; - } - - const updatedForeignKeys = currentForeignKeys.filter( - (foreignKey: any) => - foreignKey.foreign_name !== remoteObjectMetadataName && - foreignKey.foreign_table !== remoteObjectMetadataName, - ); - - parsedComment.foreign_keys = updatedForeignKeys; - - return `@graphql(${JSON.stringify(parsedComment)})`; -}; - -export const createMigrationToAlterCommentOnForeignKeyDeletion = async ( - dataSourceService: DataSourceService, - typeORMService: TypeORMService, - workspaceMigrationService: WorkspaceMigrationService, - workspaceId: string, - relationToDelete: RelationToDelete, -) => { - const dataSourceMetadata = - await dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspaceId, - ); - - const workspaceDataSource = - await typeORMService.connectToDataSource(dataSourceMetadata); - - const alteredComment = await buildAlteredCommentOnForeignKeyDeletion( - relationToDelete.toObjectName, - relationToDelete.fromObjectName, - dataSourceMetadata.schema, - workspaceDataSource, - ); - - if (alteredComment) { - await workspaceMigrationService.createCustomMigration( - generateMigrationName( - `alter-comment-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`, - ), - workspaceId, - [ - { - name: computeTableName( - relationToDelete.toObjectName, - relationToDelete.toObjectMetadataIsCustom, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, - comment: alteredComment, - } satisfies WorkspaceMigrationCreateComment, - ], - }, - ], - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object-relations.util.ts deleted file mode 100644 index d817fc543..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object-relations.util.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { - WorkspaceMigrationTableAction, - WorkspaceMigrationColumnActionType, - WorkspaceMigrationColumnCreate, - WorkspaceMigrationTableActionType, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -export const createWorkspaceMigrationsForRemoteObjectRelations = async ( - createdObjectMetadata: ObjectMetadataEntity, - activityTargetObjectMetadata: ObjectMetadataEntity, - attachmentObjectMetadata: ObjectMetadataEntity, - timelineActivityObjectMetadata: ObjectMetadataEntity, - favoriteObjectMetadata: ObjectMetadataEntity, - primaryKeyColumnType: string, -): Promise => { - return [ - { - name: computeObjectTargetTable(activityTargetObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: computeColumnName(createdObjectMetadata.nameSingular, { - isForeignKey: true, - }), - columnType: primaryKeyColumnType, - isNullable: true, - } satisfies WorkspaceMigrationColumnCreate, - ], - }, - { - name: computeObjectTargetTable(attachmentObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: computeColumnName(createdObjectMetadata.nameSingular, { - isForeignKey: true, - }), - columnType: primaryKeyColumnType, - isNullable: true, - } satisfies WorkspaceMigrationColumnCreate, - ], - }, - { - name: computeObjectTargetTable(timelineActivityObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: computeColumnName(createdObjectMetadata.nameSingular, { - isForeignKey: true, - }), - columnType: primaryKeyColumnType, - isNullable: true, - } satisfies WorkspaceMigrationColumnCreate, - ], - }, - { - name: computeObjectTargetTable(favoriteObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: computeColumnName(createdObjectMetadata.nameSingular, { - isForeignKey: true, - }), - columnType: primaryKeyColumnType, - isNullable: true, - } satisfies WorkspaceMigrationColumnCreate, - ], - }, - ]; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module.ts new file mode 100644 index 000000000..0fa3b6e47 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; +import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), + WorkspaceMigrationModule, + ], + providers: [RemoteTableRelationsService], + exports: [RemoteTableRelationsService], +}) +export class RemoteTableRelationsModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts new file mode 100644 index 000000000..96fc92d53 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts @@ -0,0 +1,327 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; + +import { In, Repository } from 'typeorm'; + +import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { createForeignKeyDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; +import { + ACTIVITY_TARGET_STANDARD_FIELD_IDS, + ATTACHMENT_STANDARD_FIELD_IDS, + FAVORITE_STANDARD_FIELD_IDS, + TIMELINE_ACTIVITY_STANDARD_FIELD_IDS, +} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { buildMigrationsToCreateRemoteTableRelations } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-create-remote-table-relations.util'; +import { buildMigrationsToRemoveRemoteTableRelations } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-remove-remote-table-relations.util'; +import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; +import { createRelationForeignKeyFieldMetadataName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-field-metadata-name.util'; + +@Injectable() +export class RemoteTableRelationsService { + constructor( + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + private readonly workspaceMigrationService: WorkspaceMigrationService, + ) {} + + public async createForeignKeysMetadataAndMigrations( + workspaceId: string, + remoteObjectMetadata: ObjectMetadataEntity, + objectPrimaryKeyFieldSettings: + | FieldMetadataSettings + | undefined, + objectPrimaryKeyColumnType?: string, + ) { + const objectPrimaryKeyFieldType = mapUdtNameToFieldType( + objectPrimaryKeyColumnType ?? 'uuid', + ); + + const favoriteObjectMetadata = await this.createFavoriteRelation( + workspaceId, + remoteObjectMetadata, + objectPrimaryKeyFieldType, + objectPrimaryKeyFieldSettings, + ); + + const activityTargetObjectMetadata = + await this.createActivityTargetRelation( + workspaceId, + remoteObjectMetadata, + objectPrimaryKeyFieldType, + objectPrimaryKeyFieldSettings, + ); + + const attachmentObjectMetadata = await this.createAttachmentRelation( + workspaceId, + remoteObjectMetadata, + objectPrimaryKeyFieldType, + objectPrimaryKeyFieldSettings, + ); + + const timelineActivityObjectMetadata = + await this.createTimelineActivityRelation( + workspaceId, + remoteObjectMetadata, + objectPrimaryKeyFieldType, + objectPrimaryKeyFieldSettings, + ); + + // create migration to add foreign key columns + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `add-foreign-keys-${remoteObjectMetadata.nameSingular}`, + ), + workspaceId, + buildMigrationsToCreateRemoteTableRelations( + remoteObjectMetadata.nameSingular, + [ + favoriteObjectMetadata, + activityTargetObjectMetadata, + attachmentObjectMetadata, + timelineActivityObjectMetadata, + ], + objectPrimaryKeyColumnType ?? 'uuid', + ), + ); + } + + public async deleteForeignKeysMetadataAndCreateMigrations( + workspaceId: string, + remoteObjectMetadata: ObjectMetadataEntity, + ) { + // find favorite, activityTarget, attachment, timelineActivity objects + const favoriteObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + nameSingular: 'favorite', + workspaceId: workspaceId, + }); + + const activityTargetObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + nameSingular: 'activityTarget', + workspaceId: workspaceId, + }); + + const attachmentObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + nameSingular: 'attachment', + workspaceId: workspaceId, + }); + + const timelineActivityObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + nameSingular: 'timelineActivity', + workspaceId: workspaceId, + }); + + // compute the target column name + const targetColumnName = createRelationForeignKeyFieldMetadataName( + remoteObjectMetadata.nameSingular, + ); + + // find the foreign key fields to delete + const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({ + where: { + name: targetColumnName, + objectMetadataId: In([ + favoriteObjectMetadata.id, + activityTargetObjectMetadata.id, + attachmentObjectMetadata.id, + timelineActivityObjectMetadata.id, + ]), + workspaceId, + }, + }); + + const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map( + (field) => field.id, + ); + + await this.fieldMetadataRepository.delete(foreignKeyFieldsToDeleteIds); + + // create migration to drop foreign key columns + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `delete-foreign-keys-${remoteObjectMetadata.nameSingular}`, + ), + workspaceId, + buildMigrationsToRemoveRemoteTableRelations(targetColumnName, [ + favoriteObjectMetadata, + activityTargetObjectMetadata, + attachmentObjectMetadata, + timelineActivityObjectMetadata, + ]), + ); + } + + private async createActivityTargetRelation( + workspaceId: string, + createdObjectMetadata: ObjectMetadataEntity, + objectPrimaryKeyType: FieldMetadataType, + objectPrimaryKeyFieldSettings: + | FieldMetadataSettings + | undefined, + ) { + const activityTargetObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + nameSingular: 'activityTarget', + workspaceId: workspaceId, + }); + + await this.fieldMetadataRepository.save( + // Foreign key + { + standardId: createForeignKeyDeterministicUuid({ + objectId: createdObjectMetadata.id, + standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.custom, + }), + objectMetadataId: activityTargetObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: objectPrimaryKeyType, + name: `${createdObjectMetadata.nameSingular}Id`, + label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, + description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`, + icon: undefined, + isNullable: true, + isSystem: true, + defaultValue: undefined, + settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true }, + }, + ); + + return activityTargetObjectMetadata; + } + + private async createAttachmentRelation( + workspaceId: string, + createdObjectMetadata: ObjectMetadataEntity, + objectPrimaryKeyType: FieldMetadataType, + objectPrimaryKeyFieldSettings: + | FieldMetadataSettings + | undefined, + ) { + const attachmentObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + nameSingular: 'attachment', + workspaceId: workspaceId, + }); + + await this.fieldMetadataRepository.save( + // Foreign key + { + standardId: createForeignKeyDeterministicUuid({ + objectId: createdObjectMetadata.id, + standardId: ATTACHMENT_STANDARD_FIELD_IDS.custom, + }), + objectMetadataId: attachmentObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: objectPrimaryKeyType, + name: `${createdObjectMetadata.nameSingular}Id`, + label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, + description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`, + icon: undefined, + isNullable: true, + isSystem: true, + defaultValue: undefined, + settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true }, + }, + ); + + return attachmentObjectMetadata; + } + + private async createTimelineActivityRelation( + workspaceId: string, + createdObjectMetadata: ObjectMetadataEntity, + objectPrimaryKeyType: FieldMetadataType, + objectPrimaryKeyFieldSettings: + | FieldMetadataSettings + | undefined, + ) { + const timelineActivityObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + nameSingular: 'timelineActivity', + workspaceId: workspaceId, + }); + + await this.fieldMetadataRepository.save( + // Foreign key + { + standardId: createForeignKeyDeterministicUuid({ + objectId: createdObjectMetadata.id, + standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom, + }), + objectMetadataId: timelineActivityObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: objectPrimaryKeyType, + name: `${createdObjectMetadata.nameSingular}Id`, + label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, + description: `Timeline Activity ${createdObjectMetadata.labelSingular} id foreign key`, + icon: undefined, + isNullable: true, + isSystem: true, + defaultValue: undefined, + settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true }, + }, + ); + + return timelineActivityObjectMetadata; + } + + private async createFavoriteRelation( + workspaceId: string, + createdObjectMetadata: ObjectMetadataEntity, + objectPrimaryKeyType: FieldMetadataType, + objectPrimaryKeyFieldSettings: + | FieldMetadataSettings + | undefined, + ) { + const favoriteObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + nameSingular: 'favorite', + workspaceId: workspaceId, + }); + + await this.fieldMetadataRepository.save( + // Foreign key + { + standardId: createForeignKeyDeterministicUuid({ + objectId: createdObjectMetadata.id, + standardId: FAVORITE_STANDARD_FIELD_IDS.custom, + }), + objectMetadataId: favoriteObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: objectPrimaryKeyType, + name: `${createdObjectMetadata.nameSingular}Id`, + label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, + description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`, + icon: undefined, + isNullable: true, + isSystem: true, + defaultValue: undefined, + settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true }, + }, + ); + + return favoriteObjectMetadata; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-create-remote-table-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-create-remote-table-relations.util.ts new file mode 100644 index 000000000..1cf526edf --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-create-remote-table-relations.util.ts @@ -0,0 +1,29 @@ +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { + WorkspaceMigrationTableAction, + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnCreate, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; + +export const buildMigrationsToCreateRemoteTableRelations = ( + createdObjectNameSingular: string, + targetObjectMetadataList: ObjectMetadataEntity[], + primaryKeyColumnType: string, +): WorkspaceMigrationTableAction[] => + targetObjectMetadataList.map((targetObjectMetadata) => ({ + name: computeObjectTargetTable(targetObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: computeColumnName(createdObjectNameSingular, { + isForeignKey: true, + }), + columnType: primaryKeyColumnType, + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + })); diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-remove-remote-table-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-remove-remote-table-relations.util.ts new file mode 100644 index 000000000..a928570d7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-remove-remote-table-relations.util.ts @@ -0,0 +1,26 @@ +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { + WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnDrop, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; + +export const buildMigrationsToRemoveRemoteTableRelations = ( + targetColumnName: string, + targetObjectMetadataList: ObjectMetadataEntity[], +): WorkspaceMigrationTableAction[] => + targetObjectMetadataList.map((objectMetadata) => ({ + name: computeTableName( + objectMetadata.nameSingular, + objectMetadata.isCustom, + ), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.DROP, + columnName: targetColumnName, + } satisfies WorkspaceMigrationColumnDrop, + ], + }));