Feat/workspace health core fix (#3863)
* feat: add deletion support on sync metadata command * fix: remove debug * feat: wip workspace health command add --fix option fix: remove test * feat: core of --fix option for workspace-health
This commit is contained in:
@ -2,6 +2,7 @@ import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { WorkspaceHealthMode } from 'src/workspace/workspace-health/interfaces/workspace-health-options.interface';
|
||||
import { WorkspaceHealthFixKind } from 'src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface';
|
||||
|
||||
import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace-health.service';
|
||||
|
||||
@ -9,6 +10,7 @@ interface WorkspaceHealthCommandOptions {
|
||||
workspaceId: string;
|
||||
verbose?: boolean;
|
||||
mode?: WorkspaceHealthMode;
|
||||
fix?: WorkspaceHealthFixKind;
|
||||
}
|
||||
|
||||
@Command({
|
||||
@ -44,6 +46,14 @@ export class WorkspaceHealthCommand extends CommandRunner {
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
if (options.fix) {
|
||||
await this.workspaceHealthService.fixIssues(
|
||||
options.workspaceId,
|
||||
issues,
|
||||
options.fix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Option({
|
||||
@ -55,6 +65,19 @@ export class WorkspaceHealthCommand extends CommandRunner {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-f, --fix [kind]',
|
||||
description: 'fix issues',
|
||||
required: false,
|
||||
})
|
||||
fix(value: string): WorkspaceHealthFixKind {
|
||||
if (!Object.values(WorkspaceHealthFixKind).includes(value as any)) {
|
||||
throw new Error(`Invalid fix kind ${value}`);
|
||||
}
|
||||
|
||||
return value as WorkspaceHealthFixKind;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-v, --verbose',
|
||||
description: 'Detailed output',
|
||||
@ -71,6 +94,10 @@ export class WorkspaceHealthCommand extends CommandRunner {
|
||||
defaultValue: WorkspaceHealthMode.All,
|
||||
})
|
||||
parseMode(value: string): WorkspaceHealthMode {
|
||||
if (!Object.values(WorkspaceHealthMode).includes(value as any)) {
|
||||
throw new Error(`Invalid mode ${value}`);
|
||||
}
|
||||
|
||||
return value as WorkspaceHealthMode;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export enum WorkspaceHealthFixKind {
|
||||
Nullable = 'nullable',
|
||||
Type = 'type',
|
||||
DefaultValue = 'default-value',
|
||||
TargetColumnMap = 'target-column-map',
|
||||
}
|
||||
@ -31,19 +31,31 @@ export enum WorkspaceHealthIssueType {
|
||||
RELATION_TYPE_NOT_VALID = 'RELATION_TYPE_NOT_VALID',
|
||||
}
|
||||
|
||||
export interface WorkspaceHealthTableIssue {
|
||||
type:
|
||||
type ConditionalType<
|
||||
T extends WorkspaceHealthIssueType | null,
|
||||
U,
|
||||
> = T extends WorkspaceHealthIssueType ? T : U;
|
||||
|
||||
export interface WorkspaceHealthTableIssue<
|
||||
T extends WorkspaceHealthIssueType | null = null,
|
||||
> {
|
||||
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;
|
||||
| WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID
|
||||
>;
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceHealthColumnIssue {
|
||||
type:
|
||||
export interface WorkspaceHealthColumnIssue<
|
||||
T extends WorkspaceHealthIssueType | null = null,
|
||||
> {
|
||||
type: ConditionalType<
|
||||
T,
|
||||
| WorkspaceHealthIssueType.MISSING_COLUMN
|
||||
| WorkspaceHealthIssueType.MISSING_INDEX
|
||||
| WorkspaceHealthIssueType.MISSING_FOREIGN_KEY
|
||||
@ -57,19 +69,24 @@ export interface WorkspaceHealthColumnIssue {
|
||||
| WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT
|
||||
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT
|
||||
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID
|
||||
| WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID;
|
||||
| WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID
|
||||
>;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
columnStructure?: WorkspaceTableStructure;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceHealthRelationIssue {
|
||||
type:
|
||||
export interface WorkspaceHealthRelationIssue<
|
||||
T extends WorkspaceHealthIssueType | null = null,
|
||||
> {
|
||||
type: ConditionalType<
|
||||
T,
|
||||
| WorkspaceHealthIssueType.RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID
|
||||
| WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_NOT_VALID
|
||||
| WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT
|
||||
| WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_CONFLICT
|
||||
| WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID;
|
||||
| WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID
|
||||
>;
|
||||
fromFieldMetadata: FieldMetadataEntity | undefined;
|
||||
toFieldMetadata: FieldMetadataEntity | undefined;
|
||||
relationMetadata: RelationMetadataEntity;
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import {
|
||||
WorkspaceHealthColumnIssue,
|
||||
WorkspaceHealthIssueType,
|
||||
WorkspaceHealthRelationIssue,
|
||||
} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
|
||||
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
type WorkspaceHealthNullableIssue =
|
||||
| WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>
|
||||
| WorkspaceHealthRelationIssue<WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT>;
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceFixNullableService {
|
||||
constructor() {}
|
||||
|
||||
async fix(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
manager: EntityManager,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
issues: WorkspaceHealthNullableIssue[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
// TODO: Implement nullable fix
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceHealthFixKind } from 'src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface';
|
||||
import { WorkspaceHealthIssue } from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
|
||||
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import { isWorkspaceHealthNullableIssue } from 'src/workspace/workspace-health/utils/is-workspace-health-issue-type.util';
|
||||
|
||||
import { WorkspaceFixNullableService } from './workspace-fix-nullable.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceFixService {
|
||||
constructor(
|
||||
private readonly workspaceFixNullableService: WorkspaceFixNullableService,
|
||||
) {}
|
||||
|
||||
async fix(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
type: WorkspaceHealthFixKind,
|
||||
issues: WorkspaceHealthIssue[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
const services = {
|
||||
[WorkspaceHealthFixKind.Nullable]: {
|
||||
service: this.workspaceFixNullableService,
|
||||
issues: issues.filter((issue) =>
|
||||
isWorkspaceHealthNullableIssue(issue.type),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return services[type].service.fix(
|
||||
manager,
|
||||
objectMetadataCollection,
|
||||
services[type].issues,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { WorkspaceHealthIssueType } from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
|
||||
export const isWorkspaceHealthNullableIssue = (
|
||||
type: WorkspaceHealthIssueType,
|
||||
): type is
|
||||
| WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT
|
||||
| WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT => {
|
||||
return type === WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT ||
|
||||
type === WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
@ -9,6 +9,11 @@ import { FieldMetadataHealthService } from 'src/workspace/workspace-health/servi
|
||||
import { ObjectMetadataHealthService } from 'src/workspace/workspace-health/services/object-metadata-health.service';
|
||||
import { RelationMetadataHealthService } from 'src/workspace/workspace-health/services/relation-metadata.health.service';
|
||||
import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace-health.service';
|
||||
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 { WorkspaceFixService } from './services/workspace-fix.service';
|
||||
import { WorkspaceFixNullableService } from './services/workspace-fix-nullable.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -16,6 +21,8 @@ import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace
|
||||
TypeORMModule,
|
||||
ObjectMetadataModule,
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMigrationBuilderModule,
|
||||
],
|
||||
providers: [
|
||||
WorkspaceHealthService,
|
||||
@ -23,6 +30,8 @@ import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace
|
||||
ObjectMetadataHealthService,
|
||||
FieldMetadataHealthService,
|
||||
RelationMetadataHealthService,
|
||||
WorkspaceFixNullableService,
|
||||
WorkspaceFixService,
|
||||
],
|
||||
exports: [WorkspaceHealthService],
|
||||
})
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { WorkspaceHealthIssue } from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
import {
|
||||
WorkspaceHealthMode,
|
||||
WorkspaceHealthOptions,
|
||||
} from 'src/workspace/workspace-health/interfaces/workspace-health-options.interface';
|
||||
import { WorkspaceHealthFixKind } from 'src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||
@ -15,10 +19,15 @@ import { FieldMetadataHealthService } from 'src/workspace/workspace-health/servi
|
||||
import { RelationMetadataHealthService } from 'src/workspace/workspace-health/services/relation-metadata.health.service';
|
||||
import { DatabaseStructureService } from 'src/workspace/workspace-health/services/database-structure.service';
|
||||
import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
|
||||
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { WorkspaceFixService } from 'src/workspace/workspace-health/services/workspace-fix.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceHealthService {
|
||||
constructor(
|
||||
@InjectDataSource('metadata')
|
||||
private readonly metadataDataSource: DataSource,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
@ -27,6 +36,8 @@ export class WorkspaceHealthService {
|
||||
private readonly objectMetadataHealthService: ObjectMetadataHealthService,
|
||||
private readonly fieldMetadataHealthService: FieldMetadataHealthService,
|
||||
private readonly relationMetadataHealthService: RelationMetadataHealthService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceFixService: WorkspaceFixService,
|
||||
) {}
|
||||
|
||||
async healthCheck(
|
||||
@ -106,4 +117,48 @@ export class WorkspaceHealthService {
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
async fixIssues(
|
||||
workspaceId: string,
|
||||
issues: WorkspaceHealthIssue[],
|
||||
type: WorkspaceHealthFixKind,
|
||||
): Promise<void> {
|
||||
const queryRunner = this.metadataDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const manager = queryRunner.manager;
|
||||
|
||||
try {
|
||||
const workspaceMigrationRepository = manager.getRepository(
|
||||
WorkspaceMigrationEntity,
|
||||
);
|
||||
const objectMetadataCollection =
|
||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
||||
|
||||
const workspaceMigrations = await this.workspaceFixService.fix(
|
||||
manager,
|
||||
objectMetadataCollection,
|
||||
type,
|
||||
issues,
|
||||
);
|
||||
|
||||
// Save workspace migrations into the database
|
||||
await workspaceMigrationRepository.save(workspaceMigrations);
|
||||
|
||||
// Commit the transaction
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// Apply pending migrations
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
console.error('Fix of issues failed with:', error);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user