151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
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),
|
|
});
|
|
|
|
export type GalleryPhoto = z.output<typeof GalleryPhotoSchema>;
|
|
|
|
// KV Storage
|
|
const galleryPhotosKV = createKV<GalleryPhoto>("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: GalleryPhoto = {
|
|
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: GalleryPhoto | null = null;
|
|
for (const p of all) {
|
|
const isCover = p.id === input.id;
|
|
const next: GalleryPhoto = { ...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: GalleryPhoto[] = [];
|
|
for (const { id, order } of input.photoOrders) {
|
|
const existing = await galleryPhotosKV.getItem(id);
|
|
if (!existing) continue;
|
|
const updatedPhoto: GalleryPhoto = { ...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,
|
|
};
|
|
|
|
|