import type { H3Event } from 'h3' import { ConflictError, ValidationError } from 'airspace' import { invalidatePublicReads, requireAdminAirspace } from '../airspace' import { collections } from '#shared/collections' type Airspace = Awaited> /** The collections the generic CRUD routes serve. `location` is a singleton and has its own routes. */ export type CollectionName = Exclude const nsidOf = (name: CollectionName): string => collections[name].nsid async function clientFor (event: H3Event, name: K): Promise { return (await requireAdminAirspace(event))[name] } /** * Each helper below is generic over the collection, so `airspace[name]` is a * union of clients whose methods TypeScript cannot call in common. The public * signatures stay precise; only the dispatch inside is untyped. */ type AnyClient = { list: () => Promise get: (rkey: string) => Promise create: (value: any) => Promise put: (rkey: string, value: any) => Promise delete: (rkey: string) => Promise } const untyped = (client: unknown): AnyClient => client as AnyClient function assertRkey (rkey: string | undefined): asserts rkey is string { if (!rkey || rkey === 'undefined' || rkey === 'null') { throw createError({ statusCode: 400, statusMessage: 'Missing or invalid rkey.' }) } } /** * airspace's errors already name the collection and record key; this only * picks the status code the admin UI expects. */ function asHttpError (err: unknown, name: CollectionName): never { if (err instanceof ValidationError) { throw createError({ statusCode: 422, statusMessage: `Invalid ${nsidOf(name)}: ${err.message}` }) } if (err instanceof ConflictError) { throw createError({ statusCode: 409, statusMessage: err.message }) } throw err } /** Run a write and drop the public client's cache for the collection it touched. */ async function write (name: CollectionName, run: () => Promise): Promise { try { return await run() } catch (err) { asHttpError(err, name) } finally { invalidatePublicReads(nsidOf(name)) } } export async function listAdminRecords (event: H3Event, name: K): Promise>> { return await untyped(await clientFor(event, name)).list() as never } export async function getAdminRecord (event: H3Event, name: K, rkey: string | undefined): Promise>>> { assertRkey(rkey) const record = await untyped(await clientFor(event, name)).get(rkey) if (!record) { throw createError({ statusCode: 404, statusMessage: `${nsidOf(name)}/${rkey} not found.` }) } return record as never } export async function createAdminRecord ( event: H3Event, name: K, value: Parameters[0], ): Promise>> { const client = untyped(await clientFor(event, name)) return await write(name, () => client.create(value)) } export async function updateAdminRecord ( event: H3Event, name: K, rkey: string | undefined, value: Parameters[1], ): Promise>> { assertRkey(rkey) const client = untyped(await clientFor(event, name)) return await write(name, () => client.put(rkey, value)) } export async function deleteAdminRecord (event: H3Event, name: K, rkey: string | undefined) { assertRkey(rkey) const client = untyped(await clientFor(event, name)) await write(name, () => client.delete(rkey)) return { rkey, deleted: true as const } }