feat: drop target column map (#4670)

This PR is dropping the column `targetColumnMap` of fieldMetadata
entities.
The goal of this column was to properly map field to their respecting
column in the table.
We decide to drop it and instead compute the column name on the fly when
we need it, as it's more easier to support.
Some parts of the code has been refactored to try making implementation
of composite type more easier to understand and maintain.

Fix #3760

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2024-04-08 16:00:28 +02:00
committed by GitHub
parent 84f8c14e52
commit 5019b5febc
72 changed files with 1432 additions and 1853 deletions

1
.vscode/launch.json vendored
View File

@@ -7,6 +7,7 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"runtimeExecutable": "yarn", "runtimeExecutable": "yarn",
"runtimeVersion": "18",
"runtimeArgs": [ "runtimeArgs": [
"nx", "nx",
"run", "run",

View File

@@ -7,6 +7,7 @@ import {
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { GraphQLModule } from '@nestjs/graphql'; import { GraphQLModule } from '@nestjs/graphql';
import { DevtoolsModule } from '@nestjs/devtools-integration';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { join } from 'path'; 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 { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.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 { 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 { UserWorkspaceMiddleware } from 'src/engine/middlewares/user-workspace.middleware';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; 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({ @Module({
imports: [ imports: [
// Nest.js devtools, use devtools.nestjs.com to debug // Nest.js devtools, use devtools.nestjs.com to debug
// DevtoolsModule.registerAsync({ DevtoolsModule.registerAsync({
// useFactory: (environmentService: EnvironmentService) => ({ useFactory: (environmentService: EnvironmentService) => ({
// http: environmentService.get('DEBUG_MODE'), http: environmentService.get('DEBUG_MODE'),
// port: environmentService.get('DEBUG_PORT'), port: environmentService.get('DEBUG_PORT'),
// }), }),
// inject: [EnvironmentService], inject: [EnvironmentService],
// }), }),
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
}), }),

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class DropTargetColumnMap1711459912762 implements MigrationInterface {
name = 'DropTargetColumnMap1711459912762';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "targetColumnMap"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" ADD "targetColumnMap" jsonb NOT NULL`,
);
}
}

View File

@@ -6,7 +6,6 @@ export const fieldNumberMock = {
type: FieldMetadataType.NUMBER, type: FieldMetadataType.NUMBER,
isNullable: false, isNullable: false,
defaultValue: null, defaultValue: null,
targetColumnMap: { value: 'fieldNumber' },
}; };
export const fieldStringMock = { export const fieldStringMock = {
@@ -14,7 +13,6 @@ export const fieldStringMock = {
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
targetColumnMap: { value: 'fieldString' },
}; };
export const fieldLinkMock = { export const fieldLinkMock = {
@@ -22,23 +20,18 @@ export const fieldLinkMock = {
type: FieldMetadataType.LINK, type: FieldMetadataType.LINK,
isNullable: false, isNullable: false,
defaultValue: { label: '', url: '' }, defaultValue: { label: '', url: '' },
targetColumnMap: { label: 'fieldLinkLabel', url: 'fieldLinkUrl' },
}; };
export const fieldCurrencyMock = { export const fieldCurrencyMock = {
name: 'fieldCurrency', name: 'fieldCurrency',
type: FieldMetadataType.CURRENCY, type: FieldMetadataType.CURRENCY,
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: { amountMicros: null, currencyCode: "''" },
targetColumnMap: {
amountMicros: 'fieldCurrencyAmountMicros',
currencyCode: 'fieldCurrencyCurrencyCode',
},
}; };
export const objectMetadataItemMock: DeepPartial<ObjectMetadataEntity> = { export const objectMetadataItemMock = {
targetTableName: 'testingObject', targetTableName: 'testingObject',
nameSingular: 'objectName', nameSingular: 'objectName',
namePlural: 'objectsName', namePlural: 'objectsName',
fields: [fieldNumberMock, fieldStringMock, fieldLinkMock, fieldCurrencyMock], fields: [fieldNumberMock, fieldStringMock, fieldLinkMock, fieldCurrencyMock],
}; } as ObjectMetadataEntity;

View File

@@ -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 { 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() @Injectable()
export class ArgsAliasFactory { export class ArgsAliasFactory {
private readonly logger = new Logger(ArgsAliasFactory.name);
create( create(
args: Record<string, any>, args: Record<string, any>,
fieldMetadataCollection: FieldMetadataInterface[], fieldMetadataCollection: FieldMetadataInterface[],
@@ -39,25 +45,42 @@ export class ArgsAliasFactory {
for (const [key, value] of Object.entries(args)) { for (const [key, value] of Object.entries(args)) {
const fieldMetadata = fieldMetadataMap.get(key); 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 ( if (
fieldMetadata && fieldMetadata &&
typeof value === 'object' &&
value !== null && value !== null &&
Object.values(fieldMetadata.targetColumnMap).length > 1 isCompositeFieldMetadataType(fieldMetadata.type)
) { ) {
for (const [subKey, subValue] of Object.entries(value)) { // Get composite type definition
const mappedKey = fieldMetadata.targetColumnMap[subKey]; const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
if (mappedKey) { if (!compositeType) {
newArgs[mappedKey] = subValue; 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) { } else if (fieldMetadata) {
// Otherwise we just need to map the value newArgs[key] = value;
const mappedKey = fieldMetadata.targetColumnMap.value;
newArgs[mappedKey ?? key] = value;
} else { } else {
// Recurse if value is a nested object, otherwise append field or alias // Recurse if value is a nested object, otherwise append field or alias
newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap); newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap);

View File

@@ -2,29 +2,49 @@ import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; 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() @Injectable()
export class FieldAliasFactory { export class FieldAliasFactory {
private readonly logger = new Logger(FieldAliasFactory.name); private readonly logger = new Logger(FieldAliasFactory.name);
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) { create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
const entries = Object.entries(fieldMetadata.targetColumnMap); // If it's not a composite field, we can just return the alias
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
if (entries.length === 0) { const alias = computeColumnName(fieldMetadata);
return null;
}
if (entries.length === 1) {
// If there is only one value, use it as the alias
const alias = entries[0][1];
return `${fieldKey}: ${alias}`; return `${fieldKey}: ${alias}`;
} }
// Otherwise it means it's a special type with multiple values, so we need map all columns // If it's a composite field, we need to get the definition
return ` const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
${entries
.map(([key, value]) => `___${fieldMetadata.name}_${key}: ${value}`) if (!compositeType) {
.join('\n')} 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');
} }
} }

View File

@@ -14,6 +14,7 @@ import {
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util'; 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 { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; 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 { FieldsStringFactory } from './fields-string.factory';
import { ArgsStringFactory } from './args-string.factory'; import { ArgsStringFactory } from './args-string.factory';
@@ -118,9 +119,7 @@ export class RelationFieldAliasFactory {
`; `;
} }
let relationAlias = fieldMetadata.isCustom let relationAlias = `${fieldKey}: ${computeColumnName(fieldMetadata)}`;
? `${fieldKey}: _${fieldMetadata.name}`
: fieldKey;
// For one to one relations, pg_graphql use the target TableName on the side that is not storing the foreign key // 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 // so we need to alias it to the field key

View File

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

View File

@@ -1,19 +1,9 @@
import { createCompositeFieldKey } from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util';
import { import {
isSpecialKey, handleCompositeKey,
handleSpecialKey,
parseResult, parseResult,
} from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util'; } 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', () => { describe('handleSpecialKey', () => {
let result; let result;
@@ -21,8 +11,12 @@ describe('handleSpecialKey', () => {
result = {}; result = {};
}); });
test('should correctly process a special key and add it to the result object', () => { test('should correctly process a composite key and add it to the result object', () => {
handleSpecialKey(result, '___complexField_link', 'value1'); handleCompositeKey(
result,
createCompositeFieldKey('complexField', 'link'),
'value1',
);
expect(result).toEqual({ expect(result).toEqual({
complexField: { complexField: {
link: 'value1', link: 'value1',
@@ -31,8 +25,16 @@ describe('handleSpecialKey', () => {
}); });
test('should add values under the same newKey if called multiple times', () => { test('should add values under the same newKey if called multiple times', () => {
handleSpecialKey(result, '___complexField_link', 'value1'); handleCompositeKey(
handleSpecialKey(result, '___complexField_text', 'value2'); result,
createCompositeFieldKey('complexField', 'link'),
'value1',
);
handleCompositeKey(
result,
createCompositeFieldKey('complexField', 'text'),
'value2',
);
expect(result).toEqual({ expect(result).toEqual({
complexField: { complexField: {
link: 'value1', link: 'value1',
@@ -41,8 +43,8 @@ describe('handleSpecialKey', () => {
}); });
}); });
test('should not create a new field if the special key is not correctly formed', () => { test('should not create a new field if the composite key is not correctly formed', () => {
handleSpecialKey(result, '___complexField', 'value1'); handleCompositeKey(result, 'COMPOSITE___complexField', 'value1');
expect(result).toEqual({}); expect(result).toEqual({});
}); });
}); });
@@ -51,9 +53,9 @@ describe('parseResult', () => {
test('should recursively parse an object and handle special keys', () => { test('should recursively parse an object and handle special keys', () => {
const obj = { const obj = {
normalField: 'value1', normalField: 'value1',
___specialField_part1: 'value2', COMPOSITE___specialField_part1: 'value2',
nested: { nested: {
___specialFieldNested_part2: 'value3', COMPOSITE___specialFieldNested_part2: 'value3',
}, },
}; };
@@ -75,10 +77,10 @@ describe('parseResult', () => {
test('should handle arrays and parse each element', () => { test('should handle arrays and parse each element', () => {
const objArray = [ const objArray = [
{ {
___specialField_part1: 'value1', COMPOSITE___specialField_part1: 'value1',
}, },
{ {
___specialField_part2: 'value2', COMPOSITE___specialField_part2: 'value2',
}, },
]; ];

View File

@@ -1,27 +1,25 @@
export const isSpecialKey = (key: string): boolean => { import {
return key.startsWith('___'); isPrefixedCompositeField,
}; parseCompositeFieldKey,
} from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util';
export const handleSpecialKey = ( export const handleCompositeKey = (
result: any, result: any,
key: string, key: string,
value: any, value: any,
): void => { ): void => {
const parts = key.split('_').filter((part) => part); const parsedFieldKey = parseCompositeFieldKey(key);
// If parts don't contain enough information, return without altering result // If composite field key can't be parsed, return
if (parts.length < 2) { if (!parsedFieldKey) {
return; return;
} }
const newKey = parts.slice(0, -1).join(''); if (!result[parsedFieldKey.parentFieldName]) {
const subKey = parts[parts.length - 1]; result[parsedFieldKey.parentFieldName] = {};
if (!result[newKey]) {
result[newKey] = {};
} }
result[newKey][subKey] = value; result[parsedFieldKey.parentFieldName][parsedFieldKey.childFieldName] = value;
}; };
export const parseResult = (obj: any): any => { export const parseResult = (obj: any): any => {
@@ -41,8 +39,8 @@ export const parseResult = (obj: any): any => {
result[key] = parseResult(obj[key]); result[key] = parseResult(obj[key]);
} else if (key === '__typename') { } else if (key === '__typename') {
result[key] = obj[key].replace(/^_*/, ''); result[key] = obj[key].replace(/^_*/, '');
} else if (isSpecialKey(key)) { } else if (isPrefixedCompositeField(key)) {
handleSpecialKey(result, key, obj[key]); handleCompositeKey(result, key, obj[key]);
} else { } else {
result[key] = obj[key]; result[key] = obj[key];
} }

View File

@@ -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;
}
}

View File

@@ -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<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
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;
}
}

View File

@@ -1,4 +1,6 @@
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; 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 { ArgsFactory } from './args.factory';
import { InputTypeFactory } from './input-type.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 { OutputTypeFactory } from './output-type.factory';
import { QueryTypeFactory } from './query-type.factory'; import { QueryTypeFactory } from './query-type.factory';
import { RootTypeFactory } from './root-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 { ConnectionTypeFactory } from './connection-type.factory';
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory'; import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
import { EdgeTypeFactory } from './edge-type.factory'; import { EdgeTypeFactory } from './edge-type.factory';
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory'; import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
import { MutationTypeFactory } from './mutation-type.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 { RelationTypeFactory } from './relation-type.factory';
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory'; import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
import { OrphanedTypesFactory } from './orphaned-types.factory'; import { OrphanedTypesFactory } from './orphaned-types.factory';
@@ -24,15 +22,13 @@ export const workspaceSchemaBuilderFactories = [
ArgsFactory, ArgsFactory,
InputTypeFactory, InputTypeFactory,
InputTypeDefinitionFactory, InputTypeDefinitionFactory,
CompositeInputTypeDefinitionFactory,
OutputTypeFactory, OutputTypeFactory,
ObjectTypeDefinitionFactory, ObjectTypeDefinitionFactory,
CompositeObjectTypeDefinitionFactory,
EnumTypeDefinitionFactory, EnumTypeDefinitionFactory,
RelationTypeFactory, RelationTypeFactory,
ExtendObjectTypeDefinitionFactory, ExtendObjectTypeDefinitionFactory,
FilterTypeFactory,
FilterTypeDefinitionFactory,
OrderByTypeFactory,
OrderByTypeDefinitionFactory,
ConnectionTypeFactory, ConnectionTypeFactory,
ConnectionTypeDefinitionFactory, ConnectionTypeDefinitionFactory,
EdgeTypeFactory, EdgeTypeFactory,

View File

@@ -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;
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Inject, Injectable, forwardRef } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; 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 { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; 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 { 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'; import { InputTypeFactory } from './input-type.factory';
@@ -26,23 +28,60 @@ export interface InputTypeDefinition {
@Injectable() @Injectable()
export class InputTypeDefinitionFactory { export class InputTypeDefinitionFactory {
constructor(private readonly inputTypeFactory: InputTypeFactory) {} constructor(
@Inject(forwardRef(() => InputTypeFactory))
private readonly inputTypeFactory: InputTypeFactory,
private readonly typeMapperService: TypeMapperService,
) {}
public create( public create(
objectMetadata: ObjectMetadataInterface, objectMetadata: ObjectMetadataInterface,
kind: InputTypeDefinitionKind, kind: InputTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
): InputTypeDefinition { ): 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 { return {
target: objectMetadata.id, target: objectMetadata.id,
kind, kind,
type: new GraphQLInputObjectType({ type: inputType,
name: `${pascalCase(
objectMetadata.nameSingular,
)}${kind.toString()}Input`,
description: objectMetadata.description,
fields: this.generateFields(objectMetadata, kind, options),
}),
}; };
} }
@@ -59,17 +98,25 @@ export class InputTypeDefinitionFactory {
continue; continue;
} }
const type = this.inputTypeFactory.create(fieldMetadata, kind, options, { const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
const type = this.inputTypeFactory.create(
target,
fieldMetadata.type,
kind,
options,
{
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue, defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
}); },
);
fields[fieldMetadata.name] = { fields[fieldMetadata.name] = {
type, type,
description: fieldMetadata.description, description: fieldMetadata.description,
// TODO: Add default value
defaultValue: undefined,
}; };
} }

View File

@@ -1,16 +1,17 @@
import { Injectable, Logger } from '@nestjs/common'; 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 { 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 { import {
TypeMapperService, TypeMapperService,
TypeOptions, TypeOptions,
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; } 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 { 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'; import { InputTypeDefinitionKind } from './input-type-definition.factory';
@@ -24,30 +25,60 @@ export class InputTypeFactory {
) {} ) {}
public create( public create(
fieldMetadata: FieldMetadataInterface, target: string,
type: FieldMetadataType,
kind: InputTypeDefinitionKind, kind: InputTypeDefinitionKind,
buildOtions: WorkspaceBuildSchemaOptions, buildOptions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions, typeOptions: TypeOptions,
): GraphQLInputType { ): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type) let inputType: GraphQLInputType | undefined;
? fieldMetadata.type.toString()
: fieldMetadata.id; switch (kind) {
let inputType: GraphQLInputType | undefined = /**
this.typeMapperService.mapToScalarType( * Create and Update input types are classic scalar types
fieldMetadata.type, */
buildOtions.dateScalarMode, case InputTypeDefinitionKind.Create:
buildOtions.numberScalarMode, 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.getInputTypeByKey(target, kind);
inputType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
if (!inputType) { if (!inputType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, { this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata, type,
kind, kind,
buildOtions, buildOptions,
typeOptions, typeOptions,
}); });
@@ -56,4 +87,24 @@ export class InputTypeFactory {
return this.typeMapperService.mapToGqlType(inputType, typeOptions); 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 },
}),
});
}
} }

View File

@@ -8,6 +8,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
import { pascalCase } from 'src/utils/pascal-case'; import { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; 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 { 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'; import { OutputTypeFactory } from './output-type.factory';
@@ -56,10 +57,20 @@ export class ObjectTypeDefinitionFactory {
continue; continue;
} }
const type = this.outputTypeFactory.create(fieldMetadata, kind, options, { const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
const type = this.outputTypeFactory.create(
target,
fieldMetadata.type,
kind,
options,
{
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
}); },
);
fields[fieldMetadata.name] = { fields[fieldMetadata.name] = {
type, type,

View File

@@ -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;
}
}

View File

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

View File

@@ -3,14 +3,13 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql'; import { GraphQLOutputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { import {
TypeMapperService, TypeMapperService,
TypeOptions, TypeOptions,
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; } 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 { 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'; import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@@ -24,28 +23,24 @@ export class OutputTypeFactory {
) {} ) {}
public create( public create(
fieldMetadata: FieldMetadataInterface, target: string,
type: FieldMetadataType,
kind: ObjectTypeDefinitionKind, kind: ObjectTypeDefinitionKind,
buildOtions: WorkspaceBuildSchemaOptions, buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions, typeOptions: TypeOptions,
): GraphQLOutputType { ): GraphQLOutputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let gqlType: GraphQLOutputType | undefined = let gqlType: GraphQLOutputType | undefined =
this.typeMapperService.mapToScalarType( this.typeMapperService.mapToScalarType(
fieldMetadata.type, type,
buildOtions.dateScalarMode, buildOtions.dateScalarMode,
buildOtions.numberScalarMode, buildOtions.numberScalarMode,
); );
gqlType ??= this.typeDefinitionsStorage.getObjectTypeByKey(target, kind); gqlType ??= this.typeDefinitionsStorage.getOutputTypeByKey(target, kind);
gqlType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
if (!gqlType) { if (!gqlType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, { this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata, type,
buildOtions, buildOtions,
typeOptions, typeOptions,
}); });

View File

@@ -57,7 +57,6 @@ export class TypeMapperService {
const numberScalar = const numberScalar =
numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt; numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt;
// LINK and CURRENCY are handled in the factories because they are objects
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([ const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
[FieldMetadataType.UUID, GraphQLID], [FieldMetadataType.UUID, GraphQLID],
[FieldMetadataType.TEXT, GraphQLString], [FieldMetadataType.TEXT, GraphQLString],
@@ -86,7 +85,6 @@ export class TypeMapperService {
const numberScalar = const numberScalar =
numberScalarMode === 'float' ? FloatFilterType : IntFilterType; numberScalarMode === 'float' ? FloatFilterType : IntFilterType;
// LINK and CURRENCY are handled in the factories because they are objects
const typeFilterMapping = new Map< const typeFilterMapping = new Map<
FieldMetadataType, FieldMetadataType,
GraphQLInputObjectType | GraphQLScalarType GraphQLInputObjectType | GraphQLScalarType
@@ -111,7 +109,6 @@ export class TypeMapperService {
mapToOrderByType( mapToOrderByType(
fieldMetadataType: FieldMetadataType, fieldMetadataType: FieldMetadataType,
): GraphQLInputType | undefined { ): GraphQLInputType | undefined {
// LINK and CURRENCY are handled in the factories because they are objects
const typeOrderByMapping = new Map<FieldMetadataType, GraphQLEnumType>([ const typeOrderByMapping = new Map<FieldMetadataType, GraphQLEnumType>([
[FieldMetadataType.UUID, OrderByDirectionType], [FieldMetadataType.UUID, OrderByDirectionType],
[FieldMetadataType.TEXT, OrderByDirectionType], [FieldMetadataType.TEXT, OrderByDirectionType],

View File

@@ -17,7 +17,12 @@ import {
ObjectTypeDefinitionKind, ObjectTypeDefinitionKind,
} from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; } 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 }) @Injectable({ scope: Scope.REQUEST, durable: true })
export class TypeDefinitionsStorage { export class TypeDefinitionsStorage {
private readonly enumTypeDefinitions = new Map<string, EnumTypeDefinition>(); private readonly enumTypeDefinitions = new Map<string, EnumTypeDefinition>();
@@ -68,10 +73,27 @@ export class TypeDefinitionsStorage {
getInputTypeByKey( getInputTypeByKey(
target: string, target: string,
kind: InputTypeDefinitionKind, kind: InputTypeDefinitionKind,
): GraphQLInputObjectType | undefined { ): GraphQLInputObjectType | GraphQLEnumType | undefined {
return this.inputTypeDefinitions.get( const key = this.generateCompositeKey(target, kind);
this.generateCompositeKey(target, kind), let definition: GqlInputType | undefined;
)?.type;
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 { getEnumTypeByKey(target: string): GraphQLEnumType | undefined {

View File

@@ -1,15 +1,12 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; 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 { 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 { TypeDefinitionsStorage } from './storages/type-definitions.storage';
import { import {
@@ -20,24 +17,12 @@ import {
InputTypeDefinitionFactory, InputTypeDefinitionFactory,
InputTypeDefinitionKind, InputTypeDefinitionKind,
} from './factories/input-type-definition.factory'; } 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 { 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 { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
import { EdgeTypeDefinitionFactory } from './factories/edge-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 { ExtendObjectTypeDefinitionFactory } from './factories/extend-object-type-definition.factory';
import { objectContainsRelationField } from './utils/object-contains-relation-field'; 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() @Injectable()
export class TypeDefinitionsGenerator { export class TypeDefinitionsGenerator {
private readonly logger = new Logger(TypeDefinitionsGenerator.name); private readonly logger = new Logger(TypeDefinitionsGenerator.name);
@@ -45,10 +30,10 @@ export class TypeDefinitionsGenerator {
constructor( constructor(
private readonly typeDefinitionsStorage: TypeDefinitionsStorage, private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory, private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory,
private readonly compositeObjectTypeDefinitionFactory: CompositeObjectTypeDefinitionFactory,
private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory, private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory,
private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory, private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory,
private readonly filterTypeDefintionFactory: FilterTypeDefinitionFactory, private readonly compositeInputTypeDefinitionFactory: CompositeInputTypeDefinitionFactory,
private readonly orderByTypeDefinitionFactory: OrderByTypeDefinitionFactory,
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory, private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory, private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory,
private readonly extendObjectTypeDefinitionFactory: ExtendObjectTypeDefinitionFactory, private readonly extendObjectTypeDefinitionFactory: ExtendObjectTypeDefinitionFactory,
@@ -58,38 +43,96 @@ export class TypeDefinitionsGenerator {
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
) { ) {
// Generate static objects first because they can be used in dynamic objects // Generate composite type objects first because they can be used in dynamic objects
this.generateStaticObjectTypeDefs(options); this.generateCompositeTypeDefs(options);
// Generate dynamic objects // Generate metadata objects
this.generateDynamicObjectTypeDefs(objectMetadataCollection, options); this.generateMetadataTypeDefs(objectMetadataCollection, options);
} }
private generateStaticObjectTypeDefs(options: WorkspaceBuildSchemaOptions) { /**
const staticObjectMetadataCollection = [ * GENERATE COMPOSITE TYPE OBJECTS
currencyObjectDefinition, */
linkObjectDefinition, private generateCompositeTypeDefs(options: WorkspaceBuildSchemaOptions) {
fullNameObjectDefinition, const compositeTypeCollection = [...compositeTypeDefintions.values()];
addressObjectDefinition,
] satisfies ObjectMetadataInterface[];
this.logger.log( this.logger.log(
`Generating staticObjects: [${staticObjectMetadataCollection `Generating composite type objects: [${compositeTypeCollection
.map((object) => object.nameSingular) .map((compositeType) => compositeType.type)
.join(', ')}]`, .join(', ')}]`,
); );
// Generate static objects first because they can be used in dynamic objects // Generate composite types first because they can be used in metadata objects
this.generateEnumTypeDefs(staticObjectMetadataCollection, options); this.generateCompositeObjectTypeDefs(compositeTypeCollection, options);
this.generateObjectTypeDefs(staticObjectMetadataCollection, options); this.generateCompositeInputTypeDefs(compositeTypeCollection, options);
this.generateInputTypeDefs(staticObjectMetadataCollection, 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[], dynamicObjectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
) { ) {
this.logger.log( this.logger.log(
`Generating dynamicObjects: [${dynamicObjectMetadataCollection `Generating metadata objects: [${dynamicObjectMetadataCollection
.map((object) => object.nameSingular) .map((object) => object.nameSingular)
.join(', ')}]`, .join(', ')}]`,
); );
@@ -106,22 +149,16 @@ export class TypeDefinitionsGenerator {
} }
private generateObjectTypeDefs( private generateObjectTypeDefs(
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[] | CompositeType[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
) { ) {
const objectTypeDefs = objectMetadataCollection.map((objectMetadata) => { const objectTypeDefs = objectMetadataCollection.map((objectMetadata) =>
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); this.objectTypeDefinitionFactory.create(
const extendedObjectMetadata = { objectMetadata,
...objectMetadata,
fields,
};
return this.objectTypeDefinitionFactory.create(
extendedObjectMetadata,
ObjectTypeDefinitionKind.Plain, ObjectTypeDefinitionKind.Plain,
options, options,
),
); );
});
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs); this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
} }
@@ -130,35 +167,15 @@ export class TypeDefinitionsGenerator {
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
) { ) {
const edgeTypeDefs = objectMetadataCollection.map((objectMetadata) => { const edgeTypeDefs = objectMetadataCollection.map((objectMetadata) =>
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); this.edgeTypeDefinitionFactory.create(objectMetadata, options),
const extendedObjectMetadata = {
...objectMetadata,
fields,
};
return this.edgeTypeDefinitionFactory.create(
extendedObjectMetadata,
options,
); );
});
this.typeDefinitionsStorage.addObjectTypes(edgeTypeDefs); this.typeDefinitionsStorage.addObjectTypes(edgeTypeDefs);
// Connection type defs are using edge type defs // Connection type defs are using edge type defs
const connectionTypeDefs = objectMetadataCollection.map( const connectionTypeDefs = objectMetadataCollection.map((objectMetadata) =>
(objectMetadata) => { this.connectionTypeDefinitionFactory.create(objectMetadata, options),
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
const extendedObjectMetadata = {
...objectMetadata,
fields,
};
return this.connectionTypeDefinitionFactory.create(
extendedObjectMetadata,
options,
);
},
); );
this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs); this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs);
@@ -170,20 +187,18 @@ export class TypeDefinitionsGenerator {
) { ) {
const inputTypeDefs = objectMetadataCollection const inputTypeDefs = objectMetadataCollection
.map((objectMetadata) => { .map((objectMetadata) => {
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
const requiredExtendedObjectMetadata = {
...objectMetadata,
fields,
};
const optionalExtendedObjectMetadata = { const optionalExtendedObjectMetadata = {
...objectMetadata, ...objectMetadata,
fields: fields.map((field) => ({ ...field, isNullable: true })), fields: objectMetadata.fields.map((field) => ({
...field,
isNullable: true,
})),
}; };
return [ return [
// Input type for create // Input type for create
this.inputTypeDefinitionFactory.create( this.inputTypeDefinitionFactory.create(
requiredExtendedObjectMetadata, objectMetadata,
InputTypeDefinitionKind.Create, InputTypeDefinitionKind.Create,
options, options,
), ),
@@ -194,13 +209,15 @@ export class TypeDefinitionsGenerator {
options, options,
), ),
// Filter input type // Filter input type
this.filterTypeDefintionFactory.create( this.inputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata, optionalExtendedObjectMetadata,
InputTypeDefinitionKind.Filter,
options, options,
), ),
// OrderBy input type // OrderBy input type
this.orderByTypeDefinitionFactory.create( this.inputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata, optionalExtendedObjectMetadata,
InputTypeDefinitionKind.OrderBy,
options, options,
), ),
]; ];
@@ -237,16 +254,4 @@ export class TypeDefinitionsGenerator {
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs); 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];
}
} }

View File

@@ -12,10 +12,10 @@ import { TypeMapperService } from './services/type-mapper.service';
@Module({ @Module({
imports: [ObjectMetadataModule], imports: [ObjectMetadataModule],
providers: [ providers: [
...workspaceSchemaBuilderFactories,
TypeDefinitionsGenerator,
TypeDefinitionsStorage, TypeDefinitionsStorage,
TypeMapperService, TypeMapperService,
...workspaceSchemaBuilderFactories,
TypeDefinitionsGenerator,
WorkspaceGraphQLSchemaFactory, WorkspaceGraphQLSchemaFactory,
], ],
exports: [WorkspaceGraphQLSchemaFactory], exports: [WorkspaceGraphQLSchemaFactory],

View File

@@ -1,36 +1,55 @@
import { BadRequestException } from '@nestjs/common'; 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 { 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'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const getFieldType = ( export const getFieldType = (
objectMetadataItem, objectMetadata: ObjectMetadataEntity,
fieldName, fieldName: string,
): FieldMetadataType | undefined => { ): FieldMetadataType | undefined => {
for (const itemField of objectMetadataItem.fields) { for (const fieldMetdata of objectMetadata.fields) {
if (fieldName === itemField.name) { if (fieldName === fieldMetdata.name) {
return itemField.type; 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) { for (const fieldName of fieldNames) {
if ( if (!fieldMetadataNames.includes(fieldName)) {
!objectMetadataItem.fields
.reduce(
(acc, itemField) => [
...acc,
itemField.name,
...Object.keys(itemField.targetColumnMap),
],
[],
)
.includes(fieldName)
) {
throw new BadRequestException( throw new BadRequestException(
`field '${fieldName}' does not exist in '${computeObjectTargetTable( `field '${fieldName}' does not exist in '${computeObjectTargetTable(
objectMetadataItem, objectMetadata,
)}' object`, )}' object`,
); );
} }

View File

@@ -3,6 +3,7 @@ import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metad
const DEFAULT_DEPTH_VALUE = 2; const DEFAULT_DEPTH_VALUE = 2;
// TODO: Should be properly type and based on composite type definitions
export const mapFieldMetadataToGraphqlQuery = ( export const mapFieldMetadataToGraphqlQuery = (
objectMetadataItems, objectMetadataItems,
field, field,

View File

@@ -11,6 +11,7 @@ import {
computeLimitParameters, computeLimitParameters,
computeOrderByParameters, computeOrderByParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils'; } 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; type Property = OpenAPIV3_1.SchemaObject;
@@ -60,14 +61,14 @@ const getSchemaComponentsProperties = (
case FieldMetadataType.ADDRESS: case FieldMetadataType.ADDRESS:
itemProperty = { itemProperty = {
type: 'object', type: 'object',
properties: Object.keys(field.targetColumnMap).reduce( properties: compositeTypeDefintions
(properties, key) => { .get(field.type)
properties[key] = { type: 'string' }; ?.properties?.reduce((properties, property) => {
// TODO: This should not be statically typed, instead we should do someting recursive
properties[property.name] = { type: 'string' };
return properties; return properties;
}, }, {} as Properties),
{} as Properties,
),
}; };
break; break;
case FieldMetadataType.RAW_JSON: case FieldMetadataType.RAW_JSON:

View File

@@ -1,188 +1,61 @@
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 { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; 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 = ( export const addressCompositeType: CompositeType = {
fieldMetadata?: FieldMetadataInterface, type: FieldMetadataType.ADDRESS,
): FieldMetadataInterface[] => { properties: [
const inferredFieldMetadata = fieldMetadata as
| FieldMetadataInterface<FieldMetadataType.ADDRESS>
| 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 [
{ {
id: 'addressStreet1',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
name: 'addressStreet1', name: 'addressStreet1',
label: 'Address',
targetColumnMap: {
value: targetColumnMap.addressStreet1,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressStreet1 ?? undefined,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressStreet2',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(), hidden: false,
isRequired: false,
},
{
name: 'addressStreet2', name: 'addressStreet2',
label: 'Address 2',
targetColumnMap: {
value: targetColumnMap.addressStreet2,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressStreet2 ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressCity',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(), hidden: false,
isRequired: false,
},
{
name: 'addressCity', name: 'addressCity',
label: 'City',
targetColumnMap: {
value: targetColumnMap.addressCity,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressCity ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressPostcode',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(), hidden: false,
isRequired: false,
},
{
name: 'addressPostcode', name: 'addressPostcode',
label: 'Postcode',
targetColumnMap: {
value: targetColumnMap.addressPostcode,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressPostcode ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressState',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(), hidden: false,
isRequired: false,
},
{
name: 'addressState', name: 'addressState',
label: 'State',
targetColumnMap: {
value: targetColumnMap.addressState,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressState ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressCountry',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(), hidden: false,
isRequired: false,
},
{
name: 'addressCountry', name: 'addressCountry',
label: 'Country', type: FieldMetadataType.TEXT,
targetColumnMap: { hidden: false,
value: targetColumnMap.addressCountry, isRequired: false,
}, },
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressCountry ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{ {
id: 'addressLat',
type: FieldMetadataType.NUMBER,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
name: 'addressLat', name: 'addressLat',
label: 'Latitude', type: FieldMetadataType.NUMERIC,
targetColumnMap: { hidden: false,
value: targetColumnMap.addressLat, isRequired: false,
}, },
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressLat ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.NUMBER>,
{ {
id: 'addressLng',
type: FieldMetadataType.NUMBER,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
name: 'addressLng', name: 'addressLng',
label: 'Longitude', type: FieldMetadataType.NUMERIC,
targetColumnMap: { hidden: false,
value: targetColumnMap.addressLng, isRequired: false,
}, },
isNullable: true, ],
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressLng ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.NUMBER>,
];
}; };
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 = { export type AddressMetadata = {
addressStreet1: string; addressStreet1: string;
addressStreet2: string; addressStreet2: string;

View File

@@ -1,80 +1,25 @@
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 { 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 { 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 = ( export const currencyCompositeType: CompositeType = {
fieldMetadata?: FieldMetadataInterface, type: FieldMetadataType.CURRENCY,
): FieldMetadataInterface[] => { properties: [
const inferredFieldMetadata = fieldMetadata as
| FieldMetadataInterface<FieldMetadataType.CURRENCY>
| undefined;
const targetColumnMap = inferredFieldMetadata
? generateTargetColumnMap(
inferredFieldMetadata.type,
inferredFieldMetadata.isCustom ?? false,
inferredFieldMetadata.name,
)
: {
amountMicros: 'amountMicros',
currencyCode: 'currencyCode',
};
return [
{ {
id: 'amountMicros',
type: FieldMetadataType.NUMERIC,
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'amountMicros', name: 'amountMicros',
label: 'AmountMicros', type: FieldMetadataType.NUMERIC,
targetColumnMap: { hidden: false,
value: targetColumnMap.amountMicros, isRequired: false,
}, },
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.amountMicros ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.NUMERIC>,
{ {
id: 'currencyCode',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'currencyCode', name: 'currencyCode',
label: 'Currency Code', type: FieldMetadataType.TEXT,
targetColumnMap: { hidden: false,
value: targetColumnMap.currencyCode, isRequired: false,
}, },
isNullable: true, ],
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.currencyCode ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
];
}; };
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 = { export type CurrencyMetadata = {
amountMicros: number; amountMicros: number;
currencyCode: string; currencyCode: string;

View File

@@ -1,78 +1,25 @@
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 { 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 { 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 = ( export const fullNameCompositeType: CompositeType = {
fieldMetadata?: FieldMetadataInterface, type: FieldMetadataType.FULL_NAME,
): FieldMetadataInterface[] => { properties: [
const inferredFieldMetadata = fieldMetadata as
| FieldMetadataInterface<FieldMetadataType.FULL_NAME>
| undefined;
const targetColumnMap = inferredFieldMetadata
? generateTargetColumnMap(
inferredFieldMetadata.type,
inferredFieldMetadata.isCustom ?? false,
inferredFieldMetadata.name,
)
: {
firstName: 'firstName',
lastName: 'lastName',
};
return [
{ {
id: 'firstName',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.FULL_NAME.toString(),
name: 'firstName', name: 'firstName',
label: 'First Name',
targetColumnMap: {
value: targetColumnMap.firstName,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue: inferredFieldMetadata.defaultValue?.firstName ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'lastName',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.FULL_NAME.toString(), hidden: false,
name: 'lastName', isRequired: false,
label: 'Last Name',
targetColumnMap: {
value: targetColumnMap.lastName,
}, },
isNullable: true, {
...(inferredFieldMetadata name: 'lastName',
? { type: FieldMetadataType.TEXT,
defaultValue: inferredFieldMetadata.defaultValue?.lastName ?? null, hidden: false,
} isRequired: false,
: {}), },
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>, ],
];
}; };
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 = { export type FullNameMetadata = {
firstName: string; firstName: string;
lastName: string; lastName: string;

View File

@@ -1,21 +1,22 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-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 { currencyFields } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; import { currencyCompositeType } 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 { fullNameCompositeType } 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 { 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 { 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 = ( export type CompositeFieldsDefinitionFunction = (
fieldMetadata?: FieldMetadataInterface, fieldMetadata?: FieldMetadataInterface,
) => FieldMetadataInterface[]; ) => FieldMetadataInterface[];
export const compositeDefinitions = new Map< export const compositeTypeDefintions = new Map<
string, FieldMetadataType,
CompositeFieldsDefinitionFunction CompositeType
>([ >([
[FieldMetadataType.LINK, linkFields], [FieldMetadataType.LINK, linkCompositeType],
[FieldMetadataType.CURRENCY, currencyFields], [FieldMetadataType.CURRENCY, currencyCompositeType],
[FieldMetadataType.FULL_NAME, fullNameFields], [FieldMetadataType.FULL_NAME, fullNameCompositeType],
[FieldMetadataType.ADDRESS, addressFields], [FieldMetadataType.ADDRESS, addressCompositeType],
]); ]);

View File

@@ -1,78 +1,25 @@
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 { 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 { 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 = ( export const linkCompositeType: CompositeType = {
fieldMetadata?: FieldMetadataInterface, type: FieldMetadataType.LINK,
): FieldMetadataInterface[] => { properties: [
const inferredFieldMetadata = fieldMetadata as
| FieldMetadataInterface<FieldMetadataType.LINK>
| undefined;
const targetColumnMap = inferredFieldMetadata
? generateTargetColumnMap(
inferredFieldMetadata.type,
inferredFieldMetadata.isCustom ?? false,
inferredFieldMetadata.name,
)
: {
label: 'label',
url: 'url',
};
return [
{ {
id: 'label',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.LINK.toString(),
name: 'label', name: 'label',
label: 'Label',
targetColumnMap: {
value: targetColumnMap.label,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue: inferredFieldMetadata.defaultValue?.label ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'url',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.LINK.toString(), hidden: false,
name: 'url', isRequired: false,
label: 'Url',
targetColumnMap: {
value: targetColumnMap.url,
}, },
isNullable: true, {
...(inferredFieldMetadata name: 'url',
? { type: FieldMetadataType.TEXT,
defaultValue: inferredFieldMetadata.defaultValue?.url ?? null, hidden: false,
} isRequired: false,
: {}), },
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>, ],
];
}; };
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 = { export type LinkMetadata = {
label: string; label: string;
url: string; url: string;

View File

@@ -11,7 +11,6 @@ import {
} from 'typeorm'; } from 'typeorm';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; 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 { 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'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
@@ -74,9 +73,6 @@ export class FieldMetadataEntity<
@Column({ nullable: false }) @Column({ nullable: false })
label: string; label: string;
@Column({ nullable: false, type: 'jsonb' })
targetColumnMap: FieldMetadataTargetColumnMap<T>;
@Column({ nullable: true, type: 'jsonb' }) @Column({ nullable: true, type: 'jsonb' })
defaultValue: FieldMetadataDefaultValue<T>; defaultValue: FieldMetadataDefaultValue<T>;

View File

@@ -19,7 +19,6 @@ import {
WorkspaceMigrationColumnDrop, WorkspaceMigrationColumnDrop,
WorkspaceMigrationTableAction, WorkspaceMigrationTableAction,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; } 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 { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.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'; import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
@@ -37,7 +36,7 @@ import {
RelationMetadataType, RelationMetadataType,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; 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 { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { import {
@@ -128,11 +127,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
const createdFieldMetadata = await fieldMetadataRepository.save({ const createdFieldMetadata = await fieldMetadataRepository.save({
...fieldMetadataInput, ...fieldMetadataInput,
targetColumnMap: generateTargetColumnMap(
fieldMetadataInput.type,
!fieldMetadataInput.isRemoteCreation,
fieldMetadataInput.name,
),
isNullable: generateNullable( isNullable: generateNullable(
fieldMetadataInput.type, fieldMetadataInput.type,
fieldMetadataInput.isNullable, fieldMetadataInput.isNullable,
@@ -318,14 +312,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
: updatableFieldInput.defaultValue !== null : updatableFieldInput.defaultValue !== null
? updatableFieldInput.defaultValue ? updatableFieldInput.defaultValue
: null, : null,
// If the name is updated, the targetColumnMap should be updated as well
targetColumnMap: updatableFieldInput.name
? generateTargetColumnMap(
existingFieldMetadata.type,
existingFieldMetadata.isCustom,
updatableFieldInput.name,
)
: existingFieldMetadata.targetColumnMap,
}); });
const updatedFieldMetadata = await fieldMetadataRepository.findOneOrFail({ const updatedFieldMetadata = await fieldMetadataRepository.findOneOrFail({
where: { id }, where: { id },
@@ -417,10 +403,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.DROP, action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeCustomName( columnName: computeColumnName(fieldMetadata),
fieldMetadata.name,
fieldMetadata.isCustom,
),
} satisfies WorkspaceMigrationColumnDrop, } satisfies WorkspaceMigrationColumnDrop,
], ],
} satisfies WorkspaceMigrationTableAction, } satisfies WorkspaceMigrationTableAction,

View File

@@ -0,0 +1,14 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export interface CompositeProperty {
name: string;
description?: string;
type: FieldMetadataType;
hidden: 'input' | 'output' | true | false;
isRequired: boolean;
}
export interface CompositeType {
type: FieldMetadataType;
properties: CompositeProperty[];
}

View File

@@ -1,54 +0,0 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export interface FieldMetadataTargetColumnMapValue {
value: string;
}
export interface FieldMetadataTargetColumnMapLink {
label: string;
url: string;
}
export interface FieldMetadataTargetColumnMapCurrency {
amountMicros: string;
currencyCode: string;
}
export interface FieldMetadataTargetColumnMapFullName {
firstName: string;
lastName: string;
}
export type FieldMetadataTargetColumnMapAddress = {
addressStreet1: string;
addressStreet2: string;
addressCity: string;
addressState: string;
addressZipCode: string;
addressCountry: string;
addressLat: number;
addressLng: number;
};
type AllFieldMetadataTypes = {
[key: string]: string;
};
type FieldMetadataTypeMapping = {
[FieldMetadataType.LINK]: FieldMetadataTargetColumnMapLink;
[FieldMetadataType.CURRENCY]: FieldMetadataTargetColumnMapCurrency;
[FieldMetadataType.FULL_NAME]: FieldMetadataTargetColumnMapFullName;
[FieldMetadataType.ADDRESS]: FieldMetadataTargetColumnMapAddress;
};
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> = [
T,
] extends [keyof FieldMetadataTypeMapping]
? FieldMetadataTypeMapping[T]
: T extends 'default'
? AllFieldMetadataTypes
: FieldMetadataTargetColumnMapValue;
export type FieldMetadataTargetColumnMap<
T extends FieldMetadataType | 'default' = 'default',
> = TypeByFieldMetadata<T>;

View File

@@ -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 { 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'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
@@ -12,7 +11,6 @@ export interface FieldMetadataInterface<
type: FieldMetadataType; type: FieldMetadataType;
name: string; name: string;
label: string; label: string;
targetColumnMap: FieldMetadataTargetColumnMap<T>;
defaultValue?: FieldMetadataDefaultValue<T>; defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>; options?: FieldMetadataOptions<T>;
objectMetadataId: string; objectMetadataId: string;

View File

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

View File

@@ -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<T extends FieldMetadataType | 'default'>(
fieldMetadata: FieldMetadataInterface<T>,
ioptions?: ComputeColumnNameOptions,
): string;
// TODO: If we need to implement custom name logic for columns, we can do it here
export function computeColumnName<T extends FieldMetadataType | 'default'>(
fieldMetadataOrFieldName: FieldMetadataInterface<T> | 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<T>,
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)}`;
};

View File

@@ -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}`);
}
}

View File

@@ -28,7 +28,7 @@ import {
RelationMetadataType, RelationMetadataType,
RelationOnDeleteAction, RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; } 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 { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; 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'; 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'; } 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 { 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 { 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'; import { ObjectMetadataEntity } from './object-metadata.entity';
@@ -172,7 +173,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId, workspaceId,
[ [
{ {
name: computeCustomName( name: computeTableName(
relationToDelete.toObjectName, relationToDelete.toObjectName,
relationToDelete.toObjectMetadataIsCustom, relationToDelete.toObjectMetadataIsCustom,
), ),
@@ -180,9 +181,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.DROP, action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeCustomName( columnName: computeColumnName(
`${relationToDelete.toFieldMetadataName}Id`, relationToDelete.toFieldMetadataName,
relationToDelete.toFieldMetadataIsCustom, { isForeignKey: true },
), ),
} satisfies WorkspaceMigrationColumnDrop, } satisfies WorkspaceMigrationColumnDrop,
], ],
@@ -252,9 +253,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: 'id', name: 'id',
label: 'Id', label: 'Id',
targetColumnMap: {
value: 'id',
},
icon: 'Icon123', icon: 'Icon123',
description: 'Id', description: 'Id',
isNullable: false, isNullable: false,
@@ -269,9 +267,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
name: 'name', name: 'name',
label: 'Name', label: 'Name',
targetColumnMap: {
value: 'name',
},
icon: 'IconAbc', icon: 'IconAbc',
description: 'Name', description: 'Name',
isNullable: false, isNullable: false,
@@ -285,9 +280,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
name: 'createdAt', name: 'createdAt',
label: 'Creation date', label: 'Creation date',
targetColumnMap: {
value: 'createdAt',
},
icon: 'IconCalendar', icon: 'IconCalendar',
description: 'Creation date', description: 'Creation date',
isNullable: false, isNullable: false,
@@ -301,9 +293,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
name: 'updatedAt', name: 'updatedAt',
label: 'Update date', label: 'Update date',
targetColumnMap: {
value: 'updatedAt',
},
icon: 'IconCalendar', icon: 'IconCalendar',
description: 'Update date', description: 'Update date',
isNullable: false, isNullable: false,
@@ -318,9 +307,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.POSITION, type: FieldMetadataType.POSITION,
name: 'position', name: 'position',
label: 'Position', label: 'Position',
targetColumnMap: {
value: 'position',
},
icon: 'IconHierarchy2', icon: 'IconHierarchy2',
description: 'Position', description: 'Position',
isNullable: true, isNullable: true,
@@ -518,7 +504,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'activityTargets', name: 'activityTargets',
label: 'Activities', label: 'Activities',
targetColumnMap: {},
description: `Activities tied to the ${createdObjectMetadata.labelSingular}`, description: `Activities tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconCheckbox', icon: 'IconCheckbox',
isNullable: true, isNullable: true,
@@ -536,7 +521,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular, name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular, label: createdObjectMetadata.labelSingular,
targetColumnMap: {},
description: `ActivityTarget ${createdObjectMetadata.labelSingular}`, description: `ActivityTarget ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper', icon: 'IconBuildingSkyscraper',
isNullable: true, isNullable: true,
@@ -554,12 +538,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: {
value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
},
description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`, description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined, icon: undefined,
isNullable: true, isNullable: true,
@@ -621,7 +599,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'attachments', name: 'attachments',
label: 'Attachments', label: 'Attachments',
targetColumnMap: {},
description: `Attachments tied to the ${createdObjectMetadata.labelSingular}`, description: `Attachments tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconFileImport', icon: 'IconFileImport',
isNullable: true, isNullable: true,
@@ -639,7 +616,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular, name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular, label: createdObjectMetadata.labelSingular,
targetColumnMap: {},
description: `Attachment ${createdObjectMetadata.labelSingular}`, description: `Attachment ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper', icon: 'IconBuildingSkyscraper',
isNullable: true, isNullable: true,
@@ -657,12 +633,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: {
value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
},
description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`, description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined, icon: undefined,
isNullable: true, isNullable: true,
@@ -721,7 +691,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'events', name: 'events',
label: 'Events', label: 'Events',
targetColumnMap: {},
description: `Events tied to the ${createdObjectMetadata.labelSingular}`, description: `Events tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconFileImport', icon: 'IconFileImport',
isNullable: true, isNullable: true,
@@ -739,7 +708,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular, name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular, label: createdObjectMetadata.labelSingular,
targetColumnMap: {},
description: `Event ${createdObjectMetadata.labelSingular}`, description: `Event ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper', icon: 'IconBuildingSkyscraper',
isNullable: true, isNullable: true,
@@ -757,12 +725,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: {
value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
},
description: `Event ${createdObjectMetadata.labelSingular} id foreign key`, description: `Event ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined, icon: undefined,
isNullable: true, isNullable: true,
@@ -822,7 +784,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'favorites', name: 'favorites',
label: 'Favorites', label: 'Favorites',
targetColumnMap: {},
description: `Favorites tied to the ${createdObjectMetadata.labelSingular}`, description: `Favorites tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconHeart', icon: 'IconHeart',
isNullable: true, isNullable: true,
@@ -840,7 +801,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular, name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular, label: createdObjectMetadata.labelSingular,
targetColumnMap: {},
description: `Favorite ${createdObjectMetadata.labelSingular}`, description: `Favorite ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper', icon: 'IconBuildingSkyscraper',
isNullable: true, isNullable: true,
@@ -858,12 +818,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: {
value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
},
description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`, description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined, icon: undefined,
isNullable: true, isNullable: true,

View File

@@ -1,3 +1,4 @@
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { import {
@@ -5,7 +6,6 @@ import {
WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate, WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const buildWorkspaceMigrationsForCustomObject = ( export const buildWorkspaceMigrationsForCustomObject = (
@@ -26,10 +26,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: 'uuid', columnType: 'uuid',
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
@@ -41,10 +40,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
referencedTableName: computeObjectTargetTable(createdObjectMetadata), referencedTableName: computeObjectTargetTable(createdObjectMetadata),
referencedTableColumnName: 'id', referencedTableColumnName: 'id',
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
@@ -58,10 +56,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: 'uuid', columnType: 'uuid',
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
@@ -73,10 +70,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
referencedTableName: computeObjectTargetTable(createdObjectMetadata), referencedTableName: computeObjectTargetTable(createdObjectMetadata),
referencedTableColumnName: 'id', referencedTableColumnName: 'id',
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
@@ -90,10 +86,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: 'uuid', columnType: 'uuid',
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
@@ -105,10 +100,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
referencedTableName: computeObjectTargetTable(createdObjectMetadata), referencedTableName: computeObjectTargetTable(createdObjectMetadata),
referencedTableColumnName: 'id', referencedTableColumnName: 'id',
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
@@ -122,10 +116,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: 'uuid', columnType: 'uuid',
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
@@ -137,10 +130,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
referencedTableName: computeObjectTargetTable(createdObjectMetadata), referencedTableName: computeObjectTargetTable(createdObjectMetadata),
referencedTableColumnName: 'id', referencedTableColumnName: 'id',
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,

View File

@@ -1,12 +1,12 @@
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { import {
WorkspaceMigrationTableAction, WorkspaceMigrationTableAction,
WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate, WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
const buildCommentForRemoteObjectForeignKey = async ( const buildCommentForRemoteObjectForeignKey = async (
@@ -65,10 +65,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: remoteTablePrimaryKeyColumnType, columnType: remoteTablePrimaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
@@ -80,10 +79,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: remoteTablePrimaryKeyColumnType, columnType: remoteTablePrimaryKeyColumnType,
}, },
], ],
@@ -110,10 +108,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: remoteTablePrimaryKeyColumnType, columnType: remoteTablePrimaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
@@ -125,10 +122,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: remoteTablePrimaryKeyColumnType, columnType: remoteTablePrimaryKeyColumnType,
}, },
], ],
@@ -155,10 +151,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: remoteTablePrimaryKeyColumnType, columnType: remoteTablePrimaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
@@ -170,10 +165,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: remoteTablePrimaryKeyColumnType, columnType: remoteTablePrimaryKeyColumnType,
}, },
], ],
@@ -200,10 +194,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: remoteTablePrimaryKeyColumnType, columnType: remoteTablePrimaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
@@ -215,10 +208,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName( columnName: computeColumnName(createdObjectMetadata.nameSingular, {
createdObjectMetadata.nameSingular, isForeignKey: true,
false, }),
)}Id`,
columnType: remoteTablePrimaryKeyColumnType, columnType: remoteTablePrimaryKeyColumnType,
}, },
], ],

View File

@@ -19,9 +19,7 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceMigrationColumnActionType } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationColumnActionType } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { createCustomColumnName } from 'src/engine/utils/create-custom-column-name.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { createRelationForeignKeyColumnName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-column-name.util';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import { import {
@@ -57,11 +55,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
// NOTE: this logic is called to create relation through metadata graphql endpoint (so only for custom field relations) // NOTE: this logic is called to create relation through metadata graphql endpoint (so only for custom field relations)
const isCustom = true; const isCustom = true;
const baseColumnName = `${camelCase(relationMetadataInput.toName)}Id`; const columnName = `${camelCase(relationMetadataInput.toName)}Id`;
const foreignKeyColumnName = createRelationForeignKeyColumnName(
relationMetadataInput.toName,
isCustom,
);
const fromId = uuidV4(); const fromId = uuidV4();
const toId = uuidV4(); const toId = uuidV4();
@@ -79,11 +73,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
isCustom, isCustom,
toId, toId,
), ),
this.createForeignKeyFieldMetadata( this.createForeignKeyFieldMetadata(relationMetadataInput, columnName),
relationMetadataInput,
baseColumnName,
foreignKeyColumnName,
),
]); ]);
const createdRelationMetadata = await super.createOne({ const createdRelationMetadata = await super.createOne({
@@ -95,7 +85,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
await this.createWorkspaceCustomMigration( await this.createWorkspaceCustomMigration(
relationMetadataInput, relationMetadataInput,
objectMetadataMap, objectMetadataMap,
foreignKeyColumnName, columnName,
); );
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
@@ -170,7 +160,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
private async createWorkspaceCustomMigration( private async createWorkspaceCustomMigration(
relationMetadataInput: CreateRelationInput, relationMetadataInput: CreateRelationInput,
objectMetadataMap: { [key: string]: ObjectMetadataEntity }, objectMetadataMap: { [key: string]: ObjectMetadataEntity },
foreignKeyColumnName: string, columnName: string,
) { ) {
await this.workspaceMigrationService.createCustomMigration( await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-${relationMetadataInput.fromName}`), generateMigrationName(`create-${relationMetadataInput.fromName}`),
@@ -185,7 +175,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: foreignKeyColumnName, columnName,
columnType: 'uuid', columnType: 'uuid',
isNullable: true, isNullable: true,
}, },
@@ -200,7 +190,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: foreignKeyColumnName, columnName,
referencedTableName: computeObjectTargetTable( referencedTableName: computeObjectTargetTable(
objectMetadataMap[relationMetadataInput.fromObjectMetadataId], objectMetadataMap[relationMetadataInput.fromObjectMetadataId],
), ),
@@ -229,12 +219,6 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
description: relationMetadataInput[`${relationDirection}Description`], description: relationMetadataInput[`${relationDirection}Description`],
icon: relationMetadataInput[`${relationDirection}Icon`], icon: relationMetadataInput[`${relationDirection}Icon`],
isCustom: true, isCustom: true,
targetColumnMap:
relationDirection === 'to'
? isCustom
? createCustomColumnName(relationMetadataInput.toName)
: relationMetadataInput.toName
: {},
isActive: true, isActive: true,
isNullable: true, isNullable: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
@@ -246,18 +230,16 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
private createForeignKeyFieldMetadata( private createForeignKeyFieldMetadata(
relationMetadataInput: CreateRelationInput, relationMetadataInput: CreateRelationInput,
baseColumnName: string, columnName: string,
foreignKeyColumnName: string,
) { ) {
return { return {
name: baseColumnName, name: columnName,
label: `${relationMetadataInput.toLabel} Foreign Key`, label: `${relationMetadataInput.toLabel} Foreign Key`,
description: relationMetadataInput.toDescription description: relationMetadataInput.toDescription
? `${relationMetadataInput.toDescription} Foreign Key` ? `${relationMetadataInput.toDescription} Foreign Key`
: undefined, : undefined,
icon: undefined, icon: undefined,
isCustom: true, isCustom: true,
targetColumnMap: { value: foreignKeyColumnName },
isActive: true, isActive: true,
isNullable: true, isNullable: true,
isSystem: true, isSystem: true,

View File

@@ -1,15 +0,0 @@
import { createCustomColumnName } from 'src/engine/utils/create-custom-column-name.util';
import { camelCase } from 'src/utils/camel-case';
export const createRelationForeignKeyColumnName = (
name: string,
isCustom: boolean,
) => {
const baseColumnName = `${camelCase(name)}Id`;
const foreignKeyColumnName = isCustom
? createCustomColumnName(baseColumnName)
: baseColumnName;
return foreignKeyColumnName;
};

View File

@@ -12,6 +12,7 @@ import {
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; 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 { 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 { 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 = export type BasicFieldMetadataType =
| FieldMetadataType.UUID | FieldMetadataType.UUID
@@ -33,29 +34,32 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
protected handleCreateAction( protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>, fieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
options?: WorkspaceColumnActionOptions, options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate { ): WorkspaceMigrationColumnCreate[] {
const columnName = computeColumnName(fieldMetadata);
const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue; const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue); const serializedDefaultValue = serializeDefaultValue(defaultValue);
return { return [
{
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value, columnName,
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
isNullable: fieldMetadata.isNullable, isNullable: fieldMetadata.isNullable,
defaultValue: serializedDefaultValue, defaultValue: serializedDefaultValue,
}; },
];
} }
protected handleAlterAction( protected handleAlterAction(
currentFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>, currentFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
alteredFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>, alteredFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
options?: WorkspaceColumnActionOptions, options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter { ): WorkspaceMigrationColumnAlter[] {
const currentColumnName = computeColumnName(currentFieldMetadata);
const alteredColumnName = computeColumnName(alteredFieldMetadata);
const defaultValue = const defaultValue =
alteredFieldMetadata.defaultValue ?? options?.defaultValue; alteredFieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue); const serializedDefaultValue = serializeDefaultValue(defaultValue);
const currentColumnName = currentFieldMetadata.targetColumnMap.value;
const alteredColumnName = alteredFieldMetadata.targetColumnMap.value;
if (!currentColumnName || !alteredColumnName) { if (!currentColumnName || !alteredColumnName) {
this.logger.error( this.logger.error(
@@ -66,13 +70,16 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
); );
} }
return { return [
{
action: WorkspaceMigrationColumnActionType.ALTER, action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: { currentColumnDefinition: {
columnName: currentColumnName, columnName: currentColumnName,
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type), columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
isNullable: currentFieldMetadata.isNullable, isNullable: currentFieldMetadata.isNullable,
defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue), defaultValue: serializeDefaultValue(
currentFieldMetadata.defaultValue,
),
}, },
alteredColumnDefinition: { alteredColumnDefinition: {
columnName: alteredColumnName, columnName: alteredColumnName,
@@ -80,6 +87,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
isNullable: alteredFieldMetadata.isNullable, isNullable: alteredFieldMetadata.isNullable,
defaultValue: serializedDefaultValue, defaultValue: serializedDefaultValue,
}, },
}; },
];
} }
} }

View File

@@ -26,7 +26,7 @@ export class ColumnActionAbstractFactory<
currentFieldMetadata: FieldMetadataInterface<T> | undefined, currentFieldMetadata: FieldMetadataInterface<T> | undefined,
alteredFieldMetadata: FieldMetadataInterface<T>, alteredFieldMetadata: FieldMetadataInterface<T>,
options?: WorkspaceColumnActionOptions, options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAction { ): WorkspaceMigrationColumnAction[] {
switch (action) { switch (action) {
case WorkspaceMigrationColumnActionType.CREATE: case WorkspaceMigrationColumnActionType.CREATE:
return this.handleCreateAction(alteredFieldMetadata, options); return this.handleCreateAction(alteredFieldMetadata, options);
@@ -52,7 +52,7 @@ export class ColumnActionAbstractFactory<
protected handleCreateAction( protected handleCreateAction(
_fieldMetadata: FieldMetadataInterface<T>, _fieldMetadata: FieldMetadataInterface<T>,
_options?: WorkspaceColumnActionOptions, _options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate { ): WorkspaceMigrationColumnCreate[] {
throw new Error('handleCreateAction method not implemented.'); throw new Error('handleCreateAction method not implemented.');
} }
@@ -60,7 +60,7 @@ export class ColumnActionAbstractFactory<
_currentFieldMetadata: FieldMetadataInterface<T>, _currentFieldMetadata: FieldMetadataInterface<T>,
_alteredFieldMetadata: FieldMetadataInterface<T>, _alteredFieldMetadata: FieldMetadataInterface<T>,
_options?: WorkspaceColumnActionOptions, _options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter { ): WorkspaceMigrationColumnAlter[] {
throw new Error('handleAlterAction method not implemented.'); throw new Error('handleAlterAction method not implemented.');
} }
} }

View File

@@ -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<CompositeFieldMetadataType> {
protected readonly logger = new Logger(CompositeColumnActionFactory.name);
protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<CompositeFieldMetadataType>,
): 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<CompositeFieldMetadataType>,
alteredFieldMetadata: FieldMetadataInterface<CompositeFieldMetadataType>,
): 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;
}
}

View File

@@ -12,6 +12,7 @@ import {
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; 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 { 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 { 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 = export type EnumFieldMetadataType =
| FieldMetadataType.RATING | FieldMetadataType.RATING
@@ -25,29 +26,34 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
protected handleCreateAction( protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>, fieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
options: WorkspaceColumnActionOptions, options: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate { ): WorkspaceMigrationColumnCreate[] {
const columnName = computeColumnName(fieldMetadata);
const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue; const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue); const serializedDefaultValue = serializeDefaultValue(defaultValue);
const enumOptions = fieldMetadata.options const enumOptions = fieldMetadata.options
? [...fieldMetadata.options.map((option) => option.value)] ? [...fieldMetadata.options.map((option) => option.value)]
: undefined; : undefined;
return { return [
{
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value, columnName,
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
enum: enumOptions, enum: enumOptions,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: fieldMetadata.isNullable, isNullable: fieldMetadata.isNullable,
defaultValue: serializedDefaultValue, defaultValue: serializedDefaultValue,
}; },
];
} }
protected handleAlterAction( protected handleAlterAction(
currentFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>, currentFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
alteredFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>, alteredFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
options: WorkspaceColumnActionOptions, options: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter { ): WorkspaceMigrationColumnAlter[] {
const currentColumnName = computeColumnName(currentFieldMetadata);
const alteredColumnName = computeColumnName(alteredFieldMetadata);
const defaultValue = const defaultValue =
alteredFieldMetadata.defaultValue ?? options?.defaultValue; alteredFieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue); const serializedDefaultValue = serializeDefaultValue(defaultValue);
@@ -71,8 +77,6 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
}), }),
] ]
: undefined; : undefined;
const currentColumnName = currentFieldMetadata.targetColumnMap.value;
const alteredColumnName = alteredFieldMetadata.targetColumnMap.value;
if (!currentColumnName || !alteredColumnName) { if (!currentColumnName || !alteredColumnName) {
this.logger.error( this.logger.error(
@@ -83,7 +87,8 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
); );
} }
return { return [
{
action: WorkspaceMigrationColumnActionType.ALTER, action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: { currentColumnDefinition: {
columnName: currentColumnName, columnName: currentColumnName,
@@ -93,7 +98,9 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
: undefined, : undefined,
isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT, isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: currentFieldMetadata.isNullable, isNullable: currentFieldMetadata.isNullable,
defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue), defaultValue: serializeDefaultValue(
currentFieldMetadata.defaultValue,
),
}, },
alteredColumnDefinition: { alteredColumnDefinition: {
columnName: alteredColumnName, columnName: alteredColumnName,
@@ -103,6 +110,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
isNullable: alteredFieldMetadata.isNullable, isNullable: alteredFieldMetadata.isNullable,
defaultValue: serializedDefaultValue, defaultValue: serializedDefaultValue,
}, },
}; },
];
} }
} }

View File

@@ -1,7 +1,9 @@
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; 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'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
export const workspaceColumnActionFactories = [ export const workspaceColumnActionFactories = [
BasicColumnActionFactory, BasicColumnActionFactory,
EnumColumnActionFactory, EnumColumnActionFactory,
CompositeColumnActionFactory,
]; ];

View File

@@ -17,5 +17,5 @@ export interface WorkspaceColumnActionFactory<
currentFieldMetadata: FieldMetadataInterface<T> | undefined, currentFieldMetadata: FieldMetadataInterface<T> | undefined,
alteredFieldMetadata: FieldMetadataInterface<T>, alteredFieldMetadata: FieldMetadataInterface<T>,
options?: WorkspaceColumnActionOptions, options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAction; ): WorkspaceMigrationColumnAction[];
} }

View File

@@ -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 { 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 { 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 { import {
WorkspaceMigrationColumnAction, WorkspaceMigrationColumnAction,
WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; } 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 { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
import { compositeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; 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() @Injectable()
export class WorkspaceMigrationFactory { export class WorkspaceMigrationFactory {
@@ -28,6 +27,7 @@ export class WorkspaceMigrationFactory {
constructor( constructor(
private readonly basicColumnActionFactory: BasicColumnActionFactory, private readonly basicColumnActionFactory: BasicColumnActionFactory,
private readonly enumColumnActionFactory: EnumColumnActionFactory, private readonly enumColumnActionFactory: EnumColumnActionFactory,
private readonly compositeColumnActionFactory: CompositeColumnActionFactory,
) { ) {
this.factoriesMap = new Map< this.factoriesMap = new Map<
FieldMetadataType, FieldMetadataType,
@@ -80,6 +80,19 @@ export class WorkspaceMigrationFactory {
FieldMetadataType.MULTI_SELECT, FieldMetadataType.MULTI_SELECT,
{ factory: this.enumColumnActionFactory }, { 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}`); 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 const columnActions = this.createColumnAction(
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(
action, action,
currentFieldMetadata, currentFieldMetadata,
alteredFieldMetadata, alteredFieldMetadata,
); );
return [columnAction]; return columnActions;
} }
private createColumnAction( private createColumnAction(
@@ -162,7 +147,7 @@ export class WorkspaceMigrationFactory {
| WorkspaceMigrationColumnActionType.ALTER, | WorkspaceMigrationColumnActionType.ALTER,
currentFieldMetadata: FieldMetadataInterface | undefined, currentFieldMetadata: FieldMetadataInterface | undefined,
alteredFieldMetadata: FieldMetadataInterface, alteredFieldMetadata: FieldMetadataInterface,
): WorkspaceMigrationColumnAction { ): WorkspaceMigrationColumnAction[] {
const { factory, options } = const { factory, options } =
this.factoriesMap.get(alteredFieldMetadata.type) ?? {}; this.factoriesMap.get(alteredFieldMetadata.type) ?? {};

View File

@@ -17,9 +17,6 @@ describe('deduceRelationDirection', () => {
name: 'field_name', name: 'field_name',
label: 'Field Name', label: 'Field Name',
description: 'Field Description', description: 'Field Description',
targetColumnMap: {
default: 'default_column',
},
}; };
const relationMetadata = { const relationMetadata = {
@@ -48,9 +45,6 @@ describe('deduceRelationDirection', () => {
name: 'field_name', name: 'field_name',
label: 'Field Name', label: 'Field Name',
description: 'Field Description', description: 'Field Description',
targetColumnMap: {
default: 'default_column',
},
}; };
const relationMetadata = { const relationMetadata = {
@@ -78,9 +72,6 @@ describe('deduceRelationDirection', () => {
name: 'field_name', name: 'field_name',
label: 'Field Name', label: 'Field Name',
description: 'Field Description', description: 'Field Description',
targetColumnMap: {
default: 'default_column',
},
}; };
const relationMetadata = { const relationMetadata = {

View File

@@ -1,12 +1,9 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; 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 = ( export const computeObjectTargetTable = (
objectMetadata: ObjectMetadataInterface, objectMetadata: ObjectMetadataInterface,
) => { ) => {
return computeCustomName( return computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom);
objectMetadata.nameSingular,
objectMetadata.isCustom,
);
}; };

View File

@@ -1,5 +1,5 @@
export const customNamePrefix = '_'; export const customNamePrefix = '_';
export const computeCustomName = (name: string, isCustom: boolean) => { export const computeTableName = (name: string, isCustom: boolean) => {
return isCustom ? `${customNamePrefix}${name}` : name; return isCustom ? `${customNamePrefix}${name}` : name;
}; };

View File

@@ -1,3 +0,0 @@
export const createCustomColumnName = (name: string) => {
return `_${name}`;
};

View File

@@ -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 { WorkspaceNullableFixer } from './workspace-nullable.fixer';
import { WorkspaceDefaultValueFixer } from './workspace-default-value.fixer'; import { WorkspaceDefaultValueFixer } from './workspace-default-value.fixer';
import { WorkspaceTypeFixer } from './workspace-type.fixer'; import { WorkspaceTypeFixer } from './workspace-type.fixer';
import { WorkspaceTargetColumnMapFixer } from './workspace-target-column-map.fixer';
export const workspaceFixers = [ export const workspaceFixers = [
WorkspaceNullableFixer, WorkspaceNullableFixer,
WorkspaceDefaultValueFixer, WorkspaceDefaultValueFixer,
WorkspaceTypeFixer, WorkspaceTypeFixer,
WorkspaceTargetColumnMapFixer, WorkspaceMissingColumnFixer,
]; ];

View File

@@ -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<WorkspaceHealthIssueType.MISSING_COLUMN> {
constructor(
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
) {
super(WorkspaceHealthIssueType.MISSING_COLUMN);
}
async createWorkspaceMigrations(
manager: EntityManager,
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.MISSING_COLUMN>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
if (issues.length <= 0) {
return [];
}
return this.fixMissingColumnIssues(objectMetadataCollection, issues);
}
private async fixMissingColumnIssues(
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.MISSING_COLUMN>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
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,
);
}
}

View File

@@ -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<WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
if (issues.length <= 0) {
return [];
}
return this.fixStructureTargetColumnMapIssues(
manager,
objectMetadataCollection,
issues,
);
}
async createMetadataUpdates(
manager: EntityManager,
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID>[],
): Promise<CompareEntity<FieldMetadataEntity>[]> {
if (issues.length <= 0) {
return [];
}
return this.fixMetadataTargetColumnMapIssues(manager, issues);
}
private async fixStructureTargetColumnMapIssues(
manager: EntityManager,
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrationCollection: Partial<WorkspaceMigrationEntity>[] =
[];
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<WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID>[],
): Promise<CompareEntity<FieldMetadataEntity>[]> {
const fieldMetadataRepository = manager.getRepository(FieldMetadataEntity);
const updatedEntities: CompareEntity<FieldMetadataEntity>[] = [];
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;
}
}

View File

@@ -2,5 +2,5 @@ export enum WorkspaceHealthFixKind {
Nullable = 'nullable', Nullable = 'nullable',
Type = 'type', Type = 'type',
DefaultValue = 'default-value', DefaultValue = 'default-value',
TargetColumnMap = 'target-column-map', MissingColumn = 'missing-column',
} }

View File

@@ -15,8 +15,7 @@ export enum WorkspaceHealthIssueType {
MISSING_FOREIGN_KEY = 'MISSING_FOREIGN_KEY', MISSING_FOREIGN_KEY = 'MISSING_FOREIGN_KEY',
MISSING_COMPOSITE_TYPE = 'MISSING_COMPOSITE_TYPE', MISSING_COMPOSITE_TYPE = 'MISSING_COMPOSITE_TYPE',
COLUMN_NAME_SHOULD_NOT_BE_PREFIXED = 'COLUMN_NAME_SHOULD_NOT_BE_PREFIXED', 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_NOT_BE_CUSTOM = 'COLUMN_NAME_SHOULD_NOT_BE_CUSTOM',
COLUMN_NAME_SHOULD_BE_CUSTOM = 'COLUMN_NAME_SHOULD_BE_CUSTOM',
COLUMN_OBJECT_REFERENCE_INVALID = 'COLUMN_OBJECT_REFERENCE_INVALID', COLUMN_OBJECT_REFERENCE_INVALID = 'COLUMN_OBJECT_REFERENCE_INVALID',
COLUMN_NAME_NOT_VALID = 'COLUMN_NAME_NOT_VALID', COLUMN_NAME_NOT_VALID = 'COLUMN_NAME_NOT_VALID',
COLUMN_TYPE_NOT_VALID = 'COLUMN_TYPE_NOT_VALID', COLUMN_TYPE_NOT_VALID = 'COLUMN_TYPE_NOT_VALID',
@@ -58,8 +57,7 @@ export type WorkspaceColumnIssueTypes =
| WorkspaceHealthIssueType.MISSING_FOREIGN_KEY | WorkspaceHealthIssueType.MISSING_FOREIGN_KEY
| WorkspaceHealthIssueType.MISSING_COMPOSITE_TYPE | WorkspaceHealthIssueType.MISSING_COMPOSITE_TYPE
| WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED
| WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM
| WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM
| WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID | WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID
| WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID | WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID
| WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID | WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID
@@ -75,6 +73,7 @@ export interface WorkspaceHealthColumnIssue<
type: T; type: T;
fieldMetadata: FieldMetadataEntity; fieldMetadata: FieldMetadataEntity;
columnStructure?: WorkspaceTableStructure; columnStructure?: WorkspaceTableStructure;
columnStructures?: WorkspaceTableStructure[];
message: string; message: string;
} }

View File

@@ -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 { 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 { 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 { FieldMetadataDefaultValueFunctionNames } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@Injectable() @Injectable()
export class DatabaseStructureService { export class DatabaseStructureService {
@@ -156,22 +157,40 @@ export class DatabaseStructureService {
return results.length >= 1; return results.length >= 1;
} }
getPostgresDataType(fieldMetadata: FieldMetadataEntity): string { getPostgresDataTypes(fieldMetadata: FieldMetadataEntity): string[] {
const typeORMType = fieldMetadataTypeToColumnType(fieldMetadata.type);
const mainDataSource = this.typeORMService.getMainDataSource(); const mainDataSource = this.typeORMService.getMainDataSource();
const normalizer = (type: FieldMetadataType, columnName: string) => {
const typeORMType = fieldMetadataTypeToColumnType(type);
// Compute enum name to compare data type properly // Compute enum name to compare data type properly
if (typeORMType === 'enum') { if (typeORMType === 'enum') {
const objectName = fieldMetadata.object?.nameSingular; const objectName = fieldMetadata.object?.nameSingular;
const prefix = fieldMetadata.isCustom ? '_' : ''; const prefix = fieldMetadata.isCustom ? '_' : '';
const fieldName = fieldMetadata.name;
return `${objectName}_${prefix}${fieldName}_enum`; return `${objectName}_${prefix}${columnName}_enum`;
} }
return mainDataSource.driver.normalizeType({ return mainDataSource.driver.normalizeType({
type: typeORMType, 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 [normalizer(fieldMetadata.type, fieldMetadata.name)];
} }
getFieldMetadataTypeFromPostgresDataType( getFieldMetadataTypeFromPostgresDataType(
@@ -207,18 +226,23 @@ export class DatabaseStructureService {
return null; return null;
} }
getPostgresDefault( getPostgresDefaults(
fieldMetadataType: FieldMetadataType, fieldMetadataType: FieldMetadataType,
defaultValue: initialDefaultValue:
| FieldMetadataDefaultValue | FieldMetadataDefaultValue
// Old format for default values // Old format for default values
// TODO: Should be removed once all default values are migrated // TODO: Should be removed once all default values are migrated
| { type: FieldMetadataDefaultValueFunctionNames } | { type: FieldMetadataDefaultValueFunctionNames }
| null, | null,
): string | null | undefined { ): (string | null | undefined)[] {
const typeORMType = fieldMetadataTypeToColumnType( const normalizer = (
fieldMetadataType, type: FieldMetadataType,
) as ColumnType; defaultValue:
| FieldMetadataDefaultValue
| { type: FieldMetadataDefaultValueFunctionNames }
| null,
) => {
const typeORMType = fieldMetadataTypeToColumnType(type) as ColumnType;
const mainDataSource = this.typeORMService.getMainDataSource(); const mainDataSource = this.typeORMService.getMainDataSource();
let value: any = let value: any =
@@ -258,6 +282,28 @@ export class DatabaseStructureService {
isArray: false, isArray: false,
// Workaround to use normalizeDefault without a complete ColumnMetadata object // Workaround to use normalizeDefault without a complete ColumnMetadata object
} as ColumnMetadata); } 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,
),
);
}
return [normalizer(fieldMetadataType, initialDefaultValue)];
} }
private computeFunctionDefaultValue( private computeFunctionDefaultValue(

View File

@@ -1,7 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import isEqual from 'lodash.isequal';
import { import {
WorkspaceHealthIssue, WorkspaceHealthIssue,
WorkspaceHealthIssueType, WorkspaceHealthIssueType,
@@ -17,7 +15,6 @@ import {
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; 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 { 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 { 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 { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util';
import { import {
EnumFieldMetadataUnionType, EnumFieldMetadataUnionType,
@@ -25,10 +22,13 @@ import {
} from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; } 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 { 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 { 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 { customNamePrefix } from 'src/engine/utils/compute-table-name.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 { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.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() @Injectable()
export class FieldMetadataHealthService { export class FieldMetadataHealthService {
@@ -50,49 +50,6 @@ export class FieldMetadataHealthService {
continue; continue;
} }
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeFieldMetadataCollection =
compositeDefinitions.get(fieldMetadata.type)?.(fieldMetadata) ?? [];
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( const fieldIssues = await this.healthCheckField(
tableName, tableName,
workspaceTableColumns, workspaceTableColumns,
@@ -102,7 +59,6 @@ export class FieldMetadataHealthService {
issues.push(...fieldIssues); issues.push(...fieldIssues);
} }
}
return issues; return issues;
} }
@@ -139,42 +95,68 @@ export class FieldMetadataHealthService {
workspaceTableColumns: WorkspaceTableStructure[], workspaceTableColumns: WorkspaceTableStructure[],
fieldMetadata: FieldMetadataEntity, fieldMetadata: FieldMetadataEntity,
): WorkspaceHealthIssue[] { ): WorkspaceHealthIssue[] {
const dataTypes =
this.databaseStructureService.getPostgresDataTypes(fieldMetadata);
const issues: WorkspaceHealthIssue[] = []; const issues: WorkspaceHealthIssue[] = [];
const columnName = fieldMetadata.targetColumnMap.value; let columnNames: string[] = [];
const dataType = if (isCompositeFieldMetadataType(fieldMetadata.type)) {
this.databaseStructureService.getPostgresDataType(fieldMetadata); 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.type,
fieldMetadata.defaultValue, fieldMetadata.defaultValue,
); );
// Check if column exist in database // Check if column exist in database
const columnStructure = workspaceTableColumns.find( const columnStructureMap = workspaceTableColumns.reduce(
(tableDefinition) => tableDefinition.columnName === columnName, (acc, workspaceTableColumn) => {
const columnName = workspaceTableColumn.columnName;
if (columnNames.includes(columnName)) {
acc[columnName] = workspaceTableColumn;
}
return acc;
},
{} as { [key: string]: WorkspaceTableStructure },
); );
for (const [index, columnName] of columnNames.entries()) {
const columnStructure = columnStructureMap[columnName];
if (!columnStructure) { if (!columnStructure) {
issues.push({ issues.push({
type: WorkspaceHealthIssueType.MISSING_COLUMN, type: WorkspaceHealthIssueType.MISSING_COLUMN,
fieldMetadata, fieldMetadata,
columnStructure, columnStructures: workspaceTableColumns,
message: `Column ${columnName} not found in table ${tableName}`, message: `Column ${columnName} not found in table ${tableName}`,
}); });
return issues; continue;
} }
const columnDefaultValue = columnStructure.columnDefault?.split('::')?.[0]; const columnDefaultValue =
columnStructure.columnDefault?.split('::')?.[0];
// Check if column data type is the same // Check if column data type is the same
if (columnStructure.dataType !== dataType) { if (!dataTypes[index] || columnStructure.dataType !== dataTypes[index]) {
issues.push({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT, type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT,
fieldMetadata, fieldMetadata,
columnStructure, columnStructure,
message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataType}"`, message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataTypes[index]}"`,
}); });
} }
@@ -204,14 +186,15 @@ export class FieldMetadataHealthService {
} }
} }
if (columnDefaultValue !== defaultValue) { if (columnDefaultValue !== defaultValues[index]) {
issues.push({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT, type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
fieldMetadata, fieldMetadata,
columnStructure, columnStructure,
message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValue}"`, message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValues[index]}"`,
}); });
} }
}
return issues; return issues;
} }
@@ -221,14 +204,9 @@ export class FieldMetadataHealthService {
fieldMetadata: FieldMetadataEntity, fieldMetadata: FieldMetadataEntity,
): WorkspaceHealthIssue[] { ): WorkspaceHealthIssue[] {
const issues: WorkspaceHealthIssue[] = []; const issues: WorkspaceHealthIssue[] = [];
const columnName = fieldMetadata.targetColumnMap.value; const columnName = fieldMetadata.name;
const targetColumnMapIssue = this.targetColumnMapCheck(fieldMetadata);
const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata); const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata);
if (targetColumnMapIssue) {
issues.push(targetColumnMapIssue);
}
if (fieldMetadata.name.startsWith(customNamePrefix)) { if (fieldMetadata.name.startsWith(customNamePrefix)) {
issues.push({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED, 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({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM, type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM,
fieldMetadata, 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({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID,
fieldMetadata, 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; 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( private defaultValueHealthCheck(
fieldMetadata: FieldMetadataEntity, fieldMetadata: FieldMetadataEntity,
): WorkspaceHealthIssue[] { ): WorkspaceHealthIssue[] {

View File

@@ -20,10 +20,10 @@ import {
deduceRelationDirection, deduceRelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util'; } from 'src/engine/utils/deduce-relation-direction.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { 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 { 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 { 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 { 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() @Injectable()
export class RelationMetadataHealthService { export class RelationMetadataHealthService {
@@ -145,11 +145,7 @@ export class RelationMetadataHealthService {
return []; return [];
} }
const isCustom = toFieldMetadata.isCustom ?? false; const foreignKeyColumnName = `${camelCase(toFieldMetadata.name)}Id`;
const foreignKeyColumnName = createRelationForeignKeyColumnName(
toFieldMetadata.name,
isCustom,
);
const relationColumn = workspaceTableColumns.find( const relationColumn = workspaceTableColumns.find(
(column) => column.columnName === foreignKeyColumnName, (column) => column.columnName === foreignKeyColumnName,
); );

View File

@@ -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 { 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 { 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 { 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 { 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() @Injectable()
export class WorkspaceFixService { export class WorkspaceFixService {
@@ -19,7 +19,7 @@ export class WorkspaceFixService {
private readonly workspaceNullableFixer: WorkspaceNullableFixer, private readonly workspaceNullableFixer: WorkspaceNullableFixer,
private readonly workspaceDefaultValueFixer: WorkspaceDefaultValueFixer, private readonly workspaceDefaultValueFixer: WorkspaceDefaultValueFixer,
private readonly workspaceTypeFixer: WorkspaceTypeFixer, private readonly workspaceTypeFixer: WorkspaceTypeFixer,
private readonly workspaceTargetColumnMapFixer: WorkspaceTargetColumnMapFixer, private readonly workspaceMissingColumnFixer: WorkspaceMissingColumnFixer,
) {} ) {}
async createWorkspaceMigrations( async createWorkspaceMigrations(
@@ -57,11 +57,11 @@ export class WorkspaceFixService {
filteredIssues, filteredIssues,
); );
} }
case WorkspaceHealthFixKind.TargetColumnMap: { case WorkspaceHealthFixKind.MissingColumn: {
const filteredIssues = const filteredIssues =
this.workspaceTargetColumnMapFixer.filterIssues(issues); this.workspaceMissingColumnFixer.filterIssues(issues);
return this.workspaceTargetColumnMapFixer.createWorkspaceMigrations( return this.workspaceMissingColumnFixer.createWorkspaceMigrations(
manager, manager,
objectMetadataCollection, objectMetadataCollection,
filteredIssues, filteredIssues,
@@ -77,19 +77,10 @@ export class WorkspaceFixService {
manager: EntityManager, manager: EntityManager,
objectMetadataCollection: ObjectMetadataEntity[], objectMetadataCollection: ObjectMetadataEntity[],
type: WorkspaceHealthFixKind, type: WorkspaceHealthFixKind,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
issues: WorkspaceHealthIssue[], issues: WorkspaceHealthIssue[],
): Promise<CompareEntity<unknown>[]> { ): Promise<CompareEntity<unknown>[]> {
switch (type) { switch (type) {
case WorkspaceHealthFixKind.TargetColumnMap: {
const filteredIssues =
this.workspaceTargetColumnMapFixer.filterIssues(issues);
return this.workspaceTargetColumnMapFixer.createMetadataUpdates(
manager,
objectMetadataCollection,
filteredIssues,
);
}
case WorkspaceHealthFixKind.DefaultValue: { case WorkspaceHealthFixKind.DefaultValue: {
const filteredIssues = const filteredIssues =
this.workspaceDefaultValueFixer.filterIssues(issues); this.workspaceDefaultValueFixer.filterIssues(issues);

View File

@@ -17,9 +17,3 @@ export const isWorkspaceHealthDefaultValueIssue = (
): type is WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT => { ): type is WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT => {
return type === 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;
};

View File

@@ -16,7 +16,6 @@ describe('WorkspaceFieldComparator', () => {
type: 'TEXT', type: 'TEXT',
name: 'DefaultFieldName', name: 'DefaultFieldName',
label: 'Default Field Label', label: 'Default Field Label',
targetColumnMap: 'default_column',
defaultValue: null, defaultValue: null,
description: 'Default description', description: 'Default description',
isCustom: false, isCustom: false,

View File

@@ -25,7 +25,7 @@ const commonFieldPropertiesToIgnore = [
'options', 'options',
]; ];
const fieldPropertiesToStringify = ['targetColumnMap', 'defaultValue'] as const; const fieldPropertiesToStringify = ['defaultValue'] as const;
@Injectable() @Injectable()
export class WorkspaceFieldComparator { export class WorkspaceFieldComparator {

View File

@@ -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 { 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 { 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 { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
import { TypedReflect } from 'src/utils/typed-reflect'; import { TypedReflect } from 'src/utils/typed-reflect';
import { createDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; import { createDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
@@ -70,7 +69,6 @@ function generateFieldMetadata<T extends FieldMetadataType>(
isSystem: boolean, isSystem: boolean,
gate: GateDecoratorParams | undefined = undefined, gate: GateDecoratorParams | undefined = undefined,
): ReflectFieldMetadata[string] { ): ReflectFieldMetadata[string] {
const targetColumnMap = generateTargetColumnMap(params.type, false, fieldKey);
const defaultValue = (params.defaultValue ?? const defaultValue = (params.defaultValue ??
generateDefaultValue( generateDefaultValue(
params.type, params.type,
@@ -79,7 +77,6 @@ function generateFieldMetadata<T extends FieldMetadataType>(
return { return {
name: fieldKey, name: fieldKey,
...params, ...params,
targetColumnMap,
isNullable: params.type === FieldMetadataType.RELATION ? true : isNullable, isNullable: params.type === FieldMetadataType.RELATION ? true : isNullable,
isSystem, isSystem,
isCustom: false, isCustom: false,

View File

@@ -1,7 +1,6 @@
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; 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 { 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 { 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 { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -26,7 +25,6 @@ export interface ReflectFieldMetadata {
> & { > & {
name: string; name: string;
type: FieldMetadataType; type: FieldMetadataType;
targetColumnMap: FieldMetadataTargetColumnMap<'default'>;
isNullable: boolean; isNullable: boolean;
isSystem: boolean; isSystem: boolean;
isCustom: boolean; isCustom: boolean;

View File

@@ -5,7 +5,6 @@ import {
import { ComputedPartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; 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 { 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 { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { import {
createForeignKeyDeterministicUuid, createForeignKeyDeterministicUuid,
@@ -42,7 +41,6 @@ export const computeStandardObject = (
...rest, ...rest,
standardId: relationStandardId, standardId: relationStandardId,
defaultValue: null, defaultValue: null,
targetColumnMap: {},
}); });
// Foreign key // Foreign key
@@ -55,11 +53,6 @@ export const computeStandardObject = (
description: `${data.description} id foreign key`, description: `${data.description} id foreign key`,
defaultValue: null, defaultValue: null,
icon: undefined, icon: undefined,
targetColumnMap: generateTargetColumnMap(
FieldMetadataType.UUID,
rest.isCustom,
joinColumn,
),
isSystem: true, isSystem: true,
}); });
} }