feat: workspace health relation (#3466)

feat: add check relation health
This commit is contained in:
Jérémy M
2024-01-17 17:05:35 +01:00
committed by GitHub
parent 64110c591a
commit 4b7e42c38e
9 changed files with 401 additions and 52 deletions

View File

@ -26,6 +26,8 @@ import {
RelationMetadataType, RelationMetadataType,
} from './relation-metadata.entity'; } from './relation-metadata.entity';
import { createRelationMetadataForeignKey } from './utils/create-relation-metadata-foreign-key.util';
@Injectable() @Injectable()
export class RelationMetadataService extends TypeOrmQueryService<RelationMetadataEntity> { export class RelationMetadataService extends TypeOrmQueryService<RelationMetadataEntity> {
constructor( constructor(
@ -54,10 +56,10 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
// NOTE: this logic is called to create relation through metadata graphql endpoint (so only for custom field relations) // NOTE: this logic is called to create relation through metadata graphql endpoint (so only for custom field relations)
const isCustom = true; const isCustom = true;
const baseColumnName = `${camelCase(relationMetadataInput.toName)}Id`; const baseColumnName = `${camelCase(relationMetadataInput.toName)}Id`;
const foreignKeyColumnName = createRelationMetadataForeignKey(
const foreignKeyColumnName = isCustom relationMetadataInput.toName,
? createCustomColumnName(baseColumnName) isCustom,
: baseColumnName; );
const createdFields = await this.fieldMetadataService.createMany([ const createdFields = await this.fieldMetadataService.createMany([
this.createFieldMetadataForRelationMetadata( this.createFieldMetadataForRelationMetadata(

View File

@ -0,0 +1,15 @@
import { createCustomColumnName } from 'src/metadata/utils/create-custom-column-name.util';
import { camelCase } from 'src/utils/camel-case';
export const createRelationMetadataForeignKey = (
name: string,
isCustom?: boolean,
) => {
const baseColumnName = `${camelCase(name)}Id`;
const foreignKeyColumnName = isCustom
? createCustomColumnName(baseColumnName)
: baseColumnName;
return foreignKeyColumnName;
};

View File

@ -2,6 +2,7 @@ import { WorkspaceTableStructure } from 'src/workspace/workspace-health/interfac
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
export enum WorkspaceHealthIssueType { export enum WorkspaceHealthIssueType {
MISSING_TABLE = 'MISSING_TABLE', MISSING_TABLE = 'MISSING_TABLE',
@ -23,6 +24,11 @@ export enum WorkspaceHealthIssueType {
COLUMN_DEFAULT_VALUE_CONFLICT = 'COLUMN_DEFAULT_VALUE_CONFLICT', COLUMN_DEFAULT_VALUE_CONFLICT = 'COLUMN_DEFAULT_VALUE_CONFLICT',
COLUMN_DEFAULT_VALUE_NOT_VALID = 'COLUMN_DEFAULT_VALUE_NOT_VALID', COLUMN_DEFAULT_VALUE_NOT_VALID = 'COLUMN_DEFAULT_VALUE_NOT_VALID',
COLUMN_OPTIONS_NOT_VALID = 'COLUMN_OPTIONS_NOT_VALID', 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',
} }
export interface WorkspaceHealthTableIssue { export interface WorkspaceHealthTableIssue {
@ -57,6 +63,21 @@ export interface WorkspaceHealthColumnIssue {
message: string; message: string;
} }
export interface WorkspaceHealthRelationIssue {
type:
| 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;
fromFieldMetadata: FieldMetadataEntity | undefined;
toFieldMetadata: FieldMetadataEntity | undefined;
relationMetadata: RelationMetadataEntity;
columnStructure?: WorkspaceTableStructure;
message: string;
}
export type WorkspaceHealthIssue = export type WorkspaceHealthIssue =
| WorkspaceHealthTableIssue | WorkspaceHealthTableIssue
| WorkspaceHealthColumnIssue; | WorkspaceHealthColumnIssue
| WorkspaceHealthRelationIssue;

View File

@ -3,7 +3,13 @@ export interface WorkspaceTableStructure {
tableName: string; tableName: string;
columnName: string; columnName: string;
dataType: string; dataType: string;
isNullable: string;
columnDefault: string; columnDefault: string;
isPrimaryKey: string; isNullable: boolean;
isPrimaryKey: boolean;
isForeignKey: boolean;
isUnique: boolean;
} }
export type WorkspaceTableStructureResult = {
[P in keyof WorkspaceTableStructure]: string;
};

View File

@ -3,7 +3,10 @@ import { Injectable } from '@nestjs/common';
import { ColumnType } from 'typeorm'; import { ColumnType } from 'typeorm';
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'; import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
import { WorkspaceTableStructure } from 'src/workspace/workspace-health/interfaces/workspace-table-definition.interface'; import {
WorkspaceTableStructure,
WorkspaceTableStructureResult,
} from 'src/workspace/workspace-health/interfaces/workspace-table-definition.interface';
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service';
@ -20,33 +23,101 @@ export class DatabaseStructureService {
tableName: string, tableName: string,
): Promise<WorkspaceTableStructure[]> { ): Promise<WorkspaceTableStructure[]> {
const mainDataSource = this.typeORMService.getMainDataSource(); const mainDataSource = this.typeORMService.getMainDataSource();
const results = await mainDataSource.query<
return mainDataSource.query<WorkspaceTableStructure[]>(` WorkspaceTableStructureResult[]
SELECT >(`
c.table_schema as "tableSchema", WITH foreign_keys AS (
c.table_name as "tableName", SELECT
c.column_name as "columnName", kcu.table_schema AS schema_name,
c.data_type as "dataType", kcu.table_name AS table_name,
c.is_nullable as "isNullable", kcu.column_name AS column_name,
c.column_default as "columnDefault", tc.constraint_name AS constraint_name
CASE FROM
information_schema.key_column_usage AS kcu
JOIN
information_schema.table_constraints AS tc
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE
tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = '${schemaName}'
AND tc.table_name = '${tableName}'
),
unique_constraints AS (
SELECT
tc.table_schema AS schema_name,
tc.table_name AS table_name,
kcu.column_name AS column_name
FROM
information_schema.key_column_usage AS kcu
JOIN
information_schema.table_constraints AS tc
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE
tc.constraint_type = 'UNIQUE'
AND tc.table_schema = '${schemaName}'
AND tc.table_name = '${tableName}'
)
SELECT
c.table_schema AS "tableSchema",
c.table_name AS "tableName",
c.column_name AS "columnName",
c.data_type AS "dataType",
c.is_nullable AS "isNullable",
c.column_default AS "columnDefault",
CASE
WHEN pk.constraint_type = 'PRIMARY KEY' THEN 'TRUE' WHEN pk.constraint_type = 'PRIMARY KEY' THEN 'TRUE'
ELSE 'FALSE' ELSE 'FALSE'
END as "isPrimaryKey" END AS "isPrimaryKey",
FROM CASE
information_schema.columns c WHEN fk.constraint_name IS NOT NULL THEN 'TRUE'
LEFT JOIN ELSE 'FALSE'
information_schema.constraint_column_usage as ccu ON c.column_name = ccu.column_name AND c.table_name = ccu.table_name END AS "isForeignKey",
LEFT JOIN CASE
information_schema.table_constraints as pk ON pk.constraint_name = ccu.constraint_name AND pk.constraint_type = 'PRIMARY KEY' WHEN uc.column_name IS NOT NULL THEN 'TRUE'
WHERE ELSE 'FALSE'
END AS "isUnique"
FROM
information_schema.columns AS c
LEFT JOIN
information_schema.constraint_column_usage AS ccu
ON c.column_name = ccu.column_name
AND c.table_name = ccu.table_name
LEFT JOIN
information_schema.table_constraints AS pk
ON pk.constraint_name = ccu.constraint_name
AND pk.constraint_type = 'PRIMARY KEY'
LEFT JOIN
foreign_keys AS fk
ON c.table_schema = fk.schema_name
AND c.table_name = fk.table_name
AND c.column_name = fk.column_name
LEFT JOIN
unique_constraints AS uc
ON c.table_schema = uc.schema_name
AND c.table_name = uc.table_name
AND c.column_name = uc.column_name
WHERE
c.table_schema = '${schemaName}' c.table_schema = '${schemaName}'
AND c.table_name = '${tableName}'; AND c.table_name = '${tableName}';
`); `);
if (!results || results.length === 0) {
return [];
}
return results.map((item) => ({
...item,
isNullable: item.isNullable === 'YES',
isPrimaryKey: item.isPrimaryKey === 'TRUE',
isForeignKey: item.isForeignKey === 'TRUE',
isUnique: item.isUnique === 'TRUE',
}));
} }
getPostgresDataType(fieldMedataType: FieldMetadataType): string { getPostgresDataType(fieldMetadataType: FieldMetadataType): string {
const typeORMType = fieldMetadataTypeToColumnType(fieldMedataType); const typeORMType = fieldMetadataTypeToColumnType(fieldMetadataType);
const mainDataSource = this.typeORMService.getMainDataSource(); const mainDataSource = this.typeORMService.getMainDataSource();
return mainDataSource.driver.normalizeType({ return mainDataSource.driver.normalizeType({

View File

@ -1,8 +1,4 @@
import { import { Injectable, InternalServerErrorException } from '@nestjs/common';
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { import {
WorkspaceHealthIssue, WorkspaceHealthIssue,
@ -30,31 +26,19 @@ export class FieldMetadataHealthService {
) {} ) {}
async healthCheck( async healthCheck(
schemaName: string,
tableName: string, tableName: string,
workspaceTableColumns: WorkspaceTableStructure[],
fieldMetadataCollection: FieldMetadataEntity[], fieldMetadataCollection: FieldMetadataEntity[],
options: WorkspaceHealthOptions, options: WorkspaceHealthOptions,
): Promise<WorkspaceHealthIssue[]> { ): Promise<WorkspaceHealthIssue[]> {
const issues: 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) { for (const fieldMetadata of fieldMetadataCollection) {
// Ignore relation fields for now // Relation metadata are checked in another service
if ( if (
fieldMetadata.fromRelationMetadata || fieldMetadata.fromRelationMetadata ||
fieldMetadata.toRelationMetadata fieldMetadata.toRelationMetadata
) { ) {
// TODO: Check relation fields
continue; continue;
} }
@ -140,7 +124,6 @@ export class FieldMetadataHealthService {
fieldMetadata.type, fieldMetadata.type,
fieldMetadata.defaultValue, fieldMetadata.defaultValue,
); );
const isNullable = fieldMetadata.isNullable ? 'TRUE' : 'FALSE';
// Check if column exist in database // Check if column exist in database
const columnStructure = workspaceTableColumns.find( const columnStructure = workspaceTableColumns.find(
(tableDefinition) => tableDefinition.columnName === columnName, (tableDefinition) => tableDefinition.columnName === columnName,
@ -167,7 +150,7 @@ export class FieldMetadataHealthService {
}); });
} }
if (columnStructure.isNullable !== isNullable) { if (columnStructure.isNullable !== fieldMetadata.isNullable) {
issues.push({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT, type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT,
fieldMetadata, fieldMetadata,
@ -211,7 +194,7 @@ export class FieldMetadataHealthService {
issues.push(...targetColumnMapIssues); issues.push(...targetColumnMapIssues);
if (fieldMetadata.isCustom && !columnName.startsWith('_')) { 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,

View File

@ -0,0 +1,222 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceTableStructure } from 'src/workspace/workspace-health/interfaces/workspace-table-definition.interface';
import {
WorkspaceHealthIssue,
WorkspaceHealthIssueType,
} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface';
import {
WorkspaceHealthMode,
WorkspaceHealthOptions,
} from 'src/workspace/workspace-health/interfaces/workspace-health-options.interface';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import {
RelationMetadataEntity,
RelationMetadataType,
} from 'src/metadata/relation-metadata/relation-metadata.entity';
import { createRelationMetadataForeignKey } from 'src/metadata/relation-metadata/utils/create-relation-metadata-foreign-key.util';
import {
RelationDirection,
deduceRelationDirection,
} from 'src/workspace/utils/deduce-relation-direction.util';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
@Injectable()
export class RelationMetadataHealthService {
constructor() {}
public healthCheck(
workspaceTableColumns: WorkspaceTableStructure[],
objectMetadataCollection: ObjectMetadataEntity[],
objectMetadata: ObjectMetadataEntity,
options: WorkspaceHealthOptions,
) {
const issues: WorkspaceHealthIssue[] = [];
for (const fieldMetadata of objectMetadata.fields) {
// We're only interested in relation fields
if (fieldMetadata.type !== FieldMetadataType.RELATION) {
continue;
}
const relationMetadata =
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
const relationDirection = deduceRelationDirection(
objectMetadata.id,
relationMetadata,
);
// Many to many relations are not supported yet
if (relationMetadata.relationType === RelationMetadataType.MANY_TO_MANY) {
return [];
}
const fromObjectMetadata = objectMetadataCollection.find(
(objectMetadata) =>
objectMetadata.id === relationMetadata.fromObjectMetadataId,
);
const fromFieldMetadata = fromObjectMetadata?.fields.find(
(fieldMetadata) =>
fieldMetadata.id === relationMetadata.fromFieldMetadataId,
);
const toObjectMetadata = objectMetadataCollection.find(
(objectMetadata) =>
objectMetadata.id === relationMetadata.toObjectMetadataId,
);
const toFieldMetadata = toObjectMetadata?.fields.find(
(fieldMetadata) =>
fieldMetadata.id === relationMetadata.toFieldMetadataId,
);
if (!fromFieldMetadata || !toFieldMetadata) {
issues.push({
type: WorkspaceHealthIssueType.RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID,
fromFieldMetadata,
toFieldMetadata,
relationMetadata,
message: `Relation ${relationMetadata.id} has invalid from or to field metadata`,
});
return issues;
}
if (
options.mode === WorkspaceHealthMode.All ||
options.mode === WorkspaceHealthMode.Structure
) {
// Check relation structure
const structureIssues = this.structureRelationCheck(
fromFieldMetadata,
toFieldMetadata,
toObjectMetadata?.fields ?? [],
relationDirection,
relationMetadata,
workspaceTableColumns,
);
issues.push(...structureIssues);
}
if (
options.mode === WorkspaceHealthMode.All ||
options.mode === WorkspaceHealthMode.Metadata
) {
// Check relation metadata
const metadataIssues = this.metadataRelationCheck(
fromFieldMetadata,
toFieldMetadata,
relationDirection,
relationMetadata,
);
issues.push(...metadataIssues);
}
}
return issues;
}
private structureRelationCheck(
fromFieldMetadata: FieldMetadataEntity,
toFieldMetadata: FieldMetadataEntity,
toObjectMetadataFields: FieldMetadataEntity[],
relationDirection: RelationDirection,
relationMetadata: RelationMetadataEntity,
workspaceTableColumns: WorkspaceTableStructure[],
): WorkspaceHealthIssue[] {
const issues: WorkspaceHealthIssue[] = [];
// Nothing to check on the structure
if (relationDirection === RelationDirection.FROM) {
return [];
}
const isCustom = toFieldMetadata.isCustom ?? false;
const foreignKeyColumnName = createRelationMetadataForeignKey(
toFieldMetadata.name,
isCustom,
);
const relationColumn = workspaceTableColumns.find(
(column) => column.columnName === foreignKeyColumnName,
);
const relationFieldMetadata = toObjectMetadataFields.find(
(fieldMetadata) => fieldMetadata.name === foreignKeyColumnName,
);
if (!relationColumn || !relationFieldMetadata) {
issues.push({
type: WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_NOT_VALID,
fromFieldMetadata,
toFieldMetadata,
relationMetadata,
message: `Relation ${relationMetadata.id} doesn't have a valid foreign key`,
});
return issues;
}
if (!relationColumn.isForeignKey) {
issues.push({
type: WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_CONFLICT,
fromFieldMetadata,
toFieldMetadata,
relationMetadata,
message: `Relation ${relationMetadata.id} foreign key is not properly set`,
});
}
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
) {
issues.push({
type: WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_CONFLICT,
fromFieldMetadata,
toFieldMetadata,
relationMetadata,
message: `Relation ${relationMetadata.id} foreign key is not marked as unique and relation type is one-to-one`,
});
}
return issues;
}
private metadataRelationCheck(
fromFieldMetadata: FieldMetadataEntity,
toFieldMetadata: FieldMetadataEntity,
relationDirection: RelationDirection,
relationMetadata: RelationMetadataEntity,
): WorkspaceHealthIssue[] {
const issues: WorkspaceHealthIssue[] = [];
if (
!Object.values(RelationMetadataType).includes(
relationMetadata.relationType,
)
) {
issues.push({
type: WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID,
fromFieldMetadata,
toFieldMetadata,
relationMetadata,
message: `Relation ${relationMetadata.id} has invalid relation type`,
});
}
return issues;
}
}

View File

@ -7,6 +7,7 @@ import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/wo
import { DatabaseStructureService } from 'src/workspace/workspace-health/services/database-structure.service'; import { DatabaseStructureService } from 'src/workspace/workspace-health/services/database-structure.service';
import { FieldMetadataHealthService } from 'src/workspace/workspace-health/services/field-metadata-health.service'; import { FieldMetadataHealthService } from 'src/workspace/workspace-health/services/field-metadata-health.service';
import { ObjectMetadataHealthService } from 'src/workspace/workspace-health/services/object-metadata-health.service'; 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 { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace-health.service';
@Module({ @Module({
@ -21,6 +22,7 @@ import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace
DatabaseStructureService, DatabaseStructureService,
ObjectMetadataHealthService, ObjectMetadataHealthService,
FieldMetadataHealthService, FieldMetadataHealthService,
RelationMetadataHealthService,
], ],
exports: [WorkspaceHealthService], exports: [WorkspaceHealthService],
}) })

View File

@ -12,6 +12,8 @@ import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metad
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { ObjectMetadataHealthService } from 'src/workspace/workspace-health/services/object-metadata-health.service'; import { ObjectMetadataHealthService } from 'src/workspace/workspace-health/services/object-metadata-health.service';
import { FieldMetadataHealthService } from 'src/workspace/workspace-health/services/field-metadata-health.service'; import { FieldMetadataHealthService } from 'src/workspace/workspace-health/services/field-metadata-health.service';
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 { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
@Injectable() @Injectable()
@ -20,9 +22,11 @@ export class WorkspaceHealthService {
private readonly dataSourceService: DataSourceService, private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService, private readonly typeORMService: TypeORMService,
private readonly objectMetadataService: ObjectMetadataService, private readonly objectMetadataService: ObjectMetadataService,
private readonly databaseStructureService: DatabaseStructureService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly objectMetadataHealthService: ObjectMetadataHealthService, private readonly objectMetadataHealthService: ObjectMetadataHealthService,
private readonly fieldMetadataHealthService: FieldMetadataHealthService, private readonly fieldMetadataHealthService: FieldMetadataHealthService,
private readonly relationMetadataHealthService: RelationMetadataHealthService,
) {} ) {}
async healthCheck( async healthCheck(
@ -41,7 +45,7 @@ export class WorkspaceHealthService {
// Check if a data source exists for this workspace // Check if a data source exists for this workspace
if (!dataSourceMetadata) { if (!dataSourceMetadata) {
throw new NotFoundException( throw new NotFoundException(
`Datasource for workspace id ${workspaceId} not found`, `DataSource for workspace id ${workspaceId} not found`,
); );
} }
@ -57,6 +61,19 @@ export class WorkspaceHealthService {
} }
for (const objectMetadata of objectMetadataCollection) { for (const objectMetadata of objectMetadataCollection) {
const tableName = computeObjectTargetTable(objectMetadata);
const workspaceTableColumns =
await this.databaseStructureService.getWorkspaceTableColumns(
schemaName,
tableName,
);
if (!workspaceTableColumns || workspaceTableColumns.length === 0) {
throw new NotFoundException(
`Table ${tableName} not found in schema ${schemaName}`,
);
}
// Check object metadata health // Check object metadata health
const objectIssues = await this.objectMetadataHealthService.healthCheck( const objectIssues = await this.objectMetadataHealthService.healthCheck(
schemaName, schemaName,
@ -68,13 +85,23 @@ export class WorkspaceHealthService {
// Check fields metadata health // Check fields metadata health
const fieldIssues = await this.fieldMetadataHealthService.healthCheck( const fieldIssues = await this.fieldMetadataHealthService.healthCheck(
schemaName,
computeObjectTargetTable(objectMetadata), computeObjectTargetTable(objectMetadata),
workspaceTableColumns,
objectMetadata.fields, objectMetadata.fields,
options, options,
); );
issues.push(...fieldIssues); issues.push(...fieldIssues);
// Check relation metadata health
const relationIssues = this.relationMetadataHealthService.healthCheck(
workspaceTableColumns,
objectMetadataCollection,
objectMetadata,
options,
);
issues.push(...relationIssues);
} }
return issues; return issues;