Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@emigrate/cli

Package Overview
Dependencies
Maintainers
1
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@emigrate/cli - npm Package Compare versions

Comparing version 0.15.0 to 0.16.0

dist/defaults.d.ts

69

dist/cli.js

@@ -5,4 +5,5 @@ #!/usr/bin/env node

import importFromEsm from 'import-from-esm';
import { ShowUsageError } from './errors.js';
import { CommandAbortError, ShowUsageError } from './errors.js';
import { getConfig } from './get-config.js';
import { DEFAULT_RESPITE_SECONDS } from './defaults.js';
const useColors = (values) => {

@@ -19,3 +20,3 @@ if (values['no-color']) {

};
const up = async (args) => {
const up = async (args, abortSignal) => {
const config = await getConfig('up');

@@ -77,2 +78,5 @@ const { values } = parseArgs({

},
'abort-respite': {
type: 'string',
},
},

@@ -104,2 +108,3 @@ allowPositionals: false,

which is useful if you want to mark migrations as successful after running them manually
--abort-respite <sec> The number of seconds to wait before abandoning running migrations after the command has been aborted (default: ${DEFAULT_RESPITE_SECONDS})

@@ -122,5 +127,6 @@ Examples:

const cwd = process.cwd();
const { directory = config.directory, storage = config.storage, reporter = config.reporter, dry, from, to, limit: limitString, import: imports = [], 'no-execution': noExecution, } = values;
const { directory = config.directory, storage = config.storage, reporter = config.reporter, dry, from, to, limit: limitString, import: imports = [], 'abort-respite': abortRespiteString, 'no-execution': noExecution, } = values;
const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])];
const limit = limitString === undefined ? undefined : Number.parseInt(limitString, 10);
const abortRespite = abortRespiteString === undefined ? config.abortRespite : Number.parseInt(abortRespiteString, 10);
if (Number.isNaN(limit)) {

@@ -132,2 +138,8 @@ console.error('Invalid limit value, expected an integer but was:', limitString);

}
if (Number.isNaN(abortRespite)) {
console.error('Invalid abortRespite value, expected an integer but was:', abortRespiteString ?? config.abortRespite);
console.log(usage);
process.exitCode = 1;
return;
}
await importAll(cwd, imports);

