c13bfad16f
* feat: improve OpenAPI request contracts and examples * fix: align AJV docs plugin typing * fix: preserve runtime behavior for OpenAPI schemas * fix: align medication OpenAPI body schema with app payloads
115 lines
3.2 KiB
TypeScript
115 lines
3.2 KiB
TypeScript
import type { FastifyInstance, RouteOptions } from "fastify";
|
|
|
|
type SecurityEntry = Readonly<Record<string, readonly string[]>>;
|
|
|
|
const defaultProtectedSecurity: readonly SecurityEntry[] = [{ bearerAuth: [] }, { cookieAuth: [] }];
|
|
|
|
export const genericErrorSchema = {
|
|
type: "object",
|
|
properties: {
|
|
error: { type: "string" },
|
|
code: { type: "string" },
|
|
},
|
|
} as const;
|
|
|
|
export const validationErrorSchema = {
|
|
type: "object",
|
|
additionalProperties: true,
|
|
} as const;
|
|
|
|
export const idParamsSchema = {
|
|
type: "object",
|
|
required: ["id"],
|
|
properties: {
|
|
id: { type: "string", pattern: "^\\d+$" },
|
|
},
|
|
} as const;
|
|
|
|
export const tokenParamsSchema = {
|
|
type: "object",
|
|
required: ["token"],
|
|
properties: {
|
|
token: { type: "string", minLength: 1 },
|
|
},
|
|
} as const;
|
|
|
|
export const successResponseSchema = {
|
|
type: "object",
|
|
properties: {
|
|
success: { type: "boolean" },
|
|
},
|
|
} as const;
|
|
|
|
export const messageResponseSchema = {
|
|
type: "object",
|
|
properties: {
|
|
success: { type: "boolean" },
|
|
message: { type: "string" },
|
|
},
|
|
} as const;
|
|
|
|
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 = [];
|
|
}
|
|
});
|
|
}
|