UNPKG

next-rest-framework

Version:

Next REST Framework - Type-safe, self-documenting APIs for Next.js

671 lines (660 loc) 18.1 kB
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 };