From 5019b5febcc6e820f5712e2d2dcbb8951e087930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Mon, 8 Apr 2024 16:00:28 +0200 Subject: [PATCH] feat: drop target column map (#4670) This PR is dropping the column `targetColumnMap` of fieldMetadata entities. The goal of this column was to properly map field to their respecting column in the table. We decide to drop it and instead compute the column name on the fly when we need it, as it's more easier to support. Some parts of the code has been refactored to try making implementation of composite type more easier to understand and maintain. Fix #3760 --------- Co-authored-by: Charles Bochet --- .vscode/launch.json | 1 + packages/twenty-server/src/app.module.ts | 16 +- .../1711459912762-dropTargetColumnMap.ts | 17 ++ .../__mocks__/object-metadata-item.mock.ts | 13 +- .../factories/args-alias.factory.ts | 47 +++- .../factories/field-alias.factory.ts | 50 ++-- .../factories/relation-field-alias.factory.ts | 5 +- .../utils/composite-field-metadata.util.ts | 38 +++ .../utils/__tests__/parse-result.spec.ts | 46 ++-- .../utils/parse-result.util.ts | 28 +- ...composite-input-type-definition.factory.ts | 84 ++++++ ...omposite-object-type-definition.factory.ts | 84 ++++++ .../factories/factories.ts | 12 +- .../filter-type-definition.factory.ts | 91 ------- .../factories/filter-type.factory.ts | 102 ------- .../input-type-definition.factory.ts | 79 ++++-- .../factories/input-type.factory.ts | 87 ++++-- .../object-type-definition.factory.ts | 19 +- .../order-by-type-definition.factory.ts | 66 ----- .../factories/order-by-type.factory.ts | 55 ---- .../factories/output-type.factory.ts | 17 +- .../services/type-mapper.service.ts | 3 - .../storages/type-definitions.storage.ts | 32 ++- .../type-definitions.generator.ts | 201 +++++++------- .../workspace-schema-builder.module.ts | 4 +- .../utils/fields.utils.ts | 57 ++-- ...p-field-metadata-to-graphql-query.utils.ts | 1 + .../open-api/utils/components.utils.ts | 13 +- .../composite-types/address.composite-type.ts | 201 +++----------- .../currency.composite-type.ts | 81 +----- .../full-name.composite-type.ts | 79 +----- .../field-metadata/composite-types/index.ts | 23 +- .../composite-types/link.composite-type.ts | 79 +----- .../field-metadata/field-metadata.entity.ts | 4 - .../field-metadata/field-metadata.service.ts | 21 +- .../interfaces/composite-type.interface.ts | 14 + ...ld-metadata-target-column-map.interface.ts | 54 ---- .../interfaces/field-metadata.interface.ts | 2 - .../generate-target-column-map.spec.ts | 41 --- .../utils/compute-column-name.util.ts | 53 ++++ .../utils/generate-target-column-map.util.ts | 73 ----- .../object-metadata.service.ts | 58 +--- ...space-migrations-for-custom-object.util.ts | 58 ++-- ...space-migrations-for-remote-object.util.ts | 58 ++-- .../relation-metadata.service.ts | 34 +-- ...e-relation-foreign-key-column-name.util.ts | 15 -- .../factories/basic-column-action.factory.ts | 58 ++-- .../column-action-abstract.factory.ts | 6 +- .../composite-column-action.factory.ts | 132 ++++++++++ .../factories/enum-column-action.factory.ts | 74 +++--- .../factories/factories.ts | 2 + ...rkspace-column-action-factory.interface.ts | 2 +- .../workspace-migration.factory.ts | 55 ++-- .../deduce-relation-direction.spec.ts | 9 - .../utils/compute-object-target-table.util.ts | 7 +- ...ame.util.ts => compute-table-name.util.ts} | 2 +- .../utils/create-custom-column-name.util.ts | 3 - .../workspace-health/fixer/index.ts | 5 +- .../fixer/workspace-missing-column.fixer.ts | 82 ++++++ .../workspace-target-column-map.fixer.ts | 174 ------------ .../workspace-health-fix-kind.interface.ts | 2 +- .../workspace-health-issue.interface.ts | 7 +- .../services/database-structure.service.ts | 150 +++++++---- .../services/field-metadata-health.service.ts | 249 +++++++----------- .../relation-metadata.health.service.ts | 8 +- .../services/workspace-fix.service.ts | 21 +- .../is-workspace-health-issue-type.util.ts | 6 - .../workspace-field.comparator.spec.ts | 1 - .../comparators/workspace-field.comparator.ts | 2 +- .../decorators/field-metadata.decorator.ts | 3 - .../reflect-field-metadata.interface.ts | 2 - .../utils/compute-standard-object.util.ts | 7 - 72 files changed, 1432 insertions(+), 1853 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1711459912762-dropTargetColumnMap.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/filter-type-definition.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/filter-type.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/order-by-type-definition.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/order-by-type.factory.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/generate-target-column-map.spec.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-column-name.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts rename packages/twenty-server/src/engine/utils/{compute-custom-name.util.ts => compute-table-name.util.ts} (57%) delete mode 100644 packages/twenty-server/src/engine/utils/create-custom-column-name.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-target-column-map.fixer.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 80198e9cc..48983ed70 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,6 +7,7 @@ "type": "node", "request": "launch", "runtimeExecutable": "yarn", + "runtimeVersion": "18", "runtimeArgs": [ "nx", "run", diff --git a/packages/twenty-server/src/app.module.ts b/packages/twenty-server/src/app.module.ts index 3c63eebce..c7af6a645 100644 --- a/packages/twenty-server/src/app.module.ts +++ b/packages/twenty-server/src/app.module.ts @@ -7,6 +7,7 @@ import { import { ConfigModule } from '@nestjs/config'; import { ServeStaticModule } from '@nestjs/serve-static'; import { GraphQLModule } from '@nestjs/graphql'; +import { DevtoolsModule } from '@nestjs/devtools-integration'; import { existsSync } from 'fs'; import { join } from 'path'; @@ -19,6 +20,7 @@ import { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.mo import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module'; import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module'; import { GraphQLConfigService } from 'src/engine/api/graphql/graphql-config/graphql-config.service'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { UserWorkspaceMiddleware } from 'src/engine/middlewares/user-workspace.middleware'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; @@ -28,13 +30,13 @@ import { IntegrationsModule } from './engine/integrations/integrations.module'; @Module({ imports: [ // Nest.js devtools, use devtools.nestjs.com to debug - // DevtoolsModule.registerAsync({ - // useFactory: (environmentService: EnvironmentService) => ({ - // http: environmentService.get('DEBUG_MODE'), - // port: environmentService.get('DEBUG_PORT'), - // }), - // inject: [EnvironmentService], - // }), + DevtoolsModule.registerAsync({ + useFactory: (environmentService: EnvironmentService) => ({ + http: environmentService.get('DEBUG_MODE'), + port: environmentService.get('DEBUG_PORT'), + }), + inject: [EnvironmentService], + }), ConfigModule.forRoot({ isGlobal: true, }), diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711459912762-dropTargetColumnMap.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711459912762-dropTargetColumnMap.ts new file mode 100644 index 000000000..0c10d5339 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711459912762-dropTargetColumnMap.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class DropTargetColumnMap1711459912762 implements MigrationInterface { + name = 'DropTargetColumnMap1711459912762'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "targetColumnMap"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" ADD "targetColumnMap" jsonb NOT NULL`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index 77c1fd5f2..4402d3a51 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -6,7 +6,6 @@ export const fieldNumberMock = { type: FieldMetadataType.NUMBER, isNullable: false, defaultValue: null, - targetColumnMap: { value: 'fieldNumber' }, }; export const fieldStringMock = { @@ -14,7 +13,6 @@ export const fieldStringMock = { type: FieldMetadataType.TEXT, isNullable: true, defaultValue: null, - targetColumnMap: { value: 'fieldString' }, }; export const fieldLinkMock = { @@ -22,23 +20,18 @@ export const fieldLinkMock = { type: FieldMetadataType.LINK, isNullable: false, defaultValue: { label: '', url: '' }, - targetColumnMap: { label: 'fieldLinkLabel', url: 'fieldLinkUrl' }, }; export const fieldCurrencyMock = { name: 'fieldCurrency', type: FieldMetadataType.CURRENCY, isNullable: true, - defaultValue: null, - targetColumnMap: { - amountMicros: 'fieldCurrencyAmountMicros', - currencyCode: 'fieldCurrencyCurrencyCode', - }, + defaultValue: { amountMicros: null, currencyCode: "''" }, }; -export const objectMetadataItemMock: DeepPartial = { +export const objectMetadataItemMock = { targetTableName: 'testingObject', nameSingular: 'objectName', namePlural: 'objectsName', fields: [fieldNumberMock, fieldStringMock, fieldLinkMock, fieldCurrencyMock], -}; +} as ObjectMetadataEntity; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory.ts index ca0a8b857..5703a9747 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory.ts @@ -1,9 +1,15 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; + @Injectable() export class ArgsAliasFactory { + private readonly logger = new Logger(ArgsAliasFactory.name); + create( args: Record, fieldMetadataCollection: FieldMetadataInterface[], @@ -39,25 +45,42 @@ export class ArgsAliasFactory { for (const [key, value] of Object.entries(args)) { const fieldMetadata = fieldMetadataMap.get(key); - // If it's a special complex field, we need to map all columns + // If it's a composite type, we need to transform args to properly map column name if ( fieldMetadata && - typeof value === 'object' && value !== null && - Object.values(fieldMetadata.targetColumnMap).length > 1 + isCompositeFieldMetadataType(fieldMetadata.type) ) { - for (const [subKey, subValue] of Object.entries(value)) { - const mappedKey = fieldMetadata.targetColumnMap[subKey]; + // Get composite type definition + const compositeType = compositeTypeDefintions.get(fieldMetadata.type); - if (mappedKey) { - newArgs[mappedKey] = subValue; + if (!compositeType) { + this.logger.error( + `Composite type definition not found for type: ${fieldMetadata.type}`, + ); + throw new Error( + `Composite type definition not found for type: ${fieldMetadata.type}`, + ); + } + + // Loop through sub values and map them to composite property + for (const [subKey, subValue] of Object.entries(value)) { + // Find composite property + const compositeProperty = compositeType.properties.find( + (property) => property.name === subKey, + ); + + if (compositeProperty) { + const columnName = computeCompositeColumnName( + fieldMetadata, + compositeProperty, + ); + + newArgs[columnName] = subValue; } } } else if (fieldMetadata) { - // Otherwise we just need to map the value - const mappedKey = fieldMetadata.targetColumnMap.value; - - newArgs[mappedKey ?? key] = value; + newArgs[key] = value; } else { // Recurse if value is a nested object, otherwise append field or alias newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/field-alias.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/field-alias.factory.ts index 547c8704a..67a22534f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/field-alias.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/field-alias.factory.ts @@ -2,29 +2,49 @@ import { Injectable, Logger } from '@nestjs/common'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { createCompositeFieldKey } from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util'; +import { + computeColumnName, + computeCompositeColumnName, +} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; + @Injectable() export class FieldAliasFactory { private readonly logger = new Logger(FieldAliasFactory.name); create(fieldKey: string, fieldMetadata: FieldMetadataInterface) { - const entries = Object.entries(fieldMetadata.targetColumnMap); - - if (entries.length === 0) { - return null; - } - - if (entries.length === 1) { - // If there is only one value, use it as the alias - const alias = entries[0][1]; + // If it's not a composite field, we can just return the alias + if (!isCompositeFieldMetadataType(fieldMetadata.type)) { + const alias = computeColumnName(fieldMetadata); return `${fieldKey}: ${alias}`; } - // Otherwise it means it's a special type with multiple values, so we need map all columns - return ` - ${entries - .map(([key, value]) => `___${fieldMetadata.name}_${key}: ${value}`) - .join('\n')} - `; + // If it's a composite field, we need to get the definition + const compositeType = compositeTypeDefintions.get(fieldMetadata.type); + + if (!compositeType) { + this.logger.error( + `Composite type not found for field metadata type: ${fieldMetadata.type}`, + ); + throw new Error( + `Composite type not found for field metadata type: ${fieldMetadata.type}`, + ); + } + + return compositeType.properties + .map((property) => { + // Generate a prefixed key for the composite field, this will be computed when the query has ran + const compositeKey = createCompositeFieldKey( + fieldMetadata.name, + property.name, + ); + const alias = computeCompositeColumnName(fieldMetadata, property); + + return `${compositeKey}: ${alias}`; + }) + .join('\n'); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/relation-field-alias.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/relation-field-alias.factory.ts index 113f2af43..862c439ac 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/relation-field-alias.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/relation-field-alias.factory.ts @@ -14,6 +14,7 @@ import { import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { FieldsStringFactory } from './fields-string.factory'; import { ArgsStringFactory } from './args-string.factory'; @@ -118,9 +119,7 @@ export class RelationFieldAliasFactory { `; } - let relationAlias = fieldMetadata.isCustom - ? `${fieldKey}: _${fieldMetadata.name}` - : fieldKey; + let relationAlias = `${fieldKey}: ${computeColumnName(fieldMetadata)}`; // For one to one relations, pg_graphql use the target TableName on the side that is not storing the foreign key // so we need to alias it to the field key diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util.ts new file mode 100644 index 000000000..4dc21dd6f --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util.ts @@ -0,0 +1,38 @@ +/** + * Composite key are structured as follows: + * COMPOSITE___{parentFieldName}_{childFieldName} + * This util are here to pre-process and post-process the composite keys before and after querying the database + */ + +export const compositeFieldPrefix = 'COMPOSITE___'; + +export const createCompositeFieldKey = ( + fieldName: string, + propertyName: string, +): string => { + return `${compositeFieldPrefix}${fieldName}_${propertyName}`; +}; + +export const isPrefixedCompositeField = (key: string): boolean => { + return key.startsWith(compositeFieldPrefix); +}; + +export const parseCompositeFieldKey = ( + key: string, +): { + parentFieldName: string; + childFieldName: string; +} | null => { + const [parentFieldName, childFieldName] = key + .replace(compositeFieldPrefix, '') + .split('_'); + + if (!parentFieldName || !childFieldName) { + return null; + } + + return { + parentFieldName, + childFieldName, + }; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/__tests__/parse-result.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/__tests__/parse-result.spec.ts index 3b731d627..462b45ace 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/__tests__/parse-result.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/__tests__/parse-result.spec.ts @@ -1,19 +1,9 @@ +import { createCompositeFieldKey } from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util'; import { - isSpecialKey, - handleSpecialKey, + handleCompositeKey, parseResult, } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util'; -describe('isSpecialKey', () => { - test('should return true if the key starts with "___"', () => { - expect(isSpecialKey('___specialKey')).toBe(true); - }); - - test('should return false if the key does not start with "___"', () => { - expect(isSpecialKey('normalKey')).toBe(false); - }); -}); - describe('handleSpecialKey', () => { let result; @@ -21,8 +11,12 @@ describe('handleSpecialKey', () => { result = {}; }); - test('should correctly process a special key and add it to the result object', () => { - handleSpecialKey(result, '___complexField_link', 'value1'); + test('should correctly process a composite key and add it to the result object', () => { + handleCompositeKey( + result, + createCompositeFieldKey('complexField', 'link'), + 'value1', + ); expect(result).toEqual({ complexField: { link: 'value1', @@ -31,8 +25,16 @@ describe('handleSpecialKey', () => { }); test('should add values under the same newKey if called multiple times', () => { - handleSpecialKey(result, '___complexField_link', 'value1'); - handleSpecialKey(result, '___complexField_text', 'value2'); + handleCompositeKey( + result, + createCompositeFieldKey('complexField', 'link'), + 'value1', + ); + handleCompositeKey( + result, + createCompositeFieldKey('complexField', 'text'), + 'value2', + ); expect(result).toEqual({ complexField: { link: 'value1', @@ -41,8 +43,8 @@ describe('handleSpecialKey', () => { }); }); - test('should not create a new field if the special key is not correctly formed', () => { - handleSpecialKey(result, '___complexField', 'value1'); + test('should not create a new field if the composite key is not correctly formed', () => { + handleCompositeKey(result, 'COMPOSITE___complexField', 'value1'); expect(result).toEqual({}); }); }); @@ -51,9 +53,9 @@ describe('parseResult', () => { test('should recursively parse an object and handle special keys', () => { const obj = { normalField: 'value1', - ___specialField_part1: 'value2', + COMPOSITE___specialField_part1: 'value2', nested: { - ___specialFieldNested_part2: 'value3', + COMPOSITE___specialFieldNested_part2: 'value3', }, }; @@ -75,10 +77,10 @@ describe('parseResult', () => { test('should handle arrays and parse each element', () => { const objArray = [ { - ___specialField_part1: 'value1', + COMPOSITE___specialField_part1: 'value1', }, { - ___specialField_part2: 'value2', + COMPOSITE___specialField_part2: 'value2', }, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/parse-result.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/parse-result.util.ts index aedbb7fd1..b8ad1383e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/parse-result.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/parse-result.util.ts @@ -1,27 +1,25 @@ -export const isSpecialKey = (key: string): boolean => { - return key.startsWith('___'); -}; +import { + isPrefixedCompositeField, + parseCompositeFieldKey, +} from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util'; -export const handleSpecialKey = ( +export const handleCompositeKey = ( result: any, key: string, value: any, ): void => { - const parts = key.split('_').filter((part) => part); + const parsedFieldKey = parseCompositeFieldKey(key); - // If parts don't contain enough information, return without altering result - if (parts.length < 2) { + // If composite field key can't be parsed, return + if (!parsedFieldKey) { return; } - const newKey = parts.slice(0, -1).join(''); - const subKey = parts[parts.length - 1]; - - if (!result[newKey]) { - result[newKey] = {}; + if (!result[parsedFieldKey.parentFieldName]) { + result[parsedFieldKey.parentFieldName] = {}; } - result[newKey][subKey] = value; + result[parsedFieldKey.parentFieldName][parsedFieldKey.childFieldName] = value; }; export const parseResult = (obj: any): any => { @@ -41,8 +39,8 @@ export const parseResult = (obj: any): any => { result[key] = parseResult(obj[key]); } else if (key === '__typename') { result[key] = obj[key].replace(/^_*/, ''); - } else if (isSpecialKey(key)) { - handleSpecialKey(result, key, obj[key]); + } else if (isPrefixedCompositeField(key)) { + handleCompositeKey(result, key, obj[key]); } else { result[key] = obj[key]; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory.ts new file mode 100644 index 000000000..c12124007 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory.ts @@ -0,0 +1,84 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; + +import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; + +import { pascalCase } from 'src/utils/pascal-case'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + InputTypeDefinition, + InputTypeDefinitionKind, +} from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; + +import { InputTypeFactory } from './input-type.factory'; + +@Injectable() +export class CompositeInputTypeDefinitionFactory { + private readonly logger = new Logger( + CompositeInputTypeDefinitionFactory.name, + ); + constructor(private readonly inputTypeFactory: InputTypeFactory) {} + + public create( + compositeType: CompositeType, + kind: InputTypeDefinitionKind, + options: WorkspaceBuildSchemaOptions, + ): InputTypeDefinition { + const name = pascalCase(compositeType.type.toString().toLowerCase()); + + return { + target: compositeType.type.toString(), + kind, + type: new GraphQLInputObjectType({ + name: `${pascalCase(name)}${kind.toString()}Input`, + fields: this.generateFields(compositeType, kind, options), + }), + }; + } + + private generateFields( + compositeType: CompositeType, + kind: InputTypeDefinitionKind, + options: WorkspaceBuildSchemaOptions, + ): GraphQLInputFieldConfigMap { + const fields: GraphQLInputFieldConfigMap = {}; + + for (const property of compositeType.properties) { + // Relation fields are not supported in composite types + if (isRelationFieldMetadataType(property.type)) { + this.logger.error( + 'Relation fields are not supported in composite types', + { compositeType, property }, + ); + + throw new Error('Relation fields are not supported in composite types'); + } + + // Skip hidden fields + if (property.hidden === true || property.hidden === 'input') { + continue; + } + + const type = this.inputTypeFactory.create( + property.name, + property.type, + kind, + options, + { + nullable: !property.isRequired, + isArray: property.type === FieldMetadataType.MULTI_SELECT, + }, + ); + + fields[property.name] = { + type, + description: property.description, + }; + } + + return fields; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory.ts new file mode 100644 index 000000000..e39da0ae3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory.ts @@ -0,0 +1,84 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; + +import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; + +import { pascalCase } from 'src/utils/pascal-case'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { OutputTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory'; +import { + ObjectTypeDefinition, + ObjectTypeDefinitionKind, +} from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; + +@Injectable() +export class CompositeObjectTypeDefinitionFactory { + private readonly logger = new Logger( + CompositeObjectTypeDefinitionFactory.name, + ); + + constructor(private readonly outputTypeFactory: OutputTypeFactory) {} + + public create( + compositeType: CompositeType, + options: WorkspaceBuildSchemaOptions, + ): ObjectTypeDefinition { + const name = pascalCase(compositeType.type.toString().toLowerCase()); + const kind = ObjectTypeDefinitionKind.Plain; + + return { + target: compositeType.type.toString(), + kind, + type: new GraphQLObjectType({ + name: `${name}${kind.toString()}`, + fields: this.generateFields(compositeType, kind, options), + }), + }; + } + + private generateFields( + compositeType: CompositeType, + kind: ObjectTypeDefinitionKind, + options: WorkspaceBuildSchemaOptions, + ): GraphQLFieldConfigMap { + const fields: GraphQLFieldConfigMap = {}; + + for (const property of compositeType.properties) { + // Relation fields are not supported in composite types + if (isRelationFieldMetadataType(property.type)) { + this.logger.error( + 'Relation fields are not supported in composite types', + { compositeType, property }, + ); + + throw new Error('Relation fields are not supported in composite types'); + } + + // Skip hidden fields + if (property.hidden === true || property.hidden === 'output') { + continue; + } + + const type = this.outputTypeFactory.create( + property.name, + property.type, + kind, + options, + { + nullable: !property.isRequired, + isArray: property.type === FieldMetadataType.MULTI_SELECT, + }, + ); + + fields[property.name] = { + type, + description: property.description, + }; + } + + return fields; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts index 71663a0d0..eed7d7d43 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts @@ -1,4 +1,6 @@ import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; +import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory'; +import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory'; import { ArgsFactory } from './args.factory'; import { InputTypeFactory } from './input-type.factory'; @@ -7,15 +9,11 @@ import { ObjectTypeDefinitionFactory } from './object-type-definition.factory'; import { OutputTypeFactory } from './output-type.factory'; import { QueryTypeFactory } from './query-type.factory'; import { RootTypeFactory } from './root-type.factory'; -import { FilterTypeFactory } from './filter-type.factory'; -import { FilterTypeDefinitionFactory } from './filter-type-definition.factory'; import { ConnectionTypeFactory } from './connection-type.factory'; import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory'; import { EdgeTypeFactory } from './edge-type.factory'; import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory'; import { MutationTypeFactory } from './mutation-type.factory'; -import { OrderByTypeFactory } from './order-by-type.factory'; -import { OrderByTypeDefinitionFactory } from './order-by-type-definition.factory'; import { RelationTypeFactory } from './relation-type.factory'; import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory'; import { OrphanedTypesFactory } from './orphaned-types.factory'; @@ -24,15 +22,13 @@ export const workspaceSchemaBuilderFactories = [ ArgsFactory, InputTypeFactory, InputTypeDefinitionFactory, + CompositeInputTypeDefinitionFactory, OutputTypeFactory, ObjectTypeDefinitionFactory, + CompositeObjectTypeDefinitionFactory, EnumTypeDefinitionFactory, RelationTypeFactory, ExtendObjectTypeDefinitionFactory, - FilterTypeFactory, - FilterTypeDefinitionFactory, - OrderByTypeFactory, - OrderByTypeDefinitionFactory, ConnectionTypeFactory, ConnectionTypeDefinitionFactory, EdgeTypeFactory, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/filter-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/filter-type-definition.factory.ts deleted file mode 100644 index 5a545f058..000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/filter-type-definition.factory.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; - -import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; - -import { pascalCase } from 'src/utils/pascal-case'; -import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; - -import { FilterTypeFactory } from './filter-type.factory'; -import { - InputTypeDefinition, - InputTypeDefinitionKind, -} from './input-type-definition.factory'; - -@Injectable() -export class FilterTypeDefinitionFactory { - constructor( - private readonly filterTypeFactory: FilterTypeFactory, - private readonly typeMapperService: TypeMapperService, - ) {} - - public create( - objectMetadata: ObjectMetadataInterface, - options: WorkspaceBuildSchemaOptions, - ): InputTypeDefinition { - const kind = InputTypeDefinitionKind.Filter; - const filterInputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`, - description: objectMetadata.description, - fields: () => { - const andOrType = this.typeMapperService.mapToGqlType(filterInputType, { - isArray: true, - arrayDepth: 1, - nullable: true, - }); - - return { - ...this.generateFields(objectMetadata, options), - and: { - type: andOrType, - }, - or: { - type: andOrType, - }, - not: { - type: this.typeMapperService.mapToGqlType(filterInputType, { - nullable: true, - }), - }, - }; - }, - }); - - return { - target: objectMetadata.id, - kind, - type: filterInputType, - }; - } - - private generateFields( - objectMetadata: ObjectMetadataInterface, - options: WorkspaceBuildSchemaOptions, - ): GraphQLInputFieldConfigMap { - const fields: GraphQLInputFieldConfigMap = {}; - - for (const fieldMetadata of objectMetadata.fields) { - // Relation types are generated during extension of object type definition - if (isRelationFieldMetadataType(fieldMetadata.type)) { - continue; - } - - const type = this.filterTypeFactory.create(fieldMetadata, options, { - nullable: fieldMetadata.isNullable, - defaultValue: fieldMetadata.defaultValue, - }); - - fields[fieldMetadata.name] = { - type, - description: fieldMetadata.description, - // TODO: Add default value - defaultValue: undefined, - }; - } - - return fields; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/filter-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/filter-type.factory.ts deleted file mode 100644 index 98fd306f8..000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/filter-type.factory.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { - GraphQLInputObjectType, - GraphQLInputType, - GraphQLList, - GraphQLScalarType, -} from 'graphql'; - -import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - -import { - TypeMapperService, - TypeOptions, -} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; -import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type'; - -import { InputTypeDefinitionKind } from './input-type-definition.factory'; - -@Injectable() -export class FilterTypeFactory { - private readonly logger = new Logger(FilterTypeFactory.name); - - constructor( - private readonly typeMapperService: TypeMapperService, - private readonly typeDefinitionsStorage: TypeDefinitionsStorage, - ) {} - - public create( - fieldMetadata: FieldMetadataInterface, - buildOptions: WorkspaceBuildSchemaOptions, - typeOptions: TypeOptions, - ): GraphQLInputType { - const target = isCompositeFieldMetadataType(fieldMetadata.type) - ? fieldMetadata.type.toString() - : fieldMetadata.id; - let filterType: GraphQLInputObjectType | GraphQLScalarType | undefined = - undefined; - - if (isEnumFieldMetadataType(fieldMetadata.type)) { - filterType = this.createEnumFilterType(fieldMetadata); - } else { - filterType = this.typeMapperService.mapToFilterType( - fieldMetadata.type, - buildOptions.dateScalarMode, - buildOptions.numberScalarMode, - ); - - filterType ??= this.typeDefinitionsStorage.getInputTypeByKey( - target, - InputTypeDefinitionKind.Filter, - ); - } - - if (!filterType) { - this.logger.error(`Could not find a GraphQL type for ${target}`, { - fieldMetadata, - buildOptions, - typeOptions, - }); - - throw new Error(`Could not find a GraphQL type for ${target}`); - } - - return this.typeMapperService.mapToGqlType(filterType, typeOptions); - } - - private createEnumFilterType( - fieldMetadata: FieldMetadataInterface, - ): GraphQLInputObjectType { - const enumType = this.typeDefinitionsStorage.getEnumTypeByKey( - fieldMetadata.id, - ); - - if (!enumType) { - this.logger.error( - `Could not find a GraphQL enum type for ${fieldMetadata.id}`, - { - fieldMetadata, - }, - ); - - throw new Error( - `Could not find a GraphQL enum type for ${fieldMetadata.id}`, - ); - } - - return new GraphQLInputObjectType({ - name: `${enumType.name}Filter`, - fields: () => ({ - eq: { type: enumType }, - neq: { type: enumType }, - in: { type: new GraphQLList(enumType) }, - is: { type: FilterIs }, - }), - }); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts index 6908bc9f3..183a41903 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable, forwardRef } from '@nestjs/common'; import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; @@ -8,6 +8,8 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad import { pascalCase } from 'src/utils/pascal-case'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { InputTypeFactory } from './input-type.factory'; @@ -26,23 +28,60 @@ export interface InputTypeDefinition { @Injectable() export class InputTypeDefinitionFactory { - constructor(private readonly inputTypeFactory: InputTypeFactory) {} + constructor( + @Inject(forwardRef(() => InputTypeFactory)) + private readonly inputTypeFactory: InputTypeFactory, + private readonly typeMapperService: TypeMapperService, + ) {} public create( objectMetadata: ObjectMetadataInterface, kind: InputTypeDefinitionKind, options: WorkspaceBuildSchemaOptions, ): InputTypeDefinition { + const inputType = new GraphQLInputObjectType({ + name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`, + description: objectMetadata.description, + fields: () => { + switch (kind) { + /** + * Filter input type has additional fields for filtering and is self referencing + */ + case InputTypeDefinitionKind.Filter: { + const andOrType = this.typeMapperService.mapToGqlType(inputType, { + isArray: true, + arrayDepth: 1, + nullable: true, + }); + + return { + ...this.generateFields(objectMetadata, kind, options), + and: { + type: andOrType, + }, + or: { + type: andOrType, + }, + not: { + type: this.typeMapperService.mapToGqlType(inputType, { + nullable: true, + }), + }, + }; + } + /** + * Other input types are generated with fields only + */ + default: + return this.generateFields(objectMetadata, kind, options); + } + }, + }); + return { target: objectMetadata.id, kind, - type: new GraphQLInputObjectType({ - name: `${pascalCase( - objectMetadata.nameSingular, - )}${kind.toString()}Input`, - description: objectMetadata.description, - fields: this.generateFields(objectMetadata, kind, options), - }), + type: inputType, }; } @@ -59,17 +98,25 @@ export class InputTypeDefinitionFactory { continue; } - const type = this.inputTypeFactory.create(fieldMetadata, kind, options, { - nullable: fieldMetadata.isNullable, - defaultValue: fieldMetadata.defaultValue, - isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, - }); + const target = isCompositeFieldMetadataType(fieldMetadata.type) + ? fieldMetadata.type.toString() + : fieldMetadata.id; + + const type = this.inputTypeFactory.create( + target, + fieldMetadata.type, + kind, + options, + { + nullable: fieldMetadata.isNullable, + defaultValue: fieldMetadata.defaultValue, + isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, + }, + ); fields[fieldMetadata.name] = { type, description: fieldMetadata.description, - // TODO: Add default value - defaultValue: undefined, }; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts index d1bd5f248..1449a59fc 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts @@ -1,16 +1,17 @@ import { Injectable, Logger } from '@nestjs/common'; -import { GraphQLInputType } from 'graphql'; +import { GraphQLInputObjectType, GraphQLInputType, GraphQLList } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { TypeMapperService, TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type'; +import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { InputTypeDefinitionKind } from './input-type-definition.factory'; @@ -24,30 +25,60 @@ export class InputTypeFactory { ) {} public create( - fieldMetadata: FieldMetadataInterface, + target: string, + type: FieldMetadataType, kind: InputTypeDefinitionKind, - buildOtions: WorkspaceBuildSchemaOptions, + buildOptions: WorkspaceBuildSchemaOptions, typeOptions: TypeOptions, ): GraphQLInputType { - const target = isCompositeFieldMetadataType(fieldMetadata.type) - ? fieldMetadata.type.toString() - : fieldMetadata.id; - let inputType: GraphQLInputType | undefined = - this.typeMapperService.mapToScalarType( - fieldMetadata.type, - buildOtions.dateScalarMode, - buildOtions.numberScalarMode, - ); + let inputType: GraphQLInputType | undefined; + switch (kind) { + /** + * Create and Update input types are classic scalar types + */ + case InputTypeDefinitionKind.Create: + case InputTypeDefinitionKind.Update: + inputType = this.typeMapperService.mapToScalarType( + type, + buildOptions.dateScalarMode, + buildOptions.numberScalarMode, + ); + break; + /** + * Filter input maps to special filter type + */ + case InputTypeDefinitionKind.Filter: { + if (isEnumFieldMetadataType(type)) { + inputType = this.createEnumFilterType(target); + } else { + inputType = this.typeMapperService.mapToFilterType( + type, + buildOptions.dateScalarMode, + buildOptions.numberScalarMode, + ); + } + + break; + } + /** + * OrderBy input maps to special order by type + */ + case InputTypeDefinitionKind.OrderBy: + inputType = this.typeMapperService.mapToOrderByType(type); + break; + } + + /** + * If input type is not scalar, we're looking for it in the type definitions storage as it can be an object type + */ inputType ??= this.typeDefinitionsStorage.getInputTypeByKey(target, kind); - inputType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target); - if (!inputType) { this.logger.error(`Could not find a GraphQL type for ${target}`, { - fieldMetadata, + type, kind, - buildOtions, + buildOptions, typeOptions, }); @@ -56,4 +87,24 @@ export class InputTypeFactory { return this.typeMapperService.mapToGqlType(inputType, typeOptions); } + + private createEnumFilterType(target: string): GraphQLInputObjectType { + const enumType = this.typeDefinitionsStorage.getEnumTypeByKey(target); + + if (!enumType) { + this.logger.error(`Could not find a GraphQL enum type for ${target}`); + + throw new Error(`Could not find a GraphQL enum type for ${target}`); + } + + return new GraphQLInputObjectType({ + name: `${enumType.name}Filter`, + fields: () => ({ + eq: { type: enumType }, + neq: { type: enumType }, + in: { type: new GraphQLList(enumType) }, + is: { type: FilterIs }, + }), + }); + } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts index 9974139b8..e0e078fe1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts @@ -8,6 +8,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad import { pascalCase } from 'src/utils/pascal-case'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { OutputTypeFactory } from './output-type.factory'; @@ -56,10 +57,20 @@ export class ObjectTypeDefinitionFactory { continue; } - const type = this.outputTypeFactory.create(fieldMetadata, kind, options, { - nullable: fieldMetadata.isNullable, - isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, - }); + const target = isCompositeFieldMetadataType(fieldMetadata.type) + ? fieldMetadata.type.toString() + : fieldMetadata.id; + + const type = this.outputTypeFactory.create( + target, + fieldMetadata.type, + kind, + options, + { + nullable: fieldMetadata.isNullable, + isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, + }, + ); fields[fieldMetadata.name] = { type, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/order-by-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/order-by-type-definition.factory.ts deleted file mode 100644 index 2ea6d00c0..000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/order-by-type-definition.factory.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; - -import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; - -import { pascalCase } from 'src/utils/pascal-case'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; - -import { - InputTypeDefinition, - InputTypeDefinitionKind, -} from './input-type-definition.factory'; -import { OrderByTypeFactory } from './order-by-type.factory'; - -@Injectable() -export class OrderByTypeDefinitionFactory { - constructor(private readonly orderByTypeFactory: OrderByTypeFactory) {} - - public create( - objectMetadata: ObjectMetadataInterface, - options: WorkspaceBuildSchemaOptions, - ): InputTypeDefinition { - const kind = InputTypeDefinitionKind.OrderBy; - - return { - target: objectMetadata.id, - kind, - type: new GraphQLInputObjectType({ - name: `${pascalCase( - objectMetadata.nameSingular, - )}${kind.toString()}Input`, - description: objectMetadata.description, - fields: this.generateFields(objectMetadata, options), - }), - }; - } - - private generateFields( - objectMetadata: ObjectMetadataInterface, - options: WorkspaceBuildSchemaOptions, - ): GraphQLInputFieldConfigMap { - const fields: GraphQLInputFieldConfigMap = {}; - - for (const fieldMetadata of objectMetadata.fields) { - // Relation field types are generated during extension of object type definition - if (isRelationFieldMetadataType(fieldMetadata.type)) { - continue; - } - - const type = this.orderByTypeFactory.create(fieldMetadata, options, { - nullable: fieldMetadata.isNullable, - }); - - fields[fieldMetadata.name] = { - type, - description: fieldMetadata.description, - // TODO: Add default value - defaultValue: undefined, - }; - } - - return fields; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/order-by-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/order-by-type.factory.ts deleted file mode 100644 index 0ab71f514..000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/order-by-type.factory.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { GraphQLInputType } from 'graphql'; - -import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - -import { - TypeMapperService, - TypeOptions, -} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; -import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; - -import { InputTypeDefinitionKind } from './input-type-definition.factory'; - -@Injectable() -export class OrderByTypeFactory { - private readonly logger = new Logger(OrderByTypeFactory.name); - - constructor( - private readonly typeMapperService: TypeMapperService, - private readonly typeDefinitionsStorage: TypeDefinitionsStorage, - ) {} - - public create( - fieldMetadata: FieldMetadataInterface, - buildOtions: WorkspaceBuildSchemaOptions, - typeOptions: TypeOptions, - ): GraphQLInputType { - const target = isCompositeFieldMetadataType(fieldMetadata.type) - ? fieldMetadata.type.toString() - : fieldMetadata.id; - let orderByType = this.typeMapperService.mapToOrderByType( - fieldMetadata.type, - ); - - orderByType ??= this.typeDefinitionsStorage.getInputTypeByKey( - target, - InputTypeDefinitionKind.OrderBy, - ); - - if (!orderByType) { - this.logger.error(`Could not find a GraphQL type for ${target}`, { - fieldMetadata, - buildOtions, - typeOptions, - }); - - throw new Error(`Could not find a GraphQL type for ${target}`); - } - - return this.typeMapperService.mapToGqlType(orderByType, typeOptions); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory.ts index 9a10ef836..425bee70d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory.ts @@ -3,14 +3,13 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLOutputType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { TypeMapperService, TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectTypeDefinitionKind } from './object-type-definition.factory'; @@ -24,28 +23,24 @@ export class OutputTypeFactory { ) {} public create( - fieldMetadata: FieldMetadataInterface, + target: string, + type: FieldMetadataType, kind: ObjectTypeDefinitionKind, buildOtions: WorkspaceBuildSchemaOptions, typeOptions: TypeOptions, ): GraphQLOutputType { - const target = isCompositeFieldMetadataType(fieldMetadata.type) - ? fieldMetadata.type.toString() - : fieldMetadata.id; let gqlType: GraphQLOutputType | undefined = this.typeMapperService.mapToScalarType( - fieldMetadata.type, + type, buildOtions.dateScalarMode, buildOtions.numberScalarMode, ); - gqlType ??= this.typeDefinitionsStorage.getObjectTypeByKey(target, kind); - - gqlType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target); + gqlType ??= this.typeDefinitionsStorage.getOutputTypeByKey(target, kind); if (!gqlType) { this.logger.error(`Could not find a GraphQL type for ${target}`, { - fieldMetadata, + type, buildOtions, typeOptions, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index abbc0a05c..83773136a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -57,7 +57,6 @@ export class TypeMapperService { const numberScalar = numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt; - // LINK and CURRENCY are handled in the factories because they are objects const typeScalarMapping = new Map([ [FieldMetadataType.UUID, GraphQLID], [FieldMetadataType.TEXT, GraphQLString], @@ -86,7 +85,6 @@ export class TypeMapperService { const numberScalar = numberScalarMode === 'float' ? FloatFilterType : IntFilterType; - // LINK and CURRENCY are handled in the factories because they are objects const typeFilterMapping = new Map< FieldMetadataType, GraphQLInputObjectType | GraphQLScalarType @@ -111,7 +109,6 @@ export class TypeMapperService { mapToOrderByType( fieldMetadataType: FieldMetadataType, ): GraphQLInputType | undefined { - // LINK and CURRENCY are handled in the factories because they are objects const typeOrderByMapping = new Map([ [FieldMetadataType.UUID, OrderByDirectionType], [FieldMetadataType.TEXT, OrderByDirectionType], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage.ts index 2d89820e1..585e9ad99 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage.ts @@ -17,7 +17,12 @@ import { ObjectTypeDefinitionKind, } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; -// Must be scoped on REQUEST level +export type GqlInputType = InputTypeDefinition | EnumTypeDefinition; + +export type GqlOutputType = ObjectTypeDefinition | EnumTypeDefinition; + +// Must be scoped on REQUEST level, because we need to recreate it for each workspaces +// TODO: Implement properly durable by workspace @Injectable({ scope: Scope.REQUEST, durable: true }) export class TypeDefinitionsStorage { private readonly enumTypeDefinitions = new Map(); @@ -68,10 +73,27 @@ export class TypeDefinitionsStorage { getInputTypeByKey( target: string, kind: InputTypeDefinitionKind, - ): GraphQLInputObjectType | undefined { - return this.inputTypeDefinitions.get( - this.generateCompositeKey(target, kind), - )?.type; + ): GraphQLInputObjectType | GraphQLEnumType | undefined { + const key = this.generateCompositeKey(target, kind); + let definition: GqlInputType | undefined; + + definition ??= this.inputTypeDefinitions.get(key); + definition ??= this.enumTypeDefinitions.get(target); + + return definition?.type; + } + + getOutputTypeByKey( + target: string, + kind: ObjectTypeDefinitionKind, + ): GraphQLObjectType | GraphQLEnumType | undefined { + const key = this.generateCompositeKey(target, kind); + let definition: GqlOutputType | undefined; + + definition ??= this.objectTypeDefinitions.get(key); + definition ??= this.enumTypeDefinitions.get(target); + + return definition?.type; } getEnumTypeByKey(target: string): GraphQLEnumType | undefined { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts index f73f08d55..9b91550fa 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts @@ -1,15 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { customTableDefaultColumns } from 'src/engine/workspace-manager/workspace-migration-runner/utils/custom-table-default-column.util'; -import { fullNameObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; -import { currencyObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; -import { linkObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type'; import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; -import { addressObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type'; +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory'; +import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory'; import { TypeDefinitionsStorage } from './storages/type-definitions.storage'; import { @@ -20,24 +17,12 @@ import { InputTypeDefinitionFactory, InputTypeDefinitionKind, } from './factories/input-type-definition.factory'; -import { getFieldMetadataType } from './utils/get-field-metadata-type.util'; import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface'; -import { FilterTypeDefinitionFactory } from './factories/filter-type-definition.factory'; import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory'; import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory'; -import { OrderByTypeDefinitionFactory } from './factories/order-by-type-definition.factory'; import { ExtendObjectTypeDefinitionFactory } from './factories/extend-object-type-definition.factory'; import { objectContainsRelationField } from './utils/object-contains-relation-field'; -// Create a default field for each custom table default column -const defaultFields = customTableDefaultColumns.map((column) => { - return { - type: getFieldMetadataType(column.type), - name: column.name, - isNullable: true, - } as FieldMetadataEntity; -}); - @Injectable() export class TypeDefinitionsGenerator { private readonly logger = new Logger(TypeDefinitionsGenerator.name); @@ -45,10 +30,10 @@ export class TypeDefinitionsGenerator { constructor( private readonly typeDefinitionsStorage: TypeDefinitionsStorage, private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory, + private readonly compositeObjectTypeDefinitionFactory: CompositeObjectTypeDefinitionFactory, private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory, private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory, - private readonly filterTypeDefintionFactory: FilterTypeDefinitionFactory, - private readonly orderByTypeDefinitionFactory: OrderByTypeDefinitionFactory, + private readonly compositeInputTypeDefinitionFactory: CompositeInputTypeDefinitionFactory, private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory, private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory, private readonly extendObjectTypeDefinitionFactory: ExtendObjectTypeDefinitionFactory, @@ -58,38 +43,96 @@ export class TypeDefinitionsGenerator { objectMetadataCollection: ObjectMetadataInterface[], options: WorkspaceBuildSchemaOptions, ) { - // Generate static objects first because they can be used in dynamic objects - this.generateStaticObjectTypeDefs(options); - // Generate dynamic objects - this.generateDynamicObjectTypeDefs(objectMetadataCollection, options); + // Generate composite type objects first because they can be used in dynamic objects + this.generateCompositeTypeDefs(options); + // Generate metadata objects + this.generateMetadataTypeDefs(objectMetadataCollection, options); } - private generateStaticObjectTypeDefs(options: WorkspaceBuildSchemaOptions) { - const staticObjectMetadataCollection = [ - currencyObjectDefinition, - linkObjectDefinition, - fullNameObjectDefinition, - addressObjectDefinition, - ] satisfies ObjectMetadataInterface[]; + /** + * GENERATE COMPOSITE TYPE OBJECTS + */ + private generateCompositeTypeDefs(options: WorkspaceBuildSchemaOptions) { + const compositeTypeCollection = [...compositeTypeDefintions.values()]; this.logger.log( - `Generating staticObjects: [${staticObjectMetadataCollection - .map((object) => object.nameSingular) + `Generating composite type objects: [${compositeTypeCollection + .map((compositeType) => compositeType.type) .join(', ')}]`, ); - // Generate static objects first because they can be used in dynamic objects - this.generateEnumTypeDefs(staticObjectMetadataCollection, options); - this.generateObjectTypeDefs(staticObjectMetadataCollection, options); - this.generateInputTypeDefs(staticObjectMetadataCollection, options); + // Generate composite types first because they can be used in metadata objects + this.generateCompositeObjectTypeDefs(compositeTypeCollection, options); + this.generateCompositeInputTypeDefs(compositeTypeCollection, options); } - private generateDynamicObjectTypeDefs( + private generateCompositeObjectTypeDefs( + compositeTypes: CompositeType[], + options: WorkspaceBuildSchemaOptions, + ) { + const compositeObjectTypeDefs = compositeTypes.map((compositeType) => + this.compositeObjectTypeDefinitionFactory.create(compositeType, options), + ); + + this.typeDefinitionsStorage.addObjectTypes(compositeObjectTypeDefs); + } + + private generateCompositeInputTypeDefs( + compisteTypes: CompositeType[], + options: WorkspaceBuildSchemaOptions, + ) { + const inputTypeDefs = compisteTypes + .map((compositeType) => { + const optionalExtendedObjectMetadata = { + ...compositeType, + properties: compositeType.properties.map((property) => ({ + ...property, + isRequired: false, + })), + }; + + return [ + // Input type for create + this.compositeInputTypeDefinitionFactory.create( + compositeType, + InputTypeDefinitionKind.Create, + options, + ), + // Input type for update + this.compositeInputTypeDefinitionFactory.create( + optionalExtendedObjectMetadata, + InputTypeDefinitionKind.Update, + options, + ), + // Filter input type + this.compositeInputTypeDefinitionFactory.create( + optionalExtendedObjectMetadata, + InputTypeDefinitionKind.Filter, + options, + ), + // OrderBy input type + this.compositeInputTypeDefinitionFactory.create( + optionalExtendedObjectMetadata, + InputTypeDefinitionKind.OrderBy, + options, + ), + ]; + }) + .flat(); + + this.typeDefinitionsStorage.addInputTypes(inputTypeDefs); + } + + /** + * GENERATE METADATA OBJECTS + */ + + private generateMetadataTypeDefs( dynamicObjectMetadataCollection: ObjectMetadataInterface[], options: WorkspaceBuildSchemaOptions, ) { this.logger.log( - `Generating dynamicObjects: [${dynamicObjectMetadataCollection + `Generating metadata objects: [${dynamicObjectMetadataCollection .map((object) => object.nameSingular) .join(', ')}]`, ); @@ -106,22 +149,16 @@ export class TypeDefinitionsGenerator { } private generateObjectTypeDefs( - objectMetadataCollection: ObjectMetadataInterface[], + objectMetadataCollection: ObjectMetadataInterface[] | CompositeType[], options: WorkspaceBuildSchemaOptions, ) { - const objectTypeDefs = objectMetadataCollection.map((objectMetadata) => { - const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); - const extendedObjectMetadata = { - ...objectMetadata, - fields, - }; - - return this.objectTypeDefinitionFactory.create( - extendedObjectMetadata, + const objectTypeDefs = objectMetadataCollection.map((objectMetadata) => + this.objectTypeDefinitionFactory.create( + objectMetadata, ObjectTypeDefinitionKind.Plain, options, - ); - }); + ), + ); this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs); } @@ -130,35 +167,15 @@ export class TypeDefinitionsGenerator { objectMetadataCollection: ObjectMetadataInterface[], options: WorkspaceBuildSchemaOptions, ) { - const edgeTypeDefs = objectMetadataCollection.map((objectMetadata) => { - const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); - const extendedObjectMetadata = { - ...objectMetadata, - fields, - }; - - return this.edgeTypeDefinitionFactory.create( - extendedObjectMetadata, - options, - ); - }); + const edgeTypeDefs = objectMetadataCollection.map((objectMetadata) => + this.edgeTypeDefinitionFactory.create(objectMetadata, options), + ); this.typeDefinitionsStorage.addObjectTypes(edgeTypeDefs); // Connection type defs are using edge type defs - const connectionTypeDefs = objectMetadataCollection.map( - (objectMetadata) => { - const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); - const extendedObjectMetadata = { - ...objectMetadata, - fields, - }; - - return this.connectionTypeDefinitionFactory.create( - extendedObjectMetadata, - options, - ); - }, + const connectionTypeDefs = objectMetadataCollection.map((objectMetadata) => + this.connectionTypeDefinitionFactory.create(objectMetadata, options), ); this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs); @@ -170,20 +187,18 @@ export class TypeDefinitionsGenerator { ) { const inputTypeDefs = objectMetadataCollection .map((objectMetadata) => { - const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); - const requiredExtendedObjectMetadata = { - ...objectMetadata, - fields, - }; const optionalExtendedObjectMetadata = { ...objectMetadata, - fields: fields.map((field) => ({ ...field, isNullable: true })), + fields: objectMetadata.fields.map((field) => ({ + ...field, + isNullable: true, + })), }; return [ // Input type for create this.inputTypeDefinitionFactory.create( - requiredExtendedObjectMetadata, + objectMetadata, InputTypeDefinitionKind.Create, options, ), @@ -194,13 +209,15 @@ export class TypeDefinitionsGenerator { options, ), // Filter input type - this.filterTypeDefintionFactory.create( + this.inputTypeDefinitionFactory.create( optionalExtendedObjectMetadata, + InputTypeDefinitionKind.Filter, options, ), // OrderBy input type - this.orderByTypeDefinitionFactory.create( + this.inputTypeDefinitionFactory.create( optionalExtendedObjectMetadata, + InputTypeDefinitionKind.OrderBy, options, ), ]; @@ -237,16 +254,4 @@ export class TypeDefinitionsGenerator { this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs); } - - private mergeFieldsWithDefaults( - fields: FieldMetadataInterface[], - ): FieldMetadataInterface[] { - const fieldNames = new Set(fields.map((field) => field.name)); - - const uniqueDefaultFields = defaultFields.filter( - (defaultField) => !fieldNames.has(defaultField.name), - ); - - return [...fields, ...uniqueDefaultFields]; - } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module.ts index f4be416d1..a5bc54e85 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module.ts @@ -12,10 +12,10 @@ import { TypeMapperService } from './services/type-mapper.service'; @Module({ imports: [ObjectMetadataModule], providers: [ - ...workspaceSchemaBuilderFactories, - TypeDefinitionsGenerator, TypeDefinitionsStorage, TypeMapperService, + ...workspaceSchemaBuilderFactories, + TypeDefinitionsGenerator, WorkspaceGraphQLSchemaFactory, ], exports: [WorkspaceGraphQLSchemaFactory], diff --git a/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/fields.utils.ts b/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/fields.utils.ts index 2e1e82ec3..951e05ff0 100644 --- a/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/fields.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/fields.utils.ts @@ -1,36 +1,55 @@ import { BadRequestException } from '@nestjs/common'; +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; export const getFieldType = ( - objectMetadataItem, - fieldName, + objectMetadata: ObjectMetadataEntity, + fieldName: string, ): FieldMetadataType | undefined => { - for (const itemField of objectMetadataItem.fields) { - if (fieldName === itemField.name) { - return itemField.type; + for (const fieldMetdata of objectMetadata.fields) { + if (fieldName === fieldMetdata.name) { + return fieldMetdata.type; } } }; -export const checkFields = (objectMetadataItem, fieldNames): void => { +export const checkFields = ( + objectMetadata: ObjectMetadataEntity, + fieldNames: string[], +): void => { + const fieldMetadataNames = objectMetadata.fields + .map((field) => { + if (isCompositeFieldMetadataType(field.type)) { + const compositeType = compositeTypeDefintions.get(field.type); + + if (!compositeType) { + throw new BadRequestException( + `Composite type '${field.type}' not found`, + ); + } + + // TODO: Don't really know why we need to put fieldName and compositeType name here + return [ + field.name, + compositeType.properties.map( + (compositeProperty) => compositeProperty.name, + ), + ].flat(); + } + + return field.name; + }) + .flat(); + for (const fieldName of fieldNames) { - if ( - !objectMetadataItem.fields - .reduce( - (acc, itemField) => [ - ...acc, - itemField.name, - ...Object.keys(itemField.targetColumnMap), - ], - [], - ) - .includes(fieldName) - ) { + if (!fieldMetadataNames.includes(fieldName)) { throw new BadRequestException( `field '${fieldName}' does not exist in '${computeObjectTargetTable( - objectMetadataItem, + objectMetadata, )}' object`, ); } diff --git a/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils.ts b/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils.ts index 873a67e9a..54c6cfeb5 100644 --- a/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils.ts @@ -3,6 +3,7 @@ import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metad const DEFAULT_DEPTH_VALUE = 2; +// TODO: Should be properly type and based on composite type definitions export const mapFieldMetadataToGraphqlQuery = ( objectMetadataItems, field, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 7cc346814..75307797a 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -11,6 +11,7 @@ import { computeLimitParameters, computeOrderByParameters, } from 'src/engine/core-modules/open-api/utils/parameters.utils'; +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; type Property = OpenAPIV3_1.SchemaObject; @@ -60,14 +61,14 @@ const getSchemaComponentsProperties = ( case FieldMetadataType.ADDRESS: itemProperty = { type: 'object', - properties: Object.keys(field.targetColumnMap).reduce( - (properties, key) => { - properties[key] = { type: 'string' }; + properties: compositeTypeDefintions + .get(field.type) + ?.properties?.reduce((properties, property) => { + // TODO: This should not be statically typed, instead we should do someting recursive + properties[property.name] = { type: 'string' }; return properties; - }, - {} as Properties, - ), + }, {} as Properties), }; break; case FieldMetadataType.RAW_JSON: diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/address.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/address.composite-type.ts index e3f84a0f9..a8871b037 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/address.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/address.composite-type.ts @@ -1,188 +1,61 @@ -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; -export const addressFields = ( - fieldMetadata?: FieldMetadataInterface, -): FieldMetadataInterface[] => { - const inferredFieldMetadata = fieldMetadata as - | FieldMetadataInterface - | undefined; - const targetColumnMap = inferredFieldMetadata - ? generateTargetColumnMap( - inferredFieldMetadata.type, - inferredFieldMetadata.isCustom ?? false, - inferredFieldMetadata.name, - ) - : { - addressStreet1: 'addressStreet1', - addressStreet2: 'addressStreet2', - addressCity: 'addressCity', - addressPostcode: 'addressPostcode', - addressState: 'addressState', - addressCountry: 'addressCountry', - addressLat: 'addressLat', - addressLng: 'addressLng', - }; - - return [ +export const addressCompositeType: CompositeType = { + type: FieldMetadataType.ADDRESS, + properties: [ { - id: 'addressStreet1', - type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.ADDRESS.toString(), name: 'addressStreet1', - label: 'Address', - targetColumnMap: { - value: targetColumnMap.addressStreet1, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.addressStreet1 ?? undefined, - } - : {}), - } satisfies FieldMetadataInterface, - { - id: 'addressStreet2', type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.ADDRESS.toString(), + hidden: false, + isRequired: false, + }, + { name: 'addressStreet2', - label: 'Address 2', - targetColumnMap: { - value: targetColumnMap.addressStreet2, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.addressStreet2 ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - { - id: 'addressCity', type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.ADDRESS.toString(), + hidden: false, + isRequired: false, + }, + { name: 'addressCity', - label: 'City', - targetColumnMap: { - value: targetColumnMap.addressCity, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.addressCity ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - { - id: 'addressPostcode', type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.ADDRESS.toString(), + hidden: false, + isRequired: false, + }, + { name: 'addressPostcode', - label: 'Postcode', - targetColumnMap: { - value: targetColumnMap.addressPostcode, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.addressPostcode ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - { - id: 'addressState', type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.ADDRESS.toString(), + hidden: false, + isRequired: false, + }, + { name: 'addressState', - label: 'State', - targetColumnMap: { - value: targetColumnMap.addressState, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.addressState ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - { - id: 'addressCountry', type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.ADDRESS.toString(), + hidden: false, + isRequired: false, + }, + { name: 'addressCountry', - label: 'Country', - targetColumnMap: { - value: targetColumnMap.addressCountry, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.addressCountry ?? null, - } - : {}), - } satisfies FieldMetadataInterface, + type: FieldMetadataType.TEXT, + hidden: false, + isRequired: false, + }, { - id: 'addressLat', - type: FieldMetadataType.NUMBER, - objectMetadataId: FieldMetadataType.ADDRESS.toString(), name: 'addressLat', - label: 'Latitude', - targetColumnMap: { - value: targetColumnMap.addressLat, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.addressLat ?? null, - } - : {}), - } satisfies FieldMetadataInterface, + type: FieldMetadataType.NUMERIC, + hidden: false, + isRequired: false, + }, { - id: 'addressLng', - type: FieldMetadataType.NUMBER, - objectMetadataId: FieldMetadataType.ADDRESS.toString(), name: 'addressLng', - label: 'Longitude', - targetColumnMap: { - value: targetColumnMap.addressLng, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.addressLng ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - ]; + type: FieldMetadataType.NUMERIC, + hidden: false, + isRequired: false, + }, + ], }; -export const addressObjectDefinition = { - id: FieldMetadataType.ADDRESS.toString(), - nameSingular: 'address', - namePlural: 'address', - labelSingular: 'Address', - labelPlural: 'Addresses', - targetTableName: '', - fields: addressFields(), - fromRelations: [], - toRelations: [], - isActive: true, - isSystem: true, - isCustom: false, - isRemote: false, -} satisfies ObjectMetadataInterface; - export type AddressMetadata = { addressStreet1: string; addressStreet2: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type.ts index 3a94dcda0..c7ce2147b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type.ts @@ -1,80 +1,25 @@ -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; -export const currencyFields = ( - fieldMetadata?: FieldMetadataInterface, -): FieldMetadataInterface[] => { - const inferredFieldMetadata = fieldMetadata as - | FieldMetadataInterface - | undefined; - const targetColumnMap = inferredFieldMetadata - ? generateTargetColumnMap( - inferredFieldMetadata.type, - inferredFieldMetadata.isCustom ?? false, - inferredFieldMetadata.name, - ) - : { - amountMicros: 'amountMicros', - currencyCode: 'currencyCode', - }; - - return [ +export const currencyCompositeType: CompositeType = { + type: FieldMetadataType.CURRENCY, + properties: [ { - id: 'amountMicros', - type: FieldMetadataType.NUMERIC, - objectMetadataId: FieldMetadataType.CURRENCY.toString(), name: 'amountMicros', - label: 'AmountMicros', - targetColumnMap: { - value: targetColumnMap.amountMicros, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.amountMicros ?? null, - } - : {}), - } satisfies FieldMetadataInterface, + type: FieldMetadataType.NUMERIC, + hidden: false, + isRequired: false, + }, { - id: 'currencyCode', - type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.CURRENCY.toString(), name: 'currencyCode', - label: 'Currency Code', - targetColumnMap: { - value: targetColumnMap.currencyCode, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: - inferredFieldMetadata.defaultValue?.currencyCode ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - ]; + type: FieldMetadataType.TEXT, + hidden: false, + isRequired: false, + }, + ], }; -export const currencyObjectDefinition = { - id: FieldMetadataType.CURRENCY.toString(), - nameSingular: 'currency', - namePlural: 'currency', - labelSingular: 'Currency', - labelPlural: 'Currency', - targetTableName: '', - fields: currencyFields(), - fromRelations: [], - toRelations: [], - isActive: true, - isSystem: true, - isCustom: false, - isRemote: false, -} satisfies ObjectMetadataInterface; - export type CurrencyMetadata = { amountMicros: number; currencyCode: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts index 614cd6677..835adb703 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts @@ -1,78 +1,25 @@ -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; -export const fullNameFields = ( - fieldMetadata?: FieldMetadataInterface, -): FieldMetadataInterface[] => { - const inferredFieldMetadata = fieldMetadata as - | FieldMetadataInterface - | undefined; - const targetColumnMap = inferredFieldMetadata - ? generateTargetColumnMap( - inferredFieldMetadata.type, - inferredFieldMetadata.isCustom ?? false, - inferredFieldMetadata.name, - ) - : { - firstName: 'firstName', - lastName: 'lastName', - }; - - return [ +export const fullNameCompositeType: CompositeType = { + type: FieldMetadataType.FULL_NAME, + properties: [ { - id: 'firstName', - type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.FULL_NAME.toString(), name: 'firstName', - label: 'First Name', - targetColumnMap: { - value: targetColumnMap.firstName, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: inferredFieldMetadata.defaultValue?.firstName ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - { - id: 'lastName', type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.FULL_NAME.toString(), + hidden: false, + isRequired: false, + }, + { name: 'lastName', - label: 'Last Name', - targetColumnMap: { - value: targetColumnMap.lastName, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: inferredFieldMetadata.defaultValue?.lastName ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - ]; + type: FieldMetadataType.TEXT, + hidden: false, + isRequired: false, + }, + ], }; -export const fullNameObjectDefinition = { - id: FieldMetadataType.FULL_NAME.toString(), - nameSingular: 'fullName', - namePlural: 'fullName', - labelSingular: 'FullName', - labelPlural: 'FullName', - targetTableName: '', - fields: fullNameFields(), - fromRelations: [], - toRelations: [], - isActive: true, - isSystem: true, - isCustom: false, - isRemote: false, -} satisfies ObjectMetadataInterface; - export type FullNameMetadata = { firstName: string; lastName: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts index 7867830be..970ac9baa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts @@ -1,21 +1,22 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; -import { currencyFields } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; -import { fullNameFields } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; -import { linkFields } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type'; +import { currencyCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; +import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; +import { linkCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { addressFields } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type'; +import { addressCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type'; export type CompositeFieldsDefinitionFunction = ( fieldMetadata?: FieldMetadataInterface, ) => FieldMetadataInterface[]; -export const compositeDefinitions = new Map< - string, - CompositeFieldsDefinitionFunction +export const compositeTypeDefintions = new Map< + FieldMetadataType, + CompositeType >([ - [FieldMetadataType.LINK, linkFields], - [FieldMetadataType.CURRENCY, currencyFields], - [FieldMetadataType.FULL_NAME, fullNameFields], - [FieldMetadataType.ADDRESS, addressFields], + [FieldMetadataType.LINK, linkCompositeType], + [FieldMetadataType.CURRENCY, currencyCompositeType], + [FieldMetadataType.FULL_NAME, fullNameCompositeType], + [FieldMetadataType.ADDRESS, addressCompositeType], ]); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/link.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/link.composite-type.ts index 88561f131..4037a5d5c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/link.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/link.composite-type.ts @@ -1,78 +1,25 @@ -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; -export const linkFields = ( - fieldMetadata?: FieldMetadataInterface, -): FieldMetadataInterface[] => { - const inferredFieldMetadata = fieldMetadata as - | FieldMetadataInterface - | undefined; - const targetColumnMap = inferredFieldMetadata - ? generateTargetColumnMap( - inferredFieldMetadata.type, - inferredFieldMetadata.isCustom ?? false, - inferredFieldMetadata.name, - ) - : { - label: 'label', - url: 'url', - }; - - return [ +export const linkCompositeType: CompositeType = { + type: FieldMetadataType.LINK, + properties: [ { - id: 'label', - type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.LINK.toString(), name: 'label', - label: 'Label', - targetColumnMap: { - value: targetColumnMap.label, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: inferredFieldMetadata.defaultValue?.label ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - { - id: 'url', type: FieldMetadataType.TEXT, - objectMetadataId: FieldMetadataType.LINK.toString(), + hidden: false, + isRequired: false, + }, + { name: 'url', - label: 'Url', - targetColumnMap: { - value: targetColumnMap.url, - }, - isNullable: true, - ...(inferredFieldMetadata - ? { - defaultValue: inferredFieldMetadata.defaultValue?.url ?? null, - } - : {}), - } satisfies FieldMetadataInterface, - ]; + type: FieldMetadataType.TEXT, + hidden: false, + isRequired: false, + }, + ], }; -export const linkObjectDefinition = { - id: FieldMetadataType.LINK.toString(), - nameSingular: 'link', - namePlural: 'link', - labelSingular: 'Link', - labelPlural: 'Link', - targetTableName: '', - fields: linkFields(), - fromRelations: [], - toRelations: [], - isActive: true, - isSystem: true, - isCustom: false, - isRemote: false, -} satisfies ObjectMetadataInterface; - export type LinkMetadata = { label: string; url: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index e21f98fc4..1678b3388 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -11,7 +11,6 @@ import { } from 'typeorm'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; @@ -74,9 +73,6 @@ export class FieldMetadataEntity< @Column({ nullable: false }) label: string; - @Column({ nullable: false, type: 'jsonb' }) - targetColumnMap: FieldMetadataTargetColumnMap; - @Column({ nullable: true, type: 'jsonb' }) defaultValue: FieldMetadataDefaultValue; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 1f5aff80b..670d2c1d8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -19,7 +19,6 @@ import { WorkspaceMigrationColumnDrop, WorkspaceMigrationTableAction, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; @@ -37,7 +36,7 @@ import { RelationMetadataType, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; -import { computeCustomName } from 'src/engine/utils/compute-custom-name.util'; +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { @@ -128,11 +127,6 @@ export class FieldMetadataService extends TypeOrmQueryService = [ - T, -] extends [keyof FieldMetadataTypeMapping] - ? FieldMetadataTypeMapping[T] - : T extends 'default' - ? AllFieldMetadataTypes - : FieldMetadataTargetColumnMapValue; - -export type FieldMetadataTargetColumnMap< - T extends FieldMetadataType | 'default' = 'default', -> = TypeByFieldMetadata; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts index 2f276ae21..0dff90644 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts @@ -1,4 +1,3 @@ -import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; @@ -12,7 +11,6 @@ export interface FieldMetadataInterface< type: FieldMetadataType; name: string; label: string; - targetColumnMap: FieldMetadataTargetColumnMap; defaultValue?: FieldMetadataDefaultValue; options?: FieldMetadataOptions; objectMetadataId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/generate-target-column-map.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/generate-target-column-map.spec.ts deleted file mode 100644 index 578e19191..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/generate-target-column-map.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; - -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; - -describe('generateTargetColumnMap', () => { - it('should generate a target column map for a given type', () => { - const textMap = generateTargetColumnMap( - FieldMetadataType.TEXT, - false, - 'name', - ); - - expect(textMap).toEqual({ value: 'name' }); - - const linkMap = generateTargetColumnMap( - FieldMetadataType.LINK, - false, - 'website', - ); - - expect(linkMap).toEqual({ label: 'websiteLabel', url: 'websiteUrl' }); - - const currencyMap = generateTargetColumnMap( - FieldMetadataType.CURRENCY, - true, - 'price', - ); - - expect(currencyMap).toEqual({ - amountMicros: '_priceAmountMicros', - currencyCode: '_priceCurrencyCode', - }); - }); - - it('should throw an error for an unknown type', () => { - expect(() => - generateTargetColumnMap('invalid' as FieldMetadataType, false, 'name'), - ).toThrow(BadRequestException); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts new file mode 100644 index 000000000..ac912a4ca --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts @@ -0,0 +1,53 @@ +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; + +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { pascalCase } from 'src/utils/pascal-case'; + +type ComputeColumnNameOptions = { isForeignKey?: boolean }; + +export function computeColumnName( + fieldName: string, + options?: ComputeColumnNameOptions, +): string; +export function computeColumnName( + fieldMetadata: FieldMetadataInterface, + ioptions?: ComputeColumnNameOptions, +): string; +// TODO: If we need to implement custom name logic for columns, we can do it here +export function computeColumnName( + fieldMetadataOrFieldName: FieldMetadataInterface | string, + options?: ComputeColumnNameOptions, +): string { + const generateName = (name: string) => { + return options?.isForeignKey ? `${name}Id` : name; + }; + + if (typeof fieldMetadataOrFieldName === 'string') { + return generateName(fieldMetadataOrFieldName); + } + + if (isCompositeFieldMetadataType(fieldMetadataOrFieldName.type)) { + throw new Error( + `Cannot compute column name for composite field metadata type: ${fieldMetadataOrFieldName.type}`, + ); + } + + return generateName(fieldMetadataOrFieldName.name); +} + +export const computeCompositeColumnName = < + T extends FieldMetadataType | 'default', +>( + fieldMetadata: FieldMetadataInterface, + compositeProperty: CompositeProperty, +): string => { + if (!isCompositeFieldMetadataType(fieldMetadata.type)) { + throw new Error( + `Cannot compute composite column name for non-composite field metadata type: ${fieldMetadata.type}`, + ); + } + + return `${fieldMetadata.name}${pascalCase(compositeProperty.name)}`; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util.ts deleted file mode 100644 index adc09ae1c..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; - -import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface'; - -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { createCustomColumnName } from 'src/engine/utils/create-custom-column-name.util'; - -/** - * Generate a target column map for a given type, this is used to map the field to the correct column(s) in the database. - * This is used to support fields that map to multiple columns in the database. - * - * @param type string - * @returns FieldMetadataTargetColumnMap - */ -export function generateTargetColumnMap( - type: FieldMetadataType, - isCustomField: boolean, - fieldName: string, -): FieldMetadataTargetColumnMap { - const columnName = isCustomField - ? createCustomColumnName(fieldName) - : fieldName; - - switch (type) { - case FieldMetadataType.UUID: - case FieldMetadataType.TEXT: - case FieldMetadataType.PHONE: - case FieldMetadataType.EMAIL: - case FieldMetadataType.NUMBER: - case FieldMetadataType.NUMERIC: - case FieldMetadataType.PROBABILITY: - case FieldMetadataType.BOOLEAN: - case FieldMetadataType.DATE_TIME: - case FieldMetadataType.RATING: - case FieldMetadataType.SELECT: - case FieldMetadataType.MULTI_SELECT: - case FieldMetadataType.POSITION: - case FieldMetadataType.RAW_JSON: - return { - value: columnName, - }; - case FieldMetadataType.LINK: - return { - label: `${columnName}Label`, - url: `${columnName}Url`, - }; - case FieldMetadataType.CURRENCY: - return { - amountMicros: `${columnName}AmountMicros`, - currencyCode: `${columnName}CurrencyCode`, - }; - case FieldMetadataType.FULL_NAME: - return { - firstName: `${columnName}FirstName`, - lastName: `${columnName}LastName`, - }; - case FieldMetadataType.ADDRESS: - return { - addressStreet1: `${columnName}AddressStreet1`, - addressStreet2: `${columnName}AddressStreet2`, - addressCity: `${columnName}AddressCity`, - addressPostcode: `${columnName}AddressPostcode`, - addressState: `${columnName}AddressState`, - addressCountry: `${columnName}AddressCountry`, - addressLat: `${columnName}AddressLat`, - addressLng: `${columnName}AddressLng`, - }; - case FieldMetadataType.RELATION: - return {}; - default: - throw new BadRequestException(`Unknown type ${type}`); - } -} 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 e637fbd08..05da882e8 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 @@ -28,7 +28,7 @@ import { RelationMetadataType, RelationOnDeleteAction, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { computeCustomName } from 'src/engine/utils/compute-custom-name.util'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete'; @@ -47,6 +47,7 @@ import { } 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.util'; import { buildWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.util'; +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { ObjectMetadataEntity } from './object-metadata.entity'; @@ -172,7 +173,7 @@ export class ObjectMetadataService extends TypeOrmQueryService { - const baseColumnName = `${camelCase(name)}Id`; - - const foreignKeyColumnName = isCustom - ? createCustomColumnName(baseColumnName) - : baseColumnName; - - return foreignKeyColumnName; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts index 0f0d349f8..5c2903f64 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts @@ -12,6 +12,7 @@ import { import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; export type BasicFieldMetadataType = | FieldMetadataType.UUID @@ -33,29 +34,32 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory, options?: WorkspaceColumnActionOptions, - ): WorkspaceMigrationColumnCreate { + ): WorkspaceMigrationColumnCreate[] { + const columnName = computeColumnName(fieldMetadata); const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue; const serializedDefaultValue = serializeDefaultValue(defaultValue); - return { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), - isNullable: fieldMetadata.isNullable, - defaultValue: serializedDefaultValue, - }; + return [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName, + columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), + isNullable: fieldMetadata.isNullable, + defaultValue: serializedDefaultValue, + }, + ]; } protected handleAlterAction( currentFieldMetadata: FieldMetadataInterface, alteredFieldMetadata: FieldMetadataInterface, options?: WorkspaceColumnActionOptions, - ): WorkspaceMigrationColumnAlter { + ): WorkspaceMigrationColumnAlter[] { + const currentColumnName = computeColumnName(currentFieldMetadata); + const alteredColumnName = computeColumnName(alteredFieldMetadata); const defaultValue = alteredFieldMetadata.defaultValue ?? options?.defaultValue; const serializedDefaultValue = serializeDefaultValue(defaultValue); - const currentColumnName = currentFieldMetadata.targetColumnMap.value; - const alteredColumnName = alteredFieldMetadata.targetColumnMap.value; if (!currentColumnName || !alteredColumnName) { this.logger.error( @@ -66,20 +70,24 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory | undefined, alteredFieldMetadata: FieldMetadataInterface, options?: WorkspaceColumnActionOptions, - ): WorkspaceMigrationColumnAction { + ): WorkspaceMigrationColumnAction[] { switch (action) { case WorkspaceMigrationColumnActionType.CREATE: return this.handleCreateAction(alteredFieldMetadata, options); @@ -52,7 +52,7 @@ export class ColumnActionAbstractFactory< protected handleCreateAction( _fieldMetadata: FieldMetadataInterface, _options?: WorkspaceColumnActionOptions, - ): WorkspaceMigrationColumnCreate { + ): WorkspaceMigrationColumnCreate[] { throw new Error('handleCreateAction method not implemented.'); } @@ -60,7 +60,7 @@ export class ColumnActionAbstractFactory< _currentFieldMetadata: FieldMetadataInterface, _alteredFieldMetadata: FieldMetadataInterface, _options?: WorkspaceColumnActionOptions, - ): WorkspaceMigrationColumnAlter { + ): WorkspaceMigrationColumnAlter[] { throw new Error('handleAlterAction method not implemented.'); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts new file mode 100644 index 000000000..f449b1400 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts @@ -0,0 +1,132 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnAlter, + WorkspaceMigrationColumnCreate, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; +import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; +import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; +import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; + +export type CompositeFieldMetadataType = + | FieldMetadataType.ADDRESS + | FieldMetadataType.CURRENCY + | FieldMetadataType.FULL_NAME + | FieldMetadataType.LINK; + +@Injectable() +export class CompositeColumnActionFactory extends ColumnActionAbstractFactory { + protected readonly logger = new Logger(CompositeColumnActionFactory.name); + + protected handleCreateAction( + fieldMetadata: FieldMetadataInterface, + ): WorkspaceMigrationColumnCreate[] { + const compositeType = compositeTypeDefintions.get(fieldMetadata.type); + + if (!compositeType) { + this.logger.error( + `Composite type not found for field metadata type: ${fieldMetadata.type}`, + ); + throw new Error( + `Composite type not found for field metadata type: ${fieldMetadata.type}`, + ); + } + + const columnActions: WorkspaceMigrationColumnCreate[] = []; + + for (const property of compositeType.properties) { + const columnName = computeCompositeColumnName(fieldMetadata, property); + const defaultValue = fieldMetadata.defaultValue?.[property.name]; + const serializedDefaultValue = serializeDefaultValue(defaultValue); + + columnActions.push({ + action: WorkspaceMigrationColumnActionType.CREATE, + columnName, + columnType: fieldMetadataTypeToColumnType(property.type), + isNullable: fieldMetadata.isNullable || !property.isRequired, + defaultValue: serializedDefaultValue, + }); + } + + return columnActions; + } + + protected handleAlterAction( + currentFieldMetadata: FieldMetadataInterface, + alteredFieldMetadata: FieldMetadataInterface, + ): WorkspaceMigrationColumnAlter[] { + const currentCompositeType = compositeTypeDefintions.get( + currentFieldMetadata.type, + ); + const alteredCompositeType = compositeTypeDefintions.get( + alteredFieldMetadata.type, + ); + + if (!currentCompositeType || !alteredCompositeType) { + this.logger.error( + `Composite type not found for field metadata type: ${currentFieldMetadata.type} or ${alteredFieldMetadata.type}`, + ); + throw new Error( + `Composite type not found for field metadata type: ${currentFieldMetadata.type} or ${alteredFieldMetadata.type}`, + ); + } + + const columnActions: WorkspaceMigrationColumnAlter[] = []; + + for (const alteredProperty of alteredCompositeType.properties) { + // TODO: Based on the name for now, we can add a more robust check in the future + const currentProperty = currentCompositeType.properties.find( + (p) => p.name === alteredProperty.name, + ); + + if (!currentProperty) { + this.logger.error( + `Current property not found for altered property: ${alteredProperty.name}`, + ); + throw new Error( + `Current property not found for altered property: ${alteredProperty.name}`, + ); + } + + const currentColumnName = computeCompositeColumnName( + currentFieldMetadata, + currentProperty, + ); + const alteredColumnName = computeCompositeColumnName( + alteredFieldMetadata, + alteredProperty, + ); + const defaultValue = + alteredFieldMetadata.defaultValue?.[alteredProperty.name]; + const serializedDefaultValue = serializeDefaultValue(defaultValue); + + columnActions.push({ + action: WorkspaceMigrationColumnActionType.ALTER, + currentColumnDefinition: { + columnName: currentColumnName, + columnType: fieldMetadataTypeToColumnType(currentProperty.type), + isNullable: + currentFieldMetadata.isNullable || !currentProperty.isRequired, + defaultValue: serializeDefaultValue( + currentFieldMetadata.defaultValue?.[currentProperty.name], + ), + }, + alteredColumnDefinition: { + columnName: alteredColumnName, + columnType: fieldMetadataTypeToColumnType(alteredProperty.type), + isNullable: + alteredFieldMetadata.isNullable || !alteredProperty.isRequired, + defaultValue: serializedDefaultValue, + }, + }); + } + + return columnActions; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory.ts index 3af7cd7f3..1007f41e6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory.ts @@ -12,6 +12,7 @@ import { import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; export type EnumFieldMetadataType = | FieldMetadataType.RATING @@ -25,29 +26,34 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory, options: WorkspaceColumnActionOptions, - ): WorkspaceMigrationColumnCreate { + ): WorkspaceMigrationColumnCreate[] { + const columnName = computeColumnName(fieldMetadata); const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue; const serializedDefaultValue = serializeDefaultValue(defaultValue); const enumOptions = fieldMetadata.options ? [...fieldMetadata.options.map((option) => option.value)] : undefined; - return { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), - enum: enumOptions, - isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, - isNullable: fieldMetadata.isNullable, - defaultValue: serializedDefaultValue, - }; + return [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName, + columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), + enum: enumOptions, + isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, + isNullable: fieldMetadata.isNullable, + defaultValue: serializedDefaultValue, + }, + ]; } protected handleAlterAction( currentFieldMetadata: FieldMetadataInterface, alteredFieldMetadata: FieldMetadataInterface, options: WorkspaceColumnActionOptions, - ): WorkspaceMigrationColumnAlter { + ): WorkspaceMigrationColumnAlter[] { + const currentColumnName = computeColumnName(currentFieldMetadata); + const alteredColumnName = computeColumnName(alteredFieldMetadata); const defaultValue = alteredFieldMetadata.defaultValue ?? options?.defaultValue; const serializedDefaultValue = serializeDefaultValue(defaultValue); @@ -71,8 +77,6 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory option.value)] - : undefined, - isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT, - isNullable: currentFieldMetadata.isNullable, - defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue), + return [ + { + action: WorkspaceMigrationColumnActionType.ALTER, + currentColumnDefinition: { + columnName: currentColumnName, + columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type), + enum: currentFieldMetadata.options + ? [...currentFieldMetadata.options.map((option) => option.value)] + : undefined, + isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT, + isNullable: currentFieldMetadata.isNullable, + defaultValue: serializeDefaultValue( + currentFieldMetadata.defaultValue, + ), + }, + alteredColumnDefinition: { + columnName: alteredColumnName, + columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type), + enum: enumOptions, + isArray: alteredFieldMetadata.type === FieldMetadataType.MULTI_SELECT, + isNullable: alteredFieldMetadata.isNullable, + defaultValue: serializedDefaultValue, + }, }, - alteredColumnDefinition: { - columnName: alteredColumnName, - columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type), - enum: enumOptions, - isArray: alteredFieldMetadata.type === FieldMetadataType.MULTI_SELECT, - isNullable: alteredFieldMetadata.isNullable, - defaultValue: serializedDefaultValue, - }, - }; + ]; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts index 919ead74c..5bda4bac1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts @@ -1,7 +1,9 @@ import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; +import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; export const workspaceColumnActionFactories = [ BasicColumnActionFactory, EnumColumnActionFactory, + CompositeColumnActionFactory, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface.ts index 05ef6f313..b1114c181 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface.ts @@ -17,5 +17,5 @@ export interface WorkspaceColumnActionFactory< currentFieldMetadata: FieldMetadataInterface | undefined, alteredFieldMetadata: FieldMetadataInterface, options?: WorkspaceColumnActionOptions, - ): WorkspaceMigrationColumnAction; + ): WorkspaceMigrationColumnAction[]; } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts index bd884e821..ac178277a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts @@ -5,14 +5,13 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; -import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; import { WorkspaceMigrationColumnAction, WorkspaceMigrationColumnActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { compositeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; +import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; +import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; @Injectable() export class WorkspaceMigrationFactory { @@ -28,6 +27,7 @@ export class WorkspaceMigrationFactory { constructor( private readonly basicColumnActionFactory: BasicColumnActionFactory, private readonly enumColumnActionFactory: EnumColumnActionFactory, + private readonly compositeColumnActionFactory: CompositeColumnActionFactory, ) { this.factoriesMap = new Map< FieldMetadataType, @@ -80,6 +80,19 @@ export class WorkspaceMigrationFactory { FieldMetadataType.MULTI_SELECT, { factory: this.enumColumnActionFactory }, ], + [FieldMetadataType.LINK, { factory: this.compositeColumnActionFactory }], + [ + FieldMetadataType.CURRENCY, + { factory: this.compositeColumnActionFactory }, + ], + [ + FieldMetadataType.ADDRESS, + { factory: this.compositeColumnActionFactory }, + ], + [ + FieldMetadataType.FULL_NAME, + { factory: this.compositeColumnActionFactory }, + ], ]); } @@ -119,41 +132,13 @@ export class WorkspaceMigrationFactory { throw new Error(`No field metadata provided for action ${action}`); } - // If it's a composite field type, we need to create a column action for each of the fields - if (isCompositeFieldMetadataType(alteredFieldMetadata.type)) { - const fieldMetadataSplitterFunction = compositeDefinitions.get( - alteredFieldMetadata.type, - ); - - if (!fieldMetadataSplitterFunction) { - this.logger.error( - `No composite definition found for type ${alteredFieldMetadata.type}`, - { - alteredFieldMetadata, - }, - ); - - throw new Error( - `No composite definition found for type ${alteredFieldMetadata.type}`, - ); - } - - const fieldMetadataCollection = - fieldMetadataSplitterFunction(alteredFieldMetadata); - - return fieldMetadataCollection.map((fieldMetadata) => - this.createColumnAction(action, fieldMetadata, fieldMetadata), - ); - } - - // Otherwise, we create a single column action - const columnAction = this.createColumnAction( + const columnActions = this.createColumnAction( action, currentFieldMetadata, alteredFieldMetadata, ); - return [columnAction]; + return columnActions; } private createColumnAction( @@ -162,7 +147,7 @@ export class WorkspaceMigrationFactory { | WorkspaceMigrationColumnActionType.ALTER, currentFieldMetadata: FieldMetadataInterface | undefined, alteredFieldMetadata: FieldMetadataInterface, - ): WorkspaceMigrationColumnAction { + ): WorkspaceMigrationColumnAction[] { const { factory, options } = this.factoriesMap.get(alteredFieldMetadata.type) ?? {}; diff --git a/packages/twenty-server/src/engine/utils/__tests__/deduce-relation-direction.spec.ts b/packages/twenty-server/src/engine/utils/__tests__/deduce-relation-direction.spec.ts index 19630b0ec..71114d678 100644 --- a/packages/twenty-server/src/engine/utils/__tests__/deduce-relation-direction.spec.ts +++ b/packages/twenty-server/src/engine/utils/__tests__/deduce-relation-direction.spec.ts @@ -17,9 +17,6 @@ describe('deduceRelationDirection', () => { name: 'field_name', label: 'Field Name', description: 'Field Description', - targetColumnMap: { - default: 'default_column', - }, }; const relationMetadata = { @@ -48,9 +45,6 @@ describe('deduceRelationDirection', () => { name: 'field_name', label: 'Field Name', description: 'Field Description', - targetColumnMap: { - default: 'default_column', - }, }; const relationMetadata = { @@ -78,9 +72,6 @@ describe('deduceRelationDirection', () => { name: 'field_name', label: 'Field Name', description: 'Field Description', - targetColumnMap: { - default: 'default_column', - }, }; const relationMetadata = { diff --git a/packages/twenty-server/src/engine/utils/compute-object-target-table.util.ts b/packages/twenty-server/src/engine/utils/compute-object-target-table.util.ts index 05e22fb17..03430009d 100644 --- a/packages/twenty-server/src/engine/utils/compute-object-target-table.util.ts +++ b/packages/twenty-server/src/engine/utils/compute-object-target-table.util.ts @@ -1,12 +1,9 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { computeCustomName } from './compute-custom-name.util'; +import { computeTableName } from './compute-table-name.util'; export const computeObjectTargetTable = ( objectMetadata: ObjectMetadataInterface, ) => { - return computeCustomName( - objectMetadata.nameSingular, - objectMetadata.isCustom, - ); + return computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom); }; diff --git a/packages/twenty-server/src/engine/utils/compute-custom-name.util.ts b/packages/twenty-server/src/engine/utils/compute-table-name.util.ts similarity index 57% rename from packages/twenty-server/src/engine/utils/compute-custom-name.util.ts rename to packages/twenty-server/src/engine/utils/compute-table-name.util.ts index 3e6c10d96..ca031ed31 100644 --- a/packages/twenty-server/src/engine/utils/compute-custom-name.util.ts +++ b/packages/twenty-server/src/engine/utils/compute-table-name.util.ts @@ -1,5 +1,5 @@ export const customNamePrefix = '_'; -export const computeCustomName = (name: string, isCustom: boolean) => { +export const computeTableName = (name: string, isCustom: boolean) => { return isCustom ? `${customNamePrefix}${name}` : name; }; diff --git a/packages/twenty-server/src/engine/utils/create-custom-column-name.util.ts b/packages/twenty-server/src/engine/utils/create-custom-column-name.util.ts deleted file mode 100644 index 2eb4674e4..000000000 --- a/packages/twenty-server/src/engine/utils/create-custom-column-name.util.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const createCustomColumnName = (name: string) => { - return `_${name}`; -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts index ca9d93aac..ad32c60fc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts @@ -1,11 +1,12 @@ +import { WorkspaceMissingColumnFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer'; + import { WorkspaceNullableFixer } from './workspace-nullable.fixer'; import { WorkspaceDefaultValueFixer } from './workspace-default-value.fixer'; import { WorkspaceTypeFixer } from './workspace-type.fixer'; -import { WorkspaceTargetColumnMapFixer } from './workspace-target-column-map.fixer'; export const workspaceFixers = [ WorkspaceNullableFixer, WorkspaceDefaultValueFixer, WorkspaceTypeFixer, - WorkspaceTargetColumnMapFixer, + WorkspaceMissingColumnFixer, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts new file mode 100644 index 000000000..072475f32 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { + WorkspaceHealthColumnIssue, + WorkspaceHealthIssueType, +} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; +import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { + FieldMetadataUpdate, + WorkspaceMigrationFieldFactory, +} from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; + +import { AbstractWorkspaceFixer } from './abstract-workspace.fixer'; + +@Injectable() +export class WorkspaceMissingColumnFixer extends AbstractWorkspaceFixer { + constructor( + private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, + ) { + super(WorkspaceHealthIssueType.MISSING_COLUMN); + } + + async createWorkspaceMigrations( + manager: EntityManager, + objectMetadataCollection: ObjectMetadataEntity[], + issues: WorkspaceHealthColumnIssue[], + ): Promise[]> { + if (issues.length <= 0) { + return []; + } + + return this.fixMissingColumnIssues(objectMetadataCollection, issues); + } + + private async fixMissingColumnIssues( + objectMetadataCollection: ObjectMetadataEntity[], + issues: WorkspaceHealthColumnIssue[], + ): Promise[]> { + const fieldMetadataUpdateCollection: FieldMetadataUpdate[] = []; + + for (const issue of issues) { + if (!issue.columnStructures) { + continue; + } + + /** + * Check if the column is prefixed with an underscore as it was the old convention + */ + const oldColumnName = `_${issue.fieldMetadata.name}`; + const oldColumnStructure = issue.columnStructures.find( + (columnStructure) => columnStructure.columnName === oldColumnName, + ); + + if (!oldColumnStructure) { + continue; + } + + fieldMetadataUpdateCollection.push({ + current: { + ...issue.fieldMetadata, + name: oldColumnName, + }, + altered: issue.fieldMetadata, + }); + } + + if (fieldMetadataUpdateCollection.length <= 0) { + return []; + } + + return this.workspaceMigrationFieldFactory.create( + objectMetadataCollection, + fieldMetadataUpdateCollection, + WorkspaceMigrationBuilderAction.UPDATE, + ); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-target-column-map.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-target-column-map.fixer.ts deleted file mode 100644 index 5b759c2bd..000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-target-column-map.fixer.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { EntityManager } from 'typeorm'; -import isEqual from 'lodash.isequal'; - -import { - WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; - -import { - AbstractWorkspaceFixer, - CompareEntity, -} from './abstract-workspace.fixer'; - -@Injectable() -export class WorkspaceTargetColumnMapFixer extends AbstractWorkspaceFixer< - WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID, - FieldMetadataEntity -> { - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - private readonly databaseStructureService: DatabaseStructureService, - ) { - super(WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID); - } - - async createWorkspaceMigrations( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - return this.fixStructureTargetColumnMapIssues( - manager, - objectMetadataCollection, - issues, - ); - } - - async createMetadataUpdates( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - return this.fixMetadataTargetColumnMapIssues(manager, issues); - } - - private async fixStructureTargetColumnMapIssues( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const workspaceMigrationCollection: Partial[] = - []; - const dataSourceRepository = manager.getRepository(DataSourceEntity); - - for (const issue of issues) { - const objectMetadata = objectMetadataCollection.find( - (metadata) => metadata.id === issue.fieldMetadata.objectMetadataId, - ); - const targetColumnMap = generateTargetColumnMap( - issue.fieldMetadata.type, - issue.fieldMetadata.isCustom, - issue.fieldMetadata.name, - ); - - // Skip composite fields, too complicated to fix for now - if (isCompositeFieldMetadataType(issue.fieldMetadata.type)) { - continue; - } - - if (!objectMetadata) { - throw new Error( - `Object metadata with id ${issue.fieldMetadata.objectMetadataId} not found`, - ); - } - - if (!isEqual(issue.fieldMetadata.targetColumnMap, targetColumnMap)) { - // Retrieve the data source to get the schema name - const dataSource = await dataSourceRepository.findOne({ - where: { - id: objectMetadata.dataSourceId, - }, - }); - - if (!dataSource) { - throw new Error( - `Data source with id ${objectMetadata.dataSourceId} not found`, - ); - } - - const columnName = issue.fieldMetadata.targetColumnMap?.value; - const columnExist = - await this.databaseStructureService.workspaceColumnExist( - dataSource.schema, - computeObjectTargetTable(objectMetadata), - columnName, - ); - - if (!columnExist) { - continue; - } - - const workspaceMigration = - await this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - [ - { - current: issue.fieldMetadata, - altered: { - ...issue.fieldMetadata, - targetColumnMap, - }, - }, - ], - WorkspaceMigrationBuilderAction.UPDATE, - ); - - workspaceMigrationCollection.push(workspaceMigration[0]); - } - } - - return workspaceMigrationCollection; - } - - private async fixMetadataTargetColumnMapIssues( - manager: EntityManager, - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataRepository = manager.getRepository(FieldMetadataEntity); - const updatedEntities: CompareEntity[] = []; - - for (const issue of issues) { - await fieldMetadataRepository.update(issue.fieldMetadata.id, { - targetColumnMap: generateTargetColumnMap( - issue.fieldMetadata.type, - issue.fieldMetadata.isCustom, - issue.fieldMetadata.name, - ), - }); - const alteredEntity = await fieldMetadataRepository.findOne({ - where: { - id: issue.fieldMetadata.id, - }, - }); - - updatedEntities.push({ - current: issue.fieldMetadata, - altered: alteredEntity as FieldMetadataEntity | null, - }); - } - - return updatedEntities; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts index 58bf30646..907c02f2e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts @@ -2,5 +2,5 @@ export enum WorkspaceHealthFixKind { Nullable = 'nullable', Type = 'type', DefaultValue = 'default-value', - TargetColumnMap = 'target-column-map', + MissingColumn = 'missing-column', } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts index 6c7afab3f..c399ec30d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts @@ -15,8 +15,7 @@ export enum WorkspaceHealthIssueType { MISSING_FOREIGN_KEY = 'MISSING_FOREIGN_KEY', MISSING_COMPOSITE_TYPE = 'MISSING_COMPOSITE_TYPE', COLUMN_NAME_SHOULD_NOT_BE_PREFIXED = 'COLUMN_NAME_SHOULD_NOT_BE_PREFIXED', - COLUMN_TARGET_COLUMN_MAP_NOT_VALID = 'COLUMN_TARGET_COLUMN_MAP_NOT_VALID', - COLUMN_NAME_SHOULD_BE_CUSTOM = 'COLUMN_NAME_SHOULD_BE_CUSTOM', + COLUMN_NAME_SHOULD_NOT_BE_CUSTOM = 'COLUMN_NAME_SHOULD_NOT_BE_CUSTOM', COLUMN_OBJECT_REFERENCE_INVALID = 'COLUMN_OBJECT_REFERENCE_INVALID', COLUMN_NAME_NOT_VALID = 'COLUMN_NAME_NOT_VALID', COLUMN_TYPE_NOT_VALID = 'COLUMN_TYPE_NOT_VALID', @@ -58,8 +57,7 @@ export type WorkspaceColumnIssueTypes = | WorkspaceHealthIssueType.MISSING_FOREIGN_KEY | WorkspaceHealthIssueType.MISSING_COMPOSITE_TYPE | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED - | WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID - | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM + | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM | WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID | WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID | WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID @@ -75,6 +73,7 @@ export interface WorkspaceHealthColumnIssue< type: T; fieldMetadata: FieldMetadataEntity; columnStructure?: WorkspaceTableStructure; + columnStructures?: WorkspaceTableStructure[]; message: string; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts index f2c11cc58..a3752aba7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts @@ -23,6 +23,7 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field- import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util'; import { FieldMetadataDefaultValueFunctionNames } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; @Injectable() export class DatabaseStructureService { @@ -156,22 +157,40 @@ export class DatabaseStructureService { return results.length >= 1; } - getPostgresDataType(fieldMetadata: FieldMetadataEntity): string { - const typeORMType = fieldMetadataTypeToColumnType(fieldMetadata.type); + getPostgresDataTypes(fieldMetadata: FieldMetadataEntity): string[] { const mainDataSource = this.typeORMService.getMainDataSource(); - // Compute enum name to compare data type properly - if (typeORMType === 'enum') { - const objectName = fieldMetadata.object?.nameSingular; - const prefix = fieldMetadata.isCustom ? '_' : ''; - const fieldName = fieldMetadata.name; + const normalizer = (type: FieldMetadataType, columnName: string) => { + const typeORMType = fieldMetadataTypeToColumnType(type); - return `${objectName}_${prefix}${fieldName}_enum`; + // Compute enum name to compare data type properly + if (typeORMType === 'enum') { + const objectName = fieldMetadata.object?.nameSingular; + const prefix = fieldMetadata.isCustom ? '_' : ''; + + return `${objectName}_${prefix}${columnName}_enum`; + } + + return mainDataSource.driver.normalizeType({ + type: typeORMType, + }); + }; + + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + const compositeType = compositeTypeDefintions.get(fieldMetadata.type); + + if (!compositeType) { + throw new Error( + `Composite type definition not found for ${fieldMetadata.type}`, + ); + } + + return compositeType.properties.map((compositeProperty) => + normalizer(compositeProperty.type, compositeProperty.name), + ); } - return mainDataSource.driver.normalizeType({ - type: typeORMType, - }); + return [normalizer(fieldMetadata.type, fieldMetadata.name)]; } getFieldMetadataTypeFromPostgresDataType( @@ -207,57 +226,84 @@ export class DatabaseStructureService { return null; } - getPostgresDefault( + getPostgresDefaults( fieldMetadataType: FieldMetadataType, - defaultValue: + initialDefaultValue: | FieldMetadataDefaultValue // Old format for default values // TODO: Should be removed once all default values are migrated | { type: FieldMetadataDefaultValueFunctionNames } | null, - ): string | null | undefined { - const typeORMType = fieldMetadataTypeToColumnType( - fieldMetadataType, - ) as ColumnType; - const mainDataSource = this.typeORMService.getMainDataSource(); + ): (string | null | undefined)[] { + const normalizer = ( + type: FieldMetadataType, + defaultValue: + | FieldMetadataDefaultValue + | { type: FieldMetadataDefaultValueFunctionNames } + | null, + ) => { + const typeORMType = fieldMetadataTypeToColumnType(type) as ColumnType; + const mainDataSource = this.typeORMService.getMainDataSource(); - let value: any = - // Old formart default values - defaultValue && - typeof defaultValue === 'object' && - 'value' in defaultValue - ? defaultValue.value - : defaultValue; + let value: any = + // Old formart default values + defaultValue && + typeof defaultValue === 'object' && + 'value' in defaultValue + ? defaultValue.value + : defaultValue; - // Old format for default values - // TODO: Should be removed once all default values are migrated - if ( - defaultValue && - typeof defaultValue === 'object' && - 'type' in defaultValue - ) { - return this.computeFunctionDefaultValue(defaultValue.type); + // Old format for default values + // TODO: Should be removed once all default values are migrated + if ( + defaultValue && + typeof defaultValue === 'object' && + 'type' in defaultValue + ) { + return this.computeFunctionDefaultValue(defaultValue.type); + } + + if (isFunctionDefaultValue(value)) { + return this.computeFunctionDefaultValue(value); + } + + if (typeof value === 'number') { + return value.toString(); + } + + // Remove leading and trailing single quotes for string default values as it's already handled by TypeORM + if (typeof value === 'string' && value.match(/^'.*'$/)) { + value = value.replace(/^'/, '').replace(/'$/, ''); + } + + return mainDataSource.driver.normalizeDefault({ + type: typeORMType, + default: value, + isArray: false, + // Workaround to use normalizeDefault without a complete ColumnMetadata object + } as ColumnMetadata); + }; + + if (isCompositeFieldMetadataType(fieldMetadataType)) { + const compositeType = compositeTypeDefintions.get(fieldMetadataType); + + if (!compositeType) { + throw new Error( + `Composite type definition not found for ${fieldMetadataType}`, + ); + } + + return compositeType.properties.map((compositeProperty) => + normalizer( + compositeProperty.type, + typeof initialDefaultValue === 'object' + ? initialDefaultValue?.[compositeProperty.name] + : null, + ), + ); } - if (isFunctionDefaultValue(value)) { - return this.computeFunctionDefaultValue(value); - } - - if (typeof value === 'number') { - return value.toString(); - } - - // Remove leading and trailing single quotes for string default values as it's already handled by TypeORM - if (typeof value === 'string' && value.match(/^'.*'$/)) { - value = value.replace(/^'/, '').replace(/'$/, ''); - } - - return mainDataSource.driver.normalizeDefault({ - type: typeORMType, - default: value, - isArray: false, - // Workaround to use normalizeDefault without a complete ColumnMetadata object - } as ColumnMetadata); + return [normalizer(fieldMetadataType, initialDefaultValue)]; } private computeFunctionDefaultValue( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts index e6e1e03c0..ea33fe2b1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@nestjs/common'; -import isEqual from 'lodash.isequal'; - import { WorkspaceHealthIssue, WorkspaceHealthIssueType, @@ -17,7 +15,6 @@ import { import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; import { validName } from 'src/engine/workspace-manager/workspace-health/utils/valid-name.util'; -import { compositeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util'; import { EnumFieldMetadataUnionType, @@ -25,10 +22,13 @@ import { } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { validateOptionsForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; -import { computeCompositeFieldMetadata } from 'src/engine/workspace-manager/workspace-health/utils/compute-composite-field-metadata.util'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; -import { customNamePrefix } from 'src/engine/utils/compute-custom-name.util'; +import { customNamePrefix } from 'src/engine/utils/compute-table-name.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { + computeColumnName, + computeCompositeColumnName, +} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; @Injectable() export class FieldMetadataHealthService { @@ -50,58 +50,14 @@ export class FieldMetadataHealthService { continue; } - if (isCompositeFieldMetadataType(fieldMetadata.type)) { - const compositeFieldMetadataCollection = - compositeDefinitions.get(fieldMetadata.type)?.(fieldMetadata) ?? []; + const fieldIssues = await this.healthCheckField( + tableName, + workspaceTableColumns, + fieldMetadata, + options, + ); - if (options.mode === 'metadata' || options.mode === 'all') { - const targetColumnMapIssue = this.targetColumnMapCheck(fieldMetadata); - - if (targetColumnMapIssue) { - issues.push(targetColumnMapIssue); - } - - const defaultValueIssues = - this.defaultValueHealthCheck(fieldMetadata); - - issues.push(...defaultValueIssues); - } - - // Only check structure on nested composite fields - if (options.mode === 'structure' || options.mode === 'all') { - for (const compositeFieldMetadata of compositeFieldMetadataCollection) { - const compositeFieldStructureIssues = this.structureFieldCheck( - tableName, - workspaceTableColumns, - computeCompositeFieldMetadata( - compositeFieldMetadata, - fieldMetadata, - ), - ); - - issues.push(...compositeFieldStructureIssues); - } - } - - // Only check metadata on the parent composite field - if (options.mode === 'metadata' || options.mode === 'all') { - const compositeFieldMetadataIssues = this.metadataFieldCheck( - tableName, - fieldMetadata, - ); - - issues.push(...compositeFieldMetadataIssues); - } - } else { - const fieldIssues = await this.healthCheckField( - tableName, - workspaceTableColumns, - fieldMetadata, - options, - ); - - issues.push(...fieldIssues); - } + issues.push(...fieldIssues); } return issues; @@ -139,78 +95,105 @@ export class FieldMetadataHealthService { workspaceTableColumns: WorkspaceTableStructure[], fieldMetadata: FieldMetadataEntity, ): WorkspaceHealthIssue[] { + const dataTypes = + this.databaseStructureService.getPostgresDataTypes(fieldMetadata); const issues: WorkspaceHealthIssue[] = []; - const columnName = fieldMetadata.targetColumnMap.value; + let columnNames: string[] = []; - const dataType = - this.databaseStructureService.getPostgresDataType(fieldMetadata); + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + const compositeType = compositeTypeDefintions.get(fieldMetadata.type); - const defaultValue = this.databaseStructureService.getPostgresDefault( + if (!compositeType) { + throw new Error(`Composite type ${fieldMetadata.type} is not defined`); + } + + columnNames = compositeType.properties.map((compositeProperty) => + computeCompositeColumnName(fieldMetadata, compositeProperty), + ); + } else { + columnNames = [computeColumnName(fieldMetadata)]; + } + + const defaultValues = this.databaseStructureService.getPostgresDefaults( fieldMetadata.type, fieldMetadata.defaultValue, ); // Check if column exist in database - const columnStructure = workspaceTableColumns.find( - (tableDefinition) => tableDefinition.columnName === columnName, + const columnStructureMap = workspaceTableColumns.reduce( + (acc, workspaceTableColumn) => { + const columnName = workspaceTableColumn.columnName; + + if (columnNames.includes(columnName)) { + acc[columnName] = workspaceTableColumn; + } + + return acc; + }, + {} as { [key: string]: WorkspaceTableStructure }, ); - if (!columnStructure) { - issues.push({ - type: WorkspaceHealthIssueType.MISSING_COLUMN, - fieldMetadata, - columnStructure, - message: `Column ${columnName} not found in table ${tableName}`, - }); + for (const [index, columnName] of columnNames.entries()) { + const columnStructure = columnStructureMap[columnName]; - return issues; - } - - const columnDefaultValue = columnStructure.columnDefault?.split('::')?.[0]; - - // Check if column data type is the same - if (columnStructure.dataType !== dataType) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataType}"`, - }); - } - - if (columnStructure.isNullable !== fieldMetadata.isNullable) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} is expected to be ${ - fieldMetadata.isNullable ? 'nullable' : 'not nullable' - } but is ${columnStructure.isNullable ? 'nullable' : 'not nullable'}`, - }); - } - - if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) { - const enumValues = fieldMetadata.options?.map((option) => - serializeDefaultValue(`'${option.value}'`), - ); - - if (!enumValues.includes(columnDefaultValue)) { + if (!columnStructure) { issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, + type: WorkspaceHealthIssueType.MISSING_COLUMN, + fieldMetadata, + columnStructures: workspaceTableColumns, + message: `Column ${columnName} not found in table ${tableName}`, + }); + + continue; + } + + const columnDefaultValue = + columnStructure.columnDefault?.split('::')?.[0]; + + // Check if column data type is the same + if (!dataTypes[index] || columnStructure.dataType !== dataTypes[index]) { + issues.push({ + type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT, fieldMetadata, columnStructure, - message: `Column ${columnName} default value is not in the enum values "${columnDefaultValue}" NOT IN "${enumValues}"`, + message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataTypes[index]}"`, }); } - } - if (columnDefaultValue !== defaultValue) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValue}"`, - }); + if (columnStructure.isNullable !== fieldMetadata.isNullable) { + issues.push({ + type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT, + fieldMetadata, + columnStructure, + message: `Column ${columnName} is expected to be ${ + fieldMetadata.isNullable ? 'nullable' : 'not nullable' + } but is ${columnStructure.isNullable ? 'nullable' : 'not nullable'}`, + }); + } + + if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) { + const enumValues = fieldMetadata.options?.map((option) => + serializeDefaultValue(`'${option.value}'`), + ); + + if (!enumValues.includes(columnDefaultValue)) { + issues.push({ + type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, + fieldMetadata, + columnStructure, + message: `Column ${columnName} default value is not in the enum values "${columnDefaultValue}" NOT IN "${enumValues}"`, + }); + } + } + + if (columnDefaultValue !== defaultValues[index]) { + issues.push({ + type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT, + fieldMetadata, + columnStructure, + message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValues[index]}"`, + }); + } } return issues; @@ -221,14 +204,9 @@ export class FieldMetadataHealthService { fieldMetadata: FieldMetadataEntity, ): WorkspaceHealthIssue[] { const issues: WorkspaceHealthIssue[] = []; - const columnName = fieldMetadata.targetColumnMap.value; - const targetColumnMapIssue = this.targetColumnMapCheck(fieldMetadata); + const columnName = fieldMetadata.name; const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata); - if (targetColumnMapIssue) { - issues.push(targetColumnMapIssue); - } - if (fieldMetadata.name.startsWith(customNamePrefix)) { issues.push({ type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED, @@ -237,11 +215,11 @@ export class FieldMetadataHealthService { }); } - if (fieldMetadata.isCustom && !columnName?.startsWith(customNamePrefix)) { + if (fieldMetadata.isCustom && columnName?.startsWith(customNamePrefix)) { issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM, + type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM, fieldMetadata, - message: `Column ${columnName} is marked as custom in table ${tableName} but doesn't start with "_"`, + message: `Column ${columnName} is marked as custom in table ${tableName} but and start with "_", this behavior has been removed. Please remove the prefix.`, }); } @@ -280,7 +258,7 @@ export class FieldMetadataHealthService { issues.push({ type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, fieldMetadata, - message: `Column options of ${fieldMetadata.targetColumnMap?.value} is not valid`, + message: `Column options of ${fieldMetadata.name} is not valid`, }); } @@ -289,33 +267,6 @@ export class FieldMetadataHealthService { return issues; } - private targetColumnMapCheck( - fieldMetadata: FieldMetadataEntity, - ): WorkspaceHealthIssue | null { - const targetColumnMap = generateTargetColumnMap( - fieldMetadata.type, - fieldMetadata.isCustom, - fieldMetadata.name, - ); - - if ( - !fieldMetadata.targetColumnMap || - !isEqual(targetColumnMap, fieldMetadata.targetColumnMap) - ) { - return { - type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID, - fieldMetadata, - message: `Column targetColumnMap "${JSON.stringify( - fieldMetadata.targetColumnMap, - )}" is not the same as the generated one "${JSON.stringify( - targetColumnMap, - )}"`, - }; - } - - return null; - } - private defaultValueHealthCheck( fieldMetadata: FieldMetadataEntity, ): WorkspaceHealthIssue[] { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/relation-metadata.health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/relation-metadata.health.service.ts index 61a45e88b..8a4f9d69d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/relation-metadata.health.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/relation-metadata.health.service.ts @@ -20,10 +20,10 @@ import { deduceRelationDirection, } from 'src/engine/utils/deduce-relation-direction.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { createRelationForeignKeyColumnName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-column-name.util'; import { createRelationForeignKeyFieldMetadataName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-field-metadata-name.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util'; +import { camelCase } from 'src/utils/camel-case'; @Injectable() export class RelationMetadataHealthService { @@ -145,11 +145,7 @@ export class RelationMetadataHealthService { return []; } - const isCustom = toFieldMetadata.isCustom ?? false; - const foreignKeyColumnName = createRelationForeignKeyColumnName( - toFieldMetadata.name, - isCustom, - ); + const foreignKeyColumnName = `${camelCase(toFieldMetadata.name)}Id`; const relationColumn = workspaceTableColumns.find( (column) => column.columnName === foreignKeyColumnName, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts index dae84af7a..fa687cfa6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts @@ -10,8 +10,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { WorkspaceNullableFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer'; import { WorkspaceDefaultValueFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer'; import { WorkspaceTypeFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer'; -import { WorkspaceTargetColumnMapFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-target-column-map.fixer'; import { CompareEntity } from 'src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer'; +import { WorkspaceMissingColumnFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer'; @Injectable() export class WorkspaceFixService { @@ -19,7 +19,7 @@ export class WorkspaceFixService { private readonly workspaceNullableFixer: WorkspaceNullableFixer, private readonly workspaceDefaultValueFixer: WorkspaceDefaultValueFixer, private readonly workspaceTypeFixer: WorkspaceTypeFixer, - private readonly workspaceTargetColumnMapFixer: WorkspaceTargetColumnMapFixer, + private readonly workspaceMissingColumnFixer: WorkspaceMissingColumnFixer, ) {} async createWorkspaceMigrations( @@ -57,11 +57,11 @@ export class WorkspaceFixService { filteredIssues, ); } - case WorkspaceHealthFixKind.TargetColumnMap: { + case WorkspaceHealthFixKind.MissingColumn: { const filteredIssues = - this.workspaceTargetColumnMapFixer.filterIssues(issues); + this.workspaceMissingColumnFixer.filterIssues(issues); - return this.workspaceTargetColumnMapFixer.createWorkspaceMigrations( + return this.workspaceMissingColumnFixer.createWorkspaceMigrations( manager, objectMetadataCollection, filteredIssues, @@ -77,19 +77,10 @@ export class WorkspaceFixService { manager: EntityManager, objectMetadataCollection: ObjectMetadataEntity[], type: WorkspaceHealthFixKind, + // eslint-disable-next-line @typescript-eslint/no-unused-vars issues: WorkspaceHealthIssue[], ): Promise[]> { switch (type) { - case WorkspaceHealthFixKind.TargetColumnMap: { - const filteredIssues = - this.workspaceTargetColumnMapFixer.filterIssues(issues); - - return this.workspaceTargetColumnMapFixer.createMetadataUpdates( - manager, - objectMetadataCollection, - filteredIssues, - ); - } case WorkspaceHealthFixKind.DefaultValue: { const filteredIssues = this.workspaceDefaultValueFixer.filterIssues(issues); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/is-workspace-health-issue-type.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/is-workspace-health-issue-type.util.ts index e4ede5903..1a43d475a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/is-workspace-health-issue-type.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/is-workspace-health-issue-type.util.ts @@ -17,9 +17,3 @@ export const isWorkspaceHealthDefaultValueIssue = ( ): type is WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT => { return type === WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT; }; - -export const isWorkspaceHealthTargetColumnMapIssue = ( - type: WorkspaceHealthIssueType, -): type is WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID => { - return type === WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID; -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/__tests__/workspace-field.comparator.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/__tests__/workspace-field.comparator.spec.ts index 77a2a8bf1..e4deb708c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/__tests__/workspace-field.comparator.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/__tests__/workspace-field.comparator.spec.ts @@ -16,7 +16,6 @@ describe('WorkspaceFieldComparator', () => { type: 'TEXT', name: 'DefaultFieldName', label: 'Default Field Label', - targetColumnMap: 'default_column', defaultValue: null, description: 'Default description', isCustom: false, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts index 6de6fd233..fb64f47fa 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts @@ -25,7 +25,7 @@ const commonFieldPropertiesToIgnore = [ 'options', ]; -const fieldPropertiesToStringify = ['targetColumnMap', 'defaultValue'] as const; +const fieldPropertiesToStringify = ['defaultValue'] as const; @Injectable() export class WorkspaceFieldComparator { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator.ts index 641c9a661..88e51b7a0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator.ts @@ -6,7 +6,6 @@ import { GateDecoratorParams } from 'src/engine/workspace-manager/workspace-sync import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value'; import { TypedReflect } from 'src/utils/typed-reflect'; import { createDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; @@ -70,7 +69,6 @@ function generateFieldMetadata( isSystem: boolean, gate: GateDecoratorParams | undefined = undefined, ): ReflectFieldMetadata[string] { - const targetColumnMap = generateTargetColumnMap(params.type, false, fieldKey); const defaultValue = (params.defaultValue ?? generateDefaultValue( params.type, @@ -79,7 +77,6 @@ function generateFieldMetadata( return { name: fieldKey, ...params, - targetColumnMap, isNullable: params.type === FieldMetadataType.RELATION ? true : isNullable, isSystem, isCustom: false, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/reflect-field-metadata.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/reflect-field-metadata.interface.ts index 74cde9084..e126c6581 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/reflect-field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/reflect-field-metadata.interface.ts @@ -1,7 +1,6 @@ import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { GateDecoratorParams } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/gate-decorator.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; -import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -26,7 +25,6 @@ export interface ReflectFieldMetadata { > & { name: string; type: FieldMetadataType; - targetColumnMap: FieldMetadataTargetColumnMap<'default'>; isNullable: boolean; isSystem: boolean; isCustom: boolean; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-object.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-object.util.ts index 44baf83f4..69dcbfbb3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-object.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-object.util.ts @@ -5,7 +5,6 @@ import { import { ComputedPartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createForeignKeyDeterministicUuid, @@ -42,7 +41,6 @@ export const computeStandardObject = ( ...rest, standardId: relationStandardId, defaultValue: null, - targetColumnMap: {}, }); // Foreign key @@ -55,11 +53,6 @@ export const computeStandardObject = ( description: `${data.description} id foreign key`, defaultValue: null, icon: undefined, - targetColumnMap: generateTargetColumnMap( - FieldMetadataType.UUID, - rest.isCustom, - joinColumn, - ), isSystem: true, }); }