diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts index a966b983a4..0930b4d8ae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts @@ -67,4 +67,8 @@ export class CreateObjectInput { @IsOptional() @Field({ nullable: true }) isRemote?: boolean; + + @IsOptional() + @Field({ nullable: true }) + remoteTablePrimaryKeyColumnType?: string; } 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 ad521bd0b8..e637fbd08d 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 @@ -45,8 +45,8 @@ import { createForeignKeyDeterministicUuid, createRelationDeterministicUuid, } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; -import { buildWorkspaceMigrationsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-custom-object'; -import { buildWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object'; +import { buildWorkspaceMigrationsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-custom-object.util'; +import { buildWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.util'; import { ObjectMetadataEntity } from './object-metadata.entity'; @@ -356,6 +356,14 @@ export class ObjectMetadataService extends TypeOrmQueryService - `@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"]}]})`; + workspaceDataSource: DataSource | undefined, +): Promise => { + const existingComment = await workspaceDataSource?.query( + `SELECT col_description('${schema}."${localObjectMetadataName}"'::regclass, 0)`, + ); -export const buildWorkspaceMigrationsForRemoteObject = ( + if (!existingComment[0]?.col_description) { + return `@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"]}]})`; + } + + const commentWithoutGraphQL = existingComment[0].col_description + .replace('@graphql(', '') + .replace(')', ''); + const parsedComment = JSON.parse(commentWithoutGraphQL); + + const foreignKey = { + local_name: `${localObjectMetadataName}Collection`, + local_columns: [`${remoteObjectMetadataName}Id`], + foreign_name: `${remoteObjectMetadataName}`, + foreign_schema: schema, + foreign_table: remoteObjectMetadataName, + foreign_columns: ['id'], + }; + + if (parsedComment.foreign_keys) { + parsedComment.foreign_keys.push(foreignKey); + } else { + parsedComment.foreign_keys = [foreignKey]; + } + + return `@graphql(${JSON.stringify(parsedComment)})`; +}; + +export const buildWorkspaceMigrationsForRemoteObject = async ( createdObjectMetadata: ObjectMetadataEntity, activityTargetObjectMetadata: ObjectMetadataEntity, attachmentObjectMetadata: ObjectMetadataEntity, eventObjectMetadata: ObjectMetadataEntity, favoriteObjectMetadata: ObjectMetadataEntity, schema: string, -): WorkspaceMigrationTableAction[] => { + remoteTablePrimaryKeyColumnType: string, + workspaceDataSource: DataSource | undefined, +): Promise => { const createdObjectName = createdObjectMetadata.nameSingular; return [ @@ -35,7 +69,7 @@ export const buildWorkspaceMigrationsForRemoteObject = ( createdObjectMetadata.nameSingular, false, )}Id`, - columnType: 'uuid', + columnType: remoteTablePrimaryKeyColumnType, isNullable: true, } satisfies WorkspaceMigrationColumnCreate, ], @@ -50,7 +84,7 @@ export const buildWorkspaceMigrationsForRemoteObject = ( createdObjectMetadata.nameSingular, false, )}Id`, - columnType: 'uuid', + columnType: remoteTablePrimaryKeyColumnType, }, ], }, @@ -60,10 +94,11 @@ export const buildWorkspaceMigrationsForRemoteObject = ( columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, - comment: buildCommentForRemoteObjectForeignKey( + comment: await buildCommentForRemoteObjectForeignKey( activityTargetObjectMetadata.nameSingular, createdObjectName, schema, + workspaceDataSource, ), }, ], @@ -79,7 +114,7 @@ export const buildWorkspaceMigrationsForRemoteObject = ( createdObjectMetadata.nameSingular, false, )}Id`, - columnType: 'uuid', + columnType: remoteTablePrimaryKeyColumnType, isNullable: true, } satisfies WorkspaceMigrationColumnCreate, ], @@ -94,7 +129,7 @@ export const buildWorkspaceMigrationsForRemoteObject = ( createdObjectMetadata.nameSingular, false, )}Id`, - columnType: 'uuid', + columnType: remoteTablePrimaryKeyColumnType, }, ], }, @@ -104,10 +139,11 @@ export const buildWorkspaceMigrationsForRemoteObject = ( columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, - comment: buildCommentForRemoteObjectForeignKey( + comment: await buildCommentForRemoteObjectForeignKey( attachmentObjectMetadata.nameSingular, createdObjectName, schema, + workspaceDataSource, ), }, ], @@ -123,7 +159,7 @@ export const buildWorkspaceMigrationsForRemoteObject = ( createdObjectMetadata.nameSingular, false, )}Id`, - columnType: 'uuid', + columnType: remoteTablePrimaryKeyColumnType, isNullable: true, } satisfies WorkspaceMigrationColumnCreate, ], @@ -138,7 +174,7 @@ export const buildWorkspaceMigrationsForRemoteObject = ( createdObjectMetadata.nameSingular, false, )}Id`, - columnType: 'uuid', + columnType: remoteTablePrimaryKeyColumnType, }, ], }, @@ -148,10 +184,11 @@ export const buildWorkspaceMigrationsForRemoteObject = ( columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, - comment: buildCommentForRemoteObjectForeignKey( + comment: await buildCommentForRemoteObjectForeignKey( eventObjectMetadata.nameSingular, createdObjectName, schema, + workspaceDataSource, ), }, ], @@ -167,7 +204,7 @@ export const buildWorkspaceMigrationsForRemoteObject = ( createdObjectMetadata.nameSingular, false, )}Id`, - columnType: 'uuid', + columnType: remoteTablePrimaryKeyColumnType, isNullable: true, } satisfies WorkspaceMigrationColumnCreate, ], @@ -182,7 +219,7 @@ export const buildWorkspaceMigrationsForRemoteObject = ( createdObjectMetadata.nameSingular, false, )}Id`, - columnType: 'uuid', + columnType: remoteTablePrimaryKeyColumnType, }, ], }, @@ -192,10 +229,11 @@ export const buildWorkspaceMigrationsForRemoteObject = ( columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, - comment: buildCommentForRemoteObjectForeignKey( + comment: await buildCommentForRemoteObjectForeignKey( favoriteObjectMetadata.nameSingular, createdObjectName, schema, + workspaceDataSource, ), }, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts index 548ebf5140..21fdaa994e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts @@ -22,9 +22,9 @@ import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dto import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { RemotePostgresTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service'; -import { snakeCase } from 'src/utils/snake-case'; -import { capitalize } from 'src/utils/capitalize'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; +import { camelCase } from 'src/utils/camel-case'; +import { camelToTitleCase } from 'src/utils/camel-to-title-case'; export class RemoteTableService { constructor( @@ -149,36 +149,44 @@ export class RemoteTableService { .map((column) => `"${column.column_name}" ${column.data_type}`) .join(', '); + const remoteTableName = `${camelCase(input.name)}Remote`; + const remoteTableLabel = camelToTitleCase(remoteTableName); + + // We only support remote tables with an id column for now. + const remoteTableIdColumn = remoteTableColumns.filter( + (column) => column.column_name === 'id', + )?.[0]; + + if (!remoteTableIdColumn) { + throw new Error('Remote table must have an id column'); + } + await workspaceDataSource.query( - `CREATE FOREIGN TABLE ${localSchema}."${input.name}Remote" (${foreignTableColumns}) SERVER "${remoteServer.foreignDataWrapperId}" OPTIONS (schema_name '${input.schema}', table_name '${input.name}')`, + `CREATE FOREIGN TABLE ${localSchema}."${remoteTableName}" (${foreignTableColumns}) SERVER "${remoteServer.foreignDataWrapperId}" OPTIONS (schema_name '${input.schema}', table_name '${input.name}')`, ); + await workspaceDataSource.query( - `COMMENT ON FOREIGN TABLE ${localSchema}."${input.name}Remote" IS e'@graphql({"primary_key_columns": ["id"], "totalCount": {"enabled": true}})'`, + `COMMENT ON FOREIGN TABLE ${localSchema}."${remoteTableName}" IS e'@graphql({"primary_key_columns": ["id"], "totalCount": {"enabled": true}})'`, ); // Should be done in a transaction. To be discussed const objectMetadata = await this.objectMetadataService.createOne({ - nameSingular: `${input.name}Remote`, - namePlural: `${input.name}Remotes`, - labelSingular: `${capitalize(snakeCase(input.name)).replace( - /_/g, - ' ', - )} remote`, - labelPlural: `${capitalize(snakeCase(input.name)).replace( - /_/g, - ' ', - )} remotes`, + nameSingular: remoteTableName, + namePlural: `${remoteTableName}s`, + labelSingular: remoteTableLabel, + labelPlural: `${remoteTableLabel}s`, description: 'Remote table', dataSourceId: dataSourceMetadata.id, workspaceId: workspaceId, icon: 'IconUser', isRemote: true, + remoteTablePrimaryKeyColumnType: remoteTableIdColumn.udt_name, } as CreateObjectInput); for (const column of remoteTableColumns) { const field = await this.fieldMetadataService.createOne({ name: column.column_name, - label: capitalize(snakeCase(column.column_name)).replace(/_/g, ' '), + label: camelToTitleCase(camelCase(column.column_name)), description: 'Field of remote', // TODO: function should work for other types than Postgres type: mapUdtNameToFieldType(column.udt_name), @@ -186,6 +194,7 @@ export class RemoteTableService { objectMetadataId: objectMetadata.id, isRemoteCreation: true, isNullable: true, + icon: 'IconUser', } as CreateFieldInput); if (column.column_name === 'id') { diff --git a/packages/twenty-server/src/utils/camel-to-title-case.ts b/packages/twenty-server/src/utils/camel-to-title-case.ts new file mode 100644 index 0000000000..149298d612 --- /dev/null +++ b/packages/twenty-server/src/utils/camel-to-title-case.ts @@ -0,0 +1,6 @@ +import { capitalize } from 'src/utils/capitalize'; + +export const camelToTitleCase = (camelCaseText: string) => + capitalize(camelCaseText) + .replace(/([A-Z])/g, ' $1') + .replace(/^./, (str) => str.toUpperCase());