@omer-x/next-openapi-route-handler
Version:
a Next.js plugin to generate OpenAPI documentation from route handlers
336 lines (318 loc) • 10.7 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
default: () => index_default,
defineRoute: () => definer_default
});
module.exports = __toCommonJS(index_exports);
// src/types/error.ts
var customErrorTypes = [
"PARSE_FORM_DATA",
"PARSE_REQUEST_BODY",
"PARSE_SEARCH_PARAMS",
"PARSE_PATH_PARAMS",
"UNNECESSARY_PATH_PARAMS"
];
// src/utils/openapi-components.ts
function createSchemaRef(schemaName, isArray) {
const refObject = { $ref: `#/components/schemas/${schemaName}` };
if (isArray) {
return { type: "array", items: refObject };
}
return refObject;
}
// src/core/createMediaType.ts
function createMediaType(schema, example, examples) {
const mediaTypeEntries = [];
mediaTypeEntries.push(["schema", schema]);
if (example) {
mediaTypeEntries.push(["example", example]);
}
if (examples) {
mediaTypeEntries.push(["examples", examples]);
}
return Object.fromEntries(mediaTypeEntries);
}
// src/core/zod-to-openapi.ts
var import_zod = require("zod");
function convertToOpenAPI(schema, isArray) {
return import_zod.z.toJSONSchema(isArray ? schema.array() : schema);
}
// src/core/content.ts
function resolveContent(source, isArray = false, isFormData = false, customExample, customExamples) {
if (!source) return void 0;
const schema = typeof source === "string" ? createSchemaRef(source, isArray) : convertToOpenAPI(source, isArray);
return {
[isFormData ? "multipart/form-data" : "application/json"]: createMediaType(schema, customExample, customExamples)
};
}
// src/utils/array-serialization.ts
function serializeArray(value) {
return value.join(",");
}
function deserializeArray(value) {
if (!value.length) return [];
return value.split(",");
}
// src/utils/boolean.ts
function getTrueBoolean(input) {
if (input === "true") return true;
if (input === "false") return false;
return null;
}
// src/utils/type-conversion.ts
function convertStringToNumber(input, keys) {
return keys.reduce((mutation, key) => {
return { ...mutation, [key]: parseFloat(mutation[key]) };
}, input);
}
function convertStringToBoolean(input, keys) {
return keys.reduce((mutation, key) => {
return { ...mutation, [key]: getTrueBoolean(mutation[key]) };
}, input);
}
function convertStringToArray(input, keys) {
return keys.reduce((mutation, key) => {
return { ...mutation, [key]: deserializeArray(mutation[key]) };
}, input);
}
// src/core/zod-error-handler.ts
function safeParse(schema, input) {
const result = schema.safeParse(input);
if (!result.success) {
for (const issue of result.error.issues) {
const [key] = issue.path;
if (issue.code === "invalid_type" && typeof key === "string" && key in input && typeof input[key] === "string") {
if (issue.expected === "number") {
return safeParse(schema, convertStringToNumber(input, issue.path));
}
if (issue.expected === "boolean") {
return safeParse(schema, convertStringToBoolean(input, issue.path));
}
if (issue.expected === "array") {
return safeParse(schema, convertStringToArray(input, issue.path));
}
}
}
throw result.error;
}
return result.data;
}
// src/core/body.ts
function resolveRequestBody(source, isFormData = false, customExample, customExamples) {
if (!source) return void 0;
return {
// description: "", // how to fill this?
required: true,
content: resolveContent(source, false, isFormData, customExample, customExamples)
};
}
async function parseRequestBody(request, method, schema, isFormData = false) {
if (!schema || typeof schema === "string") return null;
if (method === "GET") throw new Error("GET routes can't have request body");
if (isFormData) {
const formData = await request.formData();
const body = Array.from(formData.keys()).reduce((collection, key) => ({
...collection,
[key]: formData.get(key)
}), {});
try {
return safeParse(schema, body);
} catch (error) {
if (process.env.NODE_ENV !== "production") {
console.log(error.issues);
}
throw new Error("PARSE_FORM_DATA", { cause: error.issues });
}
}
try {
return schema.parse(await request.json());
} catch (error) {
if (error instanceof Error && error.message === "Unexpected end of JSON input") {
const result = schema.safeParse({});
throw new Error("PARSE_REQUEST_BODY", { cause: result.error?.issues });
}
if (process.env.NODE_ENV !== "production") {
console.log(error.issues);
}
throw new Error("PARSE_REQUEST_BODY", { cause: error.issues });
}
}
// src/core/params.ts
function resolveParams(kind, source) {
if (!source) return [];
const schema = convertToOpenAPI(source, false);
if (!("properties" in schema)) throw new Error("Invalid object schema");
const entries = Object.entries(schema.properties ?? {});
return entries.map(([name, definition]) => ({
in: kind,
name,
required: schema.required?.includes(name) ?? false,
description: definition.description,
schema: definition
}));
}
// src/core/path-params.ts
function parsePathParams(source, schema) {
if (!schema) return null;
if (!source) throw new Error("UNNECESSARY_PATH_PARAMS");
try {
return safeParse(schema, source);
} catch (error) {
if (process.env.NODE_ENV !== "production") {
console.log(error.issues);
}
throw new Error("PARSE_PATH_PARAMS", { cause: error.issues });
}
}
// src/core/responses.ts
function addBadRequest(queryParams, requestBody) {
if (!queryParams && !requestBody) return void 0;
return { description: "Bad Request" };
}
function bundleResponses(collection) {
return Object.entries(collection).reduce((bundle, [key, response]) => {
if (response.content) {
return {
...bundle,
[key]: {
description: response.description,
content: resolveContent(response.content, response.isArray, false, response.example, response.examples)
}
};
}
if (response.customContent) {
return {
...bundle,
[key]: {
description: response.description,
content: response.customContent
}
};
}
return {
...bundle,
[key]: {
description: response.description
}
};
}, {});
}
// src/core/search-params.ts
function parseSearchParams(source, schema) {
if (!schema) return null;
const sourceKeys = Array.from(new Set(source.keys()));
const params = sourceKeys.reduce((collection, key) => {
const values = source.getAll(key);
return {
...collection,
[key]: serializeArray(values)
};
}, {});
try {
return safeParse(schema, params);
} catch (error) {
if (process.env.NODE_ENV !== "production") {
console.log(error.issues);
}
throw new Error("PARSE_SEARCH_PARAMS", { cause: error.issues });
}
}
// src/core/definer.ts
function defineRoute(input) {
const handler = async (request, context) => {
try {
const { searchParams } = new URL(request.url);
const nextSegmentParams = context ? await context.params : void 0;
const pathParams = parsePathParams(nextSegmentParams, input.pathParams);
const queryParams = parseSearchParams(searchParams, input.queryParams);
const body = await parseRequestBody(request, input.method, input.requestBody, input.hasFormData);
return await input.action({ pathParams, queryParams, body }, request);
} catch (error) {
if (input.handleErrors) {
if (error instanceof Error) {
const errorMessage = error.message;
if (customErrorTypes.includes(errorMessage)) {
return input.handleErrors(errorMessage, error.cause);
}
}
return input.handleErrors("UNKNOWN_ERROR");
}
if (error instanceof Error) {
switch (error.message) {
case "PARSE_FORM_DATA":
case "PARSE_REQUEST_BODY":
case "PARSE_SEARCH_PARAMS":
return new Response(null, { status: 400 });
case "PARSE_PATH_PARAMS":
return new Response(null, { status: 404 });
case "UNNECESSARY_PATH_PARAMS": {
if (process.env.NODE_ENV !== "production") {
console.log([
"[Next OpenAPI Route Handler]",
"You tried to add pathParams to a route which doesn't have any dynamic params.",
"Maybe you meant queryParams instead?"
].join(" "));
}
}
}
}
return new Response(null, { status: 500 });
}
};
const parameters = [
...resolveParams("path", input.pathParams),
...resolveParams("query", input.queryParams)
];
const responses = bundleResponses(input.responses);
if (!input.responses["400"]) {
const response400 = addBadRequest(input.queryParams, input.requestBody);
if (response400) {
responses["400"] = response400;
}
}
if (!input.responses["500"]) {
responses["500"] = { description: "Internal Server Error" };
}
handler.apiData = {
operationId: input.operationId,
summary: input.summary,
description: input.description,
tags: input.tags,
parameters: parameters.length ? parameters : void 0,
requestBody: resolveRequestBody(input.requestBody, input.hasFormData, input.requestBodyExample, input.requestBodyExamples),
responses,
security: input.security
};
if (input.middleware) {
const alteredHandler = input.middleware(handler);
alteredHandler.apiData = handler.apiData;
return { [input.method]: alteredHandler };
}
return { [input.method]: handler };
}
var definer_default = defineRoute;
// src/index.ts
var index_default = definer_default;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
defineRoute
});