/** * Image-metadata storage on the shared `recflare` D1 database. Each image is a * single JSON blob in the `data` column; queryable fields (Id, ImageName, * PlayerId, RoomId) are SQLite generated (virtual) columns extracted from that * JSON — the same JSON-blob pattern the rooms/accounts tables use. * * The `img` worker owns this schema/migration (migrations/0001_images.sql, applied * with its own `migrations_table` so it doesn't clash with the other workers' * migrations on the shared database). The `api` worker writes a row on upload and * reads it back, keeping its own copy of these helpers in sync. */ /** Schema DDL (mirror of migrations/0001_image.sql, sans any seed rows). */ export const SCHEMA_DDL: string[] = [ `CREATE TABLE IF NOT EXISTS image ( data TEXT NOT NULL, id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.Id')) VIRTUAL, image_name TEXT GENERATED ALWAYS AS (json_extract(data, '$.ImageName')) VIRTUAL, player_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.PlayerId')) VIRTUAL, room_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.RoomId')) VIRTUAL )`, `CREATE UNIQUE INDEX IF NOT EXISTS idx_image_id ON image (id)`, `CREATE INDEX IF NOT EXISTS idx_image_image_name ON image (image_name)`, `CREATE INDEX IF NOT EXISTS idx_image_player_id ON image (player_id)`, `CREATE INDEX IF NOT EXISTS idx_image_room_id ON image (room_id)`, ] /** A stored image record (the client-facing SavedImage shape). */ export interface SavedImage { Id: number Type: number Accessibility: number AccessibilityLocked: boolean ImageName: string Description: string | null PlayerId: number TaggedPlayerIds: number[] RoomId: number | null PlayerEventId: number | null CreatedAt: string CheerCount: number CommentCount: number } interface ImageRow { data: string } /** Fields supplied at upload time (from `imgMeta`); everything else defaults. */ export interface NewImage { imageName: string playerId: number type?: number accessibility?: number roomId?: number | null description?: string | null taggedPlayerIds?: number[] playerEventId?: number | null } /** Insert a new image record for an upload, returning the stored row. */ export async function createImage(db: D1Database, input: NewImage): Promise { // Sequential id: one past the current max (the table starts empty). const row = await db .prepare('SELECT COALESCE(MAX(id), 0) + 1 AS next FROM image') .first<{ next: number }>() const image: SavedImage = { Id: row?.next ?? 1, Type: input.type ?? 1, Accessibility: input.accessibility ?? 1, AccessibilityLocked: false, ImageName: input.imageName, Description: input.description ?? null, PlayerId: input.playerId, TaggedPlayerIds: input.taggedPlayerIds ?? [], RoomId: input.roomId ?? null, PlayerEventId: input.playerEventId ?? null, CreatedAt: new Date().toISOString(), CheerCount: 0, CommentCount: 0, } await db.prepare('INSERT INTO image (data) VALUES (?1)').bind(JSON.stringify(image)).run() return image } /** Look up an image record by its ImageName (the R2 key / filename), or null. */ export async function getImageByName(db: D1Database, name: string): Promise { const row = await db .prepare('SELECT data FROM image WHERE image_name = ?1') .bind(name) .first() return row ? (JSON.parse(row.data) as SavedImage) : null }