Skip to content

Commit

Permalink
feat(cli): implement the "up" command with support for "storage" and …
Browse files Browse the repository at this point in the history
…"loader" plugins
  • Loading branch information
joakimbeng committed Nov 15, 2023
1 parent a058ebf commit b56794a
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-toys-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@emigrate/cli': minor
---

Implement the "up" command with support for "storage" and "loader" plugins
49 changes: 33 additions & 16 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import { getConfig } from './get-config.js';
type Action = (args: string[]) => Promise<void>;

const up: Action = async (args) => {
const config = await getConfig('up');
const { values } = parseArgs({
args,
options: {
help: {
type: 'boolean',
short: 'h',
},
dir: {
directory: {
type: 'string',
short: 'd',
},
dry: {
type: 'boolean',
},
plugin: {
type: 'string',
short: 'p',
Expand All @@ -28,33 +32,46 @@ const up: Action = async (args) => {
allowPositionals: false,
});

const showHelp = !values.dir || values.help;

if (!values.dir) {
console.error('Missing required option: --dir\n');
}

if (showHelp) {
console.log(`Usage: emigrate up [options]
const usage = `Usage: emigrate up [options]
Run all pending migrations
Options:
-h, --help Show this help message and exit
-d, --dir The directory where the migration files are located (required)
-p, --plugin The plugin(s) to use (can be specified multiple times)
-h, --help Show this help message and exit
-d, --directory The directory where the migration files are located (required)
-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:
emigrate up --dir src/migrations
emigrate up --dir ./migrations --plugin @emigrate/plugin-storage-mysql
`);
emigrate up --directory src/migrations
emigrate up -d ./migrations --plugin @emigrate/plugin-storage-mysql
emigrate up -d src/migrations --dry
`;

if (values.help) {
console.log(usage);
process.exitCode = 1;
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) => {
Expand Down
117 changes: 117 additions & 0 deletions packages/cli/src/up-command.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}

0 comments on commit b56794a

Please sign in to comment.