feat(cli): add --from and --to options to limit what migrations to run
This commit is contained in:
parent
02c142e39a
commit
9ef0fa2776
8 changed files with 485 additions and 346 deletions
5
.changeset/quiet-ravens-sleep.md
Normal file
5
.changeset/quiet-ravens-sleep.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add --from and --to CLI options to control which migrations to include or skip when executing migrations.
|
||||||
35
README.md
35
README.md
|
|
@ -41,6 +41,41 @@ bun add @emigrate/cli
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: emigrate up [options]
|
||||||
|
|
||||||
|
Run all pending migrations
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-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
|
||||||
|
--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)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
emigrate up --directory src/migrations -s fs
|
||||||
|
emigrate up -d ./migrations --storage @emigrate/mysql
|
||||||
|
emigrate up -d src/migrations -s postgres -r json --dry
|
||||||
|
emigrate up -d ./migrations -s mysql --import dotenv/config
|
||||||
|
emigrate up --limit 1
|
||||||
|
emigrate up --to 20231122120529381_some_migration_file.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
Create a new migration:
|
Create a new migration:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,24 @@ and "skipped" for the migrations that also haven't been run but won't because of
|
||||||
|
|
||||||
The directory where the migration files are located. The given path should be absolute or relative to the current working directory.
|
The directory where the migration files are located. The given path should be absolute or relative to the current working directory.
|
||||||
|
|
||||||
|
### `-f`, `--from <name>`
|
||||||
|
|
||||||
|
The name of the migration to start from. This can be used to run only a subset of the pending migrations.
|
||||||
|
|
||||||
|
The given migration name does not need to exist and is compared in lexicographical order with the migration names, so it can be a prefix of a migration name or similar.
|
||||||
|
|
||||||
|
Can be combined with `--dry` which will show "pending" for the migrations that would be run if not in dry-run mode,
|
||||||
|
and "skipped" for the migrations that also haven't been run but won't because of the set "from".
|
||||||
|
|
||||||
|
### `-t`, `--to <name>`
|
||||||
|
|
||||||
|
The name of the migration to end at. This can be used to run only a subset of the pending migrations.
|
||||||
|
|
||||||
|
The given migration name does not need to exist and is compared in lexicographical order with the migration names, so it can be a prefix of a migration name or similar.
|
||||||
|
|
||||||
|
Can be combined with `--dry` which will show "pending" for the migrations that would be run if not in dry-run mode,
|
||||||
|
and "skipped" for the migrations that also haven't been run but won't because of the set "to".
|
||||||
|
|
||||||
### `-i`, `--import <module>`
|
### `-i`, `--import <module>`
|
||||||
|
|
||||||
A module to import before running the migrations. This option can be specified multiple times.
|
A module to import before running the migrations. This option can be specified multiple times.
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ Options:
|
||||||
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
-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
|
-r, --reporter <name> The reporter to use for reporting the migration progress
|
||||||
-l, --limit <count> Limit the number of migrations to run
|
-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
|
||||||
--dry List the pending migrations that would be run without actually running them
|
--dry List the pending migrations that would be run without actually running them
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
@ -45,6 +49,8 @@ Examples:
|
||||||
emigrate up -d ./migrations --storage @emigrate/mysql
|
emigrate up -d ./migrations --storage @emigrate/mysql
|
||||||
emigrate up -d src/migrations -s postgres -r json --dry
|
emigrate up -d src/migrations -s postgres -r json --dry
|
||||||
emigrate up -d ./migrations -s mysql --import dotenv/config
|
emigrate up -d ./migrations -s mysql --import dotenv/config
|
||||||
|
emigrate up --limit 1
|
||||||
|
emigrate up --to 20231122120529381_some_migration_file.js
|
||||||
```
|
```
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,14 @@ const up: Action = async (args) => {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
short: 'l',
|
short: 'l',
|
||||||
},
|
},
|
||||||
|
from: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'f',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
type: 'string',
|
||||||
|
short: 't',
|
||||||
|
},
|
||||||
dry: {
|
dry: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
|
@ -85,6 +93,10 @@ Options:
|
||||||
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
-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
|
-r, --reporter <name> The reporter to use for reporting the migration progress
|
||||||
-l, --limit <count> Limit the number of migrations to run
|
-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
|
||||||
--dry List the pending migrations that would be run without actually running them
|
--dry List the pending migrations that would be run without actually running them
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
@ -95,6 +107,8 @@ Examples:
|
||||||
emigrate up -d ./migrations --storage @emigrate/mysql
|
emigrate up -d ./migrations --storage @emigrate/mysql
|
||||||
emigrate up -d src/migrations -s postgres -r json --dry
|
emigrate up -d src/migrations -s postgres -r json --dry
|
||||||
emigrate up -d ./migrations -s mysql --import dotenv/config
|
emigrate up -d ./migrations -s mysql --import dotenv/config
|
||||||
|
emigrate up --limit 1
|
||||||
|
emigrate up --to 20231122120529381_some_migration_file.js
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (values.help) {
|
if (values.help) {
|
||||||
|
|
@ -109,6 +123,8 @@ Examples:
|
||||||
storage = config.storage,
|
storage = config.storage,
|
||||||
reporter = config.reporter,
|
reporter = config.reporter,
|
||||||
dry,
|
dry,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
limit: limitString,
|
limit: limitString,
|
||||||
import: imports = [],
|
import: imports = [],
|
||||||
} = values;
|
} = values;
|
||||||
|
|
@ -135,6 +151,8 @@ Examples:
|
||||||
cwd,
|
cwd,
|
||||||
dry,
|
dry,
|
||||||
limit,
|
limit,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
color: useColors(values),
|
color: useColors(values),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ import {
|
||||||
type SerializedError,
|
type SerializedError,
|
||||||
type FailedMigrationHistoryEntry,
|
type FailedMigrationHistoryEntry,
|
||||||
type NonFailedMigrationHistoryEntry,
|
type NonFailedMigrationHistoryEntry,
|
||||||
|
type MigrationMetadataFinished,
|
||||||
} from '@emigrate/types';
|
} from '@emigrate/types';
|
||||||
import { deserializeError } from 'serialize-error';
|
import { deserializeError } from 'serialize-error';
|
||||||
import { version } from '../get-package-info.js';
|
import { version } from '../get-package-info.js';
|
||||||
|
import { BadOptionError, MigrationHistoryError, MigrationRunError, StorageInitError } from '../errors.js';
|
||||||
import upCommand from './up.js';
|
import upCommand from './up.js';
|
||||||
|
|
||||||
type Mocked<T> = {
|
type Mocked<T> = {
|
||||||
|
|
@ -27,25 +29,7 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0);
|
assert.strictEqual(exitCode, 0);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled({ dry: false }, reporter, []);
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
|
||||||
{
|
|
||||||
command: 'up',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
version,
|
|
||||||
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);
|
|
||||||
assert.deepStrictEqual(reporter.onFinished.mock.calls[0]?.arguments, [[], undefined]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when all migrations have already been run', async () => {
|
it('returns 0 and finishes without an error when all migrations have already been run', async () => {
|
||||||
|
|
@ -54,25 +38,7 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0);
|
assert.strictEqual(exitCode, 0);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled({ dry: false }, reporter, []);
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
|
||||||
{
|
|
||||||
command: 'up',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
version,
|
|
||||||
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);
|
|
||||||
assert.deepStrictEqual(reporter.onFinished.mock.calls[0]?.arguments, [[], undefined]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when all migrations have already been run even when the history responds without file extensions', async () => {
|
it('returns 0 and finishes without an error when all migrations have already been run even when the history responds without file extensions', async () => {
|
||||||
|
|
@ -81,25 +47,7 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0);
|
assert.strictEqual(exitCode, 0);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled({ dry: false }, reporter, []);
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
|
||||||
{
|
|
||||||
command: 'up',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
version,
|
|
||||||
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);
|
|
||||||
assert.deepStrictEqual(reporter.onFinished.mock.calls[0]?.arguments, [[], undefined]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin', async () => {
|
it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin', async () => {
|
||||||
|
|
@ -108,48 +56,40 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1);
|
assert.strictEqual(exitCode, 1);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled(
|
||||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1);
|
{ dry: false },
|
||||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1);
|
reporter,
|
||||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
[
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
{ name: 'some_other.js', status: 'skipped' },
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
{
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
name: 'some_file.sql',
|
||||||
const args = reporter.onFinished.mock.calls[0]?.arguments;
|
status: 'failed',
|
||||||
assert.strictEqual(args?.length, 2);
|
error: BadOptionError.fromOption('plugin', 'No loader plugin found for file extension: .sql'),
|
||||||
const entries = args[0];
|
},
|
||||||
const error = args[1];
|
],
|
||||||
assert.deepStrictEqual(
|
BadOptionError.fromOption('plugin', 'No loader plugin found for file extension: .sql'),
|
||||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
|
||||||
['some_other.js (skipped)', 'some_file.sql (failed)'],
|
|
||||||
);
|
);
|
||||||
assert.strictEqual(entries.length, 2);
|
|
||||||
assert.strictEqual(error?.message, 'No loader plugin found for file extension: .sql');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
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 { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], getStorage([]));
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run({ dry: true });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1);
|
assert.strictEqual(exitCode, 1);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled(
|
||||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1);
|
{ dry: true },
|
||||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1);
|
reporter,
|
||||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
[
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
{ name: 'some_other.js', status: 'skipped' },
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
{
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
name: 'some_file.sql',
|
||||||
const args = reporter.onFinished.mock.calls[0]?.arguments;
|
status: 'failed',
|
||||||
assert.strictEqual(args?.length, 2);
|
error: BadOptionError.fromOption('plugin', 'No loader plugin found for file extension: .sql'),
|
||||||
const entries = args[0];
|
},
|
||||||
const error = args[1];
|
],
|
||||||
assert.strictEqual(entries.length, 2);
|
BadOptionError.fromOption('plugin', 'No loader plugin found for file extension: .sql'),
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when there are failed migrations in the history', async () => {
|
it('returns 1 and finishes with an error when there are failed migrations in the history', async () => {
|
||||||
|
|
@ -159,38 +99,24 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1);
|
assert.strictEqual(exitCode, 1);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled(
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
{ dry: false },
|
||||||
|
reporter,
|
||||||
|
[
|
||||||
{
|
{
|
||||||
command: 'up',
|
name: 'some_failed_migration.js',
|
||||||
cwd: '/emigrate',
|
status: 'failed',
|
||||||
version,
|
error: new MigrationHistoryError(
|
||||||
dry: false,
|
'Migration some_failed_migration.js is in a failed state, it should be fixed and removed',
|
||||||
color: undefined,
|
{ cause: failedEntry.error },
|
||||||
directory: 'migrations',
|
),
|
||||||
},
|
},
|
||||||
]);
|
{ name: 'some_file.js', status: 'skipped' },
|
||||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1);
|
],
|
||||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1);
|
new MigrationHistoryError(
|
||||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
'Migration some_failed_migration.js is in a failed state, it should be fixed and removed',
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
{ cause: failedEntry.error },
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
),
|
||||||
assert.deepStrictEqual(
|
|
||||||
getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]),
|
|
||||||
deserializeError(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.deepStrictEqual(getErrorCause(error), deserializeError(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)'],
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -201,38 +127,24 @@ describe('up', () => {
|
||||||
const exitCode = await run({ dry: true });
|
const exitCode = await run({ dry: true });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1);
|
assert.strictEqual(exitCode, 1);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled(
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
{ dry: true },
|
||||||
|
reporter,
|
||||||
|
[
|
||||||
{
|
{
|
||||||
command: 'up',
|
name: 'some_failed_migration.js',
|
||||||
cwd: '/emigrate',
|
status: 'failed',
|
||||||
version,
|
error: new MigrationHistoryError(
|
||||||
dry: true,
|
'Migration some_failed_migration.js is in a failed state, it should be fixed and removed',
|
||||||
color: undefined,
|
{ cause: failedEntry.error },
|
||||||
directory: 'migrations',
|
),
|
||||||
},
|
},
|
||||||
]);
|
{ name: 'some_file.js', status: 'skipped' },
|
||||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1);
|
],
|
||||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1);
|
new MigrationHistoryError(
|
||||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
'Migration some_failed_migration.js is in a failed state, it should be fixed and removed',
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
{ cause: failedEntry.error },
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
),
|
||||||
assert.deepStrictEqual(
|
|
||||||
getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]),
|
|
||||||
deserializeError(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.deepStrictEqual(getErrorCause(error), deserializeError(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)'],
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -243,25 +155,7 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0);
|
assert.strictEqual(exitCode, 0);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled({ dry: false }, reporter, []);
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
|
||||||
{
|
|
||||||
command: 'up',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
version,
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
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);
|
|
||||||
assert.deepStrictEqual(reporter.onFinished.mock.calls[0]?.arguments, [[], undefined]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
||||||
|
|
@ -270,32 +164,7 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1);
|
assert.strictEqual(exitCode, 1);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFailed({ dry: false }, reporter, StorageInitError.fromError(new Error('No storage configured')));
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
|
||||||
{
|
|
||||||
command: 'up',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
version,
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
directory: 'migrations',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 0);
|
|
||||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0);
|
|
||||||
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 entries = args[0];
|
|
||||||
const error = args[1];
|
|
||||||
const cause = getErrorCause(error);
|
|
||||||
assert.deepStrictEqual(entries, []);
|
|
||||||
assert.strictEqual(error?.message, 'Could not initialize storage');
|
|
||||||
assert.strictEqual(cause?.message, 'No storage configured');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when all pending migrations are run successfully', async () => {
|
it('returns 0 and finishes without an error when all pending migrations are run successfully', async () => {
|
||||||
|
|
@ -317,31 +186,10 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0);
|
assert.strictEqual(exitCode, 0);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
{ name: 'some_migration.js', status: 'done', started: true },
|
||||||
{
|
{ name: 'some_other_migration.js', status: 'done', started: true },
|
||||||
command: 'up',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
version,
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
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 0 and finishes without an error when the given number of pending migrations are run successfully', async () => {
|
it('returns 0 and finishes without an error when the given number of pending migrations are run successfully', async () => {
|
||||||
|
|
@ -363,31 +211,10 @@ describe('up', () => {
|
||||||
const exitCode = await run({ limit: 1 });
|
const exitCode = await run({ limit: 1 });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0);
|
assert.strictEqual(exitCode, 0);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
{ name: 'some_migration.js', status: 'done', started: true },
|
||||||
{
|
{ name: 'some_other_migration.js', status: 'skipped' },
|
||||||
command: 'up',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
version,
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
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, 1);
|
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 1);
|
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0);
|
|
||||||
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, undefined);
|
|
||||||
assert.strictEqual(entries?.length, 2);
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
|
||||||
['some_migration.js (done)', 'some_other_migration.js (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 () => {
|
it('returns 0 and finishes without an error with the given number of pending migrations are validated and listed successfully in dry-mode', async () => {
|
||||||
|
|
@ -399,31 +226,125 @@ describe('up', () => {
|
||||||
const exitCode = await run({ dry: true, limit: 1 });
|
const exitCode = await run({ dry: true, limit: 1 });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0);
|
assert.strictEqual(exitCode, 0);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled({ dry: true }, reporter, [
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
{ name: 'some_migration.js', status: 'pending' },
|
||||||
{
|
{ name: 'some_other_migration.js', status: 'skipped' },
|
||||||
command: 'up',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
version,
|
|
||||||
dry: true,
|
|
||||||
color: undefined,
|
|
||||||
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);
|
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 () => {
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
const { reporter, run } = getUpCommand(
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0);
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 2);
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
[
|
||||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
{
|
||||||
assert.strictEqual(error, undefined);
|
loadableExtensions: ['.js'],
|
||||||
assert.strictEqual(entries?.length, 2);
|
async loadMigration() {
|
||||||
assert.deepStrictEqual(
|
return async () => {
|
||||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
// Success
|
||||||
['some_migration.js (pending)', 'some_other_migration.js (skipped)'],
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const exitCode = await run({ from: '3_non_existing_migration.js' });
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 0);
|
||||||
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
|
{ name: '2_some_migration.js', status: 'skipped' },
|
||||||
|
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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' });
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 0);
|
||||||
|
assertPreconditionsFulfilled({ dry: true }, reporter, [
|
||||||
|
{ name: '2_some_migration.js', status: 'skipped' },
|
||||||
|
{ name: '4_some_other_migration.js', status: 'pending' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
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']),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
loadableExtensions: ['.js'],
|
||||||
|
async loadMigration() {
|
||||||
|
return async () => {
|
||||||
|
// Success
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const exitCode = await run({ to: '3_non_existing_migration.js' });
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 0);
|
||||||
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
|
{ name: '2_some_migration.js', status: 'done', started: true },
|
||||||
|
{ name: '4_some_other_migration.js', status: 'skipped' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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' });
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 0);
|
||||||
|
assertPreconditionsFulfilled({ dry: true }, reporter, [
|
||||||
|
{ name: '2_some_migration.js', status: 'pending' },
|
||||||
|
{ name: '4_some_other_migration.js', status: 'skipped' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 and finishes without an error when the pending migrations fulfilling "from", "to" and "limit" are run successfully', async () => {
|
||||||
|
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 async () => {
|
||||||
|
// Success
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const exitCode = await run({ from: '3_another_migration.js', to: '5_yet_another_migration.js', limit: 2 });
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 0);
|
||||||
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
|
{ name: '2_some_migration.js', status: 'skipped' },
|
||||||
|
{ name: '3_another_migration.js', status: 'done', started: true },
|
||||||
|
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
||||||
|
{ name: '5_yet_another_migration.js', status: 'skipped' },
|
||||||
|
{ name: '6_some_more_migration.js', status: 'skipped' },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
|
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
|
||||||
|
|
@ -447,33 +368,15 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1);
|
assert.strictEqual(exitCode, 1);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assertPreconditionsFulfilled(
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
{ dry: false },
|
||||||
{
|
reporter,
|
||||||
command: 'up',
|
[
|
||||||
cwd: '/emigrate',
|
{ name: 'some_migration.js', status: 'done', started: true },
|
||||||
version,
|
{ name: 'fail.js', status: 'failed', started: true, error: new Error('Oh noes!') },
|
||||||
dry: false,
|
{ name: 'some_other_migration.js', status: 'skipped' },
|
||||||
color: undefined,
|
],
|
||||||
directory: 'migrations',
|
new MigrationRunError('Failed to run migration: migrations/fail.js', { cause: new Error('Oh noes!') }),
|
||||||
},
|
|
||||||
]);
|
|
||||||
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)'],
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -610,3 +513,127 @@ function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugi
|
||||||
run,
|
run,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertPreconditionsFulfilled(
|
||||||
|
options: { dry: boolean },
|
||||||
|
reporter: Mocked<Required<EmigrateReporter>>,
|
||||||
|
expected: Array<{ name: string; status: MigrationMetadataFinished['status']; started?: boolean; error?: Error }>,
|
||||||
|
finishedError?: Error,
|
||||||
|
) {
|
||||||
|
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
||||||
|
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
||||||
|
{
|
||||||
|
command: 'up',
|
||||||
|
cwd: '/emigrate',
|
||||||
|
version,
|
||||||
|
dry: options.dry,
|
||||||
|
color: undefined,
|
||||||
|
directory: 'migrations',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let started = 0;
|
||||||
|
let done = 0;
|
||||||
|
let failed = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let pending = 0;
|
||||||
|
const failedEntries: typeof expected = [];
|
||||||
|
|
||||||
|
for (const entry of expected) {
|
||||||
|
if (entry.started) {
|
||||||
|
started++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line default-case
|
||||||
|
switch (entry.status) {
|
||||||
|
case 'done': {
|
||||||
|
done++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'failed': {
|
||||||
|
failed++;
|
||||||
|
failedEntries.push(entry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'skipped': {
|
||||||
|
skipped++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'pending': {
|
||||||
|
pending++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1, 'Collected call');
|
||||||
|
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1, 'Locked call');
|
||||||
|
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, started, 'Started migrations');
|
||||||
|
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, done, 'Successful migrations');
|
||||||
|
assert.strictEqual(reporter.onMigrationError.mock.calls.length, failed, 'Failed migrations');
|
||||||
|
|
||||||
|
for (const [index, entry] of failedEntries.entries()) {
|
||||||
|
if (entry.status === 'failed') {
|
||||||
|
const error = reporter.onMigrationError.mock.calls[index]?.arguments[1];
|
||||||
|
assert.deepStrictEqual(error, entry.error, 'Error');
|
||||||
|
const cause = entry.error?.cause;
|
||||||
|
assert.deepStrictEqual(error?.cause, cause ? deserializeError(cause) : cause, 'Error cause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, pending + skipped, 'Total pending and skipped');
|
||||||
|
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
||||||
|
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||||
|
assert.deepStrictEqual(error, finishedError, 'Finished error');
|
||||||
|
const cause = getErrorCause(error);
|
||||||
|
const expectedCause = finishedError?.cause;
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
cause,
|
||||||
|
expectedCause ? deserializeError(expectedCause) : expectedCause,
|
||||||
|
'Finished error cause',
|
||||||
|
);
|
||||||
|
assert.strictEqual(entries?.length, expected.length, 'Finished entries length');
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||||
|
expected.map((entry) => `${entry.name} (${entry.status})`),
|
||||||
|
'Finished entries',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPreconditionsFailed(
|
||||||
|
options: { dry: boolean },
|
||||||
|
reporter: Mocked<Required<EmigrateReporter>>,
|
||||||
|
finishedError?: Error,
|
||||||
|
) {
|
||||||
|
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
||||||
|
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
||||||
|
{
|
||||||
|
command: 'up',
|
||||||
|
cwd: '/emigrate',
|
||||||
|
version,
|
||||||
|
dry: options.dry,
|
||||||
|
color: undefined,
|
||||||
|
directory: 'migrations',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 0, 'Collected call');
|
||||||
|
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0, 'Locked call');
|
||||||
|
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0, 'Started migrations');
|
||||||
|
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0, 'Successful migrations');
|
||||||
|
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0, 'Failed migrations');
|
||||||
|
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0, 'Total pending and skipped');
|
||||||
|
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
||||||
|
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||||
|
assert.deepStrictEqual(error, finishedError, 'Finished error');
|
||||||
|
const cause = getErrorCause(error);
|
||||||
|
const expectedCause = finishedError?.cause;
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
cause,
|
||||||
|
expectedCause ? deserializeError(expectedCause) : expectedCause,
|
||||||
|
'Finished error cause',
|
||||||
|
);
|
||||||
|
assert.strictEqual(entries?.length, 0, 'Finished entries length');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ type ExtraFlags = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
dry?: boolean;
|
dry?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
getMigrations?: GetMigrationsFunction;
|
getMigrations?: GetMigrationsFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -27,6 +29,8 @@ export default async function upCommand({
|
||||||
directory,
|
directory,
|
||||||
color,
|
color,
|
||||||
limit,
|
limit,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
dry = false,
|
dry = false,
|
||||||
plugins = [],
|
plugins = [],
|
||||||
cwd,
|
cwd,
|
||||||
|
|
@ -86,6 +90,8 @@ export default async function upCommand({
|
||||||
const error = await migrationRunner({
|
const error = await migrationRunner({
|
||||||
dry,
|
dry,
|
||||||
limit,
|
limit,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
storage,
|
||||||
migrations: await arrayFromAsync(collectedMigrations),
|
migrations: await arrayFromAsync(collectedMigrations),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import { getDuration } from './get-duration.js';
|
||||||
type MigrationRunnerParameters = {
|
type MigrationRunnerParameters = {
|
||||||
dry: boolean;
|
dry: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
reporter: EmigrateReporter;
|
reporter: EmigrateReporter;
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
migrations: Array<MigrationMetadata | MigrationMetadataFinished>;
|
migrations: Array<MigrationMetadata | MigrationMetadataFinished>;
|
||||||
|
|
@ -26,6 +28,8 @@ type MigrationRunnerParameters = {
|
||||||
export const migrationRunner = async ({
|
export const migrationRunner = async ({
|
||||||
dry,
|
dry,
|
||||||
limit,
|
limit,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
storage,
|
||||||
migrations,
|
migrations,
|
||||||
|
|
@ -34,8 +38,8 @@ export const migrationRunner = async ({
|
||||||
}: MigrationRunnerParameters): Promise<Error | undefined> => {
|
}: MigrationRunnerParameters): Promise<Error | undefined> => {
|
||||||
await reporter.onCollectedMigrations?.(migrations);
|
await reporter.onCollectedMigrations?.(migrations);
|
||||||
|
|
||||||
const finishedMigrations: MigrationMetadataFinished[] = [];
|
const validatedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
||||||
const migrationsToRun: MigrationMetadata[] = [];
|
const migrationsToLock: MigrationMetadata[] = [];
|
||||||
|
|
||||||
let skip = false;
|
let skip = false;
|
||||||
|
|
||||||
|
|
@ -43,24 +47,35 @@ export const migrationRunner = async ({
|
||||||
if (isFinishedMigration(migration)) {
|
if (isFinishedMigration(migration)) {
|
||||||
skip ||= migration.status === 'failed' || migration.status === 'skipped';
|
skip ||= migration.status === 'failed' || migration.status === 'skipped';
|
||||||
|
|
||||||
finishedMigrations.push(migration);
|
validatedMigrations.push(migration);
|
||||||
} else if (skip) {
|
} else if (
|
||||||
finishedMigrations.push({
|
skip ||
|
||||||
|
Boolean(from && migration.name < from) ||
|
||||||
|
Boolean(to && migration.name > to) ||
|
||||||
|
(limit && migrationsToLock.length >= limit)
|
||||||
|
) {
|
||||||
|
validatedMigrations.push({
|
||||||
...migration,
|
...migration,
|
||||||
status: dry ? 'pending' : 'skipped',
|
status: 'skipped',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await validate(migration);
|
await validate(migration);
|
||||||
migrationsToRun.push(migration);
|
migrationsToLock.push(migration);
|
||||||
|
validatedMigrations.push(migration);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for await (const migration of migrationsToRun) {
|
for (const migration of migrationsToLock) {
|
||||||
finishedMigrations.push({ ...migration, status: 'skipped' });
|
const validatedIndex = validatedMigrations.indexOf(migration);
|
||||||
|
|
||||||
|
validatedMigrations[validatedIndex] = {
|
||||||
|
...migration,
|
||||||
|
status: 'skipped',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationsToRun.length = 0;
|
migrationsToLock.length = 0;
|
||||||
|
|
||||||
finishedMigrations.push({
|
validatedMigrations.push({
|
||||||
...migration,
|
...migration,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
|
@ -72,50 +87,70 @@ export const migrationRunner = async ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrationsToLock = limit ? migrationsToRun.slice(0, limit) : migrationsToRun;
|
|
||||||
const migrationsToSkip = limit ? migrationsToRun.slice(limit) : [];
|
|
||||||
|
|
||||||
const [lockedMigrations, lockError] = dry
|
const [lockedMigrations, lockError] = dry
|
||||||
? [migrationsToLock]
|
? [migrationsToLock]
|
||||||
: await exec(async () => storage.lock(migrationsToLock));
|
: await exec(async () => storage.lock(migrationsToLock));
|
||||||
|
|
||||||
if (lockError) {
|
if (lockError) {
|
||||||
for await (const migration of migrationsToRun) {
|
for (const migration of migrationsToLock) {
|
||||||
finishedMigrations.push({ ...migration, status: 'skipped' });
|
const validatedIndex = validatedMigrations.indexOf(migration);
|
||||||
|
|
||||||
|
validatedMigrations[validatedIndex] = {
|
||||||
|
...migration,
|
||||||
|
status: 'skipped',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationsToRun.length = 0;
|
migrationsToLock.length = 0;
|
||||||
|
|
||||||
skip = true;
|
skip = true;
|
||||||
} else {
|
} else {
|
||||||
|
for (const migration of migrationsToLock) {
|
||||||
|
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
||||||
|
|
||||||
|
if (!isLocked) {
|
||||||
|
const validatedIndex = validatedMigrations.indexOf(migration);
|
||||||
|
|
||||||
|
validatedMigrations[validatedIndex] = {
|
||||||
|
...migration,
|
||||||
|
status: 'skipped',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await reporter.onLockedMigrations?.(lockedMigrations);
|
await reporter.onLockedMigrations?.(lockedMigrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const finishedMigration of finishedMigrations) {
|
const finishedMigrations: MigrationMetadataFinished[] = [];
|
||||||
switch (finishedMigration.status) {
|
|
||||||
|
for await (const migration of validatedMigrations) {
|
||||||
|
if (isFinishedMigration(migration)) {
|
||||||
|
switch (migration.status) {
|
||||||
case 'failed': {
|
case 'failed': {
|
||||||
await reporter.onMigrationError?.(finishedMigration, finishedMigration.error);
|
await reporter.onMigrationError?.(migration, migration.error);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'pending': {
|
case 'pending': {
|
||||||
await reporter.onMigrationSkip?.(finishedMigration);
|
await reporter.onMigrationSkip?.(migration);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'skipped': {
|
case 'skipped': {
|
||||||
await reporter.onMigrationSkip?.(finishedMigration);
|
await reporter.onMigrationSkip?.(migration);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
await reporter.onMigrationSuccess?.(finishedMigration);
|
await reporter.onMigrationSuccess?.(migration);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finishedMigrations.push(migration);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const migration of lockedMigrations ?? []) {
|
|
||||||
if (dry || skip) {
|
if (dry || skip) {
|
||||||
const finishedMigration: MigrationMetadataFinished = {
|
const finishedMigration: MigrationMetadataFinished = {
|
||||||
...migration,
|
...migration,
|
||||||
|
|
@ -159,17 +194,6 @@ export const migrationRunner = async ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const migration of migrationsToSkip) {
|
|
||||||
const finishedMigration: MigrationMetadataFinished = {
|
|
||||||
...migration,
|
|
||||||
status: 'skipped',
|
|
||||||
};
|
|
||||||
|
|
||||||
await reporter.onMigrationSkip?.(finishedMigration);
|
|
||||||
|
|
||||||
finishedMigrations.push(finishedMigration);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
|
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue