next-rest-framework
Version:
Next REST Framework - Type-safe, self-documenting APIs for Next.js
764 lines (753 loc) • 21.9 kB
JavaScript
import {
DEFAULT_DESCRIPTION,
DEFAULT_ERRORS,
DEFAULT_FAVICON_URL,
DEFAULT_LOGO_URL,
DEFAULT_OG_TYPE,
DEFAULT_TITLE,
ERROR_MESSAGE_SCHEMA,
HOMEPAGE,
INVALID_PATH_PARAMETERS_RESPONSE,
INVALID_QUERY_PARAMETERS_RESPONSE,
INVALID_REQUEST_BODY_RESPONSE,
INVALID_RPC_REQUEST_RESPONSE,
MESSAGE_WITH_ERRORS_SCHEMA,
UNEXPECTED_ERROR_RESPONSE,
VERSION,
ValidMethod,
ZOD_ISSUE_SCHEMA
} from "./chunk-SO2Y7NAF.mjs";
// src/shared/schemas.ts
import { zodToJsonSchema } from "zod-to-json-schema";
import chalk from "chalk";
var isZodSchema = (schema) => !!schema && typeof schema === "object" && "_def" in schema;
var zodSchemaValidator = ({
schema,
obj
}) => {
const data = schema.safeParse(obj);
const errors = !data.success ? data.error.issues : null;
return {
valid: data.success,
errors,
data: data.success ? data.data : null
};
};
var validateSchema = ({
schema,
obj
}) => {
if (isZodSchema(schema)) {
return zodSchemaValidator({ schema, obj });
}
throw Error("Invalid schema.");
};
var getJsonSchema = ({
schema,
operationId,
type
}) => {
if (isZodSchema(schema)) {
try {
return zodToJsonSchema(schema, {
$refStrategy: "none",
target: "openApi3"
});
} catch (error) {
const solutions = {
"input-params": "paramsSchema",
"input-query": "querySchema",
"input-body": "bodySchema",
"output-body": "bodySchema"
};
console.warn(
chalk.yellowBright(
`
Warning: ${type} schema for operation ${operationId} could not be converted to a JSON schema. The OpenAPI spec may not be accurate.
This is most likely related to an issue with the \`zod-to-json-schema\`: https://github.com/StefanTerdell/zod-to-json-schema?tab=readme-ov-file#known-issues
Please consider using the ${solutions[type]} property in addition to the Zod schema.`
)
);
return {};
}
}
throw Error("Invalid schema.");
};
// src/shared/rpc-operation.ts
var rpcOperation = (openApiOperation) => {
function createOperation({
input,
outputs,
middleware1,
middleware2,
middleware3,
handler
}) {
const callOperation = async (body) => {
let middlewareOptions = {};
if (middleware1) {
middlewareOptions = await middleware1(body, middlewareOptions);
if (middleware2) {
middlewareOptions = await middleware2(body, middlewareOptions);
if (middleware3) {
middlewareOptions = await middleware3(body, middlewareOptions);
}
}
}
if (input?.body) {
const { valid, errors } = validateSchema({
schema: input.body,
obj: body
});
if (!valid) {
throw Error(`${DEFAULT_ERRORS.invalidRequestBody}: ${errors}`);
}
}
if (!handler) {
throw Error("Handler not found.");
}
const res = await handler(
body,
middlewareOptions
);
return res;
};
const meta = {
openApiOperation,
input,
outputs,
middleware1,
middleware2,
middleware3,
handler
};
if (input?.body === void 0) {
const operation = async () => await callOperation();
operation._meta = meta;
return operation;
} else {
const operation = async (body) => await callOperation(body);
operation._meta = meta;
return operation;
}
}
return {
input: (input) => ({
outputs: (outputs) => ({
middleware: (middleware1) => ({
middleware: (middleware2) => ({
middleware: (middleware3) => ({
handler: (handler) => createOperation({
input,
outputs,
middleware1,
middleware2,
middleware3,
handler
})
}),
handler: (handler) => createOperation({
input,
outputs,
middleware1,
middleware2,
handler
})
}),
handler: (handler) => createOperation({
input,
outputs,
middleware1,
handler
})
}),
handler: (handler) => createOperation({
input,
outputs,
handler
})
}),
middleware: (middleware1) => ({
middleware: (middleware2) => ({
middleware: (middleware3) => ({
outputs: (outputs) => ({
handler: (handler) => createOperation({
input,
outputs,
middleware1,
middleware2,
middleware3,
handler
})
}),
handler: (handler) => createOperation({
input,
middleware1,
middleware2,
middleware3,
handler
})
}),
outputs: (outputs) => ({
handler: (handler) => createOperation({
input,
outputs,
middleware1,
middleware2,
handler
})
}),
handler: (handler) => createOperation({
input,
middleware1,
middleware2,
handler
})
}),
outputs: (outputs) => ({
handler: (handler) => createOperation({
input,
outputs,
middleware1,
handler
})
}),
handler: (handler) => createOperation({
input,
middleware1,
handler
})
}),
handler: (handler) => createOperation({
input,
handler
})
}),
outputs: (outputs) => ({
middleware: (middleware1) => ({
middleware: (middleware2) => ({
middleware: (middleware3) => ({
handler: (handler) => createOperation({
outputs,
middleware1,
middleware2,
middleware3,
handler
})
}),
handler: (handler) => createOperation({
outputs,
middleware1,
middleware2,
handler
})
}),
handler: (handler) => createOperation({
outputs,
middleware1,
handler
})
}),
handler: (handler) => createOperation({
outputs,
handler
})
}),
middleware: (middleware1) => ({
middleware: (middleware2) => ({
middleware: (middleware3) => ({
handler: (handler) => createOperation({ middleware1, middleware2, middleware3, handler })
}),
handler: (handler) => createOperation({ middleware1, middleware2, handler })
}),
handler: (handler) => createOperation({ middleware1, handler })
}),
handler: (handler) => createOperation({ handler })
};
};
// src/shared/config.ts
import { merge } from "lodash";
var DEFAULT_CONFIG = {
deniedPaths: [],
allowedPaths: ["**"],
openApiObject: {
info: {
title: DEFAULT_TITLE,
description: DEFAULT_DESCRIPTION,
version: `v${VERSION}`
}
},
openApiJsonPath: "/openapi.json",
docsConfig: {
provider: "redoc",
title: DEFAULT_TITLE,
description: DEFAULT_DESCRIPTION,
faviconUrl: DEFAULT_FAVICON_URL,
logoUrl: DEFAULT_LOGO_URL,
ogConfig: {
title: DEFAULT_TITLE,
type: DEFAULT_OG_TYPE,
url: HOMEPAGE,
imageUrl: DEFAULT_LOGO_URL
}
}
};
var getConfig = (config) => merge({}, DEFAULT_CONFIG, config);
// src/shared/docs.ts
var getHtmlForDocs = ({
config: {
openApiJsonPath,
openApiObject,
docsConfig: {
provider,
title = openApiObject?.info?.title ?? DEFAULT_TITLE,
description = openApiObject?.info?.description ?? DEFAULT_DESCRIPTION,
faviconUrl = DEFAULT_FAVICON_URL,
logoUrl = DEFAULT_LOGO_URL,
ogConfig: {
title: ogTitle = title,
type: ogType = DEFAULT_OG_TYPE,
url: orgUrl = HOMEPAGE,
imageUrl: ogImageUrl = DEFAULT_LOGO_URL
} = {
title: DEFAULT_TITLE,
type: "website",
url: HOMEPAGE,
imageUrl: DEFAULT_LOGO_URL
}
}
},
host
}) => {
const url = `//${host}${openApiJsonPath}`;
const htmlMetaTags = `<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${title}</title>
<meta
name="description"
content="${description}"
/>
<link rel="icon" type="image/x-icon" href="${faviconUrl}">
<meta property="og:title" content="${ogTitle}" />
<meta property="og:type" content="${ogType}" />
<meta property="og:url" content="${orgUrl}" />
<meta property="og:image" content="${ogImageUrl}" />`;
const redocHtml = `<!DOCTYPE html>
<html>
<head>
${htmlMetaTags}
<link
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
rel="stylesheet"
/>
</head>
<body>
<div id="redoc"></div>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
<script>
window.onload = () => {
fetch('${url}')
.then(res => res.json())
.then(spec => {
spec.info['title'] = "${title}";
spec.info['description'] = "${description}";
spec.info['x-logo'] = { url: "${logoUrl}" };
Redoc.init(spec, {}, document.getElementById('redoc'));
});
};
</script>
</body>
</html>`;
const swaggerUiHtml = `<!DOCTYPE html>
<html lang="en">
<head>
${htmlMetaTags}
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" />
<style>
.topbar-wrapper img {
content:url('${logoUrl}');
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js" crossorigin></script>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-standalone-preset.js" crossorigin></script>
<script>
window.onload = () => {
fetch('${url}')
.then(res => res.json())
.then(spec => {
spec.info['title'] = "${title}";
spec.info['description'] = "${description}";
window.ui = SwaggerUIBundle({
spec,
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
layout: 'StandaloneLayout',
deepLinking: true,
displayOperationId: true,
displayRequestDuration: true,
filter: true
});
});
};
</script>
</body>
</html>`;
if (provider === "swagger-ui") {
return swaggerUiHtml;
}
return redocHtml;
};
// src/shared/logging.ts
import chalk2 from "chalk";
var logPagesEdgeRuntimeErrorForRoute = (route) => {
console.error(
chalk2.red(`---
${route} is using Edge runtime in \`/pages\` folder that is not supported with \`apiRoute\`.
Please use \`route\` instead: https://vercel.com/docs/functions/edge-functions/quickstart
---`)
);
};
var logNextRestFrameworkError = (error) => {
console.error(
chalk2.red(`Next REST Framework encountered an error:
${error}`)
);
};
// src/shared/paths.ts
import { merge as merge2 } from "lodash";
// src/shared/utils.ts
var isValidMethod = (x) => Object.values(ValidMethod).includes(x);
var capitalizeFirstLetter = (str) => str[0]?.toUpperCase() + str.slice(1);
var parseRpcOperationResponseJson = async (res) => {
if (res instanceof FormData || res instanceof URLSearchParams) {
const body = {};
for (const [key, value] of res.entries()) {
body[key] = value;
}
return body;
}
return res;
};
// src/shared/paths.ts
var isSchemaRef = (schema) => "$ref" in schema;
var getPathsFromRoute = ({
operations,
options,
route
}) => {
const paths = {};
paths[route] = {
...options?.openApiPath
};
const requestBodySchemas = {};
const responseBodySchemas = {};
const baseResponseBodySchemaMapping = {
ErrorMessage: ERROR_MESSAGE_SCHEMA
};
Object.entries(operations).forEach(
([operationId, { openApiOperation, method: _method, input, outputs }]) => {
if (!isValidMethod(_method)) {
return;
}
const method = _method?.toLowerCase();
const generatedOperationObject = {
operationId
};
if (input?.body && input?.contentType) {
const key = `${capitalizeFirstLetter(operationId)}RequestBody`;
const schema = input.bodySchema ?? getJsonSchema({
schema: input.body,
operationId,
type: "input-body"
});
const ref = isSchemaRef(schema) ? schema.$ref : `#/components/schemas/${key}`;
if (!isSchemaRef(schema)) {
requestBodySchemas[method] = {
key,
ref,
schema
};
}
generatedOperationObject.requestBody = {
content: {
[input.contentType]: {
schema: {
$ref: ref
}
}
}
};
const description = input.bodySchema?.description ?? input.body._def.description;
if (description) {
generatedOperationObject.requestBody.description = description;
}
}
const usedStatusCodes = [];
const baseOperationResponses = {
500: UNEXPECTED_ERROR_RESPONSE
};
if (input?.bodySchema ?? input?.querySchema ?? input?.paramsSchema) {
baseResponseBodySchemaMapping.ZodIssue = ZOD_ISSUE_SCHEMA;
}
if (input?.bodySchema) {
baseOperationResponses[400] = INVALID_REQUEST_BODY_RESPONSE;
baseResponseBodySchemaMapping.MessageWithErrors = MESSAGE_WITH_ERRORS_SCHEMA;
}
if (input?.querySchema) {
baseOperationResponses[400] = INVALID_QUERY_PARAMETERS_RESPONSE;
baseResponseBodySchemaMapping.InvalidQueryParameters = MESSAGE_WITH_ERRORS_SCHEMA;
}
if (input?.paramsSchema) {
baseOperationResponses[400] = INVALID_PATH_PARAMETERS_RESPONSE;
baseResponseBodySchemaMapping.InvalidPathParameters = MESSAGE_WITH_ERRORS_SCHEMA;
}
generatedOperationObject.responses = outputs?.reduce(
(obj, { status, contentType, body, bodySchema, name }) => {
const occurrenceOfStatusCode = usedStatusCodes.includes(status) ? usedStatusCodes.filter((s) => s === status).length + 1 : "";
const key = name ?? `${capitalizeFirstLetter(
operationId
)}${status}ResponseBody${occurrenceOfStatusCode}`;
usedStatusCodes.push(status);
const schema = bodySchema ?? getJsonSchema({
schema: body,
operationId,
type: "output-body"
});
const ref = isSchemaRef(schema) ? schema.$ref : `#/components/schemas/${key}`;
if (!isSchemaRef(schema)) {
responseBodySchemas[method] = [
...responseBodySchemas[method] ?? [],
{
key,
ref,
schema
}
];
}
const description = bodySchema?.description ?? body._def.description ?? `Response for status ${status}`;
return Object.assign(obj, {
[status]: {
description,
content: {
[contentType]: {
schema: {
$ref: ref
}
}
}
}
});
},
baseOperationResponses
);
let pathParameters = [];
if (input?.params) {
const schema = input.paramsSchema ?? getJsonSchema({
schema: input.params,
operationId,
type: "input-params"
}).properties ?? {};
pathParameters = Object.entries(schema).map(([name, schema2]) => {
const _schema = input.params.shape[name];
return {
name,
in: "path",
required: !_schema.isOptional(),
schema: schema2
};
});
generatedOperationObject.parameters = [
...generatedOperationObject.parameters ?? [],
...pathParameters
];
}
const automaticPathParameters = route.match(/{([^}]+)}/g)?.map((param) => param.replace(/[{}]/g, "")).filter((_name) => !pathParameters?.some(({ name }) => name === _name));
if (automaticPathParameters?.length) {
generatedOperationObject.parameters = [
...generatedOperationObject.parameters ?? [],
...automaticPathParameters.map((name) => ({
name,
in: "path",
required: true,
schema: { type: "string" }
}))
];
}
if (input?.query) {
const schema = input.querySchema ?? getJsonSchema({
schema: input.query,
operationId,
type: "input-query"
}).properties ?? {};
generatedOperationObject.parameters = [
...generatedOperationObject.parameters ?? [],
...Object.entries(schema).map(([name, schema2]) => {
const _schema = input.query.shape[name];
return {
name,
in: "query",
required: !_schema.isOptional(),
schema: schema2
};
})
];
}
paths[route] = {
...paths[route],
[method]: merge2(generatedOperationObject, openApiOperation)
};
}
);
const requestBodySchemaMapping = Object.values(requestBodySchemas).reduce((acc, { key, schema }) => {
acc[key] = schema;
return acc;
}, {});
const responseBodySchemaMapping = Object.values(responseBodySchemas).flatMap((val) => val).reduce(
(acc, { key, schema }) => {
acc[key] = schema;
return acc;
},
baseResponseBodySchemaMapping
);
const schemas = {
...requestBodySchemaMapping,
...responseBodySchemaMapping
};
return { paths, schemas };
};
var getPathsFromRpcRoute = ({
operations,
options,
route: _route
}) => {
const paths = {};
const requestBodySchemas = {};
const responseBodySchemas = {};
const baseResponseBodySchemaMapping = {
ErrorMessage: ERROR_MESSAGE_SCHEMA
};
Object.entries(operations).forEach(
([
operationId,
{
_meta: { openApiOperation, input, outputs }
}
]) => {
const route = _route + `/${operationId}`;
paths[route] = {
...options?.openApiPath
};
const generatedOperationObject = {
operationId
};
if (input?.body && input.contentType) {
const key = `${capitalizeFirstLetter(operationId)}RequestBody`;
const schema = input.bodySchema ?? getJsonSchema({
schema: input.body,
operationId,
type: "input-body"
});
const ref = isSchemaRef(schema) ? schema.$ref : `#/components/schemas/${key}`;
if (!isSchemaRef(schema)) {
requestBodySchemas[operationId] = {
key,
ref,
schema
};
}
generatedOperationObject.requestBody = {
content: {
[input.contentType]: {
schema: {
$ref: ref
}
}
}
};
}
const baseOperationResponses = {};
if (input?.bodySchema) {
baseOperationResponses[400] = INVALID_RPC_REQUEST_RESPONSE;
baseResponseBodySchemaMapping.ZodIssue = ZOD_ISSUE_SCHEMA;
baseResponseBodySchemaMapping.MessageWithErrors = MESSAGE_WITH_ERRORS_SCHEMA;
} else {
baseOperationResponses[400] = UNEXPECTED_ERROR_RESPONSE;
}
generatedOperationObject.responses = outputs?.reduce(
(obj, { body, bodySchema, contentType, name }, i) => {
const key = name ?? `${capitalizeFirstLetter(operationId)}ResponseBody${i > 0 ? i + 1 : ""}`;
const schema = bodySchema ?? getJsonSchema({
schema: body,
operationId,
type: "output-body"
});
const ref = isSchemaRef(schema) ? schema.$ref : `#/components/schemas/${key}`;
if (!isSchemaRef(schema)) {
responseBodySchemas[operationId] = [
...responseBodySchemas[operationId] ?? [],
{
key,
ref,
schema
}
];
}
return Object.assign(obj, {
200: {
description: key,
content: {
[contentType]: {
schema: {
$ref: ref
}
}
}
}
});
},
baseOperationResponses
);
paths[route] = {
...paths[route],
["post"]: merge2(
generatedOperationObject,
openApiOperation
)
};
}
);
const requestBodySchemaMapping = Object.values(requestBodySchemas).reduce((acc, { key, schema }) => {
acc[key] = schema;
return acc;
}, {});
const responseBodySchemaMapping = Object.values(responseBodySchemas).flatMap((val) => val).reduce(
(acc, { key, schema }) => {
acc[key] = schema;
return acc;
},
baseResponseBodySchemaMapping
);
const schemas = {
...requestBodySchemaMapping,
...responseBodySchemaMapping
};
return { paths, schemas };
};
export {
getConfig,
getHtmlForDocs,
logPagesEdgeRuntimeErrorForRoute,
logNextRestFrameworkError,
validateSchema,
isValidMethod,
parseRpcOperationResponseJson,
getPathsFromRoute,
getPathsFromRpcRoute,
rpcOperation
};