233 lines
6.3 KiB
TypeScript
233 lines
6.3 KiB
TypeScript
import process from 'node:process';
|
|
import {
|
|
type PluginFromType,
|
|
type PluginType,
|
|
type GeneratorPlugin,
|
|
type EmigrateReporter,
|
|
type EmigrateStorage,
|
|
type LoaderPlugin,
|
|
type StringOrModule,
|
|
type SerializedError,
|
|
} from './types.js';
|
|
|
|
export const serializeError = (error: Error): SerializedError => {
|
|
const properties: Record<string, unknown> = {
|
|
name: error.name,
|
|
};
|
|
|
|
for (const key of Object.getOwnPropertyNames(error)) {
|
|
const value = error[key as keyof Error];
|
|
properties[key] = value instanceof Error ? serializeError(value) : value;
|
|
}
|
|
|
|
return properties as SerializedError;
|
|
};
|
|
|
|
export const isGeneratorPlugin = (plugin: any): plugin is GeneratorPlugin => {
|
|
if (!plugin || typeof plugin !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
return typeof plugin.generateMigration === 'function';
|
|
};
|
|
|
|
export const isEmigrateStorage = (plugin: any): plugin is EmigrateStorage => {
|
|
if (!plugin || typeof plugin !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
return typeof plugin.initializeStorage === 'function';
|
|
};
|
|
|
|
export const isLoaderPlugin = (plugin: any): plugin is LoaderPlugin => {
|
|
if (!plugin || typeof plugin !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
return typeof plugin.loadMigration === 'function' && Array.isArray(plugin.loadableExtensions);
|
|
};
|
|
|
|
export const isEmigrateReporter = (plugin: any): plugin is EmigrateReporter => {
|
|
if (!plugin || typeof plugin !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
const reporterFunctions = [
|
|
'onInit',
|
|
'onCollectedMigrations',
|
|
'onLockedMigrations',
|
|
'onNewMigration',
|
|
'onMigrationRemoveStart',
|
|
'onMigrationRemoveSuccess',
|
|
'onMigrationRemoveError',
|
|
'onMigrationStart',
|
|
'onMigrationSuccess',
|
|
'onMigrationError',
|
|
'onMigrationSkip',
|
|
'onFinished',
|
|
];
|
|
|
|
return reporterFunctions.some((fn) => typeof plugin[fn] === 'function');
|
|
};
|
|
|
|
export const isPluginOfType = <T extends PluginType>(type: T, plugin: any): plugin is PluginFromType<T> => {
|
|
if (type === 'generator') {
|
|
return isGeneratorPlugin(plugin);
|
|
}
|
|
|
|
if (type === 'loader') {
|
|
return isLoaderPlugin(plugin);
|
|
}
|
|
|
|
throw new Error(`Unknown plugin type: ${type}`);
|
|
};
|
|
|
|
export const getOrLoadStorage = async (
|
|
potentialStorages: Array<StringOrModule<unknown>>,
|
|
): Promise<EmigrateStorage | undefined> => {
|
|
return getOrLoad(
|
|
potentialStorages,
|
|
['@emigrate/storage-', 'emigrate-storage-', '@emigrate/plugin-storage-', '@emigrate/'],
|
|
isEmigrateStorage,
|
|
);
|
|
};
|
|
|
|
export const getOrLoadReporter = async (
|
|
potentialReporters: Array<StringOrModule<unknown>>,
|
|
): Promise<EmigrateReporter | undefined> => {
|
|
return getOrLoad(potentialReporters, ['@emigrate/reporter-', 'emigrate-reporter-', '@emigrate/'], isEmigrateReporter);
|
|
};
|
|
|
|
export const getOrLoadPlugin = async <T extends PluginType>(
|
|
type: T,
|
|
plugins: Array<StringOrModule<unknown>>,
|
|
): Promise<PluginFromType<T> | undefined> => {
|
|
return getOrLoad(
|
|
plugins,
|
|
['@emigrate/plugin-', 'emigrate-plugin-', '@emigrate/'],
|
|
(value: unknown): value is PluginFromType<T> => isPluginOfType(type, value),
|
|
);
|
|
};
|
|
|
|
export const getOrLoadPlugins = async <T extends PluginType>(
|
|
type: T,
|
|
plugins: Array<StringOrModule<unknown>>,
|
|
): Promise<Array<PluginFromType<T>>> => {
|
|
const result: Array<PluginFromType<T>> = [];
|
|
const reversePlugins = [...plugins].reverse();
|
|
|
|
for await (let plugin of reversePlugins) {
|
|
if (typeof plugin === 'function') {
|
|
plugin = await plugin();
|
|
}
|
|
|
|
if (isPluginOfType(type, plugin)) {
|
|
result.push(plugin);
|
|
continue;
|
|
}
|
|
|
|
// Support export default ...
|
|
if (plugin && typeof plugin === 'object' && 'default' in plugin && isPluginOfType(type, plugin.default)) {
|
|
result.push(plugin.default);
|
|
continue;
|
|
}
|
|
|
|
const loadedPlugin = typeof plugin === 'string' ? await loadPlugin(type, plugin) : undefined;
|
|
|
|
if (loadedPlugin) {
|
|
result.push(loadedPlugin);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
const getOrLoad = async <T>(
|
|
potentials: Array<StringOrModule<unknown>>,
|
|
prefixes: string[],
|
|
check: (value: unknown) => value is T,
|
|
) => {
|
|
const reversed = [...potentials].reverse();
|
|
|
|
for await (let potential of reversed) {
|
|
if (typeof potential === 'string') {
|
|
return load(potential, prefixes, check);
|
|
}
|
|
|
|
if (typeof potential === 'function') {
|
|
potential = await potential();
|
|
}
|
|
|
|
if (check(potential)) {
|
|
return potential;
|
|
}
|
|
|
|
// Support export default ...
|
|
if (potential && typeof potential === 'object' && 'default' in potential && check(potential.default)) {
|
|
return potential.default;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
const loadPlugin = async <T extends PluginType>(type: T, plugin: string): Promise<PluginFromType<T> | undefined> => {
|
|
return load(
|
|
plugin,
|
|
['@emigrate/plugin-', 'emigrate-plugin-', '@emigrate/'],
|
|
(value: unknown): value is PluginFromType<T> => {
|
|
return isPluginOfType(type, value);
|
|
},
|
|
);
|
|
};
|
|
|
|
const load = async <T>(
|
|
name: string,
|
|
prefixes: string[],
|
|
check: (value: unknown) => value is T,
|
|
): Promise<T | undefined> => {
|
|
const { default: importFromEsm } = await import('import-from-esm');
|
|
|
|
const importsToTry = name.startsWith('.') ? [name] : [...prefixes.map((prefix) => `${prefix}${name}`), name];
|
|
|
|
for await (const importPath of importsToTry) {
|
|
try {
|
|
const module: unknown = await importFromEsm(process.cwd(), importPath);
|
|
|
|
// Support module.exports = ...
|
|
if (check(module)) {
|
|
return module;
|
|
}
|
|
|
|
// Support export default ...
|
|
if (module && typeof module === 'object' && 'default' in module && check(module.default)) {
|
|
return module.default;
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* Get a timestamp string in the format YYYYMMDDHHmmssmmm based on the current time (UTC)
|
|
*
|
|
* Can be used to prefix migration filenames so that they are executed in the correct order
|
|
*
|
|
* @returns A timestamp string in the format YYYYMMDDHHmmssmmm
|
|
*/
|
|
export const getTimestampPrefix = () => new Date().toISOString().replaceAll(/[-:ZT.]/g, '');
|
|
|
|
/**
|
|
* A utility function to sanitize a migration name so that it can be used as a filename
|
|
*
|
|
* @param name A migration name to sanitize
|
|
* @returns A sanitized migration name that can be used as a filename
|
|
*/
|
|
export const sanitizeMigrationName = (name: string) =>
|
|
name
|
|
.replaceAll(/[\W/\\:|*?'"<>_]+/g, '_')
|
|
.trim()
|
|
.replace(/^_|_$/, '');
|