feat: workspace health (#3344)
* feat: wip workspace health * feat: split structure and metadata check * feat: check default value structure health * feat: check targetColumnMap structure health * fix: composite types doesn't have default value properly defined * feat: check default value structure health * feat: check options structure health * fix: verbose option not working properly * fix: word issue * fix: tests * fix: remove console.log * fix: TRUE and FALSE instead of YES and NO * fix: fieldMetadataType instead of type
This commit is contained in:
@ -0,0 +1,355 @@
|
||||
import {
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import {
|
||||
WorkspaceHealthIssue,
|
||||
WorkspaceHealthIssueType,
|
||||
} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
import { WorkspaceTableStructure } from 'src/workspace/workspace-health/interfaces/workspace-table-definition.interface';
|
||||
import { WorkspaceHealthOptions } from 'src/workspace/workspace-health/interfaces/workspace-health-options.interface';
|
||||
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { DatabaseStructureService } from 'src/workspace/workspace-health/services/database-structure.service';
|
||||
import { validName } from 'src/workspace/workspace-health/utils/valid-name.util';
|
||||
import { compositeDefinitions } from 'src/metadata/field-metadata/composite-types';
|
||||
import { validateDefaultValueForType } from 'src/metadata/field-metadata/utils/validate-default-value-for-type.util';
|
||||
import { isEnumFieldMetadataType } 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';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataHealthService {
|
||||
constructor(
|
||||
private readonly databaseStructureService: DatabaseStructureService,
|
||||
) {}
|
||||
|
||||
async healthCheck(
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
fieldMetadataCollection: FieldMetadataEntity[],
|
||||
options: WorkspaceHealthOptions,
|
||||
): Promise<WorkspaceHealthIssue[]> {
|
||||
const issues: WorkspaceHealthIssue[] = [];
|
||||
const workspaceTableColumns =
|
||||
await this.databaseStructureService.getWorkspaceTableColumns(
|
||||
schemaName,
|
||||
tableName,
|
||||
);
|
||||
|
||||
if (!workspaceTableColumns) {
|
||||
throw new NotFoundException(
|
||||
`Table ${tableName} not found in schema ${schemaName}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const fieldMetadata of fieldMetadataCollection) {
|
||||
// Ignore relation fields for now
|
||||
if (
|
||||
fieldMetadata.fromRelationMetadata ||
|
||||
fieldMetadata.toRelationMetadata
|
||||
) {
|
||||
// TODO: Check relation fields
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeFieldMetadataCollection =
|
||||
compositeDefinitions.get(fieldMetadata.type)?.(fieldMetadata) ?? [];
|
||||
|
||||
if (options.mode === 'metadata' || options.mode === 'all') {
|
||||
const targetColumnMapIssues =
|
||||
this.targetColumnMapCheck(fieldMetadata);
|
||||
|
||||
issues.push(...targetColumnMapIssues);
|
||||
|
||||
const defaultValueIssues =
|
||||
this.defaultValueHealthCheck(fieldMetadata);
|
||||
|
||||
issues.push(...defaultValueIssues);
|
||||
}
|
||||
|
||||
for (const compositeFieldMetadata of compositeFieldMetadataCollection) {
|
||||
const compositeFieldIssues = await this.healthCheckField(
|
||||
tableName,
|
||||
workspaceTableColumns,
|
||||
compositeFieldMetadata as FieldMetadataEntity,
|
||||
options,
|
||||
);
|
||||
|
||||
issues.push(...compositeFieldIssues);
|
||||
}
|
||||
} else {
|
||||
const fieldIssues = await this.healthCheckField(
|
||||
tableName,
|
||||
workspaceTableColumns,
|
||||
fieldMetadata,
|
||||
options,
|
||||
);
|
||||
|
||||
issues.push(...fieldIssues);
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
private async healthCheckField(
|
||||
tableName: string,
|
||||
workspaceTableColumns: WorkspaceTableStructure[],
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
options: WorkspaceHealthOptions,
|
||||
): Promise<WorkspaceHealthIssue[]> {
|
||||
const issues: WorkspaceHealthIssue[] = [];
|
||||
|
||||
if (options.mode === 'structure' || options.mode === 'all') {
|
||||
const structureIssues = this.structureFieldCheck(
|
||||
tableName,
|
||||
workspaceTableColumns,
|
||||
fieldMetadata,
|
||||
);
|
||||
|
||||
issues.push(...structureIssues);
|
||||
}
|
||||
|
||||
if (options.mode === 'metadata' || options.mode === 'all') {
|
||||
const metadataIssues = this.metadataFieldCheck(tableName, fieldMetadata);
|
||||
|
||||
issues.push(...metadataIssues);
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
private structureFieldCheck(
|
||||
tableName: string,
|
||||
workspaceTableColumns: WorkspaceTableStructure[],
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
): WorkspaceHealthIssue[] {
|
||||
const issues: WorkspaceHealthIssue[] = [];
|
||||
const columnName = fieldMetadata.targetColumnMap.value;
|
||||
const dataType = this.databaseStructureService.getPostgresDataType(
|
||||
fieldMetadata.type,
|
||||
);
|
||||
const defaultValue = this.databaseStructureService.getPostgresDefault(
|
||||
fieldMetadata.type,
|
||||
fieldMetadata.defaultValue,
|
||||
);
|
||||
const isNullable = fieldMetadata.isNullable ? 'TRUE' : 'FALSE';
|
||||
// Check if column exist in database
|
||||
const columnStructure = workspaceTableColumns.find(
|
||||
(tableDefinition) => tableDefinition.columnName === columnName,
|
||||
);
|
||||
|
||||
if (!columnStructure) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.MISSING_COLUMN,
|
||||
fieldMetadata,
|
||||
columnStructure,
|
||||
message: `Column ${columnName} not found in table ${tableName}`,
|
||||
});
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
// Check if column data type is the same
|
||||
if (columnStructure.dataType !== dataType) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT,
|
||||
fieldMetadata,
|
||||
columnStructure,
|
||||
message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataType}"`,
|
||||
});
|
||||
}
|
||||
|
||||
if (columnStructure.isNullable !== isNullable) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT,
|
||||
fieldMetadata,
|
||||
columnStructure,
|
||||
message: `Column ${columnName} is not nullable as expected`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
defaultValue &&
|
||||
columnStructure.columnDefault &&
|
||||
!columnStructure.columnDefault.startsWith(defaultValue)
|
||||
) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
|
||||
fieldMetadata,
|
||||
columnStructure,
|
||||
message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValue}"`,
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
private metadataFieldCheck(
|
||||
tableName: string,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
): WorkspaceHealthIssue[] {
|
||||
const issues: WorkspaceHealthIssue[] = [];
|
||||
const columnName = fieldMetadata.targetColumnMap.value;
|
||||
const targetColumnMapIssues = this.targetColumnMapCheck(fieldMetadata);
|
||||
const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata);
|
||||
|
||||
if (Object.keys(fieldMetadata.targetColumnMap).length !== 1) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
|
||||
fieldMetadata,
|
||||
message: `Column ${columnName} has more than one target column map, it should only contains "value"`,
|
||||
});
|
||||
}
|
||||
|
||||
issues.push(...targetColumnMapIssues);
|
||||
|
||||
if (fieldMetadata.isCustom && !columnName.startsWith('_')) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_BE_CUSTOM,
|
||||
fieldMetadata,
|
||||
message: `Column ${columnName} is marked as custom in table ${tableName} but doesn't start with "_"`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!fieldMetadata.objectMetadataId) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID,
|
||||
fieldMetadata,
|
||||
message: `Column ${columnName} doesn't have a valid object metadata id`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!Object.values(FieldMetadataType).includes(fieldMetadata.type)) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID,
|
||||
fieldMetadata,
|
||||
message: `Column ${columnName} doesn't have a valid field metadata type`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!fieldMetadata.name ||
|
||||
!validName(fieldMetadata.name) ||
|
||||
!fieldMetadata.label
|
||||
) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID,
|
||||
fieldMetadata,
|
||||
message: `Column ${columnName} doesn't have a valid name or label`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isEnumFieldMetadataType(fieldMetadata.type) &&
|
||||
!validateOptionsForType(fieldMetadata.type, fieldMetadata.options)
|
||||
) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID,
|
||||
fieldMetadata,
|
||||
message: `Column options of ${fieldMetadata.targetColumnMap?.value} is not valid`,
|
||||
});
|
||||
}
|
||||
|
||||
issues.push(...defaultValueIssues);
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
private targetColumnMapCheck(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
): WorkspaceHealthIssue[] {
|
||||
const issues: WorkspaceHealthIssue[] = [];
|
||||
|
||||
if (!fieldMetadata.targetColumnMap) {
|
||||
issues.push({
|
||||
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 (
|
||||
!this.isCompositeObjectWellStructured(
|
||||
fieldMetadata.type,
|
||||
fieldMetadata.targetColumnMap,
|
||||
)
|
||||
) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
|
||||
fieldMetadata,
|
||||
message: `Column targetColumnMap for composite type ${fieldMetadata.type} is not well structured "${fieldMetadata.targetColumnMap}"`,
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
private defaultValueHealthCheck(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
): WorkspaceHealthIssue[] {
|
||||
const issues: WorkspaceHealthIssue[] = [];
|
||||
|
||||
if (
|
||||
!validateDefaultValueForType(
|
||||
fieldMetadata.type,
|
||||
fieldMetadata.defaultValue,
|
||||
)
|
||||
) {
|
||||
issues.push({
|
||||
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
|
||||
fieldMetadata,
|
||||
message: `Column default value for composite type ${fieldMetadata.type} is not well structured`,
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
private isCompositeObjectWellStructured(
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
object: any,
|
||||
): boolean {
|
||||
const subFields = compositeDefinitions.get(fieldMetadataType)?.() ?? [];
|
||||
|
||||
if (!object) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subFields.length === 0) {
|
||||
throw new InternalServerErrorException(
|
||||
`The composite field type ${fieldMetadataType} doesn't have any sub fields, it seems this one is not implemented in the composite definitions map`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const subField of subFields) {
|
||||
if (!object[subField.name]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user