@@ -147,2 +159,4 @@ try {

noExecution,
abortSignal,
abortRespite: (abortRespite ?? DEFAULT_RESPITE_SECONDS) * 1000,
color: useColors(values),

@@ -428,3 +442,3 @@ });

};
const main = async (args) => {
const main = async (args, abortSignal) => {
const { values, positionals } = parseArgs({

@@ -478,19 +492,36 @@ args,

}
await action(process.argv.slice(3));
};
try {
await main(process.argv.slice(2));
}
catch (error) {
if (error instanceof Error) {
console.error(error);
if (error.cause instanceof Error) {
console.error(error.cause);
try {
await action(process.argv.slice(3), abortSignal);
}
catch (error) {
if (error instanceof Error) {
console.error(error);
if (error.cause instanceof Error) {
console.error(error.cause);
}
}
else {
console.error(error);
}
process.exitCode = 1;
}
else {
console.error(error);
}
process.exitCode = 1;
}
};
const controller = new AbortController();
process.on('SIGINT', () => {
controller.abort(CommandAbortError.fromSignal('SIGINT'));
});
process.on('SIGTERM', () => {
controller.abort(CommandAbortError.fromSignal('SIGTERM'));
});
process.on('uncaughtException', (error) => {
controller.abort(CommandAbortError.fromReason('Uncaught exception', error));
});
process.on('unhandledRejection', (error) => {
controller.abort(CommandAbortError.fromReason('Unhandled rejection', error));
});
await main(process.argv.slice(2), controller.signal);
setTimeout(() => {
console.error('Process did not exit within 10 seconds, forcing exit');
process.exit(1);
}, 10000).unref();
//# sourceMappingURL=cli.js.map

@@ -11,5 +11,7 @@ import { type Config } from '../types.js';

getMigrations?: GetMigrationsFunction;
abortSignal?: AbortSignal;
abortRespite?: number;
};
export default function upCommand({ storage: storageConfig, reporter: reporterConfig, directory, color, limit, from, to, noExecution, dry, plugins, cwd, getMigrations, }: Config & ExtraFlags): Promise<number>;
export default function upCommand({ storage: storageConfig, reporter: reporterConfig, directory, color, limit, from, to, noExecution, abortSignal, abortRespite, dry, plugins, cwd, getMigrations, }: Config & ExtraFlags): Promise<number>;
export {};
//# sourceMappingURL=up.d.ts.map

@@ -13,3 +13,3 @@ import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';

const lazyPluginLoaderJs = async () => import('../plugin-loader-js.js');
export default async function upCommand({ storage: storageConfig, reporter: reporterConfig, directory, color, limit, from, to, noExecution, dry = false, plugins = [], cwd, getMigrations, }) {
export default async function upCommand({ storage: storageConfig, reporter: reporterConfig, directory, color, limit, from, to, noExecution, abortSignal, abortRespite, dry = false, plugins = [], cwd, getMigrations, }) {
if (!directory) {

@@ -48,2 +48,4 @@ throw MissingOptionError.fromOption('directory');

to,
abortSignal,
abortRespite,
reporter,

@@ -50,0 +52,0 @@ storage,

import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
import path from 'node:path';
import { deserializeError } from 'serialize-error';
import { deserializeError, serializeError } from 'serialize-error';
import { version } from '../get-package-info.js';
import { BadOptionError, MigrationHistoryError, MigrationRunError, StorageInitError } from '../errors.js';
import { BadOptionError, CommandAbortError, ExecutionDesertedError, MigrationHistoryError, MigrationRunError, StorageInitError, } from '../errors.js';
import upCommand from './up.js';

@@ -330,2 +330,87 @@ describe('up', () => {

});
describe("aborting the migration process before it's finished", () => {
it('returns 1 and finishes with a command abort error when the migration process is aborted prematurely', async () => {
const controller = new AbortController();
const migration = mock.fn(async () => {
// Success on second call, and abort
controller.abort(CommandAbortError.fromSignal('SIGINT'));
}, async () => {
// Success on first call
}, { times: 1 });
const { reporter, run } = getUpCommand([
'1_some_already_run_migration.js',
'2_some_migration.js',
'3_another_migration.js',
'4_some_other_migration.js',
'5_yet_another_migration.js',
'6_some_more_migration.js',
], getStorage(['1_some_already_run_migration.js']), [
{
loadableExtensions: ['.js'],
async loadMigration() {
return migration;
},
},
]);
const exitCode = await run({
abortSignal: controller.signal,
});
assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
{ name: '2_some_migration.js', status: 'done', started: true },
{ name: '3_another_migration.js', status: 'done', started: true },
{ name: '4_some_other_migration.js', status: 'skipped' },
{ name: '5_yet_another_migration.js', status: 'skipped' },
{ name: '6_some_more_migration.js', status: 'skipped' },
], CommandAbortError.fromSignal('SIGINT'));
assert.strictEqual(reporter.onAbort.mock.calls.length, 1);
assert.strictEqual(migration.mock.calls.length, 2);
});
});
it('returns 1 and finishes with a deserted error with a command abort error as cause when the migration process is aborted prematurely and stops waiting on migrations taking longer than the respite period after the abort', async () => {
const controller = new AbortController();
const migration = mock.fn(async () => {
// Success on second call, and abort
controller.abort(CommandAbortError.fromSignal('SIGINT'));
return new Promise((resolve) => {
setTimeout(resolve, 100); // Take longer than the respite period
});
}, async () => {
// Success on first call
}, { times: 1 });
const { reporter, run } = getUpCommand([
'1_some_already_run_migration.js',
'2_some_migration.js',
'3_another_migration.js',
'4_some_other_migration.js',
'5_yet_another_migration.js',
'6_some_more_migration.js',
], getStorage(['1_some_already_run_migration.js']), [
{
loadableExtensions: ['.js'],
async loadMigration() {
return migration;
},
},
]);
const exitCode = await run({
abortSignal: controller.signal,
abortRespite: 10,
});
assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
{ name: '2_some_migration.js', status: 'done', started: true },
{
name: '3_another_migration.js',
status: 'failed',
started: true,
error: ExecutionDesertedError.fromReason('Deserted after 10ms', CommandAbortError.fromSignal('SIGINT')),
},
{ name: '4_some_other_migration.js', status: 'skipped' },
{ name: '5_yet_another_migration.js', status: 'skipped' },
{ name: '6_some_more_migration.js', status: 'skipped' },
], ExecutionDesertedError.fromReason('Deserted after 10ms', CommandAbortError.fromSignal('SIGINT')));
assert.strictEqual(reporter.onAbort.mock.calls.length, 1);
assert.strictEqual(migration.mock.calls.length, 2);
});
});

