Remote objects: Fix comment override - id typing - label (#4784)

Several fixes for remote objects:
- labels are now displayed in title case. Added an util for this.
- Ids are often integers but the foreign keys on the relations were
uuid. Sending the id type to the object metadata service so it can
creates the foreign key accordingly
- Graphql comments are override when several remote objects are
imported. Building a function that fetch the existing comment and update
it

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Thomas Trompette 2024-04-04 15:35:49 +02:00 committed by GitHub
parent f8ec40dbfb
commit 41960f3593
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 102 additions and 43 deletions

View File

@ -67,4 +67,8 @@ export class CreateObjectInput {
@IsOptional() @IsOptional()
@Field({ nullable: true }) @Field({ nullable: true })
isRemote?: boolean; isRemote?: boolean;
@IsOptional()
@Field({ nullable: true })
remoteTablePrimaryKeyColumnType?: string;
} }

View File

@ -45,8 +45,8 @@ import {
createForeignKeyDeterministicUuid, createForeignKeyDeterministicUuid,
createRelationDeterministicUuid, createRelationDeterministicUuid,
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; } 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 { 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'; import { buildWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.util';
import { ObjectMetadataEntity } from './object-metadata.entity'; import { ObjectMetadataEntity } from './object-metadata.entity';
@ -356,6 +356,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
createdObjectMetadata, createdObjectMetadata,
); );
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
createdObjectMetadata.workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
await this.workspaceMigrationService.createCustomMigration( await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`), generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
createdObjectMetadata.workspaceId, createdObjectMetadata.workspaceId,
@ -367,13 +375,15 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
eventObjectMetadata, eventObjectMetadata,
favoriteObjectMetadata, favoriteObjectMetadata,
) )
: buildWorkspaceMigrationsForRemoteObject( : await buildWorkspaceMigrationsForRemoteObject(
createdObjectMetadata, createdObjectMetadata,
activityTargetObjectMetadata, activityTargetObjectMetadata,
attachmentObjectMetadata, attachmentObjectMetadata,
eventObjectMetadata, eventObjectMetadata,
favoriteObjectMetadata, favoriteObjectMetadata,
lastDataSourceMetadata.schema, lastDataSourceMetadata.schema,
objectMetadataInput.remoteTablePrimaryKeyColumnType ?? 'uuid',
workspaceDataSource,
), ),
); );
@ -381,14 +391,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
createdObjectMetadata.workspaceId, createdObjectMetadata.workspaceId,
); );
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
createdObjectMetadata.workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
const view = await workspaceDataSource?.query( const view = await workspaceDataSource?.query(
`INSERT INTO ${dataSourceMetadata.schema}."view" `INSERT INTO ${dataSourceMetadata.schema}."view"
("objectMetadataId", "type", "name", "key", "icon") ("objectMetadataId", "type", "name", "key", "icon")

View File

@ -1,3 +1,5 @@
import { DataSource } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { import {
WorkspaceMigrationTableAction, WorkspaceMigrationTableAction,
@ -7,21 +9,53 @@ import {
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util'; import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
const buildCommentForRemoteObjectForeignKey = ( const buildCommentForRemoteObjectForeignKey = async (
localObjectMetadataName: string, localObjectMetadataName: string,
remoteObjectMetadataName: string, remoteObjectMetadataName: string,
schema: string, schema: string,
): string => workspaceDataSource: DataSource | undefined,
`@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"]}]})`; ): Promise<string> => {
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, createdObjectMetadata: ObjectMetadataEntity,
activityTargetObjectMetadata: ObjectMetadataEntity, activityTargetObjectMetadata: ObjectMetadataEntity,
attachmentObjectMetadata: ObjectMetadataEntity, attachmentObjectMetadata: ObjectMetadataEntity,
eventObjectMetadata: ObjectMetadataEntity, eventObjectMetadata: ObjectMetadataEntity,
favoriteObjectMetadata: ObjectMetadataEntity, favoriteObjectMetadata: ObjectMetadataEntity,
schema: string, schema: string,
): WorkspaceMigrationTableAction[] => { remoteTablePrimaryKeyColumnType: string,
workspaceDataSource: DataSource | undefined,
): Promise<WorkspaceMigrationTableAction[]> => {
const createdObjectName = createdObjectMetadata.nameSingular; const createdObjectName = createdObjectMetadata.nameSingular;
return [ return [
@ -35,7 +69,7 @@ export const buildWorkspaceMigrationsForRemoteObject = (
createdObjectMetadata.nameSingular, createdObjectMetadata.nameSingular,
false, false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: remoteTablePrimaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
], ],
@ -50,7 +84,7 @@ export const buildWorkspaceMigrationsForRemoteObject = (
createdObjectMetadata.nameSingular, createdObjectMetadata.nameSingular,
false, false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: remoteTablePrimaryKeyColumnType,
}, },
], ],
}, },
@ -60,10 +94,11 @@ export const buildWorkspaceMigrationsForRemoteObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, action: WorkspaceMigrationColumnActionType.CREATE_COMMENT,
comment: buildCommentForRemoteObjectForeignKey( comment: await buildCommentForRemoteObjectForeignKey(
activityTargetObjectMetadata.nameSingular, activityTargetObjectMetadata.nameSingular,
createdObjectName, createdObjectName,
schema, schema,
workspaceDataSource,
), ),
}, },
], ],
@ -79,7 +114,7 @@ export const buildWorkspaceMigrationsForRemoteObject = (
createdObjectMetadata.nameSingular, createdObjectMetadata.nameSingular,
false, false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: remoteTablePrimaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
], ],
@ -94,7 +129,7 @@ export const buildWorkspaceMigrationsForRemoteObject = (
createdObjectMetadata.nameSingular, createdObjectMetadata.nameSingular,
false, false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: remoteTablePrimaryKeyColumnType,
}, },
], ],
}, },
@ -104,10 +139,11 @@ export const buildWorkspaceMigrationsForRemoteObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, action: WorkspaceMigrationColumnActionType.CREATE_COMMENT,
comment: buildCommentForRemoteObjectForeignKey( comment: await buildCommentForRemoteObjectForeignKey(
attachmentObjectMetadata.nameSingular, attachmentObjectMetadata.nameSingular,
createdObjectName, createdObjectName,
schema, schema,
workspaceDataSource,
), ),
}, },
], ],
@ -123,7 +159,7 @@ export const buildWorkspaceMigrationsForRemoteObject = (
createdObjectMetadata.nameSingular, createdObjectMetadata.nameSingular,
false, false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: remoteTablePrimaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
], ],
@ -138,7 +174,7 @@ export const buildWorkspaceMigrationsForRemoteObject = (
createdObjectMetadata.nameSingular, createdObjectMetadata.nameSingular,
false, false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: remoteTablePrimaryKeyColumnType,
}, },
], ],
}, },
@ -148,10 +184,11 @@ export const buildWorkspaceMigrationsForRemoteObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, action: WorkspaceMigrationColumnActionType.CREATE_COMMENT,
comment: buildCommentForRemoteObjectForeignKey( comment: await buildCommentForRemoteObjectForeignKey(
eventObjectMetadata.nameSingular, eventObjectMetadata.nameSingular,
createdObjectName, createdObjectName,
schema, schema,
workspaceDataSource,
), ),
}, },
], ],
@ -167,7 +204,7 @@ export const buildWorkspaceMigrationsForRemoteObject = (
createdObjectMetadata.nameSingular, createdObjectMetadata.nameSingular,
false, false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: remoteTablePrimaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
], ],
@ -182,7 +219,7 @@ export const buildWorkspaceMigrationsForRemoteObject = (
createdObjectMetadata.nameSingular, createdObjectMetadata.nameSingular,
false, false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: remoteTablePrimaryKeyColumnType,
}, },
], ],
}, },
@ -192,10 +229,11 @@ export const buildWorkspaceMigrationsForRemoteObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, action: WorkspaceMigrationColumnActionType.CREATE_COMMENT,
comment: buildCommentForRemoteObjectForeignKey( comment: await buildCommentForRemoteObjectForeignKey(
favoriteObjectMetadata.nameSingular, favoriteObjectMetadata.nameSingular,
createdObjectName, createdObjectName,
schema, schema,
workspaceDataSource,
), ),
}, },
], ],

