feat: standardize OpenAPI route docs metadata (#412)

This commit is contained in:
Daniel Volz
2026-03-10 16:52:31 +01:00
committed by GitHub
parent e0a50d01bb
commit 5264c761cf
13 changed files with 208 additions and 67 deletions
@@ -0,0 +1,70 @@
import type { FastifyInstance, RouteOptions } from "fastify";
type SecurityEntry = Readonly<Record<string, readonly string[]>>;
const defaultProtectedSecurity: readonly SecurityEntry[] = [{ bearerAuth: [] }, { cookieAuth: [] }];
export type OpenApiRouteStandardsOptions = {
tag: string;
protectedByDefault: boolean;
protectedPaths?: RegExp[];
publicPaths?: RegExp[];
};
function asMethods(method: RouteOptions["method"]): string[] {
if (Array.isArray(method)) return method.map((m) => String(m).toUpperCase());
return [String(method).toUpperCase()];
}
function pathMatches(path: string, patterns: RegExp[] | undefined): boolean {
if (!patterns || patterns.length === 0) return false;
return patterns.some((pattern) => pattern.test(path));
}
function buildDefaultSummary(methods: string[], path: string): string {
const methodText = methods.join("/");
return `${methodText} ${path}`;
}
function buildDefaultDescription(requiresAuth: boolean): string {
return requiresAuth
? "Protected endpoint. Requires Bearer token (API key or JWT) or session cookie."
: "Public endpoint.";
}
export function applyOpenApiRouteStandards(app: FastifyInstance, options: OpenApiRouteStandardsOptions): void {
app.addHook("onRoute", (routeOptions) => {
const methods = asMethods(routeOptions.method);
const path = routeOptions.url;
const isExplicitlyPublic = pathMatches(path, options.publicPaths);
const isExplicitlyProtected = pathMatches(path, options.protectedPaths);
let requiresAuth = options.protectedByDefault;
if (isExplicitlyPublic) {
requiresAuth = false;
} else if (isExplicitlyProtected) {
requiresAuth = true;
}
routeOptions.schema = routeOptions.schema ?? {};
routeOptions.schema.tags = routeOptions.schema.tags ?? [options.tag];
routeOptions.schema.summary = routeOptions.schema.summary ?? buildDefaultSummary(methods, path);
routeOptions.schema.description = routeOptions.schema.description ?? buildDefaultDescription(requiresAuth);
if (requiresAuth) {
routeOptions.schema.security = routeOptions.schema.security ?? defaultProtectedSecurity;
routeOptions.schema.response = {
...(routeOptions.schema.response ?? {}),
401: (routeOptions.schema.response as Record<number | string, unknown> | undefined)?.[401] ?? {
type: "object",
properties: {
error: { type: "string" },
code: { type: "string" },
},
},
};
} else if (routeOptions.schema.security === undefined) {
routeOptions.schema.security = [];
}
});
}