import { ensureAdditiveColumns, getDbExec, runMigrations, isPostgres, } from "@agent-native/core/db"; import { db } from "../db/index.js"; import * as schema from "../db/schema.js"; import { SEED_POEMS } from "../db/seed-data.js"; function pk(): string { return isPostgres() ? "SERIAL PRIMARY KEY" : "INTEGER PRIMARY KEY"; } function boolType(): string { return isPostgres() ? "BOOLEAN" : "INTEGER"; } function boolDefault(): string { return isPostgres() ? "false" : "0"; } /** * Every Drizzle table exported from schema.ts. Filters out type-only and * helper exports the same way the framework's own templates do: a real * table carries a Symbol-keyed drizzle metadata bag, plain exports don't. */ function isDrizzleTable(value: unknown): value is object { return ( !!value && typeof value === "object" && Object.getOwnPropertySymbols(value).some((s) => s.toString().includes("drizzle"), ) ); } const schemaTables = Object.values(schema).filter(isDrizzleTable); const runVerseQuoteMigrations = runMigrations( [ { version: 1, name: "verse-quote-app-poems-quotes-favorites", get sql() { return ` CREATE TABLE IF NOT EXISTS poems ( id ${pk()}, author TEXT NOT NULL, title TEXT NOT NULL, source TEXT, year INTEGER, created_at TEXT ); CREATE TABLE IF NOT EXISTS quotes ( id ${pk()}, poem_id INTEGER NOT NULL REFERENCES poems(id) ON DELETE CASCADE, text TEXT NOT NULL, tags_json TEXT NOT NULL DEFAULT '[]', is_user_submitted ${boolType()} NOT NULL DEFAULT ${boolDefault()}, created_at TEXT ); CREATE TABLE IF NOT EXISTS favorites ( id ${pk()}, quote_id INTEGER NOT NULL REFERENCES quotes(id) ON DELETE CASCADE, visitor_id TEXT NOT NULL, created_at TEXT, UNIQUE (quote_id, visitor_id) ); CREATE INDEX IF NOT EXISTS verse_quote_quotes_poem_id_idx ON quotes(poem_id); CREATE INDEX IF NOT EXISTS verse_quote_favorites_quote_id_idx ON favorites(quote_id); CREATE INDEX IF NOT EXISTS verse_quote_favorites_visitor_id_idx ON favorites(visitor_id); `; }, }, ], { table: "verse_quote_migrations" }, ); /** * Seeds the curated poem/quote set on first boot only. Runs after the table * migration so it works the same against any DATABASE_URL (Docker Postgres * here, but also Neon/Supabase/local SQLite) — the seed isn't tied to the * Docker container's initdb hook. */ async function seedIfEmpty(): Promise { const existing = await db().select({ id: schema.poems.id }).from(schema.poems).limit(1); if (existing.length > 0) return; for (const poem of SEED_POEMS) { const inserted = await db() .insert(schema.poems) .values({ author: poem.author, title: poem.title, source: poem.source, year: poem.year, }) .returning({ id: schema.poems.id }); const poemId = inserted[0].id; for (const quote of poem.quotes) { await db().insert(schema.quotes).values({ poem_id: poemId, text: quote.text, tags_json: JSON.stringify(quote.tags), is_user_submitted: false, }); } } console.log(`[db] Seeded ${SEED_POEMS.length} poems.`); } export default async (nitroApp: any): Promise => { await runVerseQuoteMigrations(nitroApp); try { const summary = await ensureAdditiveColumns({ db: getDbExec(), tables: schemaTables, }); if (summary.errors.length > 0) { console.warn( "[db] ensureAdditiveColumns completed with errors:", summary.errors, ); } } catch (err) { console.warn( "[db] ensureAdditiveColumns failed (non-fatal):", err instanceof Error ? err.message : err, ); } try { await seedIfEmpty(); } catch (err) { console.warn( "[db] seedIfEmpty failed (non-fatal):", err instanceof Error ? err.message : err, ); } };