Store upgrade commands error message (#19443)
# Introduction Storing the failing upgrade command formatted message in database.
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddErrorMessageToUpgradeMigration1775649426693
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddErrorMessageToUpgradeMigration1775649426693';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."upgradeMigration" ADD "errorMessage" text`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."upgradeMigration" DROP COLUMN "errorMessage"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -71,6 +71,7 @@ export class InstanceUpgradeService {
|
||||
name,
|
||||
workspaceId: null,
|
||||
executedByVersion,
|
||||
error,
|
||||
});
|
||||
|
||||
this.logger.error(
|
||||
@@ -120,6 +121,7 @@ export class InstanceUpgradeService {
|
||||
name,
|
||||
workspaceId: null,
|
||||
executedByVersion,
|
||||
error,
|
||||
});
|
||||
|
||||
this.logger.error(
|
||||
|
||||
+4
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, type QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { UpgradeMigrationEntity } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
|
||||
import { formatUpgradeErrorForStorage } from 'src/engine/core-modules/upgrade/utils/format-upgrade-error-for-storage.util';
|
||||
|
||||
@Injectable()
|
||||
export class UpgradeMigrationService {
|
||||
@@ -65,10 +66,12 @@ export class UpgradeMigrationService {
|
||||
name,
|
||||
workspaceId,
|
||||
executedByVersion,
|
||||
error,
|
||||
}: {
|
||||
name: string;
|
||||
workspaceId: string | null;
|
||||
executedByVersion: string;
|
||||
error: unknown;
|
||||
}): Promise<void> {
|
||||
const previousAttempts = await this.upgradeMigrationRepository.count({
|
||||
where: {
|
||||
@@ -83,6 +86,7 @@ export class UpgradeMigrationService {
|
||||
attempt: previousAttempts + 1,
|
||||
executedByVersion,
|
||||
workspaceId,
|
||||
errorMessage: formatUpgradeErrorForStorage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -170,6 +170,7 @@ export class WorkspaceUpgradeService {
|
||||
name: commandName,
|
||||
workspaceId,
|
||||
executedByVersion,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ export class UpgradeMigrationEntity {
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
executedByVersion: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
errorMessage: string | null;
|
||||
|
||||
@ManyToOne(() => WorkspaceEntity, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'workspaceId' })
|
||||
workspace: Relation<WorkspaceEntity> | null;
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a CustomError with code 1`] = `
|
||||
"[CustomError] Workspace not found
|
||||
Code: WORKSPACE_NOT_FOUND"
|
||||
`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a QueryFailedError with driver details 1`] = `
|
||||
"[QueryFailedError] duplicate key value violates unique constraint "UQ_name"
|
||||
PostgreSQL code: 23505
|
||||
Detail: Key (name)=(foo) already exists.
|
||||
Query: INSERT INTO "core"."upgradeMigration" VALUES ($1)"
|
||||
`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a QueryFailedError without driver code or detail 1`] = `
|
||||
"[QueryFailedError] relation "missing_table" does not exist
|
||||
Query: SELECT * FROM "missing_table""
|
||||
`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a WorkspaceMigrationBuilderException 1`] = `
|
||||
"[WorkspaceMigrationBuilderException] Workspace migration builder failed
|
||||
Report: {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"validationErrors": [
|
||||
"name must not be empty"
|
||||
]
|
||||
}
|
||||
]
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a WorkspaceMigrationRunnerException with EXECUTION_FAILED 1`] = `
|
||||
"[WorkspaceMigrationRunnerException] Migration action 'create' for 'objectMetadata' failed
|
||||
Code: EXECUTION_FAILED
|
||||
Action: create on objectMetadata
|
||||
Metadata error: column "label" cannot be null
|
||||
Schema error: table already exists"
|
||||
`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a WorkspaceMigrationRunnerException with INTERNAL_SERVER_ERROR 1`] = `
|
||||
"[WorkspaceMigrationRunnerException] Something went wrong internally
|
||||
Code: INTERNAL_SERVER_ERROR"
|
||||
`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a generic Error 1`] = `"[Error] Something unexpected happened"`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a number value 1`] = `"42"`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format a string value 1`] = `"raw string error"`;
|
||||
|
||||
exports[`formatUpgradeErrorForStorage should format an undefined value 1`] = `"undefined"`;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { CustomError } from 'twenty-shared/utils';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
import { type AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
import { type OrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
|
||||
|
||||
import { formatUpgradeErrorForStorage } from 'src/engine/core-modules/upgrade/utils/format-upgrade-error-for-storage.util';
|
||||
|
||||
const stripStack = (output: string): string =>
|
||||
output.replace(/\n\s+at .+/g, '');
|
||||
|
||||
describe('formatUpgradeErrorForStorage', () => {
|
||||
it('should format a QueryFailedError with driver details', () => {
|
||||
const driverError = new Error(
|
||||
'duplicate key value violates unique constraint "UQ_name"',
|
||||
);
|
||||
|
||||
Object.assign(driverError, {
|
||||
code: '23505',
|
||||
detail: 'Key (name)=(foo) already exists.',
|
||||
});
|
||||
|
||||
const error = new QueryFailedError(
|
||||
'INSERT INTO "core"."upgradeMigration" VALUES ($1)',
|
||||
[],
|
||||
driverError,
|
||||
);
|
||||
|
||||
expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format a QueryFailedError without driver code or detail', () => {
|
||||
const error = new QueryFailedError(
|
||||
'SELECT * FROM "missing_table"',
|
||||
[],
|
||||
new Error('relation "missing_table" does not exist'),
|
||||
);
|
||||
|
||||
expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format a WorkspaceMigrationRunnerException with INTERNAL_SERVER_ERROR', () => {
|
||||
const error = new WorkspaceMigrationRunnerException({
|
||||
message: 'Something went wrong internally',
|
||||
code: WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
|
||||
expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format a WorkspaceMigrationRunnerException with EXECUTION_FAILED', () => {
|
||||
const action = {
|
||||
type: 'create',
|
||||
metadataName: 'objectMetadata',
|
||||
} as unknown as AllUniversalWorkspaceMigrationAction;
|
||||
|
||||
const error = new WorkspaceMigrationRunnerException({
|
||||
action,
|
||||
errors: {
|
||||
metadata: new Error('column "label" cannot be null'),
|
||||
workspaceSchema: new Error('table already exists'),
|
||||
},
|
||||
code: WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED,
|
||||
});
|
||||
|
||||
expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format a WorkspaceMigrationBuilderException', () => {
|
||||
const report = {
|
||||
objectMetadata: [
|
||||
{
|
||||
validationErrors: ['name must not be empty'],
|
||||
},
|
||||
],
|
||||
} as unknown as OrchestratorFailureReport;
|
||||
|
||||
const error = new WorkspaceMigrationBuilderException({
|
||||
status: 'fail',
|
||||
report,
|
||||
});
|
||||
|
||||
expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format a CustomError with code', () => {
|
||||
const error = new CustomError('Workspace not found', 'WORKSPACE_NOT_FOUND');
|
||||
|
||||
expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format a generic Error', () => {
|
||||
const error = new Error('Something unexpected happened');
|
||||
|
||||
expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format a string value', () => {
|
||||
expect(formatUpgradeErrorForStorage('raw string error')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format an undefined value', () => {
|
||||
expect(formatUpgradeErrorForStorage(undefined)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format a number value', () => {
|
||||
expect(formatUpgradeErrorForStorage(42)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { CustomError } from 'twenty-shared/utils';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
|
||||
const MAX_ERROR_MESSAGE_LENGTH = 10_000;
|
||||
|
||||
const formatStack = (stack: string | undefined): string => {
|
||||
return (stack ?? '').split('\n').slice(1).join('\n');
|
||||
};
|
||||
|
||||
const joinParts = (parts: (string | null)[]): string => {
|
||||
const joined = parts.filter(Boolean).join('\n');
|
||||
|
||||
if (joined.length <= MAX_ERROR_MESSAGE_LENGTH) {
|
||||
return joined;
|
||||
}
|
||||
|
||||
return joined.slice(0, MAX_ERROR_MESSAGE_LENGTH) + '\n[truncated]';
|
||||
};
|
||||
|
||||
const buildErrorParts = (error: unknown): (string | null)[] => {
|
||||
if (error instanceof QueryFailedError) {
|
||||
const driverError = error.driverError;
|
||||
|
||||
return [
|
||||
`[QueryFailedError] ${error.message}`,
|
||||
driverError?.code ? `PostgreSQL code: ${driverError.code}` : null,
|
||||
driverError?.detail ? `Detail: ${driverError.detail}` : null,
|
||||
`Query: ${error.query}`,
|
||||
formatStack(error.stack),
|
||||
];
|
||||
}
|
||||
|
||||
if (error instanceof WorkspaceMigrationRunnerException) {
|
||||
return [
|
||||
`[WorkspaceMigrationRunnerException] ${error.message}`,
|
||||
`Code: ${error.code}`,
|
||||
error.action
|
||||
? `Action: ${error.action.type} on ${error.action.metadataName}`
|
||||
: null,
|
||||
error.errors?.metadata
|
||||
? `Metadata error: ${error.errors.metadata.message}`
|
||||
: null,
|
||||
error.errors?.workspaceSchema
|
||||
? `Schema error: ${error.errors.workspaceSchema.message}`
|
||||
: null,
|
||||
error.errors?.actionTranspilation
|
||||
? `Transpilation error: ${error.errors.actionTranspilation.message}`
|
||||
: null,
|
||||
formatStack(error.stack),
|
||||
];
|
||||
}
|
||||
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
return [
|
||||
`[WorkspaceMigrationBuilderException] ${error.message}`,
|
||||
`Report: ${JSON.stringify(error.failedWorkspaceMigrationBuildResult.report, null, 2)}`,
|
||||
formatStack(error.stack),
|
||||
];
|
||||
}
|
||||
|
||||
if (error instanceof CustomError) {
|
||||
return [
|
||||
`[CustomError] ${error.message}`,
|
||||
error.code ? `Code: ${error.code}` : null,
|
||||
formatStack(error.stack),
|
||||
];
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return [`[Error] ${error.message}`, formatStack(error.stack)];
|
||||
}
|
||||
|
||||
return [String(error)];
|
||||
};
|
||||
|
||||
export const formatUpgradeErrorForStorage = (error: unknown): string => {
|
||||
return joinParts(buildErrorParts(error));
|
||||
};
|
||||
Reference in New Issue
Block a user