Refactor builder to embed Object validation create/delete/update (#13934)

# Introduction
Moving validation directly in the builder that has the perfect
granularity to do it.
When importing we won't have to infer and dispatch on the operation
nature ( update delete create ) and validate accordingly

## Objects
Only migrated object validation for the moment even though create object
involves a validate flat field metadata creation call too

## TODO
- improve `otherFlatObjectMetadataMapsToValidate` naming too vague

## Next
- handle fields validation within fields actions build
- Unit test coverage validation issue on builder and validate
- integration test plugging with new feature flag

## Manual tested
- Update
- Delete
- Create TODO
This commit is contained in:
Paul Rastoin
2025-08-18 11:16:16 +02:00
committed by GitHub
parent 18b2a68b69
commit 6629092645
51 changed files with 3800 additions and 617 deletions
@@ -1,5 +1,4 @@
import { getFlatIndexMetadataMock } from 'src/engine/metadata-modules/flat-index-metadata/__mocks__/get-flat-index-metadata.mock';
import { EMPTY_FLAT_OBJECT_METADATA_MAPS } from 'src/engine/metadata-modules/flat-object-metadata-maps/constant/empty-flat-object-metadata-maps.constant';
import { FLAT_OBJECT_METADATA_MAPS_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata-maps/mocks/flat-object-metadata-maps.mock';
import { deleteObjectFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-object-from-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';
@@ -8,116 +7,187 @@ import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-obje
import { OPPORTUNITY_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/opportunity-flat-object.mock';
import { PET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock';
import { ROCKET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/rocket-flat-object.mock';
import { STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/standard-relation-target-flat-object-metadata.mocks';
import { fromFlatObjectMetadatasToFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadatas-to-flat-object-metadata-maps.util';
import { type WorkspaceMigrationBuilderTestCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/types/workspace-migration-builder-test-case.type';
// TODO prastoin implement validation exception tests cases coverage too
const DELETE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [
{
title:
'It should build a delete_object action with custom deactivated object',
context: {
input: {
fromFlatObjectMetadataMaps:
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadata: getFlatObjectMetadataMock({
...ROCKET_FLAT_OBJECT_MOCK,
isActive: false,
}),
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
}),
toFlatObjectMetadataMaps: deleteObjectFromFlatObjectMetadataMapsOrThrow(
{
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
objectMetadataId: ROCKET_FLAT_OBJECT_MOCK.id,
},
),
},
expectedActionsTypeCounter: {
deleteObject: 1,
deleteField: ROCKET_FLAT_OBJECT_MOCK.flatFieldMetadatas.length,
},
},
},
{
title:
'It should build a delete_object action with standard activated object',
context: {
input: {
buildOptions: {
inferDeletionFromMissingObjectFieldIndex: true,
isSystemBuild: true,
},
fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
toFlatObjectMetadataMaps: deleteObjectFromFlatObjectMetadataMapsOrThrow(
{
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
},
),
},
expectedActionsTypeCounter: {
deleteObject: 1,
deleteField: COMPANY_FLAT_OBJECT_MOCK.flatFieldMetadatas.length,
},
},
},
];
const CREATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [
{
title: 'It should build a create_object action with custom object',
context: {
input: {
fromFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
PET_FLAT_OBJECT_MOCK,
...STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS,
]),
toFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
PET_FLAT_OBJECT_MOCK,
ROCKET_FLAT_OBJECT_MOCK,
...STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS,
]),
},
expectedActionsTypeCounter: {
createObject: 1,
},
},
},
{
title: 'It should build a create_object action with standard object',
context: {
input: {
fromFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
ROCKET_FLAT_OBJECT_MOCK,
...STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS,
]),
toFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
PET_FLAT_OBJECT_MOCK,
ROCKET_FLAT_OBJECT_MOCK,
...STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS,
]),
},
expectedActionsTypeCounter: {
createObject: 1,
},
},
},
{
title:
'It should build a create_object and create_index actions for each of this fieldMetadata',
context: {
expectedActionsTypeCounter: {
createIndex: 1,
createObject: 1,
},
input: {
fromFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
...STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS,
]),
toFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
...STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS,
{
...ROCKET_FLAT_OBJECT_MOCK,
flatIndexMetadatas: [
getFlatIndexMetadataMock({
objectMetadataId: ROCKET_FLAT_OBJECT_MOCK.id,
uniqueIdentifier: 'field-metadata-unique-identifier-1',
}),
],
},
]),
},
},
},
];
const UPDATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [
{
title: 'It should build an update_object for custom object',
context: {
input: {
fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
toFlatObjectMetadataMaps:
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatObjectMetadata: getFlatObjectMetadataMock({
...PET_FLAT_OBJECT_MOCK,
nameSingular: 'prastouin',
namePlural: 'prastoins',
isLabelSyncedWithName: false,
}),
}),
},
expectedActionsTypeCounter: {
updateObject: 1,
},
},
},
{
title: 'It should build an update_object for standard object',
context: {
input: {
fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
toFlatObjectMetadataMaps:
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatObjectMetadata: getFlatObjectMetadataMock({
...OPPORTUNITY_FLAT_OBJECT_MOCK,
standardOverrides: {
description: 'Updated description',
},
}),
}),
},
expectedActionsTypeCounter: {
updateObject: 1,
},
},
},
];
export const WORKSPACE_MIGRATION_OBJECT_BUILDER_TEST_CASES: WorkspaceMigrationBuilderTestCase[] =
[
{
title: 'It should build an update_object for custom object',
context: {
input: {
fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
toFlatObjectMetadataMaps:
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatObjectMetadata: getFlatObjectMetadataMock({
...PET_FLAT_OBJECT_MOCK,
nameSingular: 'prastouin',
namePlural: 'prastoins',
isLabelSyncedWithName: false,
}),
}),
},
expectedActionsTypeCounter: {
updateObject: 1,
},
},
},
{
title: 'It should build an update_object for standard object',
context: {
input: {
fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
toFlatObjectMetadataMaps:
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatObjectMetadata: getFlatObjectMetadataMock({
...OPPORTUNITY_FLAT_OBJECT_MOCK,
standardOverrides: {
description: 'Updated description',
},
}),
}),
},
expectedActionsTypeCounter: {
updateObject: 1,
},
},
},
{
title: 'It should build a create_object action',
context: {
input: {
fromFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
PET_FLAT_OBJECT_MOCK,
]),
toFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
PET_FLAT_OBJECT_MOCK,
ROCKET_FLAT_OBJECT_MOCK,
]),
},
expectedActionsTypeCounter: {
createObject: 1,
},
},
},
{
title:
'It should build a create_object and create_index actions for each of this fieldMetadata',
context: {
input: {
fromFlatObjectMetadataMaps: EMPTY_FLAT_OBJECT_METADATA_MAPS,
toFlatObjectMetadataMaps:
fromFlatObjectMetadatasToFlatObjectMetadataMaps([
{
...ROCKET_FLAT_OBJECT_MOCK,
flatIndexMetadatas: [
getFlatIndexMetadataMock({
objectMetadataId: ROCKET_FLAT_OBJECT_MOCK.id,
uniqueIdentifier: 'field-metadata-unique-identifier-1',
}),
],
},
]),
},
expectedActionsTypeCounter: {
createObject: 1,
createField: 0,
createIndex: 1,
},
},
},
{
title: 'It should build a delete_object action',
context: {
input: {
fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
toFlatObjectMetadataMaps:
deleteObjectFromFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
}),
},
expectedActionsTypeCounter: {
deleteObject: 1,
deleteField: COMPANY_FLAT_OBJECT_MOCK.flatFieldMetadatas.length,
},
},
},
...DELETE_OBJECT_TEST_CASES,
...CREATE_OBJECT_TEST_CASES,
...UPDATE_OBJECT_TEST_CASES,
{
title: 'It should not infer any actions as from and to are identical',
context: {
@@ -11,9 +11,15 @@ export type ExpectedActionCounters = Partial<
Record<CamelCasedWorkspaceMigrationActionsType, number>
>;
type TestWorkspaceMigrationBuildArgs = Omit<
WorkspaceMigrationBuildArgs,
'workspaceId' | 'buildOptions'
> &
Partial<Pick<WorkspaceMigrationBuildArgs, 'buildOptions'>>;
export type WorkspaceMigrationBuilderTestCase = EachTestingContext<{
input:
| Omit<WorkspaceMigrationBuildArgs, 'workspaceId'>
| (() => Omit<WorkspaceMigrationBuildArgs, 'workspaceId'>);
| TestWorkspaceMigrationBuildArgs
| (() => TestWorkspaceMigrationBuildArgs);
expectedActionsTypeCounter?: ExpectedActionCounters;
}>;
@@ -2,6 +2,11 @@ import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-i
import { eachTestingContextFilter } from 'twenty-shared/testing';
import { capitalize } from 'twenty-shared/utils';
import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
import { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service';
import { WorkspaceMigrationV2ObjectActionsBuilder } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder';
import { WORKSPACE_MIGRATION_FIELD_BUILDER_TEST_CASES } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case';
import { WORKSPACE_MIGRATION_INDEX_BUILDER_TEST_CASES } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-index-test-case';
import { WORKSPACE_MIGRATION_OBJECT_BUILDER_TEST_CASES } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case';
@@ -74,31 +79,63 @@ describe.each(allWorkspaceBuilderTestCases)(
'Workspace migration builder $label actions test suite',
({ testCases }) => {
let service: WorkspaceMigrationBuilderV2Service;
let featureFlagService: FeatureFlagService;
let flatFieldMetadataTypeValidatorService: FlatFieldMetadataTypeValidatorService;
let flatFieldMetadataValidatorService: FlatFieldMetadataValidatorService;
let flatObjectMetadataValidatorService: FlatObjectMetadataValidatorService;
let objectActionsBuilder: WorkspaceMigrationV2ObjectActionsBuilder;
beforeEach(() => {
service = new WorkspaceMigrationBuilderV2Service();
featureFlagService = {
isFeatureEnabled: jest.fn().mockResolvedValue(true),
} as any;
flatFieldMetadataTypeValidatorService =
new FlatFieldMetadataTypeValidatorService(featureFlagService);
flatFieldMetadataValidatorService = new FlatFieldMetadataValidatorService(
flatFieldMetadataTypeValidatorService,
);
flatObjectMetadataValidatorService =
new FlatObjectMetadataValidatorService(
flatFieldMetadataValidatorService,
);
objectActionsBuilder = new WorkspaceMigrationV2ObjectActionsBuilder(
flatObjectMetadataValidatorService,
);
service = new WorkspaceMigrationBuilderV2Service(objectActionsBuilder);
});
it.each(eachTestingContextFilter(testCases))(
'$title',
({ context: { input, expectedActionsTypeCounter } }) => {
async ({ context: { input, expectedActionsTypeCounter } }) => {
const {
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex,
buildOptions = {
inferDeletionFromMissingObjectFieldIndex: true,
isSystemBuild: false,
},
} = typeof input === 'function' ? input() : input;
const workspaceMigration = service.build({
const validateAndBuildResult = await service.validateAndBuild({
buildOptions,
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex,
workspaceId: '20202020-52cc-4c64-ad63-76c26fc3a1e1',
});
expect(validateAndBuildResult.status).toBe('success');
if (validateAndBuildResult.status !== 'success') {
throw new Error('Should never occur');
}
expectedActionsTypeCounterChecker({
expectedActionsTypeCounter,
workspaceMigration,
workspaceMigration: validateAndBuildResult.workspaceMigration,
});
const { actions } = workspaceMigration;
const { actions } = validateAndBuildResult.workspaceMigration;
expect(actions).toMatchSnapshot(
actions.map(extractRecordIdsAndDatesAsExpectAny),
@@ -0,0 +1,6 @@
import { type FailedAndSuccessfulMetadataValidateAndBuildRecord } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/failed-and-successful-metadata-validate-and-build-record.type';
export const EMPTY_FAILED_AND_SUCCESSFUL_METADATA_BUILD_RECORD = {
failed: [],
successful: [],
} as const satisfies FailedAndSuccessfulMetadataValidateAndBuildRecord;
@@ -0,0 +1,10 @@
import { type FailedMetadataValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/failed-metadata-validate-and-build.type';
import { type SuccessfulMetadataValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/successful-metadata-validate-and-build.type';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type FailedAndSuccessfulMetadataValidateAndBuildRecord<
T extends WorkspaceMigrationActionV2 = WorkspaceMigrationActionV2,
> = {
failed: FailedMetadataValidateAndBuild[];
successful: SuccessfulMetadataValidateAndBuild<T>[];
};
@@ -0,0 +1,10 @@
import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type';
export type FailedMetadataValidateAndBuild = {
status: 'fail';
errors: (
| FailedFlatFieldMetadataValidationExceptions
| FailedFlatObjectMetadataValidationExceptions
)[];
};
@@ -0,0 +1,10 @@
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { type FailedAndSuccessfulMetadataValidateAndBuildRecord } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/failed-and-successful-metadata-validate-and-build-record.type';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type MetadataActionValidateAndBuildResult<
T extends WorkspaceMigrationActionV2,
> = {
results: FailedAndSuccessfulMetadataValidateAndBuildRecord<T>;
optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps;
};
@@ -0,0 +1,7 @@
import { type FailedMetadataValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/failed-metadata-validate-and-build.type';
import { type SuccessfulMetadataValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/successful-metadata-validate-and-build.type';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type MetadataValidateAndBuildResult<
T extends WorkspaceMigrationActionV2 = WorkspaceMigrationActionV2,
> = FailedMetadataValidateAndBuild | SuccessfulMetadataValidateAndBuild<T>;
@@ -0,0 +1,8 @@
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type SuccessfulMetadataValidateAndBuild<
T extends WorkspaceMigrationActionV2 = WorkspaceMigrationActionV2,
> = {
status: 'success';
result: T;
};
@@ -9,7 +9,7 @@ import {
export type UpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix = {
flatObjectMetadata: FlatObjectMetadata;
} & CustomDeletedCreatedUpdatedMatrix<'fieldMetadata', FlatFieldMetadata>;
} & CustomDeletedCreatedUpdatedMatrix<'flatFieldMetadata', FlatFieldMetadata>;
export const computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix = (
updatedObjectMetadata: FromTo<FlatObjectMetadata>[],
@@ -25,9 +25,9 @@ export const computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix = (
matrixAccumulator.push({
flatObjectMetadata: to,
createdFieldMetadata: fieldMetadataMatrix.created,
deletedFieldMetadata: fieldMetadataMatrix.deleted,
updatedFieldMetadata: fieldMetadataMatrix.updated,
createdFlatFieldMetadata: fieldMetadataMatrix.created,
deletedFlatFieldMetadata: fieldMetadataMatrix.deleted,
updatedFlatFieldMetadata: fieldMetadataMatrix.updated,
});
}
@@ -0,0 +1,19 @@
import { EMPTY_FAILED_AND_SUCCESSFUL_METADATA_BUILD_RECORD } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/constants/empty-failed-and-successful-metadata-build-record.constant';
import { type FailedAndSuccessfulMetadataValidateAndBuildRecord } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/failed-and-successful-metadata-validate-and-build-record.type';
import { type MetadataValidateAndBuildResult } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/metadata-validate-and-build-result.type';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export const fromMetadataBuildResultsToFailedAndSuccessfulMetadataBuildRecord =
<T extends WorkspaceMigrationActionV2>(
metadataBuildResults: MetadataValidateAndBuildResult<T>[],
): FailedAndSuccessfulMetadataValidateAndBuildRecord<T> =>
metadataBuildResults.reduce(
(acc, buildResult) =>
buildResult.status === 'fail'
? {
...acc,
failed: [...acc.failed, buildResult],
}
: { ...acc, successful: [...acc.successful, buildResult] },
EMPTY_FAILED_AND_SUCCESSFUL_METADATA_BUILD_RECORD,
);
@@ -1,11 +1,21 @@
import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
import { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
import { WorkspaceMigrationV2ObjectActionsBuilder } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder';
@Module({
imports: [FeatureFlagModule],
providers: [WorkspaceMigrationBuilderV2Service],
providers: [
WorkspaceMigrationBuilderV2Service,
FlatFieldMetadataValidatorService,
FlatFieldMetadataTypeValidatorService,
WorkspaceMigrationV2ObjectActionsBuilder,
FlatObjectMetadataValidatorService,
],
exports: [WorkspaceMigrationBuilderV2Service],
})
export class WorkspaceMigrationBuilderV2Module {}
@@ -5,28 +5,44 @@ import { type FromTo } from 'twenty-shared/types';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { fromFlatObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-maps-to-flat-object-metadatas.util';
import { deletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
import { type WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
import { FailedMetadataValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/failed-metadata-validate-and-build.type';
import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
import { computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-field-matrix.util';
import { computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-index-matrix.util';
import { getWorkspaceMigrationV2FieldDeleteAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions';
import { getWorkspaceMigrationV2CreateIndexAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-index-actions';
import { buildWorkspaceMigrationV2FieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-field-actions-builder';
import { buildWorkspaceMigrationIndexActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-index-actions-builder';
import { buildWorkspaceMigrationV2ObjectActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder';
import { WorkspaceMigrationV2ObjectActionsBuilder } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder';
export type WorkspaceMigrationV2BuilderOptions = {
inferDeletionFromMissingObjectFieldIndex: boolean;
isSystemBuild: boolean;
};
export type WorkspaceMigrationBuildArgs = {
workspaceId: string;
inferDeletionFromMissingObjectFieldIndex?: boolean;
buildOptions: WorkspaceMigrationV2BuilderOptions;
// Could from be optional ? we still have a race condition when if we recompute cache it could have been different than when we calculated the to
} & FromTo<FlatObjectMetadataMaps, 'FlatObjectMetadataMaps'>;
@Injectable()
export class WorkspaceMigrationBuilderV2Service {
constructor() {}
constructor(
private readonly workspaceMigrationV2ObjectActionsBuilder: WorkspaceMigrationV2ObjectActionsBuilder,
) {}
build({
public async validateAndBuild({
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
workspaceId,
inferDeletionFromMissingObjectFieldIndex = true,
}: WorkspaceMigrationBuildArgs): WorkspaceMigrationV2 {
buildOptions,
}: WorkspaceMigrationBuildArgs): Promise<
| FailedMetadataValidateAndBuild
| {
status: 'success';
workspaceMigration: WorkspaceMigrationV2;
}
> {
const fromFlatObjectMetadatas =
fromFlatObjectMetadataMapsToFlatObjectMetadatas(
fromFlatObjectMetadataMaps,
@@ -34,33 +50,50 @@ export class WorkspaceMigrationBuilderV2Service {
const toFlatObjectMetadatas =
fromFlatObjectMetadataMapsToFlatObjectMetadatas(toFlatObjectMetadataMaps);
// Dispatch
const {
created: createdObjectMetadata,
deleted: deletedObjectMetadata,
updated: updatedObjectMetadata,
created: createdFlatObjectMetadata,
deleted: deletedFlatObjectMetadata,
updated: updatedFlatObjectMetadata,
} = deletedCreatedUpdatedMatrixDispatcher({
from: fromFlatObjectMetadatas,
to: toFlatObjectMetadatas,
});
const objectMetadataDeletedCreatedUpdatedIndex =
computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix(
updatedFlatObjectMetadata,
);
const objectMetadataDeletedCreatedUpdatedFields =
computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix(
updatedFlatObjectMetadata,
);
///
// Validate and build
const objectWorkspaceMigrationActions =
buildWorkspaceMigrationV2ObjectActions({
createdObjectMetadata,
deletedObjectMetadata,
updatedObjectMetadata,
inferDeletionFromMissingObjectFieldIndex,
});
await this.workspaceMigrationV2ObjectActionsBuilder.validateAndBuildObjectActions(
{
fromFlatObjectMetadataMaps,
createdFlatObjectMetadata,
deletedFlatObjectMetadata,
toFlatObjectMetadataMaps,
updatedFlatObjectMetadata,
buildOptions,
},
);
const createdObjectMetadataCreateIndexActions =
createdObjectMetadata.flatMap((objectMetadata) =>
createdFlatObjectMetadata.flatMap((objectMetadata) =>
objectMetadata.flatIndexMetadatas.map(
getWorkspaceMigrationV2CreateIndexAction,
),
);
const deletedObjectWorkspaceMigrationDeleteFieldActions =
inferDeletionFromMissingObjectFieldIndex
? deletedObjectMetadata.flatMap((flatObjectMetadata) =>
buildOptions.inferDeletionFromMissingObjectFieldIndex
? deletedFlatObjectMetadata.flatMap((flatObjectMetadata) =>
flatObjectMetadata.flatFieldMetadatas.map((flatFieldMetadata) =>
getWorkspaceMigrationV2FieldDeleteAction({
flatFieldMetadata,
@@ -70,35 +103,43 @@ export class WorkspaceMigrationBuilderV2Service {
)
: [];
const objectMetadataDeletedCreatedUpdatedFields =
computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix(
updatedObjectMetadata,
);
const fieldWorkspaceMigrationActions =
buildWorkspaceMigrationV2FieldActions({
inferDeletionFromMissingObjectFieldIndex,
inferDeletionFromMissingObjectFieldIndex:
buildOptions.inferDeletionFromMissingObjectFieldIndex,
objectMetadataDeletedCreatedUpdatedFields,
});
const objectMetadataDeletedCreatedUpdatedIndex =
computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix(
updatedObjectMetadata,
);
const indexWorkspaceMigrationActions = buildWorkspaceMigrationIndexActions({
objectMetadataDeletedCreatedUpdatedIndex,
inferDeletionFromMissingObjectFieldIndex,
inferDeletionFromMissingObjectFieldIndex:
buildOptions.inferDeletionFromMissingObjectFieldIndex,
});
///
if (objectWorkspaceMigrationActions.results.failed.length > 0) {
return {
status: 'fail',
errors: objectWorkspaceMigrationActions.results.failed.flatMap(
(el) => el.errors,
),
};
}
return {
workspaceId,
actions: [
...deletedObjectWorkspaceMigrationDeleteFieldActions,
...objectWorkspaceMigrationActions,
...createdObjectMetadataCreateIndexActions,
...fieldWorkspaceMigrationActions,
...indexWorkspaceMigrationActions,
],
status: 'success',
workspaceMigration: {
workspaceId,
actions: [
...deletedObjectWorkspaceMigrationDeleteFieldActions,
...objectWorkspaceMigrationActions.results.successful.map(
(el) => el.result,
), // TMP create util
...createdObjectMetadataCreateIndexActions,
...fieldWorkspaceMigrationActions,
...indexWorkspaceMigrationActions,
],
},
};
}
}
@@ -21,13 +21,13 @@ export const buildWorkspaceMigrationV2FieldActions = ({
[];
for (const {
createdFieldMetadata,
deletedFieldMetadata,
updatedFieldMetadata,
createdFlatFieldMetadata,
deletedFlatFieldMetadata,
updatedFlatFieldMetadata,
flatObjectMetadata,
} of objectMetadataDeletedCreatedUpdatedFields) {
const updateFieldActions = updatedFieldMetadata.flatMap<UpdateFieldAction>(
({ from, to }) => {
const updateFieldActions =
updatedFlatFieldMetadata.flatMap<UpdateFieldAction>(({ from, to }) => {
const updates = compareTwoFlatFieldMetadata({
from,
to,
@@ -44,18 +44,18 @@ export const buildWorkspaceMigrationV2FieldActions = ({
workspaceId: to.workspaceId,
updates,
};
},
);
});
const createFieldAction = createdFieldMetadata.map((flatFieldMetadata) =>
getWorkspaceMigrationV2FieldCreateAction({
flatFieldMetadata,
flatObjectMetadata,
}),
const createFieldAction = createdFlatFieldMetadata.map(
(flatFieldMetadata) =>
getWorkspaceMigrationV2FieldCreateAction({
flatFieldMetadata,
flatObjectMetadata,
}),
);
const deleteFieldAction = inferDeletionFromMissingObjectFieldIndex
? deletedFieldMetadata.map((flatFieldMetadata) =>
? deletedFlatFieldMetadata.map((flatFieldMetadata) =>
getWorkspaceMigrationV2FieldDeleteAction({
flatFieldMetadata,
flatObjectMetadata,
@@ -1,68 +1,202 @@
import { Injectable } from '@nestjs/common';
import { FromTo } from 'twenty-shared/types';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-object-metadata-to-flat-object-metadata-maps-or-throw.util';
import { deleteObjectFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-object-from-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 { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { compareTwoFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util';
import { type CustomDeletedCreatedUpdatedMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
import { MetadataActionValidateAndBuildResult } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/metadata-action-validate-and-build-result.type';
import { MetadataValidateAndBuildResult } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/metadata-validate-and-build-result.type';
import {
type UpdateObjectAction,
type WorkspaceMigrationObjectActionV2,
UpdateObjectAction,
WorkspaceMigrationObjectActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { fromMetadataBuildResultsToFailedAndSuccessfulMetadataBuildRecord } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/from-metadata-build-results-to-failed-and-successful-metadata-build-record.util';
import { getWorkspaceMigrationV2FieldCreateAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions';
import {
getWorkspaceMigrationV2ObjectCreateAction,
getWorkspaceMigrationV2ObjectDeleteAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-object-actions';
import { WorkspaceMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
export type CreatedDeletedUpdatedObjectMetadataInputMatrix =
CustomDeletedCreatedUpdatedMatrix<'objectMetadata', FlatObjectMetadata> & {
inferDeletionFromMissingObjectFieldIndex: boolean;
export type CreatedDeletedUpdatedObjectMetadataInputMatrix = FromTo<
FlatObjectMetadataMaps,
'FlatObjectMetadataMaps'
> &
CustomDeletedCreatedUpdatedMatrix<
'flatObjectMetadata',
FlatObjectMetadata
> & {
buildOptions: WorkspaceMigrationV2BuilderOptions;
fromFlatObjectMetadataMaps: FlatObjectMetadataMaps; // should be renamed to from
};
export const buildWorkspaceMigrationV2ObjectActions = ({
createdObjectMetadata,
deletedObjectMetadata,
updatedObjectMetadata,
inferDeletionFromMissingObjectFieldIndex,
}: CreatedDeletedUpdatedObjectMetadataInputMatrix): WorkspaceMigrationObjectActionV2[] => {
const createdObjectActions = createdObjectMetadata.map(
(flatObjectMetadata) => {
const createFieldActions = flatObjectMetadata.flatFieldMetadatas.map(
(flatFieldMetadata) =>
getWorkspaceMigrationV2FieldCreateAction({
flatFieldMetadata,
flatObjectMetadata,
}),
);
return getWorkspaceMigrationV2ObjectCreateAction({
flatObjectMetadata,
createFieldActions,
});
},
);
@Injectable()
export class WorkspaceMigrationV2ObjectActionsBuilder {
constructor(
private readonly flatObjectMetadataValidatorService: FlatObjectMetadataValidatorService,
) {}
const deletedObjectActions = inferDeletionFromMissingObjectFieldIndex
? deletedObjectMetadata.map(getWorkspaceMigrationV2ObjectDeleteAction)
: [];
public async validateAndBuildObjectActions({
createdFlatObjectMetadata,
deletedFlatObjectMetadata,
updatedFlatObjectMetadata,
buildOptions,
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
}: CreatedDeletedUpdatedObjectMetadataInputMatrix): Promise<
MetadataActionValidateAndBuildResult<WorkspaceMigrationObjectActionV2>
> {
let optimisticFlatObjectMetadataMaps = structuredClone(
fromFlatObjectMetadataMaps,
);
const createdObjectBuildResults = await Promise.all(
createdFlatObjectMetadata.map<
Promise<
MetadataValidateAndBuildResult<WorkspaceMigrationObjectActionV2>
>
>(async (flatObjectMetadata) => {
const validationErrors =
await this.flatObjectMetadataValidatorService.validateFlatObjectMetadataCreation(
{
existingFlatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatObjectMetadataToValidate: flatObjectMetadata,
otherFlatObjectMetadataMapsToValidate: toFlatObjectMetadataMaps,
},
);
const updatedObjectActions =
updatedObjectMetadata.flatMap<UpdateObjectAction>(({ from, to }) => {
if (validationErrors.length > 0) {
return {
status: 'fail',
errors: validationErrors,
};
}
optimisticFlatObjectMetadataMaps =
addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow({
flatObjectMetadata: flatObjectMetadata,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
});
const createFieldActions = flatObjectMetadata.flatFieldMetadatas.map(
(flatFieldMetadata) =>
getWorkspaceMigrationV2FieldCreateAction({
flatFieldMetadata,
flatObjectMetadata,
}),
);
const createObjectAction = getWorkspaceMigrationV2ObjectCreateAction({
flatObjectMetadata,
createFieldActions,
});
return {
status: 'success',
result: createObjectAction,
};
}),
);
const deletedObjectBuildResults =
buildOptions.inferDeletionFromMissingObjectFieldIndex
? deletedFlatObjectMetadata.map<
MetadataValidateAndBuildResult<WorkspaceMigrationObjectActionV2>
>((flatObjectMetadataToDelete) => {
const validationErrors =
this.flatObjectMetadataValidatorService.validateFlatObjectMetadataDeletion(
{
buildOptions,
existingFlatObjectMetadataMaps:
optimisticFlatObjectMetadataMaps,
objectMetadataToDeleteId: flatObjectMetadataToDelete.id,
},
);
if (validationErrors.length > 0) {
return {
status: 'fail',
errors: validationErrors,
};
}
optimisticFlatObjectMetadataMaps =
deleteObjectFromFlatObjectMetadataMapsOrThrow({
objectMetadataId: flatObjectMetadataToDelete.id,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
});
const deleteObjectAction =
getWorkspaceMigrationV2ObjectDeleteAction(
flatObjectMetadataToDelete,
);
return {
status: 'success',
result: deleteObjectAction,
};
})
: [];
const updatedObjectBuildResults = updatedFlatObjectMetadata.flatMap<
MetadataValidateAndBuildResult<WorkspaceMigrationObjectActionV2>
>(({ from: fromFlatObjectMetadata, to: toFlatObjectMetadata }) => {
const objectUpdatedProperties = compareTwoFlatObjectMetadata({
from,
to,
from: fromFlatObjectMetadata,
to: toFlatObjectMetadata,
});
if (objectUpdatedProperties.length === 0) {
return [];
}
return {
const validationErrors =
this.flatObjectMetadataValidatorService.validateFlatObjectMetadataUpdate(
{
existingFlatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
updatedFlatObjectMetadata: toFlatObjectMetadata,
},
);
if (validationErrors.length > 0) {
return {
status: 'fail',
errors: validationErrors,
};
}
optimisticFlatObjectMetadataMaps =
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadata: toFlatObjectMetadata,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
});
const updateObjectAction: UpdateObjectAction = {
type: 'update_object',
objectMetadataId: to.id,
objectMetadataId: toFlatObjectMetadata.id,
updates: objectUpdatedProperties,
};
return {
status: 'success',
result: updateObjectAction,
};
});
return [
...createdObjectActions,
...deletedObjectActions,
...updatedObjectActions,
];
};
return {
optimisticFlatObjectMetadataMaps,
results: fromMetadataBuildResultsToFailedAndSuccessfulMetadataBuildRecord(
[
...createdObjectBuildResults,
...deletedObjectBuildResults,
...updatedObjectBuildResults,
],
),
};
}
}
@@ -46,6 +46,8 @@ import { WorkspaceSchemaObjectActionRunnerService } from 'src/engine/workspace-m
WorkspaceSchemaObjectActionRunnerService,
WorkspaceSchemaIndexActionRunnerService,
WorkspaceSchemaFieldActionRunnerService,
WorkspaceMetadataMigrationRunnerService,
WorkspaceSchemaMigrationRunnerService,
],
})
export class WorkspaceMigrationRunnerV2Module {}
@@ -2,13 +2,18 @@ import { Module } from '@nestjs/common';
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: [
WorkspaceMigrationBuilderV2Module,
WorkspaceMigrationRunnerV2Module,
],
providers: [],
exports: [WorkspaceMigrationRunnerV2Module],
providers: [WorkspaceMigrationValidateBuildAndRunService],
exports: [
WorkspaceMigrationRunnerV2Module,
WorkspaceMigrationBuilderV2Module,
WorkspaceMigrationValidateBuildAndRunService,
],
})
export class WorkspaceMigrationV2Module {}
@@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { MultipleMetadataValidationErrors } from 'src/engine/core-modules/error/multiple-metadata-validation-errors';
import {
WorkspaceMigrationBuildArgs,
WorkspaceMigrationBuilderV2Service,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/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 {
WorkspaceMigrationV2Exception,
WorkspaceMigrationV2ExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration.exception';
@Injectable()
export class WorkspaceMigrationValidateBuildAndRunService {
constructor(
private readonly workspaceMigrationBuilderV2Service: WorkspaceMigrationBuilderV2Service,
private readonly workspaceMigrationRunnerV2Service: WorkspaceMigrationRunnerV2Service,
) {}
public async validateBuildAndRunWorkspaceMigration({
errorMessage,
...builderArgs
}: WorkspaceMigrationBuildArgs & { errorMessage: string }) {
const validateAndBuildResult = await this.workspaceMigrationBuilderV2Service
.validateAndBuild(builderArgs)
.catch((error) => {
throw new WorkspaceMigrationV2Exception(
WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR,
error.message,
);
});
if (validateAndBuildResult.status === 'fail') {
throw new MultipleMetadataValidationErrors(
validateAndBuildResult.errors,
errorMessage,
);
}
await this.workspaceMigrationRunnerV2Service
.run(validateAndBuildResult.workspaceMigration)
.catch((error) => {
throw new WorkspaceMigrationV2Exception(
WorkspaceMigrationV2ExceptionCode.RUNNER_INTERNAL_SERVER_ERROR,
error.message,
);
});
}
}
@@ -0,0 +1,8 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceMigrationV2Exception extends CustomException<WorkspaceMigrationV2ExceptionCode> {}
export enum WorkspaceMigrationV2ExceptionCode {
BUILDER_INTERNAL_SERVER_ERROR = 'BUILDER_INTERNAL_SERVER_ERROR',
RUNNER_INTERNAL_SERVER_ERROR = 'RUNNER_INTERNAL_SERVER_ERROR',
}