// HTTP API: serves upcoming OpenMeet events at GET /api/upcoming, the // generated OpenAPI spec at GET /api/openapi.json, and a liveness probe at // GET /health. import Fastify from "fastify"; import type { FastifyInstance } from "fastify"; import swagger from "@fastify/swagger"; import type { JsonSchemaToTsProvider } from "@fastify/type-provider-json-schema-to-ts"; import type { JSONSchema } from "json-schema-to-ts"; import type { Config } from "../config.ts"; import { getOpenMeetClient, type OpenMeetClient } from "../openmeet/openmeet.ts"; // Mirrors OpenMeetEvent. Declaring it on the route's `response` schema makes // Fastify serialize only these fields, dropping the rest of the OpenMeet // payload, and lets @fastify/swagger describe the endpoint. const upcomingEventsResponse = { type: "array", items: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, slug: { type: "string" }, location: { type: "string" }, lat: { type: "string" }, lon: { type: "string" }, startDate: { type: "string", format: "date-time" }, endDate: { type: "string", format: "date-time" }, series: { type: "object", properties: { id: { type: "integer" }, name: { type: "string" }, }, }, image: { type: "object", properties: { path: { type: "string" }, }, }, }, required: ["name", "slug", "location", "startDate", "endDate"], }, } as const satisfies JSONSchema; export async function startApi( cfg: Config, opts: { aborter?: AbortController; omClient?: OpenMeetClient } = {}, ): Promise { const { aborter } = opts; const app = await buildApp(cfg, opts); const port = Number(process.env.API_PORT ?? 3000); const host = process.env.API_HOST ?? "0.0.0.0"; await app.listen({ port, host }); if (aborter) { const close = () => app.close().catch((err) => console.error("api shutdown failed:", err)); if (aborter.signal.aborted) close(); else aborter.signal.addEventListener("abort", close, { once: true }); } } export async function buildApp( cfg: Config, opts: { omClient?: OpenMeetClient; logger?: boolean } = {}, ): Promise { const { omClient, logger = true } = opts; const app = Fastify({ logger }).withTypeProvider(); await app.register(swagger, { openapi: { info: { title: "devict-events API", version: "0.1.0", }, }, }); const om = omClient ?? (await getOpenMeetClient(cfg)); app.get( "/api/upcoming", { schema: { response: { 200: upcomingEventsResponse } } }, async (_req, res) => { res.header("access-control-allow-origin", "*"); return om.getUpcomingEvents(cfg); }, ); app.get("/api/openapi.json", async () => app.swagger()); app.get("/health", async () => ({ status: "ok" })); return app; }