diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/__snapshots__/format-upgrade-error-for-storage.util.spec.ts.snap b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/__snapshots__/format-upgrade-error-for-storage.util.spec.ts.snap index d08776ee4f..17aba7bc51 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/__snapshots__/format-upgrade-error-for-storage.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/__snapshots__/format-upgrade-error-for-storage.util.spec.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`formatUpgradeErrorForStorage should format a CustomError with code 1`] = ` "[CustomError] Workspace not found @@ -34,8 +34,10 @@ exports[`formatUpgradeErrorForStorage should format a WorkspaceMigrationRunnerEx "[WorkspaceMigrationRunnerException] Migration action 'create' for 'objectMetadata' (universalIdentifier: test-object-uid) failed Code: EXECUTION_FAILED Action: create on objectMetadata -Metadata error: column "label" cannot be null -Schema error: table already exists" +Metadata error: + [Error] column "label" cannot be null +Schema error: + [Error] table already exists" `; exports[`formatUpgradeErrorForStorage should format a WorkspaceMigrationRunnerException with INTERNAL_SERVER_ERROR 1`] = ` @@ -50,3 +52,14 @@ 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"`; + +exports[`formatUpgradeErrorForStorage should surface driver details of a QueryFailedError nested in an EXECUTION_FAILED 1`] = ` +"[WorkspaceMigrationRunnerException] Migration action 'create' for 'pageLayoutWidget' (universalIdentifier: f473b435-e2d4-4928-8d90-1db0094389f7) failed +Code: EXECUTION_FAILED +Action: create on pageLayoutWidget +Metadata error: + [QueryFailedError] duplicate key value violates unique constraint "IDX_PAGE_LAYOUT_WIDGET_UNIVERSAL_ID" + PostgreSQL code: 23505 + Detail: Key (universalIdentifier)=(f473b435-e2d4-4928-8d90-1db0094389f7) already exists. + Query: INSERT INTO "core"."pageLayoutWidget" VALUES ($1)" +`; diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/format-upgrade-error-for-storage.util.spec.ts b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/format-upgrade-error-for-storage.util.spec.ts index da3969fafe..dfd1ce92d2 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/format-upgrade-error-for-storage.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/format-upgrade-error-for-storage.util.spec.ts @@ -72,6 +72,40 @@ describe('formatUpgradeErrorForStorage', () => { expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot(); }); + it('should surface driver details of a QueryFailedError nested in an EXECUTION_FAILED', () => { + const driverError = new Error( + 'duplicate key value violates unique constraint "IDX_PAGE_LAYOUT_WIDGET_UNIVERSAL_ID"', + ); + + Object.assign(driverError, { + code: '23505', + detail: + 'Key (universalIdentifier)=(f473b435-e2d4-4928-8d90-1db0094389f7) already exists.', + }); + + const action = { + type: 'create', + metadataName: 'pageLayoutWidget', + flatEntity: { + universalIdentifier: 'f473b435-e2d4-4928-8d90-1db0094389f7', + }, + } as unknown as AllUniversalWorkspaceMigrationAction; + + const error = new WorkspaceMigrationRunnerException({ + action, + errors: { + metadata: new QueryFailedError( + 'INSERT INTO "core"."pageLayoutWidget" VALUES ($1)', + [], + driverError, + ), + }, + code: WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED, + }); + + expect(stripStack(formatUpgradeErrorForStorage(error))).toMatchSnapshot(); + }); + it('should format a WorkspaceMigrationBuilderException', () => { const report = { objectMetadata: [ diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/utils/format-upgrade-error-for-storage.util.ts b/packages/twenty-server/src/engine/core-modules/upgrade/utils/format-upgrade-error-for-storage.util.ts index 8c714f0fe1..364d7540ca 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/utils/format-upgrade-error-for-storage.util.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/utils/format-upgrade-error-for-storage.util.ts @@ -1,4 +1,4 @@ -import { CustomError } from 'twenty-shared/utils'; +import { CustomError, isDefined } from 'twenty-shared/utils'; import { QueryFailedError } from 'typeorm'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; @@ -20,6 +20,31 @@ const joinParts = (parts: (string | null)[]): string => { return joined.slice(0, MAX_ERROR_MESSAGE_LENGTH) + '\n[truncated]'; }; +const indent = (text: string): string => + text + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); + +// Recurse so a nested QueryFailedError keeps its pg code/detail, not just its message. +const buildNestedErrorParts = ({ + label, + error, +}: { + label: string; + error: unknown; +}): (string | null)[] => { + if (!isDefined(error)) { + return []; + } + + const nestedParts = buildErrorParts(error).filter((part): part is string => + Boolean(part), + ); + + return [`${label}:`, ...nestedParts.map(indent)]; +}; + const buildErrorParts = (error: unknown): (string | null)[] => { if (error instanceof QueryFailedError) { const driverError = error.driverError; @@ -40,15 +65,18 @@ const buildErrorParts = (error: unknown): (string | null)[] => { 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, + ...buildNestedErrorParts({ + label: 'Metadata error', + error: error.errors?.metadata, + }), + ...buildNestedErrorParts({ + label: 'Schema error', + error: error.errors?.workspaceSchema, + }), + ...buildNestedErrorParts({ + label: 'Transpilation error', + error: error.errors?.actionTranspilation, + }), formatStack(error.stack), ]; }