UNPKG

opinionated-machine

Version:

Very opinionated DI framework for fastify, built on top of awilix

90 lines 4.18 kB
import { merge } from 'ts-deepmerge'; import { readGatewayMetadata } from "../withGatewayMetadata.js"; import { gatewayManifestSchema, } from "./manifestSchema.js"; import { normalizePath } from "./pathNormalize.js"; const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; function normalizeMethod(method) { if (Array.isArray(method)) { throw new Error(`Gateway manifest does not support multi-method routes (got [${method.join(', ')}]). Declare one route per method.`); } const upper = String(method).toUpperCase(); if (!HTTP_METHODS.includes(upper)) { throw new Error(`Unsupported HTTP method "${method}" in gateway manifest`); } return upper; } function mergeMetadata(layers) { const present = layers.filter((m) => m !== undefined); if (present.length === 0) return {}; if (present.length === 1) return present[0]; // ts-deepmerge replaces arrays in later layers (documented merge semantics). // biome-ignore lint/suspicious/noExplicitAny: ts-deepmerge generic doesn't cleanly express this return merge.withOptions({ mergeArrays: false }, ...present); } function collectRouteEntries(collected) { if (collected.kind === 'rest') { const built = collected.controller.buildRoutes(); return Object.entries(built).map(([routeKey, route]) => ({ routeKey, route })); } // AbstractApiController: routes is a Record — key becomes the routeKey. return Object.entries(collected.controller.routes).map(([routeKey, route]) => ({ routeKey, route, })); } /** * Pure manifest builder. Takes already-resolved controllers; performs no DI. * * Used by `DIContext.buildGatewayManifest()` after it resolves controllers from * the container. Exposed separately for unit testing without spinning up a DI * context. */ export function buildGatewayManifestFrom(controllers, options) { const routes = []; // Track route ids back to their origin so we can produce a useful error // message if two declarations end up with the same explicit metadata.id. const idOrigin = new Map(); for (const collected of controllers) { const controllerDefaults = collected.controller.gatewayDefaults; for (const { routeKey, route } of collectRouteEntries(collected)) { const routeMetadata = readGatewayMetadata(route); const merged = mergeMetadata([options.defaults, controllerDefaults, routeMetadata]); if (route.url === undefined) { throw new Error(`Route "${collected.name}.${routeKey}" is missing a URL — gateway manifest cannot be generated.`); } const path = normalizePath(route.url); const method = normalizeMethod(route.method); const origin = `${collected.name}.${routeKey}`; const id = merged.id ?? origin; const previousOrigin = idOrigin.get(id); if (previousOrigin) { throw new Error(`Duplicate gateway route id "${id}": declared by both ${previousOrigin} and ${origin}. Set a distinct metadata.id on one of them.`); } idOrigin.set(id, origin); routes.push({ id, method, path, controller: collected.name, routeKey, metadata: merged, }); } } // Sort for stable output across runs (gateways like deterministic configs). routes.sort((a, b) => a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path)); const manifest = { manifestVersion: '1', service: options.service, ...(options.version !== undefined ? { version: options.version } : {}), generatedAt: new Date().toISOString(), routes, }; // Validate the per-route merged metadata too. The route-level types narrow // header/query keys, but service- and controller-level defaults are // contract-unbound, so a runtime check at the boundary protects generators. return gatewayManifestSchema.parse(manifest); } //# sourceMappingURL=buildManifest.js.map