New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.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.16.2 to 0.17.0

dist/array-map-async.d.ts

29

dist/cli.js

@@ -89,18 +89,31 @@ #!/usr/bin/env node

-h, --help Show this help message and exit
-d, --directory <path> The directory where the migration files are located (required)
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
For example if you want to use Dotenv to load environment variables or when using TypeScript
-s, --storage <name> The storage to use for where to store the migration history (required)
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
-r, --reporter <name> The reporter to use for reporting the migration progress
-l, --limit <count> Limit the number of migrations to run
-f, --from <name> Start running migrations from the given migration name, the given name doesn't need to exist
and is compared in lexicographical order
-t, --to <name> Skip migrations after the given migration name, the given name doesn't need to exist
and is compared in lexicographical order
-f, --from <name/path> Start running migrations from the given migration name or relative file path to a migration file,
the given name or path needs to exist. The same migration and those after it lexicographically will be run
-t, --to <name/path> Skip migrations after the given migration name or relative file path to a migration file,
the given name or path needs to exist. The same migration and those before it lexicographically will be run
--dry List the pending migrations that would be run without actually running them
--color Force color output (this option is passed to the reporter)
--no-color Disable color output (this option is passed to the reporter)
--no-execution Mark the migrations as executed and successful without actually running them,
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})

@@ -380,3 +393,3 @@

});
const usage = `Usage: emigrate remove [options] <name>
const usage = `Usage: emigrate remove [options] <name/path>

@@ -388,3 +401,3 @@ Remove entries from the migration history.

name The name of the migration file to remove from the history (required)
name/path The name of or relative path to the migration file to remove from the history (required)

@@ -399,4 +412,3 @@ Options:

-s, --storage <name> The storage to use to get the migration history (required)
-f, --force Force removal of the migration history entry even if the migration file does not exist
or it's in a non-failed state
-f, --force Force removal of the migration history entry even if the migration is not in a failed state
--color Force color output (this option is passed to the reporter)

@@ -410,2 +422,3 @@ --no-color Disable color output (this option is passed to the reporter)

