feat(db): bundle migration SQL via Vite glob + auto-create data dirs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,15 @@
|
|||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
import { dirname } from 'node:path';
|
||||||
import { runMigrations } from './migrate';
|
import { runMigrations } from './migrate';
|
||||||
|
|
||||||
let instance: Database.Database | null = null;
|
let instance: Database.Database | null = null;
|
||||||
|
|
||||||
export function getDb(path = process.env.DATABASE_PATH ?? './data/kochwas.db'): Database.Database {
|
export function getDb(path = process.env.DATABASE_PATH ?? './data/kochwas.db'): Database.Database {
|
||||||
if (instance) return instance;
|
if (instance) return instance;
|
||||||
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
|
const imageDir = process.env.IMAGE_DIR ?? './data/images';
|
||||||
|
mkdirSync(imageDir, { recursive: true });
|
||||||
instance = new Database(path);
|
instance = new Database(path);
|
||||||
instance.pragma('journal_mode = WAL');
|
instance.pragma('journal_mode = WAL');
|
||||||
instance.pragma('foreign_keys = ON');
|
instance.pragma('foreign_keys = ON');
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import type Database from 'better-sqlite3';
|
import type Database from 'better-sqlite3';
|
||||||
import { readdirSync, readFileSync } from 'node:fs';
|
|
||||||
import { join, dirname } from 'node:path';
|
// Vite bundles these SQL files as strings at build time. In dev this is also live.
|
||||||
import { fileURLToPath } from 'node:url';
|
const migrations = import.meta.glob('./migrations/*.sql', {
|
||||||
|
eager: true,
|
||||||
|
query: '?raw',
|
||||||
|
import: 'default'
|
||||||
|
}) as Record<string, string>;
|
||||||
|
|
||||||
|
function orderedMigrations(): { name: string; sql: string }[] {
|
||||||
|
return Object.entries(migrations)
|
||||||
|
.map(([path, sql]) => ({ name: path.split('/').pop()!, sql }))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
export function runMigrations(db: Database.Database): void {
|
export function runMigrations(db: Database.Database): void {
|
||||||
db.exec(`CREATE TABLE IF NOT EXISTS schema_migration (
|
db.exec(`CREATE TABLE IF NOT EXISTS schema_migration (
|
||||||
@@ -9,22 +19,15 @@ export function runMigrations(db: Database.Database): void {
|
|||||||
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);`);
|
);`);
|
||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const dir = join(here, 'migrations');
|
|
||||||
const files = readdirSync(dir)
|
|
||||||
.filter((f) => f.endsWith('.sql'))
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
const applied = new Set(
|
const applied = new Set(
|
||||||
(db.prepare('SELECT name FROM schema_migration').all() as { name: string }[]).map((r) => r.name)
|
(db.prepare('SELECT name FROM schema_migration').all() as { name: string }[]).map((r) => r.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const { name, sql } of orderedMigrations()) {
|
||||||
if (applied.has(file)) continue;
|
if (applied.has(name)) continue;
|
||||||
const sql = readFileSync(join(dir, file), 'utf8');
|
|
||||||
const tx = db.transaction(() => {
|
const tx = db.transaction(() => {
|
||||||
db.exec(sql);
|
db.exec(sql);
|
||||||
db.prepare('INSERT INTO schema_migration(name) VALUES (?)').run(file);
|
db.prepare('INSERT INTO schema_migration(name) VALUES (?)').run(name);
|
||||||
});
|
});
|
||||||
tx();
|
tx();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user