- Copied as-is from learning_voice_ai_agent/services/tracker-service - 45 tests passing (vitest) - Fastify 5 + Cosmos DB + jose + Zod + @fastify/rate-limit - Modules: items, comments, votes, public - Port 4004
84 lines
2.6 KiB
TypeScript
84 lines
2.6 KiB
TypeScript
/**
|
|
* Tracker Service — Fastify server entry point.
|
|
*
|
|
* Modules: items, comments, votes.
|
|
* Port: 4004 (configurable via PORT env var).
|
|
* Product-agnostic: all data scoped by productId.
|
|
*/
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
import Fastify from "fastify";
|
|
import cors from "@fastify/cors";
|
|
import swagger from "@fastify/swagger";
|
|
import metricsPlugin from "fastify-metrics";
|
|
import { ServiceError } from "./lib/errors.js";
|
|
import { itemRoutes } from "./modules/items/routes.js";
|
|
import { commentRoutes } from "./modules/comments/routes.js";
|
|
import { voteRoutes } from "./modules/votes/routes.js";
|
|
import { publicRoutes } from "./modules/public/routes.js";
|
|
import { config } from "./lib/config.js";
|
|
|
|
const PORT = config.PORT;
|
|
const HOST = config.HOST;
|
|
|
|
const app = Fastify({ logger: true });
|
|
|
|
// CORS — restrict to specific origins in production via CORS_ORIGIN (comma-separated)
|
|
const corsOrigin = config.CORS_ORIGIN;
|
|
await app.register(cors, {
|
|
origin: corsOrigin ? corsOrigin.split(",").map((o) => o.trim()) : true,
|
|
});
|
|
|
|
// OpenAPI spec auto-generation (GET /api/docs/json)
|
|
await app.register(swagger, {
|
|
openapi: {
|
|
info: { title: "Tracker Service", version: "0.1.0", description: "Feature requests, bugs, tasks — product-agnostic" },
|
|
servers: [{ url: `http://localhost:${PORT}` }],
|
|
},
|
|
});
|
|
|
|
// Prometheus metrics
|
|
await app.register(metricsPlugin, { endpoint: "/metrics" });
|
|
|
|
// x-request-id: propagate incoming header or generate a new one
|
|
app.addHook("onRequest", async (req, reply) => {
|
|
const requestId = (req.headers["x-request-id"] as string) || randomUUID();
|
|
req.headers["x-request-id"] = requestId;
|
|
reply.header("x-request-id", requestId);
|
|
req.log = req.log.child({ requestId });
|
|
});
|
|
|
|
// Health check
|
|
app.get("/health", async (req) => ({
|
|
status: "ok",
|
|
service: "tracker-service",
|
|
version: "0.1.0",
|
|
timestamp: new Date().toISOString(),
|
|
requestId: req.headers["x-request-id"],
|
|
}));
|
|
|
|
// Custom error handler
|
|
app.setErrorHandler((error, _req, reply) => {
|
|
if (error instanceof ServiceError) {
|
|
reply.code(error.statusCode).send({ error: error.message });
|
|
return;
|
|
}
|
|
app.log.error(error);
|
|
reply.code(500).send({ error: "Internal server error" });
|
|
});
|
|
|
|
// Register route modules
|
|
await app.register(itemRoutes, { prefix: "/api" });
|
|
await app.register(commentRoutes, { prefix: "/api" });
|
|
await app.register(voteRoutes, { prefix: "/api" });
|
|
await app.register(publicRoutes, { prefix: "/api" });
|
|
|
|
// Start
|
|
try {
|
|
await app.listen({ port: PORT, host: HOST });
|
|
app.log.info(`Tracker Service listening on ${HOST}:${PORT}`);
|
|
} catch (err) {
|
|
app.log.error(err);
|
|
process.exit(1);
|
|
}
|