View File

@ -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 { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.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 { 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 { 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 { export class RemoteTableService {
constructor( constructor(
@ -149,36 +149,44 @@ export class RemoteTableService {
.map((column) => `"${column.column_name}" ${column.data_type}`) .map((column) => `"${column.column_name}" ${column.data_type}`)
.join(', '); .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( 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( 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 // Should be done in a transaction. To be discussed
const objectMetadata = await this.objectMetadataService.createOne({ const objectMetadata = await this.objectMetadataService.createOne({
nameSingular: `${input.name}Remote`, nameSingular: remoteTableName,
namePlural: `${input.name}Remotes`, namePlural: `${remoteTableName}s`,
labelSingular: `${capitalize(snakeCase(input.name)).replace( labelSingular: remoteTableLabel,
/_/g, labelPlural: `${remoteTableLabel}s`,
' ',
)} remote`,
labelPlural: `${capitalize(snakeCase(input.name)).replace(
/_/g,
' ',
)} remotes`,
description: 'Remote table', description: 'Remote table',
dataSourceId: dataSourceMetadata.id, dataSourceId: dataSourceMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
icon: 'IconUser', icon: 'IconUser',
isRemote: true, isRemote: true,
remoteTablePrimaryKeyColumnType: remoteTableIdColumn.udt_name,
} as CreateObjectInput); } as CreateObjectInput);
for (const column of remoteTableColumns) { for (const column of remoteTableColumns) {
const field = await this.fieldMetadataService.createOne({ const field = await this.fieldMetadataService.createOne({
name: column.column_name, name: column.column_name,
label: capitalize(snakeCase(column.column_name)).replace(/_/g, ' '), label: camelToTitleCase(camelCase(column.column_name)),
description: 'Field of remote', description: 'Field of remote',
// TODO: function should work for other types than Postgres // TODO: function should work for other types than Postgres
type: mapUdtNameToFieldType(column.udt_name), type: mapUdtNameToFieldType(column.udt_name),
@ -186,6 +194,7 @@ export class RemoteTableService {
objectMetadataId: objectMetadata.id, objectMetadataId: objectMetadata.id,
isRemoteCreation: true, isRemoteCreation: true,
isNullable: true, isNullable: true,
icon: 'IconUser',
} as CreateFieldInput); } as CreateFieldInput);
if (column.column_name === 'id') { if (column.column_name === 'id') {

View File

@ -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());