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

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

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

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 { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util';
import { FieldMetadataDefaultValueFunctionNames } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@Injectable()
export class DatabaseStructureService {
@ -156,22 +157,40 @@ export class DatabaseStructureService {
return results.length >= 1;
}
getPostgresDataType(fieldMetadata: FieldMetadataEntity): string {
const typeORMType = fieldMetadataTypeToColumnType(fieldMetadata.type);
getPostgresDataTypes(fieldMetadata: FieldMetadataEntity): string[] {
const mainDataSource = this.typeORMService.getMainDataSource();
// Compute enum name to compare data type properly
if (typeORMType === 'enum') {
const objectName = fieldMetadata.object?.nameSingular;
const prefix = fieldMetadata.isCustom ? '_' : '';
const fieldName = fieldMetadata.name;
const normalizer = (type: FieldMetadataType, columnName: string) => {
const typeORMType = fieldMetadataTypeToColumnType(type);
return `${objectName}_${prefix}${fieldName}_enum`;
// Compute enum name to compare data type properly
if (typeORMType === 'enum') {
const objectName = fieldMetadata.object?.nameSingular;
const prefix = fieldMetadata.isCustom ? '_' : '';
return `${objectName}_${prefix}${columnName}_enum`;
}
return mainDataSource.driver.normalizeType({
type: typeORMType,
});
};
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
if (!compositeType) {
throw new Error(
`Composite type definition not found for ${fieldMetadata.type}`,
);
}
return compositeType.properties.map((compositeProperty) =>
normalizer(compositeProperty.type, compositeProperty.name),
);
}
return mainDataSource.driver.normalizeType({
type: typeORMType,
});
return [normalizer(fieldMetadata.type, fieldMetadata.name)];
}
getFieldMetadataTypeFromPostgresDataType(
@ -207,57 +226,84 @@ export class DatabaseStructureService {
return null;
}
getPostgresDefault(
getPostgresDefaults(
fieldMetadataType: FieldMetadataType,
defaultValue:
initialDefaultValue:
| FieldMetadataDefaultValue
// Old format for default values
// TODO: Should be removed once all default values are migrated
| { type: FieldMetadataDefaultValueFunctionNames }
| null,
): string | null | undefined {
const typeORMType = fieldMetadataTypeToColumnType(
fieldMetadataType,
) as ColumnType;
const mainDataSource = this.typeORMService.getMainDataSource();
): (string | null | undefined)[] {
const normalizer = (
type: FieldMetadataType,
defaultValue:
| FieldMetadataDefaultValue
| { type: FieldMetadataDefaultValueFunctionNames }
| null,
) => {
const typeORMType = fieldMetadataTypeToColumnType(type) as ColumnType;
const mainDataSource = this.typeORMService.getMainDataSource();
let value: any =
// Old formart default values
defaultValue &&
typeof defaultValue === 'object' &&
'value' in defaultValue
? defaultValue.value
: defaultValue;
let value: any =
// Old formart default values
defaultValue &&
typeof defaultValue === 'object' &&
'value' in defaultValue
? defaultValue.value
: defaultValue;
// Old format for default values
// TODO: Should be removed once all default values are migrated
if (
defaultValue &&
typeof defaultValue === 'object' &&
'type' in defaultValue
) {
return this.computeFunctionDefaultValue(defaultValue.type);
// Old format for default values
// TODO: Should be removed once all default values are migrated
if (
defaultValue &&
typeof defaultValue === 'object' &&
'type' in defaultValue
) {
return this.computeFunctionDefaultValue(defaultValue.type);
}
if (isFunctionDefaultValue(value)) {
return this.computeFunctionDefaultValue(value);
}
if (typeof value === 'number') {
return value.toString();
}
// Remove leading and trailing single quotes for string default values as it's already handled by TypeORM
if (typeof value === 'string' && value.match(/^'.*'$/)) {
value = value.replace(/^'/, '').replace(/'$/, '');
}
return mainDataSource.driver.normalizeDefault({
type: typeORMType,
default: value,
isArray: false,
// Workaround to use normalizeDefault without a complete ColumnMetadata object
} as ColumnMetadata);
};
if (isCompositeFieldMetadataType(fieldMetadataType)) {
const compositeType = compositeTypeDefintions.get(fieldMetadataType);
if (!compositeType) {
throw new Error(
`Composite type definition not found for ${fieldMetadataType}`,
);
}
return compositeType.properties.map((compositeProperty) =>
normalizer(
compositeProperty.type,
typeof initialDefaultValue === 'object'
? initialDefaultValue?.[compositeProperty.name]
: null,
),
);
}
if (isFunctionDefaultValue(value)) {
return this.computeFunctionDefaultValue(value);
}
if (typeof value === 'number') {
return value.toString();
}
// Remove leading and trailing single quotes for string default values as it's already handled by TypeORM
if (typeof value === 'string' && value.match(/^'.*'$/)) {
value = value.replace(/^'/, '').replace(/'$/, '');
}
return mainDataSource.driver.normalizeDefault({
type: typeORMType,
default: value,
isArray: false,
// Workaround to use normalizeDefault without a complete ColumnMetadata object
} as ColumnMetadata);
return [normalizer(fieldMetadataType, initialDefaultValue)];
}
private computeFunctionDefaultValue(

View File

@ -1,7 +1,5 @@
import { Injectable } from '@nestjs/common';
import isEqual from 'lodash.isequal';
import {
WorkspaceHealthIssue,
WorkspaceHealthIssueType,
@ -17,7 +15,6 @@ import {
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service';
import { validName } from 'src/engine/workspace-manager/workspace-health/utils/valid-name.util';
import { compositeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util';
import {
EnumFieldMetadataUnionType,
@ -25,10 +22,13 @@ import {
} from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { validateOptionsForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { computeCompositeFieldMetadata } from 'src/engine/workspace-manager/workspace-health/utils/compute-composite-field-metadata.util';
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
import { customNamePrefix } from 'src/engine/utils/compute-custom-name.util';
import { customNamePrefix } from 'src/engine/utils/compute-table-name.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import {
computeColumnName,
computeCompositeColumnName,
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
@Injectable()
export class FieldMetadataHealthService {
@ -50,58 +50,14 @@ export class FieldMetadataHealthService {
continue;
}
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeFieldMetadataCollection =
compositeDefinitions.get(fieldMetadata.type)?.(fieldMetadata) ?? [];
const fieldIssues = await this.healthCheckField(
tableName,
workspaceTableColumns,
fieldMetadata,
options,
);
if (options.mode === 'metadata' || options.mode === 'all') {
const targetColumnMapIssue = this.targetColumnMapCheck(fieldMetadata);
if (targetColumnMapIssue) {
issues.push(targetColumnMapIssue);
}
const defaultValueIssues =
this.defaultValueHealthCheck(fieldMetadata);
issues.push(...defaultValueIssues);
}
// Only check structure on nested composite fields
if (options.mode === 'structure' || options.mode === 'all') {
for (const compositeFieldMetadata of compositeFieldMetadataCollection) {
const compositeFieldStructureIssues = this.structureFieldCheck(
tableName,
workspaceTableColumns,
computeCompositeFieldMetadata(
compositeFieldMetadata,
fieldMetadata,
),
);
issues.push(...compositeFieldStructureIssues);
}
}
// Only check metadata on the parent composite field
if (options.mode === 'metadata' || options.mode === 'all') {
const compositeFieldMetadataIssues = this.metadataFieldCheck(
tableName,
fieldMetadata,
);
issues.push(...compositeFieldMetadataIssues);
}
} else {
const fieldIssues = await this.healthCheckField(
tableName,
workspaceTableColumns,
fieldMetadata,
options,
);
issues.push(...fieldIssues);
}
issues.push(...fieldIssues);
}
return issues;
@ -139,78 +95,105 @@ export class FieldMetadataHealthService {
workspaceTableColumns: WorkspaceTableStructure[],
fieldMetadata: FieldMetadataEntity,
): WorkspaceHealthIssue[] {
const dataTypes =
this.databaseStructureService.getPostgresDataTypes(fieldMetadata);
const issues: WorkspaceHealthIssue[] = [];
const columnName = fieldMetadata.targetColumnMap.value;
let columnNames: string[] = [];
const dataType =
this.databaseStructureService.getPostgresDataType(fieldMetadata);
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
const defaultValue = this.databaseStructureService.getPostgresDefault(
if (!compositeType) {
throw new Error(`Composite type ${fieldMetadata.type} is not defined`);
}
columnNames = compositeType.properties.map((compositeProperty) =>
computeCompositeColumnName(fieldMetadata, compositeProperty),
);
} else {
columnNames = [computeColumnName(fieldMetadata)];
}
const defaultValues = this.databaseStructureService.getPostgresDefaults(
fieldMetadata.type,
fieldMetadata.defaultValue,
);
// Check if column exist in database
const columnStructure = workspaceTableColumns.find(
(tableDefinition) => tableDefinition.columnName === columnName,
const columnStructureMap = workspaceTableColumns.reduce(
(acc, workspaceTableColumn) => {
const columnName = workspaceTableColumn.columnName;
if (columnNames.includes(columnName)) {
acc[columnName] = workspaceTableColumn;
}
return acc;
},
{} as { [key: string]: WorkspaceTableStructure },
);
if (!columnStructure) {
issues.push({
type: WorkspaceHealthIssueType.MISSING_COLUMN,
fieldMetadata,
columnStructure,
message: `Column ${columnName} not found in table ${tableName}`,
});
for (const [index, columnName] of columnNames.entries()) {
const columnStructure = columnStructureMap[columnName];
return issues;
}
const columnDefaultValue = columnStructure.columnDefault?.split('::')?.[0];
// Check if column data type is the same
if (columnStructure.dataType !== dataType) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT,
fieldMetadata,
columnStructure,
message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataType}"`,
});
}
if (columnStructure.isNullable !== fieldMetadata.isNullable) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT,
fieldMetadata,
columnStructure,
message: `Column ${columnName} is expected to be ${
fieldMetadata.isNullable ? 'nullable' : 'not nullable'
} but is ${columnStructure.isNullable ? 'nullable' : 'not nullable'}`,
});
}
if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) {
const enumValues = fieldMetadata.options?.map((option) =>
serializeDefaultValue(`'${option.value}'`),
);
if (!enumValues.includes(columnDefaultValue)) {
if (!columnStructure) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
type: WorkspaceHealthIssueType.MISSING_COLUMN,
fieldMetadata,
columnStructures: workspaceTableColumns,
message: `Column ${columnName} not found in table ${tableName}`,
});
continue;
}
const columnDefaultValue =
columnStructure.columnDefault?.split('::')?.[0];
// Check if column data type is the same
if (!dataTypes[index] || columnStructure.dataType !== dataTypes[index]) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT,
fieldMetadata,
columnStructure,
message: `Column ${columnName} default value is not in the enum values "${columnDefaultValue}" NOT IN "${enumValues}"`,
message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataTypes[index]}"`,
});
}
}
if (columnDefaultValue !== defaultValue) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
fieldMetadata,
columnStructure,
message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValue}"`,
});
if (columnStructure.isNullable !== fieldMetadata.isNullable) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT,
fieldMetadata,
columnStructure,
message: `Column ${columnName} is expected to be ${
fieldMetadata.isNullable ? 'nullable' : 'not nullable'
} but is ${columnStructure.isNullable ? 'nullable' : 'not nullable'}`,
});
}
if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) {
const enumValues = fieldMetadata.options?.map((option) =>
serializeDefaultValue(`'${option.value}'`),
);
if (!enumValues.includes(columnDefaultValue)) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
fieldMetadata,
columnStructure,
message: `Column ${columnName} default value is not in the enum values "${columnDefaultValue}" NOT IN "${enumValues}"`,
});
}
}
if (columnDefaultValue !== defaultValues[index]) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
fieldMetadata,
columnStructure,
message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValues[index]}"`,
});
}
}
return issues;
@ -221,14 +204,9 @@ export class FieldMetadataHealthService {
fieldMetadata: FieldMetadataEntity,
): WorkspaceHealthIssue[] {
const issues: WorkspaceHealthIssue[] = [];
const columnName = fieldMetadata.targetColumnMap.value;
const targetColumnMapIssue = this.targetColumnMapCheck(fieldMetadata);
const columnName = fieldMetadata.name;
const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata);
if (targetColumnMapIssue) {
issues.push(targetColumnMapIssue);
}
if (fieldMetadata.name.startsWith(customNamePrefix)) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED,
@ -237,11 +215,11 @@ export class FieldMetadataHealthService {
});
}
if (fieldMetadata.isCustom && !columnName?.startsWith(customNamePrefix)) {
if (fieldMetadata.isCustom && columnName?.startsWith(customNamePrefix)) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM,
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM,
fieldMetadata,
message: `Column ${columnName} is marked as custom in table ${tableName} but doesn't start with "_"`,
message: `Column ${columnName} is marked as custom in table ${tableName} but and start with "_", this behavior has been removed. Please remove the prefix.`,
});
}
@ -280,7 +258,7 @@ export class FieldMetadataHealthService {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID,
fieldMetadata,
message: `Column options of ${fieldMetadata.targetColumnMap?.value} is not valid`,
message: `Column options of ${fieldMetadata.name} is not valid`,
});
}
@ -289,33 +267,6 @@ export class FieldMetadataHealthService {
return issues;
}
private targetColumnMapCheck(
fieldMetadata: FieldMetadataEntity,
): WorkspaceHealthIssue | null {
const targetColumnMap = generateTargetColumnMap(
fieldMetadata.type,
fieldMetadata.isCustom,
fieldMetadata.name,
);
if (
!fieldMetadata.targetColumnMap ||
!isEqual(targetColumnMap, fieldMetadata.targetColumnMap)
) {
return {
type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
fieldMetadata,
message: `Column targetColumnMap "${JSON.stringify(
fieldMetadata.targetColumnMap,
)}" is not the same as the generated one "${JSON.stringify(
targetColumnMap,
)}"`,
};
}
return null;
}
private defaultValueHealthCheck(
fieldMetadata: FieldMetadataEntity,
): WorkspaceHealthIssue[] {

View File

@ -20,10 +20,10 @@ import {
deduceRelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { createRelationForeignKeyColumnName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-column-name.util';
import { createRelationForeignKeyFieldMetadataName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-field-metadata-name.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util';
import { camelCase } from 'src/utils/camel-case';
@Injectable()
export class RelationMetadataHealthService {
@ -145,11 +145,7 @@ export class RelationMetadataHealthService {
return [];
}
const isCustom = toFieldMetadata.isCustom ?? false;
const foreignKeyColumnName = createRelationForeignKeyColumnName(
toFieldMetadata.name,
isCustom,
);
const foreignKeyColumnName = `${camelCase(toFieldMetadata.name)}Id`;
const relationColumn = workspaceTableColumns.find(
(column) => column.columnName === foreignKeyColumnName,
);

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

View File

@ -17,9 +17,3 @@ export const isWorkspaceHealthDefaultValueIssue = (
): type is WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT => {
return type === WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT;
};
export const isWorkspaceHealthTargetColumnMapIssue = (
type: WorkspaceHealthIssueType,
): type is WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID => {
return type === WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID;
};

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { GateDecoratorParams } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/gate-decorator.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@ -26,7 +25,6 @@ export interface ReflectFieldMetadata {
> & {
name: string;
type: FieldMetadataType;
targetColumnMap: FieldMetadataTargetColumnMap<'default'>;
isNullable: boolean;
isSystem: boolean;
isCustom: boolean;

View File

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