UNPKG

chanfana

Version:

OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!

1,536 lines (1,508 loc) 50.6 kB
// src/index.ts import { extendZodWithOpenApi as extendZodWithOpenApi3 } from "@asteasolutions/zod-to-openapi"; // src/openapi.ts import { OpenApiGeneratorV3, OpenApiGeneratorV31 } from "@asteasolutions/zod-to-openapi"; import yaml from "js-yaml"; import { z } from "zod"; // src/ui.ts function getSwaggerUI(schemaUrl) { schemaUrl = schemaUrl.replace(/\/+(\/|$)/g, "$1"); return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="description" content="SwaggerIU"/> <title>SwaggerUI</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.17.14/swagger-ui.css" integrity="sha256-QBcPDuhZ0X+SExunBzKaiKBw5PZodNETZemnfSMvYRc=" crossorigin="anonymous"> <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlMb//2ux//9or///ZKz//wlv5f8JcOf/CnXv/why7/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2vi/wZo3/9ytf//b7P//2uw//+BvP//DHbp/w568P8Md+//CnXv/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApv4/8HbOH/lMf//3W3//9ytf//brL//w946v8SfvH/EHzw/w558P8AAAAAAAAAAAAAAAAAAAAAAAAAABF56f8Ndef/C3Dj/whs4f98u///eLn//3W3//+Evv//FoPx/xSA8f8SfvD/EHvw/wAAAAAAAAAAAAAAAA1EeF0WgOz/EXrp/w515v8LceT/lsn//3+9//97u///eLj//xaB7f8YhfL/FoLx/xSA8f8JP/deAAAAAAAAAAAgjfH/HIjw/xeB7P8Te+n/AAAAAAAAAACGwf//gr///369//+Iwf//HIny/xqH8v8YhfL/FYLx/wAAAAAnlfPlJJLy/yGO8v8cifD/GILt/wAAAAAAAAAAmMz//4nD//+Fwf//gb///xyJ8P8ejPP/HIny/xmH8v8XhPLnK5r0/yiW8/8lk/P/IpDy/wAAAAAAAAAAAAAAAAAAAACPx///jMX//4jD//+MxP//IpD0/yCO8/8di/P/G4ny/y6e9f8sm/T/KZj0/yaV8/8AAAAAAAAAAAAAAAAAAAAAlsz//5LJ//+Px///lMn//yaV9P8kkvT/IZD0/x+O8/8yo/blMKD1/y2d9f8qmfT/KJbz/wAAAAAAAAAAqdb//53Q//+Zzv//lsv//yiY8/8qmvX/KJf1/yWV9P8jkvTQAAAAADSl9v8xofX/Lp71/yyb9P8AAAAAAAAAAKfW//+k1P//oNL//6rW//8wofb/Lp72/yuc9f8pmfX/AAAAAAAAAAAcVHtcNab2/zKj9v8voPX/LZz0/7vh//+u2///qtj//6fW//8wofT/NKX3/zKj9/8voPb/F8/6XgAAAAAAAAAAAAAAADmr9/82qPf/M6T2/zCg9f+44f//td///7Hd//++4v//Oqz4/ziq+P81p/f/M6X3/wAAAAAAAAAAAAAAAAAAAAAAAAAAOqz4/zep9//M6///v+X//7vj//+44f//OKn1/z6x+f88rvn/Oaz4/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD6x+f8qmfP/yOv//8bq///C5///z+z//0O3+v9Ctfr/QLP5/z2x+f8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0u///8jr///I6///yOv//zmq9f9Dt/r/Q7f6/0O3+v8AAAAAAAAAAAAAAAAAAAAA8A8AAOAHAADgBwAAwAMAAMADAACGAQAABgAAAA8AAAAPAAAABgAAAIYBAADAAwAAwAMAAOAHAADgBwAA8A8AAA==" /> </head> <body> <div id="swagger-ui"></div> <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.17.14/swagger-ui-bundle.js" integrity="sha256-wuSp7wgUSDn/R8FCAgY+z+TlnnCk5xVKJr1Q2IDIi6E=" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.17.14/swagger-ui-standalone-preset.js" integrity="sha256-M7em9a/KxJAv35MoG+LS4S2xXyQdOEYG5ubRd0W3+G8=" crossorigin="anonymous"></script> <script> window.onload = () => { window.ui = SwaggerUIBundle({ url: '${schemaUrl}', dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis ] }); }; </script> </body> </html>`; } function getReDocUI(schemaUrl) { schemaUrl = schemaUrl.replace(/\/+(\/|$)/g, "$1"); return `<!DOCTYPE html> <html> <head> <title>ReDocUI</title> <!-- needed for adaptive design --> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlMb//2ux//9or///ZKz//wlv5f8JcOf/CnXv/why7/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2vi/wZo3/9ytf//b7P//2uw//+BvP//DHbp/w568P8Md+//CnXv/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApv4/8HbOH/lMf//3W3//9ytf//brL//w946v8SfvH/EHzw/w558P8AAAAAAAAAAAAAAAAAAAAAAAAAABF56f8Ndef/C3Dj/whs4f98u///eLn//3W3//+Evv//FoPx/xSA8f8SfvD/EHvw/wAAAAAAAAAAAAAAAA1EeF0WgOz/EXrp/w515v8LceT/lsn//3+9//97u///eLj//xaB7f8YhfL/FoLx/xSA8f8JP/deAAAAAAAAAAAgjfH/HIjw/xeB7P8Te+n/AAAAAAAAAACGwf//gr///369//+Iwf//HIny/xqH8v8YhfL/FYLx/wAAAAAnlfPlJJLy/yGO8v8cifD/GILt/wAAAAAAAAAAmMz//4nD//+Fwf//gb///xyJ8P8ejPP/HIny/xmH8v8XhPLnK5r0/yiW8/8lk/P/IpDy/wAAAAAAAAAAAAAAAAAAAACPx///jMX//4jD//+MxP//IpD0/yCO8/8di/P/G4ny/y6e9f8sm/T/KZj0/yaV8/8AAAAAAAAAAAAAAAAAAAAAlsz//5LJ//+Px///lMn//yaV9P8kkvT/IZD0/x+O8/8yo/blMKD1/y2d9f8qmfT/KJbz/wAAAAAAAAAAqdb//53Q//+Zzv//lsv//yiY8/8qmvX/KJf1/yWV9P8jkvTQAAAAADSl9v8xofX/Lp71/yyb9P8AAAAAAAAAAKfW//+k1P//oNL//6rW//8wofb/Lp72/yuc9f8pmfX/AAAAAAAAAAAcVHtcNab2/zKj9v8voPX/LZz0/7vh//+u2///qtj//6fW//8wofT/NKX3/zKj9/8voPb/F8/6XgAAAAAAAAAAAAAAADmr9/82qPf/M6T2/zCg9f+44f//td///7Hd//++4v//Oqz4/ziq+P81p/f/M6X3/wAAAAAAAAAAAAAAAAAAAAAAAAAAOqz4/zep9//M6///v+X//7vj//+44f//OKn1/z6x+f88rvn/Oaz4/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD6x+f8qmfP/yOv//8bq///C5///z+z//0O3+v9Ctfr/QLP5/z2x+f8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0u///8jr///I6///yOv//zmq9f9Dt/r/Q7f6/0O3+v8AAAAAAAAAAAAAAAAAAAAA8A8AAOAHAADgBwAAwAMAAMADAACGAQAABgAAAA8AAAAPAAAABgAAAIYBAADAAwAAwAMAAOAHAADgBwAA8A8AAA==" /> <!-- ReDoc doesn't change outer page styles --> <style> body { margin: 0; padding: 0; } </style> </head> <body> <redoc spec-url="${schemaUrl}"></redoc> <script src="https://cdn.jsdelivr.net/npm/redoc@2.1.5/bundles/redoc.standalone.js" integrity="sha256-vlwzMMjDW4/OsppbdVKtRb/8L9lJT+LhqC+pQXnrX48=" crossorigin="anonymous"></script> </body> </html>`; } // src/zod/registry.ts import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; var OpenAPIRegistryMerger = class extends OpenAPIRegistry { _definitions = []; merge(registry, basePath) { if (!registry || !registry._definitions) return; for (const definition of registry._definitions) { if (basePath) { this._definitions.push({ ...definition, route: { ...definition.route, path: `${basePath}${definition.route.path}` } }); } else { this._definitions.push({ ...definition }); } } } }; // src/openapi.ts var OpenAPIHandler = class { router; options; registry; allowedMethods = ["get", "head", "post", "put", "delete", "patch"]; constructor(router, options) { this.router = router; this.options = options || {}; this.registry = new OpenAPIRegistryMerger(); this.createDocsRoutes(); } createDocsRoutes() { if (this.options?.docs_url !== null && this.options?.openapi_url !== null) { this.router.get(this.options?.docs_url || "/docs", () => { return new Response(getSwaggerUI((this.options?.base || "") + (this.options?.openapi_url || "/openapi.json")), { headers: { "content-type": "text/html; charset=UTF-8" }, status: 200 }); }); } if (this.options?.redoc_url !== null && this.options?.openapi_url !== null) { this.router.get(this.options?.redoc_url || "/redocs", () => { return new Response(getReDocUI((this.options?.base || "") + (this.options?.openapi_url || "/openapi.json")), { headers: { "content-type": "text/html; charset=UTF-8" }, status: 200 }); }); } if (this.options?.openapi_url !== null) { this.router.get((this.options?.base || "") + (this.options?.openapi_url || "/openapi.json"), () => { return new Response(JSON.stringify(this.getGeneratedSchema()), { headers: { "content-type": "application/json;charset=UTF-8" }, status: 200 }); }); this.router.get( (this.options?.base || "") + (this.options?.openapi_url || "/openapi.json").replace(".json", ".yaml"), () => { return new Response(yaml.dump(this.getGeneratedSchema()), { headers: { "content-type": "text/yaml;charset=UTF-8" }, status: 200 }); } ); } } getGeneratedSchema() { let openapiGenerator = OpenApiGeneratorV31; if (this.options?.openapiVersion === "3") openapiGenerator = OpenApiGeneratorV3; const generator = new openapiGenerator(this.registry.definitions); return generator.generateDocument({ openapi: this.options?.openapiVersion === "3" ? "3.0.3" : "3.1.0", info: { version: this.options?.schema?.info?.version || "1.0.0", title: this.options?.schema?.info?.title || "OpenAPI", ...this.options?.schema?.info }, ...this.options?.schema }); } registerNestedRouter(params) { const path = params.nestedRouter.options?.base ? void 0 : params.path ? params.path.replaceAll(/\/+(\/|$)/g, "$1").replaceAll(/:(\w+)/g, "{$1}") : void 0; this.registry.merge(params.nestedRouter.registry, path); return [params.nestedRouter.fetch]; } parseRoute(path) { return ((this.options.base || "") + path).replaceAll(/\/+(\/|$)/g, "$1").replaceAll(/:(\w+)/g, "{$1}"); } registerRoute(params) { const parsedRoute = this.parseRoute(params.path); const parsedParams = ((this.options.base || "") + params.path).match(/:(\w+)/g); let urlParams = []; if (parsedParams) { urlParams = parsedParams.map((obj) => obj.replace(":", "")); } let schema = void 0; let operationId = void 0; for (const handler of params.handlers) { if (handler.name) { operationId = `${params.method}_${handler.name}`; } if (handler.isRoute === true) { schema = new handler({ route: parsedRoute, urlParams }).getSchemaZod(); break; } } if (operationId === void 0) { operationId = `${params.method}_${parsedRoute.replaceAll("/", "_")}`; } if (schema === void 0) { schema = { operationId, responses: { 200: { description: "Successful response." } } }; if (urlParams.length > 0) { schema.request = { params: z.object( urlParams.reduce( (obj, item) => Object.assign(obj, { [item]: z.string() }), {} ) ) }; } } else { if (!schema.operationId) { if (this.options?.generateOperationIds === false && !schema.operationId) { throw new Error(`Route ${params.path} don't have operationId set!`); } schema.operationId = operationId; } } if (params.doRegister === void 0 || params.doRegister) { this.registry.registerPath({ ...schema, // @ts-ignore method: params.method, path: parsedRoute }); } return params.handlers.map((handler) => { if (handler.isRoute) { return (...params2) => new handler({ router: this, route: parsedRoute, urlParams // raiseUnknownParameters: openapiConfig.raiseUnknownParameters, TODO }).execute(...params2); } return handler; }); } handleCommonProxy(target, prop, ...args) { if (prop === "middleware") { return []; } if (prop === "isChanfana") { return true; } if (prop === "original") { return this.router; } if (prop === "schema") { return this.getGeneratedSchema(); } if (prop === "registry") { return this.registry; } return void 0; } getRequest(args) { throw new Error("getRequest not implemented"); } getUrlParams(args) { throw new Error("getUrlParams not implemented"); } getBindings(args) { throw new Error("getBindings not implemented"); } }; // src/parameters.ts import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; import { z as z2 } from "zod"; // src/zod/utils.ts function isAnyZodType(schema) { return schema._def !== void 0; } function isSpecificZodType(field, typeName) { return field._def.typeName === typeName || field._def.innerType?._def.typeName === typeName || field._def.schema?._def.innerType?._def.typeName === typeName || field.unwrap?.()._def.typeName === typeName || field.unwrap?.().unwrap?.()._def.typeName === typeName || field._def.innerType?._def?.innerType?._def?.typeName === typeName; } function legacyTypeIntoZod(type, params) { params = params || {}; if (type === null) { return Str({ required: false, ...params }); } if (isAnyZodType(type)) { if (params) { return convertParams(type, params); } return type; } if (type === String) { return Str(params); } if (typeof type === "string") { return Str({ example: type }); } if (type === Number) { return Num(params); } if (typeof type === "number") { return Num({ example: type }); } if (type === Boolean) { return Bool(params); } if (typeof type === "boolean") { return Bool({ example: type }); } if (type === Date) { return DateTime(params); } if (Array.isArray(type)) { if (type.length === 0) { throw new Error("Arr must have a type"); } return Arr(type[0], params); } if (typeof type === "object") { return Obj(type, params); } return type(params); } // src/parameters.ts extendZodWithOpenApi(z2); function convertParams(field, params) { params = params || {}; if (params.required === false) field = field.optional(); if (params.description) field = field.describe(params.description); if (params.default) field = field.default(params.default); if (params.example) { field = field.openapi({ example: params.example }); } if (params.format) { field = field.openapi({ format: params.format }); } return field; } function Arr(innerType, params) { return convertParams(legacyTypeIntoZod(innerType).array(), params); } function Obj(fields, params) { const parsed = {}; for (const [key, value] of Object.entries(fields)) { parsed[key] = legacyTypeIntoZod(value); } return convertParams(z2.object(parsed), params); } function Num(params) { return convertParams(z2.number(), params).openapi({ type: "number" }); } function Int(params) { return convertParams(z2.number().int(), params).openapi({ type: "integer" }); } function Str(params) { return convertParams(z2.string(), params); } function DateTime(params) { return convertParams( z2.string().datetime({ message: "Must be in the following format: YYYY-mm-ddTHH:MM:ssZ" }), params ); } function Regex(params) { return convertParams( // @ts-ignore z2.string().regex(params.pattern, params.patternError || "Invalid"), params ); } function Email(params) { return convertParams(z2.string().email(), params); } function Uuid(params) { return convertParams(z2.string().uuid(), params); } function Hostname(params) { return convertParams( z2.string().regex( /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/ ), params ); } function Ipv4(params) { return convertParams(z2.string().ip({ version: "v4" }), params); } function Ipv6(params) { return convertParams(z2.string().ip({ version: "v6" }), params); } function Ip(params) { return convertParams(z2.string().ip(), params); } function DateOnly(params) { return convertParams(z2.date(), params); } function Bool(params) { return convertParams(z2.boolean(), params).openapi({ type: "boolean" }); } function Enumeration(params) { let { values } = params; const originalValues = { ...values }; if (Array.isArray(values)) values = Object.fromEntries(values.map((x) => [x, x])); const originalKeys = Object.keys(values); if (params.enumCaseSensitive === false) { values = Object.keys(values).reduce((accumulator, key) => { accumulator[key.toLowerCase()] = values[key]; return accumulator; }, {}); } const keys = Object.keys(values); let field; if ([void 0, true].includes(params.enumCaseSensitive)) { field = z2.enum(keys); } else { field = z2.preprocess((val) => String(val).toLowerCase(), z2.enum(keys)).openapi({ enum: originalKeys }); } field = field.transform((val) => values[val]); const result = convertParams(field, params); result.values = originalValues; return result; } function coerceInputs(data, schema) { if (data.size === 0 || data.size === void 0 && typeof data === "object" && Object.keys(data).length === 0) { return null; } const params = {}; const entries = data.entries ? data.entries() : Object.entries(data); for (let [key, value] of entries) { if (value === "") { value = null; } if (params[key] === void 0) { params[key] = value; } else if (!Array.isArray(params[key])) { params[key] = [params[key], value]; } else { params[key].push(value); } let innerType; if (schema && schema.shape && schema.shape[key]) { innerType = schema.shape[key]; } else if (schema) { innerType = schema; } if (innerType) { if (isSpecificZodType(innerType, "ZodArray") && !Array.isArray(params[key])) { params[key] = [params[key]]; } else if (isSpecificZodType(innerType, "ZodBoolean")) { const _val = params[key].toLowerCase().trim(); if (_val === "true" || _val === "false") { params[key] = _val === "true"; } } else if (isSpecificZodType(innerType, "ZodNumber") || innerType instanceof z2.ZodNumber) { params[key] = Number.parseFloat(params[key]); } else if (isSpecificZodType(innerType, "ZodBigInt") || innerType instanceof z2.ZodBigInt) { params[key] = Number.parseInt(params[key]); } else if (isSpecificZodType(innerType, "ZodDate") || innerType instanceof z2.ZodDate) { params[key] = new Date(params[key]); } } } return params; } // src/route.ts import { extendZodWithOpenApi as extendZodWithOpenApi2 } from "@asteasolutions/zod-to-openapi"; import { z as z5 } from "zod"; // src/exceptions.ts import { z as z4 } from "zod"; // src/contentTypes.ts import { z as z3 } from "zod"; var contentJson = (schema) => ({ content: { "application/json": { schema: schema instanceof z3.ZodType ? schema : legacyTypeIntoZod(schema) } } }); // src/exceptions.ts var ApiException = class extends Error { isVisible = true; message; default_message = "Internal Error"; status = 500; code = 7e3; includesPath = false; constructor(message = "") { super(message); this.message = message; } buildResponse() { return [ { code: this.code, message: this.isVisible ? this.message || this.default_message : "Internal Error" } ]; } static schema() { const inst = new this(); const innerError = { code: inst.code, message: inst.default_message }; if (inst.includesPath === true) { innerError.path = ["body", "fieldName"]; } return { [inst.status]: { description: inst.default_message, ...contentJson({ success: z4.literal(false), errors: [innerError] }) } }; } }; var InputValidationException = class extends ApiException { isVisible = true; default_message = "Input Validation Error"; status = 400; code = 7001; path = null; includesPath = true; constructor(message, path) { super(message); this.path = path; } buildResponse() { return [ { code: this.code, message: this.isVisible ? this.message : "Internal Error", path: this.path } ]; } }; var MultiException = class extends ApiException { isVisible = true; errors; status = 400; constructor(errors) { super("Multiple Exceptions"); this.errors = errors; for (const err of errors) { if (err.status > this.status) { this.status = err.status; } if (!err.isVisible && this.isVisible) { this.isVisible = false; } } } buildResponse() { return this.errors.flatMap((err) => err.buildResponse()); } }; var NotFoundException = class extends ApiException { isVisible = true; default_message = "Not Found"; status = 404; code = 7002; }; // src/utils.ts function jsonResp(data, params) { return new Response(JSON.stringify(data), { headers: { "content-type": "application/json;charset=UTF-8" }, // @ts-ignore status: params?.status ? params.status : 200, ...params }); } // src/route.ts extendZodWithOpenApi2(z5); var OpenAPIRoute = class { handle(...args) { throw new Error("Method not implemented."); } static isRoute = true; args; // Args the execute() was called with validatedData = void 0; // this acts as a cache, in case the users calls the validate method twice params; schema = {}; constructor(params) { this.params = params; this.args = []; } async getValidatedData() { const request = this.params.router.getRequest(this.args); if (this.validatedData !== void 0) return this.validatedData; const data = await this.validateRequest(request); this.validatedData = data; return data; } getSchema() { return this.schema; } getSchemaZod() { const schema = { ...this.getSchema() }; if (!schema.responses) { schema.responses = { "200": { description: "Successful response", content: { "application/json": { schema: {} } } } }; } return schema; } handleValidationError(errors) { return jsonResp( { errors, success: false, result: {} }, { status: 400 } ); } async execute(...args) { this.validatedData = void 0; this.args = args; let resp; try { resp = await this.handle(...args); } catch (e) { if (e instanceof z5.ZodError) { return this.handleValidationError(e.errors); } throw e; } if (!(resp instanceof Response) && typeof resp === "object") { return jsonResp(resp); } return resp; } async validateRequest(request) { const schema = this.getSchemaZod(); const unvalidatedData = {}; const rawSchema = {}; if (schema.request?.params) { rawSchema.params = schema.request?.params; unvalidatedData.params = coerceInputs(this.params.router.getUrlParams(this.args), schema.request?.params); } if (schema.request?.query) { rawSchema.query = schema.request?.query; unvalidatedData.query = {}; } if (schema.request?.headers) { rawSchema.headers = schema.request?.headers; unvalidatedData.headers = {}; } const { searchParams } = new URL(request.url); const queryParams = coerceInputs(searchParams, schema.request?.query); if (queryParams !== null) unvalidatedData.query = queryParams; if (schema.request?.headers) { const tmpHeaders = {}; const rHeaders = new Headers(request.headers); for (const header of Object.keys((schema.request?.headers).shape)) { tmpHeaders[header] = rHeaders.get(header); } unvalidatedData.headers = coerceInputs(tmpHeaders, schema.request?.headers); } if (request.method.toLowerCase() !== "get" && schema.request?.body && schema.request?.body.content["application/json"] && schema.request?.body.content["application/json"].schema) { rawSchema.body = schema.request.body.content["application/json"].schema; try { unvalidatedData.body = await request.json(); } catch (e) { unvalidatedData.body = {}; } } let validationSchema = z5.object(rawSchema); if (this.params?.raiseUnknownParameters === void 0 || this.params?.raiseUnknownParameters === true) { validationSchema = validationSchema.strict(); } return await validationSchema.parseAsync(unvalidatedData); } }; // src/adapters/ittyRouter.ts var IttyRouterOpenAPIHandler = class extends OpenAPIHandler { getRequest(args) { return args[0]; } getUrlParams(args) { return args[0].params; } getBindings(args) { return args[1]; } }; function fromIttyRouter(router, options) { const openapiRouter = new IttyRouterOpenAPIHandler(router, options); return new Proxy(router, { get: (target, prop, ...args) => { const _result = openapiRouter.handleCommonProxy(target, prop, ...args); if (_result !== void 0) { return _result; } return (route, ...handlers) => { if (prop !== "fetch") { if (handlers.length === 1 && handlers[0].isChanfana === true) { handlers = openapiRouter.registerNestedRouter({ method: prop, nestedRouter: handlers[0], path: void 0 }); } else if (openapiRouter.allowedMethods.includes(prop)) { handlers = openapiRouter.registerRoute({ method: prop, path: route, handlers }); } } return Reflect.get(target, prop, ...args)(route, ...handlers); }; } }); } // src/adapters/hono.ts var HIJACKED_METHODS = /* @__PURE__ */ new Set(["basePath", "on", "route", "delete", "get", "patch", "post", "put", "all"]); var HonoOpenAPIHandler = class extends OpenAPIHandler { getRequest(args) { return args[0].req.raw; } getUrlParams(args) { return args[0].req.param(); } getBindings(args) { return args[0].env; } }; function fromHono(router, options) { const openapiRouter = new HonoOpenAPIHandler(router, options); const proxy = new Proxy(router, { get: (target, prop, ...args) => { const _result = openapiRouter.handleCommonProxy(target, prop, ...args); if (_result !== void 0) { return _result; } if (typeof target[prop] !== "function") { return target[prop]; } return (route, ...handlers) => { if (prop !== "fetch") { if (prop === "route" && handlers.length === 1 && handlers[0].isChanfana === true) { openapiRouter.registerNestedRouter({ method: "", nestedRouter: handlers[0], path: route }); const subApp = handlers[0].original.basePath(""); const excludePath = /* @__PURE__ */ new Set(["/openapi.json", "/openapi.yaml", "/docs", "/redocs"]); subApp.routes = subApp.routes.filter((obj) => { return !excludePath.has(obj.path); }); router.route(route, subApp); return proxy; } if (prop === "all" && handlers.length === 1 && handlers[0].isRoute) { handlers = openapiRouter.registerRoute({ method: prop, path: route, handlers, doRegister: false }); } else if (openapiRouter.allowedMethods.includes(prop)) { handlers = openapiRouter.registerRoute({ method: prop, path: route, handlers }); } else if (prop === "on") { const methods = route; const paths = handlers.shift(); if (Array.isArray(methods) || Array.isArray(paths)) { throw new Error("chanfana only supports single method+path on hono.on('method', 'path', EndpointClass)"); } handlers = openapiRouter.registerRoute({ method: methods.toLowerCase(), path: paths, handlers }); handlers = [paths, ...handlers]; } } const resp = Reflect.get(target, prop, ...args)(route, ...handlers); if (HIJACKED_METHODS.has(prop)) { return proxy; } return resp; }; } }); return proxy; } // src/endpoints/types.ts function MetaGenerator(meta) { return { fields: meta.fields ?? meta.model.schema, model: { serializer: (obj) => obj, serializerSchema: meta.model.schema, ...meta.model }, pathParameters: meta.pathParameters ?? null }; } // src/endpoints/create.ts var CreateEndpoint = class extends OpenAPIRoute { // @ts-ignore _meta; get meta() { return MetaGenerator(this._meta); } getSchema() { const bodyParameters = this.meta.fields.omit( (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}) ); const pathParameters = this.meta.fields.pick( (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}) ); return { request: { body: contentJson(bodyParameters), params: Object.keys(pathParameters.shape).length ? pathParameters : void 0, ...this.schema?.request }, responses: { "201": { description: "Returns the created Object", ...contentJson({ success: Boolean, result: this.meta.model.serializerSchema }), ...this.schema?.responses?.[200] }, ...InputValidationException.schema(), ...this.schema?.responses }, ...this.schema }; } async getObject() { const data = await this.getValidatedData(); const newData = { ...data.body }; for (const param of this.params.urlParams) { newData[param] = data.params[param]; } return newData; } async before(data) { return data; } async after(data) { return data; } async create(data) { return data; } async handle(...args) { let obj = await this.getObject(); obj = await this.before(obj); obj = await this.create(obj); obj = await this.after(obj); return Response.json( { success: true, result: this.meta.model.serializer(obj) }, { status: 201 } ); } }; // src/endpoints/delete.ts var DeleteEndpoint = class extends OpenAPIRoute { // @ts-ignore _meta; get meta() { return MetaGenerator(this._meta); } getSchema() { const bodyParameters = this.meta.fields.pick((this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {})).omit((this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {})); const pathParameters = this.meta.fields.pick((this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {})).pick((this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {})); return { request: { body: Object.keys(bodyParameters.shape).length ? contentJson(bodyParameters) : void 0, params: Object.keys(pathParameters.shape).length ? pathParameters : void 0, ...this.schema?.request }, responses: { "200": { description: "Returns the Object if it was successfully deleted", ...contentJson({ success: Boolean, result: this.meta.model.serializerSchema }), ...this.schema?.responses?.[200] }, ...NotFoundException.schema(), ...this.schema?.responses }, ...this.schema }; } async getFilters() { const data = await this.getValidatedData(); const filters = []; for (const part of [data.params, data.body]) { if (part) { for (const [key, value] of Object.entries(part)) { filters.push({ field: key, operator: "EQ", value }); } } } return { filters }; } async before(oldObj, filters) { return filters; } async after(data) { return data; } async delete(oldObj, filters) { return null; } async getObject(filters) { return null; } async handle(...args) { let filters = await this.getFilters(); const oldObj = await this.getObject(filters); if (oldObj === null) { throw new NotFoundException(); } filters = await this.before(oldObj, filters); let obj = await this.delete(oldObj, filters); if (obj === null) { throw new NotFoundException(); } obj = await this.after(obj); return { success: true, result: this.meta.model.serializer(obj) }; } }; // src/endpoints/read.ts var ReadEndpoint = class extends OpenAPIRoute { // @ts-ignore _meta; get meta() { return MetaGenerator(this._meta); } getSchema() { if (!this.meta.pathParameters && this.meta.model.primaryKeys.sort().toString() !== this.params.urlParams.sort().toString()) { throw Error( `Model primaryKeys differ from urlParameters on: ${this.params.route}: ${JSON.stringify(this.meta.model.primaryKeys)} !== ${JSON.stringify(this.params.urlParams)}, fix url parameters or define pathParameters in your Model` ); } const inputPathParameters = this.meta.pathParameters ?? this.meta.model.primaryKeys; const pathParameters = this.meta.fields.pick( (inputPathParameters || []).reduce((a, v) => ({ ...a, [v]: true }), {}) ); return { request: { //query: queryParameters, params: Object.keys(pathParameters.shape).length ? pathParameters : void 0, ...this.schema?.request }, responses: { "200": { description: "Returns a single object if found", ...contentJson({ success: Boolean, result: this.meta.model.serializerSchema }), ...this.schema?.responses?.[200] }, ...NotFoundException.schema(), ...this.schema?.responses }, ...this.schema }; } async getFilters() { const data = await this.getValidatedData(); const filters = []; for (const part of [data.params, data.query]) { if (part) { for (const [key, value] of Object.entries(part)) { filters.push({ field: key, operator: "EQ", value }); } } } return { filters, options: {} // TODO: make a new type for this }; } async before(filters) { return filters; } async after(data) { return data; } async fetch(filters) { return null; } async handle(...args) { let filters = await this.getFilters(); filters = await this.before(filters); let obj = await this.fetch(filters); if (!obj) { throw new NotFoundException(); } obj = await this.after(obj); return { success: true, result: this.meta.model.serializer(obj) }; } }; // src/endpoints/list.ts import { z as z6 } from "zod"; var ListEndpoint = class extends OpenAPIRoute { // @ts-ignore _meta; get meta() { return MetaGenerator(this._meta); } filterFields; searchFields; searchFieldName = "search"; optionFields = ["page", "per_page", "order_by", "order_by_direction"]; orderByFields = []; defaultOrderBy; getSchema() { const parsedQueryParameters = this.meta.fields.pick((this.filterFields || []).reduce((a, v) => ({ ...a, [v]: true }), {})).omit((this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {})).shape; const pathParameters = this.meta.fields.pick( (this.params.urlParams || this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {}) ); for (const [key, value] of Object.entries(parsedQueryParameters)) { parsedQueryParameters[key] = value.optional(); } if (this.searchFields) { parsedQueryParameters[this.searchFieldName] = z6.string().optional().openapi({ description: `Search by ${this.searchFields.join(", ")}` }); } let queryParameters = z6.object({ page: z6.number().int().min(1).optional().default(1), per_page: z6.number().int().min(1).max(100).optional().default(20) }).extend(parsedQueryParameters); if (this.orderByFields && this.orderByFields.length > 0) { queryParameters = queryParameters.extend({ order_by: Enumeration({ default: this.orderByFields[0], values: this.orderByFields, description: "Order By Column Name", required: false }), order_by_direction: Enumeration({ default: "asc", values: ["asc", "desc"], description: "Order By Direction", required: false }) }); } return { request: { params: Object.keys(pathParameters.shape).length ? pathParameters : void 0, query: queryParameters, ...this.schema?.request }, responses: { "200": { description: "List objects", ...contentJson({ success: Boolean, result: [this.meta.model.serializerSchema] }), ...this.schema?.responses?.[200] }, ...this.schema?.responses }, ...this.schema }; } async getFilters() { const data = await this.getValidatedData(); const filters = []; const options = {}; for (const part of [data.params, data.query]) { if (part) { for (const [key, value] of Object.entries(part)) { if (this.searchFields && key === this.searchFieldName) { filters.push({ field: key, operator: "LIKE", value }); } else if (this.optionFields.includes(key)) { options[key] = value; } else { filters.push({ field: key, operator: "EQ", value }); } } } } return { options, filters }; } async before(filters) { return filters; } async after(data) { return data; } async list(filters) { return { result: [] }; } async handle(...args) { let filters = await this.getFilters(); filters = await this.before(filters); let objs = await this.list(filters); objs = await this.after(objs); objs = { ...objs, result: objs.result.map(this.meta.model.serializer) }; return { success: true, ...objs }; } }; // src/endpoints/update.ts var UpdateEndpoint = class extends OpenAPIRoute { // @ts-ignore _meta; get meta() { return MetaGenerator(this._meta); } getSchema() { const bodyParameters = this.meta.fields.omit( (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}) ); const pathParameters = this.meta.model.schema.pick( (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}) ); return { request: { body: contentJson(bodyParameters), params: Object.keys(pathParameters.shape).length ? pathParameters : void 0, ...this.schema?.request }, responses: { "200": { description: "Returns the updated Object", ...contentJson({ success: Boolean, result: this.meta.model.serializerSchema }), ...this.schema?.responses?.[200] }, ...InputValidationException.schema(), ...NotFoundException.schema(), ...this.schema?.responses }, ...this.schema }; } async getFilters() { const data = await this.getValidatedData(); const filters = []; const updatedData = {}; for (const part of [data.params, data.body]) { if (part) { for (const [key, value] of Object.entries(part)) { if ((this.meta.model.primaryKeys || []).includes(key)) { filters.push({ field: key, operator: "EQ", value }); } else { updatedData[key] = value; } } } } return { filters, updatedData }; } async before(oldObj, filters) { return filters; } async after(data) { return data; } async getObject(filters) { return null; } async update(oldObj, filters) { return oldObj; } async handle(...args) { let filters = await this.getFilters(); const oldObj = await this.getObject(filters); if (oldObj === null) { throw new NotFoundException(); } filters = await this.before(oldObj, filters); let obj = await this.update(oldObj, filters); obj = await this.after(obj); return { success: true, result: this.meta.model.serializer(obj) }; } }; // src/endpoints/d1/create.ts var D1CreateEndpoint = class extends CreateEndpoint { dbName = "DB"; logger; constraintsMessages = {}; getDBBinding() { const env = this.params.router.getBindings(this.args); if (env[this.dbName] === void 0) { throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); } if (env[this.dbName].prepare === void 0) { throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); } return env[this.dbName]; } async create(data) { let inserted; try { const result = await this.getDBBinding().prepare( `INSERT INTO ${this.meta.model.tableName} (${Object.keys(data).join(", ")}) VALUES (${Object.values(data).map(() => "?").join(", ")}) RETURNING *` ).bind(...Object.values(data)).all(); inserted = result.results[0]; } catch (e) { if (this.logger) this.logger.error(`Caught exception while trying to create ${this.meta.model.tableName}: ${e.message}`); if (e.message.includes("UNIQUE constraint failed")) { const constraintMessage = e.message.split("UNIQUE constraint failed:")[1].split(":")[0].trim(); if (this.constraintsMessages[constraintMessage]) { throw this.constraintsMessages[constraintMessage]; } } throw new ApiException(e.message); } if (this.logger) this.logger.log(`Successfully created ${this.meta.model.tableName}`); return inserted; } }; // src/endpoints/d1/delete.ts var D1DeleteEndpoint = class extends DeleteEndpoint { dbName = "DB"; logger; getDBBinding() { const env = this.params.router.getBindings(this.args); if (env[this.dbName] === void 0) { throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); } if (env[this.dbName].prepare === void 0) { throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); } return env[this.dbName]; } getSafeFilters(filters) { const conditions = []; const conditionsParams = []; for (const f of filters.filters) { if (f.operator === "EQ") { conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); conditionsParams.push(f.value); } else { throw new ApiException(`operator ${f.operator} Not implemented`); } } return { conditions, conditionsParams }; } async getObject(filters) { const safeFilters = this.getSafeFilters(filters); const oldObj = await this.getDBBinding().prepare(`SELECT * FROM ${this.meta.model.tableName} WHERE ${safeFilters.conditions.join(" AND ")} LIMIT 1`).bind(...safeFilters.conditionsParams).all(); if (!oldObj.results || oldObj.results.length === 0) { return null; } return oldObj.results[0]; } async delete(oldObj, filters) { const safeFilters = this.getSafeFilters(filters); let result; try { result = await this.getDBBinding().prepare( `DELETE FROM ${this.meta.model.tableName} WHERE ${safeFilters.conditions.join(" AND ")} RETURNING * LIMIT 1` ).bind(...safeFilters.conditionsParams).all(); } catch (e) { if (this.logger) this.logger.error(`Caught exception while trying to delete ${this.meta.model.tableName}: ${e.message}`); throw new ApiException(e.message); } if (result.meta.changes === 0) { return null; } if (this.logger) this.logger.log(`Successfully deleted ${this.meta.model.tableName}`); return oldObj; } }; // src/endpoints/d1/read.ts var D1ReadEndpoint = class extends ReadEndpoint { dbName = "DB"; logger; getDBBinding() { const env = this.params.router.getBindings(this.args); if (env[this.dbName] === void 0) { throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); } if (env[this.dbName].prepare === void 0) { throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); } return env[this.dbName]; } async fetch(filters) { const conditions = filters.filters.map((obj2) => `${obj2.field} = ?`); const obj = await this.getDBBinding().prepare(`SELECT * FROM ${this.meta.model.tableName} WHERE ${conditions.join(" AND ")} LIMIT 1`).bind(...filters.filters.map((obj2) => obj2.value)).all(); if (!obj.results || obj.results.length === 0) { return null; } return obj.results[0]; } }; // src/endpoints/d1/list.ts var D1ListEndpoint = class extends ListEndpoint { dbName = "DB"; logger; getDBBinding() { const env = this.params.router.getBindings(this.args); if (env[this.dbName] === void 0) { throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); } if (env[this.dbName].prepare === void 0) { throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); } return env[this.dbName]; } async list(filters) { const offset = (filters.options.per_page || 20) * (filters.options.page || 0) - (filters.options.per_page || 20); const limit = filters.options.per_page; const conditions = []; const conditionsParams = []; for (const f of filters.filters) { if (this.searchFields && f.field === this.searchFieldName) { const searchCondition = this.searchFields.map((obj) => { return `UPPER(${obj}) like UPPER(?${conditionsParams.length + 1})`; }).join(" or "); conditions.push(`(${searchCondition})`); conditionsParams.push(`%${f.value}%`); } else if (f.operator === "EQ") { conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); conditionsParams.push(f.value); } else { throw new ApiException(`operator ${f.operator} Not implemented`); } } let where = ""; if (conditions.length > 0) { where = `WHERE ${conditions.join(" AND ")}`; } let orderBy = `ORDER BY ${this.defaultOrderBy || `${this.meta.model.primaryKeys[0]} DESC`}`; if (filters.options.order_by) { orderBy = `ORDER BY ${filters.options.order_by} ${filters.options.order_by_direction || "ASC"}`; } const results = await this.getDBBinding().prepare(`SELECT * FROM ${this.meta.model.tableName} ${where} ${orderBy} LIMIT ${limit} OFFSET ${offset}`).bind(...conditionsParams).all(); const total_count = await this.getDBBinding().prepare(`SELECT count(*) as total FROM ${this.meta.model.tableName} ${where} LIMIT ${limit}`).bind(...conditionsParams).all(); return { result: results.results, result_info: { count: results.results.length, page: filters.options.page, per_page: filters.options.per_page, total_count: total_count.results[0]?.total } }; } }; // src/endpoints/d1/update.ts var D1UpdateEndpoint = class extends UpdateEndpoint { dbName = "DB"; logger; constraintsMessages = {}; getDBBinding() { const env = this.params.router.getBindings(this.args); if (env[this.dbName] === void 0) { throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); } if (env[this.dbName].prepare === void 0) { throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); } return env[this.dbName]; } getSafeFilters(filters) { const safeFilters = filters.filters.filter((f) => { return this.meta.model.primaryKeys.includes(f.field); }); const conditions = []; const conditionsParams = []; for (const f of safeFilters) { if (f.operator === "EQ") { conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); conditionsParams.push(f.value); } else { throw new ApiException(`operator ${f.operator} Not implemented`); } } return { conditions, conditionsParams }; } async getObject(filters) { const safeFilters = this.getSafeFilters(filters); const oldObj = await this.getDBBinding().prepare( `SELECT * FROM ${this.meta.model.tableName} WHERE ${safeFilters.conditions.join(" AND ")} LIMIT 1` ).bind(...safeFilters.conditionsParams).all(); if (!oldObj.results || oldObj.results.length === 0) { return null; } return oldObj.results[0]; } async update(oldObj, filters) { const safeFilters = this.getSafeFilters(filters); let result; try { const obj = await this.getDBBinding().prepare( `UPDATE ${this.meta.model.tableName} SET ${Object.keys(filters.updatedData).map((key, index) => `${key} = ?${safeFilters.conditionsParams.length + index + 1}`)} WHERE ${safeFilters.conditions.join(" AND ")} RETURNING *` ).bind(...safeFilters.conditionsParams, ...Object.values(filters.updatedData)).all(); result = obj.results[0]; } catch (e) { if (this.logger) this.logger.error(`Caught exception while trying to update ${this.meta.model.tableName}: ${e.message}`); if (e.message.includes("UNIQUE constraint failed")) { const constraintMessage = e.message.split("UNIQUE constraint failed:")[1].split(":")[0].trim(); if (this.constraintsMessages[constraintMessage]) { throw this.constraintsMessages[constraintMessage]; } } throw new ApiException(e.message); } if (this.logger) this.logger.log(`Successfully updated ${this.meta.model.tableName}`); return result; } }; export { ApiException, Arr, Bool, CreateEndpoint, D1CreateEndpoint, D1DeleteEndpoint, D1ListEndpoint, D1ReadEndpoint, D1UpdateEndpoint, DateOnly, DateTime, D