feat: simplification of default-value specification in FieldMetadata (#4592)

* feat: wip refactor default-value

* feat: health check to migrate default value

* fix: tests

* fix: refactor defaultValue to make it more clean

* fix: unit tests

* fix: front-end default value
This commit is contained in:
Jérémy M
2024-03-27 10:56:04 +01:00
committed by GitHub
parent 90ce7709dd
commit 5c0b65eecb
43 changed files with 481 additions and 328 deletions

View File

@ -12,30 +12,72 @@ import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-met
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 { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataDefaultValueFunctionNames,
fieldMetadataDefaultValueFunctionName,
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
import { AbstractWorkspaceFixer } from './abstract-workspace.fixer';
import {
AbstractWorkspaceFixer,
CompareEntity,
} from './abstract-workspace.fixer';
type WorkspaceDefaultValueFixerType =
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID;
@Injectable()
export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT> {
export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<WorkspaceDefaultValueFixerType> {
constructor(
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
) {
super(WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT);
super(
WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
);
}
async createWorkspaceMigrations(
manager: EntityManager,
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
issues: WorkspaceHealthColumnIssue<WorkspaceDefaultValueFixerType>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
if (issues.length <= 0) {
return [];
}
const splittedIssues = this.splitIssuesByType(issues);
const issueNeedingMigration =
splittedIssues[WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT] ??
[];
return this.fixColumnDefaultValueIssues(objectMetadataCollection, issues);
return this.fixColumnDefaultValueConflictIssues(
objectMetadataCollection,
issueNeedingMigration as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
);
}
private async fixColumnDefaultValueIssues(
async createMetadataUpdates(
manager: EntityManager,
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceDefaultValueFixerType>[],
): Promise<CompareEntity<FieldMetadataEntity>[]> {
if (issues.length <= 0) {
return [];
}
const splittedIssues = this.splitIssuesByType(issues);
const issueNeedingMetadataUpdate =
splittedIssues[WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID] ??
[];
return this.fixColumnDefaultValueNotValidIssues(
manager,
issueNeedingMetadataUpdate as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID>[],
);
}
private async fixColumnDefaultValueConflictIssues(
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
@ -61,6 +103,90 @@ export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<Workspace
);
}
private async fixColumnDefaultValueNotValidIssues(
manager: EntityManager,
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID>[],
): Promise<CompareEntity<FieldMetadataEntity>[]> {
const fieldMetadataRepository = manager.getRepository(FieldMetadataEntity);
const updatedEntities: CompareEntity<FieldMetadataEntity>[] = [];
for (const issue of issues) {
const currentDefaultValue:
| FieldMetadataDefaultValue<'default'>
// Old format for default values
// TODO: Remove this after all workspaces are migrated
| { type: FieldMetadataDefaultValueFunctionNames }
| null = issue.fieldMetadata.defaultValue;
let alteredDefaultValue: FieldMetadataDefaultValue<'default'> | null =
null;
// Check if it's an old function default value
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (currentDefaultValue && 'type' in currentDefaultValue) {
alteredDefaultValue =
currentDefaultValue.type as FieldMetadataDefaultValueFunctionNames;
}
// Check if it's an old string default value
if (currentDefaultValue) {
for (const key of Object.keys(currentDefaultValue)) {
if (key === 'type') {
continue;
}
const value = currentDefaultValue[key];
const newValue =
typeof value === 'string' &&
!value.startsWith("'") &&
!Object.values(fieldMetadataDefaultValueFunctionName).includes(
value as FieldMetadataDefaultValueFunctionNames,
)
? `'${value}'`
: value;
alteredDefaultValue = {
...(currentDefaultValue as any),
...(alteredDefaultValue as any),
[key]: newValue,
};
}
}
// Old formart default values
if (
alteredDefaultValue &&
typeof alteredDefaultValue === 'object' &&
'value' in alteredDefaultValue
) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
alteredDefaultValue = alteredDefaultValue.value;
}
if (alteredDefaultValue === null) {
continue;
}
await fieldMetadataRepository.update(issue.fieldMetadata.id, {
defaultValue: alteredDefaultValue,
});
const alteredEntity = await fieldMetadataRepository.findOne({
where: {
id: issue.fieldMetadata.id,
},
});
updatedEntities.push({
current: issue.fieldMetadata,
altered: alteredEntity as FieldMetadataEntity | null,
});
}
return updatedEntities;
}
private computeFieldMetadataDefaultValueFromColumnDefault(
columnDefault: string | undefined,
): FieldMetadataDefaultValue<'default'> {
@ -73,29 +199,29 @@ export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<Workspace
}
if (!isNaN(Number(columnDefault))) {
return { value: +columnDefault };
return +columnDefault;
}
if (columnDefault === 'true') {
return { value: true };
return true;
}
if (columnDefault === 'false') {
return { value: false };
return false;
}
if (columnDefault === '') {
return { value: '' };
return "''";
}
if (columnDefault === 'now()') {
return { type: 'now' };
return 'now';
}
if (columnDefault.startsWith('public.uuid_generate_v4')) {
return { type: 'uuid' };
return 'uuid';
}
return { value: columnDefault };
return columnDefault;
}
}

View File

@ -7,7 +7,10 @@ import {
WorkspaceTableStructure,
WorkspaceTableStructureResult,
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import {
FieldMetadataDefaultValue,
FieldMetadataFunctionDefaultValue,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import {
@ -15,9 +18,11 @@ import {
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { serializeTypeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-type-default-value.util';
import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-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 { FieldMetadataDefaultValueFunctionNames } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
@Injectable()
export class DatabaseStructureService {
@ -204,31 +209,49 @@ export class DatabaseStructureService {
getPostgresDefault(
fieldMetadataType: FieldMetadataType,
defaultValue: FieldMetadataDefaultValue | null,
defaultValue:
| 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();
if (defaultValue && 'type' in defaultValue) {
const serializedDefaultValue = serializeTypeDefaultValue(defaultValue);
let value: any =
// Old formart default values
defaultValue &&
typeof defaultValue === 'object' &&
'value' in defaultValue
? defaultValue.value
: defaultValue;
// Special case for uuid_generate_v4() default value
if (serializedDefaultValue === 'public.uuid_generate_v4()') {
return 'uuid_generate_v4()';
}
return serializedDefaultValue;
// 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);
}
const value =
defaultValue && 'value' in defaultValue ? defaultValue.value : 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,
@ -236,4 +259,17 @@ export class DatabaseStructureService {
// Workaround to use normalizeDefault without a complete ColumnMetadata object
} as ColumnMetadata);
}
private computeFunctionDefaultValue(
value: FieldMetadataFunctionDefaultValue,
) {
const serializedDefaultValue = serializeFunctionDefaultValue(value);
// Special case for uuid_generate_v4() default value
if (serializedDefaultValue === 'public.uuid_generate_v4()') {
return 'uuid_generate_v4()';
}
return serializedDefaultValue;
}
}

View File

@ -1,4 +1,4 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import isEqual from 'lodash.isequal';
@ -67,18 +67,30 @@ export class FieldMetadataHealthService {
issues.push(...defaultValueIssues);
}
for (const compositeFieldMetadata of compositeFieldMetadataCollection) {
const compositeFieldIssues = await this.healthCheckField(
// 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,
workspaceTableColumns,
computeCompositeFieldMetadata(
compositeFieldMetadata,
fieldMetadata,
),
options,
fieldMetadata,
);
issues.push(...compositeFieldIssues);
issues.push(...compositeFieldMetadataIssues);
}
} else {
const fieldIssues = await this.healthCheckField(
@ -137,6 +149,7 @@ export class FieldMetadataHealthService {
fieldMetadata.type,
fieldMetadata.defaultValue,
);
// Check if column exist in database
const columnStructure = workspaceTableColumns.find(
(tableDefinition) => tableDefinition.columnName === columnName,
@ -178,7 +191,7 @@ export class FieldMetadataHealthService {
if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) {
const enumValues = fieldMetadata.options?.map((option) =>
serializeDefaultValue(option.value),
serializeDefaultValue(`'${option.value}'`),
);
if (!enumValues.includes(columnDefaultValue)) {
@ -325,10 +338,11 @@ export class FieldMetadataHealthService {
isEnumFieldMetadataType(fieldMetadata.type) &&
fieldMetadata.defaultValue
) {
const enumValues = fieldMetadata.options?.map((option) => option.value);
const metadataDefaultValue = (
fieldMetadata.defaultValue as FieldMetadataDefaultValue<EnumFieldMetadataUnionType>
)?.value;
const enumValues = fieldMetadata.options?.map((option) =>
serializeDefaultValue(`'${option.value}'`),
);
const metadataDefaultValue =
fieldMetadata.defaultValue as FieldMetadataDefaultValue<EnumFieldMetadataUnionType>;
if (metadataDefaultValue && !enumValues.includes(metadataDefaultValue)) {
issues.push({
@ -341,29 +355,4 @@ export class FieldMetadataHealthService {
return issues;
}
private isCompositeObjectWellStructured(
fieldMetadataType: FieldMetadataType,
object: any,
): boolean {
const subFields = compositeDefinitions.get(fieldMetadataType)?.() ?? [];
if (!object) {
return true;
}
if (subFields.length === 0) {
throw new InternalServerErrorException(
`The composite field type ${fieldMetadataType} doesn't have any sub fields, it seems this one is not implemented in the composite definitions map`,
);
}
for (const subField of subFields) {
if (!object[subField.name]) {
return false;
}
}
return true;
}
}

View File

@ -90,6 +90,16 @@ export class WorkspaceFixService {
filteredIssues,
);
}
case WorkspaceHealthFixKind.DefaultValue: {
const filteredIssues =
this.workspaceDefaultValueFixer.filterIssues(issues);
return this.workspaceDefaultValueFixer.createMetadataUpdates(
manager,
objectMetadataCollection,
filteredIssues,
);
}
default: {
return [];
}

View File

@ -23,7 +23,7 @@ export class CustomObjectMetadata extends BaseObjectMetadata {
description: 'Name',
type: FieldMetadataType.TEXT,
icon: 'IconAbc',
defaultValue: { value: 'Untitled' },
defaultValue: "'Untitled'",
})
name: string;

View File

@ -9,7 +9,7 @@ export abstract class BaseObjectMetadata {
type: FieldMetadataType.UUID,
label: 'Id',
description: 'Id',
defaultValue: { type: 'uuid' },
defaultValue: 'uuid',
icon: 'Icon123',
})
@IsSystem()
@ -21,7 +21,7 @@ export abstract class BaseObjectMetadata {
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
defaultValue: { type: 'now' },
defaultValue: 'now',
})
createdAt: Date;
@ -31,7 +31,7 @@ export abstract class BaseObjectMetadata {
label: 'Update date',
description: 'Update date',
icon: 'IconCalendar',
defaultValue: { type: 'now' },
defaultValue: 'now',
})
@IsSystem()
updatedAt: Date;