Refactor schema migration runner service discovery (#13988)

## Context
This refactoring improves scalability in the workspace migration runner
in terms of actions. Instead of relying on a switch case and multiple
changes and services + duplicate things between metadata and workspace
schema, we now have a unique module for all kind of actions and a
registry pattern to register them and run them dynamically.

Example on how to add a new "create_role" action with the future design:
```typescript
// packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/create-role-action-handler.service.ts
export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
  'create_role',
) {
  constructor(
    private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
    private readonly roleRepository: Repository<Role>,
  ) {}

  async executeForMetadata(
    context: WorkspaceMigrationActionRunnerArgs<CreateRoleAction>,
  ): Promise<void> {
      this.roleRepository.save({...}); // technically could use the queryRunner from context param directly instead of injecting the repository
  }

  async executeForWorkspaceSchema(
    context: WorkspaceMigrationActionRunnerArgs<CreateRoleAction>,
  ): Promise<void> {
      // this.workspaceSchemaManagerService.tableManager... -> Nothing to do in the workspace schema in the role case
  }
}
```
```typescript
// packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts
@Module({
  imports: [WorkspaceSchemaManagerModule, +TypeOrmModule.forFeature([Role]),
  providers: [
     ...
     +RoleCreateActionService
  ],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
```
```typescript
export type WorkspaceMigrationAction=
  | WorkspaceMigrationObjectAction
  ...
  +| WorkspaceMigrationRoleAction;

export type CreateRoleAction = {
   type = 'create_role',
   role: RoleEntity
};

export type WorkspaceMigrationRoleAction = CreateRoleAction;
```
This commit is contained in:
Weiko
2025-08-20 18:30:40 +02:00
committed by GitHub
parent f994f9d512
commit deec9c96da
41 changed files with 845 additions and 2456 deletions
@@ -26,7 +26,7 @@ import { deleteFieldFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadat
import { getSubFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-or-throw.util';
import { replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-validate-build-and-run-service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class FieldMetadataServiceV2 {
@@ -23,7 +23,7 @@ import {
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-validate-build-and-run-service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class ObjectMetadataServiceV2 {
@@ -5,7 +5,7 @@ import {
WorkspaceMigrationBuildArgs,
WorkspaceMigrationBuilderV2Service,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service';
import {
WorkspaceMigrationV2Exception,
WorkspaceMigrationV2ExceptionCode,
@@ -8,3 +8,8 @@ export type WorkspaceMigrationActionV2 =
| WorkspaceMigrationIndexActionV2;
export type WorkspaceMigrationActionTypeV2 = WorkspaceMigrationActionV2['type'];
export type ExtractAction<T extends WorkspaceMigrationActionTypeV2> = Extract<
WorkspaceMigrationActionV2,
{ type: T }
>;
@@ -0,0 +1,85 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-with-flat-field-maps-in-flat-object-metadata-maps-or-throw.util';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { type CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/get-workspace-schema-context-for-migration.util';
import {
collectEnumOperationsForField,
EnumOperation,
executeBatchEnumOperations,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/workspace-schema-enum-operations.util';
@Injectable()
export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_field',
) {
constructor(
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateFieldAction>,
): Promise<void> {
const { action, queryRunner } = context;
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const { flatFieldMetadata } = action;
await fieldMetadataRepository.save(flatFieldMetadata);
}
async executeForWorkspaceSchema(
context: WorkspaceMigrationActionRunnerArgs<CreateFieldAction>,
): Promise<void> {
const { action, queryRunner, flatObjectMetadataMaps, workspaceId } =
context;
const { flatFieldMetadata } = action;
const flatObjectMetadataWithFlatFieldMaps =
findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId: flatFieldMetadata.objectMetadataId,
});
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
workspaceId,
flatObjectMetadata: flatObjectMetadataWithFlatFieldMaps,
});
const enumOperations = collectEnumOperationsForField({
flatFieldMetadata: flatFieldMetadata,
tableName,
operation: EnumOperation.CREATE,
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
const columnDefinitions = generateColumnDefinitions({
flatFieldMetadata: flatFieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadataWithFlatFieldMaps,
});
await this.workspaceSchemaManagerService.columnManager.addColumns({
queryRunner,
schemaName,
tableName,
columnDefinitions,
});
}
}
@@ -0,0 +1,104 @@
import { Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util';
import { findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-with-flat-field-maps-in-flat-object-metadata-maps-or-throw.util';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { type DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/get-workspace-schema-context-for-migration.util';
import {
collectEnumOperationsForField,
EnumOperation,
executeBatchEnumOperations,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/workspace-schema-enum-operations.util';
@Injectable()
export class DeleteFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_field',
) {
constructor(
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteFieldAction>,
) {
const { action, queryRunner } = context;
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const { fieldMetadataId } = action;
await fieldMetadataRepository.delete({
id: In([fieldMetadataId]),
});
}
async executeForWorkspaceSchema(
context: WorkspaceMigrationActionRunnerArgs<DeleteFieldAction>,
) {
const { action, queryRunner, flatObjectMetadataMaps, workspaceId } =
context;
const { objectMetadataId, fieldMetadataId } = action;
const flatObjectMetadataWithFlatFieldMaps =
findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
});
const fieldMetadata = findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
fieldMetadataId,
});
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
workspaceId,
flatObjectMetadata: flatObjectMetadataWithFlatFieldMaps,
});
const flatObjectMetadata =
findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
});
const columnDefinitions = generateColumnDefinitions({
flatFieldMetadata: fieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadata,
});
const columnNamesToDrop = columnDefinitions.map((def) => def.name);
await this.workspaceSchemaManagerService.columnManager.dropColumns({
queryRunner,
schemaName,
tableName,
columnNames: columnNamesToDrop,
});
const enumOperations = collectEnumOperationsForField({
flatFieldMetadata: fieldMetadata,
tableName,
operation: EnumOperation.DROP,
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
}
}
@@ -1,16 +1,17 @@
import { Injectable } from '@nestjs/common';
import { type FromTo } from 'twenty-shared/types';
import { type EnumFieldMetadataType, type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type QueryRunner } from 'typeorm';
import { type FieldMetadataDefaultValueForAnyType } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import {
FieldMetadataDefaultOption,
type FieldMetadataComplexOption,
type FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { EnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/enum-field-metadata-type.type';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/get-composite-type-or-throw.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
@@ -19,151 +20,84 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { isRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-relation-flat-field-metadata.util';
import { findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util';
import { findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-with-flat-field-maps-in-flat-object-metadata-maps-or-throw.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import {
type CreateFieldAction,
type DeleteFieldAction,
type UpdateFieldAction,
type WorkspaceMigrationFieldActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/exceptions/workspace-migration-runner.exception';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import {
WorkspaceSchemaMigrationException,
WorkspaceSchemaMigrationExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/exceptions/workspace-schema-migration.exception';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/utils/generate-column-definitions.util';
import {
prepareFieldWorkspaceSchemaContext,
prepareWorkspaceSchemaContext,
} from './utils/workspace-schema-context.util';
import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/get-workspace-schema-context-for-migration.util';
import {
collectEnumOperationsForField,
EnumOperation,
executeBatchEnumOperations,
} from './utils/workspace-schema-enum-operations.util';
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/workspace-schema-enum-operations.util';
@Injectable()
export class WorkspaceSchemaFieldActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationFieldActionTypeV2, 'schema'>
{
export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_field',
) {
constructor(
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {}
) {
super();
}
runDeleteFieldSchemaMigration = async ({
action,
queryRunner,
flatObjectMetadataMaps,
}: WorkspaceMigrationActionRunnerArgs<DeleteFieldAction>) => {
const { objectMetadataId, fieldMetadataId } = action;
const { schemaName, tableName, fieldMetadata } =
prepareFieldWorkspaceSchemaContext({
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateFieldAction>,
): Promise<void> {
const { action, queryRunner } = context;
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const { fieldMetadataId } = action;
await fieldMetadataRepository.update(
fieldMetadataId,
fromWorkspaceMigrationUpdateActionToPartialEntity(action),
);
}
async executeForWorkspaceSchema(
context: WorkspaceMigrationActionRunnerArgs<UpdateFieldAction>,
): Promise<void> {
const { action, queryRunner, flatObjectMetadataMaps, workspaceId } =
context;
const { objectMetadataId, fieldMetadataId, updates } = action;
const flatObjectMetadataWithFlatFieldMaps =
findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
});
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
workspaceId,
flatObjectMetadata: flatObjectMetadataWithFlatFieldMaps,
});
const flatObjectMetadata =
findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
});
const currentFlatFieldMetadata =
findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
fieldMetadataId,
});
const flatObjectMetadata =
findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
});
const columnDefinitions = generateColumnDefinitions({
flatFieldMetadata: fieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadata,
});
const columnNamesToDrop = columnDefinitions.map((def) => def.name);
await this.workspaceSchemaManagerService.columnManager.dropColumns({
queryRunner,
schemaName,
tableName,
columnNames: columnNamesToDrop,
});
const enumOperations = collectEnumOperationsForField({
flatFieldMetadata: fieldMetadata,
tableName,
operation: EnumOperation.DROP,
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
return;
};
runCreateFieldSchemaMigration = async ({
action,
queryRunner,
flatObjectMetadataMaps,
}: WorkspaceMigrationActionRunnerArgs<CreateFieldAction>) => {
const { flatFieldMetadata } = action;
const {
schemaName,
tableName,
flatObjectMetadataWithFlatFieldMaps: flatObjectMetadata,
} = prepareWorkspaceSchemaContext({
flatObjectMetadataMaps,
objectMetadataId: flatFieldMetadata.objectMetadataId,
});
const enumOperations = collectEnumOperationsForField({
flatFieldMetadata: flatFieldMetadata,
tableName,
operation: EnumOperation.CREATE,
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
const columnDefinitions = generateColumnDefinitions({
flatFieldMetadata: flatFieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadata,
});
await this.workspaceSchemaManagerService.columnManager.addColumns({
queryRunner,
schemaName,
tableName,
columnDefinitions,
});
return;
};
runUpdateFieldSchemaMigration = async ({
action,
queryRunner,
flatObjectMetadataMaps,
}: WorkspaceMigrationActionRunnerArgs<UpdateFieldAction>) => {
const { objectMetadataId, fieldMetadataId, updates } = action;
const {
schemaName,
tableName,
flatObjectMetadataWithFlatFieldMaps: flatObjectMetadata,
fieldMetadata: currentFlatFieldMetadata,
} = prepareFieldWorkspaceSchemaContext({
flatObjectMetadataMaps,
objectMetadataId,
fieldMetadataId,
});
let optimisticFlatFieldMetadata = structuredClone(currentFlatFieldMetadata);
for (const update of updates) {
@@ -202,9 +136,7 @@ export class WorkspaceSchemaFieldActionRunnerService
optimisticFlatFieldMetadata.options = update.to ?? [];
}
}
return;
};
}
private async handleFieldNameUpdate(
queryRunner: QueryRunner,
@@ -220,9 +152,9 @@ export class WorkspaceSchemaFieldActionRunnerService
for (const property of compositeType.properties) {
if (isRelationFieldMetadataType(property.type)) {
throw new WorkspaceSchemaMigrationException(
throw new WorkspaceMigrationRunnerException(
'Relation field metadata in composite type is not supported yet',
WorkspaceSchemaMigrationExceptionCode.NOT_SUPPORTED,
WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED,
);
}
@@ -245,9 +177,9 @@ export class WorkspaceSchemaFieldActionRunnerService
}
} else {
if (isRelationFlatFieldMetadata(flatFieldMetadata)) {
throw new WorkspaceSchemaMigrationException(
throw new WorkspaceMigrationRunnerException(
'Relation field metadata name update is not supported yet',
WorkspaceSchemaMigrationExceptionCode.NOT_SUPPORTED,
WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED,
);
}
await this.workspaceSchemaManagerService.columnManager.renameColumn({
@@ -290,9 +222,9 @@ export class WorkspaceSchemaFieldActionRunnerService
for (const property of compositeType.properties) {
if (isRelationFieldMetadataType(property.type)) {
throw new WorkspaceSchemaMigrationException(
throw new WorkspaceMigrationRunnerException(
'Relation field metadata in composite type is not supported yet',
WorkspaceSchemaMigrationExceptionCode.NOT_SUPPORTED,
WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED,
);
}
@@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { type CreateIndexAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateIndexActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_index',
) {
async executeForMetadata(
_context: WorkspaceMigrationActionRunnerArgs<CreateIndexAction>,
): Promise<void> {
throw new Error('Not implemented');
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateIndexAction>,
): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { type DeleteIndexAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteIndexActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_index',
) {
async executeForMetadata(
_context: WorkspaceMigrationActionRunnerArgs<DeleteIndexAction>,
): Promise<void> {
throw new Error('Not implemented');
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteIndexAction>,
): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -0,0 +1,119 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { type CreateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/get-workspace-schema-context-for-migration.util';
import {
collectEnumOperationsForObject,
EnumOperation,
executeBatchEnumOperations,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/workspace-schema-enum-operations.util';
@Injectable()
export class CreateObjectActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_object',
) {
constructor(
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateObjectAction>,
): Promise<void> {
const { action, queryRunner } = context;
const { flatObjectMetadataWithoutFields, createFieldActions } = action;
const objectMetadataRepository =
queryRunner.manager.getRepository<ObjectMetadataEntity>(
ObjectMetadataEntity,
);
const dataSourceRepository =
queryRunner.manager.getRepository<DataSourceEntity>(DataSourceEntity);
const lastDataSourceMetadata = await dataSourceRepository.findOneOrFail({
where: {
workspaceId: flatObjectMetadataWithoutFields.workspaceId,
},
order: { createdAt: 'DESC' },
});
await objectMetadataRepository.save({
...flatObjectMetadataWithoutFields,
dataSourceId: lastDataSourceMetadata.id,
targetTableName: 'DEPRECATED',
});
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
for (const createFieldAction of createFieldActions) {
const { flatFieldMetadata } = createFieldAction;
await fieldMetadataRepository.save(flatFieldMetadata);
}
}
async executeForWorkspaceSchema(
context: WorkspaceMigrationActionRunnerArgs<CreateObjectAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatObjectMetadataWithoutFields, createFieldActions } = action;
const flatObjectMetadata = flatObjectMetadataWithoutFields;
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
workspaceId,
flatObjectMetadata: flatObjectMetadata,
});
const columnDefinitions = createFieldActions.flatMap((createFieldAction) =>
generateColumnDefinitions({
flatFieldMetadata: createFieldAction.flatFieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadataWithoutFields,
}),
);
const enumOrCompositeFlatFieldMetadatas = createFieldActions
.map((createFieldAction) => createFieldAction.flatFieldMetadata)
.filter((field): field is FlatFieldMetadata => field != null)
.filter(
(field) =>
isEnumFlatFieldMetadata(field) || isCompositeFlatFieldMetadata(field),
);
const enumOperations = collectEnumOperationsForObject({
flatFieldMetadatas: enumOrCompositeFlatFieldMetadatas,
tableName,
operation: EnumOperation.CREATE,
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
await this.workspaceSchemaManagerService.tableManager.createTable({
queryRunner,
schemaName,
tableName,
columnDefinitions,
});
}
}
@@ -0,0 +1,90 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-with-flat-field-maps-in-flat-object-metadata-maps-or-throw.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { type DeleteObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/get-workspace-schema-context-for-migration.util';
import {
collectEnumOperationsForObject,
EnumOperation,
executeBatchEnumOperations,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/workspace-schema-enum-operations.util';
@Injectable()
export class DeleteObjectActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_object',
) {
constructor(
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteObjectAction>,
): Promise<void> {
const { action, queryRunner } = context;
const { objectMetadataId } = action;
const objectMetadataRepository =
queryRunner.manager.getRepository<ObjectMetadataEntity>(
ObjectMetadataEntity,
);
await objectMetadataRepository.delete(objectMetadataId);
}
async executeForWorkspaceSchema(
context: WorkspaceMigrationActionRunnerArgs<DeleteObjectAction>,
): Promise<void> {
const { action, queryRunner, flatObjectMetadataMaps, workspaceId } =
context;
const { objectMetadataId } = action;
const flatObjectMetadataWithFlatFieldMaps =
findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
});
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
workspaceId,
flatObjectMetadata: flatObjectMetadataWithFlatFieldMaps,
});
await this.workspaceSchemaManagerService.tableManager.dropTable({
queryRunner,
schemaName,
tableName,
});
const enumOrCompositeFlatFieldMetadatas = Object.values(
flatObjectMetadataWithFlatFieldMaps.fieldsById,
)
.filter((field): field is FlatFieldMetadata => field != null)
.filter(
(field) =>
isEnumFlatFieldMetadata(field) || isCompositeFlatFieldMetadata(field),
);
const enumOperations = collectEnumOperationsForObject({
flatFieldMetadatas: enumOrCompositeFlatFieldMetadatas,
tableName,
operation: EnumOperation.DROP,
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
}
}
@@ -0,0 +1,115 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-with-flat-field-maps-in-flat-object-metadata-maps-or-throw.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { type UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/get-workspace-schema-context-for-migration.util';
import {
collectEnumOperationsForObject,
EnumOperation,
executeBatchEnumOperations,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/workspace-schema-enum-operations.util';
@Injectable()
export class UpdateObjectActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_object',
) {
constructor(
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateObjectAction>,
): Promise<void> {
const { action, queryRunner } = context;
const objectMetadataRepository =
queryRunner.manager.getRepository<ObjectMetadataEntity>(
ObjectMetadataEntity,
);
await objectMetadataRepository.update(
action.objectMetadataId,
fromWorkspaceMigrationUpdateActionToPartialEntity(action),
);
}
async executeForWorkspaceSchema(
context: WorkspaceMigrationActionRunnerArgs<UpdateObjectAction>,
): Promise<void> {
const { action, queryRunner, flatObjectMetadataMaps, workspaceId } =
context;
const { objectMetadataId, updates } = action;
const flatObjectMetadataWithFlatFieldMaps =
findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
});
const { schemaName, tableName: currentTableName } =
getWorkspaceSchemaContextForMigration({
workspaceId,
flatObjectMetadata: flatObjectMetadataWithFlatFieldMaps,
});
for (const update of updates) {
if (update.property !== 'nameSingular') {
continue;
}
const updatedObjectMetadata = {
...flatObjectMetadataWithFlatFieldMaps,
[update.property]: update.to,
};
const newTableName = computeObjectTargetTable(updatedObjectMetadata);
if (currentTableName !== newTableName) {
await this.workspaceSchemaManagerService.tableManager.renameTable({
queryRunner,
schemaName,
oldTableName: currentTableName,
newTableName,
});
const enumOrCompositeFlatFieldMetadatas = Object.values(
flatObjectMetadataWithFlatFieldMaps.fieldsById,
)
.filter((field): field is FlatFieldMetadata => field != null)
.filter(
(field) =>
isEnumFlatFieldMetadata(field) ||
isCompositeFlatFieldMetadata(field),
);
const enumOperations = collectEnumOperationsForObject({
flatFieldMetadatas: enumOrCompositeFlatFieldMetadatas,
tableName: currentTableName,
operation: EnumOperation.RENAME,
options: {
newTableName,
},
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
}
}
}
}
@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { CreateFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/create-field-action-handler.service';
import { DeleteFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/delete-field-action-handler.service';
import { UpdateFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service';
import { CreateIndexActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service';
import { DeleteIndexActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/delete-index-action-handler.service';
import { CreateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service';
import { DeleteObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service';
import { UpdateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service';
@Module({
imports: [WorkspaceSchemaManagerModule],
providers: [
CreateFieldActionHandlerService,
UpdateFieldActionHandlerService,
DeleteFieldActionHandlerService,
CreateObjectActionHandlerService,
UpdateObjectActionHandlerService,
DeleteObjectActionHandlerService,
CreateIndexActionHandlerService,
DeleteIndexActionHandlerService,
],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
@@ -0,0 +1,2 @@
export const WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY =
'workspace_migration_action_handler_metadata_key';
@@ -10,4 +10,8 @@ export class WorkspaceMigrationRunnerException extends CustomException<
export const WorkspaceMigrationRunnerExceptionCode = appendCommonExceptionCode({
FIELD_METADATA_NOT_FOUND: 'FIELD_METADATA_NOT_FOUND',
OBJECT_METADATA_NOT_FOUND: 'OBJECT_METADATA_NOT_FOUND',
ENUM_OPERATION_FAILED: 'ENUM_OPERATION_FAILED',
UNSUPPORTED_COMPOSITE_COLUMN_TYPE: 'UNSUPPORTED_COMPOSITE_COLUMN_TYPE',
NOT_SUPPORTED: 'NOT_SUPPORTED',
INVALID_ACTION_TYPE: 'INVALID_ACTION_TYPE',
} as const);
@@ -0,0 +1,51 @@
import { SetMetadata } from '@nestjs/common';
import {
type ExtractAction,
type WorkspaceMigrationActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import { WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/constants/workspace-migration-action-handler-metadata-key.constant';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
export interface WorkspaceMigrationRunnerActionHandlerService<
T extends WorkspaceMigrationActionTypeV2,
> {
execute(
context: WorkspaceMigrationActionRunnerArgs<ExtractAction<T>>,
): Promise<void>;
}
export abstract class BaseWorkspaceMigrationRunnerActionHandlerService<
T extends WorkspaceMigrationActionTypeV2,
> implements WorkspaceMigrationRunnerActionHandlerService<T>
{
abstract executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<ExtractAction<T>>,
): Promise<void>;
abstract executeForWorkspaceSchema(
context: WorkspaceMigrationActionRunnerArgs<ExtractAction<T>>,
): Promise<void>;
async execute(
context: WorkspaceMigrationActionRunnerArgs<ExtractAction<T>>,
): Promise<void> {
await Promise.all([
this.executeForMetadata(context),
this.executeForWorkspaceSchema(context),
]);
}
}
export function WorkspaceMigrationRunnerActionHandler<
T extends WorkspaceMigrationActionTypeV2,
>(actionType: T): typeof BaseWorkspaceMigrationRunnerActionHandlerService<T> {
abstract class ActionHandlerService extends BaseWorkspaceMigrationRunnerActionHandlerService<T> {}
SetMetadata(
WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY,
actionType,
)(ActionHandlerService);
return ActionHandlerService;
}
@@ -0,0 +1,69 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService } from '@nestjs/core';
import { type WorkspaceMigrationRunnerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import {
type WorkspaceMigrationActionTypeV2,
type WorkspaceMigrationActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import { WorkspaceSchemaMigrationRunnerActionHandlersModule } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module';
import { WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/constants/workspace-migration-action-handler-metadata-key.constant';
import {
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/exceptions/workspace-migration-runner.exception';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceMigrationRunnerActionHandlerRegistryService
implements OnModuleInit
{
private readonly actionHandlers = new Map<
WorkspaceMigrationActionTypeV2,
WorkspaceMigrationRunnerActionHandlerService<WorkspaceMigrationActionTypeV2>
>();
constructor(private readonly discoveryService: DiscoveryService) {}
async onModuleInit() {
this.discoverAndRegisterActionHandlers();
}
private discoverAndRegisterActionHandlers(): void {
const providers = this.discoveryService.getProviders({
include: [WorkspaceSchemaMigrationRunnerActionHandlersModule],
});
providers.forEach((wrapper) => {
const { instance, metatype } = wrapper;
if (!instance || !metatype) return;
const actionType = Reflect.getMetadata(
WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY,
metatype,
);
if (actionType && instance.execute) {
this.actionHandlers.set(actionType, instance);
}
});
}
async executeActionHandler<T extends WorkspaceMigrationActionV2>(
actionType: WorkspaceMigrationActionTypeV2,
context: WorkspaceMigrationActionRunnerArgs<T>,
): Promise<void> {
const handler = this.actionHandlers.get(actionType);
if (!handler) {
throw new WorkspaceMigrationRunnerException(
`No migration runner action handler found for action: ${actionType}`,
WorkspaceMigrationRunnerExceptionCode.INVALID_ACTION_TYPE,
);
}
await handler.execute(context);
}
}
@@ -8,20 +8,18 @@ import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/works
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { type WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
import { applyWorkspaceMigrationActionOnFlatObjectMetadataMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/apply-workspace-migration-action-on-flat-object-metadata-maps';
import { WorkspaceMetadataMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner-service';
import { WorkspaceSchemaMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service';
import { WorkspaceMigrationRunnerActionHandlerRegistryService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/registry/workspace-migration-runner-action-handler-registry.service';
import { applyWorkspaceMigrationActionOnFlatObjectMetadataMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/apply-workspace-migration-action-on-flat-object-metadata-maps';
@Injectable()
export class WorkspaceMigrationRunnerV2Service {
constructor(
private readonly workspaceMetadataMigrationRunner: WorkspaceMetadataMigrationRunnerService,
private readonly workspaceSchemaMigrationRunner: WorkspaceSchemaMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
@InjectDataSource('core')
private readonly coreDataSource: DataSource,
private readonly workspaceMigrationRunnerActionHandlerRegistry: WorkspaceMigrationRunnerActionHandlerRegistryService,
) {}
run = async (
@@ -44,23 +42,21 @@ export class WorkspaceMigrationRunnerV2Service {
try {
for (const action of workspaceMigration.actions) {
await Promise.all([
this.workspaceMetadataMigrationRunner.runWorkspaceMetadataMigration({
await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionHandler(
action.type,
{
action,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
queryRunner,
action,
}),
this.workspaceSchemaMigrationRunner.runWorkspaceSchemaMigration({
queryRunner,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
action,
}),
]);
workspaceId: workspaceMigration.workspaceId,
},
);
optimisticFlatObjectMetadataMaps =
applyWorkspaceMigrationActionOnFlatObjectMetadataMaps({
action,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
workspaceId: workspaceMigration.workspaceId,
});
}
@@ -1,17 +0,0 @@
import { type ConvertActionTypeToCamelCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/convert-action-type-to-camel-case.type';
import {
type WorkspaceMigrationActionTypeV2,
type WorkspaceMigrationActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
export type RunnerMethodForActionType<
TAction extends WorkspaceMigrationActionTypeV2,
TRunner extends 'metadata' | 'schema',
> = {
[P in TAction as `run${Capitalize<ConvertActionTypeToCamelCase<P>>}${Capitalize<TRunner>}Migration`]: (
arg: WorkspaceMigrationActionRunnerArgs<
Extract<WorkspaceMigrationActionV2, { type: P }>
>,
) => Promise<void>;
};
@@ -9,4 +9,5 @@ export type WorkspaceMigrationActionRunnerArgs<
queryRunner: QueryRunner;
action: T;
flatObjectMetadataMaps: FlatObjectMetadataMaps;
workspaceId: string;
};
@@ -1,10 +0,0 @@
import { type QueryRunner } from 'typeorm';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type WorkspaceMigrationRunnerArgs = {
action: WorkspaceMigrationActionV2;
queryRunner: QueryRunner;
flatObjectMetadataMaps: FlatObjectMetadataMaps;
};
@@ -3,7 +3,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/utils/generate-column-definitions.util';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util';
describe('Generate Column Definitions', () => {
const mockObjectMetadata = getFlatObjectMetadataMock({
@@ -3,13 +3,13 @@ import { type QueryRunner } from 'typeorm';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { WorkspaceSchemaMigrationException } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/exceptions/workspace-schema-migration.exception';
import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/exceptions/workspace-migration-runner.exception';
import {
collectEnumOperationsForField,
EnumOperation,
type EnumOperationSpec,
executeBatchEnumOperations,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/utils/workspace-schema-enum-operations.util';
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/workspace-schema-enum-operations.util';
describe('WorkspaceSchemaEnumOperations', () => {
let mockSchemaManagerService: jest.Mocked<WorkspaceSchemaManagerService>;
@@ -61,7 +61,7 @@ describe('WorkspaceSchemaEnumOperations', () => {
schemaName: 'test_schema',
workspaceSchemaManagerService: mockSchemaManagerService,
}),
).rejects.toThrow(WorkspaceSchemaMigrationException);
).rejects.toThrow(WorkspaceMigrationRunnerException);
// All operations should be attempted in parallel despite failure
expect(
@@ -9,18 +9,21 @@ import { findFlatFieldMetadataInFlatObjectMetadataMaps } from 'src/engine/metada
import { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps.util';
import { replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util';
import { type WorkspaceMigrationRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-runner-args.type';
import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import {
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner.exception';
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/exceptions/workspace-migration-runner.exception';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util';
export const applyWorkspaceMigrationActionOnFlatObjectMetadataMaps = ({
export const applyWorkspaceMigrationActionOnFlatObjectMetadataMaps = <
T extends WorkspaceMigrationActionV2,
>({
action,
flatObjectMetadataMaps,
}: Omit<
WorkspaceMigrationRunnerArgs,
WorkspaceMigrationActionRunnerArgs<T>,
'queryRunner'
>): FlatObjectMetadataMaps => {
switch (action.type) {
@@ -18,11 +18,10 @@ import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/works
import { type WorkspaceSchemaColumnDefinition } from 'src/engine/twenty-orm/workspace-schema-manager/types/workspace-schema-column-definition.type';
import { computePostgresEnumName } from 'src/engine/workspace-manager/workspace-migration-runner/utils/compute-postgres-enum-name.util';
import {
WorkspaceSchemaMigrationException,
WorkspaceSchemaMigrationExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/exceptions/workspace-schema-migration.exception';
import { getWorkspaceSchemaContextForMigration } from './get-workspace-schema-context-for-migration.util';
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/exceptions/workspace-migration-runner.exception';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/get-workspace-schema-context-for-migration.util';
export const generateCompositeColumnDefinition = ({
compositeProperty,
@@ -35,16 +34,16 @@ export const generateCompositeColumnDefinition = ({
}): WorkspaceSchemaColumnDefinition => {
const { tableName, schemaName } = getWorkspaceSchemaContextForMigration({
workspaceId: flatObjectMetadataWithoutFields.workspaceId,
flatObjectMetadataWithoutFields,
flatObjectMetadata: flatObjectMetadataWithoutFields,
});
if (
compositeProperty.type === FieldMetadataType.RELATION ||
compositeProperty.type === FieldMetadataType.MORPH_RELATION
) {
throw new WorkspaceSchemaMigrationException(
throw new WorkspaceMigrationRunnerException(
`Relation type not supported for composite columns`,
WorkspaceSchemaMigrationExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE,
WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE,
);
}
@@ -156,7 +155,7 @@ export const generateColumnDefinitions = ({
}): WorkspaceSchemaColumnDefinition[] => {
const { tableName, schemaName } = getWorkspaceSchemaContextForMigration({
workspaceId: flatObjectMetadataWithoutFields.workspaceId,
flatObjectMetadataWithoutFields: flatObjectMetadataWithoutFields,
flatObjectMetadata: flatObjectMetadataWithoutFields,
});
if (isCompositeFlatFieldMetadata(flatFieldMetadata)) {
@@ -1,4 +1,7 @@
import { type FlatObjectMetadataWithoutFields } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
type FlatObjectMetadata,
type FlatObjectMetadataWithoutFields,
} from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
@@ -9,13 +12,13 @@ export type WorkspaceSchemaContextForMigration = {
export const getWorkspaceSchemaContextForMigration = ({
workspaceId,
flatObjectMetadataWithoutFields,
flatObjectMetadata,
}: {
workspaceId: string;
flatObjectMetadataWithoutFields: FlatObjectMetadataWithoutFields;
flatObjectMetadata: FlatObjectMetadata | FlatObjectMetadataWithoutFields;
}): WorkspaceSchemaContextForMigration => {
return {
schemaName: getWorkspaceSchemaName(workspaceId),
tableName: computeObjectTargetTable(flatObjectMetadataWithoutFields),
tableName: computeObjectTargetTable(flatObjectMetadata),
};
};
@@ -12,9 +12,9 @@ import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-
import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { computePostgresEnumName } from 'src/engine/workspace-manager/workspace-migration-runner/utils/compute-postgres-enum-name.util';
import {
WorkspaceSchemaMigrationException,
WorkspaceSchemaMigrationExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/exceptions/workspace-schema-migration.exception';
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/exceptions/workspace-migration-runner.exception';
export interface CreateEnumOperationSpec {
operation: EnumOperation.CREATE;
@@ -246,9 +246,9 @@ export const executeBatchEnumOperations = async ({
await Promise.all(enumPromises);
} catch (error) {
throw new WorkspaceSchemaMigrationException(
throw new WorkspaceMigrationRunnerException(
`Failed to execute batch enum operations: ${error instanceof Error ? error.message : 'Unknown error'}`,
WorkspaceSchemaMigrationExceptionCode.ENUM_OPERATION_FAILED,
WorkspaceMigrationRunnerExceptionCode.ENUM_OPERATION_FAILED,
);
}
};
@@ -1,63 +0,0 @@
import { In } from 'typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
type CreateFieldAction,
type DeleteFieldAction,
type UpdateFieldAction,
type WorkspaceMigrationFieldActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util';
export class WorkspaceMetadataFieldActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationFieldActionTypeV2, 'metadata'>
{
runDeleteFieldMetadataMigration = async ({
action,
queryRunner,
}: WorkspaceMigrationActionRunnerArgs<DeleteFieldAction>) => {
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const { fieldMetadataId } = action;
await fieldMetadataRepository.delete({
id: In([fieldMetadataId]),
});
};
runCreateFieldMetadataMigration = async ({
action,
queryRunner,
}: WorkspaceMigrationActionRunnerArgs<CreateFieldAction>) => {
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const { flatFieldMetadata } = action;
await fieldMetadataRepository.save(flatFieldMetadata);
};
runUpdateFieldMetadataMigration = async ({
action,
queryRunner,
}: WorkspaceMigrationActionRunnerArgs<UpdateFieldAction>) => {
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const { fieldMetadataId } = action;
await fieldMetadataRepository.update(
fieldMetadataId,
fromWorkspaceMigrationUpdateActionToPartialEntity(action),
);
};
}
@@ -1,26 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
type CreateIndexAction,
type DeleteIndexAction,
type WorkspaceMigrationIndexActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2';
import { type RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceMetadataIndexActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationIndexActionTypeV2, 'metadata'>
{
runDeleteIndexMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<DeleteIndexAction>,
) => {
return;
};
runCreateIndexMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<CreateIndexAction>,
) => {
return;
};
}
@@ -1,80 +0,0 @@
import { Injectable } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import { type WorkspaceMigrationRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-runner-args.type';
import { WorkspaceMetadataFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-field-action-runner.service';
import { WorkspaceMetadataIndexActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-index-action-runner.service';
import { WorkspaceMetadataObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-object-action-runner.service';
@Injectable()
export class WorkspaceMetadataMigrationRunnerService {
constructor(
private readonly workspaceMetadataObjectMigrationRunnerService: WorkspaceMetadataObjectActionRunnerService,
private readonly workspaceMetadataIndexMigrationRunnerService: WorkspaceMetadataIndexActionRunnerService,
private readonly workspaceMetadataFieldMigrationRunnerService: WorkspaceMetadataFieldActionRunnerService,
) {}
runWorkspaceMetadataMigration = async ({
action,
queryRunner,
flatObjectMetadataMaps,
}: WorkspaceMigrationRunnerArgs) => {
switch (action.type) {
case 'delete_object': {
await this.workspaceMetadataObjectMigrationRunnerService.runDeleteObjectMetadataMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'create_object': {
await this.workspaceMetadataObjectMigrationRunnerService.runCreateObjectMetadataMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'update_object': {
await this.workspaceMetadataObjectMigrationRunnerService.runUpdateObjectMetadataMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'create_field': {
await this.workspaceMetadataFieldMigrationRunnerService.runCreateFieldMetadataMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'update_field': {
await this.workspaceMetadataFieldMigrationRunnerService.runUpdateFieldMetadataMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'delete_field': {
await this.workspaceMetadataFieldMigrationRunnerService.runDeleteFieldMetadataMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'create_index': {
await this.workspaceMetadataIndexMigrationRunnerService.runCreateIndexMetadataMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'delete_index': {
await this.workspaceMetadataIndexMigrationRunnerService.runDeleteIndexMetadataMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
default: {
assertUnreachable(
action,
'Should never occur, encountered an unsupported workspace migration action type',
);
}
}
};
}
@@ -1,83 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
type CreateObjectAction,
type DeleteObjectAction,
type UpdateObjectAction,
type WorkspaceMigrationObjectActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { type RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util';
import { WorkspaceMetadataFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-field-action-runner.service';
@Injectable()
export class WorkspaceMetadataObjectActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationObjectActionTypeV2, 'metadata'>
{
constructor(
private readonly dataSourceService: DataSourceService,
private readonly workspaceMetadataFieldActionRunnerService: WorkspaceMetadataFieldActionRunnerService,
) {}
runDeleteObjectMetadataMigration = async ({
action: { objectMetadataId },
queryRunner,
}: WorkspaceMigrationActionRunnerArgs<DeleteObjectAction>) => {
const objectMetadataRepository =
queryRunner.manager.getRepository<ObjectMetadataEntity>(
ObjectMetadataEntity,
);
await objectMetadataRepository.delete(objectMetadataId);
};
runCreateObjectMetadataMigration = async ({
action: { flatObjectMetadataWithoutFields, createFieldActions },
queryRunner,
flatObjectMetadataMaps,
}: WorkspaceMigrationActionRunnerArgs<CreateObjectAction>) => {
const objectMetadataRepository =
queryRunner.manager.getRepository<ObjectMetadataEntity>(
ObjectMetadataEntity,
);
const lastDataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
flatObjectMetadataWithoutFields.workspaceId,
);
await objectMetadataRepository.save({
...flatObjectMetadataWithoutFields,
dataSourceId: lastDataSourceMetadata.id,
targetTableName: 'DEPRECATED',
});
for (const createFieldAction of createFieldActions) {
await this.workspaceMetadataFieldActionRunnerService.runCreateFieldMetadataMigration(
{
action: createFieldAction,
queryRunner,
flatObjectMetadataMaps,
},
);
}
};
runUpdateObjectMetadataMigration = async ({
action,
queryRunner,
}: WorkspaceMigrationActionRunnerArgs<UpdateObjectAction>) => {
const objectMetadataRepository =
queryRunner.manager.getRepository<ObjectMetadataEntity>(
ObjectMetadataEntity,
);
await objectMetadataRepository.update(
action.objectMetadataId,
fromWorkspaceMigrationUpdateActionToPartialEntity(action),
);
};
}
@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@@ -6,16 +7,9 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceMetadataFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-field-action-runner.service';
import { WorkspaceMetadataIndexActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-index-action-runner.service';
import { WorkspaceMetadataMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner-service';
import { WorkspaceMetadataObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-object-action-runner.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
import { WorkspaceSchemaFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-field-action-runner.service';
import { WorkspaceSchemaIndexActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-index-action-runner.service';
import { WorkspaceSchemaMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service';
import { WorkspaceSchemaObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-object-action-runner.service';
import { WorkspaceSchemaMigrationRunnerActionHandlersModule } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module';
import { WorkspaceMigrationRunnerActionHandlerRegistryService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/registry/workspace-migration-runner-action-handler-registry.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service';
@Module({
imports: [
@@ -25,18 +19,12 @@ import { WorkspaceSchemaObjectActionRunnerService } from 'src/engine/workspace-m
WorkspaceMetadataVersionModule,
WorkspacePermissionsCacheModule,
WorkspaceMetadataCacheModule,
WorkspaceSchemaManagerModule,
WorkspaceSchemaMigrationRunnerActionHandlersModule,
DiscoveryModule,
],
providers: [
WorkspaceMetadataObjectActionRunnerService,
WorkspaceMetadataIndexActionRunnerService,
WorkspaceMetadataFieldActionRunnerService,
WorkspaceSchemaIndexActionRunnerService,
WorkspaceMetadataMigrationRunnerService,
WorkspaceSchemaMigrationRunnerService,
WorkspaceMigrationRunnerV2Service,
WorkspaceSchemaObjectActionRunnerService,
WorkspaceSchemaFieldActionRunnerService,
WorkspaceMigrationRunnerActionHandlerRegistryService,
],
exports: [WorkspaceMigrationRunnerV2Service],
})
@@ -1,730 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type QueryRunner } from 'typeorm';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WorkspaceSchemaFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-field-action-runner.service';
describe('WorkspaceSchemaFieldActionRunner', () => {
let service: WorkspaceSchemaFieldActionRunnerService;
let mockSchemaManagerService: jest.Mocked<WorkspaceSchemaManagerService>;
let mockQueryRunner: jest.Mocked<QueryRunner>;
const mockWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const mockSchemaName = getWorkspaceSchemaName(mockWorkspaceId);
const mockObjectMetadataId = '20202020-1c25-4d02-bf25-6aeccf7ea418';
const mockFieldMetadataId = '20202020-1c25-4d02-bf25-6aeccf7ea417';
const createMockFlatObjectMetadataMaps = (
objectMetadata: any,
fieldMetadata: any,
) => ({
byId: {
[mockObjectMetadataId]: {
...objectMetadata,
fieldsById: {
[mockFieldMetadataId]: fieldMetadata,
},
},
},
idByNameSingular: {
[objectMetadata.nameSingular]: mockObjectMetadataId,
},
});
beforeEach(() => {
mockSchemaManagerService = {
columnManager: {
dropColumns: jest.fn(),
addColumns: jest.fn(),
renameColumn: jest.fn(),
alterColumnDefault: jest.fn(),
},
enumManager: {
createEnum: jest.fn(),
dropEnum: jest.fn(),
renameEnum: jest.fn(),
alterEnumValues: jest.fn(),
},
tableManager: {
createTable: jest.fn(),
dropTable: jest.fn(),
renameTable: jest.fn(),
},
} as any;
mockQueryRunner = {} as any;
service = new WorkspaceSchemaFieldActionRunnerService(
mockSchemaManagerService,
);
});
describe('DELETE Field Migration', () => {
it('should properly delete composite ADDRESS fields with all sub-columns', async () => {
const addressField = getFlatFieldMetadataMock({
id: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.ADDRESS,
name: 'homeAddress',
uniqueIdentifier: 'homeAddress',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'person',
uniqueIdentifier: 'person',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
addressField,
);
await service.runDeleteFieldSchemaMigration({
action: {
type: 'delete_field',
fieldMetadataId: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// All 8 ADDRESS composite columns must be dropped
expect(
mockSchemaManagerService.columnManager.dropColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_person',
columnNames: [
'homeAddressAddressStreet1',
'homeAddressAddressStreet2',
'homeAddressAddressCity',
'homeAddressAddressPostcode',
'homeAddressAddressState',
'homeAddressAddressCountry',
'homeAddressAddressLat',
'homeAddressAddressLng',
],
});
// No enum operations should be performed for ADDRESS fields
expect(
mockSchemaManagerService.enumManager.dropEnum,
).not.toHaveBeenCalled();
});
it('should properly delete SELECT fields with enum cleanup', async () => {
const selectField = getFlatFieldMetadataMock({
id: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'status',
options: [
{
id: '1',
value: 'ACTIVE',
label: 'Active',
color: 'green',
position: 0,
},
{
id: '2',
value: 'INACTIVE',
label: 'Inactive',
color: 'red',
position: 1,
},
],
uniqueIdentifier: 'status',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'person',
uniqueIdentifier: 'person',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
selectField,
);
await service.runDeleteFieldSchemaMigration({
action: {
type: 'delete_field',
fieldMetadataId: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Column must be dropped first
expect(
mockSchemaManagerService.columnManager.dropColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_person',
columnNames: ['status'],
});
// Enum type must be properly cleaned up
expect(
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
enumName: '_person_status_enum',
});
});
it('should properly delete RELATION fields with foreign key cleanup', async () => {
const relationField = getFlatFieldMetadataMock({
id: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.RELATION,
name: 'company',
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'companyId',
},
uniqueIdentifier: 'company',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'person',
uniqueIdentifier: 'person',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
relationField,
);
await service.runDeleteFieldSchemaMigration({
action: {
type: 'delete_field',
fieldMetadataId: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Foreign key column must be dropped
expect(
mockSchemaManagerService.columnManager.dropColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_person',
columnNames: ['companyId'],
});
// No enum operations for relation fields
expect(
mockSchemaManagerService.enumManager.dropEnum,
).not.toHaveBeenCalled();
});
});
describe('CREATE Field Migration', () => {
it('should create SELECT fields with proper enum-first ordering', async () => {
const selectField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'priority',
options: [
{ id: '1', value: 'HIGH', label: 'High', color: 'red', position: 0 },
{ id: '2', value: 'LOW', label: 'Low', color: 'blue', position: 1 },
],
uniqueIdentifier: 'priority',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'task',
uniqueIdentifier: 'task',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
selectField,
);
await service.runCreateFieldSchemaMigration({
action: {
type: 'create_field',
flatFieldMetadata: selectField,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Enum must be created BEFORE column that depends on it
const enumCreateMock = mockSchemaManagerService.enumManager
.createEnum as unknown as jest.Mock;
const addColumnsMock = mockSchemaManagerService.columnManager
.addColumns as unknown as jest.Mock;
const allCalls = [
...enumCreateMock.mock.calls.map((call: unknown[]) => ({
type: 'enum',
call,
})),
...addColumnsMock.mock.calls.map((call: unknown[]) => ({
type: 'column',
call,
})),
];
expect(allCalls).toHaveLength(2);
expect(allCalls[0].type).toBe('enum');
expect(allCalls[1].type).toBe('column');
// Enum creation with correct parameters
expect(
mockSchemaManagerService.enumManager.createEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
enumName: '_task_priority_enum',
values: ['HIGH', 'LOW'],
});
// Column creation with enum reference
expect(
mockSchemaManagerService.columnManager.addColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_task',
columnDefinitions: [
{
name: 'priority',
type: `"${mockSchemaName}"."_task_priority_enum"`,
isNullable: true,
isArray: false,
isUnique: false,
default: null,
},
],
});
});
it('should create CURRENCY composite fields with proper column generation', async () => {
const currencyField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.CURRENCY,
name: 'salary',
defaultValue: {
amountMicros: '5000000000',
currencyCode: 'USD',
},
uniqueIdentifier: 'salary',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'employee',
uniqueIdentifier: 'employee',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
currencyField,
);
await service.runCreateFieldSchemaMigration({
action: {
type: 'create_field',
flatFieldMetadata: currencyField,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// No enum operations for composite fields
expect(
mockSchemaManagerService.enumManager.createEnum,
).not.toHaveBeenCalled();
// Both composite columns must be created
expect(
mockSchemaManagerService.columnManager.addColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_employee',
columnDefinitions: [
{
name: 'salaryAmountMicros',
type: 'numeric',
isNullable: true,
isUnique: false,
default: '5000000000',
isArray: false,
},
{
name: 'salaryCurrencyCode',
type: 'text',
isNullable: true,
isUnique: false,
default: 'USD',
isArray: false,
},
],
});
});
});
describe('UPDATE Field Migration', () => {
it('should handle field name updates with composite field column renaming', async () => {
const addressField = getFlatFieldMetadataMock({
id: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.ADDRESS,
name: 'oldAddress',
uniqueIdentifier: 'oldAddress',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'company',
uniqueIdentifier: 'company',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
addressField,
);
await service.runUpdateFieldSchemaMigration({
action: {
type: 'update_field',
workspaceId: mockWorkspaceId,
fieldMetadataId: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
updates: [
{
property: 'name',
from: 'oldAddress',
to: 'newAddress',
},
],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// All 8 ADDRESS composite columns must be renamed
const expectedRenameCalls = [
['oldAddressAddressStreet1', 'newAddressAddressStreet1'],
['oldAddressAddressStreet2', 'newAddressAddressStreet2'],
['oldAddressAddressCity', 'newAddressAddressCity'],
['oldAddressAddressPostcode', 'newAddressAddressPostcode'],
['oldAddressAddressState', 'newAddressAddressState'],
['oldAddressAddressCountry', 'newAddressAddressCountry'],
['oldAddressAddressLat', 'newAddressAddressLat'],
['oldAddressAddressLng', 'newAddressAddressLng'],
];
expect(
mockSchemaManagerService.columnManager.renameColumn,
).toHaveBeenCalledTimes(8);
expectedRenameCalls.forEach(([fromName, toName]) => {
expect(
mockSchemaManagerService.columnManager.renameColumn,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_company',
oldColumnName: fromName,
newColumnName: toName,
});
});
});
it('should handle enum field option updates with proper value mapping', async () => {
const selectField = getFlatFieldMetadataMock({
id: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'status',
options: [
{
id: '1',
value: 'UPDATED_ACTIVE',
label: 'Updated Active',
color: 'green',
position: 0,
},
{
id: '2',
value: 'UPDATED_INACTIVE',
label: 'Updated Inactive',
color: 'red',
position: 1,
},
],
uniqueIdentifier: 'status',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'person',
uniqueIdentifier: 'person',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
selectField,
);
await service.runUpdateFieldSchemaMigration({
action: {
type: 'update_field',
workspaceId: mockWorkspaceId,
fieldMetadataId: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
updates: [
{
property: 'options',
from: [
{
id: '1',
value: 'ACTIVE',
label: 'Active',
color: 'green',
position: 0,
},
{
id: '2',
value: 'INACTIVE',
label: 'Inactive',
color: 'red',
position: 1,
},
],
to: [
{
id: '1',
value: 'UPDATED_ACTIVE',
label: 'Updated Active',
color: 'green',
position: 0,
},
{
id: '2',
value: 'UPDATED_INACTIVE',
label: 'Updated Inactive',
color: 'red',
position: 1,
},
],
},
],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Enum values must be updated with proper mapping
expect(
mockSchemaManagerService.enumManager.alterEnumValues,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_person',
enumValues: ['UPDATED_ACTIVE', 'UPDATED_INACTIVE'],
columnDefinition: {
name: 'status',
type: `"${mockSchemaName}"."_person_status_enum"`,
isNullable: true,
isArray: false,
isUnique: false,
default: null,
},
oldToNewEnumOptionMap: {
ACTIVE: 'UPDATED_ACTIVE', // Keep original values
INACTIVE: 'UPDATED_INACTIVE',
},
});
});
it('should handle default value updates for composite fields', async () => {
const currencyField = getFlatFieldMetadataMock({
id: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.CURRENCY,
name: 'price',
defaultValue: {
amountMicros: "'0'",
currencyCode: "'USD'",
},
uniqueIdentifier: 'price',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'product',
uniqueIdentifier: 'product',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
currencyField,
);
await service.runUpdateFieldSchemaMigration({
action: {
type: 'update_field',
workspaceId: mockWorkspaceId,
fieldMetadataId: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
updates: [
{
property: 'defaultValue',
from: {
amountMicros: "'0'",
currencyCode: "'USD'",
},
to: {
amountMicros: "'100000000'",
currencyCode: "'EUR'",
},
},
],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Both composite column defaults must be updated
expect(
mockSchemaManagerService.columnManager.alterColumnDefault,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_product',
columnName: 'priceAmountMicros',
defaultValue: '100000000',
});
expect(
mockSchemaManagerService.columnManager.alterColumnDefault,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_product',
columnName: 'priceCurrencyCode',
defaultValue: 'EUR',
});
});
});
describe('Error Handling and Edge Cases', () => {
it('should handle fields with no join column settings gracefully', async () => {
const relationFieldWithoutSettings = getFlatFieldMetadataMock({
id: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.RELATION,
name: 'company',
settings: undefined,
uniqueIdentifier: 'company',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'person',
uniqueIdentifier: 'person',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
relationFieldWithoutSettings,
);
await service.runDeleteFieldSchemaMigration({
action: {
type: 'delete_field',
fieldMetadataId: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Should not attempt to drop columns for relations without join columns
expect(
mockSchemaManagerService.columnManager.dropColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_person',
columnNames: [], // Empty array - no columns to drop
});
});
it('should handle enum fields without options safely', async () => {
const selectFieldWithoutOptions = getFlatFieldMetadataMock({
id: mockFieldMetadataId,
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'emptyStatus',
options: [],
uniqueIdentifier: 'emptyStatus',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'test',
uniqueIdentifier: 'test',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
selectFieldWithoutOptions,
);
await service.runCreateFieldSchemaMigration({
action: {
type: 'create_field',
flatFieldMetadata: selectFieldWithoutOptions,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Should still create enum even with empty values
expect(
mockSchemaManagerService.enumManager.createEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
enumName: '_test_emptyStatus_enum',
values: [], // Empty enum values array
});
});
});
});
@@ -1,873 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type QueryRunner } from 'typeorm';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WorkspaceSchemaObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-object-action-runner.service';
describe('WorkspaceSchemaObjectActionRunner', () => {
let service: WorkspaceSchemaObjectActionRunnerService;
let mockSchemaManagerService: jest.Mocked<WorkspaceSchemaManagerService>;
let mockQueryRunner: jest.Mocked<QueryRunner>;
const mockWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const mockObjectMetadataId = '20202020-1c25-4d02-bf25-6aeccf7ea418';
const mockSchemaName = getWorkspaceSchemaName(mockWorkspaceId);
const createMockFlatObjectMetadataMaps = (
objectMetadata: any,
fields: any[] = [],
) => {
const fieldsById = fields.reduce((acc, field, index) => {
const fieldId = `field-${index}`;
acc[fieldId] = { ...field, id: fieldId };
return acc;
}, {});
return {
byId: {
[mockObjectMetadataId]: {
...objectMetadata,
fieldsById,
},
},
idByNameSingular: {
[objectMetadata.nameSingular]: mockObjectMetadataId,
},
};
};
beforeEach(() => {
mockSchemaManagerService = {
columnManager: {
dropColumns: jest.fn(),
addColumns: jest.fn(),
renameColumn: jest.fn(),
alterColumnDefault: jest.fn(),
},
enumManager: {
createEnum: jest.fn(),
dropEnum: jest.fn(),
renameEnum: jest.fn(),
alterEnumValues: jest.fn(),
},
tableManager: {
createTable: jest.fn(),
dropTable: jest.fn(),
renameTable: jest.fn(),
},
} as any;
mockQueryRunner = {} as any;
service = new WorkspaceSchemaObjectActionRunnerService(
mockSchemaManagerService,
);
});
describe('DELETE Object Migration', () => {
it('should properly delete objects with SELECT field enum cleanup', async () => {
const selectField1 = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'status',
uniqueIdentifier: 'status',
options: [
{
id: '1',
value: 'ACTIVE',
label: 'Active',
color: 'green',
position: 0,
},
],
});
const selectField2 = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'priority',
uniqueIdentifier: 'priority',
options: [
{ id: '1', value: 'HIGH', label: 'High', color: 'red', position: 0 },
],
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'task',
uniqueIdentifier: 'task',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
[selectField1, selectField2],
);
await service.runDeleteObjectSchemaMigration({
action: {
type: 'delete_object',
objectMetadataId: mockObjectMetadataId,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Table must be dropped first
expect(
mockSchemaManagerService.tableManager.dropTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_task',
});
// All enum types associated with the object must be cleaned up
expect(
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledTimes(2);
expect(
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
enumName: '_task_status_enum',
});
expect(
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
enumName: '_task_priority_enum',
});
});
it('should handle object deletion with mixed field types', async () => {
const textField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.TEXT,
name: 'description',
uniqueIdentifier: 'description',
});
const addressField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.ADDRESS,
name: 'location',
uniqueIdentifier: 'location',
});
const multiSelectField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.MULTI_SELECT,
name: 'tags',
uniqueIdentifier: 'tags',
options: [
{
id: '1',
value: 'URGENT',
label: 'Urgent',
color: 'red',
position: 0,
},
],
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'project',
uniqueIdentifier: 'project',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
[textField, addressField, multiSelectField],
);
await service.runDeleteObjectSchemaMigration({
action: {
type: 'delete_object',
objectMetadataId: mockObjectMetadataId,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Table must be dropped
expect(
mockSchemaManagerService.tableManager.dropTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_project',
});
// Only enum fields should trigger enum cleanup
expect(
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledTimes(1);
expect(
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
enumName: '_project_tags_enum',
});
});
it('should handle object deletion with no enum fields', async () => {
const textField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.TEXT,
name: 'name',
uniqueIdentifier: 'name',
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'simpleObject',
uniqueIdentifier: 'simpleObject',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
[textField],
);
await service.runDeleteObjectSchemaMigration({
action: {
type: 'delete_object',
objectMetadataId: mockObjectMetadataId,
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Table must be dropped
expect(
mockSchemaManagerService.tableManager.dropTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_simpleObject',
});
// No enum cleanup should occur
expect(
mockSchemaManagerService.enumManager.dropEnum,
).not.toHaveBeenCalled();
});
});
describe('CREATE Object Migration', () => {
it('should create objects with mixed field types and proper column generation', async () => {
const textFieldAction = {
type: 'create_field' as const,
flatFieldMetadata: getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.TEXT,
name: 'title',
uniqueIdentifier: 'title',
}),
};
const selectFieldAction = {
type: 'create_field' as const,
flatFieldMetadata: getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'status',
uniqueIdentifier: 'status',
options: [
{
id: '1',
value: 'DRAFT',
label: 'Draft',
color: 'gray',
position: 0,
},
{
id: '2',
value: 'PUBLISHED',
label: 'Published',
color: 'green',
position: 1,
},
],
}),
};
const currencyFieldAction = {
type: 'create_field' as const,
flatFieldMetadata: getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.CURRENCY,
name: 'price',
uniqueIdentifier: 'price',
}),
};
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'article',
uniqueIdentifier: 'article',
});
await service.runCreateObjectSchemaMigration({
action: {
type: 'create_object',
flatObjectMetadataWithoutFields: objectMetadata,
createFieldActions: [
textFieldAction,
selectFieldAction,
currencyFieldAction,
],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps: {
byId: {
[mockObjectMetadataId]: {
...objectMetadata,
fieldsById: {},
fieldIdByJoinColumnName: {},
fieldIdByName: {},
},
},
idByNameSingular: {
[objectMetadata.nameSingular]: mockObjectMetadataId,
},
},
});
// Table must be created with all field columns and enums
expect(
mockSchemaManagerService.tableManager.createTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_article',
columnDefinitions: [
// TEXT field column
{
name: 'title',
type: 'text',
isNullable: true,
isArray: false,
isUnique: false,
default: null,
},
// SELECT field column
{
name: 'status',
type: `"${mockSchemaName}"."_article_status_enum"`,
isNullable: true,
isArray: false,
isUnique: false,
default: null,
},
// CURRENCY field columns (2 composite columns)
{
name: 'priceAmountMicros',
type: 'numeric',
isNullable: true,
isArray: false,
isUnique: false,
default: null,
},
{
name: 'priceCurrencyCode',
type: 'text',
isNullable: true,
isArray: false,
isUnique: false,
default: null,
},
],
});
expect(
mockSchemaManagerService.enumManager.createEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
enumName: '_article_status_enum',
values: ['DRAFT', 'PUBLISHED'],
});
});
it('should handle object creation with ADDRESS composite fields', async () => {
const addressFieldAction = {
type: 'create_field' as const,
flatFieldMetadata: getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.ADDRESS,
name: 'headquarters',
isNullable: false, // Test non-nullable composite field
uniqueIdentifier: 'headquarters',
}),
};
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'company',
uniqueIdentifier: 'company',
});
await service.runCreateObjectSchemaMigration({
action: {
type: 'create_object',
flatObjectMetadataWithoutFields: objectMetadata,
createFieldActions: [addressFieldAction],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps: {
byId: {
[mockObjectMetadataId]: {
...objectMetadata,
fieldsById: {},
fieldIdByJoinColumnName: {},
fieldIdByName: {},
},
},
idByNameSingular: {
[objectMetadata.nameSingular]: mockObjectMetadataId,
},
},
});
// Table must be created with all 8 ADDRESS composite columns
expect(
mockSchemaManagerService.tableManager.createTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_company',
columnDefinitions: [
{
name: 'headquartersAddressStreet1',
type: 'text',
isNullable: true,
isUnique: false,
default: null,
isArray: false,
},
{
name: 'headquartersAddressStreet2',
type: 'text',
isNullable: true,
isUnique: false,
default: null,
isArray: false,
},
{
name: 'headquartersAddressCity',
type: 'text',
isNullable: true,
isUnique: false,
default: null,
isArray: false,
},
{
name: 'headquartersAddressPostcode',
type: 'text',
isNullable: true,
isUnique: false,
default: null,
isArray: false,
},
{
name: 'headquartersAddressState',
type: 'text',
isNullable: true,
isUnique: false,
default: null,
isArray: false,
},
{
name: 'headquartersAddressCountry',
type: 'text',
isNullable: true,
isUnique: false,
default: null,
isArray: false,
},
{
name: 'headquartersAddressLat',
type: 'numeric',
isNullable: true,
isUnique: false,
default: null,
isArray: false,
},
{
name: 'headquartersAddressLng',
type: 'numeric',
isNullable: true,
isUnique: false,
default: null,
isArray: false,
},
],
});
});
it('should handle object creation with no field actions', async () => {
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'emptyObject',
uniqueIdentifier: 'emptyObject',
});
await service.runCreateObjectSchemaMigration({
action: {
type: 'create_object',
flatObjectMetadataWithoutFields: objectMetadata,
createFieldActions: [],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps: {
byId: {
[mockObjectMetadataId]: {
...objectMetadata,
fieldsById: {},
fieldIdByJoinColumnName: {},
fieldIdByName: {},
},
},
idByNameSingular: {
[objectMetadata.nameSingular]: mockObjectMetadataId,
},
},
});
// Table must be created even with no columns
expect(
mockSchemaManagerService.tableManager.createTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_emptyObject',
columnDefinitions: [], // Empty columns array
});
});
});
describe('UPDATE Object Migration', () => {
it('should handle object name changes with enum type updates', async () => {
const selectField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'category',
uniqueIdentifier: 'category',
options: [
{
id: '1',
value: 'TECH',
label: 'Technology',
color: 'blue',
position: 0,
},
],
});
const multiSelectField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.MULTI_SELECT,
name: 'tags',
uniqueIdentifier: 'tags',
options: [
{
id: '1',
value: 'FEATURED',
label: 'Featured',
color: 'yellow',
position: 0,
},
],
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'blogPost',
uniqueIdentifier: 'blogPost',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
[selectField, multiSelectField],
);
await service.runUpdateObjectSchemaMigration({
action: {
type: 'update_object',
objectMetadataId: mockObjectMetadataId,
updates: [
{
property: 'nameSingular',
from: 'blogPost',
to: 'article',
},
],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Table must be renamed
expect(
mockSchemaManagerService.tableManager.renameTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
oldTableName: '_blogPost',
newTableName: '_article',
});
// All enum types must be renamed to match new table name
expect(
mockSchemaManagerService.enumManager.renameEnum,
).toHaveBeenCalledTimes(2);
expect(
mockSchemaManagerService.enumManager.renameEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
oldEnumName: '_blogPost_category_enum',
newEnumName: '_article_category_enum',
});
expect(
mockSchemaManagerService.enumManager.renameEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
oldEnumName: '_blogPost_tags_enum',
newEnumName: '_article_tags_enum',
});
});
it('should handle object name changes with no actual table rename needed', async () => {
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'person',
uniqueIdentifier: 'person',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
[],
);
await service.runUpdateObjectSchemaMigration({
action: {
type: 'update_object',
objectMetadataId: mockObjectMetadataId,
updates: [
{
property: 'nameSingular',
from: 'person',
to: 'person', // Same name - no change needed
},
],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// No table operations should occur when names are the same
expect(
mockSchemaManagerService.tableManager.renameTable,
).not.toHaveBeenCalled();
expect(
mockSchemaManagerService.enumManager.renameEnum,
).not.toHaveBeenCalled();
});
it('should handle object updates with complex field combinations', async () => {
const textField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.TEXT,
name: 'description',
uniqueIdentifier: 'description',
});
const selectField = getFlatFieldMetadataMock({
objectMetadataId: mockObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'status',
uniqueIdentifier: 'status',
options: [
{
id: '1',
value: 'ACTIVE',
label: 'Active',
color: 'green',
position: 0,
},
],
});
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'oldEntity',
uniqueIdentifier: 'oldEntity',
});
const flatObjectMetadataMaps = createMockFlatObjectMetadataMaps(
objectMetadata,
[textField, selectField],
);
await service.runUpdateObjectSchemaMigration({
action: {
type: 'update_object',
objectMetadataId: mockObjectMetadataId,
updates: [
{
property: 'nameSingular',
from: 'oldEntity',
to: 'newEntity',
},
],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Table rename
expect(
mockSchemaManagerService.tableManager.renameTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
oldTableName: '_oldEntity',
newTableName: '_newEntity',
});
// Only SELECT field should trigger enum rename
expect(
mockSchemaManagerService.enumManager.renameEnum,
).toHaveBeenCalledTimes(1);
expect(
mockSchemaManagerService.enumManager.renameEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
oldEnumName: '_oldEntity_status_enum',
newEnumName: '_newEntity_status_enum',
});
});
});
describe('Edge Cases and Error Conditions', () => {
it('should handle objects with undefined fieldsById gracefully', async () => {
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'testObject',
uniqueIdentifier: 'testObject',
});
// Create maps with undefined fieldsById
const flatObjectMetadataMaps = {
byId: {
[mockObjectMetadataId]: {
...objectMetadata,
fieldsById: undefined as unknown as Record<string, any>,
fieldIdByJoinColumnName: {},
fieldIdByName: {},
},
},
idByNameSingular: {
[objectMetadata.nameSingular]: mockObjectMetadataId,
},
};
await service.runCreateObjectSchemaMigration({
action: {
type: 'create_object',
flatObjectMetadataWithoutFields: objectMetadata,
createFieldActions: [],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Should still drop table
expect(
mockSchemaManagerService.tableManager.createTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
tableName: '_testObject',
columnDefinitions: [],
});
});
it('should handle empty fieldsById object', async () => {
const objectMetadata = getFlatObjectMetadataMock({
id: mockObjectMetadataId,
workspaceId: mockWorkspaceId,
nameSingular: 'emptyFieldsObject',
uniqueIdentifier: 'emptyFieldsObject',
});
const flatObjectMetadataMaps = {
byId: {
[mockObjectMetadataId]: {
...objectMetadata,
fieldsById: {},
fieldIdByJoinColumnName: {},
fieldIdByName: {},
},
},
idByNameSingular: {
[objectMetadata.nameSingular]: mockObjectMetadataId,
},
};
await service.runUpdateObjectSchemaMigration({
action: {
type: 'update_object',
objectMetadataId: mockObjectMetadataId,
updates: [
{
property: 'nameSingular',
from: 'emptyFieldsObject',
to: 'renamedEmptyFieldsObject',
},
],
},
queryRunner: mockQueryRunner,
flatObjectMetadataMaps,
});
// Should rename table
expect(
mockSchemaManagerService.tableManager.renameTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: mockSchemaName,
oldTableName: '_emptyFieldsObject',
newTableName: '_renamedEmptyFieldsObject',
});
// Should not perform enum operations
expect(
mockSchemaManagerService.enumManager.renameEnum,
).not.toHaveBeenCalled();
});
});
});
@@ -1,13 +0,0 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceSchemaMigrationException extends CustomException<WorkspaceSchemaMigrationExceptionCode> {}
export enum WorkspaceSchemaMigrationExceptionCode {
COMPOSITE_TYPE_NOT_FOUND = 'COMPOSITE_TYPE_NOT_FOUND',
INVALID_FIELD_TYPE = 'INVALID_FIELD_TYPE',
ENUM_OPERATION_FAILED = 'ENUM_OPERATION_FAILED',
COLUMN_OPERATION_FAILED = 'COLUMN_OPERATION_FAILED',
TABLE_OPERATION_FAILED = 'TABLE_OPERATION_FAILED',
UNSUPPORTED_COMPOSITE_COLUMN_TYPE = 'UNSUPPORTED_COMPOSITE_COLUMN_TYPE',
NOT_SUPPORTED = 'NOT_SUPPORTED',
}
@@ -1,63 +0,0 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { type FlatObjectMetadataWithFlatFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-with-flat-field-metadata-maps.type';
import { findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-with-flat-field-maps-in-flat-object-metadata-maps-or-throw.util';
import { getWorkspaceSchemaContextForMigration } from './get-workspace-schema-context-for-migration.util';
export interface WorkspaceSchemaContext {
flatObjectMetadataWithFlatFieldMaps: FlatObjectMetadataWithFlatFieldMaps;
schemaName: string;
tableName: string;
}
export const prepareWorkspaceSchemaContext = ({
flatObjectMetadataMaps,
objectMetadataId,
}: {
flatObjectMetadataMaps: FlatObjectMetadataMaps;
objectMetadataId: string;
}): WorkspaceSchemaContext => {
const flatObjectMetadataWithFlatFieldMaps =
findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
});
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
workspaceId: flatObjectMetadataWithFlatFieldMaps.workspaceId,
flatObjectMetadataWithoutFields: flatObjectMetadataWithFlatFieldMaps,
});
return {
flatObjectMetadataWithFlatFieldMaps,
schemaName,
tableName,
};
};
export const prepareFieldWorkspaceSchemaContext = ({
flatObjectMetadataMaps,
objectMetadataId,
fieldMetadataId,
}: {
flatObjectMetadataMaps: FlatObjectMetadataMaps;
objectMetadataId: string;
fieldMetadataId: string;
}): WorkspaceSchemaContext & { fieldMetadata: FlatFieldMetadata } => {
const context = prepareWorkspaceSchemaContext({
flatObjectMetadataMaps,
objectMetadataId,
});
const fieldMetadata = findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
objectMetadataId,
fieldMetadataId,
});
return {
...context,
fieldMetadata,
};
};
@@ -1,28 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
type CreateIndexAction,
type DeleteIndexAction,
type WorkspaceMigrationIndexActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2';
import { type RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceSchemaIndexActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationIndexActionTypeV2, 'schema'>
{
// TODO: Implement index schema operations
runDeleteIndexSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<DeleteIndexAction>,
) => {
return;
};
runCreateIndexSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<CreateIndexAction>,
) => {
return;
};
}
@@ -1,80 +0,0 @@
import { Injectable } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import { type WorkspaceMigrationRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-runner-args.type';
import { WorkspaceSchemaFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-field-action-runner.service';
import { WorkspaceSchemaIndexActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-index-action-runner.service';
import { WorkspaceSchemaObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-object-action-runner.service';
@Injectable()
export class WorkspaceSchemaMigrationRunnerService {
constructor(
private readonly workspaceSchemaObjectMigrationRunnerService: WorkspaceSchemaObjectActionRunnerService,
private readonly workspaceSchemaIndexMigrationRunnerService: WorkspaceSchemaIndexActionRunnerService,
private readonly workspaceSchemaFieldMigrationRunnerService: WorkspaceSchemaFieldActionRunnerService,
) {}
runWorkspaceSchemaMigration = async ({
action,
queryRunner,
flatObjectMetadataMaps,
}: WorkspaceMigrationRunnerArgs) => {
switch (action.type) {
case 'delete_object': {
await this.workspaceSchemaObjectMigrationRunnerService.runDeleteObjectSchemaMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'create_object': {
await this.workspaceSchemaObjectMigrationRunnerService.runCreateObjectSchemaMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'update_object': {
await this.workspaceSchemaObjectMigrationRunnerService.runUpdateObjectSchemaMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'create_field': {
await this.workspaceSchemaFieldMigrationRunnerService.runCreateFieldSchemaMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'update_field': {
await this.workspaceSchemaFieldMigrationRunnerService.runUpdateFieldSchemaMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'delete_field': {
await this.workspaceSchemaFieldMigrationRunnerService.runDeleteFieldSchemaMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'create_index': {
await this.workspaceSchemaIndexMigrationRunnerService.runCreateIndexSchemaMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
case 'delete_index': {
await this.workspaceSchemaIndexMigrationRunnerService.runDeleteIndexSchemaMigration(
{ action, queryRunner, flatObjectMetadataMaps },
);
break;
}
default: {
assertUnreachable(
action,
'Should never occur, encountered an unsupported workspace migration action type',
);
}
}
};
}
@@ -1,181 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import {
type CreateObjectAction,
type DeleteObjectAction,
type UpdateObjectAction,
type WorkspaceMigrationObjectActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { type RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/utils/generate-column-definitions.util';
import { prepareWorkspaceSchemaContext } from './utils/workspace-schema-context.util';
import {
collectEnumOperationsForObject,
EnumOperation,
executeBatchEnumOperations,
} from './utils/workspace-schema-enum-operations.util';
@Injectable()
export class WorkspaceSchemaObjectActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationObjectActionTypeV2, 'schema'>
{
constructor(
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {}
runDeleteObjectSchemaMigration = async ({
action: { objectMetadataId },
queryRunner,
flatObjectMetadataMaps,
}: WorkspaceMigrationActionRunnerArgs<DeleteObjectAction>) => {
const { schemaName, tableName, flatObjectMetadataWithFlatFieldMaps } =
prepareWorkspaceSchemaContext({
flatObjectMetadataMaps,
objectMetadataId,
});
await this.workspaceSchemaManagerService.tableManager.dropTable({
queryRunner,
schemaName,
tableName,
});
const enumOrCompositeFlatFieldMetadatas = Object.values(
flatObjectMetadataWithFlatFieldMaps.fieldsById,
)
.filter((field): field is FlatFieldMetadata => field != null)
.filter(
(field) =>
isEnumFlatFieldMetadata(field) || isCompositeFlatFieldMetadata(field),
);
const enumOperations = collectEnumOperationsForObject({
flatFieldMetadatas: enumOrCompositeFlatFieldMetadatas,
tableName,
operation: EnumOperation.DROP,
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
};
runCreateObjectSchemaMigration = async ({
action: { flatObjectMetadataWithoutFields, createFieldActions },
queryRunner,
}: WorkspaceMigrationActionRunnerArgs<CreateObjectAction>) => {
const flatObjectMetadata = flatObjectMetadataWithoutFields;
const schemaName = getWorkspaceSchemaName(flatObjectMetadata.workspaceId);
const tableName = computeObjectTargetTable(flatObjectMetadata);
const columnDefinitions = createFieldActions.flatMap((createFieldAction) =>
generateColumnDefinitions({
flatFieldMetadata: createFieldAction.flatFieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadataWithoutFields,
}),
);
const enumOrCompositeFlatFieldMetadatas = createFieldActions
.map((createFieldAction) => createFieldAction.flatFieldMetadata)
.filter((field): field is FlatFieldMetadata => field != null)
.filter(
(field) =>
isEnumFlatFieldMetadata(field) || isCompositeFlatFieldMetadata(field),
);
const enumOperations = collectEnumOperationsForObject({
flatFieldMetadatas: enumOrCompositeFlatFieldMetadatas,
tableName,
operation: EnumOperation.CREATE,
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
await this.workspaceSchemaManagerService.tableManager.createTable({
queryRunner,
schemaName,
tableName,
columnDefinitions,
});
};
runUpdateObjectSchemaMigration = async ({
action,
queryRunner,
flatObjectMetadataMaps,
}: WorkspaceMigrationActionRunnerArgs<UpdateObjectAction>) => {
const { objectMetadataId, updates } = action;
const {
schemaName,
tableName: currentTableName,
flatObjectMetadataWithFlatFieldMaps,
} = prepareWorkspaceSchemaContext({
flatObjectMetadataMaps,
objectMetadataId,
});
for (const update of updates) {
if (update.property !== 'nameSingular') {
continue;
}
const updatedObjectMetadata = {
...flatObjectMetadataWithFlatFieldMaps,
[update.property]: update.to,
};
const newTableName = computeObjectTargetTable(updatedObjectMetadata);
if (currentTableName !== newTableName) {
await this.workspaceSchemaManagerService.tableManager.renameTable({
queryRunner,
schemaName,
oldTableName: currentTableName,
newTableName,
});
const enumOrCompositeFlatFieldMetadatas = Object.values(
flatObjectMetadataWithFlatFieldMaps.fieldsById,
)
.filter((field): field is FlatFieldMetadata => field != null)
.filter(
(field) =>
isEnumFlatFieldMetadata(field) ||
isCompositeFlatFieldMetadata(field),
);
const enumOperations = collectEnumOperationsForObject({
flatFieldMetadatas: enumOrCompositeFlatFieldMetadatas,
tableName: currentTableName,
operation: EnumOperation.RENAME,
options: {
newTableName,
},
});
await executeBatchEnumOperations({
enumOperations,
queryRunner,
schemaName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
});
}
}
};
}
@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { WorkspaceMigrationBuilderV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.module';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
import { WorkspaceMigrationBuilderV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module';
import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-validate-build-and-run-service';
@Module({
imports: [
@@ -10,10 +10,6 @@ import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspa
WorkspaceMigrationRunnerV2Module,
],
providers: [WorkspaceMigrationValidateBuildAndRunService],
exports: [
WorkspaceMigrationRunnerV2Module,
WorkspaceMigrationBuilderV2Module,
WorkspaceMigrationValidateBuildAndRunService,
],
exports: [WorkspaceMigrationValidateBuildAndRunService],
})
export class WorkspaceMigrationV2Module {}