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:
Jérémy M
2024-03-15 14:40:58 +01:00
committed by GitHub
parent 52f1b3ac98
commit 94487f6737
760 changed files with 3215 additions and 3155 deletions

View File

@ -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>[]>;
}

View File

@ -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,
];

View File

@ -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 };
}
}

View File

@ -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,
);
}
}

View File

@ -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;
}
}

View File

@ -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,
);
}
}