Files
verse-quote-app/server/plugins/db.ts
T
exe.dev user 0a18c774a5
CI / build (push) Successful in 4m26s
Build Verse: a poem-quote app on the agent-native chat template
Adds a Postgres-backed quotes domain (poems/quotes/favorites) exposed as
five agent-native actions (list-quotes, get-random-quote, create-quote,
toggle-favorite, list-favorites) callable from both chat and the React UI
via useActionQuery/useActionMutation. Moves the primary UI from chat to a
mobile-first Quotes/Favorites experience (chat moves to /chat), adds
manual OpenTelemetry instrumentation exporting traces/metrics/logs over
OTLP, and wires a local Docker Postgres for shared state.
2026-07-25 17:42:04 +00:00

144 lines
3.8 KiB
TypeScript

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<void> {
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<void> => {
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,
);
}
};