feat: workspace:health nullable fix (#3882)

This commit is contained in:
Jérémy M
2024-02-08 18:22:29 +01:00
committed by GitHub
parent 2ba9a209e8
commit d3fe1b9e31
15 changed files with 265 additions and 131 deletions

View File

@ -5,12 +5,14 @@ import { WorkspaceHealthMode } from 'src/workspace/workspace-health/interfaces/w
import { WorkspaceHealthFixKind } from 'src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface';
import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace-health.service';
import { CommandLogger } from 'src/commands/command-logger';
interface WorkspaceHealthCommandOptions {
workspaceId: string;
verbose?: boolean;
mode?: WorkspaceHealthMode;
fix?: WorkspaceHealthFixKind;
dryRun?: boolean;
}
@Command({
@ -18,6 +20,10 @@ interface WorkspaceHealthCommandOptions {
description: 'Check health of the given workspace.',
})
export class WorkspaceHealthCommand extends CommandRunner {
private readonly commandLogger = new CommandLogger(
WorkspaceHealthCommand.name,
);
constructor(private readonly workspaceHealthService: WorkspaceHealthService) {
super();
}
@ -48,11 +54,29 @@ export class WorkspaceHealthCommand extends CommandRunner {
}
if (options.fix) {
await this.workspaceHealthService.fixIssues(
console.log(chalk.yellow('Fixing issues'));
const workspaceMigrations = await this.workspaceHealthService.fixIssues(
options.workspaceId,
issues,
options.fix,
{
type: options.fix,
applyChanges: !options.dryRun,
},
);
if (options.dryRun) {
await this.commandLogger.writeLog(
`workspace-health-${options.fix}-migrations`,
workspaceMigrations,
);
} else {
console.log(
chalk.green(
`Fixed ${workspaceMigrations.length}/${issues.length} issues`,
),
);
}
}
}
@ -100,4 +124,13 @@ export class WorkspaceHealthCommand extends CommandRunner {
return value as WorkspaceHealthMode;
}
@Option({
flags: '-d, --dry-run',
description: 'Dry run without applying changes',
required: false,
})
dryRun(): boolean {
return true;
}
}

View File

@ -26,7 +26,6 @@ export enum WorkspaceHealthIssueType {
COLUMN_OPTIONS_NOT_VALID = 'COLUMN_OPTIONS_NOT_VALID',
RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID = 'RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID',
RELATION_FOREIGN_KEY_NOT_VALID = 'RELATION_FOREIGN_KEY_NOT_VALID',
RELATION_NULLABILITY_CONFLICT = 'RELATION_NULLABILITY_CONFLICT',
RELATION_FOREIGN_KEY_CONFLICT = 'RELATION_FOREIGN_KEY_CONFLICT',
RELATION_TYPE_NOT_VALID = 'RELATION_TYPE_NOT_VALID',
}
@ -83,7 +82,6 @@ export interface WorkspaceHealthRelationIssue<
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
>;

View File

@ -188,16 +188,6 @@ export class RelationMetadataHealthService {
});
}
if (relationColumn.isNullable !== relationFieldMetadata.isNullable) {
issues.push({
type: WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT,
fromFieldMetadata,
toFieldMetadata,
relationMetadata,
message: `Relation ${relationMetadata.id} foreign key is not properly set`,
});
}
if (
relationMetadata.relationType === RelationMetadataType.ONE_TO_ONE &&
!relationColumn.isUnique

View File

@ -5,29 +5,69 @@ import { EntityManager } from 'typeorm';
import {
WorkspaceHealthColumnIssue,
WorkspaceHealthIssueType,
WorkspaceHealthRelationIssue,
} 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 { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
type WorkspaceHealthNullableIssue =
| WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>
| WorkspaceHealthRelationIssue<WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT>;
WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>;
@Injectable()
export class WorkspaceFixNullableService {
constructor() {}
constructor(
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
) {}
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 [];
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
for (const issue of issues) {
switch (issue.type) {
case WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT: {
const columnNullabilityWorkspaceMigrations =
await this.fixColumnNullabilityIssues(
objectMetadataCollection,
issues.filter(
(issue) =>
issue.type ===
WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT,
) as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>[],
);
workspaceMigrations.push(...columnNullabilityWorkspaceMigrations);
break;
}
}
}
return workspaceMigrations;
}
private async fixColumnNullabilityIssues(
objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const fieldMetadataUpdateCollection = issues.map((issue) => {
return {
current: {
...issue.fieldMetadata,
isNullable: issue.columnStructure?.isNullable ?? false,
},
altered: issue.fieldMetadata,
};
});
return this.workspaceMigrationFieldFactory.create(
objectMetadataCollection,
fieldMetadataUpdateCollection,
WorkspaceMigrationBuilderAction.UPDATE,
);
}
}

View File

@ -2,11 +2,6 @@ import { WorkspaceHealthIssueType } from 'src/workspace/workspace-health/interfa
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;
): type is WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT => {
return type === WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT;
};

View File

@ -121,8 +121,16 @@ export class WorkspaceHealthService {
async fixIssues(
workspaceId: string,
issues: WorkspaceHealthIssue[],
type: WorkspaceHealthFixKind,
): Promise<void> {
options: {
type: WorkspaceHealthFixKind;
applyChanges?: boolean;
},
): Promise<Partial<WorkspaceMigrationEntity>[]> {
let workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
// Set default options
options.applyChanges ??= true;
const queryRunner = this.metadataDataSource.createQueryRunner();
await queryRunner.connect();
@ -137,16 +145,25 @@ export class WorkspaceHealthService {
const objectMetadataCollection =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
const workspaceMigrations = await this.workspaceFixService.fix(
workspaceMigrations = await this.workspaceFixService.fix(
manager,
objectMetadataCollection,
type,
options.type,
issues,
);
// Save workspace migrations into the database
await workspaceMigrationRepository.save(workspaceMigrations);
if (!options.applyChanges) {
// Rollback transactions
await queryRunner.rollbackTransaction();
await queryRunner.release();
return workspaceMigrations;
}
// Commit the transaction
await queryRunner.commitTransaction();
@ -160,5 +177,7 @@ export class WorkspaceHealthService {
} finally {
await queryRunner.release();
}
return workspaceMigrations;
}
}