import { call, os } from "@orpc/server"; import { z } from "zod"; import { randomUUID } from "crypto"; import { createKV } from "../lib/create-kv.js"; import { assertOwner } from "../lib/auth.js"; // Schema Definition const GalleryPhotoSchema = z.object({ id: z.string(), base64Data: z.string(), title: z.string().optional().default(""), order: z.number().int(), createdAt: z.string(), cover: z.boolean().optional().default(false), }); // KV Storage const galleryPhotosKV = createKV("galleryPhotos"); // Authentication centralized in ../lib/auth.ts // CRUD Endpoints const uploadPhoto = os .input(z.object({ sessionId: z.string(), base64Data: z .string() .regex(/^data:image\/(png|jpe?g|webp|gif);base64,/i, 'Unsupported image format'), title: z.string().optional().default(""), })) .handler(async ({ input }) => { try { await assertOwner(input.sessionId); const id = randomUUID(); const existing = await galleryPhotosKV.getAllItems(); const maxOrder = existing.length > 0 ? Math.max(...existing.map((p) => p.order)) : -1; const nextOrder = maxOrder + 1; const photo = { id, base64Data: input.base64Data, title: input.title ?? "", order: nextOrder, createdAt: new Date().toISOString(), cover: false, }; await galleryPhotosKV.setItem(id, photo); return photo; } catch (err) { console.error("gallery.uploadPhoto error", err); throw err; } }); const setCoverPhoto = os .input(z.object({ sessionId: z.string(), id: z.string() })) .handler(async ({ input }) => { await assertOwner(input.sessionId); const all = await galleryPhotosKV.getAllItems(); let updatedCover = null; for (const p of all) { const isCover = p.id === input.id; const next = { ...p, cover: isCover }; await galleryPhotosKV.setItem(p.id, next); if (isCover) updatedCover = next; } return updatedCover; }); const deletePhoto = os .input(z.object({ sessionId: z.string(), id: z.string() })) .handler(async ({ input }) => { await assertOwner(input.sessionId); await galleryPhotosKV.removeItem(input.id); }); const updatePhotoOrder = os .input(z.object({ sessionId: z.string(), photoOrders: z.array(z.object({ id: z.string(), order: z.number().int() })), })) .handler(async ({ input }) => { await assertOwner(input.sessionId); const updated = []; for (const { id, order } of input.photoOrders) { const existing = await galleryPhotosKV.getItem(id); if (!existing) continue; const updatedPhoto = { ...existing, order }; await galleryPhotosKV.setItem(id, updatedPhoto); updated.push(updatedPhoto); } const all = await galleryPhotosKV.getAllItems(); return all.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)); }); const listPhotos = os.handler(async () => { const all = await galleryPhotosKV.getAllItems(); return all.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)); }); const adminListPhotos = os .input(z.object({ sessionId: z.string() })) .handler(async ({ input }) => { await assertOwner(input.sessionId); const all = await galleryPhotosKV.getAllItems(); return all.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)); }); // Live Queries const live = { listPhotos: os.handler(async function* ({ signal }) { yield call(listPhotos, {}, { signal }); for await (const _ of galleryPhotosKV.subscribe()) { yield call(listPhotos, {}, { signal }); } }), adminListPhotos: os .input(z.object({ sessionId: z.string() })) .handler(async function* ({ input, signal }) { await assertOwner(input.sessionId); const all = await galleryPhotosKV.getAllItems(); const sorted = all.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)); yield sorted; for await (const _ of galleryPhotosKV.subscribe()) { const updated = await galleryPhotosKV.getAllItems(); const sortedUpdated = updated.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)); yield sortedUpdated; } }), }; export const router = { uploadPhoto, deletePhoto, updatePhotoOrder, listPhotos, adminListPhotos, setCoverPhoto, live, };