import type { Collection, CollectionWithCount, Generation } from './db'; /** * The one place a database row becomes JSON. * * Every API serialiser lives in this file — a structural choice, not tidiness: it is the one place * that can enforce snake_case across every endpoint. See the standalone repo's test for the incident * this guarded against (`serialize.test.ts`, ported below). * * Snake case throughout, because the response body is deliberately OpenAI-shaped and OpenAI is snake. */ /** * `/files/`, relative from this plugin's own mount — NOT an absolute `PUBLIC_URL` any more. * * The standalone service served images unauthenticated with a durable absolute URL; inside Officer the * route is behind the permission gate like everything else, so there is no host to bake in and no * reason to. An API caller already holding a bearer token re-fetches this path with the same * `Authorization` header they used to get here. The Officer frontend (which cannot set that header on * an ``) appends its own `?token=` — see `plugins/atelier/web/lib/api.ts` — the same escape * hatch `FileViewer` and the music plugin already use for exactly this reason. */ const imageUrl = (row: Generation) => (row.imagePath ? `/files/${row.imagePath}` : null); /** What every image reports about itself, wherever it appears. */ const measured = (row: Generation) => ({ width: row.width, height: row.height, // `has_alpha` is whether the file carries an alpha channel; `opaque` is whether every pixel in it is // fully opaque. Both, because an alpha channel the model never used is the failure worth catching. has_alpha: row.hasAlpha, opaque: row.opaque, cost_usd: row.costUsd, duration_ms: row.durationMs, }); /** The compact form, for `atelier.images[]` — no prompt, which at n:10 would repeat 32000 chars ten times. */ export const serializeGenerationImage = (row: Generation) => ({ id: row.id, status: row.status, url: imageUrl(row), ...measured(row), error: row.error, }); /** * The full row, for history and collections. * * `prompt`, `options` and `input_images` are here deliberately and are not to be normalised or * truncated: every disappointing result so far has been traced to what was asked for, and these three * are what make that diagnosis possible after the fact. */ export const serializeGeneration = (row: Generation) => ({ id: row.id, request_id: row.requestId, index: row.index, operation: row.operation, status: row.status, provider: row.provider, model: row.model, prompt: row.prompt, options: row.options, input_images: row.inputImages, mask_image: row.maskImage, image_path: row.imagePath, url: imageUrl(row), ...measured(row), error: row.error, created_at: row.createdAt, started_at: row.startedAt, finished_at: row.finishedAt, }); /** * A collection, as the API returns it. * * Drizzle hands back camelCase because that is what the schema declares in TypeScript. Returning the * row untouched is how a camelCase field would reach the wire in a snake_case API — this is where one * becomes the other. */ export const serializeCollection = (row: Collection) => ({ id: row.id, name: row.name, description: row.description, created_at: row.createdAt, updated_at: row.updatedAt, }); /** The list form, with the counts the index screen needs. */ export const serializeCollectionSummary = (row: CollectionWithCount) => ({ ...serializeCollection(row), item_count: row.itemCount, cover_image_path: row.coverImagePath, cover_url: row.coverImagePath ? `/files/${row.coverImagePath}` : null, });