CI / build (push) Successful in 4m26s
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.
144 lines
3.8 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
};
|