feat(types): move Emigrate types to separate package and improve types (#41)
* feat(types): move Emigrate types to separate package Also refactor the types to use discriminating unions for easier error handling and such. Errors passed to storage plugins should now be serialized and storage plugins are expected to return already serialized errors on failed history entries. * fix(mysql): handle the new type changes * fix(storage-fs): handle the new type changes * feat(cli): better error handling and types Adapt to the new types from the @emigrate/types package, like discriminating union types and serializing and deserializing errors
This commit is contained in:
parent
afe56594c5
commit
cae6d11d53
38 changed files with 630 additions and 259 deletions
5
.changeset/early-hornets-marry.md
Normal file
5
.changeset/early-hornets-marry.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Make Emigrate Error instances deserializable using the serialize-error package, and also switch to its serializeError method
|
||||||
5
.changeset/eleven-sheep-think.md
Normal file
5
.changeset/eleven-sheep-think.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Shutdown the storage correctly in case of directory or file reading errors
|
||||||
5
.changeset/healthy-monkeys-tan.md
Normal file
5
.changeset/healthy-monkeys-tan.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/types': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Use discriminating union types for migration types for easier error handling and such
|
||||||
6
.changeset/hip-mayflies-provide.md
Normal file
6
.changeset/hip-mayflies-provide.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'@emigrate/plugin-tools': minor
|
||||||
|
'@emigrate/types': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Move the Emigrate plugin types to a separate package for fewer version bumps in plugins hopefully
|
||||||
5
.changeset/moody-penguins-repair.md
Normal file
5
.changeset/moody-penguins-repair.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Adapt to the new discriminating union types in @emigrate/types
|
||||||
5
.changeset/spotty-otters-attack.md
Normal file
5
.changeset/spotty-otters-attack.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/types': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Make it easier for storage plugins by serializing errors passed to the onError method and let it respond with serialized errors in the getHistory method
|
||||||
|
|
@ -44,13 +44,15 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emigrate/plugin-tools": "workspace:*",
|
"@emigrate/plugin-tools": "workspace:*",
|
||||||
|
"@emigrate/types": "workspace:*",
|
||||||
"ansis": "2.0.2",
|
"ansis": "2.0.2",
|
||||||
"cosmiconfig": "8.3.6",
|
"cosmiconfig": "8.3.6",
|
||||||
"elegant-spinner": "3.0.0",
|
"elegant-spinner": "3.0.0",
|
||||||
"figures": "6.0.1",
|
"figures": "6.0.1",
|
||||||
"is-interactive": "2.0.0",
|
"is-interactive": "2.0.0",
|
||||||
"log-update": "6.0.0",
|
"log-update": "6.0.0",
|
||||||
"pretty-ms": "8.0.0"
|
"pretty-ms": "8.0.0",
|
||||||
|
"serialize-error": "11.0.3"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"extends": "../../package.json"
|
"extends": "../../package.json"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { type MigrationHistoryEntry, type MigrationMetadata, type MigrationMetadataFinished } from '@emigrate/types';
|
||||||
type MigrationHistoryEntry,
|
|
||||||
type MigrationMetadata,
|
|
||||||
type MigrationMetadataFinished,
|
|
||||||
} from '@emigrate/plugin-tools/types';
|
|
||||||
import { toMigrationMetadata } from './to-migration-metadata.js';
|
import { toMigrationMetadata } from './to-migration-metadata.js';
|
||||||
import { getMigrations as getMigrationsOriginal } from './get-migrations.js';
|
import { getMigrations as getMigrationsOriginal } from './get-migrations.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||||
import { BadOptionError, MissingOptionError, StorageInitError } from '../errors.js';
|
import { BadOptionError, MissingOptionError, StorageInitError, toError } from '../errors.js';
|
||||||
import { type Config } from '../types.js';
|
import { type Config } from '../types.js';
|
||||||
import { exec } from '../exec.js';
|
import { exec } from '../exec.js';
|
||||||
import { migrationRunner } from '../migration-runner.js';
|
import { migrationRunner } from '../migration-runner.js';
|
||||||
|
|
@ -12,20 +12,20 @@ const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||||
|
|
||||||
export default async function listCommand({ directory, reporter: reporterConfig, storage: storageConfig }: Config) {
|
export default async function listCommand({ directory, reporter: reporterConfig, storage: storageConfig }: Config) {
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw new MissingOptionError('directory');
|
throw MissingOptionError.fromOption('directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
||||||
|
|
||||||
if (!storagePlugin) {
|
if (!storagePlugin) {
|
||||||
throw new BadOptionError('storage', 'No storage found, please specify a storage using the storage option');
|
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||||
}
|
}
|
||||||
|
|
||||||
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||||
|
|
||||||
if (!reporter) {
|
if (!reporter) {
|
||||||
throw new BadOptionError(
|
throw BadOptionError.fromOption(
|
||||||
'reporter',
|
'reporter',
|
||||||
'No reporter found, please specify an existing reporter using the reporter option',
|
'No reporter found, please specify an existing reporter using the reporter option',
|
||||||
);
|
);
|
||||||
|
|
@ -36,11 +36,12 @@ export default async function listCommand({ directory, reporter: reporterConfig,
|
||||||
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
||||||
|
|
||||||
if (storageError) {
|
if (storageError) {
|
||||||
await reporter.onFinished?.([], new StorageInitError('Could not initialize storage', { cause: storageError }));
|
await reporter.onFinished?.([], StorageInitError.fromError(storageError));
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const collectedMigrations = collectMigrations(cwd, directory, storage.getHistory());
|
const collectedMigrations = collectMigrations(cwd, directory, storage.getHistory());
|
||||||
|
|
||||||
const error = await migrationRunner({
|
const error = await migrationRunner({
|
||||||
|
|
@ -57,4 +58,11 @@ export default async function listCommand({ directory, reporter: reporterConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
return error ? 1 : 0;
|
return error ? 1 : 0;
|
||||||
|
} catch (error) {
|
||||||
|
await reporter.onFinished?.([], toError(error));
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
} finally {
|
||||||
|
await storage.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,19 @@ import process from 'node:process';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin, getOrLoadReporter } from '@emigrate/plugin-tools';
|
import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin, getOrLoadReporter } from '@emigrate/plugin-tools';
|
||||||
import { type MigrationMetadata } from '@emigrate/plugin-tools/types';
|
import { type MigrationMetadataFinished, type MigrationMetadata, isFailedMigration } from '@emigrate/types';
|
||||||
import { BadOptionError, MissingArgumentsError, MissingOptionError, UnexpectedError } from '../errors.js';
|
import {
|
||||||
|
BadOptionError,
|
||||||
|
EmigrateError,
|
||||||
|
MissingArgumentsError,
|
||||||
|
MissingOptionError,
|
||||||
|
UnexpectedError,
|
||||||
|
toError,
|
||||||
|
} from '../errors.js';
|
||||||
import { type Config } from '../types.js';
|
import { type Config } from '../types.js';
|
||||||
import { withLeadingPeriod } from '../with-leading-period.js';
|
import { withLeadingPeriod } from '../with-leading-period.js';
|
||||||
import { version } from '../get-package-info.js';
|
import { version } from '../get-package-info.js';
|
||||||
|
import { getDuration } from '../get-duration.js';
|
||||||
|
|
||||||
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||||
|
|
||||||
|
|
@ -15,15 +23,15 @@ export default async function newCommand(
|
||||||
name: string,
|
name: string,
|
||||||
) {
|
) {
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw new MissingOptionError('directory');
|
throw MissingOptionError.fromOption('directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new MissingArgumentsError('name');
|
throw MissingArgumentsError.fromArgument('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!extension && !template && plugins.length === 0) {
|
if (!extension && !template && plugins.length === 0) {
|
||||||
throw new MissingOptionError(['extension', 'template', 'plugin']);
|
throw MissingOptionError.fromOption(['extension', 'template', 'plugin']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
@ -31,7 +39,7 @@ export default async function newCommand(
|
||||||
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||||
|
|
||||||
if (!reporter) {
|
if (!reporter) {
|
||||||
throw new BadOptionError(
|
throw BadOptionError.fromOption(
|
||||||
'reporter',
|
'reporter',
|
||||||
'No reporter found, please specify an existing reporter using the reporter option',
|
'No reporter found, please specify an existing reporter using the reporter option',
|
||||||
);
|
);
|
||||||
|
|
@ -39,6 +47,8 @@ export default async function newCommand(
|
||||||
|
|
||||||
await reporter.onInit?.({ command: 'new', version, cwd, dry: false, directory });
|
await reporter.onInit?.({ command: 'new', version, cwd, dry: false, directory });
|
||||||
|
|
||||||
|
const start = process.hrtime();
|
||||||
|
|
||||||
let filename: string | undefined;
|
let filename: string | undefined;
|
||||||
let content: string | undefined;
|
let content: string | undefined;
|
||||||
|
|
||||||
|
|
@ -82,7 +92,7 @@ export default async function newCommand(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filename || content === undefined) {
|
if (!filename || content === undefined) {
|
||||||
throw new BadOptionError(
|
throw BadOptionError.fromOption(
|
||||||
'plugin',
|
'plugin',
|
||||||
'No generator plugin found, please specify a generator plugin using the plugin option',
|
'No generator plugin found, please specify a generator plugin using the plugin option',
|
||||||
);
|
);
|
||||||
|
|
@ -102,19 +112,31 @@ export default async function newCommand(
|
||||||
|
|
||||||
await reporter.onNewMigration?.(migration, content);
|
await reporter.onNewMigration?.(migration, content);
|
||||||
|
|
||||||
let saveError: Error | undefined;
|
const finishedMigrations: MigrationMetadataFinished[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createDirectory(directoryPath);
|
await createDirectory(directoryPath);
|
||||||
await saveFile(filePath, content);
|
await saveFile(filePath, content);
|
||||||
|
const duration = getDuration(start);
|
||||||
|
finishedMigrations.push({ ...migration, status: 'done', duration });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
saveError = error instanceof Error ? error : new Error(String(error));
|
const duration = getDuration(start);
|
||||||
|
const errorInstance = toError(error);
|
||||||
|
finishedMigrations.push({ ...migration, status: 'failed', duration, error: errorInstance });
|
||||||
}
|
}
|
||||||
|
|
||||||
await reporter.onFinished?.(
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
[{ ...migration, status: saveError ? 'failed' : 'done', error: saveError, duration: 0 }],
|
const firstFailed = finishedMigrations.find(isFailedMigration);
|
||||||
saveError,
|
const firstError =
|
||||||
);
|
firstFailed?.error instanceof EmigrateError
|
||||||
|
? firstFailed.error
|
||||||
|
: firstFailed
|
||||||
|
? new UnexpectedError(`Failed to create migration file: ${firstFailed.relativeFilePath}`, {
|
||||||
|
cause: firstFailed?.error,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
await reporter.onFinished?.(finishedMigrations, firstError);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDirectory(directoryPath: string) {
|
async function createDirectory(directoryPath: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||||
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/plugin-tools/types';
|
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/types';
|
||||||
import {
|
import {
|
||||||
BadOptionError,
|
BadOptionError,
|
||||||
MigrationNotRunError,
|
MigrationNotRunError,
|
||||||
|
|
@ -26,24 +26,24 @@ export default async function removeCommand(
|
||||||
name: string,
|
name: string,
|
||||||
) {
|
) {
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw new MissingOptionError('directory');
|
throw MissingOptionError.fromOption('directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new MissingArgumentsError('name');
|
throw MissingArgumentsError.fromArgument('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
||||||
|
|
||||||
if (!storagePlugin) {
|
if (!storagePlugin) {
|
||||||
throw new BadOptionError('storage', 'No storage found, please specify a storage using the storage option');
|
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||||
}
|
}
|
||||||
|
|
||||||
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||||
|
|
||||||
if (!reporter) {
|
if (!reporter) {
|
||||||
throw new BadOptionError(
|
throw BadOptionError.fromOption(
|
||||||
'reporter',
|
'reporter',
|
||||||
'No reporter found, please specify an existing reporter using the reporter option',
|
'No reporter found, please specify an existing reporter using the reporter option',
|
||||||
);
|
);
|
||||||
|
|
@ -52,14 +52,22 @@ export default async function removeCommand(
|
||||||
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
||||||
|
|
||||||
if (storageError) {
|
if (storageError) {
|
||||||
await reporter.onFinished?.([], new StorageInitError('Could not initialize storage', { cause: storageError }));
|
await reporter.onFinished?.([], StorageInitError.fromError(storageError));
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
await reporter.onInit?.({ command: 'remove', version, cwd, dry: false, directory });
|
await reporter.onInit?.({ command: 'remove', version, cwd, dry: false, directory });
|
||||||
|
|
||||||
const migrationFile = await getMigration(cwd, directory, name, !force);
|
const [migrationFile, fileError] = await exec(async () => getMigration(cwd, directory, name, !force));
|
||||||
|
|
||||||
|
if (fileError) {
|
||||||
|
await reporter.onFinished?.([], fileError);
|
||||||
|
|
||||||
|
await storage.end();
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
const finishedMigrations: MigrationMetadataFinished[] = [];
|
const finishedMigrations: MigrationMetadataFinished[] = [];
|
||||||
let historyEntry: MigrationHistoryEntry | undefined;
|
let historyEntry: MigrationHistoryEntry | undefined;
|
||||||
|
|
@ -71,7 +79,7 @@ export default async function removeCommand(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (migrationHistoryEntry.status === 'done' && !force) {
|
if (migrationHistoryEntry.status === 'done' && !force) {
|
||||||
removalError = new OptionNeededError(
|
removalError = OptionNeededError.fromOption(
|
||||||
'force',
|
'force',
|
||||||
`The migration "${migrationFile.name}" is not in a failed state. Use the "force" option to force its removal`,
|
`The migration "${migrationFile.name}" is not in a failed state. Use the "force" option to force its removal`,
|
||||||
);
|
);
|
||||||
|
|
@ -98,10 +106,7 @@ export default async function removeCommand(
|
||||||
removalError = error instanceof Error ? error : new Error(String(error));
|
removalError = error instanceof Error ? error : new Error(String(error));
|
||||||
}
|
}
|
||||||
} else if (!removalError) {
|
} else if (!removalError) {
|
||||||
removalError = new MigrationNotRunError(
|
removalError = MigrationNotRunError.fromMetadata(migrationFile);
|
||||||
`Migration "${migrationFile.name}" is not in the migration history`,
|
|
||||||
migrationFile,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removalError) {
|
if (removalError) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { describe, it, mock, type Mock } from 'node:test';
|
import { describe, it, mock, type Mock } from 'node:test';
|
||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { serializeError } from '@emigrate/plugin-tools';
|
|
||||||
import {
|
import {
|
||||||
type EmigrateReporter,
|
type EmigrateReporter,
|
||||||
type MigrationHistoryEntry,
|
type MigrationHistoryEntry,
|
||||||
|
|
@ -9,7 +8,10 @@ import {
|
||||||
type Storage,
|
type Storage,
|
||||||
type Plugin,
|
type Plugin,
|
||||||
type SerializedError,
|
type SerializedError,
|
||||||
} from '@emigrate/plugin-tools/types';
|
type FailedMigrationHistoryEntry,
|
||||||
|
type NonFailedMigrationHistoryEntry,
|
||||||
|
} from '@emigrate/types';
|
||||||
|
import { deserializeError } from 'serialize-error';
|
||||||
import { version } from '../get-package-info.js';
|
import { version } from '../get-package-info.js';
|
||||||
import upCommand from './up.js';
|
import upCommand from './up.js';
|
||||||
|
|
||||||
|
|
@ -117,7 +119,10 @@ describe('up', () => {
|
||||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
||||||
assert.strictEqual(getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]), failedEntry.error);
|
assert.deepStrictEqual(
|
||||||
|
getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]),
|
||||||
|
deserializeError(failedEntry.error),
|
||||||
|
);
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
||||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
||||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||||
|
|
@ -125,7 +130,7 @@ describe('up', () => {
|
||||||
error?.message,
|
error?.message,
|
||||||
`Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`,
|
`Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`,
|
||||||
);
|
);
|
||||||
assert.strictEqual(getErrorCause(error), failedEntry.error);
|
assert.deepStrictEqual(getErrorCause(error), deserializeError(failedEntry.error));
|
||||||
assert.strictEqual(entries?.length, 2);
|
assert.strictEqual(entries?.length, 2);
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||||
|
|
@ -155,7 +160,10 @@ describe('up', () => {
|
||||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
||||||
assert.strictEqual(getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]), failedEntry.error);
|
assert.deepStrictEqual(
|
||||||
|
getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]),
|
||||||
|
deserializeError(failedEntry.error),
|
||||||
|
);
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
||||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
||||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||||
|
|
@ -163,7 +171,7 @@ describe('up', () => {
|
||||||
error?.message,
|
error?.message,
|
||||||
`Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`,
|
`Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`,
|
||||||
);
|
);
|
||||||
assert.strictEqual(getErrorCause(error), failedEntry.error);
|
assert.deepStrictEqual(getErrorCause(error), deserializeError(failedEntry.error));
|
||||||
assert.strictEqual(entries?.length, 2);
|
assert.strictEqual(entries?.length, 2);
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||||
|
|
@ -354,27 +362,38 @@ function toMigrations(cwd: string, directory: string, names: string[]): Migratio
|
||||||
return names.map((name) => toMigration(cwd, directory, name));
|
return names.map((name) => toMigration(cwd, directory, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toEntry(
|
function toEntry(name: MigrationHistoryEntry): MigrationHistoryEntry;
|
||||||
name: string | MigrationHistoryEntry,
|
function toEntry<S extends MigrationHistoryEntry['status']>(
|
||||||
status: MigrationHistoryEntry['status'] = 'done',
|
name: string,
|
||||||
): MigrationHistoryEntry {
|
status?: S,
|
||||||
if (typeof name === 'string') {
|
): S extends 'failed' ? FailedMigrationHistoryEntry : NonFailedMigrationHistoryEntry;
|
||||||
|
|
||||||
|
function toEntry(name: string | MigrationHistoryEntry, status?: 'done' | 'failed'): MigrationHistoryEntry {
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
return name.status === 'failed' ? name : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'failed') {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
error: status === 'failed' ? serializeError(new Error('Failed')) : undefined,
|
error: { name: 'Error', message: 'Failed' },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return name;
|
return {
|
||||||
|
name,
|
||||||
|
status: status ?? 'done',
|
||||||
|
date: new Date(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toEntries(
|
function toEntries(
|
||||||
names: Array<string | MigrationHistoryEntry>,
|
names: Array<string | MigrationHistoryEntry>,
|
||||||
status: MigrationHistoryEntry['status'] = 'done',
|
status?: MigrationHistoryEntry['status'],
|
||||||
): MigrationHistoryEntry[] {
|
): MigrationHistoryEntry[] {
|
||||||
return names.map((name) => toEntry(name, status));
|
return names.map((name) => (typeof name === 'string' ? toEntry(name, status) : name));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function noop() {
|
async function noop() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||||
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/plugin-tools/types';
|
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types';
|
||||||
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError } from '../errors.js';
|
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError, toError } from '../errors.js';
|
||||||
import { type Config } from '../types.js';
|
import { type Config } from '../types.js';
|
||||||
import { withLeadingPeriod } from '../with-leading-period.js';
|
import { withLeadingPeriod } from '../with-leading-period.js';
|
||||||
import { type GetMigrationsFunction } from '../get-migrations.js';
|
import { type GetMigrationsFunction } from '../get-migrations.js';
|
||||||
|
|
@ -31,19 +31,19 @@ export default async function upCommand({
|
||||||
getMigrations,
|
getMigrations,
|
||||||
}: Config & ExtraFlags): Promise<number> {
|
}: Config & ExtraFlags): Promise<number> {
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw new MissingOptionError('directory');
|
throw MissingOptionError.fromOption('directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
||||||
|
|
||||||
if (!storagePlugin) {
|
if (!storagePlugin) {
|
||||||
throw new BadOptionError('storage', 'No storage found, please specify a storage using the storage option');
|
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||||
}
|
}
|
||||||
|
|
||||||
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||||
|
|
||||||
if (!reporter) {
|
if (!reporter) {
|
||||||
throw new BadOptionError(
|
throw BadOptionError.fromOption(
|
||||||
'reporter',
|
'reporter',
|
||||||
'No reporter found, please specify an existing reporter using the reporter option',
|
'No reporter found, please specify an existing reporter using the reporter option',
|
||||||
);
|
);
|
||||||
|
|
@ -54,11 +54,12 @@ export default async function upCommand({
|
||||||
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
||||||
|
|
||||||
if (storageError) {
|
if (storageError) {
|
||||||
await reporter.onFinished?.([], new StorageInitError('Could not initialize storage', { cause: storageError }));
|
await reporter.onFinished?.([], StorageInitError.fromError(storageError));
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const collectedMigrations = filterAsync(
|
const collectedMigrations = filterAsync(
|
||||||
collectMigrations(cwd, directory, storage.getHistory(), getMigrations),
|
collectMigrations(cwd, directory, storage.getHistory(), getMigrations),
|
||||||
(migration) => !isFinishedMigration(migration) || migration.status === 'failed',
|
(migration) => !isFinishedMigration(migration) || migration.status === 'failed',
|
||||||
|
|
@ -89,7 +90,10 @@ export default async function upCommand({
|
||||||
const loader = getLoaderByExtension(migration.extension);
|
const loader = getLoaderByExtension(migration.extension);
|
||||||
|
|
||||||
if (!loader) {
|
if (!loader) {
|
||||||
throw new BadOptionError('plugin', `No loader plugin found for file extension: ${migration.extension}`);
|
throw BadOptionError.fromOption(
|
||||||
|
'plugin',
|
||||||
|
`No loader plugin found for file extension: ${migration.extension}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async execute(migration) {
|
async execute(migration) {
|
||||||
|
|
@ -97,9 +101,7 @@ export default async function upCommand({
|
||||||
const [migrationFunction, loadError] = await exec(async () => loader.loadMigration(migration));
|
const [migrationFunction, loadError] = await exec(async () => loader.loadMigration(migration));
|
||||||
|
|
||||||
if (loadError) {
|
if (loadError) {
|
||||||
throw new MigrationLoadError(`Failed to load migration file: ${migration.relativeFilePath}`, migration, {
|
throw MigrationLoadError.fromMetadata(migration, loadError);
|
||||||
cause: loadError,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await migrationFunction();
|
await migrationFunction();
|
||||||
|
|
@ -107,4 +109,11 @@ export default async function upCommand({
|
||||||
});
|
});
|
||||||
|
|
||||||
return error ? 1 : 0;
|
return error ? 1 : 0;
|
||||||
|
} catch (error) {
|
||||||
|
await reporter.onFinished?.([], toError(error));
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
} finally {
|
||||||
|
await storage.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,26 @@
|
||||||
import { type MigrationHistoryEntry, type MigrationMetadata } from '@emigrate/plugin-tools/types';
|
import {
|
||||||
|
type SerializedError,
|
||||||
|
type MigrationMetadata,
|
||||||
|
type FailedMigrationMetadata,
|
||||||
|
type FailedMigrationHistoryEntry,
|
||||||
|
} from '@emigrate/types';
|
||||||
|
import { serializeError, errorConstructors, deserializeError } from 'serialize-error';
|
||||||
|
|
||||||
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'disjunction' });
|
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'disjunction' });
|
||||||
|
|
||||||
export const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
export const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
||||||
|
|
||||||
|
export const toSerializedError = (error: unknown) => {
|
||||||
|
const errorInstance = toError(error);
|
||||||
|
|
||||||
|
return serializeError(errorInstance) as unknown as SerializedError;
|
||||||
|
};
|
||||||
|
|
||||||
export class EmigrateError extends Error {
|
export class EmigrateError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
public code: string,
|
message: string | undefined,
|
||||||
message: string,
|
|
||||||
options?: ErrorOptions,
|
options?: ErrorOptions,
|
||||||
|
public code?: string,
|
||||||
) {
|
) {
|
||||||
super(message, options);
|
super(message, options);
|
||||||
}
|
}
|
||||||
|
|
@ -17,82 +29,132 @@ export class EmigrateError extends Error {
|
||||||
export class ShowUsageError extends EmigrateError {}
|
export class ShowUsageError extends EmigrateError {}
|
||||||
|
|
||||||
export class MissingOptionError extends ShowUsageError {
|
export class MissingOptionError extends ShowUsageError {
|
||||||
constructor(public option: string | string[]) {
|
static fromOption(option: string | string[]) {
|
||||||
super('ERR_MISSING_OPT', `Missing required option: ${Array.isArray(option) ? formatter.format(option) : option}`);
|
return new MissingOptionError(
|
||||||
|
`Missing required option: ${Array.isArray(option) ? formatter.format(option) : option}`,
|
||||||
|
undefined,
|
||||||
|
option,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string | undefined,
|
||||||
|
options?: ErrorOptions,
|
||||||
|
public option: string | string[] = '',
|
||||||
|
) {
|
||||||
|
super(message, options, 'ERR_MISSING_OPT');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MissingArgumentsError extends ShowUsageError {
|
export class MissingArgumentsError extends ShowUsageError {
|
||||||
constructor(public argument: string) {
|
static fromArgument(argument: string) {
|
||||||
super('ERR_MISSING_ARGS', `Missing required argument: ${argument}`);
|
return new MissingArgumentsError(`Missing required argument: ${argument}`, undefined, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string | undefined,
|
||||||
|
options?: ErrorOptions,
|
||||||
|
public argument = '',
|
||||||
|
) {
|
||||||
|
super(message, options, 'ERR_MISSING_ARGS');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OptionNeededError extends ShowUsageError {
|
export class OptionNeededError extends ShowUsageError {
|
||||||
|
static fromOption(option: string, message: string) {
|
||||||
|
return new OptionNeededError(message, undefined, option);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public option: string,
|
message: string | undefined,
|
||||||
message: string,
|
options?: ErrorOptions,
|
||||||
|
public option = '',
|
||||||
) {
|
) {
|
||||||
super('ERR_OPT_NEEDED', message);
|
super(message, options, 'ERR_OPT_NEEDED');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BadOptionError extends ShowUsageError {
|
export class BadOptionError extends ShowUsageError {
|
||||||
|
static fromOption(option: string, message: string) {
|
||||||
|
return new BadOptionError(message, undefined, option);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public option: string,
|
message: string | undefined,
|
||||||
message: string,
|
options?: ErrorOptions,
|
||||||
|
public option = '',
|
||||||
) {
|
) {
|
||||||
super('ERR_BAD_OPT', message);
|
super(message, options, 'ERR_BAD_OPT');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnexpectedError extends EmigrateError {
|
export class UnexpectedError extends EmigrateError {
|
||||||
constructor(message: string, options?: ErrorOptions) {
|
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||||
super('ERR_UNEXPECTED', message, options);
|
super(message, options, 'ERR_UNEXPECTED');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationHistoryError extends EmigrateError {
|
export class MigrationHistoryError extends EmigrateError {
|
||||||
constructor(
|
static fromHistoryEntry(entry: FailedMigrationHistoryEntry) {
|
||||||
message: string,
|
return new MigrationHistoryError(`Migration ${entry.name} is in a failed state, it should be fixed and removed`, {
|
||||||
public entry: MigrationHistoryEntry,
|
cause: deserializeError(entry.error),
|
||||||
) {
|
});
|
||||||
super('ERR_MIGRATION_HISTORY', message, { cause: entry.error });
|
}
|
||||||
|
|
||||||
|
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||||
|
super(message, options, 'ERR_MIGRATION_HISTORY');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationLoadError extends EmigrateError {
|
export class MigrationLoadError extends EmigrateError {
|
||||||
constructor(
|
static fromMetadata(metadata: MigrationMetadata, cause?: Error) {
|
||||||
message: string,
|
return new MigrationLoadError(`Failed to load migration file: ${metadata.relativeFilePath}`, { cause });
|
||||||
public metadata: MigrationMetadata,
|
}
|
||||||
options?: ErrorOptions,
|
|
||||||
) {
|
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||||
super('ERR_MIGRATION_LOAD', message, options);
|
super(message, options, 'ERR_MIGRATION_LOAD');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationRunError extends EmigrateError {
|
export class MigrationRunError extends EmigrateError {
|
||||||
constructor(
|
static fromMetadata(metadata: FailedMigrationMetadata) {
|
||||||
message: string,
|
return new MigrationRunError(`Failed to run migration: ${metadata.relativeFilePath}`, { cause: metadata.error });
|
||||||
public metadata: MigrationMetadata,
|
}
|
||||||
options?: ErrorOptions,
|
|
||||||
) {
|
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||||
super('ERR_MIGRATION_RUN', message, options);
|
super(message, options, 'ERR_MIGRATION_RUN');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationNotRunError extends EmigrateError {
|
export class MigrationNotRunError extends EmigrateError {
|
||||||
constructor(
|
static fromMetadata(metadata: MigrationMetadata, cause?: Error) {
|
||||||
message: string,
|
return new MigrationNotRunError(`Migration "${metadata.name}" is not in the migration history`, { cause });
|
||||||
public metadata: MigrationMetadata,
|
}
|
||||||
options?: ErrorOptions,
|
|
||||||
) {
|
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||||
super('ERR_MIGRATION_NOT_RUN', message, options);
|
super(message, options, 'ERR_MIGRATION_NOT_RUN');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StorageInitError extends EmigrateError {
|
export class StorageInitError extends EmigrateError {
|
||||||
constructor(message: string, options?: ErrorOptions) {
|
static fromError(error: Error) {
|
||||||
super('ERR_STORAGE_INIT', message, options);
|
return new StorageInitError('Could not initialize storage', { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||||
|
super(message, options, 'ERR_STORAGE_INIT');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorConstructors.set('EmigrateError', EmigrateError as ErrorConstructor);
|
||||||
|
errorConstructors.set('ShowUsageError', ShowUsageError as ErrorConstructor);
|
||||||
|
errorConstructors.set('MissingOptionError', MissingOptionError as unknown as ErrorConstructor);
|
||||||
|
errorConstructors.set('MissingArgumentsError', MissingArgumentsError as unknown as ErrorConstructor);
|
||||||
|
errorConstructors.set('OptionNeededError', OptionNeededError as unknown as ErrorConstructor);
|
||||||
|
errorConstructors.set('BadOptionError', BadOptionError as unknown as ErrorConstructor);
|
||||||
|
errorConstructors.set('UnexpectedError', UnexpectedError as ErrorConstructor);
|
||||||
|
errorConstructors.set('MigrationHistoryError', MigrationHistoryError as unknown as ErrorConstructor);
|
||||||
|
errorConstructors.set('MigrationLoadError', MigrationLoadError as unknown as ErrorConstructor);
|
||||||
|
errorConstructors.set('MigrationRunError', MigrationRunError as unknown as ErrorConstructor);
|
||||||
|
errorConstructors.set('MigrationNotRunError', MigrationNotRunError as unknown as ErrorConstructor);
|
||||||
|
errorConstructors.set('StorageInitError', StorageInitError as unknown as ErrorConstructor);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { type MigrationMetadata } from '@emigrate/plugin-tools/types';
|
import { type MigrationMetadata } from '@emigrate/types';
|
||||||
import { withLeadingPeriod } from './with-leading-period.js';
|
import { withLeadingPeriod } from './with-leading-period.js';
|
||||||
import { OptionNeededError } from './errors.js';
|
import { OptionNeededError } from './errors.js';
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ const checkMigrationFile = async (name: string, filePath: string) => {
|
||||||
throw new Error('Not a file');
|
throw new Error('Not a file');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
throw new OptionNeededError(
|
throw OptionNeededError.fromOption(
|
||||||
'force',
|
'force',
|
||||||
`The given migration name "${name}" does not exist or is not a file. Use the "force" option to ignore this error`,
|
`The given migration name "${name}" does not exist or is not a file. Use the "force" option to ignore this error`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { type MigrationMetadata } from '@emigrate/plugin-tools/types';
|
import { type Dirent } from 'node:fs';
|
||||||
|
import { type MigrationMetadata } from '@emigrate/types';
|
||||||
import { withLeadingPeriod } from './with-leading-period.js';
|
import { withLeadingPeriod } from './with-leading-period.js';
|
||||||
|
import { BadOptionError } from './errors.js';
|
||||||
|
|
||||||
export type GetMigrationsFunction = typeof getMigrations;
|
export type GetMigrationsFunction = typeof getMigrations;
|
||||||
|
|
||||||
export const getMigrations = async (cwd: string, directory: string): Promise<MigrationMetadata[]> => {
|
const tryReadDirectory = async (directoryPath: string): Promise<Dirent[]> => {
|
||||||
const allFilesInMigrationDirectory = await fs.readdir(path.resolve(cwd, directory), {
|
try {
|
||||||
|
return await fs.readdir(directoryPath, {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
throw BadOptionError.fromOption('directory', `Couldn't read directory: ${directoryPath}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMigrations = async (cwd: string, directory: string): Promise<MigrationMetadata[]> => {
|
||||||
|
const directoryPath = path.resolve(cwd, directory);
|
||||||
|
|
||||||
|
const allFilesInMigrationDirectory = await tryReadDirectory(directoryPath);
|
||||||
|
|
||||||
const migrationFiles: MigrationMetadata[] = allFilesInMigrationDirectory
|
const migrationFiles: MigrationMetadata[] = allFilesInMigrationDirectory
|
||||||
.filter((file) => file.isFile() && !file.name.startsWith('.') && !file.name.startsWith('_'))
|
.filter((file) => file.isFile() && !file.name.startsWith('.') && !file.name.startsWith('_'))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map(({ name }) => {
|
.map(({ name }) => {
|
||||||
const filePath = path.resolve(cwd, directory, name);
|
const filePath = path.join(directoryPath, name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { UnexpectedError } from './errors.js';
|
||||||
|
|
||||||
type PackageInfo = {
|
type PackageInfo = {
|
||||||
version: string;
|
version: string;
|
||||||
|
|
@ -24,7 +25,7 @@ const getPackageInfo = async () => {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Could not read package info from: ${packageInfoPath}`);
|
throw new UnexpectedError(`Could not read package info from: ${packageInfoPath}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const { version } = await getPackageInfo();
|
export const { version } = await getPackageInfo();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import {
|
import {
|
||||||
isFinishedMigration,
|
isFinishedMigration,
|
||||||
|
isFailedMigration,
|
||||||
type EmigrateReporter,
|
type EmigrateReporter,
|
||||||
type MigrationMetadata,
|
type MigrationMetadata,
|
||||||
type MigrationMetadataFinished,
|
type MigrationMetadataFinished,
|
||||||
type Storage,
|
type Storage,
|
||||||
} from '@emigrate/plugin-tools/types';
|
type FailedMigrationMetadata,
|
||||||
import { toError, EmigrateError, MigrationRunError } from './errors.js';
|
type SuccessfulMigrationMetadata,
|
||||||
|
} from '@emigrate/types';
|
||||||
|
import { toError, EmigrateError, MigrationRunError, toSerializedError } from './errors.js';
|
||||||
import { exec } from './exec.js';
|
import { exec } from './exec.js';
|
||||||
import { getDuration } from './get-duration.js';
|
import { getDuration } from './get-duration.js';
|
||||||
|
|
||||||
|
|
@ -43,7 +46,6 @@ export const migrationRunner = async ({
|
||||||
finishedMigrations.push({
|
finishedMigrations.push({
|
||||||
...migration,
|
...migration,
|
||||||
status: dry ? 'pending' : 'skipped',
|
status: dry ? 'pending' : 'skipped',
|
||||||
duration: 0,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|
@ -51,7 +53,7 @@ export const migrationRunner = async ({
|
||||||
migrationsToRun.push(migration);
|
migrationsToRun.push(migration);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for await (const migration of migrationsToRun) {
|
for await (const migration of migrationsToRun) {
|
||||||
finishedMigrations.push({ ...migration, status: 'skipped', duration: 0 });
|
finishedMigrations.push({ ...migration, status: 'skipped' });
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationsToRun.length = 0;
|
migrationsToRun.length = 0;
|
||||||
|
|
@ -72,7 +74,7 @@ export const migrationRunner = async ({
|
||||||
|
|
||||||
if (lockError) {
|
if (lockError) {
|
||||||
for await (const migration of migrationsToRun) {
|
for await (const migration of migrationsToRun) {
|
||||||
finishedMigrations.push({ ...migration, duration: 0, status: 'skipped' });
|
finishedMigrations.push({ ...migration, status: 'skipped' });
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationsToRun.length = 0;
|
migrationsToRun.length = 0;
|
||||||
|
|
@ -85,7 +87,7 @@ export const migrationRunner = async ({
|
||||||
for await (const finishedMigration of finishedMigrations) {
|
for await (const finishedMigration of finishedMigrations) {
|
||||||
switch (finishedMigration.status) {
|
switch (finishedMigration.status) {
|
||||||
case 'failed': {
|
case 'failed': {
|
||||||
await reporter.onMigrationError?.(finishedMigration, finishedMigration.error!);
|
await reporter.onMigrationError?.(finishedMigration, finishedMigration.error);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,7 +113,6 @@ export const migrationRunner = async ({
|
||||||
const finishedMigration: MigrationMetadataFinished = {
|
const finishedMigration: MigrationMetadataFinished = {
|
||||||
...migration,
|
...migration,
|
||||||
status: dry ? 'pending' : 'skipped',
|
status: dry ? 'pending' : 'skipped',
|
||||||
duration: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await reporter.onMigrationSkip?.(finishedMigration);
|
await reporter.onMigrationSkip?.(finishedMigration);
|
||||||
|
|
@ -127,39 +128,43 @@ export const migrationRunner = async ({
|
||||||
const [, migrationError] = await exec(async () => execute(migration));
|
const [, migrationError] = await exec(async () => execute(migration));
|
||||||
|
|
||||||
const duration = getDuration(start);
|
const duration = getDuration(start);
|
||||||
const finishedMigration: MigrationMetadataFinished = {
|
|
||||||
|
if (migrationError) {
|
||||||
|
const finishedMigration: FailedMigrationMetadata = {
|
||||||
...migration,
|
...migration,
|
||||||
status: migrationError ? 'failed' : 'done',
|
status: 'failed',
|
||||||
duration,
|
duration,
|
||||||
error: migrationError,
|
error: migrationError,
|
||||||
};
|
};
|
||||||
finishedMigrations.push(finishedMigration);
|
await storage.onError(finishedMigration, toSerializedError(migrationError));
|
||||||
|
|
||||||
if (migrationError) {
|
|
||||||
await storage.onError(finishedMigration, migrationError);
|
|
||||||
await reporter.onMigrationError?.(finishedMigration, migrationError);
|
await reporter.onMigrationError?.(finishedMigration, migrationError);
|
||||||
|
finishedMigrations.push(finishedMigration);
|
||||||
skip = true;
|
skip = true;
|
||||||
} else {
|
} else {
|
||||||
|
const finishedMigration: SuccessfulMigrationMetadata = {
|
||||||
|
...migration,
|
||||||
|
status: 'done',
|
||||||
|
duration,
|
||||||
|
};
|
||||||
await storage.onSuccess(finishedMigration);
|
await storage.onSuccess(finishedMigration);
|
||||||
await reporter.onMigrationSuccess?.(finishedMigration);
|
await reporter.onMigrationSuccess?.(finishedMigration);
|
||||||
|
finishedMigrations.push(finishedMigration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
|
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
|
||||||
|
|
||||||
const firstFailed = finishedMigrations.find((migration) => migration.status === 'failed');
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
|
const firstFailed = finishedMigrations.find(isFailedMigration);
|
||||||
const firstError =
|
const firstError =
|
||||||
firstFailed?.error instanceof EmigrateError
|
firstFailed?.error instanceof EmigrateError
|
||||||
? firstFailed.error
|
? firstFailed.error
|
||||||
: firstFailed
|
: firstFailed
|
||||||
? new MigrationRunError(`Failed to run migration: ${firstFailed.relativeFilePath}`, firstFailed, {
|
? MigrationRunError.fromMetadata(firstFailed)
|
||||||
cause: firstFailed?.error,
|
|
||||||
})
|
|
||||||
: undefined;
|
: undefined;
|
||||||
const error = unlockError ?? firstError ?? lockError;
|
const error = unlockError ?? firstError ?? lockError;
|
||||||
|
|
||||||
await reporter.onFinished?.(finishedMigrations, error);
|
await reporter.onFinished?.(finishedMigrations, error);
|
||||||
await storage.end();
|
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import { type LoaderPlugin } from '@emigrate/plugin-tools/types';
|
import { type LoaderPlugin } from '@emigrate/types';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
const promisifyIfNeeded = <T extends Function>(fn: T) => {
|
const promisifyIfNeeded = <T extends Function>(fn: T) => {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ import {
|
||||||
type EmigrateReporter,
|
type EmigrateReporter,
|
||||||
type ReporterInitParameters,
|
type ReporterInitParameters,
|
||||||
type Awaitable,
|
type Awaitable,
|
||||||
} from '@emigrate/plugin-tools/types';
|
} from '@emigrate/types';
|
||||||
import { EmigrateError } from '../errors.js';
|
|
||||||
|
|
||||||
type Status = ReturnType<typeof getMigrationStatus>;
|
type Status = ReturnType<typeof getMigrationStatus>;
|
||||||
|
|
||||||
|
|
@ -147,15 +146,14 @@ const getError = (error?: ErrorLike, indent = ' ') => {
|
||||||
others[property] = error[property as keyof ErrorLike];
|
others[property] = error[property as keyof ErrorLike];
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeString = typeof others['code'] === 'string' ? others['code'] : undefined;
|
const codeString =
|
||||||
|
typeof others['code'] === 'string' || typeof others['code'] === 'number' ? others['code'] : undefined;
|
||||||
const code = codeString ? ` [${codeString}]` : '';
|
const code = codeString ? ` [${codeString}]` : '';
|
||||||
|
|
||||||
const errorTitle = error.name
|
const errorTitle = error.name ? `${error.name}${code}: ${error.message}` : error.message;
|
||||||
? `${error.name}${codeString && !error.name.includes(codeString) ? code : ''}: ${error.message}`
|
|
||||||
: error.message;
|
|
||||||
const parts = [`${indent}${bold.red(errorTitle)}`, ...stack.map((line) => `${indent} ${dim(line.trim())}`)];
|
const parts = [`${indent}${bold.red(errorTitle)}`, ...stack.map((line) => `${indent} ${dim(line.trim())}`)];
|
||||||
|
|
||||||
if (properties.length > 0 && !(error instanceof EmigrateError)) {
|
if (properties.length > 0) {
|
||||||
parts.push(`${indent} ${JSON.stringify(others, undefined, 2).split('\n').join(`\n${indent} `)}`);
|
parts.push(`${indent} ${JSON.stringify(others, undefined, 2).split('\n').join(`\n${indent} `)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/plugin-tools/types';
|
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/types';
|
||||||
import { withLeadingPeriod } from './with-leading-period.js';
|
import { withLeadingPeriod } from './with-leading-period.js';
|
||||||
import { MigrationHistoryError } from './errors.js';
|
import { MigrationHistoryError } from './errors.js';
|
||||||
|
|
||||||
|
|
@ -8,7 +8,22 @@ export const toMigrationMetadata = (
|
||||||
{ cwd, directory }: { cwd: string; directory: string },
|
{ cwd, directory }: { cwd: string; directory: string },
|
||||||
): MigrationMetadataFinished => {
|
): MigrationMetadataFinished => {
|
||||||
const filePath = path.resolve(cwd, directory, entry.name);
|
const filePath = path.resolve(cwd, directory, entry.name);
|
||||||
const finishedMigration: MigrationMetadataFinished = {
|
|
||||||
|
if (entry.status === 'failed') {
|
||||||
|
return {
|
||||||
|
name: entry.name,
|
||||||
|
status: entry.status,
|
||||||
|
filePath,
|
||||||
|
relativeFilePath: path.relative(cwd, filePath),
|
||||||
|
extension: withLeadingPeriod(path.extname(entry.name)),
|
||||||
|
directory,
|
||||||
|
cwd,
|
||||||
|
duration: 0,
|
||||||
|
error: MigrationHistoryError.fromHistoryEntry(entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
status: entry.status,
|
status: entry.status,
|
||||||
filePath,
|
filePath,
|
||||||
|
|
@ -18,13 +33,4 @@ export const toMigrationMetadata = (
|
||||||
cwd,
|
cwd,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (entry.status === 'failed') {
|
|
||||||
finishedMigration.error = new MigrationHistoryError(
|
|
||||||
`Migration ${entry.name} is in a failed state, it should be fixed and removed`,
|
|
||||||
entry,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return finishedMigration;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { type EmigrateStorage, type Awaitable, type Plugin, type EmigrateReporter } from '@emigrate/plugin-tools/types';
|
import { type EmigrateStorage, type Awaitable, type Plugin, type EmigrateReporter } from '@emigrate/types';
|
||||||
|
|
||||||
export type EmigratePlugin = Plugin;
|
export type EmigratePlugin = Plugin;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emigrate/plugin-tools": "workspace:*",
|
"@emigrate/plugin-tools": "workspace:*",
|
||||||
|
"@emigrate/types": "workspace:*",
|
||||||
"mysql2": "3.6.5"
|
"mysql2": "3.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
type ResultSetHeader,
|
type ResultSetHeader,
|
||||||
type RowDataPacket,
|
type RowDataPacket,
|
||||||
} from 'mysql2/promise';
|
} from 'mysql2/promise';
|
||||||
|
import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools';
|
||||||
import {
|
import {
|
||||||
type MigrationMetadata,
|
type MigrationMetadata,
|
||||||
type EmigrateStorage,
|
type EmigrateStorage,
|
||||||
|
|
@ -19,8 +20,8 @@ import {
|
||||||
type MigrationMetadataFinished,
|
type MigrationMetadataFinished,
|
||||||
type GenerateMigrationFunction,
|
type GenerateMigrationFunction,
|
||||||
type GeneratorPlugin,
|
type GeneratorPlugin,
|
||||||
} from '@emigrate/plugin-tools/types';
|
type SerializedError,
|
||||||
import { getTimestampPrefix, sanitizeMigrationName, serializeError } from '@emigrate/plugin-tools';
|
} from '@emigrate/types';
|
||||||
|
|
||||||
const defaultTable = 'migrations';
|
const defaultTable = 'migrations';
|
||||||
|
|
||||||
|
|
@ -87,7 +88,7 @@ type HistoryEntry = {
|
||||||
name: string;
|
name: string;
|
||||||
status: MigrationStatus;
|
status: MigrationStatus;
|
||||||
date: Date;
|
date: Date;
|
||||||
error?: unknown;
|
error?: SerializedError;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lockMigration = async (pool: Pool, table: string, migration: MigrationMetadata) => {
|
const lockMigration = async (pool: Pool, table: string, migration: MigrationMetadata) => {
|
||||||
|
|
@ -117,7 +118,12 @@ const unlockMigration = async (pool: Pool, table: string, migration: MigrationMe
|
||||||
return result.affectedRows === 1;
|
return result.affectedRows === 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishMigration = async (pool: Pool, table: string, migration: MigrationMetadataFinished) => {
|
const finishMigration = async (
|
||||||
|
pool: Pool,
|
||||||
|
table: string,
|
||||||
|
migration: MigrationMetadataFinished,
|
||||||
|
_error?: SerializedError,
|
||||||
|
) => {
|
||||||
const [result] = await pool.execute<ResultSetHeader>({
|
const [result] = await pool.execute<ResultSetHeader>({
|
||||||
sql: `
|
sql: `
|
||||||
UPDATE
|
UPDATE
|
||||||
|
|
@ -208,12 +214,20 @@ export const createMysqlStorage = ({ table = defaultTable, connection }: MysqlSt
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
if (row.status === 'failed') {
|
||||||
|
yield {
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
date: new Date(row.date),
|
||||||
|
error: row.error ?? { name: 'Error', message: 'Unknown error' },
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
name: row.name,
|
name: row.name,
|
||||||
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
|
|
||||||
error: row.status === 'failed' ? serializeError(new Error('Unknown error reason')) : undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -221,7 +235,7 @@ export const createMysqlStorage = ({ table = defaultTable, connection }: MysqlSt
|
||||||
await finishMigration(pool, table, migration);
|
await finishMigration(pool, table, migration);
|
||||||
},
|
},
|
||||||
async onError(migration, error) {
|
async onError(migration, error) {
|
||||||
await finishMigration(pool, table, { ...migration, status: 'failed', error });
|
await finishMigration(pool, table, migration, error);
|
||||||
},
|
},
|
||||||
async end() {
|
async end() {
|
||||||
await pool.end();
|
await pool.end();
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@
|
||||||
"bugs": "https://github.com/aboviq/emigrate/issues",
|
"bugs": "https://github.com/aboviq/emigrate/issues",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emigrate/plugin-tools": "workspace:*"
|
"@emigrate/plugin-tools": "workspace:*",
|
||||||
|
"@emigrate/types": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emigrate/tsconfig": "workspace:*"
|
"@emigrate/tsconfig": "workspace:*"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools';
|
import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools';
|
||||||
import { type GenerateMigrationFunction } from '@emigrate/plugin-tools/types';
|
import { type GenerateMigrationFunction } from '@emigrate/types';
|
||||||
|
|
||||||
export const generateMigration: GenerateMigrationFunction = async (name) => {
|
export const generateMigration: GenerateMigrationFunction = async (name) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,6 @@
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts"
|
"types": "./dist/index.d.ts"
|
||||||
},
|
|
||||||
"./types": {
|
|
||||||
"import": "./dist/types.js",
|
|
||||||
"types": "./dist/types.d.ts"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
@ -43,6 +39,7 @@
|
||||||
"@emigrate/tsconfig": "workspace:*"
|
"@emigrate/tsconfig": "workspace:*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emigrate/types": "workspace:*",
|
||||||
"import-from-esm": "1.3.3"
|
"import-from-esm": "1.3.3"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,7 @@ import {
|
||||||
type EmigrateStorage,
|
type EmigrateStorage,
|
||||||
type LoaderPlugin,
|
type LoaderPlugin,
|
||||||
type StringOrModule,
|
type StringOrModule,
|
||||||
type SerializedError,
|
} from '@emigrate/types';
|
||||||
} from './types.js';
|
|
||||||
|
|
||||||
export const serializeError = (error: Error): SerializedError => {
|
|
||||||
const properties: Record<string, unknown> = {
|
|
||||||
name: error.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 => {
|
||||||
if (!plugin || typeof plugin !== 'object') {
|
if (!plugin || typeof plugin !== 'object') {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
"bugs": "https://github.com/aboviq/emigrate/issues",
|
"bugs": "https://github.com/aboviq/emigrate/issues",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emigrate/plugin-tools": "workspace:*",
|
"@emigrate/types": "workspace:*",
|
||||||
"pino": "8.16.2"
|
"pino": "8.16.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
type MigrationMetadataFinished,
|
type MigrationMetadataFinished,
|
||||||
type ReporterInitParameters,
|
type ReporterInitParameters,
|
||||||
type EmigrateReporter,
|
type EmigrateReporter,
|
||||||
} from '@emigrate/plugin-tools/types';
|
} from '@emigrate/types';
|
||||||
|
|
||||||
type PinoReporterOptions = {
|
type PinoReporterOptions = {
|
||||||
level?: string;
|
level?: string;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
"bugs": "https://github.com/aboviq/emigrate/issues",
|
"bugs": "https://github.com/aboviq/emigrate/issues",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emigrate/plugin-tools": "workspace:*"
|
"@emigrate/types": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emigrate/tsconfig": "workspace:*"
|
"@emigrate/tsconfig": "workspace:*"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +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/types';
|
||||||
import { type SerializedError, type EmigrateStorage, type MigrationStatus } from '@emigrate/plugin-tools/types';
|
|
||||||
|
|
||||||
export type StorageFsOptions = {
|
export type StorageFsOptions = {
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|
@ -27,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();
|
||||||
|
|
||||||
|
|
@ -36,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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -99,11 +98,20 @@ export default function storageFs({ filename }: StorageFsOptions): EmigrateStora
|
||||||
const history = await read();
|
const history = await read();
|
||||||
|
|
||||||
for (const [name, { status, date, error }] of Object.entries(history)) {
|
for (const [name, { status, date, error }] of Object.entries(history)) {
|
||||||
|
if (status === 'failed') {
|
||||||
|
yield {
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
date: new Date(date),
|
||||||
|
error: error ?? { name: 'Error', message: 'Unknown error' },
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
date: new Date(date),
|
date: new Date(date),
|
||||||
error,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
82
packages/types/CHANGELOG.md
Normal file
82
packages/types/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# @emigrate/types
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- bad4e25: Pass the Emigrate CLI's version number to reporters
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- a79f8e8: 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
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- a79f8e8: Serialization of errors now happens inside storage plugins because it makes more sense and the types are easier to work with this way
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 703e6f0: Add "end" method to storage plugins so they can cleanup resources when a command is finished
|
||||||
|
- c1d5597: Add serializeError utility function for serializing Error instances
|
||||||
|
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 20ed2e8: Try importing plugins (and reporters) using prefixes before importing without, this is to avoid issue with accidentaly importing other non-emigrate related packages. E.g. setting the reporter to "pino" would import the "pino" package without this fix and will import "@emigrate/reporter-pino" with this fix.
|
||||||
|
- d916043: Fix a regression issue where plugins wasn't correctly loaded if specified as strings
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 5e8572b: Pass the current command to the reporter
|
||||||
|
- 8e87ade: Move storages and reporters out from the plugin option into their own separate options (i.e. "--reporter" and "--storage" respectively). This makes it easier to change the interfaces of storages and reporters, and it's also more similar to other tools.
|
||||||
|
- 672fae1: Include "@emigrate/" in the plugin prefix list, i.e. when searching for the plugin "blaha" it will look for the packages "blaha", "@emigrate/blaha", "@emigrate/plugin-blaha" and "emigrate-plugin-blaha" and use the first of them that exists
|
||||||
|
- d8a6a24: Implement the "remove" command for removing migration entries from the history
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 60ae3b8: Fix loading of lazy loaded plugins with default exports
|
||||||
|
- acb0b4f: Keep upper cased letters in migration file names by default
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 8f35812: Add support for "reporter" plugins and implement a simple default reporter
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 23a323c: Add the convenience functions `getOrLoadPlugin` and `getOrLoadPlugins`
|
||||||
|
- 62bd5a4: Add more properties to the MigrationMetadata type to ease writing "loader" plugins
|
||||||
|
- 81fde2e: Prepare for supporting "loader" plugins. A loader plugin is used to transform a migration file of a given type (file extension) to a function that will execute the actual migration.
|
||||||
|
- 9f5abf7: Simplify plugin interfaces by getting rid of the "type" string, in preparation for having packages that contains multiple different plugins
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 1799b6e: Add missing types and utility methods related to the new "loader" plugins
|
||||||
|
- 3e0ff07: Specify files to include in published NPM package
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 50fce0a: Add some simple README's for each package and add homepage, repository and bugs URLs to each package.json file
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- cdafd05: First version of the @emigrate/plugin-tools package which contains some nice to have utilities when building and using Emigrate plugins
|
||||||
|
- 9c239e0: Use import-from-esm to resolve plugins relative to the current working directory and add a convenient plugin loader helper (loadPlugin)
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 1634094: Remove double and trailing underscores in sanitized filenames and lower case the result for consistent filenames
|
||||||
3
packages/types/README.md
Normal file
3
packages/types/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# @emigrate/plugin-tools
|
||||||
|
|
||||||
|
This package contains utilities and types to simplify creating and working with Emigrate plugins.
|
||||||
42
packages/types/package.json
Normal file
42
packages/types/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "@emigrate/types",
|
||||||
|
"version": "0.7.0",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"description": "Common Emigrate TypeScript types to ease plugin development.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.js",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --pretty",
|
||||||
|
"build:watch": "tsc --pretty --watch",
|
||||||
|
"lint": "xo --cwd=../.. $(pwd)"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"emigrate",
|
||||||
|
"typescript",
|
||||||
|
"migrations",
|
||||||
|
"types"
|
||||||
|
],
|
||||||
|
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
|
||||||
|
"homepage": "https://github.com/aboviq/emigrate/tree/main/packages/types#readme",
|
||||||
|
"repository": "https://github.com/aboviq/emigrate/tree/main/packages/types",
|
||||||
|
"bugs": "https://github.com/aboviq/emigrate/issues",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@emigrate/tsconfig": "workspace:*"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"extends": "../../package.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,21 +4,23 @@ 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 = Record<string, unknown>;
|
||||||
[key: string]: unknown;
|
|
||||||
name?: string;
|
export type FailedMigrationHistoryEntry = {
|
||||||
message: string;
|
name: string;
|
||||||
stack?: string;
|
status: 'failed';
|
||||||
cause?: unknown;
|
date: Date;
|
||||||
|
error: SerializedError;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MigrationHistoryEntry = {
|
export type NonFailedMigrationHistoryEntry = {
|
||||||
name: string;
|
name: string;
|
||||||
status: MigrationStatus;
|
status: Exclude<MigrationStatus, 'failed'>;
|
||||||
date: Date;
|
date: Date;
|
||||||
error?: SerializedError;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MigrationHistoryEntry = FailedMigrationHistoryEntry | NonFailedMigrationHistoryEntry;
|
||||||
|
|
||||||
export type Storage = {
|
export type Storage = {
|
||||||
/**
|
/**
|
||||||
* Acquire a lock on the given migrations.
|
* Acquire a lock on the given migrations.
|
||||||
|
|
@ -71,10 +73,13 @@ export type Storage = {
|
||||||
/**
|
/**
|
||||||
* Called when a migration has failed.
|
* Called when a migration has failed.
|
||||||
*
|
*
|
||||||
|
* The passed error will be serialized so it's easily storable it in the history.
|
||||||
|
* If the original Error instance is needed it's available as the `error` property on the finished migration.
|
||||||
|
*
|
||||||
* @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. Serialized for easy storage.
|
||||||
*/
|
*/
|
||||||
onError(migration: MigrationMetadataFinished, error: Error): Promise<void>;
|
onError(migration: MigrationMetadataFinished, error: SerializedError): 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).
|
||||||
*
|
*
|
||||||
|
|
@ -153,18 +158,38 @@ export type MigrationMetadata = {
|
||||||
extension: string;
|
extension: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MigrationMetadataFinished = MigrationMetadata & {
|
export type FailedMigrationMetadata = MigrationMetadata & {
|
||||||
status: MigrationStatus | 'skipped';
|
status: 'failed';
|
||||||
duration: number;
|
duration: number;
|
||||||
error?: Error;
|
error: Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SkippedMigrationMetadata = MigrationMetadata & {
|
||||||
|
status: 'skipped' | 'pending';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SuccessfulMigrationMetadata = MigrationMetadata & {
|
||||||
|
status: 'done';
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MigrationMetadataFinished =
|
||||||
|
| FailedMigrationMetadata
|
||||||
|
| SkippedMigrationMetadata
|
||||||
|
| SuccessfulMigrationMetadata;
|
||||||
|
|
||||||
export const isFinishedMigration = (
|
export const isFinishedMigration = (
|
||||||
migration: MigrationMetadata | MigrationMetadataFinished,
|
migration: MigrationMetadata | MigrationMetadataFinished,
|
||||||
): migration is MigrationMetadataFinished => {
|
): migration is MigrationMetadataFinished => {
|
||||||
return 'status' in migration;
|
return 'status' in migration;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFailedMigration = (
|
||||||
|
migration: MigrationMetadata | MigrationMetadataFinished,
|
||||||
|
): migration is FailedMigrationMetadata => {
|
||||||
|
return 'status' in migration && migration.status === 'failed';
|
||||||
|
};
|
||||||
|
|
||||||
export type LoaderPlugin = {
|
export type LoaderPlugin = {
|
||||||
/**
|
/**
|
||||||
* The file extensions that this plugin can load.
|
* The file extensions that this plugin can load.
|
||||||
|
|
@ -243,13 +268,13 @@ export type EmigrateReporter = Partial<{
|
||||||
*
|
*
|
||||||
* This is only called when the command is 'remove'.
|
* This is only called when the command is 'remove'.
|
||||||
*/
|
*/
|
||||||
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void>;
|
onMigrationRemoveSuccess(migration: SuccessfulMigrationMetadata): Awaitable<void>;
|
||||||
/**
|
/**
|
||||||
* Called when a migration couldn't be removed from the migration history.
|
* Called when a migration couldn't be removed from the migration history.
|
||||||
*
|
*
|
||||||
* This is only called when the command is 'remove'.
|
* This is only called when the command is 'remove'.
|
||||||
*/
|
*/
|
||||||
onMigrationRemoveError(migration: MigrationMetadataFinished, error: Error): Awaitable<void>;
|
onMigrationRemoveError(migration: FailedMigrationMetadata, error: Error): Awaitable<void>;
|
||||||
/**
|
/**
|
||||||
* Called when a migration is about to be executed.
|
* Called when a migration is about to be executed.
|
||||||
*
|
*
|
||||||
|
|
@ -266,7 +291,7 @@ export type EmigrateReporter = Partial<{
|
||||||
*
|
*
|
||||||
* @param migration Information about the migration that was executed.
|
* @param migration Information about the migration that was executed.
|
||||||
*/
|
*/
|
||||||
onMigrationSuccess(migration: MigrationMetadataFinished): Awaitable<void>;
|
onMigrationSuccess(migration: SuccessfulMigrationMetadata): Awaitable<void>;
|
||||||
/**
|
/**
|
||||||
* Called when a migration has failed.
|
* Called when a migration has failed.
|
||||||
*
|
*
|
||||||
|
|
@ -276,7 +301,7 @@ export type EmigrateReporter = Partial<{
|
||||||
* @param migration Information about the migration that failed.
|
* @param migration Information about the migration that failed.
|
||||||
* @param error The error that caused the migration to fail.
|
* @param error The error that caused the migration to fail.
|
||||||
*/
|
*/
|
||||||
onMigrationError(migration: MigrationMetadataFinished, error: Error): Awaitable<void>;
|
onMigrationError(migration: FailedMigrationMetadata, error: Error): Awaitable<void>;
|
||||||
/**
|
/**
|
||||||
* Called when a migration is skipped
|
* Called when a migration is skipped
|
||||||
*
|
*
|
||||||
|
|
@ -288,7 +313,7 @@ export type EmigrateReporter = Partial<{
|
||||||
*
|
*
|
||||||
* @param migration Information about the migration that was skipped.
|
* @param migration Information about the migration that was skipped.
|
||||||
*/
|
*/
|
||||||
onMigrationSkip(migration: MigrationMetadataFinished): Awaitable<void>;
|
onMigrationSkip(migration: SkippedMigrationMetadata): Awaitable<void>;
|
||||||
/**
|
/**
|
||||||
* Called as a final step after all migrations have been executed, listed or removed.
|
* Called as a final step after all migrations have been executed, listed or removed.
|
||||||
*
|
*
|
||||||
8
packages/types/tsconfig.json
Normal file
8
packages/types/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
|
|
@ -56,6 +56,9 @@ importers:
|
||||||
'@emigrate/plugin-tools':
|
'@emigrate/plugin-tools':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../plugin-tools
|
version: link:../plugin-tools
|
||||||
|
'@emigrate/types':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../types
|
||||||
ansis:
|
ansis:
|
||||||
specifier: 2.0.2
|
specifier: 2.0.2
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
|
|
@ -77,6 +80,9 @@ importers:
|
||||||
pretty-ms:
|
pretty-ms:
|
||||||
specifier: 8.0.0
|
specifier: 8.0.0
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
|
serialize-error:
|
||||||
|
specifier: 11.0.3
|
||||||
|
version: 11.0.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@emigrate/tsconfig':
|
'@emigrate/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
|
@ -87,6 +93,9 @@ importers:
|
||||||
'@emigrate/plugin-tools':
|
'@emigrate/plugin-tools':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../plugin-tools
|
version: link:../plugin-tools
|
||||||
|
'@emigrate/types':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../types
|
||||||
mysql2:
|
mysql2:
|
||||||
specifier: 3.6.5
|
specifier: 3.6.5
|
||||||
version: 3.6.5
|
version: 3.6.5
|
||||||
|
|
@ -100,6 +109,9 @@ importers:
|
||||||
'@emigrate/plugin-tools':
|
'@emigrate/plugin-tools':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../plugin-tools
|
version: link:../plugin-tools
|
||||||
|
'@emigrate/types':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../types
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@emigrate/tsconfig':
|
'@emigrate/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
|
@ -107,6 +119,9 @@ importers:
|
||||||
|
|
||||||
packages/plugin-tools:
|
packages/plugin-tools:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@emigrate/types':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../types
|
||||||
import-from-esm:
|
import-from-esm:
|
||||||
specifier: 1.3.3
|
specifier: 1.3.3
|
||||||
version: 1.3.3
|
version: 1.3.3
|
||||||
|
|
@ -117,9 +132,9 @@ importers:
|
||||||
|
|
||||||
packages/reporter-pino:
|
packages/reporter-pino:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emigrate/plugin-tools':
|
'@emigrate/types':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../plugin-tools
|
version: link:../types
|
||||||
pino:
|
pino:
|
||||||
specifier: 8.16.2
|
specifier: 8.16.2
|
||||||
version: 8.16.2
|
version: 8.16.2
|
||||||
|
|
@ -130,9 +145,9 @@ importers:
|
||||||
|
|
||||||
packages/storage-fs:
|
packages/storage-fs:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emigrate/plugin-tools':
|
'@emigrate/types':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../plugin-tools
|
version: link:../types
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@emigrate/tsconfig':
|
'@emigrate/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
|
@ -140,6 +155,12 @@ importers:
|
||||||
|
|
||||||
packages/tsconfig: {}
|
packages/tsconfig: {}
|
||||||
|
|
||||||
|
packages/types:
|
||||||
|
devDependencies:
|
||||||
|
'@emigrate/tsconfig':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../tsconfig
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
/@aashutoshrathi/word-wrap@1.2.6:
|
/@aashutoshrathi/word-wrap@1.2.6:
|
||||||
|
|
@ -4919,6 +4940,13 @@ packages:
|
||||||
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
|
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/serialize-error@11.0.3:
|
||||||
|
resolution: {integrity: sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==}
|
||||||
|
engines: {node: '>=14.16'}
|
||||||
|
dependencies:
|
||||||
|
type-fest: 2.19.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/serialize-javascript@6.0.1:
|
/serialize-javascript@6.0.1:
|
||||||
resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==}
|
resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue