Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899// 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<void> { 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<FastifyInstance> { const { omClient, logger = true } = opts; const app = Fastify({ logger }).withTypeProvider<JsonSchemaToTsProvider>();
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;}