emigrate remove -i dotenv/config -d ./migrations -s postgres 20231122120529381_some_migration_file.sql
emigrate remove -i dotenv/config -d ./migrations -s postgres migrations/20231122120529381_some_migration_file.sql
`;

@@ -412,0 +425,0 @@ if (values.help) {

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

import { migrationRunner } from '../migration-runner.js';
import { arrayFromAsync } from '../array-from-async.js';
import { collectMigrations } from '../collect-migrations.js';

@@ -34,3 +33,3 @@ import { version } from '../get-package-info.js';

storage,
migrations: await arrayFromAsync(collectedMigrations),
migrations: collectedMigrations,
async validate() {

@@ -42,2 +41,8 @@ // No-op

},
async onSuccess() {
throw new Error('Unexpected onSuccess call');
},
async onError() {
throw new Error('Unexpected onError call');
},
});

@@ -44,0 +49,0 @@ return error ? 1 : 0;

import { type Config } from '../types.js';
import { type GetMigrationsFunction } from '../get-migrations.js';
type ExtraFlags = {
cwd: string;
force?: boolean;
getMigrations?: GetMigrationsFunction;
};
export default function removeCommand({ directory, reporter: reporterConfig, storage: storageConfig, color, cwd, force }: Config & ExtraFlags, name: string): Promise<0 | 1>;
export default function removeCommand({ directory, reporter: reporterConfig, storage: storageConfig, color, cwd, force, getMigrations, }: Config & ExtraFlags, name: string): Promise<0 | 1>;
export {};
//# sourceMappingURL=remove.d.ts.map

@@ -1,10 +0,12 @@

import process from 'node:process';
import path from 'node:path';
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
import { BadOptionError, MigrationNotRunError, MissingArgumentsError, MissingOptionError, OptionNeededError, StorageInitError, } from '../errors.js';
import { getMigration } from '../get-migration.js';
import { getDuration } from '../get-duration.js';
import { isFinishedMigration } from '@emigrate/types';
import { BadOptionError, MigrationNotRunError, MigrationRemovalError, MissingArgumentsError, MissingOptionError, OptionNeededError, StorageInitError, toError, } from '../errors.js';
import { exec } from '../exec.js';
import { version } from '../get-package-info.js';
import { collectMigrations } from '../collect-migrations.js';
import { migrationRunner } from '../migration-runner.js';
import { arrayMapAsync } from '../array-map-async.js';
const lazyDefaultReporter = async () => import('../reporters/default.js');
export default async function removeCommand({ directory, reporter: reporterConfig, storage: storageConfig, color, cwd, force = false }, name) {
export default async function removeCommand({ directory, reporter: reporterConfig, storage: storageConfig, color, cwd, force = false, getMigrations, }, name) {
if (!directory) {

@@ -30,54 +32,65 @@ throw MissingOptionError.fromOption('directory');

}
const [migrationFile, fileError] = await exec(async () => getMigration(cwd, directory, name, !force));
if (fileError) {
await reporter.onFinished?.([], fileError);
await storage.end();
return 1;
}
const finishedMigrations = [];
let historyEntry;
let removalError;
for await (const migrationHistoryEntry of storage.getHistory()) {
if (migrationHistoryEntry.name !== migrationFile.name) {
continue;
try {
const collectedMigrations = arrayMapAsync(collectMigrations(cwd, directory, storage.getHistory(), getMigrations), (migration) => {
if (isFinishedMigration(migration)) {
if (migration.status === 'failed') {
const { status, duration, error, ...pendingMigration } = migration;
const removableMigration = { ...pendingMigration, originalStatus: status };
return removableMigration;
}
if (migration.status === 'done') {
const { status, duration, ...pendingMigration } = migration;
const removableMigration = { ...pendingMigration, originalStatus: status };
return removableMigration;
}
throw new Error(`Unexpected migration status: ${migration.status}`);
}
return migration;
});
if (!name.includes(path.sep)) {
name = path.join(directory, name);
}
if (migrationHistoryEntry.status === 'done' && !force) {
removalError = OptionNeededError.fromOption('force', `The migration "${migrationFile.name}" is not in a failed state. Use the "force" option to force its removal`);
}
else {
historyEntry = migrationHistoryEntry;
}
const error = await migrationRunner({
dry: false,
lock: false,
name,
reporter,
storage,
migrations: collectedMigrations,
migrationFilter(migration) {
return migration.relativeFilePath === name;
},
async validate(migration) {
if (migration.originalStatus === 'done' && !force) {
throw OptionNeededError.fromOption('force', `The migration "${migration.name}" is not in a failed state. Use the "force" option to force its removal`);
}
if (!migration.originalStatus) {
throw MigrationNotRunError.fromMetadata(migration);
}
},
async execute(migration) {
try {
await storage.remove(migration);
}
catch (error) {
throw MigrationRemovalError.fromMetadata(migration, toError(error));
}
},
async onSuccess() {
// No-op
},
async onError() {
// No-op
},
});
return error ? 1 : 0;
}
await reporter.onMigrationRemoveStart?.(migrationFile);
const start = process.hrtime();
if (historyEntry) {
try {
await storage.remove(migrationFile);
const duration = getDuration(start);
const finishedMigration = { ...migrationFile, status: 'done', duration };
await reporter.onMigrationRemoveSuccess?.(finishedMigration);
finishedMigrations.push(finishedMigration);
}
catch (error) {
removalError = error instanceof Error ? error : new Error(String(error));
}
catch (error) {
await reporter.onFinished?.([], toError(error));
return 1;
}
else if (!removalError) {
removalError = MigrationNotRunError.fromMetadata(migrationFile);
finally {
await storage.end();
}
if (removalError) {
const duration = getDuration(start);
const finishedMigration = {
...migrationFile,
status: 'failed',
error: removalError,
duration,
};
await reporter.onMigrationRemoveError?.(finishedMigration, removalError);
finishedMigrations.push(finishedMigration);
}
await reporter.onFinished?.(finishedMigrations, removalError);
await storage.end();
return removalError ? 1 : 0;
}
//# sourceMappingURL=remove.js.map

@@ -0,10 +1,9 @@

import path from 'node:path';
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
import { isFinishedMigration } from '@emigrate/types';
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError, toError } from '../errors.js';
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError, toError, toSerializedError, } from '../errors.js';
import { withLeadingPeriod } from '../with-leading-period.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';
import { version } from '../get-package-info.js';

@@ -32,3 +31,3 @@ const lazyDefaultReporter = async () => import('../reporters/default.js');

try {
const collectedMigrations = filterAsync(collectMigrations(cwd, directory, storage.getHistory(), getMigrations), (migration) => !isFinishedMigration(migration) || migration.status === 'failed');
const collectedMigrations = collectMigrations(cwd, directory, storage.getHistory(), getMigrations);
const loaderPlugins = await getOrLoadPlugins('loader', [lazyPluginLoaderJs, ...plugins]);

@@ -43,2 +42,8 @@ const loaderByExtension = new Map();

};
if (from && !from.includes(path.sep)) {
from = path.join(directory, from);
}
if (to && !to.includes(path.sep)) {
to = path.join(directory, to);
}
const error = await migrationRunner({

@@ -53,3 +58,6 @@ dry,

storage,
migrations: await arrayFromAsync(collectedMigrations),
migrations: collectedMigrations,
migrationFilter(migration) {
return !isFinishedMigration(migration) || migration.status === 'failed';
},
async validate(migration) {

@@ -75,2 +83,8 @@ if (noExecution) {

},
async onSuccess(migration) {
await storage.onSuccess(migration);
},
async onError(migration, error) {
await storage.onError(migration, toSerializedError(error));
},
});

@@ -77,0 +91,0 @@ return error ? 1 : 0;

import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
import { deserializeError, serializeError } from 'serialize-error';
import { deserializeError } from 'serialize-error';
import { version } from '../get-package-info.js';
import { BadOptionError, CommandAbortError, ExecutionDesertedError, MigrationHistoryError, MigrationRunError, StorageInitError, } from '../errors.js';
import { toEntries, toEntry, toMigrations } from '../test-utils.js';
import { BadOptionError, CommandAbortError, ExecutionDesertedError, MigrationHistoryError, MigrationRunError, StorageInitError, toSerializedError, } from '../errors.js';
import { toEntry, toMigrations, getMockedReporter, getMockedStorage, getErrorCause, } from '../test-utils.js';
import upCommand from './up.js';

@@ -16,18 +16,21 @@ describe('up', () => {

it('returns 0 and finishes without an error when there are no migrations to run', async () => {
const { reporter, run } = getUpCommand([], getStorage([]));
const storage = getMockedStorage([]);
const { reporter, run } = getUpCommand([], storage);
const exitCode = await run();
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, []);
assertPreconditionsFulfilled({ dry: false }, reporter, storage, []);
});
it('returns 0 and finishes without an error when all migrations have already been run', async () => {
const { reporter, run } = getUpCommand(['my_migration.js'], getStorage(['my_migration.js']));
const storage = getMockedStorage(['my_migration.js']);
const { reporter, run } = getUpCommand(['my_migration.js'], storage);
const exitCode = await run();
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, []);
assertPreconditionsFulfilled({ dry: false }, reporter, storage, []);
});
it('returns 0 and finishes without an error when all migrations have already been run even when the history responds without file extensions', async () => {
const { reporter, run } = getUpCommand(['my_migration.js'], getStorage(['my_migration']));
const storage = getMockedStorage(['my_migration']);
const { reporter, run } = getUpCommand(['my_migration.js'], storage);
const exitCode = await run();
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, []);
assertPreconditionsFulfilled({ dry: false }, reporter, storage, []);
});

@@ -38,3 +41,4 @@ 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']), [
const storage = getMockedStorage(['some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'], storage, [
{

@@ -49,3 +53,3 @@ loadableExtensions: ['.js'],

assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: 'some_migration.js', status: 'done', started: true },

@@ -57,3 +61,4 @@ { name: 'some_other_migration.js', status: 'done', started: true },

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']), [
const storage = getMockedStorage(['some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'], storage, [
{

@@ -72,3 +77,3 @@ loadableExtensions: ['.js'],

assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: 'some_migration.js', status: 'done', started: true },

@@ -81,6 +86,7 @@ { name: 'fail.js', status: 'failed', started: true, error: new Error('Oh noes!') },

it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin', async () => {
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], getStorage([]));
const storage = getMockedStorage([]);
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], storage);
const exitCode = await run();
assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: 'some_other.js', status: 'skipped' },

@@ -95,6 +101,7 @@ {

it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin in dry-run mode as well', async () => {
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], getStorage([]));
const storage = getMockedStorage([]);
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], storage);
const exitCode = await run({ dry: true });
assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: true }, reporter, [
assertPreconditionsFulfilled({ dry: true }, reporter, storage, [
{ name: 'some_other.js', status: 'skipped' },

@@ -112,6 +119,7 @@ {

const failedEntry = toEntry('some_failed_migration.js', 'failed');
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry]));
const storage = getMockedStorage([failedEntry]);
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], storage);
const exitCode = await run();
assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{

@@ -127,6 +135,7 @@ name: 'some_failed_migration.js',

const failedEntry = toEntry('some_failed_migration.js', 'failed');
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry]));
const storage = getMockedStorage([failedEntry]);
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], storage);
const exitCode = await run({ dry: true });
assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: true }, reporter, [
assertPreconditionsFulfilled({ dry: true }, reporter, storage, [
{

@@ -142,6 +151,7 @@ name: 'some_failed_migration.js',

const failedEntry = toEntry('some_failed_migration.js', 'failed');
const { reporter, run } = getUpCommand([], getStorage([failedEntry]));
const storage = getMockedStorage([failedEntry]);
const { reporter, run } = getUpCommand([], storage);
const exitCode = await run();
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, []);
assertPreconditionsFulfilled({ dry: false }, reporter, storage, []);
});

@@ -153,3 +163,4 @@ });

});
const { reporter, run } = getUpCommand(['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'], getStorage(['some_already_run_migration.js']), [
const storage = getMockedStorage(['some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'], storage, [
{

@@ -164,3 +175,3 @@ loadableExtensions: ['.js'],

assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: 'some_migration.js', status: 'done', started: true },

@@ -173,6 +184,7 @@ { name: 'some_other_migration.js', status: 'skipped' },

it('returns 0 and finishes without an error with the given number of pending migrations are validated and listed successfully in dry-mode', async () => {
const { reporter, run } = getUpCommand(['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'], getStorage(['some_already_run_migration.js']));
const storage = getMockedStorage(['some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'], storage);
const exitCode = await run({ dry: true, limit: 1 });
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: true }, reporter, [
assertPreconditionsFulfilled({ dry: true }, reporter, storage, [
{ name: 'some_migration.js', status: 'pending' },

@@ -182,7 +194,13 @@ { name: 'some_other_migration.js', status: 'skipped' },

});
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are run successfully, even when the "from" is not an existing migration', async () => {
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are run successfully', async () => {
const migration = mock.fn(async () => {
// Success
});
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'], getStorage(['1_some_already_run_migration.js']), [
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([
'1_some_already_run_migration.js',
'2_some_migration.js',
'3_existing_migration.js',
'4_some_other_migration.js',
], storage, [
{

@@ -195,24 +213,44 @@ loadableExtensions: ['.js'],

]);
const exitCode = await run({ from: '3_non_existing_migration.js' });
const exitCode = await run({ from: '3_existing_migration.js' });
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },
{ name: '3_existing_migration.js', status: 'done', started: true },
{ name: '4_some_other_migration.js', status: 'done', started: true },
]);
assert.strictEqual(migration.mock.calls.length, 1);
assert.strictEqual(migration.mock.calls.length, 2);
});
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are validated and listed successfully in dry-mode, even when the "from" is not an existing migration', async () => {
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'], getStorage(['1_some_already_run_migration.js']));
const exitCode = await run({ dry: true, from: '3_non_existing_migration.js' });
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are run successfully, when the "from" parameter is a relative path', async () => {
const migration = mock.fn(async () => {
// Success
});
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([
'1_some_already_run_migration.js',
'2_some_migration.js',
'3_existing_migration.js',
'4_some_other_migration.js',
], storage, [
{
loadableExtensions: ['.js'],
async loadMigration() {
return migration;
},
},
]);
const exitCode = await run({ from: 'migrations/3_existing_migration.js' });
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: true }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },
{ name: '4_some_other_migration.js', status: 'pending' },
{ name: '3_existing_migration.js', status: 'done', started: true },
{ name: '4_some_other_migration.js', status: 'done', started: true },
]);
assert.strictEqual(migration.mock.calls.length, 2);
});
it('returns 0 and finishes without an error when pending migrations before given "to" parameter are run successfully, even when the "to" is not an existing migration', async () => {
it('returns 0 and runs all pending migrations, if "from" is an already executed migration', async () => {
const migration = mock.fn(async () => {
// Success
});
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'], getStorage(['1_some_already_run_migration.js']), [
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'], storage, [
{

@@ -225,16 +263,123 @@ loadableExtensions: ['.js'],

]);
const exitCode = await run({ to: '3_non_existing_migration.js' });
const exitCode = await run({ from: '1_some_already_run_migration.js' });
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'done', started: true },
{ name: '4_some_other_migration.js', status: 'done', started: true },
]);
assert.strictEqual(migration.mock.calls.length, 2);
});
it('returns 1 and finishes with an error when the given "from" migration name does not exist', async () => {
const migration = mock.fn(async () => {
// Success
});
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'], storage, [
{
loadableExtensions: ['.js'],
async loadMigration() {
return migration;
},
},
]);
const exitCode = await run({ from: '3_non_existing_migration.js' });
assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },
{ name: '4_some_other_migration.js', status: 'skipped' },
], BadOptionError.fromOption('from', 'The "from" migration: "migrations/3_non_existing_migration.js" was not found'));
assert.strictEqual(migration.mock.calls.length, 0);
});
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are validated and listed successfully in dry-mode', async () => {
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '3_some_other_migration.js'], storage);
const exitCode = await run({ dry: true, from: '3_some_other_migration.js' });
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: true }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },
{ name: '3_some_other_migration.js', status: 'pending' },
]);
assert.strictEqual(migration.mock.calls.length, 1);
});
it('returns 0 and finishes without an error when pending migrations after given "to" parameter are validated and listed successfully in dry-mode, even when the "to" is not an existing migration', async () => {
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'], getStorage(['1_some_already_run_migration.js']));
const exitCode = await run({ dry: true, to: '3_non_existing_migration.js' });
it('returns 0 and finishes without an error when pending migrations before given "to" parameter are run successfully', async () => {
const migration = mock.fn(async () => {
// Success
});
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([
'1_some_already_run_migration.js',
'2_some_migration.js',
'3_existing_migration.js',
'4_some_other_migration.js',
], storage, [
{
loadableExtensions: ['.js'],
async loadMigration() {
return migration;
},
},
]);
const exitCode = await run({ to: '3_existing_migration.js' });
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: true }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'done', started: true },
{ name: '3_existing_migration.js', status: 'done', started: true },
{ name: '4_some_other_migration.js', status: 'skipped' },
]);
assert.strictEqual(migration.mock.calls.length, 2);
});
it('returns 1 and finishes with an error when the given "to" migration name does not exist', async () => {
const migration = mock.fn(async () => {
// Success
});
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'], storage, [
{
loadableExtensions: ['.js'],
async loadMigration() {
return migration;
},
},
]);
const exitCode = await run({ to: '3_non_existing_migration.js' });
assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },
{ name: '4_some_other_migration.js', status: 'skipped' },
], BadOptionError.fromOption('to', 'The "to" migration: "migrations/3_non_existing_migration.js" was not found'));
assert.strictEqual(migration.mock.calls.length, 0);
});
it('returns 0 and runs no migrations, if "to" is an already executed migration', async () => {
const migration = mock.fn(async () => {
// Success
});
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand(['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'], storage, [
{
loadableExtensions: ['.js'],
async loadMigration() {
return migration;
},
},
]);
const exitCode = await run({ to: '1_some_already_run_migration.js' });
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },
{ name: '4_some_other_migration.js', status: 'skipped' },
]);
assert.strictEqual(migration.mock.calls.length, 0);
});
it('returns 0 and finishes without an error when pending migrations after given "to" parameter are validated and listed successfully in dry-mode', async () => {
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([
'1_some_already_run_migration.js',
'2_some_migration.js',
'3_existing_migration.js',
'4_some_other_migration.js',
], storage);
const exitCode = await run({ dry: true, to: '3_existing_migration.js' });
assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: true }, reporter, storage, [
{ name: '2_some_migration.js', status: 'pending' },
{ name: '3_existing_migration.js', status: 'pending' },
{ name: '4_some_other_migration.js', status: 'skipped' },

@@ -247,2 +392,3 @@ ]);

});
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([

@@ -255,3 +401,3 @@ '1_some_already_run_migration.js',

'6_some_more_migration.js',
], getStorage(['1_some_already_run_migration.js']), [
], storage, [
{

@@ -266,3 +412,3 @@ loadableExtensions: ['.js'],

assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },

@@ -282,2 +428,3 @@ { name: '3_another_migration.js', status: 'done', started: true },

});
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([

@@ -290,3 +437,3 @@ '1_some_already_run_migration.js',

'6_some_more_migration.js',
], getStorage(['1_some_already_run_migration.js']), [
], storage, [
{

@@ -306,3 +453,3 @@ loadableExtensions: ['.js'],

assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },

@@ -321,2 +468,3 @@ { name: '3_another_migration.js', status: 'done', started: true },

});
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([

@@ -329,3 +477,3 @@ '1_some_already_run_migration.js',

'6_some_more_migration.js',
], getStorage(['1_some_already_run_migration.js']), [
], storage, [
{

@@ -345,3 +493,3 @@ loadableExtensions: ['.js'],

assert.strictEqual(exitCode, 0, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'skipped' },

@@ -364,2 +512,3 @@ { name: '3_another_migration.js', status: 'done', started: true },

}, { times: 1 });
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([

@@ -372,3 +521,3 @@ '1_some_already_run_migration.js',

'6_some_more_migration.js',
], getStorage(['1_some_already_run_migration.js']), [
], storage, [
{

@@ -385,3 +534,3 @@ loadableExtensions: ['.js'],

assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'done', started: true },

@@ -408,2 +557,3 @@ { name: '3_another_migration.js', status: 'done', started: true },

}, { times: 1 });
const storage = getMockedStorage(['1_some_already_run_migration.js']);
const { reporter, run } = getUpCommand([

@@ -416,3 +566,3 @@ '1_some_already_run_migration.js',

'6_some_more_migration.js',
], getStorage(['1_some_already_run_migration.js']), [
], storage, [
{

@@ -430,3 +580,3 @@ loadableExtensions: ['.js'],

assert.strictEqual(exitCode, 1, 'Exit code');
assertPreconditionsFulfilled({ dry: false }, reporter, [
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
{ name: '2_some_migration.js', status: 'done', started: true },

@@ -447,46 +597,4 @@ {

});
function getErrorCause(error) {
if (error?.cause instanceof Error) {
return error.cause;
}
if (typeof error?.cause === 'object' && error.cause !== null) {
return error.cause;
}
return undefined;
}
async function noop() {
// noop
}
function getStorage(historyEntries) {
const storage = {
lock: mock.fn(async (migrations) => migrations),
unlock: mock.fn(async () => {
// void
}),
getHistory: mock.fn(async function* () {
yield* toEntries(historyEntries);
}),
remove: mock.fn(),
onSuccess: mock.fn(),
onError: mock.fn(),
end: mock.fn(),
};
return storage;
}
function getUpCommand(migrationFiles, storage, plugins) {
const reporter = {
onFinished: mock.fn(noop),
onInit: mock.fn(noop),
onAbort: mock.fn(noop),
onCollectedMigrations: mock.fn(noop),
onLockedMigrations: mock.fn(noop),
onNewMigration: mock.fn(noop),
onMigrationRemoveStart: mock.fn(noop),
onMigrationRemoveSuccess: mock.fn(noop),
onMigrationRemoveError: mock.fn(noop),
onMigrationStart: mock.fn(noop),
onMigrationSuccess: mock.fn(noop),
onMigrationError: mock.fn(noop),
onMigrationSkip: mock.fn(noop),
};
const reporter = getMockedReporter();
const run = async (options) => {

@@ -518,3 +626,3 @@ return upCommand({

}
function assertPreconditionsFulfilled(options, reporter, expected, finishedError) {
function assertPreconditionsFulfilled(options, reporter, storage, expected, finishedError) {
assert.strictEqual(reporter.onInit.mock.calls.length, 1);

@@ -536,3 +644,5 @@ assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [

let pending = 0;
let failedAndStarted = 0;
const failedEntries = [];
const successfulEntries = [];
for (const entry of expected) {

@@ -546,2 +656,5 @@ if (entry.started) {

done++;
if (entry.started) {
successfulEntries.push(entry);
}
break;

@@ -552,2 +665,5 @@ }

failedEntries.push(entry);
if (entry.started) {
failedAndStarted++;
}
break;

@@ -570,2 +686,4 @@ }

assert.strictEqual(reporter.onMigrationError.mock.calls.length, failed, 'Failed migrations');
assert.strictEqual(storage.onSuccess.mock.calls.length, successfulEntries.length, 'Storage onSuccess calls');
assert.strictEqual(storage.onError.mock.calls.length, failedAndStarted, 'Storage onError calls');
for (const [index, entry] of failedEntries.entries()) {

@@ -577,2 +695,8 @@ if (entry.status === 'failed') {

assert.deepStrictEqual(error?.cause, cause ? deserializeError(cause) : cause, 'Error cause');
if (entry.started) {
const [finishedMigration, error] = storage.onError.mock.calls[index]?.arguments ?? [];
assert.strictEqual(finishedMigration?.name, entry.name);
assert.strictEqual(finishedMigration?.status, entry.status);
assertErrorEqualEnough(error, entry.error);
}
}

@@ -583,9 +707,3 @@ }

const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
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');
}
assertErrorEqualEnough(error, finishedError);
const cause = getErrorCause(error);

@@ -596,2 +714,8 @@ const expectedCause = finishedError?.cause;

assert.deepStrictEqual(entries.map((entry) => `${entry.name} (${entry.status})`), expected.map((entry) => `${entry.name} (${entry.status})`), 'Finished entries');
for (const [index, entry] of successfulEntries.entries()) {
const [finishedMigration] = storage.onSuccess.mock.calls[index]?.arguments ?? [];
assert.strictEqual(finishedMigration?.name, entry.name);
assert.strictEqual(finishedMigration?.status, entry.status);
}
assert.strictEqual(storage.end.mock.calls.length, 1, 'Storage end should always be called');
}

@@ -624,2 +748,16 @@ function assertPreconditionsFailed(options, reporter, finishedError) {

}
function assertErrorEqualEnough(actual, expected) {
if (expected === undefined) {
assert.strictEqual(actual, undefined);
return;
}
const { cause: actualCause, stack: actualStack, ...actualError } = actual instanceof Error ? toSerializedError(actual) : actual ?? {};
const { cause: expectedCause, stack: expectedStack, ...expectedError } = toSerializedError(expected);
// @ts-expect-error Ignore
const { stack: actualCauseStack, ...actualCauseRest } = actualCause ?? {};
// @ts-expect-error Ignore
const { stack: expectedCauseStack, ...expectedCauseRest } = expectedCause ?? {};
assert.deepStrictEqual(actualError, expectedError);
assert.deepStrictEqual(actualCauseRest, expectedCauseRest);
}
//# sourceMappingURL=up.test.js.map

@@ -50,2 +50,6 @@ /// <reference types="node" resolution-mode="require"/>

}
export declare class MigrationRemovalError extends EmigrateError {
static fromMetadata(metadata: MigrationMetadata, cause?: Error): MigrationRemovalError;
constructor(message: string | undefined, options?: ErrorOptions);
}
export declare class StorageInitError extends EmigrateError {

@@ -52,0 +56,0 @@ static fromError(error: Error): StorageInitError;

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

this.code = code;
this.name = this.constructor.name;
}

@@ -97,2 +98,10 @@ }

}
export class MigrationRemovalError extends EmigrateError {
static fromMetadata(metadata, cause) {
return new MigrationRemovalError(`Failed to remove migration: ${metadata.relativeFilePath}`, { cause });
}
constructor(message, options) {
super(message, options, 'ERR_MIGRATION_REMOVE');
}
}
export class StorageInitError extends EmigrateError {

@@ -136,2 +145,3 @@ static fromError(error) {

errorConstructors.set('MigrationNotRunError', MigrationNotRunError);
errorConstructors.set('MigrationRemovalError', MigrationRemovalError);
errorConstructors.set('StorageInitError', StorageInitError);

@@ -138,0 +148,0 @@ errorConstructors.set('CommandAbortError', CommandAbortError);

@@ -5,5 +5,13 @@ import path from 'node:path';

import { BadOptionError } from './errors.js';
const tryReadDirectory = async (directoryPath) => {
import { arrayFromAsync } from './array-from-async.js';
async function* tryReadDirectory(directoryPath) {
try {
return await fs.readdir(directoryPath);
for await (const entry of await fs.opendir(directoryPath)) {
if (entry.isFile() &&
!entry.name.startsWith('.') &&
!entry.name.startsWith('_') &&
path.extname(entry.name) !== '') {
yield entry.name;
}
}
}

@@ -13,10 +21,7 @@ catch {

}
};
}
export const getMigrations = async (cwd, directory) => {
const directoryPath = path.resolve(cwd, directory);
const allFilesInMigrationDirectory = await tryReadDirectory(directoryPath);
const migrationFiles = allFilesInMigrationDirectory
.filter((name) => !name.startsWith('.') && !name.startsWith('_') && path.extname(name) !== '')
.sort()
.map((name) => {
const allFilesInMigrationDirectory = await arrayFromAsync(tryReadDirectory(directoryPath));
return allFilesInMigrationDirectory.sort().map((name) => {
const filePath = path.join(directoryPath, name);

@@ -32,4 +37,3 @@ return {

});
return migrationFiles;
};
//# sourceMappingURL=get-migrations.js.map

@@ -5,14 +5,20 @@ import fs from 'node:fs/promises';

import { getMigrations } from './get-migrations.js';
const originalReaddir = fs.readdir;
const readdirMock = mock.fn(originalReaddir);
const originalOpendir = fs.opendir;
const opendirMock = mock.fn(originalOpendir);
describe('get-migrations', () => {
beforeEach(() => {
fs.readdir = readdirMock;
fs.opendir = opendirMock;
});
afterEach(() => {
readdirMock.mock.restore();
fs.readdir = originalReaddir;
opendirMock.mock.restore();
fs.opendir = originalOpendir;
});
it('should skip files with leading periods', async () => {
readdirMock.mock.mockImplementation(async () => ['.foo.js', 'bar.js', 'baz.js']);
opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: '.foo.js', isFile: () => true },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory');

@@ -39,3 +45,9 @@ assert.deepStrictEqual(migrations, [

it('should skip files with leading underscores', async () => {
readdirMock.mock.mockImplementation(async () => ['_foo.js', 'bar.js', 'baz.js']);
opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: '_foo.js', isFile: () => true },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory');

@@ -62,3 +74,9 @@ assert.deepStrictEqual(migrations, [

it('should skip files without file extensions', async () => {
readdirMock.mock.mockImplementation(async () => ['foo', 'bar.js', 'baz.js']);
opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: 'foo', isFile: () => true },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory');

@@ -84,4 +102,39 @@ assert.deepStrictEqual(migrations, [

});
it('should skip non-files', async () => {
opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: 'foo.js', isFile: () => false },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory');
assert.deepStrictEqual(migrations, [
{
name: 'bar.js',
filePath: '/cwd/directory/bar.js',
relativeFilePath: 'directory/bar.js',
extension: '.js',
directory: 'directory',
cwd: '/cwd/',
},
{
name: 'baz.js',
filePath: '/cwd/directory/baz.js',
relativeFilePath: 'directory/baz.js',
extension: '.js',
directory: 'directory',
cwd: '/cwd/',
},
]);
});
it('should sort them in lexicographical order', async () => {
readdirMock.mock.mockImplementation(async () => ['foo.js', 'bar_data.js', 'bar.js', 'baz.js']);
opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: 'foo.js', isFile: () => true },
{ name: 'bar_data.js', isFile: () => true },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory');

@@ -88,0 +141,0 @@ assert.deepStrictEqual(migrations, [

@@ -1,5 +0,7 @@

import { type EmigrateReporter, type MigrationMetadata, type MigrationMetadataFinished, type Storage } from '@emigrate/types';
type MigrationRunnerParameters = {
import { type EmigrateReporter, type MigrationMetadata, type MigrationMetadataFinished, type Storage, type FailedMigrationMetadata, type SuccessfulMigrationMetadata } from '@emigrate/types';
type MigrationRunnerParameters<T extends MigrationMetadata | MigrationMetadataFinished> = {
dry: boolean;
lock?: boolean;
limit?: number;
name?: string;
from?: string;

@@ -11,8 +13,11 @@ to?: string;

storage: Storage;
migrations: Array<MigrationMetadata | MigrationMetadataFinished>;
validate: (migration: MigrationMetadata) => Promise<void>;
execute: (migration: MigrationMetadata) => Promise<void>;
migrations: AsyncIterable<T>;
migrationFilter?: (migration: T) => boolean;
validate: (migration: T) => Promise<void>;
execute: (migration: T) => Promise<void>;
onSuccess: (migration: SuccessfulMigrationMetadata) => Promise<void>;
onError: (migration: FailedMigrationMetadata, error: Error) => Promise<void>;
};
export declare const migrationRunner: ({ dry, limit, from, to, abortSignal, abortRespite, reporter, storage, migrations, validate, execute, }: MigrationRunnerParameters) => Promise<Error | undefined>;
export declare const migrationRunner: <T extends MigrationMetadata | MigrationMetadataFinished>({ dry, lock, limit, name, from, to, abortSignal, abortRespite, reporter, storage, migrations, validate, execute, onSuccess, onError, migrationFilter, }: MigrationRunnerParameters<T>) => Promise<Error | undefined>;
export {};
//# sourceMappingURL=migration-runner.d.ts.map
import { hrtime } from 'node:process';
import { isFinishedMigration, isFailedMigration, } from '@emigrate/types';
import { toError, EmigrateError, MigrationRunError, toSerializedError } from './errors.js';
import { toError, EmigrateError, MigrationRunError, BadOptionError } from './errors.js';
import { exec } from './exec.js';
import { getDuration } from './get-duration.js';
export const migrationRunner = async ({ dry, limit, from, to, abortSignal, abortRespite, reporter, storage, migrations, validate, execute, }) => {
await reporter.onCollectedMigrations?.(migrations);
export const migrationRunner = async ({ dry, lock = true, limit, name, from, to, abortSignal, abortRespite, reporter, storage, migrations, validate, execute, onSuccess, onError, migrationFilter = () => true, }) => {
const validatedMigrations = [];

@@ -19,3 +18,18 @@ const migrationsToLock = [];

}, { once: true });
let nameFound = false;
let fromFound = false;
let toFound = false;
for await (const migration of migrations) {
if (name && migration.relativeFilePath === name) {
nameFound = true;
}
if (from && migration.relativeFilePath === from) {
fromFound = true;
}
if (to && migration.relativeFilePath === to) {
toFound = true;
}
if (!migrationFilter(migration)) {
continue;
}
if (isFinishedMigration(migration)) {

@@ -26,4 +40,4 @@ skip ||= migration.status === 'failed' || migration.status === 'skipped';

else if (skip ||
Boolean(from && migration.name < from) ||
Boolean(to && migration.name > to) ||
Boolean(from && migration.relativeFilePath < from) ||
Boolean(to && migration.relativeFilePath > to) ||
(limit && migrationsToLock.length >= limit)) {

@@ -60,3 +74,26 @@ validatedMigrations.push({

}
const [lockedMigrations, lockError] = dry
await reporter.onCollectedMigrations?.(validatedMigrations);
let optionError;
if (name && !nameFound) {
optionError = BadOptionError.fromOption('name', `The migration: "${name}" was not found`);
}
else if (from && !fromFound) {
optionError = BadOptionError.fromOption('from', `The "from" migration: "${from}" was not found`);
}
else if (to && !toFound) {
optionError = BadOptionError.fromOption('to', `The "to" migration: "${to}" was not found`);
}
if (optionError) {
dry = true;
skip = true;
for (const migration of migrationsToLock) {
const validatedIndex = validatedMigrations.indexOf(migration);
validatedMigrations[validatedIndex] = {
...migration,
status: 'skipped',
};
}
migrationsToLock.length = 0;
}
const [lockedMigrations, lockError] = dry || !lock
? [migrationsToLock]

@@ -75,3 +112,3 @@ : await exec(async () => storage.lock(migrationsToLock), { abortSignal, abortRespite });

}
else {
else if (lock) {
for (const migration of migrationsToLock) {

@@ -133,3 +170,3 @@ const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);

};
await storage.onError(finishedMigration, toSerializedError(migrationError));
await onError(finishedMigration, migrationError);
await reporter.onMigrationError?.(finishedMigration, migrationError);

@@ -145,3 +182,3 @@ finishedMigrations.push(finishedMigration);

};
await storage.onSuccess(finishedMigration);
await onSuccess(finishedMigration);
await reporter.onMigrationSuccess?.(finishedMigration);

@@ -151,5 +188,3 @@ finishedMigrations.push(finishedMigration);

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

@@ -162,3 +197,7 @@ const firstFailed = finishedMigrations.find(isFailedMigration);

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

@@ -165,0 +204,0 @@ return error;

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

onNewMigration(migration: MigrationMetadata, _content: string): Awaitable<void>;
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void>;
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void>;
onMigrationRemoveError(migration: MigrationMetadataFinished, _error: Error): Awaitable<void>;
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void>;

@@ -26,5 +23,2 @@ onMigrationSuccess(migration: MigrationMetadataFinished): void | PromiseLike<void>;

onNewMigration(migration: MigrationMetadata, _content: string): Awaitable<void>;
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void>;
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void>;
onMigrationRemoveError(migration: MigrationMetadataFinished, _error: Error): Awaitable<void>;
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void>;

@@ -31,0 +25,0 @@ onMigrationSuccess(migration: MigrationMetadataFinished): void | PromiseLike<void>;

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

const pretty = prettyMs(duration);
return yellow(pretty.replaceAll(/([^\s\d]+)/g, dim('$1')));
return yellow(pretty.replaceAll(/([^\s\d.]+)/g, dim('$1')));
};

@@ -17,6 +17,9 @@ const getTitle = ({ command, version, dry, cwd }) => {

};
const getMigrationStatus = (migration, activeMigration) => {
const getMigrationStatus = (command, migration, activeMigration) => {
if ('status' in migration) {
return migration.status;
return command === 'remove' && migration.status === 'done' ? 'removed' : migration.status;
}
if (command === 'remove' && migration.name === activeMigration?.name) {
return 'removing';
}
return migration.name === activeMigration?.name ? 'running' : 'pending';

@@ -26,2 +29,5 @@ };

switch (status) {
case 'removing': {
return cyan(spinner());
}
case 'running': {

@@ -33,2 +39,5 @@ return cyan(spinner());

}
case 'removed': {
return green(figures.tick);
}
case 'done': {

@@ -64,14 +73,11 @@ return green(figures.tick);

};
const getMigrationText = (migration, activeMigration) => {
const getMigrationText = (command, migration, activeMigration) => {
const pathWithoutName = migration.relativeFilePath.slice(0, -migration.name.length);
const nameWithoutExtension = migration.name.slice(0, -migration.extension.length);
const status = getMigrationStatus(migration, activeMigration);
const status = getMigrationStatus(command, migration, activeMigration);
const parts = [' ', getIcon(status)];
parts.push(`${dim(pathWithoutName)}${getName(nameWithoutExtension, status)}${dim(migration.extension)}`);
if ('status' in migration) {
parts.push(gray `(${migration.status})`);
if ('status' in migration || migration.name === activeMigration?.name) {
parts.push(gray `(${status})`);
}
else if (migration.name === activeMigration?.name) {
parts.push(gray `(running)`);
}
if ('duration' in migration && migration.duration) {

@@ -180,3 +186,2 @@ parts.push(formatDuration(migration.duration));

let failedCount = 0;
let unlockableCount = 0;
for (const migration of migrations) {

@@ -194,5 +199,2 @@ const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);

}
else {
unlockableCount += 1;
}
}

@@ -203,3 +205,2 @@ }

dim(statusText),
unlockableCount > 0 ? yellow(`(${unlockableCount} locked)`) : '',
skippedCount > 0 ? yellowBright(`(${skippedCount} skipped)`) : '',

@@ -234,12 +235,2 @@ failedCount > 0 ? redBright(`(${failedCount} failed)`) : '',

}
onMigrationRemoveStart(migration) {
this.#migrations = [migration];
this.#activeMigration = migration;
}
onMigrationRemoveSuccess(migration) {
this.#finishMigration(migration);
}
onMigrationRemoveError(migration, _error) {
this.#finishMigration(migration);
}
onMigrationStart(migration) {

@@ -281,3 +272,5 @@ this.#activeMigration = migration;

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

@@ -334,24 +327,15 @@ getSummary(this.#parameters.command, this.#migrations),

onNewMigration(migration, _content) {
console.log(getMigrationText(migration));
console.log(getMigrationText(this.#parameters.command, migration));
}
onMigrationRemoveStart(migration) {
console.log(getMigrationText(migration));
}
onMigrationRemoveSuccess(migration) {
console.log(getMigrationText(migration));
}
onMigrationRemoveError(migration, _error) {
console.error(getMigrationText(migration));
}
onMigrationStart(migration) {
console.log(getMigrationText(migration, migration));
console.log(getMigrationText(this.#parameters.command, migration, migration));
}
onMigrationSuccess(migration) {
console.log(getMigrationText(migration));
console.log(getMigrationText(this.#parameters.command, migration));
}
onMigrationError(migration, _error) {
console.error(getMigrationText(migration));
console.error(getMigrationText(this.#parameters.command, migration));
}
onMigrationSkip(migration) {
console.log(getMigrationText(migration));
console.log(getMigrationText(this.#parameters.command, migration));
}

@@ -358,0 +342,0 @@ onFinished(migrations, error) {

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

import { type FailedMigrationHistoryEntry, type MigrationHistoryEntry, type MigrationMetadata, type NonFailedMigrationHistoryEntry } from '@emigrate/types';
/// <reference types="node" resolution-mode="require"/>
import { type Mock } from 'node:test';
import { type SerializedError, type EmigrateReporter, type FailedMigrationHistoryEntry, type MigrationHistoryEntry, type MigrationMetadata, type NonFailedMigrationHistoryEntry, type Storage } from '@emigrate/types';
export type Mocked<T> = {
[K in keyof T]: Mock<T[K]>;
};
export declare function noop(): Promise<void>;
export declare function getErrorCause(error: Error | undefined): Error | SerializedError | undefined;
export declare function getMockedStorage(historyEntries: Array<string | MigrationHistoryEntry>): Mocked<Storage>;
export declare function getMockedReporter(): Mocked<Required<EmigrateReporter>>;
export declare function toMigration(cwd: string, directory: string, name: string): MigrationMetadata;

@@ -3,0 +12,0 @@ export declare function toMigrations(cwd: string, directory: string, names: string[]): MigrationMetadata[];

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

import { mock } from 'node:test';
import path from 'node:path';
export async function noop() {
// noop
}
export function getErrorCause(error) {
if (error?.cause instanceof Error) {
return error.cause;
}
if (typeof error?.cause === 'object' && error.cause !== null) {
return error.cause;
}
return undefined;
}
export function getMockedStorage(historyEntries) {
const storage = {
lock: mock.fn(async (migrations) => migrations),
unlock: mock.fn(async () => {
// void
}),
getHistory: mock.fn(async function* () {
yield* toEntries(historyEntries);
}),
remove: mock.fn(),
onSuccess: mock.fn(),
onError: mock.fn(),
end: mock.fn(),
};
return storage;
}
export function getMockedReporter() {
return {
onFinished: mock.fn(noop),
onInit: mock.fn(noop),
onAbort: mock.fn(noop),
onCollectedMigrations: mock.fn(noop),
onLockedMigrations: mock.fn(noop),
onNewMigration: mock.fn(noop),
onMigrationStart: mock.fn(noop),
onMigrationSuccess: mock.fn(noop),
onMigrationError: mock.fn(noop),
onMigrationSkip: mock.fn(noop),
};
}
export function toMigration(cwd, directory, name) {

@@ -3,0 +46,0 @@ return {

{
"name": "@emigrate/cli",
"version": "0.16.2",
"version": "0.17.0",
"publishConfig": {

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

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

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

@@ -24,2 +24,20 @@ # @emigrate/cli

```text
Usage: emigrate <options>/<command>
Options:
-h, --help Show this help message and exit
-v, --version Print version number and exit
Commands:
up Run all pending migrations (or do a dry run)
new Create a new migration file
list List all migrations and their status
remove Remove entries from the migration history
```
### `emigrate up`
```text
Usage: emigrate up [options]

@@ -32,18 +50,31 @@

-h, --help Show this help message and exit
-d, --directory <path> The directory where the migration files are located (required)
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
For example if you want to use Dotenv to load environment variables or when using TypeScript
-s, --storage <name> The storage to use for where to store the migration history (required)
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
-r, --reporter <name> The reporter to use for reporting the migration progress
-l, --limit <count> Limit the number of migrations to run
-f, --from <name> Start running migrations from the given migration name, the given name doesn't need to exist
and is compared in lexicographical order
-t, --to <name> Skip migrations after the given migration name, the given name doesn't need to exist
and is compared in lexicographical order
-f, --from <name/path> Start running migrations from the given migration name or relative file path to a migration file,
the given name or path needs to exist. The same migration and those after it lexicographically will be run
-t, --to <name/path> Skip migrations after the given migration name or relative file path to a migration file,
the given name or path needs to exist. The same migration and those before it lexicographically will be run
--dry List the pending migrations that would be run without actually running them
--color Force color output (this option is passed to the reporter)
--no-color Disable color output (this option is passed to the reporter)
--no-execution Mark the migrations as executed and successful without actually running them,
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)

@@ -50,0 +81,0 @@

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

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