Inspect the shipped guestbook backend route
// GET /api/entries -> recent guestbook entries (documents via ctx.db)
// POST /api/entries -> add one, with an optional avatar file (object via ctx.blob)
//
// Documents live in this riff's own store; the avatar is keyed by the entry id and
// served back through /api/avatar/:id (see backend/routes/api/avatar/[id].js).
export async function GET(ctx) {
const { docs } = await ctx.db.query("entries", {
order: { field: "createdAt", dir: "desc" },
limit: 50,
});
return ctx.json({
entries: docs.map((d) => ({
id: d.id,
name: d.data.name,
message: d.data.message,
avatar: d.data.avatarKey ? `/api/avatar/${d.id}` : null,
at: d.createdAt,
})),
});
}
export async function POST(ctx) {
const form = await ctx.req.raw.formData();
const name = String(form.get("name") ?? "").trim().slice(0, 80);
const message = String(form.get("message") ?? "").trim().slice(0, 500);
if (!name || !message) return ctx.json({ error: "name and message are required" }, 400);
// Create the entry first so the avatar can be keyed by its id.
const entry = await ctx.db.create(
"entries",
{ name, message, avatarKey: null },
{ userId: ctx.session.id },
);
const file = form.get("avatar");
if (file && typeof file === "object" && "arrayBuffer" in file) {
const buf = await file.arrayBuffer();
if (buf.byteLength > 0 && buf.byteLength <= 2_000_000) {
const key = `avatars/${entry.id}`;
await ctx.blob.put(key, buf, {
contentType: file.type || "application/octet-stream",
userId: ctx.session.id,
});
await ctx.db.update("entries", entry.id, { avatarKey: key });
}
}
return ctx.json({ ok: true, id: entry.id }, 201);
}