@@ -398,2 +483,3 @@ function getErrorCause(error) {

onInit: mock.fn(noop),
onAbort: mock.fn(noop),
onCollectedMigrations: mock.fn(noop),

@@ -495,3 +581,9 @@ onLockedMigrations: mock.fn(noop),

const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
assert.deepStrictEqual(error, finishedError, 'Finished error');
if (finishedError instanceof DOMException || error instanceof DOMException) {
// The assert library doesn't support DOMException apparently, so ugly workaround here:
assert.deepStrictEqual(deserializeError(serializeError(error)), deserializeError(serializeError(finishedError)), 'Finished error');
}
else {
assert.deepStrictEqual(error, finishedError, 'Finished error');
}
const cause = getErrorCause(error);

@@ -498,0 +590,0 @@ const expectedCause = finishedError?.cause;

@@ -0,1 +1,2 @@

/// <reference types="node" resolution-mode="require"/>
import { type SerializedError, type MigrationMetadata, type FailedMigrationMetadata, type FailedMigrationHistoryEntry } from '@emigrate/types';

@@ -53,2 +54,11 @@ export declare const toError: (error: unknown) => Error;

}
export declare class CommandAbortError extends EmigrateError {
static fromSignal(signal: NodeJS.Signals): CommandAbortError;
static fromReason(reason: string, cause?: unknown): CommandAbortError;
constructor(message: string | undefined, options?: ErrorOptions);
}
export declare class ExecutionDesertedError extends EmigrateError {
static fromReason(reason: string, cause?: Error): ExecutionDesertedError;
constructor(message: string | undefined, options?: ErrorOptions);
}
//# sourceMappingURL=errors.d.ts.map

@@ -104,2 +104,21 @@ import { serializeError, errorConstructors, deserializeError } from 'serialize-error';

}
export class CommandAbortError extends EmigrateError {
static fromSignal(signal) {
return new CommandAbortError(`Command aborted due to signal: ${signal}`);
}
static fromReason(reason, cause) {
return new CommandAbortError(`Command aborted: ${reason}`, { cause });
}
constructor(message, options) {
super(message, options, 'ERR_COMMAND_ABORT');
}
}
export class ExecutionDesertedError extends EmigrateError {
static fromReason(reason, cause) {
return new ExecutionDesertedError(`Execution deserted: ${reason}`, { cause });
}
constructor(message, options) {
super(message, options, 'ERR_EXECUTION_DESERTED');
}
}
errorConstructors.set('EmigrateError', EmigrateError);

@@ -117,2 +136,4 @@ errorConstructors.set('ShowUsageError', ShowUsageError);

errorConstructors.set('StorageInitError', StorageInitError);
errorConstructors.set('CommandAbortError', CommandAbortError);
errorConstructors.set('ExecutionDesertedError', ExecutionDesertedError);
//# sourceMappingURL=errors.js.map

@@ -1,3 +0,6 @@

