feat: workspace health relation (#3466)
feat: add check relation health
This commit is contained in:
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user