feat(plugin-tools): improve error serialization and let each storage plugin serialize errors themselves

This commit is contained in:
Joakim Carlstein 2023-12-12 15:32:58 +01:00
parent 09181f284d
commit a79f8e8e37
6 changed files with 29 additions and 10 deletions

View file

@ -0,0 +1,5 @@
---
'@emigrate/plugin-tools': minor
---
When serializing errors take all "own properties" into account to be able to serialize errors thrown by the `mysql2` package for instance without losing any information

View file

@ -0,0 +1,8 @@
---
'@emigrate/plugin-tools': patch
'@emigrate/storage-fs': patch
'@emigrate/mysql': patch
'@emigrate/cli': patch
---
Serialization of errors now happens inside storage plugins because it makes more sense and the types are easier to work with this way

View file

@ -20,7 +20,7 @@ import {
type GenerateMigrationFunction, type GenerateMigrationFunction,
type GeneratorPlugin, type GeneratorPlugin,
} from '@emigrate/plugin-tools/types'; } from '@emigrate/plugin-tools/types';
import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools'; import { getTimestampPrefix, sanitizeMigrationName, serializeError } from '@emigrate/plugin-tools';
const defaultTable = 'migrations'; const defaultTable = 'migrations';
@ -213,7 +213,7 @@ export const createMysqlStorage = ({ table = defaultTable, connection }: MysqlSt
status: row.status, status: row.status,
date: new Date(row.date), date: new Date(row.date),
// FIXME: Migrate the migrations table to support the error column // FIXME: Migrate the migrations table to support the error column
error: row.status === 'failed' ? new Error('Unknown error reason') : undefined, error: row.status === 'failed' ? serializeError(new Error('Unknown error reason')) : undefined,
}; };
} }
}, },

View file

@ -11,12 +11,16 @@ import {
} from './types.js'; } from './types.js';
export const serializeError = (error: Error): SerializedError => { export const serializeError = (error: Error): SerializedError => {
return { const properties: Record<string, unknown> = {
name: error.name, name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause instanceof Error ? serializeError(error.cause) : error.cause,
}; };
for (const key of Object.getOwnPropertyNames(error)) {
const value = error[key as keyof Error];
properties[key] = value instanceof Error ? serializeError(value) : value;
}
return properties as SerializedError;
}; };
export const isGeneratorPlugin = (plugin: any): plugin is GeneratorPlugin => { export const isGeneratorPlugin = (plugin: any): plugin is GeneratorPlugin => {

View file

@ -5,7 +5,8 @@ export type StringOrModule<T> = string | T | (() => Awaitable<T>) | (() => Await
export type MigrationStatus = 'failed' | 'done' | 'pending'; export type MigrationStatus = 'failed' | 'done' | 'pending';
export type SerializedError = { export type SerializedError = {
name: string; [key: string]: unknown;
name?: string;
message: string; message: string;
stack?: string; stack?: string;
cause?: unknown; cause?: unknown;
@ -73,7 +74,7 @@ export type Storage = {
* @param migration The name of the migration that should be marked as failed. * @param migration The name of the migration that should be marked as failed.
* @param error The error that caused the migration to fail. * @param error The error that caused the migration to fail.
*/ */
onError(migration: MigrationMetadataFinished, error: SerializedError): Promise<void>; onError(migration: MigrationMetadataFinished, error: Error): Promise<void>;
/** /**
* Called when the command is finished or aborted (e.g. by a SIGTERM or SIGINT signal). * Called when the command is finished or aborted (e.g. by a SIGTERM or SIGINT signal).
* *

View file

@ -1,6 +1,7 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { serializeError } from '@emigrate/plugin-tools';
import { type SerializedError, type EmigrateStorage, type MigrationStatus } from '@emigrate/plugin-tools/types'; import { type SerializedError, type EmigrateStorage, type MigrationStatus } from '@emigrate/plugin-tools/types';
export type StorageFsOptions = { export type StorageFsOptions = {
@ -26,7 +27,7 @@ export default function storageFs({ filename }: StorageFsOptions): EmigrateStora
let lastUpdate: Promise<void> = Promise.resolve(); let lastUpdate: Promise<void> = Promise.resolve();
const update = async (migration: string, status: MigrationStatus, error?: SerializedError) => { const update = async (migration: string, status: MigrationStatus, error?: Error) => {
lastUpdate = lastUpdate.then(async () => { lastUpdate = lastUpdate.then(async () => {
const history = await read(); const history = await read();
@ -35,7 +36,7 @@ export default function storageFs({ filename }: StorageFsOptions): EmigrateStora
[migration]: { [migration]: {
status, status,
date: new Date().toISOString(), date: new Date().toISOString(),
error, error: error ? serializeError(error) : undefined,
}, },
}; };