type Fn<Args extends any[], Result> = (...args: Args) => Result;
type Result<T> = [value: T, error: undefined] | [value: undefined, error: Error];
type ExecOptions = {
abortSignal?: AbortSignal;
abortRespite?: number;
};
/**

@@ -7,5 +10,10 @@ * Execute a function and return a result tuple

* This is a helper function to make it easier to handle errors without the extra nesting of try/catch
* If an abort signal is provided the function will reject with an ExecutionDesertedError if the signal is aborted
* and the given function has not yet resolved within the given respite time (or a default of 30 seconds)
*
* @param fn The function to execute
* @param options Options for the execution
*/
export declare const exec: <Args extends any[], Return extends Promise<any>>(fn: Fn<Args, Return>, ...args: Args) => Promise<Result<Awaited<Return>>>;
export declare const exec: <Return extends Promise<any>>(fn: () => Return, options?: ExecOptions) => Promise<Result<Awaited<Return>>>;
export {};
//# sourceMappingURL=exec.d.ts.map

@@ -1,2 +0,4 @@

import { toError } from './errors.js';
import prettyMs from 'pretty-ms';
import { ExecutionDesertedError, toError } from './errors.js';
import { DEFAULT_RESPITE_SECONDS } from './defaults.js';
/**

@@ -6,6 +8,12 @@ * Execute a function and return a result tuple

* This is a helper function to make it easier to handle errors without the extra nesting of try/catch
* If an abort signal is provided the function will reject with an ExecutionDesertedError if the signal is aborted
* and the given function has not yet resolved within the given respite time (or a default of 30 seconds)
*
* @param fn The function to execute
* @param options Options for the execution
*/
export const exec = async (fn, ...args) => {
export const exec = async (fn, options = {}) => {
try {
const result = await fn(...args);
const aborter = options.abortSignal ? getAborter(options.abortSignal, options.abortRespite) : undefined;
const result = await Promise.race(aborter ? [aborter, fn()] : [fn()]);
return [result, undefined];

@@ -17,2 +25,19 @@ }

};
/**
* Returns a promise that rejects after a given time after the given signal is aborted
*
* @param signal The abort signal to listen to
* @param respite The time in milliseconds to wait before rejecting
*/
const getAborter = async (signal, respite = DEFAULT_RESPITE_SECONDS * 1000) => {
return new Promise((_, reject) => {
if (signal.aborted) {
setTimeout(reject, respite, ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason))).unref();
return;
}
signal.addEventListener('abort', () => {
setTimeout(reject, respite, ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason))).unref();
}, { once: true });
});
};
//# sourceMappingURL=exec.js.map

@@ -7,2 +7,4 @@ import { type EmigrateReporter, type MigrationMetadata, type MigrationMetadataFinished, type Storage } from '@emigrate/types';

to?: string;
abortSignal?: AbortSignal;
abortRespite?: number;
reporter: EmigrateReporter;

@@ -14,4 +16,4 @@ storage: Storage;

};
export declare const migrationRunner: ({ dry, limit, from, to, reporter, storage, migrations, validate, execute, }: MigrationRunnerParameters) => Promise<Error | undefined>;
export declare const migrationRunner: ({ dry, limit, from, to, abortSignal, abortRespite, reporter, storage, migrations, validate, execute, }: MigrationRunnerParameters) => Promise<Error | undefined>;
export {};
//# sourceMappingURL=migration-runner.d.ts.map

@@ -6,3 +6,3 @@ import { hrtime } from 'node:process';

