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
JavaScript
// 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