Skip to content

Commit

Permalink
feat: health check to migrate 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 5c2eed8
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
IsNotEmpty,
IsNumber,
IsNumberString,
IsString,
Matches,
ValidateIf,
} from 'class-validator';

import { IsQuotedString } from 'src/engine/metadata-modules/field-metadata/validators/is-quoted-string.validator';
Expand All @@ -17,71 +17,84 @@ export const fieldMetadataDefaultValueFunctionName = {
NOW: 'now',
} as const;

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

export class FieldMetadataDefaultValueString {
@ValidateIf((object, value) => value !== null)
@IsQuotedString()
value: string | null;
}

export class FieldMetadataDefaultValueJson {
@ValidateIf((object, value) => value !== null)
@IsJSON()
value: JSON | null;
}

export class FieldMetadataDefaultValueNumber {
@ValidateIf((object, value) => value !== null)
@IsNumber()
value: number | null;
}

export class FieldMetadataDefaultValueBoolean {
@ValidateIf((object, value) => value !== null)
@IsBoolean()
value: boolean | null;
}

export class FieldMetadataDefaultValueStringArray {
@ValidateIf((object, value) => value !== null)
@IsArray()
@IsQuotedString({ each: true })
value: string[] | null;
}

export class FieldMetadataDefaultValueDateTime {
@ValidateIf((object, value) => value !== null)
@IsDate()
value: Date | null;
}

export class FieldMetadataDefaultValueLink {
@ValidateIf((object, value) => value !== null)
@IsQuotedString()
label: string | null;

@ValidateIf((object, value) => value !== null)
@IsQuotedString()
url: string | null;
}

export class FieldMetadataDefaultValueCurrency {
@ValidateIf((object, value) => value !== null)
@IsNumberString()
amountMicros: string | null;

@ValidateIf((object, value) => value !== null)
@IsQuotedString()
currencyCode: string | null;
}

export class FieldMetadataDefaultValueFullName {
@ValidateIf((object, value) => value !== null)
@IsQuotedString()
firstName: string | null;

@ValidateIf((object, value) => value !== null)
@IsQuotedString()
lastName: string | null;
}

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,76 @@ 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]
) {
alteredDefaultValue = {
...currentDefaultValue,
...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;
}
}
Loading

0 comments on commit 5c2eed8

Please sign in to comment.