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, }); }