feat: workspace health target column map fix (#3932)
* feat: workspace health fix target column map * fix: remove log * feat: refactor health fixer * fix: default-value issue and health check not working with composite * fix: enhance target column map fix * feat: create workspace migrations for target-column-map issues * feat: enhance workspace-health issue detection
This commit is contained in:
@ -39,12 +39,14 @@
|
|||||||
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch",
|
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch",
|
||||||
"class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch",
|
"class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch",
|
||||||
"graphql-middleware": "^6.1.35",
|
"graphql-middleware": "^6.1.35",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
"passport": "^0.7.0"
|
"passport": "^0.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "10.3.0",
|
"@nestjs/cli": "10.3.0",
|
||||||
"@nx/js": "17.2.8",
|
"@nx/js": "17.2.8",
|
||||||
"@types/lodash.isempty": "^4.4.7",
|
"@types/lodash.isempty": "^4.4.7",
|
||||||
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/lodash.isobject": "^3.0.7",
|
"@types/lodash.isobject": "^3.0.7",
|
||||||
"@types/lodash.omit": "^4.5.9",
|
"@types/lodash.omit": "^4.5.9",
|
||||||
"@types/lodash.snakecase": "^4.1.7",
|
"@types/lodash.snakecase": "^4.1.7",
|
||||||
|
|||||||
@ -29,10 +29,8 @@ import {
|
|||||||
RelationMetadataEntity,
|
RelationMetadataEntity,
|
||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
} from 'src/metadata/relation-metadata/relation-metadata.entity';
|
} from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||||
import {
|
import { computeCustomName } from 'src/workspace/utils/compute-custom-name.util';
|
||||||
computeCustomName,
|
import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
|
||||||
computeObjectTargetTable,
|
|
||||||
} from 'src/workspace/utils/compute-object-target-table.util';
|
|
||||||
import { DeleteOneObjectInput } from 'src/metadata/object-metadata/dtos/delete-object.input';
|
import { DeleteOneObjectInput } from 'src/metadata/object-metadata/dtos/delete-object.input';
|
||||||
import { RelationToDelete } from 'src/metadata/relation-metadata/types/relation-to-delete';
|
import { RelationToDelete } from 'src/metadata/relation-metadata/types/relation-to-delete';
|
||||||
import { generateMigrationName } from 'src/metadata/workspace-migration/utils/generate-migration-name.util';
|
import { generateMigrationName } from 'src/metadata/workspace-migration/utils/generate-migration-name.util';
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
export const customNamePrefix = '_';
|
||||||
|
|
||||||
|
export const computeCustomName = (name: string, isCustom: boolean) => {
|
||||||
|
return isCustom ? `${customNamePrefix}${name}` : name;
|
||||||
|
};
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
|
import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
|
import { BasicFieldMetadataType } from 'src/metadata/workspace-migration/factories/basic-column-action.factory';
|
||||||
|
|
||||||
|
import { computeCustomName } from './compute-custom-name.util';
|
||||||
|
|
||||||
|
export const computeFieldTargetColumn = (
|
||||||
|
fieldMetadata:
|
||||||
|
| FieldMetadataEntity<BasicFieldMetadataType>
|
||||||
|
| FieldMetadataInterface<BasicFieldMetadataType>,
|
||||||
|
) => {
|
||||||
|
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||||
|
throw new Error(
|
||||||
|
"Composite field metadata should not be computed here, as they're split into multiple fields.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return computeCustomName(fieldMetadata.name, fieldMetadata.isCustom ?? false);
|
||||||
|
};
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { computeCustomName } from './compute-custom-name.util';
|
||||||
|
|
||||||
export const computeObjectTargetTable = (
|
export const computeObjectTargetTable = (
|
||||||
objectMetadata: ObjectMetadataInterface,
|
objectMetadata: ObjectMetadataInterface,
|
||||||
) => {
|
) => {
|
||||||
@ -8,7 +10,3 @@ export const computeObjectTargetTable = (
|
|||||||
objectMetadata.isCustom,
|
objectMetadata.isCustom,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const computeCustomName = (name: string, isCustom: boolean) => {
|
|
||||||
return isCustom ? `_${name}` : name;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { CommandLogger } from 'src/commands/command-logger';
|
|||||||
|
|
||||||
interface WorkspaceHealthCommandOptions {
|
interface WorkspaceHealthCommandOptions {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
verbose?: boolean;
|
|
||||||
mode?: WorkspaceHealthMode;
|
mode?: WorkspaceHealthMode;
|
||||||
fix?: WorkspaceHealthFixKind;
|
fix?: WorkspaceHealthFixKind;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
@ -49,7 +48,7 @@ export class WorkspaceHealthCommand extends CommandRunner {
|
|||||||
chalk.red(`Workspace is not healthy, found ${issues.length} issues`),
|
chalk.red(`Workspace is not healthy, found ${issues.length} issues`),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (options.verbose) {
|
if (options.dryRun) {
|
||||||
await this.commandLogger.writeLog(
|
await this.commandLogger.writeLog(
|
||||||
`workspace-health-issues-${options.workspaceId}`,
|
`workspace-health-issues-${options.workspaceId}`,
|
||||||
issues,
|
issues,
|
||||||
@ -61,25 +60,30 @@ export class WorkspaceHealthCommand extends CommandRunner {
|
|||||||
if (options.fix) {
|
if (options.fix) {
|
||||||
this.logger.log(chalk.yellow('Fixing issues'));
|
this.logger.log(chalk.yellow('Fixing issues'));
|
||||||
|
|
||||||
const workspaceMigrations = await this.workspaceHealthService.fixIssues(
|
const { workspaceMigrations, metadataEntities } =
|
||||||
options.workspaceId,
|
await this.workspaceHealthService.fixIssues(
|
||||||
issues,
|
options.workspaceId,
|
||||||
{
|
issues,
|
||||||
type: options.fix,
|
{
|
||||||
applyChanges: !options.dryRun,
|
type: options.fix,
|
||||||
},
|
applyChanges: !options.dryRun,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
const totalCount = workspaceMigrations.length + metadataEntities.length;
|
||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
await this.commandLogger.writeLog(
|
await this.commandLogger.writeLog(
|
||||||
`workspace-health-${options.fix}-migrations`,
|
`workspace-health-${options.fix}-migrations`,
|
||||||
workspaceMigrations,
|
workspaceMigrations,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.commandLogger.writeLog(
|
||||||
|
`workspace-health-${options.fix}-metadata-entities`,
|
||||||
|
metadataEntities,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
chalk.green(
|
chalk.green(`Fixed ${totalCount}/${issues.length} issues`),
|
||||||
`Fixed ${workspaceMigrations.length}/${issues.length} issues`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,15 +111,6 @@ export class WorkspaceHealthCommand extends CommandRunner {
|
|||||||
return value as WorkspaceHealthFixKind;
|
return value as WorkspaceHealthFixKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Option({
|
|
||||||
flags: '-v, --verbose',
|
|
||||||
description: 'Detailed output',
|
|
||||||
required: false,
|
|
||||||
})
|
|
||||||
parseVerbose(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Option({
|
@Option({
|
||||||
flags: '-m, --mode [mode]',
|
flags: '-m, --mode [mode]',
|
||||||
description: 'Mode of the health check [structure, metadata, all]',
|
description: 'Mode of the health check [structure, metadata, all]',
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { EntityManager } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
WorkspaceHealthIssue,
|
||||||
|
WorkspaceHealthIssueType,
|
||||||
|
WorkspaceIssueTypeToInterface,
|
||||||
|
} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
||||||
|
|
||||||
|
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||||
|
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||||
|
|
||||||
|
export class CompareEntity<T> {
|
||||||
|
current: T | null;
|
||||||
|
altered: T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class AbstractWorkspaceFixer<
|
||||||
|
IssueTypes extends WorkspaceHealthIssueType,
|
||||||
|
UpdateRecordEntities = unknown,
|
||||||
|
> {
|
||||||
|
private issueTypes: IssueTypes[];
|
||||||
|
|
||||||
|
constructor(...issueTypes: IssueTypes[]) {
|
||||||
|
this.issueTypes = issueTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterIssues(
|
||||||
|
issues: WorkspaceHealthIssue[],
|
||||||
|
): WorkspaceIssueTypeToInterface<IssueTypes>[] {
|
||||||
|
return issues.filter(
|
||||||
|
(issue): issue is WorkspaceIssueTypeToInterface<IssueTypes> =>
|
||||||
|
this.issueTypes.includes(issue.type as IssueTypes),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected splitIssuesByType(
|
||||||
|
issues: WorkspaceIssueTypeToInterface<IssueTypes>[],
|
||||||
|
): Record<IssueTypes, WorkspaceIssueTypeToInterface<IssueTypes>[]> {
|
||||||
|
return issues.reduce(
|
||||||
|
(
|
||||||
|
acc: Record<IssueTypes, WorkspaceIssueTypeToInterface<IssueTypes>[]>,
|
||||||
|
issue,
|
||||||
|
) => {
|
||||||
|
const type = issue.type as IssueTypes;
|
||||||
|
|
||||||
|
if (!acc[type]) {
|
||||||
|
acc[type] = [];
|
||||||
|
}
|
||||||
|
acc[type].push(issue);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<IssueTypes, WorkspaceIssueTypeToInterface<IssueTypes>[]>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWorkspaceMigrations?(
|
||||||
|
manager: EntityManager,
|
||||||
|
objectMetadataCollection: ObjectMetadataEntity[],
|
||||||
|
issues: WorkspaceIssueTypeToInterface<IssueTypes>[],
|
||||||
|
): Promise<Partial<WorkspaceMigrationEntity>[]>;
|
||||||
|
|
||||||
|
async createMetadataUpdates?(
|
||||||
|
manager: EntityManager,
|
||||||
|
objectMetadataCollection: ObjectMetadataEntity[],
|
||||||
|
issues: WorkspaceIssueTypeToInterface<IssueTypes>[],
|
||||||
|
): Promise<CompareEntity<UpdateRecordEntities>[]>;
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
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,
|
||||||
|
];
|
||||||
@ -13,37 +13,26 @@ import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metada
|
|||||||
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||||
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
|
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||||
|
|
||||||
type WorkspaceHealthDefaultValueIssue =
|
import { AbstractWorkspaceFixer } from './abstract-workspace.fixer';
|
||||||
WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceFixDefaultValueService {
|
export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
||||||
) {}
|
) {
|
||||||
|
super(WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
async fix(
|
async createWorkspaceMigrations(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
objectMetadataCollection: ObjectMetadataEntity[],
|
objectMetadataCollection: ObjectMetadataEntity[],
|
||||||
issues: WorkspaceHealthDefaultValueIssue[],
|
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
|
||||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||||
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
|
if (issues.length <= 0) {
|
||||||
const defaultValueIssues = issues.filter(
|
return [];
|
||||||
(issue) =>
|
|
||||||
issue.type === WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
|
|
||||||
) as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[];
|
|
||||||
|
|
||||||
if (defaultValueIssues.length > 0) {
|
|
||||||
const columnDefaultValueWorkspaceMigrations =
|
|
||||||
await this.fixColumnDefaultValueIssues(
|
|
||||||
objectMetadataCollection,
|
|
||||||
defaultValueIssues,
|
|
||||||
);
|
|
||||||
|
|
||||||
workspaceMigrations.push(...columnDefaultValueWorkspaceMigrations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceMigrations;
|
return this.fixColumnDefaultValueIssues(objectMetadataCollection, issues);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fixColumnDefaultValueIssues(
|
private async fixColumnDefaultValueIssues(
|
||||||
@ -8,41 +8,30 @@ import {
|
|||||||
} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
||||||
import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||||
|
|
||||||
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
|
||||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||||
|
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||||
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
|
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||||
|
|
||||||
type WorkspaceHealthNullableIssue =
|
import { AbstractWorkspaceFixer } from './abstract-workspace.fixer';
|
||||||
WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceFixNullableService {
|
export class WorkspaceNullableFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
||||||
) {}
|
) {
|
||||||
|
super(WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
async fix(
|
async createWorkspaceMigrations(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
objectMetadataCollection: ObjectMetadataEntity[],
|
objectMetadataCollection: ObjectMetadataEntity[],
|
||||||
issues: WorkspaceHealthNullableIssue[],
|
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>[],
|
||||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||||
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
|
if (issues.length <= 0) {
|
||||||
const nullabilityIssues = issues.filter(
|
return [];
|
||||||
(issue) =>
|
|
||||||
issue.type === WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT,
|
|
||||||
) as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>[];
|
|
||||||
|
|
||||||
if (nullabilityIssues.length > 0) {
|
|
||||||
const columnNullabilityWorkspaceMigrations =
|
|
||||||
await this.fixColumnNullabilityIssues(
|
|
||||||
objectMetadataCollection,
|
|
||||||
nullabilityIssues,
|
|
||||||
);
|
|
||||||
|
|
||||||
workspaceMigrations.push(...columnNullabilityWorkspaceMigrations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceMigrations;
|
return this.fixColumnNullabilityIssues(objectMetadataCollection, issues);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fixColumnNullabilityIssues(
|
private async fixColumnNullabilityIssues(
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { EntityManager } from 'typeorm';
|
||||||
|
import isEqual from 'lodash.isequal';
|
||||||
|
|
||||||
|
import {
|
||||||
|
WorkspaceHealthColumnIssue,
|
||||||
|
WorkspaceHealthIssueType,
|
||||||
|
} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
||||||
|
import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||||
|
|
||||||
|
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||||
|
import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util';
|
||||||
|
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
|
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||||
|
import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
|
||||||
|
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
|
||||||
|
import { DatabaseStructureService } from 'src/workspace/workspace-health/services/database-structure.service';
|
||||||
|
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||||
|
import { isCompositeFieldMetadataType } from 'src/metadata/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,41 +11,29 @@ import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migrati
|
|||||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||||
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||||
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
|
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||||
|
import { DatabaseStructureService } from 'src/workspace/workspace-health/services/database-structure.service';
|
||||||
|
|
||||||
import { DatabaseStructureService } from './database-structure.service';
|
import { AbstractWorkspaceFixer } from './abstract-workspace.fixer';
|
||||||
|
|
||||||
type WorkspaceHealthTypeIssue =
|
|
||||||
WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT>;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceFixTypeService {
|
export class WorkspaceTypeFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
||||||
private readonly databaseStructureService: DatabaseStructureService,
|
private readonly databaseStructureService: DatabaseStructureService,
|
||||||
) {}
|
) {
|
||||||
|
super(WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
async fix(
|
async createWorkspaceMigrations(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
objectMetadataCollection: ObjectMetadataEntity[],
|
objectMetadataCollection: ObjectMetadataEntity[],
|
||||||
issues: WorkspaceHealthTypeIssue[],
|
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT>[],
|
||||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||||
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
|
if (issues.length <= 0) {
|
||||||
const columnTypeIssues = issues.filter(
|
return [];
|
||||||
(issue) =>
|
|
||||||
issue.type === WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT,
|
|
||||||
) as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT>[];
|
|
||||||
|
|
||||||
if (columnTypeIssues.length > 0) {
|
|
||||||
const columnNullabilityWorkspaceMigrations =
|
|
||||||
await this.fixColumnTypeIssues(
|
|
||||||
objectMetadataCollection,
|
|
||||||
columnTypeIssues,
|
|
||||||
);
|
|
||||||
|
|
||||||
workspaceMigrations.push(...columnNullabilityWorkspaceMigrations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceMigrations;
|
return this.fixColumnTypeIssues(objectMetadataCollection, issues);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fixColumnTypeIssues(
|
private async fixColumnTypeIssues(
|
||||||
@ -14,6 +14,7 @@ export enum WorkspaceHealthIssueType {
|
|||||||
MISSING_INDEX = 'MISSING_INDEX',
|
MISSING_INDEX = 'MISSING_INDEX',
|
||||||
MISSING_FOREIGN_KEY = 'MISSING_FOREIGN_KEY',
|
MISSING_FOREIGN_KEY = 'MISSING_FOREIGN_KEY',
|
||||||
MISSING_COMPOSITE_TYPE = 'MISSING_COMPOSITE_TYPE',
|
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_TARGET_COLUMN_MAP_NOT_VALID = 'COLUMN_TARGET_COLUMN_MAP_NOT_VALID',
|
||||||
COLUMN_NAME_SHOULD_BE_CUSTOM = 'COLUMN_NAME_SHOULD_BE_CUSTOM',
|
COLUMN_NAME_SHOULD_BE_CUSTOM = 'COLUMN_NAME_SHOULD_BE_CUSTOM',
|
||||||
COLUMN_OBJECT_REFERENCE_INVALID = 'COLUMN_OBJECT_REFERENCE_INVALID',
|
COLUMN_OBJECT_REFERENCE_INVALID = 'COLUMN_OBJECT_REFERENCE_INVALID',
|
||||||
@ -30,61 +31,64 @@ export enum WorkspaceHealthIssueType {
|
|||||||
RELATION_TYPE_NOT_VALID = 'RELATION_TYPE_NOT_VALID',
|
RELATION_TYPE_NOT_VALID = 'RELATION_TYPE_NOT_VALID',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConditionalType<
|
/**
|
||||||
T extends WorkspaceHealthIssueType | null,
|
* Table issues
|
||||||
U,
|
*/
|
||||||
> = T extends WorkspaceHealthIssueType ? T : U;
|
export type WorkspaceTableIssueTypes =
|
||||||
|
| WorkspaceHealthIssueType.MISSING_TABLE
|
||||||
|
| WorkspaceHealthIssueType.TABLE_NAME_SHOULD_BE_CUSTOM
|
||||||
|
| WorkspaceHealthIssueType.TABLE_TARGET_TABLE_NAME_NOT_VALID
|
||||||
|
| WorkspaceHealthIssueType.TABLE_DATA_SOURCE_ID_NOT_VALID
|
||||||
|
| WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID;
|
||||||
|
|
||||||
export interface WorkspaceHealthTableIssue<
|
export interface WorkspaceHealthTableIssue<T extends WorkspaceTableIssueTypes> {
|
||||||
T extends WorkspaceHealthIssueType | null = null,
|
type: T;
|
||||||
> {
|
|
||||||
type: ConditionalType<
|
|
||||||
T,
|
|
||||||
| WorkspaceHealthIssueType.MISSING_TABLE
|
|
||||||
| WorkspaceHealthIssueType.TABLE_NAME_SHOULD_BE_CUSTOM
|
|
||||||
| WorkspaceHealthIssueType.TABLE_TARGET_TABLE_NAME_NOT_VALID
|
|
||||||
| WorkspaceHealthIssueType.TABLE_DATA_SOURCE_ID_NOT_VALID
|
|
||||||
| WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID
|
|
||||||
>;
|
|
||||||
objectMetadata: ObjectMetadataEntity;
|
objectMetadata: ObjectMetadataEntity;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column issues
|
||||||
|
*/
|
||||||
|
export type WorkspaceColumnIssueTypes =
|
||||||
|
| WorkspaceHealthIssueType.MISSING_COLUMN
|
||||||
|
| WorkspaceHealthIssueType.MISSING_INDEX
|
||||||
|
| 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_OBJECT_REFERENCE_INVALID
|
||||||
|
| WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID
|
||||||
|
| WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID
|
||||||
|
| WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT
|
||||||
|
| WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT
|
||||||
|
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT
|
||||||
|
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID
|
||||||
|
| WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID;
|
||||||
|
|
||||||
export interface WorkspaceHealthColumnIssue<
|
export interface WorkspaceHealthColumnIssue<
|
||||||
T extends WorkspaceHealthIssueType | null = null,
|
T extends WorkspaceColumnIssueTypes,
|
||||||
> {
|
> {
|
||||||
type: ConditionalType<
|
type: T;
|
||||||
T,
|
|
||||||
| WorkspaceHealthIssueType.MISSING_COLUMN
|
|
||||||
| WorkspaceHealthIssueType.MISSING_INDEX
|
|
||||||
| WorkspaceHealthIssueType.MISSING_FOREIGN_KEY
|
|
||||||
| WorkspaceHealthIssueType.MISSING_COMPOSITE_TYPE
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID
|
|
||||||
| WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID
|
|
||||||
>;
|
|
||||||
fieldMetadata: FieldMetadataEntity;
|
fieldMetadata: FieldMetadataEntity;
|
||||||
columnStructure?: WorkspaceTableStructure;
|
columnStructure?: WorkspaceTableStructure;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relation issues
|
||||||
|
*/
|
||||||
|
export type WorkspaceRelationIssueTypes =
|
||||||
|
| WorkspaceHealthIssueType.RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID
|
||||||
|
| WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_NOT_VALID
|
||||||
|
| WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_CONFLICT
|
||||||
|
| WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID;
|
||||||
|
|
||||||
export interface WorkspaceHealthRelationIssue<
|
export interface WorkspaceHealthRelationIssue<
|
||||||
T extends WorkspaceHealthIssueType | null = null,
|
T extends WorkspaceRelationIssueTypes,
|
||||||
> {
|
> {
|
||||||
type: ConditionalType<
|
type: T;
|
||||||
T,
|
|
||||||
| WorkspaceHealthIssueType.RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID
|
|
||||||
| WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_NOT_VALID
|
|
||||||
| WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_CONFLICT
|
|
||||||
| WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID
|
|
||||||
>;
|
|
||||||
fromFieldMetadata: FieldMetadataEntity | undefined;
|
fromFieldMetadata: FieldMetadataEntity | undefined;
|
||||||
toFieldMetadata: FieldMetadataEntity | undefined;
|
toFieldMetadata: FieldMetadataEntity | undefined;
|
||||||
relationMetadata: RelationMetadataEntity;
|
relationMetadata: RelationMetadataEntity;
|
||||||
@ -92,7 +96,20 @@ export interface WorkspaceHealthRelationIssue<
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the interface for the issue type
|
||||||
|
*/
|
||||||
|
export type WorkspaceIssueTypeToInterface<T extends WorkspaceHealthIssueType> =
|
||||||
|
T extends WorkspaceTableIssueTypes
|
||||||
|
? WorkspaceHealthTableIssue<T>
|
||||||
|
: T extends WorkspaceColumnIssueTypes
|
||||||
|
? WorkspaceHealthColumnIssue<T>
|
||||||
|
: T extends WorkspaceRelationIssueTypes
|
||||||
|
? WorkspaceHealthRelationIssue<T>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union of all issues
|
||||||
|
*/
|
||||||
export type WorkspaceHealthIssue =
|
export type WorkspaceHealthIssue =
|
||||||
| WorkspaceHealthTableIssue
|
WorkspaceIssueTypeToInterface<WorkspaceHealthIssueType>;
|
||||||
| WorkspaceHealthColumnIssue
|
|
||||||
| WorkspaceHealthRelationIssue;
|
|
||||||
|
|||||||
@ -125,6 +125,24 @@ export class DatabaseStructureService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async workspaceColumnExist(
|
||||||
|
schemaName: string,
|
||||||
|
tableName: string,
|
||||||
|
columnName: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const mainDataSource = this.typeORMService.getMainDataSource();
|
||||||
|
const results = await mainDataSource.query(
|
||||||
|
`SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = $1
|
||||||
|
AND table_name = $2
|
||||||
|
AND column_name = $3`,
|
||||||
|
[schemaName, tableName, columnName],
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.length >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
getPostgresDataType(fieldMetadata: FieldMetadataEntity): string {
|
getPostgresDataType(fieldMetadata: FieldMetadataEntity): string {
|
||||||
const typeORMType = fieldMetadataTypeToColumnType(fieldMetadata.type);
|
const typeORMType = fieldMetadataTypeToColumnType(fieldMetadata.type);
|
||||||
const mainDataSource = this.typeORMService.getMainDataSource();
|
const mainDataSource = this.typeORMService.getMainDataSource();
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||||
|
|
||||||
|
import isEqual from 'lodash.isequal';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WorkspaceHealthIssue,
|
WorkspaceHealthIssue,
|
||||||
WorkspaceHealthIssueType,
|
WorkspaceHealthIssueType,
|
||||||
@ -23,6 +25,9 @@ import {
|
|||||||
} from 'src/metadata/field-metadata/utils/is-enum-field-metadata-type.util';
|
} from 'src/metadata/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||||
import { validateOptionsForType } from 'src/metadata/field-metadata/utils/validate-options-for-type.util';
|
import { validateOptionsForType } from 'src/metadata/field-metadata/utils/validate-options-for-type.util';
|
||||||
import { serializeDefaultValue } from 'src/metadata/field-metadata/utils/serialize-default-value';
|
import { serializeDefaultValue } from 'src/metadata/field-metadata/utils/serialize-default-value';
|
||||||
|
import { computeCompositeFieldMetadata } from 'src/workspace/workspace-health/utils/compute-composite-field-metadata.util';
|
||||||
|
import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util';
|
||||||
|
import { customNamePrefix } from 'src/workspace/utils/compute-custom-name.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FieldMetadataHealthService {
|
export class FieldMetadataHealthService {
|
||||||
@ -52,10 +57,11 @@ export class FieldMetadataHealthService {
|
|||||||
compositeDefinitions.get(fieldMetadata.type)?.(fieldMetadata) ?? [];
|
compositeDefinitions.get(fieldMetadata.type)?.(fieldMetadata) ?? [];
|
||||||
|
|
||||||
if (options.mode === 'metadata' || options.mode === 'all') {
|
if (options.mode === 'metadata' || options.mode === 'all') {
|
||||||
const targetColumnMapIssues =
|
const targetColumnMapIssue = this.targetColumnMapCheck(fieldMetadata);
|
||||||
this.targetColumnMapCheck(fieldMetadata);
|
|
||||||
|
|
||||||
issues.push(...targetColumnMapIssues);
|
if (targetColumnMapIssue) {
|
||||||
|
issues.push(targetColumnMapIssue);
|
||||||
|
}
|
||||||
|
|
||||||
const defaultValueIssues =
|
const defaultValueIssues =
|
||||||
this.defaultValueHealthCheck(fieldMetadata);
|
this.defaultValueHealthCheck(fieldMetadata);
|
||||||
@ -67,7 +73,10 @@ export class FieldMetadataHealthService {
|
|||||||
const compositeFieldIssues = await this.healthCheckField(
|
const compositeFieldIssues = await this.healthCheckField(
|
||||||
tableName,
|
tableName,
|
||||||
workspaceTableColumns,
|
workspaceTableColumns,
|
||||||
compositeFieldMetadata as FieldMetadataEntity,
|
computeCompositeFieldMetadata(
|
||||||
|
compositeFieldMetadata,
|
||||||
|
fieldMetadata,
|
||||||
|
),
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -169,11 +178,7 @@ export class FieldMetadataHealthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||||
defaultValue &&
|
|
||||||
columnDefaultValue &&
|
|
||||||
isEnumFieldMetadataType(fieldMetadata.type)
|
|
||||||
) {
|
|
||||||
const enumValues = fieldMetadata.options?.map((option) =>
|
const enumValues = fieldMetadata.options?.map((option) =>
|
||||||
serializeDefaultValue(option.value),
|
serializeDefaultValue(option.value),
|
||||||
);
|
);
|
||||||
@ -188,11 +193,7 @@ export class FieldMetadataHealthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (columnDefaultValue !== defaultValue) {
|
||||||
defaultValue &&
|
|
||||||
columnDefaultValue &&
|
|
||||||
columnDefaultValue !== defaultValue
|
|
||||||
) {
|
|
||||||
issues.push({
|
issues.push({
|
||||||
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
|
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
|
||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
@ -210,20 +211,22 @@ export class FieldMetadataHealthService {
|
|||||||
): WorkspaceHealthIssue[] {
|
): WorkspaceHealthIssue[] {
|
||||||
const issues: WorkspaceHealthIssue[] = [];
|
const issues: WorkspaceHealthIssue[] = [];
|
||||||
const columnName = fieldMetadata.targetColumnMap.value;
|
const columnName = fieldMetadata.targetColumnMap.value;
|
||||||
const targetColumnMapIssues = this.targetColumnMapCheck(fieldMetadata);
|
const targetColumnMapIssue = this.targetColumnMapCheck(fieldMetadata);
|
||||||
const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata);
|
const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata);
|
||||||
|
|
||||||
if (Object.keys(fieldMetadata.targetColumnMap).length !== 1) {
|
if (targetColumnMapIssue) {
|
||||||
|
issues.push(targetColumnMapIssue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldMetadata.name.startsWith(customNamePrefix)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
|
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED,
|
||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
message: `Column ${columnName} has more than one target column map, it should only contains "value"`,
|
message: `Column ${columnName} should not be prefixed with "${customNamePrefix}"`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
issues.push(...targetColumnMapIssues);
|
if (fieldMetadata.isCustom && !columnName?.startsWith(customNamePrefix)) {
|
||||||
|
|
||||||
if (fieldMetadata.isCustom && !columnName?.startsWith('_')) {
|
|
||||||
issues.push({
|
issues.push({
|
||||||
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM,
|
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM,
|
||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
@ -277,46 +280,29 @@ export class FieldMetadataHealthService {
|
|||||||
|
|
||||||
private targetColumnMapCheck(
|
private targetColumnMapCheck(
|
||||||
fieldMetadata: FieldMetadataEntity,
|
fieldMetadata: FieldMetadataEntity,
|
||||||
): WorkspaceHealthIssue[] {
|
): WorkspaceHealthIssue | null {
|
||||||
const issues: WorkspaceHealthIssue[] = [];
|
const targetColumnMap = generateTargetColumnMap(
|
||||||
|
fieldMetadata.type,
|
||||||
if (!fieldMetadata.targetColumnMap) {
|
fieldMetadata.isCustom,
|
||||||
issues.push({
|
fieldMetadata.name,
|
||||||
type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
|
);
|
||||||
fieldMetadata,
|
|
||||||
message: `Column targetColumnMap of ${fieldMetadata.name} is empty`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
|
||||||
if (
|
|
||||||
Object.keys(fieldMetadata.targetColumnMap).length !== 1 &&
|
|
||||||
!('value' in fieldMetadata.targetColumnMap)
|
|
||||||
) {
|
|
||||||
issues.push({
|
|
||||||
type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
|
|
||||||
fieldMetadata,
|
|
||||||
message: `Column targetColumnMap "${fieldMetadata.targetColumnMap}" is not valid or well structured`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.isCompositeObjectWellStructured(
|
!fieldMetadata.targetColumnMap ||
|
||||||
fieldMetadata.type,
|
!isEqual(targetColumnMap, fieldMetadata.targetColumnMap)
|
||||||
fieldMetadata.targetColumnMap,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
issues.push({
|
return {
|
||||||
type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
|
type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
|
||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
message: `Column targetColumnMap for composite type ${fieldMetadata.type} is not well structured "${fieldMetadata.targetColumnMap}"`,
|
message: `Column targetColumnMap "${JSON.stringify(
|
||||||
});
|
fieldMetadata.targetColumnMap,
|
||||||
|
)}" is not the same as the generated one "${JSON.stringify(
|
||||||
|
targetColumnMap,
|
||||||
|
)}"`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return issues;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private defaultValueHealthCheck(
|
private defaultValueHealthCheck(
|
||||||
|
|||||||
@ -7,55 +7,92 @@ import { WorkspaceHealthIssue } from 'src/workspace/workspace-health/interfaces/
|
|||||||
|
|
||||||
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||||
import {
|
import { WorkspaceNullableFixer } from 'src/workspace/workspace-health/fixer/workspace-nullable.fixer';
|
||||||
isWorkspaceHealthDefaultValueIssue,
|
import { WorkspaceDefaultValueFixer } from 'src/workspace/workspace-health/fixer/workspace-default-value.fixer';
|
||||||
isWorkspaceHealthNullableIssue,
|
import { WorkspaceTypeFixer } from 'src/workspace/workspace-health/fixer/workspace-type.fixer';
|
||||||
isWorkspaceHealthTypeIssue,
|
import { WorkspaceTargetColumnMapFixer } from 'src/workspace/workspace-health/fixer/workspace-target-column-map.fixer';
|
||||||
} from 'src/workspace/workspace-health/utils/is-workspace-health-issue-type.util';
|
import { CompareEntity } from 'src/workspace/workspace-health/fixer/abstract-workspace.fixer';
|
||||||
|
|
||||||
import { WorkspaceFixNullableService } from './workspace-fix-nullable.service';
|
|
||||||
import { WorkspaceFixTypeService } from './workspace-fix-type.service';
|
|
||||||
import { WorkspaceFixDefaultValueService } from './workspace-fix-default-value.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceFixService {
|
export class WorkspaceFixService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceFixNullableService: WorkspaceFixNullableService,
|
private readonly workspaceNullableFixer: WorkspaceNullableFixer,
|
||||||
private readonly workspaceFixTypeService: WorkspaceFixTypeService,
|
private readonly workspaceDefaultValueFixer: WorkspaceDefaultValueFixer,
|
||||||
private readonly workspaceFixDefaultValueService: WorkspaceFixDefaultValueService,
|
private readonly workspaceTypeFixer: WorkspaceTypeFixer,
|
||||||
|
private readonly workspaceTargetColumnMapFixer: WorkspaceTargetColumnMapFixer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async fix(
|
async createWorkspaceMigrations(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
objectMetadataCollection: ObjectMetadataEntity[],
|
objectMetadataCollection: ObjectMetadataEntity[],
|
||||||
type: WorkspaceHealthFixKind,
|
type: WorkspaceHealthFixKind,
|
||||||
issues: WorkspaceHealthIssue[],
|
issues: WorkspaceHealthIssue[],
|
||||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||||
const services = {
|
switch (type) {
|
||||||
[WorkspaceHealthFixKind.Nullable]: {
|
case WorkspaceHealthFixKind.Nullable: {
|
||||||
service: this.workspaceFixNullableService,
|
const filteredIssues = this.workspaceNullableFixer.filterIssues(issues);
|
||||||
issues: issues.filter((issue) =>
|
|
||||||
isWorkspaceHealthNullableIssue(issue.type),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
[WorkspaceHealthFixKind.Type]: {
|
|
||||||
service: this.workspaceFixTypeService,
|
|
||||||
issues: issues.filter((issue) =>
|
|
||||||
isWorkspaceHealthTypeIssue(issue.type),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
[WorkspaceHealthFixKind.DefaultValue]: {
|
|
||||||
service: this.workspaceFixDefaultValueService,
|
|
||||||
issues: issues.filter((issue) =>
|
|
||||||
isWorkspaceHealthDefaultValueIssue(issue.type),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return services[type].service.fix(
|
return this.workspaceNullableFixer.createWorkspaceMigrations(
|
||||||
manager,
|
manager,
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
services[type].issues,
|
filteredIssues,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
case WorkspaceHealthFixKind.DefaultValue: {
|
||||||
|
const filteredIssues =
|
||||||
|
this.workspaceDefaultValueFixer.filterIssues(issues);
|
||||||
|
|
||||||
|
return this.workspaceDefaultValueFixer.createWorkspaceMigrations(
|
||||||
|
manager,
|
||||||
|
objectMetadataCollection,
|
||||||
|
filteredIssues,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case WorkspaceHealthFixKind.Type: {
|
||||||
|
const filteredIssues = this.workspaceTypeFixer.filterIssues(issues);
|
||||||
|
|
||||||
|
return this.workspaceTypeFixer.createWorkspaceMigrations(
|
||||||
|
manager,
|
||||||
|
objectMetadataCollection,
|
||||||
|
filteredIssues,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case WorkspaceHealthFixKind.TargetColumnMap: {
|
||||||
|
const filteredIssues =
|
||||||
|
this.workspaceTargetColumnMapFixer.filterIssues(issues);
|
||||||
|
|
||||||
|
return this.workspaceTargetColumnMapFixer.createWorkspaceMigrations(
|
||||||
|
manager,
|
||||||
|
objectMetadataCollection,
|
||||||
|
filteredIssues,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMetadataUpdates(
|
||||||
|
manager: EntityManager,
|
||||||
|
objectMetadataCollection: ObjectMetadataEntity[],
|
||||||
|
type: WorkspaceHealthFixKind,
|
||||||
|
issues: WorkspaceHealthIssue[],
|
||||||
|
): Promise<CompareEntity<unknown>[]> {
|
||||||
|
switch (type) {
|
||||||
|
case WorkspaceHealthFixKind.TargetColumnMap: {
|
||||||
|
const filteredIssues =
|
||||||
|
this.workspaceTargetColumnMapFixer.filterIssues(issues);
|
||||||
|
|
||||||
|
return this.workspaceTargetColumnMapFixer.createMetadataUpdates(
|
||||||
|
manager,
|
||||||
|
objectMetadataCollection,
|
||||||
|
filteredIssues,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
|
import { camelCase } from 'src/utils/camel-case';
|
||||||
|
|
||||||
|
// Compute composite field metadata by combining the composite field metadata with the field metadata
|
||||||
|
export const computeCompositeFieldMetadata = (
|
||||||
|
compositeFieldMetadata: FieldMetadataInterface,
|
||||||
|
fieldMetadata: FieldMetadataEntity,
|
||||||
|
): FieldMetadataEntity => ({
|
||||||
|
...fieldMetadata,
|
||||||
|
...compositeFieldMetadata,
|
||||||
|
objectMetadataId: fieldMetadata.objectMetadataId,
|
||||||
|
name: camelCase(`${fieldMetadata.name}-${compositeFieldMetadata.name}`),
|
||||||
|
});
|
||||||
@ -17,3 +17,9 @@ export const isWorkspaceHealthDefaultValueIssue = (
|
|||||||
): type is WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT => {
|
): type is WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT => {
|
||||||
return type === 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;
|
||||||
|
};
|
||||||
|
|||||||
@ -12,10 +12,9 @@ import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace
|
|||||||
import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migration-builder/workspace-migration-builder.module';
|
import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migration-builder/workspace-migration-builder.module';
|
||||||
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
|
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
|
||||||
|
|
||||||
|
import { workspaceFixers } from './fixer';
|
||||||
|
|
||||||
import { WorkspaceFixService } from './services/workspace-fix.service';
|
import { WorkspaceFixService } from './services/workspace-fix.service';
|
||||||
import { WorkspaceFixNullableService } from './services/workspace-fix-nullable.service';
|
|
||||||
import { WorkspaceFixTypeService } from './services/workspace-fix-type.service';
|
|
||||||
import { WorkspaceFixDefaultValueService } from './services/workspace-fix-default-value.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -27,14 +26,12 @@ import { WorkspaceFixDefaultValueService } from './services/workspace-fix-defaul
|
|||||||
WorkspaceMigrationBuilderModule,
|
WorkspaceMigrationBuilderModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
...workspaceFixers,
|
||||||
WorkspaceHealthService,
|
WorkspaceHealthService,
|
||||||
DatabaseStructureService,
|
DatabaseStructureService,
|
||||||
ObjectMetadataHealthService,
|
ObjectMetadataHealthService,
|
||||||
FieldMetadataHealthService,
|
FieldMetadataHealthService,
|
||||||
RelationMetadataHealthService,
|
RelationMetadataHealthService,
|
||||||
WorkspaceFixNullableService,
|
|
||||||
WorkspaceFixTypeService,
|
|
||||||
WorkspaceFixDefaultValueService,
|
|
||||||
WorkspaceFixService,
|
WorkspaceFixService,
|
||||||
],
|
],
|
||||||
exports: [WorkspaceHealthService],
|
exports: [WorkspaceHealthService],
|
||||||
|
|||||||
@ -125,8 +125,12 @@ export class WorkspaceHealthService {
|
|||||||
type: WorkspaceHealthFixKind;
|
type: WorkspaceHealthFixKind;
|
||||||
applyChanges?: boolean;
|
applyChanges?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
): Promise<{
|
||||||
|
workspaceMigrations: Partial<WorkspaceMigrationEntity>[];
|
||||||
|
metadataEntities: unknown[];
|
||||||
|
}> {
|
||||||
let workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
|
let workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
|
||||||
|
let metadataEntities: unknown[] = [];
|
||||||
|
|
||||||
// Set default options
|
// Set default options
|
||||||
options.applyChanges ??= true;
|
options.applyChanges ??= true;
|
||||||
@ -145,7 +149,15 @@ export class WorkspaceHealthService {
|
|||||||
const objectMetadataCollection =
|
const objectMetadataCollection =
|
||||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
||||||
|
|
||||||
workspaceMigrations = await this.workspaceFixService.fix(
|
workspaceMigrations =
|
||||||
|
await this.workspaceFixService.createWorkspaceMigrations(
|
||||||
|
manager,
|
||||||
|
objectMetadataCollection,
|
||||||
|
options.type,
|
||||||
|
issues,
|
||||||
|
);
|
||||||
|
|
||||||
|
metadataEntities = await this.workspaceFixService.createMetadataUpdates(
|
||||||
manager,
|
manager,
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
options.type,
|
options.type,
|
||||||
@ -161,7 +173,10 @@ export class WorkspaceHealthService {
|
|||||||
|
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
|
|
||||||
return workspaceMigrations;
|
return {
|
||||||
|
workspaceMigrations,
|
||||||
|
metadataEntities,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
@ -178,6 +193,9 @@ export class WorkspaceHealthService {
|
|||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceMigrations;
|
return {
|
||||||
|
workspaceMigrations,
|
||||||
|
metadataEntities,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,6 +120,7 @@ export class WorkspaceMigrationFieldFactory {
|
|||||||
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
|
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
|
||||||
|
|
||||||
for (const fieldMetadataUpdate of fieldMetadataUpdateCollection) {
|
for (const fieldMetadataUpdate of fieldMetadataUpdateCollection) {
|
||||||
|
// Skip relations, because they're just representation and not real columns
|
||||||
if (fieldMetadataUpdate.altered.type === FieldMetadataType.RELATION) {
|
if (fieldMetadataUpdate.altered.type === FieldMetadataType.RELATION) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -268,7 +268,7 @@ export class WorkspaceMigrationRunnerService {
|
|||||||
|
|
||||||
// TODO: Maybe we can do something better if we can recreate the old `TableColumn` object
|
// TODO: Maybe we can do something better if we can recreate the old `TableColumn` object
|
||||||
if (enumValues) {
|
if (enumValues) {
|
||||||
// This is returning the old enum values to avoid TypeORM droping the enum type
|
// This is returning the old enum values to avoid TypeORM dropping the enum type
|
||||||
await this.workspaceMigrationEnumService.alterEnum(
|
await this.workspaceMigrationEnumService.alterEnum(
|
||||||
queryRunner,
|
queryRunner,
|
||||||
schemaName,
|
schemaName,
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||||
|
|
||||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||||
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync-metadata.service';
|
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync-metadata.service';
|
||||||
|
import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace-health.service';
|
||||||
|
|
||||||
import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service';
|
import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service';
|
||||||
|
|
||||||
@ -9,6 +12,7 @@ import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.ser
|
|||||||
interface RunWorkspaceMigrationsOptions {
|
interface RunWorkspaceMigrationsOptions {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
|
force?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
@ -16,8 +20,11 @@ interface RunWorkspaceMigrationsOptions {
|
|||||||
description: 'Sync metadata',
|
description: 'Sync metadata',
|
||||||
})
|
})
|
||||||
export class SyncWorkspaceMetadataCommand extends CommandRunner {
|
export class SyncWorkspaceMetadataCommand extends CommandRunner {
|
||||||
|
private readonly logger = new Logger(SyncWorkspaceMetadataCommand.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
||||||
|
private readonly workspaceHealthService: WorkspaceHealthService,
|
||||||
private readonly dataSourceService: DataSourceService,
|
private readonly dataSourceService: DataSourceService,
|
||||||
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
|
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
|
||||||
) {
|
) {
|
||||||
@ -28,7 +35,30 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner {
|
|||||||
_passedParam: string[],
|
_passedParam: string[],
|
||||||
options: RunWorkspaceMigrationsOptions,
|
options: RunWorkspaceMigrationsOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: run in a dedicated job + run queries in a transaction.
|
const issues = await this.workspaceHealthService.healthCheck(
|
||||||
|
options.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Security: abort if there are issues.
|
||||||
|
if (issues.length > 0) {
|
||||||
|
if (!options.force) {
|
||||||
|
this.logger.error(
|
||||||
|
`Workspace contains ${issues.length} issues, aborting.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log('If you want to force the migration, use --force flag');
|
||||||
|
this.logger.log(
|
||||||
|
'Please use `workspace:health` command to check issues and fix them before running this command.',
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(
|
||||||
|
`Workspace contains ${issues.length} issues, sync has been forced.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const dataSourceMetadata =
|
const dataSourceMetadata =
|
||||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||||
options.workspaceId,
|
options.workspaceId,
|
||||||
@ -68,4 +98,13 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner {
|
|||||||
dryRun(): boolean {
|
dryRun(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Option({
|
||||||
|
flags: '-f, --force',
|
||||||
|
description: 'Force migration',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
force(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,18 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||||
import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/workspace-sync-metadata.module';
|
import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||||
|
import { WorkspaceHealthModule } from 'src/workspace/workspace-health/workspace-health.module';
|
||||||
|
|
||||||
import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command';
|
import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command';
|
||||||
|
|
||||||
import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service';
|
import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [WorkspaceSyncMetadataModule, DataSourceModule],
|
imports: [
|
||||||
|
WorkspaceSyncMetadataModule,
|
||||||
|
WorkspaceHealthModule,
|
||||||
|
DataSourceModule,
|
||||||
|
],
|
||||||
providers: [SyncWorkspaceMetadataCommand, SyncWorkspaceLoggerService],
|
providers: [SyncWorkspaceMetadataCommand, SyncWorkspaceLoggerService],
|
||||||
})
|
})
|
||||||
export class WorkspaceSyncMetadataCommandsModule {}
|
export class WorkspaceSyncMetadataCommandsModule {}
|
||||||
|
|||||||
@ -15577,7 +15577,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/lodash.isequal@npm:^4.5.7":
|
"@types/lodash.isequal@npm:^4.5.7, @types/lodash.isequal@npm:^4.5.8":
|
||||||
version: 4.5.8
|
version: 4.5.8
|
||||||
resolution: "@types/lodash.isequal@npm:4.5.8"
|
resolution: "@types/lodash.isequal@npm:4.5.8"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -44115,6 +44115,7 @@ __metadata:
|
|||||||
"@nx/js": "npm:17.2.8"
|
"@nx/js": "npm:17.2.8"
|
||||||
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch"
|
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch"
|
||||||
"@types/lodash.isempty": "npm:^4.4.7"
|
"@types/lodash.isempty": "npm:^4.4.7"
|
||||||
|
"@types/lodash.isequal": "npm:^4.5.8"
|
||||||
"@types/lodash.isobject": "npm:^3.0.7"
|
"@types/lodash.isobject": "npm:^3.0.7"
|
||||||
"@types/lodash.omit": "npm:^4.5.9"
|
"@types/lodash.omit": "npm:^4.5.9"
|
||||||
"@types/lodash.snakecase": "npm:^4.1.7"
|
"@types/lodash.snakecase": "npm:^4.1.7"
|
||||||
@ -44122,6 +44123,7 @@ __metadata:
|
|||||||
"@types/react": "npm:^18.2.39"
|
"@types/react": "npm:^18.2.39"
|
||||||
class-validator: "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch"
|
class-validator: "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch"
|
||||||
graphql-middleware: "npm:^6.1.35"
|
graphql-middleware: "npm:^6.1.35"
|
||||||
|
lodash.isequal: "npm:^4.5.0"
|
||||||
passport: "npm:^0.7.0"
|
passport: "npm:^0.7.0"
|
||||||
rimraf: "npm:^5.0.5"
|
rimraf: "npm:^5.0.5"
|
||||||
typescript: "npm:^5.3.3"
|
typescript: "npm:^5.3.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user