Files
verse-quote-app/server/lib/quotes.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

86 lines
2.3 KiB
TypeScript

import { eq } from "drizzle-orm";
import { db, schema } from "../db/index.js";
export type PoemInfo = {
id: number;
title: string;
author: string;
source: string | null;
year: number | null;
};
export type QuoteWithPoem = {
id: number;
text: string;
tags: string[];
isUserSubmitted: boolean;
createdAt: string | null;
poem: PoemInfo;
favoriteCount: number;
isFavorited: boolean;
};
export function parseTags(tagsJson: string): string[] {
try {
const value = JSON.parse(tagsJson);
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
} catch {
return [];
}
}
type RawRow = {
quote: typeof schema.quotes.$inferSelect;
poem: typeof schema.poems.$inferSelect;
};
/**
* Joins quote rows with their poem, favorite counts, and (when visitorId is
* given) whether that visitor has favorited each quote. Shared by every
* action that returns quotes so the shape stays identical across list,
* random, favorites, and create.
*/
export async function hydrateQuotes(
rows: RawRow[],
visitorId?: string,
): Promise<QuoteWithPoem[]> {
if (rows.length === 0) return [];
const quoteIds = rows.map((r) => r.quote.id);
const allFavorites = await db().select().from(schema.favorites);
const countByQuoteId = new Map<number, number>();
const favoritedByVisitor = new Set<number>();
for (const fav of allFavorites) {
if (!quoteIds.includes(fav.quote_id)) continue;
countByQuoteId.set(fav.quote_id, (countByQuoteId.get(fav.quote_id) ?? 0) + 1);
if (visitorId && fav.visitor_id === visitorId) {
favoritedByVisitor.add(fav.quote_id);
}
}
return rows.map((row) => ({
id: row.quote.id,
text: row.quote.text,
tags: parseTags(row.quote.tags_json),
isUserSubmitted: row.quote.is_user_submitted,
createdAt: row.quote.created_at ?? null,
poem: {
id: row.poem.id,
title: row.poem.title,
author: row.poem.author,
source: row.poem.source ?? null,
year: row.poem.year ?? null,
},
favoriteCount: countByQuoteId.get(row.quote.id) ?? 0,
isFavorited: favoritedByVisitor.has(row.quote.id),
}));
}
export async function fetchAllQuoteRows(): Promise<RawRow[]> {
return db()
.select({ quote: schema.quotes, poem: schema.poems })
.from(schema.quotes)
.innerJoin(schema.poems, eq(schema.quotes.poem_id, schema.poems.id));
}