ported from the standalone officer-atelier service: providers (adapter-shaped, openai the only adapter today), the generation queue, collections, and the database layer — kept on its own Postgres database rather than folded into officer_db, since both collections and provider_settings had rows on the machine this was extracted from. what changed rather than just moved: - images are served from an authenticated GET /files/:filename instead of an unauthenticated PUBLIC_URL-prefixed static route - the OpenAI-key bootstrap fallback reads the secret store (getSecret) before .env, per-adapter - provider_settings (the one-row-per-adapter shape providers replaced) did not come along — it has no callers left anywhere in the server code, only a migration script that also did not move ping/echo (smoke-test routes with no caller) did not move either; the sidecar's own /_health covers liveness. not yet wired into the platform: sidecar/index.ts exists but nothing mounts it, and web/ has not been written, so this is not yet a discoverable plugin.
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
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/<name>`, 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 `<img src>`) 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,
|
|
});
|