96 lines
3.2 KiB
JavaScript
96 lines
3.2 KiB
JavaScript
/**
|
|
* Detects migration files that share the same numeric prefix.
|
|
*
|
|
* node-pg-migrate orders migrations by their leading number, so two files with
|
|
* the same prefix (e.g. `047_a.sql` and `047_b.sql`) have an ambiguous order and
|
|
* can apply inconsistently across environments. This guard runs in CI (and as a
|
|
* unit test) to stop new duplicates from landing on main.
|
|
*
|
|
* A small set of historical duplicates already exists on main and is grandfathered
|
|
* in via ALLOWED_DUPLICATES — those are tolerated, anything new is rejected.
|
|
*
|
|
* Exports `findDuplicateMigrations()` for the test suite; runs as a CLI (exit 1 on
|
|
* new duplicates) when executed directly.
|
|
*/
|
|
/* global console, process */
|
|
import { readdirSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
export const MIGRATIONS_DIR = join(__dirname, '..', 'src', 'infra', 'database', 'migrations');
|
|
|
|
// Pre-existing duplicate prefixes on main. Do not add to this list — fix the
|
|
// duplicate instead. These remain only because the migrations already shipped.
|
|
export const ALLOWED_DUPLICATES = new Set(['033', '047']);
|
|
|
|
const MIGRATION_FILE = /^(\d+)_.*\.sql$/;
|
|
|
|
/**
|
|
* @param {string} [dir] migrations directory to scan
|
|
* @returns {{ count: number, newDuplicates: Array<{prefix:string, files:string[]}>,
|
|
* grandfathered: Array<{prefix:string, files:string[]}>, nextPrefix: string }}
|
|
*/
|
|
export function findDuplicateMigrations(dir = MIGRATIONS_DIR) {
|
|
const byPrefix = new Map();
|
|
|
|
for (const name of readdirSync(dir)) {
|
|
const match = MIGRATION_FILE.exec(name);
|
|
if (!match) continue;
|
|
const prefix = match[1];
|
|
if (!byPrefix.has(prefix)) byPrefix.set(prefix, []);
|
|
byPrefix.get(prefix).push(name);
|
|
}
|
|
|
|
const newDuplicates = [];
|
|
const grandfathered = [];
|
|
|
|
for (const [prefix, files] of byPrefix) {
|
|
if (files.length < 2) continue;
|
|
files.sort();
|
|
(ALLOWED_DUPLICATES.has(prefix) ? grandfathered : newDuplicates).push({ prefix, files });
|
|
}
|
|
|
|
const maxPrefix = byPrefix.size
|
|
? Math.max(...[...byPrefix.keys()].map((p) => parseInt(p, 10)))
|
|
: -1;
|
|
|
|
return {
|
|
count: byPrefix.size,
|
|
newDuplicates,
|
|
grandfathered,
|
|
nextPrefix: String(maxPrefix + 1).padStart(3, '0'),
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
const { count, newDuplicates, grandfathered, nextPrefix } = findDuplicateMigrations();
|
|
|
|
if (grandfathered.length > 0) {
|
|
console.log('Known (grandfathered) duplicate migration prefixes:');
|
|
for (const { prefix, files } of grandfathered) {
|
|
console.log(` ${prefix}: ${files.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
if (newDuplicates.length > 0) {
|
|
console.error('\nERROR: duplicate migration number(s) detected:');
|
|
for (const { prefix, files } of newDuplicates) {
|
|
console.error(` ${prefix}: ${files.join(', ')}`);
|
|
}
|
|
console.error(
|
|
`\nEach migration needs a unique number. Renumber the new file to the next ` +
|
|
`available prefix (currently ${nextPrefix}_).`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\nOK: ${count} unique migration number(s), no new duplicates.`);
|
|
}
|
|
|
|
// Run as a CLI only when executed directly, not when imported by tests.
|
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
main();
|
|
}
|