Refactor workspace migration runner exception handling (#17310)
# Introduction
In this PR we catch all the runner errors coming from a single workspace
migration action execution.
**1. `WorkspaceMigrationActionExecutionException`** (low-level,
action-specific)
- Thrown from action handlers, utils, and helper functions
- Contains specific error codes: `FIELD_METADATA_NOT_FOUND`,
`OBJECT_METADATA_NOT_FOUND`, `ENUM_OPERATION_FAILED`, `NOT_SUPPORTED`,
etc.
- Simple structure: `message`, `code`, `userFriendlyMessage`
- No action context - just describes what went wrong
**2. `WorkspaceMigrationRunnerException`** (high-level, runner-scoped)
- Only two codes: `INTERNAL_SERVER_ERROR` and `EXECUTION_FAILED`
- `EXECUTION_FAILED` **requires** `action` + `errors` (contains the
action context)
- `INTERNAL_SERVER_ERROR` **requires** `message` (no action context)
## Refactor
- Removed the `relatedFlatEntityMapsKeys` from the `WorkspaceMigration`
type as they're directly inferred from passed actions
- Swallowing actions rollbacks errors in order to iterate over all of
them
## Testing
Created a very straigthforward install application from workspace
migration endpoint in order to start testing the introduced
`WorkspaceMigrationActionExecutionException`
Introduced a feature flag that stop the access to the endpoint if not
enabled
Whole taken direction are totally subjective and highly prone to
mutations ( endpoint location, naming and input schema see
`ts-expect-error` comment )
cc @martmull
## Response error
```ts
{
"eventId": "evt_a1b2c3d4-5678-90ab-cdef-1234567890ab",
"extensions": {
"action": {
"metadataName": "fieldMetadata",
"type": "delete",
"universalIdentifier": "20202020-6110-4547-9fd0-2525257a2c3f"
},
"code": "APPLICATION_INSTALLATION_FAILED",
"errors": {
"metadata": {
"code": "ENTITY_NOT_FOUND",
"message": "Could not find flat entity with universal identifier 20202020-6110-4547-9fd0-2525257a2c3f"
},
"workspaceSchema": {
"code": "ENTITY_NOT_FOUND",
"message": "Could not find flat entity in maps"
}
},
"exceptionEventId": "exc_f9e8d7c6-5432-10ba-fedc-ba0987654321",
"userFriendlyMessage": "Migration execution failed."
},
"message": "Migration action 'delete' for 'fieldMetadata' failed",
"name": "GraphQLError"
}
```
This commit is contained in:
+7
-1
@@ -12,9 +12,11 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { workspaceMigrationBuilderExceptionFormatter } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter';
|
||||
import { workspaceMigrationRunnerExceptionFormatter } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-runner-exception-formatter';
|
||||
import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor
|
||||
export class WorkspaceMigrationGraphqlApiExceptionInterceptor
|
||||
implements NestInterceptor
|
||||
{
|
||||
constructor(private readonly i18nService: I18nService) {}
|
||||
@@ -31,6 +33,10 @@ export class WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor
|
||||
workspaceMigrationBuilderExceptionFormatter(error, i18n);
|
||||
}
|
||||
|
||||
if (error instanceof WorkspaceMigrationRunnerException) {
|
||||
workspaceMigrationRunnerExceptionFormatter(error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}),
|
||||
);
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
ErrorCode,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
type WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
|
||||
export const workspaceMigrationRunnerExceptionFormatter = (
|
||||
error: WorkspaceMigrationRunnerException,
|
||||
) => {
|
||||
const isExecutionFailed =
|
||||
error.code === WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED;
|
||||
|
||||
throw new BaseGraphQLError(
|
||||
error.message,
|
||||
isExecutionFailed
|
||||
? ErrorCode.APPLICATION_INSTALLATION_FAILED
|
||||
: ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
{
|
||||
code: error.code,
|
||||
...(isExecutionFailed && {
|
||||
action: error.action,
|
||||
errors: {
|
||||
...(error.errors?.metadata && {
|
||||
metadata: {
|
||||
message: error.errors.metadata.message,
|
||||
code:
|
||||
(error.errors.metadata as { code?: string })?.code ??
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
},
|
||||
}),
|
||||
...(error.errors?.workspaceSchema && {
|
||||
workspaceSchema: {
|
||||
message: error.errors.workspaceSchema.message,
|
||||
code:
|
||||
(error.errors.workspaceSchema as { code?: string })?.code ??
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
},
|
||||
);
|
||||
};
|
||||
-8
@@ -1058,13 +1058,6 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
};
|
||||
}
|
||||
|
||||
const relatedFlatEntityMapsKeys = [
|
||||
...new Set([
|
||||
...Object.keys(fromToAllFlatEntityMaps),
|
||||
...Object.keys(dependencyAllFlatEntityMaps ?? {}),
|
||||
]),
|
||||
] as (keyof AllFlatEntityMaps)[];
|
||||
|
||||
const { aggregatedOrchestratorActionsReport } =
|
||||
aggregateOrchestratorActionsReport({
|
||||
orchestratorActionsReport,
|
||||
@@ -1075,7 +1068,6 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
return {
|
||||
status: 'success',
|
||||
workspaceMigration: {
|
||||
relatedFlatEntityMapsKeys,
|
||||
actions: [
|
||||
// Object and fields and indexes
|
||||
...aggregatedOrchestratorActionsReport.index.delete,
|
||||
|
||||
+3
-9
@@ -196,15 +196,9 @@ export class WorkspaceMigrationValidateBuildAndRunService {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workspaceMigrationRunnerService
|
||||
.run(validateAndBuildResult.workspaceMigration)
|
||||
.catch((error) => {
|
||||
this.logger.error(error);
|
||||
throw new WorkspaceMigrationV2Exception(
|
||||
WorkspaceMigrationV2ExceptionCode.RUNNER_INTERNAL_SERVER_ERROR,
|
||||
error.message,
|
||||
);
|
||||
});
|
||||
await this.workspaceMigrationRunnerService.run(
|
||||
validateAndBuildResult.workspaceMigration,
|
||||
);
|
||||
}
|
||||
|
||||
public async validateBuildAndRunWorkspaceMigration({
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export const WORKSPACE_MIGRATION_ACTION_TYPE = {
|
||||
delete: 'delete',
|
||||
create: 'create',
|
||||
update: 'update',
|
||||
} as const;
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { type WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant';
|
||||
|
||||
export type BaseCreateWorkspaceMigrationAction<T extends AllMetadataName> = {
|
||||
flatEntity: MetadataFlatEntity<T>;
|
||||
type: 'create';
|
||||
type: typeof WORKSPACE_MIGRATION_ACTION_TYPE.create;
|
||||
metadataName: T;
|
||||
};
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant';
|
||||
|
||||
export type BaseDeleteWorkspaceMigrationAction<T extends AllMetadataName> = {
|
||||
universalIdentifier: string;
|
||||
type: 'delete';
|
||||
type: typeof WORKSPACE_MIGRATION_ACTION_TYPE.delete;
|
||||
metadataName: T;
|
||||
};
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
|
||||
import { type WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant';
|
||||
|
||||
export type BaseUpdateWorkspaceMigrationAction<T extends AllMetadataName> = {
|
||||
type: 'update';
|
||||
type: typeof WORKSPACE_MIGRATION_ACTION_TYPE.update;
|
||||
metadataName: T;
|
||||
entityId: string;
|
||||
updates: FlatEntityPropertiesUpdates<T>;
|
||||
|
||||
-3
@@ -1,4 +1,3 @@
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
|
||||
export type WorkspaceMigration<
|
||||
@@ -6,6 +5,4 @@ export type WorkspaceMigration<
|
||||
> = {
|
||||
actions: TActions[];
|
||||
workspaceId: string;
|
||||
// TODO remove from workspaceMigration once we've refactored the actions to have metadata and action type grain
|
||||
relatedFlatEntityMapsKeys?: (keyof AllFlatEntityMaps)[];
|
||||
};
|
||||
|
||||
+17
-15
@@ -27,9 +27,9 @@ import { isPropertyUpdate } from 'src/engine/workspace-manager/workspace-migrati
|
||||
import { UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
|
||||
import { serializeDefaultValue } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
WorkspaceMigrationActionExecutionException,
|
||||
WorkspaceMigrationActionExecutionExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception';
|
||||
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/field-metadata-type-to-column-type.util';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
@@ -243,10 +243,10 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
);
|
||||
|
||||
if (!isDefined(foreignKeyName)) {
|
||||
throw new WorkspaceMigrationRunnerException(
|
||||
'Foreign key not found',
|
||||
WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED,
|
||||
);
|
||||
throw new WorkspaceMigrationActionExecutionException({
|
||||
message: 'Foreign key not found',
|
||||
code: WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED,
|
||||
});
|
||||
}
|
||||
|
||||
await this.workspaceSchemaManagerService.foreignKeyManager.dropForeignKey(
|
||||
@@ -300,10 +300,11 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
|
||||
for (const property of compositeType.properties) {
|
||||
if (isMorphOrRelationFieldMetadataType(property.type)) {
|
||||
throw new WorkspaceMigrationRunnerException(
|
||||
'Relation field metadata in composite type is not supported yet',
|
||||
WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED,
|
||||
);
|
||||
throw new WorkspaceMigrationActionExecutionException({
|
||||
message:
|
||||
'Relation field metadata in composite type is not supported yet',
|
||||
code: WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED,
|
||||
});
|
||||
}
|
||||
|
||||
const fromCompositeColumnName = computeCompositeColumnName(
|
||||
@@ -366,10 +367,11 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
) as ColumnType;
|
||||
|
||||
if (isMorphOrRelationFieldMetadataType(property.type)) {
|
||||
throw new WorkspaceMigrationRunnerException(
|
||||
'Relation field metadata in composite type is not supported yet',
|
||||
WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED,
|
||||
);
|
||||
throw new WorkspaceMigrationActionExecutionException({
|
||||
message:
|
||||
'Relation field metadata in composite type is not supported yet',
|
||||
code: WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED,
|
||||
});
|
||||
}
|
||||
|
||||
const compositeColumnName = computeCompositeColumnName(
|
||||
|
||||
+8
-8
@@ -2,10 +2,6 @@ import { compositeTypeDefinitions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import {
|
||||
WorkspaceQueryRunnerException,
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import {
|
||||
@@ -24,6 +20,10 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
|
||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
|
||||
import {
|
||||
WorkspaceMigrationActionExecutionException,
|
||||
WorkspaceMigrationActionExecutionExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception';
|
||||
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/get-workspace-schema-context-for-migration.util';
|
||||
|
||||
export const computeFlatIndexFieldColumnNames = ({
|
||||
@@ -104,10 +104,10 @@ export const insertIndexMetadata = async ({
|
||||
);
|
||||
|
||||
if (indexInsertResult.identifiers.length !== 1) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
'Failed to create index metadata',
|
||||
WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
throw new WorkspaceMigrationActionExecutionException({
|
||||
message: 'Failed to create index metadata',
|
||||
code: WorkspaceMigrationActionExecutionExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
const indexMetadataId = indexInsertResult.identifiers[0].id;
|
||||
|
||||
|
||||
+4
-1
@@ -67,7 +67,10 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra
|
||||
}
|
||||
|
||||
async rollbackForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<DeleteServerlessFunctionAction>,
|
||||
context: Omit<
|
||||
WorkspaceMigrationActionRunnerArgs<DeleteServerlessFunctionAction>,
|
||||
'queryRunner'
|
||||
>,
|
||||
): Promise<void> {
|
||||
const { action, allFlatEntityMaps } = context;
|
||||
const { universalIdentifier } = action;
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable, CustomError } from 'twenty-shared/utils';
|
||||
|
||||
export const WorkspaceMigrationActionExecutionExceptionCode = {
|
||||
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',
|
||||
FLAT_ENTITY_NOT_FOUND: 'FLAT_ENTITY_NOT_FOUND',
|
||||
UNSUPPORTED_FIELD_METADATA_TYPE: 'UNSUPPORTED_FIELD_METADATA_TYPE',
|
||||
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
|
||||
} as const;
|
||||
|
||||
const getWorkspaceMigrationActionExecutionExceptionUserFriendlyMessage = (
|
||||
code: keyof typeof WorkspaceMigrationActionExecutionExceptionCode,
|
||||
) => {
|
||||
switch (code) {
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.FIELD_METADATA_NOT_FOUND:
|
||||
return msg`Field metadata not found.`;
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
return msg`Object metadata not found.`;
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.ENUM_OPERATION_FAILED:
|
||||
return msg`Enum operation failed.`;
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE:
|
||||
return msg`Unsupported composite column type.`;
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED:
|
||||
return msg`This operation is not supported.`;
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.INVALID_ACTION_TYPE:
|
||||
return msg`Invalid action type.`;
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.FLAT_ENTITY_NOT_FOUND:
|
||||
return msg`Entity not found.`;
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.UNSUPPORTED_FIELD_METADATA_TYPE:
|
||||
return msg`Unsupported field metadata type.`;
|
||||
case WorkspaceMigrationActionExecutionExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
return msg`An unexpected error occurred.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class WorkspaceMigrationActionExecutionException extends CustomError {
|
||||
code: keyof typeof WorkspaceMigrationActionExecutionExceptionCode;
|
||||
userFriendlyMessage: MessageDescriptor;
|
||||
|
||||
constructor({
|
||||
message,
|
||||
code,
|
||||
userFriendlyMessage,
|
||||
}: {
|
||||
message: string;
|
||||
code: keyof typeof WorkspaceMigrationActionExecutionExceptionCode;
|
||||
userFriendlyMessage?: MessageDescriptor;
|
||||
}) {
|
||||
super(message);
|
||||
|
||||
this.code = code;
|
||||
this.userFriendlyMessage =
|
||||
userFriendlyMessage ??
|
||||
getWorkspaceMigrationActionExecutionExceptionUserFriendlyMessage(code);
|
||||
}
|
||||
}
|
||||
+50
-44
@@ -1,62 +1,68 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { assertUnreachable, CustomError } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
appendCommonExceptionCode,
|
||||
CustomException,
|
||||
} from 'src/utils/custom-exception';
|
||||
import { type WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
|
||||
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',
|
||||
FLAT_ENTITY_NOT_FOUND: 'FLAT_ENTITY_NOT_FOUND',
|
||||
UNSUPPORTED_FIELD_METADATA_TYPE: 'UNSUPPORTED_FIELD_METADATA_TYPE',
|
||||
} as const);
|
||||
export const WorkspaceMigrationRunnerExceptionCode = {
|
||||
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
|
||||
EXECUTION_FAILED: 'EXECUTION_FAILED',
|
||||
} as const;
|
||||
|
||||
const getWorkspaceMigrationRunnerExceptionUserFriendlyMessage = (
|
||||
code: keyof typeof WorkspaceMigrationRunnerExceptionCode,
|
||||
) => {
|
||||
switch (code) {
|
||||
case WorkspaceMigrationRunnerExceptionCode.FIELD_METADATA_NOT_FOUND:
|
||||
return msg`Field metadata not found.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
return msg`Object metadata not found.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.ENUM_OPERATION_FAILED:
|
||||
return msg`Enum operation failed.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE:
|
||||
return msg`Unsupported composite column type.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED:
|
||||
return msg`This operation is not supported.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.INVALID_ACTION_TYPE:
|
||||
return msg`Invalid action type.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.FLAT_ENTITY_NOT_FOUND:
|
||||
return msg`Entity not found.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
return msg`An unexpected error occurred.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_FIELD_METADATA_TYPE:
|
||||
return msg`Unsupported field metadata type.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED:
|
||||
return msg`Migration execution failed.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class WorkspaceMigrationRunnerException extends CustomException<
|
||||
keyof typeof WorkspaceMigrationRunnerExceptionCode
|
||||
> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: keyof typeof WorkspaceMigrationRunnerExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
getWorkspaceMigrationRunnerExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
export type WorkspaceMigrationRunnerExecutionErrors = {
|
||||
metadata?: Error;
|
||||
workspaceSchema?: Error;
|
||||
};
|
||||
|
||||
type WorkspaceMigrationRunnerExceptionConstructorArgs =
|
||||
| {
|
||||
message: string;
|
||||
code: typeof WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR;
|
||||
userFriendlyMessage?: MessageDescriptor;
|
||||
}
|
||||
| {
|
||||
action: WorkspaceMigrationAction;
|
||||
errors: WorkspaceMigrationRunnerExecutionErrors;
|
||||
code: typeof WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED;
|
||||
userFriendlyMessage?: MessageDescriptor;
|
||||
};
|
||||
|
||||
export class WorkspaceMigrationRunnerException extends CustomError {
|
||||
code: keyof typeof WorkspaceMigrationRunnerExceptionCode;
|
||||
userFriendlyMessage: MessageDescriptor;
|
||||
action?: WorkspaceMigrationAction;
|
||||
errors?: WorkspaceMigrationRunnerExecutionErrors;
|
||||
|
||||
constructor(args: WorkspaceMigrationRunnerExceptionConstructorArgs) {
|
||||
if (args.code === WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED) {
|
||||
super(
|
||||
`Migration action '${args.action.type}' for '${args.action.metadataName}' failed`,
|
||||
);
|
||||
|
||||
this.code = args.code;
|
||||
this.action = args.action;
|
||||
this.errors = args.errors;
|
||||
} else {
|
||||
super(args.message);
|
||||
|
||||
this.code = args.code;
|
||||
}
|
||||
|
||||
this.userFriendlyMessage =
|
||||
args.userFriendlyMessage ??
|
||||
getWorkspaceMigrationRunnerExceptionUserFriendlyMessage(args.code);
|
||||
}
|
||||
}
|
||||
|
||||
+44
-20
@@ -13,6 +13,10 @@ import {
|
||||
type WorkspaceMigrationAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
import { WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/workspace-migration-action-handler-metadata-key.constant';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
import { optimisticallyApplyCreateActionOnAllFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util';
|
||||
import { optimisticallyApplyDeleteActionOnAllFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util';
|
||||
@@ -24,7 +28,6 @@ type OptimisticallyApplyActionOnAllFlatEntityMapsArgs<
|
||||
WorkspaceMigrationActionRunnerArgs<TActionType>,
|
||||
'allFlatEntityMaps' | 'action'
|
||||
>;
|
||||
|
||||
export abstract class BaseWorkspaceMigrationRunnerActionHandlerService<
|
||||
TActionType extends WorkspaceMigrationActionType,
|
||||
TMetadataName extends AllMetadataName,
|
||||
@@ -80,7 +83,7 @@ export abstract class BaseWorkspaceMigrationRunnerActionHandlerService<
|
||||
}
|
||||
|
||||
rollbackForMetadata(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<TAction>,
|
||||
_context: Omit<WorkspaceMigrationActionRunnerArgs<TAction>, 'queryRunner'>,
|
||||
): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -94,32 +97,53 @@ export abstract class BaseWorkspaceMigrationRunnerActionHandlerService<
|
||||
| MetadataToFlatEntityMapsKey<TMetadataName>
|
||||
>
|
||||
> {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.asyncMethodPerformanceMetricWrapper({
|
||||
label: 'executeForMetadata',
|
||||
method: async () => this.executeForMetadata(context),
|
||||
}),
|
||||
this.asyncMethodPerformanceMetricWrapper({
|
||||
label: 'executeForWorkspaceSchema',
|
||||
method: async () => this.executeForWorkspaceSchema(context),
|
||||
}),
|
||||
]);
|
||||
const [metadataResult, workspaceSchemaResult] = await Promise.allSettled([
|
||||
this.asyncMethodPerformanceMetricWrapper({
|
||||
label: 'executeForMetadata',
|
||||
method: async () => this.executeForMetadata(context),
|
||||
}),
|
||||
this.asyncMethodPerformanceMetricWrapper({
|
||||
label: 'executeForWorkspaceSchema',
|
||||
method: async () => this.executeForWorkspaceSchema(context),
|
||||
}),
|
||||
]);
|
||||
|
||||
return this.optimisticallyApplyActionOnAllFlatEntityMaps({
|
||||
const hasMetadataError = metadataResult.status === 'rejected';
|
||||
const hasWorkspaceSchemaError = workspaceSchemaResult.status === 'rejected';
|
||||
|
||||
if (hasMetadataError || hasWorkspaceSchemaError) {
|
||||
throw new WorkspaceMigrationRunnerException({
|
||||
action: context.action,
|
||||
errors: {
|
||||
...(hasMetadataError && { metadata: metadataResult.reason }),
|
||||
...(hasWorkspaceSchemaError && {
|
||||
workspaceSchema: workspaceSchemaResult.reason,
|
||||
}),
|
||||
},
|
||||
code: WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED,
|
||||
});
|
||||
}
|
||||
|
||||
const partialOptimisticCache =
|
||||
this.optimisticallyApplyActionOnAllFlatEntityMaps({
|
||||
action: context.action,
|
||||
allFlatEntityMaps: context.allFlatEntityMaps,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`${this.actionType} execution failed`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return partialOptimisticCache;
|
||||
}
|
||||
|
||||
async rollback(
|
||||
context: WorkspaceMigrationActionRunnerArgs<TAction>,
|
||||
context: Omit<WorkspaceMigrationActionRunnerArgs<TAction>, 'queryRunner'>,
|
||||
): Promise<void> {
|
||||
await this.rollbackForMetadata(context);
|
||||
try {
|
||||
await this.rollbackForMetadata(context);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to rollback ${context.action.type} action for ${context.action.metadataName}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
'BaseWorkspaceMigrationRunnerActionHandlerService',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async asyncMethodPerformanceMetricWrapper({
|
||||
|
||||
+31
-24
@@ -1,22 +1,19 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { DiscoveryService } from '@nestjs/core';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { BaseWorkspaceMigrationRunnerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import {
|
||||
buildActionHandlerKey,
|
||||
type WorkspaceMigrationActionHandlerKey,
|
||||
type WorkspaceMigrationAction,
|
||||
type WorkspaceMigrationActionHandlerKey,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
import { WorkspaceSchemaMigrationRunnerActionHandlersModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module';
|
||||
import { WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/workspace-migration-action-handler-metadata-key.constant';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
WorkspaceMigrationActionExecutionException,
|
||||
WorkspaceMigrationActionExecutionExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
@@ -56,15 +53,7 @@ export class WorkspaceMigrationRunnerActionHandlerRegistryService
|
||||
});
|
||||
}
|
||||
|
||||
async executeActionHandler<T extends WorkspaceMigrationAction>({
|
||||
action,
|
||||
context,
|
||||
rollback,
|
||||
}: {
|
||||
action: T;
|
||||
context: WorkspaceMigrationActionRunnerArgs<T>;
|
||||
rollback?: boolean;
|
||||
}): Promise<Partial<AllFlatEntityMaps>> {
|
||||
private getActionHandler<T extends WorkspaceMigrationAction>(action: T) {
|
||||
const actionHandlerKey = buildActionHandlerKey(
|
||||
action.type,
|
||||
action.metadataName,
|
||||
@@ -72,18 +61,36 @@ export class WorkspaceMigrationRunnerActionHandlerRegistryService
|
||||
const handler = this.actionHandlers.get(actionHandlerKey);
|
||||
|
||||
if (!handler) {
|
||||
throw new WorkspaceMigrationRunnerException(
|
||||
`No migration runner action handler found for action: ${actionHandlerKey}`,
|
||||
WorkspaceMigrationRunnerExceptionCode.INVALID_ACTION_TYPE,
|
||||
);
|
||||
throw new WorkspaceMigrationActionExecutionException({
|
||||
message: `No migration runner action handler found for action: ${actionHandlerKey}`,
|
||||
code: WorkspaceMigrationActionExecutionExceptionCode.INVALID_ACTION_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(rollback) && rollback) {
|
||||
await handler.rollback(context);
|
||||
return handler;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
async executeActionHandler<T extends WorkspaceMigrationAction>({
|
||||
action,
|
||||
context,
|
||||
}: {
|
||||
action: T;
|
||||
context: WorkspaceMigrationActionRunnerArgs<T>;
|
||||
}) {
|
||||
const handler = this.getActionHandler(action);
|
||||
|
||||
return await handler.execute(context);
|
||||
}
|
||||
|
||||
async executeActionRollbackHandler<T extends WorkspaceMigrationAction>({
|
||||
action,
|
||||
context,
|
||||
}: {
|
||||
action: T;
|
||||
context: Omit<WorkspaceMigrationActionRunnerArgs<T>, 'queryRunner'>;
|
||||
}) {
|
||||
const handler = this.getActionHandler(action);
|
||||
|
||||
await handler.rollback(context);
|
||||
}
|
||||
}
|
||||
|
||||
+90
-84
@@ -1,23 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import { AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import {
|
||||
WorkspaceQueryRunnerException,
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataNameFromFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-name-from-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modules/view/constants/find-all-core-views-graphql-operation.constant';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration';
|
||||
import { WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
import { WorkspaceMigrationRunnerActionHandlerRegistryService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/registry/workspace-migration-runner-action-handler-registry.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -104,35 +105,93 @@ export class WorkspaceMigrationRunnerService {
|
||||
return asyncOperations;
|
||||
}
|
||||
|
||||
private async invalidateCachePostExecution({
|
||||
allFlatEntityMapsKeys,
|
||||
workspaceId,
|
||||
actions,
|
||||
}: {
|
||||
allFlatEntityMapsKeys: (keyof AllFlatEntityMaps)[];
|
||||
workspaceId: string;
|
||||
actions: WorkspaceMigrationAction[];
|
||||
}): Promise<void> {
|
||||
this.logger.time(
|
||||
'Runner',
|
||||
`Cache invalidation ${allFlatEntityMapsKeys.join()}`,
|
||||
);
|
||||
|
||||
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: allFlatEntityMapsKeys,
|
||||
});
|
||||
|
||||
const invalidationResults = await Promise.allSettled(
|
||||
this.getLegacyCacheInvalidationPromises({
|
||||
workspaceMigration: {
|
||||
actions,
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const invalidationFailures = invalidationResults.filter(
|
||||
(result) => result.status === 'rejected',
|
||||
);
|
||||
|
||||
if (invalidationFailures.length > 0) {
|
||||
invalidationFailures.forEach((err) =>
|
||||
this.logger.error(
|
||||
`Failed to invalidate a legacy cache ${err.reason}`,
|
||||
'Runner',
|
||||
),
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to invalidate ${invalidationFailures.length} cache operations`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.timeEnd(
|
||||
'Runner',
|
||||
`Cache invalidation ${allFlatEntityMapsKeys.join()}`,
|
||||
);
|
||||
}
|
||||
|
||||
run = async ({
|
||||
actions,
|
||||
workspaceId,
|
||||
relatedFlatEntityMapsKeys,
|
||||
}: WorkspaceMigration): Promise<AllFlatEntityMaps> => {
|
||||
this.logger.time('Runner', 'Total execution');
|
||||
this.logger.time('Runner', 'Initial cache retrieval');
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
const actionMetadataNames = [
|
||||
...new Set(actions.flatMap((action) => action.metadataName)),
|
||||
];
|
||||
const actionsMetadataAndRelatedMetadataNames: AllMetadataName[] = [
|
||||
...new Set([
|
||||
...actionMetadataNames,
|
||||
...actionMetadataNames.flatMap(getMetadataRelatedMetadataNames),
|
||||
]),
|
||||
];
|
||||
const allFlatEntityMapsKeys = actionsMetadataAndRelatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
let allFlatEntityMaps =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: relatedFlatEntityMapsKeys,
|
||||
flatMapsKeys: allFlatEntityMapsKeys,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.timeEnd('Runner', 'Initial cache retrieval');
|
||||
this.logger.time('Runner', 'Transaction execution');
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
let flatEntityMapsToInvalidate: (keyof AllFlatEntityMaps)[] = [];
|
||||
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
for (const action of actions) {
|
||||
const partialOptimisticCache =
|
||||
const result =
|
||||
await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionHandler(
|
||||
{
|
||||
action,
|
||||
@@ -144,20 +203,10 @@ export class WorkspaceMigrationRunnerService {
|
||||
},
|
||||
},
|
||||
);
|
||||
const optimisticallyUpdatedFlatEntityMapsKeys = Object.keys(
|
||||
partialOptimisticCache,
|
||||
) as (keyof AllFlatEntityMaps)[];
|
||||
|
||||
flatEntityMapsToInvalidate = [
|
||||
...new Set([
|
||||
...optimisticallyUpdatedFlatEntityMapsKeys,
|
||||
...flatEntityMapsToInvalidate,
|
||||
]),
|
||||
];
|
||||
|
||||
allFlatEntityMaps = {
|
||||
...allFlatEntityMaps,
|
||||
...partialOptimisticCache,
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,89 +214,46 @@ export class WorkspaceMigrationRunnerService {
|
||||
|
||||
this.logger.timeEnd('Runner', 'Transaction execution');
|
||||
|
||||
const flatEntitiesCacheToInvalidate = [
|
||||
...new Set([
|
||||
...flatEntityMapsToInvalidate,
|
||||
...flatEntityMapsToInvalidate
|
||||
.map(getMetadataNameFromFlatEntityMapsKey)
|
||||
.flatMap(getMetadataRelatedMetadataNames)
|
||||
.map(getMetadataFlatEntityMapsKey),
|
||||
]),
|
||||
];
|
||||
|
||||
this.logger.time(
|
||||
'Runner',
|
||||
`Cache invalidation ${flatEntitiesCacheToInvalidate.join()}`,
|
||||
);
|
||||
|
||||
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
||||
await this.invalidateCachePostExecution({
|
||||
allFlatEntityMapsKeys,
|
||||
workspaceId,
|
||||
flatMapsKeys: flatEntitiesCacheToInvalidate,
|
||||
actions,
|
||||
});
|
||||
|
||||
const invalidationResults = await Promise.allSettled(
|
||||
this.getLegacyCacheInvalidationPromises({
|
||||
workspaceMigration: {
|
||||
actions,
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const invalidationFailures = invalidationResults.filter(
|
||||
(result) => result.status === 'rejected',
|
||||
);
|
||||
|
||||
if (invalidationFailures.length > 0) {
|
||||
invalidationFailures.forEach((err) =>
|
||||
this.logger.error(
|
||||
`Failed to invalidate a legacy cache ${err.reason}`,
|
||||
'Runner',
|
||||
),
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to invalidate ${invalidationFailures.length} cache operations`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.timeEnd(
|
||||
'Runner',
|
||||
`Cache invalidation ${flatEntitiesCacheToInvalidate.join()}`,
|
||||
);
|
||||
this.logger.timeEnd('Runner', 'Total execution');
|
||||
|
||||
return allFlatEntityMaps;
|
||||
} catch (error) {
|
||||
if (queryRunner.isTransactionActive) {
|
||||
try {
|
||||
await queryRunner.rollbackTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction().catch((error) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.trace(`Failed to rollback transaction: ${error.message}`);
|
||||
}
|
||||
console.trace(`Failed to rollback transaction: ${error.message}`),
|
||||
);
|
||||
}
|
||||
|
||||
const invertedActions = actions.reverse();
|
||||
const invertedActions = [...actions].reverse();
|
||||
|
||||
for (const invertedAction of invertedActions) {
|
||||
await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionHandler(
|
||||
await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionRollbackHandler(
|
||||
{
|
||||
action: invertedAction,
|
||||
context: {
|
||||
action: invertedAction,
|
||||
allFlatEntityMaps: allFlatEntityMaps,
|
||||
queryRunner,
|
||||
allFlatEntityMaps,
|
||||
workspaceId,
|
||||
},
|
||||
rollback: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
error.message,
|
||||
WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
if (error instanceof WorkspaceMigrationRunnerException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new WorkspaceMigrationRunnerException({
|
||||
message: error.message,
|
||||
code: WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ 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 { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
import { WorkspaceMigrationActionExecutionException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception';
|
||||
import {
|
||||
collectEnumOperationsForField,
|
||||
EnumOperation,
|
||||
@@ -61,7 +61,7 @@ describe('WorkspaceSchemaEnumOperations', () => {
|
||||
schemaName: 'test_schema',
|
||||
workspaceSchemaManagerService: mockSchemaManagerService,
|
||||
}),
|
||||
).rejects.toThrow(WorkspaceMigrationRunnerException);
|
||||
).rejects.toThrow(WorkspaceMigrationActionExecutionException);
|
||||
|
||||
// All operations should be attempted in parallel despite failure
|
||||
expect(
|
||||
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
WorkspaceMigrationActionExecutionException,
|
||||
WorkspaceMigrationActionExecutionExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception';
|
||||
import { isTextColumnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/is-text-column-type.util';
|
||||
|
||||
export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
|
||||
@@ -41,9 +41,9 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
|
||||
case FieldMetadataType.TS_VECTOR:
|
||||
return 'tsvector';
|
||||
default:
|
||||
throw new WorkspaceMigrationRunnerException(
|
||||
`Cannot convert ${fieldMetadataType} to column type.`,
|
||||
WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_FIELD_METADATA_TYPE,
|
||||
);
|
||||
throw new WorkspaceMigrationActionExecutionException({
|
||||
message: `Cannot convert ${fieldMetadataType} to column type.`,
|
||||
code: WorkspaceMigrationActionExecutionExceptionCode.UNSUPPORTED_FIELD_METADATA_TYPE,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+7
-7
@@ -16,9 +16,9 @@ import { type WorkspaceSchemaColumnDefinition } from 'src/engine/twenty-orm/work
|
||||
import { computePostgresEnumName } from 'src/engine/workspace-manager/workspace-migration/utils/compute-postgres-enum-name.util';
|
||||
import { serializeDefaultValue } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
WorkspaceMigrationActionExecutionException,
|
||||
WorkspaceMigrationActionExecutionExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception';
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/field-metadata-type-to-column-type.util';
|
||||
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/get-workspace-schema-context-for-migration.util';
|
||||
|
||||
@@ -40,10 +40,10 @@ export const generateCompositeColumnDefinition = ({
|
||||
compositeProperty.type === FieldMetadataType.RELATION ||
|
||||
compositeProperty.type === FieldMetadataType.MORPH_RELATION
|
||||
) {
|
||||
throw new WorkspaceMigrationRunnerException(
|
||||
`Relation type not supported for composite columns`,
|
||||
WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE,
|
||||
);
|
||||
throw new WorkspaceMigrationActionExecutionException({
|
||||
message: `Relation type not supported for composite columns`,
|
||||
code: WorkspaceMigrationActionExecutionExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
const columnName = computeCompositeColumnName(
|
||||
|
||||
+7
-7
@@ -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/utils/compute-postgres-enum-name.util';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
WorkspaceMigrationActionExecutionException,
|
||||
WorkspaceMigrationActionExecutionExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception';
|
||||
|
||||
export interface CreateEnumOperationSpec {
|
||||
operation: EnumOperation.CREATE;
|
||||
@@ -246,9 +246,9 @@ export const executeBatchEnumOperations = async ({
|
||||
|
||||
await Promise.all(enumPromises);
|
||||
} catch (error) {
|
||||
throw new WorkspaceMigrationRunnerException(
|
||||
`Failed to execute batch enum operations: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
WorkspaceMigrationRunnerExceptionCode.ENUM_OPERATION_FAILED,
|
||||
);
|
||||
throw new WorkspaceMigrationActionExecutionException({
|
||||
message: `Failed to execute batch enum operations: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
code: WorkspaceMigrationActionExecutionExceptionCode.ENUM_OPERATION_FAILED,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user