@emigrate/cli
Advanced tools
Comparing version 0.6.0 to 0.7.0
import { type Config } from '../types.js'; | ||
export default function listCommand({ directory, reporter: reporterConfig, storage: storageConfig }: Config): Promise<1 | 0>; | ||
export default function listCommand({ directory, reporter: reporterConfig, storage: storageConfig }: Config): Promise<0 | 1>; | ||
//# sourceMappingURL=list.d.ts.map |
import process from 'node:process'; | ||
import path from 'node:path'; | ||
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools'; | ||
import { BadOptionError, MigrationHistoryError, MissingOptionError, StorageInitError } from '../errors.js'; | ||
import { withLeadingPeriod } from '../with-leading-period.js'; | ||
import { getMigrations } from '../get-migrations.js'; | ||
import { BadOptionError, MissingOptionError, StorageInitError } from '../errors.js'; | ||
import { exec } from '../exec.js'; | ||
import { migrationRunner } from '../migration-runner.js'; | ||
import { arrayFromAsync } from '../array-from-async.js'; | ||
import { collectMigrations } from '../collect-migrations.js'; | ||
const lazyDefaultReporter = async () => import('../reporters/default.js'); | ||
@@ -28,41 +28,17 @@ export default async function listCommand({ directory, reporter: reporterConfig, storage: storageConfig }) { | ||
} | ||
const migrationFiles = await getMigrations(cwd, directory); | ||
let migrationHistoryError; | ||
const finishedMigrations = []; | ||
for await (const migrationHistoryEntry of storage.getHistory()) { | ||
const index = migrationFiles.findIndex((migrationFile) => migrationFile.name === migrationHistoryEntry.name); | ||
if (index === -1) { | ||
// Only care about entries that exists in the current migration directory | ||
continue; | ||
} | ||
const filePath = path.resolve(cwd, directory, migrationHistoryEntry.name); | ||
const finishedMigration = { | ||
name: migrationHistoryEntry.name, | ||
status: migrationHistoryEntry.status, | ||
filePath, | ||
relativeFilePath: path.relative(cwd, filePath), | ||
extension: withLeadingPeriod(path.extname(migrationHistoryEntry.name)), | ||
directory, | ||
cwd, | ||
duration: 0, | ||
}; | ||
if (migrationHistoryEntry.status === 'failed') { | ||
migrationHistoryError = new MigrationHistoryError(`Migration ${migrationHistoryEntry.name} is in a failed state`, migrationHistoryEntry); | ||
await reporter.onMigrationError?.(finishedMigration, migrationHistoryError); | ||
} | ||
else { | ||
await reporter.onMigrationSuccess?.(finishedMigration); | ||
} | ||
finishedMigrations.push(finishedMigration); | ||
migrationFiles.splice(index, 1); | ||
} | ||
for await (const migration of migrationFiles) { | ||
const finishedMigration = { ...migration, status: 'pending', duration: 0 }; | ||
await reporter.onMigrationSkip?.(finishedMigration); | ||
finishedMigrations.push(finishedMigration); | ||
} | ||
await reporter.onFinished?.(finishedMigrations, migrationHistoryError); | ||
await storage.end(); | ||
return migrationHistoryError ? 1 : 0; | ||
const collectedMigrations = collectMigrations(cwd, directory, storage.getHistory()); | ||
const error = await migrationRunner({ | ||
dry: true, | ||
reporter, | ||
storage, | ||
migrations: await arrayFromAsync(collectedMigrations), | ||
async validate() { | ||
// No-op | ||
}, | ||
async execute() { | ||
throw new Error('Unexpected execute call'); | ||
}, | ||
}); | ||
return error ? 1 : 0; | ||
} | ||
//# sourceMappingURL=list.js.map |
@@ -5,4 +5,4 @@ import { type Config } from '../types.js'; | ||
}; | ||
export default function removeCommand({ directory, reporter: reporterConfig, storage: storageConfig, force }: Config & ExtraFlags, name: string): Promise<1 | 0>; | ||
export default function removeCommand({ directory, reporter: reporterConfig, storage: storageConfig, force }: Config & ExtraFlags, name: string): Promise<0 | 1>; | ||
export {}; | ||
//# sourceMappingURL=remove.d.ts.map |
@@ -1,12 +0,14 @@ | ||
import path from 'node:path'; | ||
import process from 'node:process'; | ||
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage, serializeError } from '@emigrate/plugin-tools'; | ||
import { BadOptionError, EmigrateError, MigrationHistoryError, MigrationLoadError, MigrationRunError, MissingOptionError, StorageInitError, toError, } from '../errors.js'; | ||
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools'; | ||
import { isFinishedMigration } from '@emigrate/plugin-tools/types'; | ||
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError } from '../errors.js'; | ||
import { withLeadingPeriod } from '../with-leading-period.js'; | ||
import { getMigrations as getMigrationsOriginal } from '../get-migrations.js'; | ||
import { getDuration } from '../get-duration.js'; | ||
import { exec } from '../exec.js'; | ||
import { migrationRunner } from '../migration-runner.js'; | ||
import { filterAsync } from '../filter-async.js'; | ||
import { collectMigrations } from '../collect-migrations.js'; | ||
import { arrayFromAsync } from '../array-from-async.js'; | ||
const lazyDefaultReporter = async () => import('../reporters/default.js'); | ||
const lazyPluginLoaderJs = async () => import('../plugin-loader-js.js'); | ||
export default async function upCommand({ storage: storageConfig, reporter: reporterConfig, directory, dry = false, plugins = [], cwd = process.cwd(), getMigrations = getMigrationsOriginal, }) { | ||
export default async function upCommand({ storage: storageConfig, reporter: reporterConfig, directory, dry = false, plugins = [], cwd = process.cwd(), getMigrations, }) { | ||
if (!directory) { | ||
@@ -29,166 +31,36 @@ throw new MissingOptionError('directory'); | ||
} | ||
const migrationFiles = await getMigrations(cwd, directory); | ||
const failedEntries = []; | ||
for await (const migrationHistoryEntry of storage.getHistory()) { | ||
const index = migrationFiles.findIndex((migrationFile) => migrationFile.name === migrationHistoryEntry.name); | ||
if (index === -1) { | ||
// Only care about entries that exists in the current migration directory | ||
continue; | ||
} | ||
if (migrationHistoryEntry.status === 'failed') { | ||
const filePath = path.resolve(cwd, directory, migrationHistoryEntry.name); | ||
const finishedMigration = { | ||
name: migrationHistoryEntry.name, | ||
status: migrationHistoryEntry.status, | ||
filePath, | ||
relativeFilePath: path.relative(cwd, filePath), | ||
extension: withLeadingPeriod(path.extname(migrationHistoryEntry.name)), | ||
error: new MigrationHistoryError(`Migration ${migrationHistoryEntry.name} is in a failed state, please fix and remove it first`, migrationHistoryEntry), | ||
directory, | ||
cwd, | ||
duration: 0, | ||
}; | ||
failedEntries.push(finishedMigration); | ||
} | ||
migrationFiles.splice(index, 1); | ||
} | ||
const migrationFileExtensions = new Set(migrationFiles.map((migration) => migration.extension)); | ||
const collectedMigrations = filterAsync(collectMigrations(cwd, directory, storage.getHistory(), getMigrations), (migration) => !isFinishedMigration(migration) || migration.status === 'failed'); | ||
const loaderPlugins = await getOrLoadPlugins('loader', [lazyPluginLoaderJs, ...plugins]); | ||
const loaderByExtension = new Map([...migrationFileExtensions].map((extension) => [ | ||
extension, | ||
loaderPlugins.find((plugin) => plugin.loadableExtensions.some((loadableExtension) => withLeadingPeriod(loadableExtension) === extension)), | ||
])); | ||
for await (const [extension, loader] of loaderByExtension) { | ||
if (!loader) { | ||
const finishedMigrations = [...failedEntries]; | ||
for await (const failedEntry of failedEntries) { | ||
await reporter.onMigrationError?.(failedEntry, failedEntry.error); | ||
} | ||
for await (const migration of migrationFiles) { | ||
if (migration.extension === extension) { | ||
const error = new BadOptionError('plugin', `No loader plugin found for file extension: ${extension}`); | ||
const finishedMigration = { ...migration, duration: 0, status: 'failed', error }; | ||
await reporter.onMigrationError?.(finishedMigration, error); | ||
finishedMigrations.push(finishedMigration); | ||
} | ||
else { | ||
const finishedMigration = { ...migration, duration: 0, status: 'skipped' }; | ||
await reporter.onMigrationSkip?.(finishedMigration); | ||
finishedMigrations.push(finishedMigration); | ||
} | ||
} | ||
await reporter.onFinished?.(finishedMigrations, new BadOptionError('plugin', `No loader plugin found for file extension: ${extension}`)); | ||
await storage.end(); | ||
return 1; | ||
const loaderByExtension = new Map(); | ||
const getLoaderByExtension = (extension) => { | ||
if (!loaderByExtension.has(extension)) { | ||
const loader = loaderPlugins.find((plugin) => plugin.loadableExtensions.some((loadableExtension) => withLeadingPeriod(loadableExtension) === extension)); | ||
loaderByExtension.set(extension, loader); | ||
} | ||
} | ||
await reporter.onCollectedMigrations?.([...failedEntries, ...migrationFiles]); | ||
if (migrationFiles.length === 0 || dry || failedEntries.length > 0) { | ||
const error = failedEntries.find((migration) => migration.status === 'failed')?.error; | ||
await reporter.onLockedMigrations?.(migrationFiles); | ||
const finishedMigrations = migrationFiles.map((migration) => ({ | ||
...migration, | ||
duration: 0, | ||
status: 'pending', | ||
})); | ||
for await (const failedMigration of failedEntries) { | ||
await reporter.onMigrationError?.(failedMigration, failedMigration.error); | ||
} | ||
for await (const migration of finishedMigrations) { | ||
await reporter.onMigrationSkip?.(migration); | ||
} | ||
await reporter.onFinished?.([...failedEntries, ...finishedMigrations], error); | ||
await storage.end(); | ||
return failedEntries.length > 0 ? 1 : 0; | ||
} | ||
let lockedMigrationFiles = []; | ||
try { | ||
lockedMigrationFiles = (await storage.lock(migrationFiles)) ?? []; | ||
await reporter.onLockedMigrations?.(lockedMigrationFiles); | ||
} | ||
catch (error) { | ||
for await (const migration of migrationFiles) { | ||
await reporter.onMigrationSkip?.({ ...migration, duration: 0, status: 'skipped' }); | ||
} | ||
await reporter.onFinished?.([], toError(error)); | ||
await storage.end(); | ||
return 1; | ||
} | ||
const nonLockedMigrations = migrationFiles.filter((migration) => !lockedMigrationFiles.includes(migration)); | ||
for await (const migration of nonLockedMigrations) { | ||
await reporter.onMigrationSkip?.({ ...migration, duration: 0, status: 'skipped' }); | ||
} | ||
let cleaningUp; | ||
const cleanup = async () => { | ||
if (cleaningUp) { | ||
return cleaningUp; | ||
} | ||
process.off('SIGINT', cleanup); | ||
process.off('SIGTERM', cleanup); | ||
cleaningUp = storage.unlock(lockedMigrationFiles).then(async () => storage.end()); | ||
return cleaningUp; | ||
return loaderByExtension.get(extension); | ||
}; | ||
process.on('SIGINT', cleanup); | ||
process.on('SIGTERM', cleanup); | ||
const finishedMigrations = []; | ||
try { | ||
for await (const migration of lockedMigrationFiles) { | ||
const lastMigrationStatus = finishedMigrations.at(-1)?.status; | ||
if (lastMigrationStatus === 'failed' || lastMigrationStatus === 'skipped') { | ||
const finishedMigration = { ...migration, status: 'skipped', duration: 0 }; | ||
await reporter.onMigrationSkip?.(finishedMigration); | ||
finishedMigrations.push(finishedMigration); | ||
continue; | ||
const error = await migrationRunner({ | ||
dry, | ||
reporter, | ||
storage, | ||
migrations: await arrayFromAsync(collectedMigrations), | ||
async validate(migration) { | ||
const loader = getLoaderByExtension(migration.extension); | ||
if (!loader) { | ||
throw new BadOptionError('plugin', `No loader plugin found for file extension: ${migration.extension}`); | ||
} | ||
await reporter.onMigrationStart?.(migration); | ||
const loader = loaderByExtension.get(migration.extension); | ||
const start = process.hrtime(); | ||
let migrationFunction; | ||
try { | ||
try { | ||
migrationFunction = await loader.loadMigration(migration); | ||
} | ||
catch (error) { | ||
throw new MigrationLoadError(`Failed to load migration file: ${migration.relativeFilePath}`, migration, { | ||
cause: error, | ||
}); | ||
} | ||
await migrationFunction(); | ||
const duration = getDuration(start); | ||
const finishedMigration = { ...migration, status: 'done', duration }; | ||
await storage.onSuccess(finishedMigration); | ||
await reporter.onMigrationSuccess?.(finishedMigration); | ||
finishedMigrations.push(finishedMigration); | ||
}, | ||
async execute(migration) { | ||
const loader = getLoaderByExtension(migration.extension); | ||
const [migrationFunction, loadError] = await exec(async () => loader.loadMigration(migration)); | ||
if (loadError) { | ||
throw new MigrationLoadError(`Failed to load migration file: ${migration.relativeFilePath}`, migration, { | ||
cause: loadError, | ||
}); | ||
} | ||
catch (error) { | ||
const errorInstance = toError(error); | ||
const serializedError = serializeError(errorInstance); | ||
const duration = getDuration(start); | ||
const finishedMigration = { | ||
...migration, | ||
status: 'failed', | ||
duration, | ||
error: serializedError, | ||
}; | ||
await storage.onError(finishedMigration, serializedError); | ||
await reporter.onMigrationError?.(finishedMigration, errorInstance); | ||
finishedMigrations.push(finishedMigration); | ||
} | ||
} | ||
const firstFailed = finishedMigrations.find((migration) => migration.status === 'failed'); | ||
return firstFailed ? 1 : 0; | ||
} | ||
finally { | ||
const firstFailed = finishedMigrations.find((migration) => migration.status === 'failed'); | ||
const firstError = firstFailed?.error instanceof EmigrateError | ||
? firstFailed.error | ||
: firstFailed | ||
? new MigrationRunError(`Failed to run migration: ${firstFailed.relativeFilePath}`, firstFailed, { | ||
cause: firstFailed?.error, | ||
}) | ||
: undefined; | ||
await cleanup(); | ||
await reporter.onFinished?.(finishedMigrations, firstError); | ||
} | ||
await migrationFunction(); | ||
}, | ||
}); | ||
return error ? 1 : 0; | ||
} | ||
//# sourceMappingURL=up.js.map |
import { describe, it, mock } from 'node:test'; | ||
import assert from 'node:assert'; | ||
import path from 'node:path'; | ||
import { serializeError } from '@emigrate/plugin-tools'; | ||
import upCommand from './up.js'; | ||
@@ -33,4 +34,4 @@ describe('up', () => { | ||
assert.strictEqual(reporter.onInit.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0); | ||
@@ -44,4 +45,4 @@ assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0); | ||
const error = args[1]; | ||
assert.deepStrictEqual(entries.map((entry) => `${entry.name} (${entry.status})`), ['some_other.js (skipped)', 'some_file.sql (failed)']); | ||
assert.strictEqual(entries.length, 2); | ||
assert.deepStrictEqual(entries.map((entry) => `${entry.name} (${entry.status})`), ['some_other.js (skipped)', 'some_file.sql (failed)']); | ||
assert.strictEqual(error?.message, 'No loader plugin found for file extension: .sql'); | ||
@@ -54,4 +55,4 @@ }); | ||
assert.strictEqual(reporter.onInit.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0); | ||
@@ -71,3 +72,3 @@ assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0); | ||
const failedEntry = toEntry('some_failed_migration.js', 'failed'); | ||
const { reporter, run } = getUpCommand([failedEntry.name], getStorage([failedEntry])); | ||
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry])); | ||
const exitCode = await run(); | ||
@@ -89,13 +90,61 @@ assert.strictEqual(exitCode, 1); | ||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1); | ||
assert.strictEqual(getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]), failedEntry.error); | ||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1); | ||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? []; | ||
assert.strictEqual(error?.message, `Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`); | ||
assert.strictEqual(getErrorCause(error), failedEntry.error); | ||
assert.strictEqual(entries?.length, 2); | ||
assert.deepStrictEqual(entries.map((entry) => `${entry.name} (${entry.status})`), ['some_failed_migration.js (failed)', 'some_file.js (skipped)']); | ||
}); | ||
it('returns 1 and finishes with an error when there are failed migrations in the history in dry-run mode as well', async () => { | ||
const failedEntry = toEntry('some_failed_migration.js', 'failed'); | ||
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry])); | ||
const exitCode = await run(true); | ||
assert.strictEqual(exitCode, 1); | ||
assert.strictEqual(reporter.onInit.mock.calls.length, 1); | ||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [ | ||
{ | ||
command: 'up', | ||
cwd: '/emigrate', | ||
dry: true, | ||
directory: 'migrations', | ||
}, | ||
]); | ||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1); | ||
assert.strictEqual(getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]), failedEntry.error); | ||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1); | ||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? []; | ||
assert.strictEqual(error?.message, `Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`); | ||
assert.strictEqual(getErrorCause(error), failedEntry.error); | ||
assert.strictEqual(entries?.length, 2); | ||
assert.deepStrictEqual(entries.map((entry) => `${entry.name} (${entry.status})`), ['some_failed_migration.js (failed)', 'some_file.js (pending)']); | ||
}); | ||
it('returns 0 and finishes without an error when the failed migrations in the history are not part of the current set of migrations', async () => { | ||
const failedEntry = toEntry('some_failed_migration.js', 'failed'); | ||
const { reporter, run } = getUpCommand([], getStorage([failedEntry])); | ||
const exitCode = await run(); | ||
assert.strictEqual(exitCode, 0); | ||
assert.strictEqual(reporter.onInit.mock.calls.length, 1); | ||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [ | ||
{ | ||
command: 'up', | ||
cwd: '/emigrate', | ||
dry: false, | ||
directory: 'migrations', | ||
}, | ||
]); | ||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1); | ||
const args = reporter.onFinished.mock.calls[0]?.arguments; | ||
assert.strictEqual(args?.length, 2); | ||
const finishedEntry = args[0]?.[0]; | ||
const error = args[1]; | ||
assert.strictEqual(finishedEntry?.name, failedEntry.name); | ||
assert.strictEqual(finishedEntry?.status, 'failed'); | ||
assert.strictEqual(finishedEntry?.error?.cause, failedEntry.error); | ||
assert.strictEqual(finishedEntry.error, error); | ||
assert.strictEqual(error?.cause, failedEntry.error); | ||
assert.deepStrictEqual(reporter.onFinished.mock.calls[0]?.arguments, [[], undefined]); | ||
}); | ||
@@ -131,2 +180,75 @@ it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => { | ||
}); | ||
it('returns 0 and finishes without an error when all pending migrations are run successfully', async () => { | ||
const { reporter, run } = getUpCommand(['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'], getStorage(['some_already_run_migration.js']), [ | ||
{ | ||
loadableExtensions: ['.js'], | ||
async loadMigration() { | ||
return async () => { | ||
// Success | ||
}; | ||
}, | ||
}, | ||
]); | ||
const exitCode = await run(); | ||
assert.strictEqual(exitCode, 0); | ||
assert.strictEqual(reporter.onInit.mock.calls.length, 1); | ||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [ | ||
{ | ||
command: 'up', | ||
cwd: '/emigrate', | ||
dry: false, | ||
directory: 'migrations', | ||
}, | ||
]); | ||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 2); | ||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 2); | ||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0); | ||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1); | ||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? []; | ||
assert.strictEqual(error, undefined); | ||
assert.strictEqual(entries?.length, 2); | ||
assert.deepStrictEqual(entries.map((entry) => `${entry.name} (${entry.status})`), ['some_migration.js (done)', 'some_other_migration.js (done)']); | ||
}); | ||
it('returns 1 and finishes with an error when a pending migration throw when run', async () => { | ||
const { reporter, run } = getUpCommand(['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'], getStorage(['some_already_run_migration.js']), [ | ||
{ | ||
loadableExtensions: ['.js'], | ||
async loadMigration(migration) { | ||
return async () => { | ||
if (migration.name === 'fail.js') { | ||
throw new Error('Oh noes!'); | ||
} | ||
}; | ||
}, | ||
}, | ||
]); | ||
const exitCode = await run(); | ||
assert.strictEqual(exitCode, 1); | ||
assert.strictEqual(reporter.onInit.mock.calls.length, 1); | ||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [ | ||
{ | ||
command: 'up', | ||
cwd: '/emigrate', | ||
dry: false, | ||
directory: 'migrations', | ||
}, | ||
]); | ||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 2); | ||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onMigrationError.mock.calls[0]?.arguments[1]?.message, 'Oh noes!'); | ||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1); | ||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1); | ||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? []; | ||
assert.strictEqual(error?.message, 'Failed to run migration: migrations/fail.js'); | ||
const cause = getErrorCause(error); | ||
assert.strictEqual(cause?.message, 'Oh noes!'); | ||
assert.strictEqual(entries?.length, 3); | ||
assert.deepStrictEqual(entries.map((entry) => `${entry.name} (${entry.status})`), ['some_migration.js (done)', 'fail.js (failed)', 'some_other_migration.js (skipped)']); | ||
}); | ||
}); | ||
@@ -137,2 +259,5 @@ function getErrorCause(error) { | ||
} | ||
if (typeof error?.cause === 'object' && error.cause !== null) { | ||
return error.cause; | ||
} | ||
return undefined; | ||
@@ -159,3 +284,3 @@ } | ||
date: new Date(), | ||
error: status === 'failed' ? new Error('Failed') : undefined, | ||
error: status === 'failed' ? serializeError(new Error('Failed')) : undefined, | ||
}; | ||
@@ -173,4 +298,6 @@ } | ||
const storage = { | ||
lock: mock.fn(), | ||
unlock: mock.fn(), | ||
lock: mock.fn(async (migrations) => migrations), | ||
unlock: mock.fn(async () => { | ||
// void | ||
}), | ||
getHistory: mock.fn(async function* () { | ||
@@ -177,0 +304,0 @@ yield* toEntries(historyEntries); |
@@ -8,3 +8,2 @@ const formatter = new Intl.ListFormat('en', { style: 'long', type: 'disjunction' }); | ||
this.code = code; | ||
this.name = `${this.name} [${this.code}]`; | ||
} | ||
@@ -11,0 +10,0 @@ } |
@@ -8,2 +8,3 @@ import path from 'node:path'; | ||
import prettyMs from 'pretty-ms'; | ||
import { EmigrateError } from '../errors.js'; | ||
const interactive = isInteractive(); | ||
@@ -85,15 +86,22 @@ const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall; | ||
} | ||
let errorTitle; | ||
let stack = []; | ||
if (error.stack) { | ||
// @ts-expect-error error won't be undefined here | ||
[errorTitle, ...stack] = error.stack.split('\n'); | ||
const stackParts = error.stack.split('\n'); | ||
const messageParts = (error.message ?? '').split('\n'); | ||
stack = stackParts.slice(messageParts.length); | ||
} | ||
else if (error.name) { | ||
errorTitle = `${error.name}: ${error.message}`; | ||
const properties = Object.getOwnPropertyNames(error).filter((property) => !['name', 'message', 'stack', 'cause'].includes(property)); | ||
const others = {}; | ||
for (const property of properties) { | ||
others[property] = error[property]; | ||
} | ||
else { | ||
errorTitle = error.message; | ||
const codeString = typeof others['code'] === 'string' ? others['code'] : undefined; | ||
const code = codeString ? ` [${codeString}]` : ''; | ||
const errorTitle = error.name | ||
? `${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())}`)]; | ||
if (properties.length > 0 && !(error instanceof EmigrateError)) { | ||
parts.push(`${indent} ${JSON.stringify(others, undefined, 2).split('\n').join(`\n${indent} `)}`); | ||
} | ||
const parts = [`${indent}${bold.red(errorTitle)}`, ...stack.map((line) => `${indent}${dim(line)}`)]; | ||
if (isErrorLike(error.cause)) { | ||
@@ -149,3 +157,3 @@ const nextIndent = `${indent} `; | ||
}; | ||
const getHeaderMessage = (migrations, lockedMigrations) => { | ||
const getHeaderMessage = (command, migrations, lockedMigrations) => { | ||
if (!migrations || !lockedMigrations) { | ||
@@ -162,3 +170,3 @@ return ''; | ||
const failedMigrations = nonLockedMigrations.filter((migration) => 'status' in migration && migration.status === 'failed'); | ||
const unlockableCount = nonLockedMigrations.length - failedMigrations.length; | ||
const unlockableCount = command === 'up' ? nonLockedMigrations.length - failedMigrations.length : 0; | ||
const parts = [ | ||
@@ -237,3 +245,3 @@ bold(`${lockedMigrations.length} of ${migrations.length}`), | ||
getTitle(this.#parameters), | ||
getHeaderMessage(this.#migrations, this.#lockedMigrations), | ||
getHeaderMessage(this.#parameters.command, this.#migrations, this.#lockedMigrations), | ||
this.#migrations?.map((migration) => getMigrationText(migration, this.#activeMigration)).join('\n') ?? '', | ||
@@ -281,3 +289,3 @@ getSummary(this.#parameters.command, this.#migrations), | ||
this.#lockedMigrations = migrations; | ||
console.log(getHeaderMessage(this.#migrations, this.#lockedMigrations)); | ||
console.log(getHeaderMessage(this.#parameters.command, this.#migrations, this.#lockedMigrations)); | ||
console.log(''); | ||
@@ -284,0 +292,0 @@ } |
{ | ||
"name": "@emigrate/cli", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"publishConfig": { | ||
@@ -46,3 +46,3 @@ "access": "public" | ||
"pretty-ms": "8.0.0", | ||
"@emigrate/plugin-tools": "0.5.0" | ||
"@emigrate/plugin-tools": "0.6.0" | ||
}, | ||
@@ -49,0 +49,0 @@ "volta": { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
186764
92
1810
+ Added@emigrate/plugin-tools@0.6.0(transitive)
+ Addeddebug@4.4.0(transitive)
+ Addedimport-from-esm@1.3.3(transitive)
+ Addedms@2.1.3(transitive)
- Removed@emigrate/plugin-tools@0.5.0(transitive)
- Removedimport-from-esm@1.2.1(transitive)
Updated@emigrate/plugin-tools@0.6.0