next-rest-framework
Version:
Next REST Framework - Type-safe, self-documenting APIs for Next.js
671 lines (660 loc) • 18.1 kB
JavaScript
import {
DEFAULT_DESCRIPTION,
DEFAULT_ERRORS,
DEFAULT_FAVICON_URL,
DEFAULT_LOGO_URL,
DEFAULT_OG_TYPE,
DEFAULT_TITLE,
HOMEPAGE,
VERSION,
ValidMethod
} from "./chunk-6XTW5TF5.mjs";
// src/shared/schemas.ts
import { zodToJsonSchema } from "zod-to-json-schema";
var isZodSchema = (schema) => !!schema && typeof schema === "object" && "_def" in schema;
var isZodObjectSchema = (schema) => isZodSchema(schema) && "shape" in schema;
var zodSchemaValidator = ({
schema,
obj
}) => {
const data = schema.safeParse(obj);
const errors = !data.success ? data.error.issues : null;
return {
valid: data.success,
errors
};
};
var validateSchema = async ({
schema,
obj
}) => {
if (isZodSchema(schema)) {
return zodSchemaValidator({ schema, obj });
}
throw Error("Invalid schema.");
};
var getJsonSchema = ({
schema
}) => {
if (isZodSchema(schema)) {
return zodToJsonSchema(schema, {
$refStrategy: "none",
target: "openApi3"
});
}
throw Error("Invalid schema.");
};
var getSchemaKeys = ({ schema }) => {
if (isZodObjectSchema(schema)) {
return Object.keys(schema._def.shape());
}
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) {
const { valid, errors } = await validateSchema({
schema: input,
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 === 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
}
},
suppressInfo: false
};
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 chalk from "chalk";
var logPagesEdgeRuntimeErrorForRoute = (route) => {
console.error(
chalk.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(
chalk.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);
// src/shared/paths.ts
var getPathsFromRoute = ({
operations,
options,
route
}) => {
const paths = {};
paths[route] = {
...options?.openApiPath
};
const requestBodySchemas = {};
const responseBodySchemas = {};
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 = getJsonSchema({ schema: input.body });
requestBodySchemas[method] = {
key,
ref: `#/components/schemas/${key}`,
schema
};
generatedOperationObject.requestBody = {
content: {
[input.contentType]: {
schema: {
$ref: `#/components/schemas/${key}`
}
}
}
};
}
const usedStatusCodes = [];
generatedOperationObject.responses = outputs?.reduce(
(obj, { status, contentType, schema, name }) => {
const occurrenceOfStatusCode = usedStatusCodes.includes(status) ? usedStatusCodes.filter((s) => s === status).length + 1 : "";
const key = name ?? `${capitalizeFirstLetter(
operationId
)}${status}ResponseBody${occurrenceOfStatusCode}`;
usedStatusCodes.push(status);
responseBodySchemas[method] = [
...responseBodySchemas[method] ?? [],
{
key,
ref: `#/components/schemas/${key}`,
schema: getJsonSchema({ schema })
}
];
return Object.assign(obj, {
[status]: {
description: `Response for status ${status}`,
content: {
[contentType]: {
schema: {
$ref: `#/components/schemas/${key}`
}
}
}
}
});
},
{
500: {
description: DEFAULT_ERRORS.unexpectedError,
content: {
"application/json": {
schema: {
$ref: `#/components/schemas/UnexpectedError`
}
}
}
}
}
);
const pathParameters = route.match(/{([^}]+)}/g)?.map((param) => param.replace(/[{}]/g, ""));
if (pathParameters) {
generatedOperationObject.parameters = pathParameters.map((name) => ({
name,
in: "path",
required: true,
schema: { type: "string" }
}));
}
if (input?.query) {
generatedOperationObject.parameters = [
...generatedOperationObject.parameters ?? [],
...getSchemaKeys({
schema: input.query
}).filter((key) => !pathParameters?.includes(key)).map((key) => {
const schema = input.query.shape[key];
return {
name: key,
in: "query",
required: !schema.isOptional(),
schema: getJsonSchema({ schema })
};
})
];
}
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;
},
{
UnexpectedError: {
type: "object",
properties: {
message: { type: "string" }
},
additionalProperties: false
}
}
);
const schemas = {
...requestBodySchemaMapping,
...responseBodySchemaMapping
};
return { paths, schemas };
};
var getPathsFromRpcRoute = ({
operations,
options,
route: _route
}) => {
const paths = {};
const requestBodySchemas = {};
const responseBodySchemas = {};
Object.entries(operations).forEach(
([
operationId,
{
_meta: { openApiOperation, input, outputs }
}
]) => {
const route = _route + `/${operationId}`;
paths[route] = {
...options?.openApiPath
};
const generatedOperationObject = {
operationId
};
if (input) {
const key = `${capitalizeFirstLetter(operationId)}RequestBody`;
const ref = `#/components/schemas/${key}`;
requestBodySchemas[operationId] = {
key,
ref,
schema: getJsonSchema({ schema: input })
};
generatedOperationObject.requestBody = {
content: {
"application/json": {
schema: {
$ref: ref
}
}
}
};
}
generatedOperationObject.responses = outputs?.reduce(
(obj, { schema, name }, i) => {
const key = name ?? `${capitalizeFirstLetter(operationId)}ResponseBody${i > 0 ? i + 1 : ""}`;
responseBodySchemas[operationId] = [
...responseBodySchemas[operationId] ?? [],
{
key,
ref: `#/components/schemas/${key}`,
schema: getJsonSchema({ schema })
}
];
return Object.assign(obj, {
200: {
description: key,
content: {
"application/json": {
schema: {
$ref: `#/components/schemas/${key}`
}
}
}
}
});
},
{
400: {
description: DEFAULT_ERRORS.unexpectedError,
content: {
"application/json": {
schema: {
$ref: `#/components/schemas/UnexpectedError`
}
}
}
}
}
);
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;
},
{
UnexpectedError: {
type: "object",
properties: {
message: { type: "string" }
},
additionalProperties: false
}
}
);
const schemas = {
...requestBodySchemaMapping,
...responseBodySchemaMapping
};
return { paths, schemas };
};
export {
getConfig,
getHtmlForDocs,
logPagesEdgeRuntimeErrorForRoute,
logNextRestFrameworkError,
validateSchema,
isValidMethod,
getPathsFromRoute,
getPathsFromRpcRoute,
rpcOperation
};