import { getDuration } from './get-duration.js';
export const migrationRunner = async ({ dry, limit, from, to, reporter, storage, migrations, validate, execute, }) => {
export const migrationRunner = async ({ dry, limit, from, to, abortSignal, abortRespite, reporter, storage, migrations, validate, execute, }) => {
await reporter.onCollectedMigrations?.(migrations);

@@ -12,2 +12,10 @@ const validatedMigrations = [];

let skip = false;
abortSignal?.addEventListener('abort', () => {
skip = true;
reporter.onAbort?.(toError(abortSignal.reason))?.then(() => {
/* noop */
}, () => {
/* noop */
});
}, { once: true });
for await (const migration of migrations) {

@@ -54,3 +62,3 @@ if (isFinishedMigration(migration)) {

? [migrationsToLock]
: await exec(async () => storage.lock(migrationsToLock));
: await exec(async () => storage.lock(migrationsToLock), { abortSignal, abortRespite });
if (lockError) {

@@ -115,3 +123,3 @@ for (const migration of migrationsToLock) {

const start = hrtime();
const [, migrationError] = await exec(async () => execute(migration));
const [, migrationError] = await exec(async () => execute(migration), { abortSignal, abortRespite });
const duration = getDuration(start);

@@ -141,3 +149,5 @@ if (migrationError) {

}
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
const [, unlockError] = dry
? []
: await exec(async () => storage.unlock(lockedMigrations ?? []), { abortSignal, abortRespite });
// eslint-disable-next-line unicorn/no-array-callback-reference

@@ -150,3 +160,3 @@ const firstFailed = finishedMigrations.find(isFailedMigration);

: undefined;
const error = unlockError ?? firstError ?? lockError;
const error = unlockError ?? firstError ?? lockError ?? (abortSignal?.aborted ? toError(abortSignal.reason) : undefined);
await reporter.onFinished?.(finishedMigrations, error);

@@ -153,0 +163,0 @@ return error;

@@ -5,2 +5,3 @@ import { type MigrationMetadata, type MigrationMetadataFinished, type EmigrateReporter, type ReporterInitParameters, type Awaitable } from '@emigrate/types';

onInit(parameters: ReporterInitParameters): void | PromiseLike<void>;
onAbort(reason: Error): void | PromiseLike<void>;
onCollectedMigrations(migrations: MigrationMetadata[]): void | PromiseLike<void>;

@@ -21,2 +22,3 @@ onLockedMigrations(migrations: MigrationMetadata[]): void | PromiseLike<void>;

onInit(parameters: ReporterInitParameters): void | PromiseLike<void>;
onAbort(reason: Error): void | PromiseLike<void>;
onCollectedMigrations(migrations: MigrationMetadata[]): void | PromiseLike<void>;

@@ -23,0 +25,0 @@ onLockedMigrations(migrations: MigrationMetadata[]): void | PromiseLike<void>;

@@ -108,2 +108,12 @@ import { black, blueBright, bold, cyan, dim, faint, gray, green, red, redBright, yellow, yellowBright } from 'ansis';

};
const getAbortMessage = (reason) => {
if (!reason) {
return '';
}
const parts = [` ${red.bold(reason.message)}`];
if (isErrorLike(reason.cause)) {
parts.push(getError(reason.cause, ' '));
}
return parts.join('\n');
};
const getSummary = (command, migrations = []) => {

@@ -200,2 +210,3 @@ const total = migrations.length;

#interval;
#abortReason;
onInit(parameters) {

@@ -205,2 +216,5 @@ this.#parameters = parameters;

}
onAbort(reason) {
this.#abortReason = reason;
}
onCollectedMigrations(migrations) {

@@ -262,2 +276,3 @@ this.#migrations = migrations;

this.#migrations?.map((migration) => getMigrationText(migration, this.#activeMigration)).join('\n') ?? '',
getAbortMessage(this.#abortReason),
getSummary(this.#parameters.command, this.#migrations),

@@ -299,2 +314,7 @@ getError(this.#error),

}
onAbort(reason) {
console.log('');
console.error(getAbortMessage(reason));
console.log('');
}
onCollectedMigrations(migrations) {

@@ -301,0 +321,0 @@ this.#migrations = migrations;

@@ -14,2 +14,3 @@ import { type EmigrateStorage, type Awaitable, type Plugin, type EmigrateReporter } from '@emigrate/types';

color?: boolean;
abortRespite?: number;
};

@@ -16,0 +17,0 @@ export type EmigrateConfig = Config & {

{
"name": "@emigrate/cli",
"version": "0.15.0",
"version": "0.16.0",
"publishConfig": {

@@ -48,4 +48,4 @@ "access": "public"

"serialize-error": "11.0.3",
"@emigrate/plugin-tools": "0.9.3",
"@emigrate/types": "0.10.0"
"@emigrate/plugin-tools": "0.9.4",
"@emigrate/types": "0.11.0"
},

@@ -52,0 +52,0 @@ "volta": {

@@ -47,2 +47,3 @@ # @emigrate/cli

which is useful if you want to mark migrations as successful after running them manually
--abort-respite <sec> The number of seconds to wait before abandoning running migrations after the command has been aborted (default: 10)

@@ -49,0 +50,0 @@ Examples:

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

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc