Skip to content

Commit

Permalink
feat: wip health check fix default value
Browse files Browse the repository at this point in the history
  • Loading branch information
magrinj committed Mar 21, 2024
1 parent 51280ff commit 1abe1e7
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
IsNotEmpty,
IsNumber,
IsNumberString,
IsString,
Matches,
} from 'class-validator';

Expand All @@ -17,6 +16,9 @@ export const fieldMetadataDefaultValueFunctionName = {
NOW: 'now',
} as const;

export type FieldMetadataDefaultValueFunctionNames =
(typeof fieldMetadataDefaultValueFunctionName)[keyof typeof fieldMetadataDefaultValueFunctionName];

export class FieldMetadataDefaultValueString {
@IsQuotedString()
value: string | null;
Expand Down Expand Up @@ -75,13 +77,11 @@ export class FieldMetadataDefaultValueFullName {
export class FieldMetadataDefaultValueUuidFunction {
@Matches(fieldMetadataDefaultValueFunctionName.UUID)
@IsNotEmpty()
@IsString()
value: typeof fieldMetadataDefaultValueFunctionName.UUID;
}

export class FieldMetadataDefaultValueNowFunction {
@Matches(fieldMetadataDefaultValueFunctionName.NOW)
@IsNotEmpty()
@IsString()
value: typeof fieldMetadataDefaultValueFunctionName.NOW;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {
FieldMetadataFunctionDefaultValue,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';

import { fieldMetadataDefaultValueFunctionName } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';

type FieldMetadataDefaultValueFunctionNameUnion =
(typeof fieldMetadataDefaultValueFunctionName)[keyof typeof fieldMetadataDefaultValueFunctionName];
import {
FieldMetadataDefaultValueFunctionNames,
fieldMetadataDefaultValueFunctionName,
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';

export const isFunctionDefaultValue = (
defaultValue: FieldMetadataDefaultSerializableValue,
Expand All @@ -15,7 +15,7 @@ export const isFunctionDefaultValue = (
typeof defaultValue === 'string' &&
!defaultValue.startsWith("'") &&
Object.values(fieldMetadataDefaultValueFunctionName).includes(
defaultValue as FieldMetadataDefaultValueFunctionNameUnion,
defaultValue as FieldMetadataDefaultValueFunctionNames,
)
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
CompareEntity,
} from './abstract-workspace.fixer';

import { AbstractWorkspaceFixer } 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.fixColumnDefaultValueConflictIssues(
objectMetadataCollection,
issueNeedingMigration as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
);
}

async createMetadataUpdates(
manager: EntityManager,
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceDefaultValueFixerType>[],
): Promise<CompareEntity<FieldMetadataEntity>[]> {
if (issues.length <= 0) {
return [];
}

return this.fixColumnDefaultValueIssues(objectMetadataCollection, issues);
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 fixColumnDefaultValueIssues(
private async fixColumnDefaultValueConflictIssues(
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
Expand All @@ -61,6 +103,78 @@ 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
if (currentDefaultValue && 'type' in currentDefaultValue) {
alteredDefaultValue = {
value:
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];

if (
typeof value === 'string' &&
!value.startsWith("'") &&
!fieldMetadataDefaultValueFunctionName[value]
) {
// This error is expcted because we're creating a default value in a loop and key is typed a string
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
alteredDefaultValue = {
...alteredDefaultValue,
[key]: `'${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'> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -19,6 +22,7 @@ import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field
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 {
Expand Down Expand Up @@ -205,35 +209,57 @@ 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();
const value =
let value =
defaultValue && 'value' in defaultValue ? defaultValue.value : null;

if (isFunctionDefaultValue(value)) {
const serializedDefaultValue = serializeFunctionDefaultValue(value);

// Special case for uuid_generate_v4() default value
if (serializedDefaultValue === 'public.uuid_generate_v4()') {
return 'uuid_generate_v4()';
}
// Old format for default values
// TODO: Should be removed once all default values are migrated
if (defaultValue && 'type' in defaultValue) {
return this.computeFunctionDefaultValue(defaultValue.type);
}

return serializedDefaultValue;
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);
}

private computeFunctionDefaultValue(
value: FieldMetadataFunctionDefaultValue['value'],
) {
const serializedDefaultValue = serializeFunctionDefaultValue(value);

// Special case for uuid_generate_v4() default value
if (serializedDefaultValue === 'public.uuid_generate_v4()') {
return 'uuid_generate_v4()';
}

return serializedDefaultValue;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';

import isEqual from 'lodash.isequal';

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -341,29 +354,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;
}
}
Loading

0 comments on commit 1abe1e7

Please sign in to comment.