mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-23 22:12:24 +03:00
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:
parent
f8ec40dbfb
commit
41960f3593
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
@ -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') {
|
||||||
|
6
packages/twenty-server/src/utils/camel-to-title-case.ts
Normal file
6
packages/twenty-server/src/utils/camel-to-title-case.ts
Normal 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());
|
Loading…
Reference in New Issue
Block a user