From 1c6f0eb577135451d706b6418ac4f471ecdf2d81 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 3 Apr 2024 14:56:51 +0200 Subject: [PATCH] Integrate relations for remote objects (#4754) Foreign table id cannot be a foreign key of a base table. But the current code use foreign keys to link object metadata with activities, events... So we will: - create a column without creating a foreign key - add a comment on the table schema so pg_graphql sees it as a foreign key This PR: - refactor a bit object metadata service so the mutation creation is separated into an util - adds the mutation creation for remote object relations - add a new type of mutation to create a comment --------- Co-authored-by: Thomas Trompette --- .../hooks/useLoadRecordIndexBoard.ts | 7 +- .../components/RecordShowContainer.tsx | 2 +- .../foreign-data-wrapper-query.factory.ts | 3 +- .../object-metadata.service.ts | 235 ++++-------------- ...-workspace-migrations-for-custom-object.ts | 175 +++++++++++++ ...-workspace-migrations-for-remote-object.ts | 204 +++++++++++++++ .../utils/validate-remote-server-input.ts | 2 +- .../workspace-migration.entity.ts | 7 + .../workspace-migration-runner.service.ts | 19 ++ 9 files changed, 454 insertions(+), 200 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-custom-object.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.ts diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index 0efe9e2f5..8e58e3448 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -47,10 +47,9 @@ export const useLoadRecordIndexBoard = ({ recordIndexFilters, objectMetadataItem?.fields ?? [], ); - const orderBy = turnSortsIntoOrderBy( - recordIndexSorts, - objectMetadataItem?.fields ?? [], - ); + const orderBy = !objectMetadataItem.isRemote + ? turnSortsIntoOrderBy(recordIndexSorts, objectMetadataItem?.fields ?? []) + : undefined; const recordIndexIsCompactModeActive = useRecoilValue( recordIndexIsCompactModeActiveState, diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 98c7d8eb5..0ab972672 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -189,7 +189,7 @@ export const RecordShowContainer = ({ objectRecordId={objectRecordId} objectNameSingular={objectNameSingular} /> - {relationFieldMetadataItems.map((fieldMetadataItem, index) => ( + {relationFieldMetadataItems?.map((fieldMetadataItem, index) => ( [ + { + name: computeObjectTargetTable(createdObjectMetadata), + action: 'create', + } satisfies WorkspaceMigrationTableAction, + // Add activity target relation + { + name: computeObjectTargetTable(activityTargetObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + }, + { + name: computeObjectTargetTable(activityTargetObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + referencedTableName: computeObjectTargetTable(createdObjectMetadata), + referencedTableColumnName: 'id', + onDelete: RelationOnDeleteAction.CASCADE, + }, + ], + }, + // Add attachment relation + { + name: computeObjectTargetTable(attachmentObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + }, + { + name: computeObjectTargetTable(attachmentObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + referencedTableName: computeObjectTargetTable(createdObjectMetadata), + referencedTableColumnName: 'id', + onDelete: RelationOnDeleteAction.CASCADE, + }, + ], + }, + // Add event relation + { + name: computeObjectTargetTable(eventObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + }, + { + name: computeObjectTargetTable(eventObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + referencedTableName: computeObjectTargetTable(createdObjectMetadata), + referencedTableColumnName: 'id', + onDelete: RelationOnDeleteAction.CASCADE, + }, + ], + }, + // Add favorite relation + { + name: computeObjectTargetTable(favoriteObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + }, + { + name: computeObjectTargetTable(favoriteObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + referencedTableName: computeObjectTargetTable(createdObjectMetadata), + referencedTableColumnName: 'id', + onDelete: RelationOnDeleteAction.CASCADE, + }, + ], + }, + { + name: computeObjectTargetTable(createdObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: 'position', + columnType: 'float', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + } satisfies WorkspaceMigrationTableAction, + // This is temporary until we implement mainIdentifier + { + name: computeObjectTargetTable(createdObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: 'name', + columnType: 'text', + defaultValue: "'Untitled'", + } satisfies WorkspaceMigrationColumnCreate, + ], + } satisfies WorkspaceMigrationTableAction, +]; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.ts new file mode 100644 index 000000000..2a593ccf7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.ts @@ -0,0 +1,204 @@ +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { + WorkspaceMigrationTableAction, + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnCreate, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { computeCustomName } from 'src/engine/utils/compute-custom-name.util'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; + +const buildCommentForRemoteObjectForeignKey = ( + localObjectMetadataName: string, + remoteObjectMetadataName: string, + schema: string, +): string => + `@graphql({"totalCount":{"enabled": true},"foreign_keys":[{"local_name":"${localObjectMetadataName}Collection","local_columns":["${remoteObjectMetadataName}Id"],"foreign_name":"${remoteObjectMetadataName}","foreign_schema":"${schema}","foreign_table":"${remoteObjectMetadataName}","foreign_columns":["id"]}]})`; + +export const buildWorkspaceMigrationsForRemoteObject = ( + createdObjectMetadata: ObjectMetadataEntity, + activityTargetObjectMetadata: ObjectMetadataEntity, + attachmentObjectMetadata: ObjectMetadataEntity, + eventObjectMetadata: ObjectMetadataEntity, + favoriteObjectMetadata: ObjectMetadataEntity, + schema: string, +): WorkspaceMigrationTableAction[] => { + const createdObjectName = createdObjectMetadata.nameSingular; + + return [ + { + name: computeObjectTargetTable(activityTargetObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + }, + { + name: computeObjectTargetTable(activityTargetObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + }, + ], + }, + { + name: computeObjectTargetTable(activityTargetObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, + comment: buildCommentForRemoteObjectForeignKey( + activityTargetObjectMetadata.nameSingular, + createdObjectName, + schema, + ), + }, + ], + }, + // Add attachment relation + { + name: computeObjectTargetTable(attachmentObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + }, + { + name: computeObjectTargetTable(attachmentObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + }, + ], + }, + { + name: computeObjectTargetTable(attachmentObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, + comment: buildCommentForRemoteObjectForeignKey( + attachmentObjectMetadata.nameSingular, + createdObjectName, + schema, + ), + }, + ], + }, + // Add event relation + { + name: computeObjectTargetTable(eventObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + }, + { + name: computeObjectTargetTable(eventObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + }, + ], + }, + { + name: computeObjectTargetTable(eventObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, + comment: buildCommentForRemoteObjectForeignKey( + eventObjectMetadata.nameSingular, + createdObjectName, + schema, + ), + }, + ], + }, + // Add favorite relation + { + name: computeObjectTargetTable(favoriteObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + isNullable: true, + } satisfies WorkspaceMigrationColumnCreate, + ], + }, + { + name: computeObjectTargetTable(favoriteObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${computeCustomName( + createdObjectMetadata.nameSingular, + false, + )}Id`, + columnType: 'uuid', + }, + ], + }, + { + name: computeObjectTargetTable(favoriteObjectMetadata), + action: 'alter', + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, + comment: buildCommentForRemoteObjectForeignKey( + favoriteObjectMetadata.nameSingular, + createdObjectName, + schema, + ), + }, + ], + }, + ]; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts index 4847351c3..5c043f2a9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts @@ -1,4 +1,4 @@ -const INPUT_REGEX = /^([A-Za-z0-9\-\_]+)$/; +const INPUT_REGEX = /^([A-Za-z0-9\-\_\.]+)$/; export const validateObject = (input: object) => { for (const [key, value] of Object.entries(input)) { diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts index 73a87cd16..c0bd136f6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts @@ -13,6 +13,7 @@ export enum WorkspaceMigrationColumnActionType { CREATE_FOREIGN_KEY = 'CREATE_FOREIGN_KEY', DROP_FOREIGN_KEY = 'DROP_FOREIGN_KEY', DROP = 'DROP', + CREATE_COMMENT = 'CREATE_COMMENT', } export type WorkspaceMigrationEnum = string | { from: string; to: string }; @@ -56,6 +57,11 @@ export type WorkspaceMigrationColumnDrop = { columnName: string; }; +export type WorkspaceMigrationCreateComment = { + action: WorkspaceMigrationColumnActionType.CREATE_COMMENT; + comment: string; +}; + export type WorkspaceMigrationColumnAction = { action: WorkspaceMigrationColumnActionType; } & ( @@ -64,6 +70,7 @@ export type WorkspaceMigrationColumnAction = { | WorkspaceMigrationColumnCreateRelation | WorkspaceMigrationColumnDropRelation | WorkspaceMigrationColumnDrop + | WorkspaceMigrationCreateComment ); export type WorkspaceMigrationTableAction = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts index 32fa10802..5c645a260 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts @@ -224,6 +224,14 @@ export class WorkspaceMigrationRunnerService { columnMigration.columnName, ); break; + case WorkspaceMigrationColumnActionType.CREATE_COMMENT: + await this.createComment( + queryRunner, + schemaName, + tableName, + columnMigration.comment, + ); + break; default: throw new Error(`Migration column action not supported`); } @@ -412,4 +420,15 @@ export class WorkspaceMigrationRunnerService { return foreignKeys[0]?.constraint_name; } + + private async createComment( + queryRunner: QueryRunner, + schemaName: string, + tableName: string, + comment: string, + ) { + await queryRunner.query(` + COMMENT ON TABLE "${schemaName}"."${tableName}" IS e'${comment}'; + `); + } }