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.
86 lines
2.3 KiB
TypeScript
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));
|
|
}
|