Files
twenty/packages/twenty-server/src/workspace/workspace-health/services/field-metadata-health.service.ts
Jérémy M 4b7e42c38e feat: workspace health relation (#3466)
feat: add check relation health
2024-01-17 17:05:35 +01:00

339 lines
11 KiB
TypeScript

import { Injectable, InternalServerErrorException } 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(
tableName: string,
workspaceTableColumns: WorkspaceTableStructure[],
fieldMetadataCollection: FieldMetadataEntity[],
options: WorkspaceHealthOptions,
): Promise<WorkspaceHealthIssue[]> {
const issues: WorkspaceHealthIssue[] = [];
for (const fieldMetadata of fieldMetadataCollection) {
// Relation metadata are checked in another service
if (
fieldMetadata.fromRelationMetadata ||
fieldMetadata.toRelationMetadata
) {
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,
);
// 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 !== fieldMetadata.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;
}
}