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:
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export class CustomObjectMetadata extends BaseObjectMetadata {
|
||||
description: 'Name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
icon: 'IconAbc',
|
||||
defaultValue: { value: 'Untitled' },
|
||||
defaultValue: "'Untitled'",
|
||||
})
|
||||
name: string;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user