scripts/generate-init-sql.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
/**
* Concatenates supabase/migrations/*.sql (sorted by name) into
* supabase/init.sql, a single file self-hosters can run in the Supabase SQL
* Editor without needing the Supabase CLI.
*
* Migrations listed in supabase/superseded-migrations.txt are left out, the
* same set CI removes before `supabase db reset`. The SQL Editor sends the
* whole file as one query, so a single failing statement rolls back every
* statement since the last COMMIT and skips the rest of the file. Including a
* migration that only works because production fake-applied it therefore
* produces a half-installed database.
*
* Usage:
* node scripts/generate-init-sql.mjs rewrite supabase/init.sql
* node scripts/generate-init-sql.mjs --check exit 1 if supabase/init.sql is stale
* node scripts/generate-init-sql.mjs --out <file> write the result somewhere else
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { migrationsDirPath, readSupersededMigrations } from './superseded-migrations.mjs'
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const MIGRATION_MARKER = /^-- Migration: (\d{14}_[^\s]+\.sql)$/gm
function initSqlPath(root) {
return path.join(root, 'supabase', 'init.sql')
}
/** Git on Windows checks files out with CRLF; the generated file is LF-only. */
function toLf(text) {
return text.replace(/\r\n/g, '\n')
}
/** Every migration file on disk, sorted, including superseded ones. */
export function listMigrationFiles({ root = DEFAULT_ROOT } = {}) {
return fs
.readdirSync(migrationsDirPath(root))
.filter((f) => f.endsWith('.sql'))
.sort()
}
function renderHeader(excluded) {
const excludedLines =
excluded.length === 0
? ['-- (none)']
: excluded.map((name) => `-- ${name}`)
return [
'-- =============================================================================',
'-- Spanlens full database initialisation script',
'-- =============================================================================',
'-- Run this once against your Supabase project to create all tables, functions,',
'-- triggers, RLS policies, and seed data required by Spanlens.',
'--',
'-- How to run:',
'-- Option A (Supabase Dashboard):',
'-- 1. Open your project → SQL Editor → New query',
'-- 2. Paste the entire contents of this file and click Run',
'--',
'-- Option B (psql / CI):',
'-- psql "postgresql://postgres:<password>@db.<ref>.supabase.co:5432/postgres" \\',
'-- -v ON_ERROR_STOP=1 -f supabase/init.sql',
'--',
'-- This file is generated from supabase/migrations/, so edit those instead.',
'-- Regenerate with: node scripts/generate-init-sql.mjs',
'--',
'-- Superseded migrations left out (see supabase/superseded-migrations.txt):',
...excludedLines,
'-- =============================================================================',
'',
'',
].join('\n')
}
/** Builds the init.sql text for a repo root. Pure apart from reading files. */
export function buildInitSql({ root = DEFAULT_ROOT } = {}) {
const superseded = new Set(readSupersededMigrations({ root }))
const files = listMigrationFiles({ root }).filter((f) => !superseded.has(f))
const migrationsDir = migrationsDirPath(root)
const parts = [renderHeader([...superseded].sort())]
for (const file of files) {
const content = toLf(fs.readFileSync(path.join(migrationsDir, file), 'utf8')).trim()
parts.push(`
-- -----------------------------------------------------------------------------
-- Migration: ${file}
-- -----------------------------------------------------------------------------
${content}
`)
}
return parts.join('')
}
function migrationMarkers(sql) {
return [...sql.matchAll(MIGRATION_MARKER)].map((m) => m[1])
}
/**
* Compares the committed supabase/init.sql with a fresh build. `missing` and
* `unexpected` name the migrations whose presence differs; an edited file with
* the same list still reports `upToDate: false`.
*/
export function diffInitSql({ root = DEFAULT_ROOT } = {}) {
const expected = buildInitSql({ root })
const file = initSqlPath(root)
const actual = fs.existsSync(file) ? toLf(fs.readFileSync(file, 'utf8')) : ''
const expectedSet = new Set(migrationMarkers(expected))
const actualSet = new Set(migrationMarkers(actual))
return {
upToDate: expected === actual,
missing: [...expectedSet].filter((f) => !actualSet.has(f)),
unexpected: [...actualSet].filter((f) => !expectedSet.has(f)),
}
}
function runCheck(root) {
const diff = diffInitSql({ root })
if (diff.upToDate) {
console.log('supabase/init.sql is up to date')
return 0
}
console.error('supabase/init.sql is out of date with supabase/migrations/.')
for (const f of diff.missing) console.error(` missing: ${f}`)
for (const f of diff.unexpected) console.error(` unexpected: ${f}`)
if (diff.missing.length === 0 && diff.unexpected.length === 0) {
console.error(' (same migrations, different content: a migration or the header changed)')
}
console.error('Regenerate with: node scripts/generate-init-sql.mjs')
return 1
}
function runWrite(root, outFile) {
const output = buildInitSql({ root })
fs.writeFileSync(outFile, output, 'utf8')
const count = migrationMarkers(output).length
const lineCount = output.split('\n').length
console.log(`Generated ${path.relative(process.cwd(), outFile) || outFile}: ${count} migrations, ${lineCount} lines`)
return 0
}
function main(argv) {
const args = argv.slice(2)
if (args.includes('--check')) return runCheck(DEFAULT_ROOT)
const outIndex = args.indexOf('--out')
if (outIndex !== -1) {
const out = args[outIndex + 1]
if (!out) {
console.error('--out needs a file path')
return 2
}
return runWrite(DEFAULT_ROOT, path.resolve(out))
}
return runWrite(DEFAULT_ROOT, initSqlPath(DEFAULT_ROOT))
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
try {
process.exitCode = main(process.argv)
} catch (err) {
console.error(err instanceof Error ? err.message : err)
process.exitCode = 1
}
}