feat: refactor folder structure (#4498)
* feat: wip refactor folder structure * Fix * fix position --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,68 @@
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import {
|
||||
WorkspaceHealthIssue,
|
||||
WorkspaceHealthIssueType,
|
||||
WorkspaceIssueTypeToInterface,
|
||||
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine-metadata/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine-metadata/workspace-migration/workspace-migration.entity';
|
||||
|
||||
export class CompareEntity<T> {
|
||||
current: T | null;
|
||||
altered: T | null;
|
||||
}
|
||||
|
||||
export abstract class AbstractWorkspaceFixer<
|
||||
IssueTypes extends WorkspaceHealthIssueType,
|
||||
UpdateRecordEntities = unknown,
|
||||
> {
|
||||
private issueTypes: IssueTypes[];
|
||||
|
||||
constructor(...issueTypes: IssueTypes[]) {
|
||||
this.issueTypes = issueTypes;
|
||||
}
|
||||
|
||||
filterIssues(
|
||||
issues: WorkspaceHealthIssue[],
|
||||
): WorkspaceIssueTypeToInterface<IssueTypes>[] {
|
||||
return issues.filter(
|
||||
(issue): issue is WorkspaceIssueTypeToInterface<IssueTypes> =>
|
||||
this.issueTypes.includes(issue.type as IssueTypes),
|
||||
);
|
||||
}
|
||||
|
||||
protected splitIssuesByType(
|
||||
issues: WorkspaceIssueTypeToInterface<IssueTypes>[],
|
||||
): Record<IssueTypes, WorkspaceIssueTypeToInterface<IssueTypes>[]> {
|
||||
return issues.reduce(
|
||||
(
|
||||
acc: Record<IssueTypes, WorkspaceIssueTypeToInterface<IssueTypes>[]>,
|
||||
issue,
|
||||
) => {
|
||||
const type = issue.type as IssueTypes;
|
||||
|
||||
if (!acc[type]) {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(issue);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<IssueTypes, WorkspaceIssueTypeToInterface<IssueTypes>[]>,
|
||||
);
|
||||
}
|
||||
|
||||
async createWorkspaceMigrations?(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceIssueTypeToInterface<IssueTypes>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]>;
|
||||
|
||||
async createMetadataUpdates?(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceIssueTypeToInterface<IssueTypes>[],
|
||||
): Promise<CompareEntity<UpdateRecordEntities>[]>;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { WorkspaceNullableFixer } from './workspace-nullable.fixer';
|
||||
import { WorkspaceDefaultValueFixer } from './workspace-default-value.fixer';
|
||||
import { WorkspaceTypeFixer } from './workspace-type.fixer';
|
||||
import { WorkspaceTargetColumnMapFixer } from './workspace-target-column-map.fixer';
|
||||
|
||||
export const workspaceFixers = [
|
||||
WorkspaceNullableFixer,
|
||||
WorkspaceDefaultValueFixer,
|
||||
WorkspaceTypeFixer,
|
||||
WorkspaceTargetColumnMapFixer,
|
||||
];
|
||||
@ -0,0 +1,101 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import {
|
||||
WorkspaceHealthColumnIssue,
|
||||
WorkspaceHealthIssueType,
|
||||
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||
import { FieldMetadataDefaultValue } from 'src/engine-metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine-metadata/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine-metadata/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||
|
||||
import { AbstractWorkspaceFixer } from './abstract-workspace.fixer';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT> {
|
||||
constructor(
|
||||
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
||||
) {
|
||||
super(WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT);
|
||||
}
|
||||
|
||||
async createWorkspaceMigrations(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
if (issues.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.fixColumnDefaultValueIssues(objectMetadataCollection, issues);
|
||||
}
|
||||
|
||||
private async fixColumnDefaultValueIssues(
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
const fieldMetadataUpdateCollection = issues.map((issue) => {
|
||||
const oldDefaultValue =
|
||||
this.computeFieldMetadataDefaultValueFromColumnDefault(
|
||||
issue.columnStructure?.columnDefault,
|
||||
);
|
||||
|
||||
return {
|
||||
current: {
|
||||
...issue.fieldMetadata,
|
||||
defaultValue: oldDefaultValue,
|
||||
},
|
||||
altered: issue.fieldMetadata,
|
||||
};
|
||||
});
|
||||
|
||||
return this.workspaceMigrationFieldFactory.create(
|
||||
objectMetadataCollection,
|
||||
fieldMetadataUpdateCollection,
|
||||
WorkspaceMigrationBuilderAction.UPDATE,
|
||||
);
|
||||
}
|
||||
|
||||
private computeFieldMetadataDefaultValueFromColumnDefault(
|
||||
columnDefault: string | undefined,
|
||||
): FieldMetadataDefaultValue<'default'> {
|
||||
if (
|
||||
columnDefault === undefined ||
|
||||
columnDefault === null ||
|
||||
columnDefault === 'NULL'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isNaN(Number(columnDefault))) {
|
||||
return { value: +columnDefault };
|
||||
}
|
||||
|
||||
if (columnDefault === 'true') {
|
||||
return { value: true };
|
||||
}
|
||||
|
||||
if (columnDefault === 'false') {
|
||||
return { value: false };
|
||||
}
|
||||
|
||||
if (columnDefault === '') {
|
||||
return { value: '' };
|
||||
}
|
||||
|
||||
if (columnDefault === 'now()') {
|
||||
return { type: 'now' };
|
||||
}
|
||||
|
||||
if (columnDefault.startsWith('public.uuid_generate_v4')) {
|
||||
return { type: 'uuid' };
|
||||
}
|
||||
|
||||
return { value: columnDefault };
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import {
|
||||
WorkspaceHealthColumnIssue,
|
||||
WorkspaceHealthIssueType,
|
||||
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine-metadata/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine-metadata/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||
|
||||
import { AbstractWorkspaceFixer } from './abstract-workspace.fixer';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceNullableFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT> {
|
||||
constructor(
|
||||
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
||||
) {
|
||||
super(WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT);
|
||||
}
|
||||
|
||||
async createWorkspaceMigrations(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
if (issues.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.fixColumnNullabilityIssues(objectMetadataCollection, issues);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
import {
|
||||
WorkspaceHealthColumnIssue,
|
||||
WorkspaceHealthIssueType,
|
||||
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine-metadata/object-metadata/object-metadata.entity';
|
||||
import { generateTargetColumnMap } from 'src/engine-metadata/field-metadata/utils/generate-target-column-map.util';
|
||||
import { FieldMetadataEntity } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine-metadata/workspace-migration/workspace-migration.entity';
|
||||
import { computeObjectTargetTable } from 'src/engine-workspace/utils/compute-object-target-table.util';
|
||||
import { DataSourceEntity } from 'src/engine-metadata/data-source/data-source.entity';
|
||||
import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service';
|
||||
import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine-metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import {
|
||||
AbstractWorkspaceFixer,
|
||||
CompareEntity,
|
||||
} from './abstract-workspace.fixer';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceTargetColumnMapFixer extends AbstractWorkspaceFixer<
|
||||
WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID,
|
||||
FieldMetadataEntity
|
||||
> {
|
||||
constructor(
|
||||
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
||||
private readonly databaseStructureService: DatabaseStructureService,
|
||||
) {
|
||||
super(WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID);
|
||||
}
|
||||
|
||||
async createWorkspaceMigrations(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
if (issues.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.fixStructureTargetColumnMapIssues(
|
||||
manager,
|
||||
objectMetadataCollection,
|
||||
issues,
|
||||
);
|
||||
}
|
||||
|
||||
async createMetadataUpdates(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID>[],
|
||||
): Promise<CompareEntity<FieldMetadataEntity>[]> {
|
||||
if (issues.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.fixMetadataTargetColumnMapIssues(manager, issues);
|
||||
}
|
||||
|
||||
private async fixStructureTargetColumnMapIssues(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
const workspaceMigrationCollection: Partial<WorkspaceMigrationEntity>[] =
|
||||
[];
|
||||
const dataSourceRepository = manager.getRepository(DataSourceEntity);
|
||||
|
||||
for (const issue of issues) {
|
||||
const objectMetadata = objectMetadataCollection.find(
|
||||
(metadata) => metadata.id === issue.fieldMetadata.objectMetadataId,
|
||||
);
|
||||
const targetColumnMap = generateTargetColumnMap(
|
||||
issue.fieldMetadata.type,
|
||||
issue.fieldMetadata.isCustom,
|
||||
issue.fieldMetadata.name,
|
||||
);
|
||||
|
||||
// Skip composite fields, too complicated to fix for now
|
||||
if (isCompositeFieldMetadataType(issue.fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new Error(
|
||||
`Object metadata with id ${issue.fieldMetadata.objectMetadataId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isEqual(issue.fieldMetadata.targetColumnMap, targetColumnMap)) {
|
||||
// Retrieve the data source to get the schema name
|
||||
const dataSource = await dataSourceRepository.findOne({
|
||||
where: {
|
||||
id: objectMetadata.dataSourceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dataSource) {
|
||||
throw new Error(
|
||||
`Data source with id ${objectMetadata.dataSourceId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const columnName = issue.fieldMetadata.targetColumnMap?.value;
|
||||
const columnExist =
|
||||
await this.databaseStructureService.workspaceColumnExist(
|
||||
dataSource.schema,
|
||||
computeObjectTargetTable(objectMetadata),
|
||||
columnName,
|
||||
);
|
||||
|
||||
if (!columnExist) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceMigration =
|
||||
await this.workspaceMigrationFieldFactory.create(
|
||||
objectMetadataCollection,
|
||||
[
|
||||
{
|
||||
current: issue.fieldMetadata,
|
||||
altered: {
|
||||
...issue.fieldMetadata,
|
||||
targetColumnMap,
|
||||
},
|
||||
},
|
||||
],
|
||||
WorkspaceMigrationBuilderAction.UPDATE,
|
||||
);
|
||||
|
||||
workspaceMigrationCollection.push(workspaceMigration[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceMigrationCollection;
|
||||
}
|
||||
|
||||
private async fixMetadataTargetColumnMapIssues(
|
||||
manager: EntityManager,
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_TARGET_COLUMN_MAP_NOT_VALID>[],
|
||||
): Promise<CompareEntity<FieldMetadataEntity>[]> {
|
||||
const fieldMetadataRepository = manager.getRepository(FieldMetadataEntity);
|
||||
const updatedEntities: CompareEntity<FieldMetadataEntity>[] = [];
|
||||
|
||||
for (const issue of issues) {
|
||||
await fieldMetadataRepository.update(issue.fieldMetadata.id, {
|
||||
targetColumnMap: generateTargetColumnMap(
|
||||
issue.fieldMetadata.type,
|
||||
issue.fieldMetadata.isCustom,
|
||||
issue.fieldMetadata.name,
|
||||
),
|
||||
});
|
||||
const alteredEntity = await fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
id: issue.fieldMetadata.id,
|
||||
},
|
||||
});
|
||||
|
||||
updatedEntities.push({
|
||||
current: issue.fieldMetadata,
|
||||
altered: alteredEntity as FieldMetadataEntity | null,
|
||||
});
|
||||
}
|
||||
|
||||
return updatedEntities;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import {
|
||||
WorkspaceHealthColumnIssue,
|
||||
WorkspaceHealthIssueType,
|
||||
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface';
|
||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine-metadata/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine-metadata/workspace-migration/workspace-migration.entity';
|
||||
import {
|
||||
FieldMetadataUpdate,
|
||||
WorkspaceMigrationFieldFactory,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||
import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service';
|
||||
|
||||
import { AbstractWorkspaceFixer } from './abstract-workspace.fixer';
|
||||
|
||||
const oldDataTypes = ['integer'];
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceTypeFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT> {
|
||||
private readonly logger = new Logger(WorkspaceTypeFixer.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
||||
private readonly databaseStructureService: DatabaseStructureService,
|
||||
) {
|
||||
super(WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT);
|
||||
}
|
||||
|
||||
async createWorkspaceMigrations(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
if (issues.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.fixColumnTypeIssues(objectMetadataCollection, issues);
|
||||
}
|
||||
|
||||
private async fixColumnTypeIssues(
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
const fieldMetadataUpdateCollection: FieldMetadataUpdate[] = [];
|
||||
|
||||
for (const issue of issues) {
|
||||
const dataType = issue.columnStructure?.dataType;
|
||||
|
||||
if (!dataType) {
|
||||
throw new Error('Column structure data type is missing');
|
||||
}
|
||||
|
||||
const type =
|
||||
this.databaseStructureService.getFieldMetadataTypeFromPostgresDataType(
|
||||
dataType,
|
||||
);
|
||||
|
||||
if (oldDataTypes.includes(dataType)) {
|
||||
this.logger.warn(
|
||||
`Old data type detected for column ${issue.columnStructure?.columnName} with data type ${dataType}. Please update the column data type manually.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
throw new Error("Can't find field metadata type from column structure");
|
||||
}
|
||||
|
||||
fieldMetadataUpdateCollection.push({
|
||||
current: {
|
||||
...issue.fieldMetadata,
|
||||
type,
|
||||
},
|
||||
altered: issue.fieldMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
return this.workspaceMigrationFieldFactory.create(
|
||||
objectMetadataCollection,
|
||||
fieldMetadataUpdateCollection,
|
||||
WorkspaceMigrationBuilderAction.UPDATE,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user