feat(cli): implement the "up" command with support for "storage" and "loader" plugins
This commit is contained in:
parent
a058ebf888
commit
b56794a269
3 changed files with 155 additions and 16 deletions
5
.changeset/happy-toys-smash.md
Normal file
5
.changeset/happy-toys-smash.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement the "up" command with support for "storage" and "loader" plugins
|
||||||
|
|
@ -7,6 +7,7 @@ import { getConfig } from './get-config.js';
|
||||||
type Action = (args: string[]) => Promise<void>;
|
type Action = (args: string[]) => Promise<void>;
|
||||||
|
|
||||||
const up: Action = async (args) => {
|
const up: Action = async (args) => {
|
||||||
|
const config = await getConfig('up');
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
args,
|
args,
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -14,10 +15,13 @@ const up: Action = async (args) => {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
short: 'h',
|
short: 'h',
|
||||||
},
|
},
|
||||||
dir: {
|
directory: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
short: 'd',
|
short: 'd',
|
||||||
},
|
},
|
||||||
|
dry: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
plugin: {
|
plugin: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
short: 'p',
|
short: 'p',
|
||||||
|
|
@ -28,33 +32,46 @@ const up: Action = async (args) => {
|
||||||
allowPositionals: false,
|
allowPositionals: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showHelp = !values.dir || values.help;
|
const usage = `Usage: emigrate up [options]
|
||||||
|
|
||||||
if (!values.dir) {
|
|
||||||
console.error('Missing required option: --dir\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showHelp) {
|
|
||||||
console.log(`Usage: emigrate up [options]
|
|
||||||
|
|
||||||
Run all pending migrations
|
Run all pending migrations
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
-d, --dir The directory where the migration files are located (required)
|
-d, --directory The directory where the migration files are located (required)
|
||||||
-p, --plugin The plugin(s) to use (can be specified multiple times)
|
-p, --plugin The plugin(s) to use (can be specified multiple times)
|
||||||
|
--dry List the pending migrations that would be run without actually running them
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
emigrate up --dir src/migrations
|
emigrate up --directory src/migrations
|
||||||
emigrate up --dir ./migrations --plugin @emigrate/plugin-storage-mysql
|
emigrate up -d ./migrations --plugin @emigrate/plugin-storage-mysql
|
||||||
`);
|
emigrate up -d src/migrations --dry
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (values.help) {
|
||||||
|
console.log(usage);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(values);
|
const { directory = config.directory, dry } = values;
|
||||||
|
const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { default: upCommand } = await import('./up-command.js');
|
||||||
|
await upCommand({ directory, plugins, dry });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ShowUsageError) {
|
||||||
|
console.error(error.message, '\n');
|
||||||
|
console.log(usage);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const newMigration: Action = async (args) => {
|
const newMigration: Action = async (args) => {
|
||||||
|
|
|
||||||
117
packages/cli/src/up-command.ts
Normal file
117
packages/cli/src/up-command.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import process from 'node:process';
|
||||||
|
import { getOrLoadPlugin, getOrLoadPlugins } from '@emigrate/plugin-tools';
|
||||||
|
import { type LoaderPlugin } from '@emigrate/plugin-tools/types';
|
||||||
|
import { ShowUsageError } from './show-usage-error.js';
|
||||||
|
import { type Config } from './types.js';
|
||||||
|
import { stripLeadingPeriod } from './strip-leading-period.js';
|
||||||
|
|
||||||
|
type ExtraFlags = {
|
||||||
|
dry?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function upCommand({ directory, dry, plugins = [] }: Config & ExtraFlags) {
|
||||||
|
if (!directory) {
|
||||||
|
throw new ShowUsageError('Missing required option: directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
const storagePlugin = await getOrLoadPlugin('storage', plugins);
|
||||||
|
|
||||||
|
if (!storagePlugin) {
|
||||||
|
throw new Error('No storage plugin found, please specify a storage plugin using the plugin option');
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = await storagePlugin.initializeStorage();
|
||||||
|
const path = await import('node:path');
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
|
||||||
|
const allFilesInMigrationDirectory = await fs.readdir(path.resolve(process.cwd(), directory), {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const migrationFiles = allFilesInMigrationDirectory
|
||||||
|
.filter((file) => file.isFile() && !file.name.startsWith('.') && !file.name.startsWith('_'))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((file) => file.name);
|
||||||
|
|
||||||
|
for await (const migrationHistoryEntry of storage.getHistory()) {
|
||||||
|
if (migrationFiles.includes(migrationHistoryEntry.name)) {
|
||||||
|
migrationFiles.splice(migrationFiles.indexOf(migrationHistoryEntry.name), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationFileExtensions = new Set(migrationFiles.map((file) => stripLeadingPeriod(path.extname(file))));
|
||||||
|
const loaderPlugins = await getOrLoadPlugins('loader', plugins);
|
||||||
|
|
||||||
|
const loaderByExtension = new Map<string, LoaderPlugin | undefined>(
|
||||||
|
[...migrationFileExtensions].map(
|
||||||
|
(extension) =>
|
||||||
|
[
|
||||||
|
extension,
|
||||||
|
loaderPlugins.find((plugin) =>
|
||||||
|
plugin.loadableExtensions.some((loadableExtension) => stripLeadingPeriod(loadableExtension) === extension),
|
||||||
|
),
|
||||||
|
] as const,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [extension, loader] of loaderByExtension) {
|
||||||
|
if (!loader) {
|
||||||
|
throw new Error(`No loader plugin found for file extension: ${extension}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dry) {
|
||||||
|
console.log('Pending migrations:');
|
||||||
|
console.log(migrationFiles.map((file) => ` - ${file}`).join('\n'));
|
||||||
|
console.log('\nDry run, exiting...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockedMigrationFiles = await storage.lock(migrationFiles);
|
||||||
|
|
||||||
|
let cleaningUp = false;
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
if (cleaningUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.off('SIGINT', cleanup);
|
||||||
|
process.off('SIGTERM', cleanup);
|
||||||
|
|
||||||
|
cleaningUp = true;
|
||||||
|
await storage.unlock(lockedMigrationFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', cleanup);
|
||||||
|
process.on('SIGTERM', cleanup);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const name of lockedMigrationFiles) {
|
||||||
|
console.log(' -', name, '...');
|
||||||
|
|
||||||
|
const extension = stripLeadingPeriod(path.extname(name));
|
||||||
|
const filename = path.resolve(process.cwd(), directory, name);
|
||||||
|
const loader = loaderByExtension.get(extension)!;
|
||||||
|
|
||||||
|
const migration = await loader.loadMigration({ name, filename, extension });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await migration();
|
||||||
|
|
||||||
|
console.log(' -', name, 'done');
|
||||||
|
|
||||||
|
await storage.onSuccess(name);
|
||||||
|
} catch (error) {
|
||||||
|
const errorInstance = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
|
console.error(' -', name, 'failed:', errorInstance.message);
|
||||||
|
|
||||||
|
await storage.onError(name, errorInstance);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue