feat(up): serialize errors before passing them to storage plugins

This commit is contained in:
Joakim Carlstein 2023-12-08 09:39:27 +01:00
parent 3b2b21f729
commit c1d55978d7
7 changed files with 43 additions and 26 deletions

View file

@ -0,0 +1,5 @@
---
'@emigrate/storage-fs': minor
---
Handle the serialized errors coming from Emigrate, so no need to serialize errors ourselves

View file

@ -0,0 +1,5 @@
---
'@emigrate/plugin-tools': minor
---
Add serializeError utility function for serializing Error instances

View file

@ -0,0 +1,5 @@
---
'@emigrate/cli': minor
---
Serialize errors before passing them to the storage so that storage plugins doesn't have to care about serialization of errors

View file

@ -1,6 +1,6 @@
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools'; import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage, serializeError } from '@emigrate/plugin-tools';
import { import {
type LoaderPlugin, type LoaderPlugin,
type MigrationFunction, type MigrationFunction,
@ -219,15 +219,16 @@ export default async function upCommand({
finishedMigrations.push(finishedMigration); finishedMigrations.push(finishedMigration);
} catch (error) { } catch (error) {
const errorInstance = error instanceof Error ? error : new Error(String(error)); const errorInstance = error instanceof Error ? error : new Error(String(error));
const serializedError = serializeError(errorInstance);
const duration = getDuration(start); const duration = getDuration(start);
const finishedMigration: MigrationMetadataFinished = { const finishedMigration: MigrationMetadataFinished = {
...migration, ...migration,
status: 'failed', status: 'failed',
duration, duration,
error: errorInstance, error: serializedError,
}; };
await storage.onError(finishedMigration, errorInstance); await storage.onError(finishedMigration, serializedError);
await reporter.onMigrationError?.(finishedMigration, errorInstance); await reporter.onMigrationError?.(finishedMigration, errorInstance);
finishedMigrations.push(finishedMigration); finishedMigrations.push(finishedMigration);

View file

@ -7,8 +7,18 @@ import {
type EmigrateStorage, type EmigrateStorage,
type LoaderPlugin, type LoaderPlugin,
type StringOrModule, type StringOrModule,
type SerializedError,
} from './types.js'; } from './types.js';
export const serializeError = (error: Error): SerializedError => {
return {
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause instanceof Error ? serializeError(error.cause) : error.cause,
};
};
export const isGeneratorPlugin = (plugin: any): plugin is GeneratorPlugin => { export const isGeneratorPlugin = (plugin: any): plugin is GeneratorPlugin => {
if (!plugin || typeof plugin !== 'object') { if (!plugin || typeof plugin !== 'object') {
return false; return false;

View file

@ -4,11 +4,18 @@ export type StringOrModule<T> = string | T | (() => Awaitable<T>) | (() => Await
export type MigrationStatus = 'failed' | 'done' | 'pending'; export type MigrationStatus = 'failed' | 'done' | 'pending';
export type SerializedError = {
name: string;
message: string;
stack?: string;
cause?: unknown;
};
export type MigrationHistoryEntry = { export type MigrationHistoryEntry = {
name: string; name: string;
status: MigrationStatus; status: MigrationStatus;
date: Date; date: Date;
error?: unknown; error?: SerializedError;
}; };
export type Storage = { export type Storage = {
@ -66,7 +73,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: Error): Promise<void>; onError(migration: MigrationMetadataFinished, error: SerializedError): Promise<void>;
}; };
export type EmigrateStorage = { export type EmigrateStorage = {
@ -142,7 +149,7 @@ export type MigrationMetadata = {
export type MigrationMetadataFinished = MigrationMetadata & { export type MigrationMetadataFinished = MigrationMetadata & {
status: MigrationStatus | 'skipped'; status: MigrationStatus | 'skipped';
duration: number; duration: number;
error?: Error; error?: SerializedError;
}; };
export type LoaderPlugin = { export type LoaderPlugin = {

View file

@ -1,28 +1,12 @@
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 { 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 = {
filename: string; filename: string;
}; };
type SerializedError = {
name: string;
message: string;
stack?: string;
cause?: unknown;
};
const serializeError = (error: Error): SerializedError => {
return {
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause instanceof Error ? serializeError(error.cause) : error.cause,
};
};
export default function storageFs({ filename }: StorageFsOptions): EmigrateStorage { export default function storageFs({ filename }: StorageFsOptions): EmigrateStorage {
const filePath = path.resolve(process.cwd(), filename); const filePath = path.resolve(process.cwd(), filename);
const lockFilePath = `${filePath}.lock`; const lockFilePath = `${filePath}.lock`;
@ -42,7 +26,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?: Error) => { const update = async (migration: string, status: MigrationStatus, error?: SerializedError) => {
lastUpdate = lastUpdate.then(async () => { lastUpdate = lastUpdate.then(async () => {
const history = await read(); const history = await read();
@ -51,7 +35,7 @@ export default function storageFs({ filename }: StorageFsOptions): EmigrateStora
[migration]: { [migration]: {
status, status,
date: new Date().toISOString(), date: new Date().toISOString(),
error: error ? serializeError(error) : undefined, error,
}, },
}; };
@ -118,7 +102,7 @@ export default function storageFs({ filename }: StorageFsOptions): EmigrateStora
name, name,
status, status,
date: new Date(date), date: new Date(date),
error: error ? new Error(error.message) : undefined, error,
}; };
} }
}, },