@backan/core
Version:
All in one web API builder. Create endpoints with type validations and OpenApi documentation, safely and quickly.
568 lines (567 loc) • 15.2 kB
JavaScript
import { swaggerUI } from "@hono/swagger-ui";
import { cache } from "hono/cache";
import { cors } from "hono/cors";
import "hono/client";
import { trimTrailingSlash, appendTrailingSlash } from "hono/trailing-slash";
import "hono/adapter";
import "hono/secure-headers";
import { ipRestriction } from "hono/ip-restriction";
import "hono/compress";
import "hono/timeout";
import "hono/body-limit";
import { prettyJSON } from "hono/pretty-json";
import "hono/combine";
import "hono/context-storage";
import "hono/cookie";
import { z, OpenAPIHono } from "@hono/zod-openapi";
import "hono/factory";
import { poweredBy } from "hono/powered-by";
import "hono/accepts";
import "hono/proxy";
import "hono/jwt";
import "hono/jwk";
import "hono/csrf";
import "hono/etag";
import "hono/logger";
import "hono/language";
import "hono/method-override";
import "hono/http-exception";
const defaultHOpts = {
error400: "Error in health check.",
summary: "Check API health status",
description: "Check if your API goes into trouble.",
additionalResponseValues: {}
};
const setHealthRoute = (app, path, opts = {}) => {
const {
error400,
additionalResponseValues,
summary,
description
} = {
...defaultHOpts,
...opts
};
if (error400) app.RESPONSE_MESSAGES.ERROR_400 = error400;
const getValidationSuccessParams = () => {
const defaultValue = { ok: app.validation.literal(true) };
if (!additionalResponseValues) return defaultValue;
const res = defaultValue;
for (const key in additionalResponseValues) {
if (Object.prototype.hasOwnProperty.call(additionalResponseValues, key)) {
const value = additionalResponseValues[key];
res[key] = app.validation.literal(value);
}
}
return res;
};
app.add(
{
method: "get",
path,
summary,
description,
responses: {
200: app.response.responseJSONSuccess(app.validation.object(getValidationSuccessParams())),
400: app.response.responseJSONError400,
500: app.response.responseJSONError500
},
tags: ["health"]
},
async (c) => {
try {
const response2 = {
ok: true,
...additionalResponseValues
};
return app.response.addSuccessResponse(c, response2);
} catch (e) {
return app.response.add500Error(c, e);
}
}
);
return app;
};
const RESPONSE_MESSAGES = {
/**
* Message for a 500 Internal Server Error.
*/
ERROR_500: "Internal Server error",
/**
* Message for a 404 Page Not Found error.
*/
ERROR_404: "Page not found",
/**
* Message for a 400 Bad Request error.
*/
ERROR_400: "Bad request",
/**
* Message indicating no data error.
*/
NO_DATA_ERROR: "No data error",
/**
* Help message for a 500 Internal Server Error.
*/
HELP_500: "Contact the developers to report the issue or wait until the issue is resolved",
/**
* Help message for a 400 Bad Request error.
*/
HELP_400: "Please read the documentation for a successful response",
/**
* Message indicating successful data fetch.
*/
SUCCESS_FETCH: "Successfully fetched data",
/**
* Message indicating a 404 Page Not Found error.
*/
ERROR_PAGE_NOT_FOUND: "Page not found",
/**
* Message indicating a validation error.
*/
ERROR_VALIDATION: "Error in validation request"
};
const ERROR_ID = {
/**
* ID for a 404 Page Not Found error.
*/
PAGE_NOT_FOUND: "PAGE-NOT-FOUND",
/**
* ID for a 400 Bad Request error.
*/
BAD_REQUEST: "BAD-REQUEST",
/**
* ID for a 500 Internal Server Error during server fetch.
*/
SERVER_FETCH: "SERVER-FETCH",
/**
* ID for validation errors.
*/
VALIDATION: "VALIDATION",
/**
* ID indicating no documentation was provided.
*/
NO_DOCS_PROVIDED: "NO-DOCS-PROVIDED"
};
const add500Error = (c, data) => {
const res = add500ErrorObject(data);
return c.json(res, res.status);
};
const add500ErrorObject = (data) => {
const status = 500;
return {
status,
...data,
message: RESPONSE_MESSAGES.ERROR_500,
error: getErrorResponse(data.error),
help: RESPONSE_MESSAGES.HELP_500
};
};
const getErrorResponse = (e) => {
if (!e) return RESPONSE_MESSAGES.NO_DATA_ERROR;
if (e instanceof Error) return e.stack ? e.stack.split("\n").map((line) => line.trim()) : "";
else if (typeof e === "object" && Object.keys(e).length <= 0) return RESPONSE_MESSAGES.NO_DATA_ERROR;
else return e;
};
const add400Error = (c, data) => {
const res = add400ErrorObject(data);
return c.json(res, res.status);
};
const add400ErrorObject = (data) => {
const status = 400;
return {
status,
...data,
error: getErrorResponse(data.error),
help: data.help || RESPONSE_MESSAGES.HELP_400
};
};
const add404Error = (c, data) => {
const status = 404;
return c.json({
status,
...data
}, status);
};
const addSuccessResponse = (c, data) => {
return c.json(
data,
200
);
};
const addSuccess201Response = (c, data) => {
return c.json(
data,
201
);
};
const addSuccess202Response = (c, data) => {
return c.json(
data,
202
);
};
const error = (status) => {
return z.object({
status: z.literal(status),
id: z.string(),
message: z.string(),
error: z.object({}),
help: z.string()
});
};
const schemaError500 = error(500);
const schemaError404 = error(404);
const schemaError400 = error(400);
const responseJSONSuccess = (schema, more) => ({
description: RESPONSE_MESSAGES.SUCCESS_FETCH,
content: { "application/json": { schema } },
...more ? more : {}
});
const responseStreamSuccess = (schema, more) => ({
description: RESPONSE_MESSAGES.SUCCESS_FETCH,
content: { "text/plain": { schema } },
...more ? more : {}
});
const responseJSONError500 = {
description: RESPONSE_MESSAGES.ERROR_500,
content: { "application/json": { schema: schemaError500 } }
};
const responseJSONError404 = {
description: RESPONSE_MESSAGES.ERROR_404,
content: { "application/json": { schema: schemaError404 } }
};
const responseJSONError400 = {
description: RESPONSE_MESSAGES.ERROR_400,
content: { "application/json": { schema: schemaError400 } }
};
const response = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
add400Error,
add400ErrorObject,
add404Error,
add500Error,
add500ErrorObject,
addSuccess201Response,
addSuccess202Response,
addSuccessResponse,
responseJSONError400,
responseJSONError404,
responseJSONError500,
responseJSONSuccess,
responseStreamSuccess,
schemaError400,
schemaError404,
schemaError500
}, Symbol.toStringTag, { value: "Module" }));
class AppSuper {
/**
* Method to add route with OpenAPI configuration.
*/
add;
constructor() {
this.add = this.app.openapi;
}
/**
* Validation option works with zod library.
* @see https://zod.dev/
* @example const stringSchema = validation.string()
*/
validation = z;
RESPONSE_MESSAGES = RESPONSE_MESSAGES;
/**
* Predefined error IDs used for consistent error identification in responses.
* These are used throughout the application to ensure uniform error handling.
*
*/
ERROR_ID = ERROR_ID;
/**
* Contains methods to generate and handle responses, including error handling.
*
* This object allows adding success responses, 500 errors, and 400 errors with predefined structures.
*/
response = {
...response,
addSuccessResponse,
add500ErrorObject,
add500Error: (c, e) => {
const data = {
id: this.ERROR_ID.SERVER_FETCH,
error: e
};
this.logger(JSON.stringify(add500ErrorObject(data)));
return add500Error(c, data);
},
add400ErrorObject: (e) => {
return add400ErrorObject({
id: e && typeof e == "object" && "message" in e ? e.message : this.ERROR_ID.BAD_REQUEST,
message: this.RESPONSE_MESSAGES.ERROR_400,
error: e
});
},
add400Error: (c, e) => {
const res = this.response.add400ErrorObject(e);
this.logger(JSON.stringify(res));
return add400Error(c, res);
}
};
app = new OpenAPIHono({ defaultHook: (res, c) => {
if (res.success) return res;
return this.response.add400Error(c, {
id: this.ERROR_ID.VALIDATION,
message: this.RESPONSE_MESSAGES.ERROR_400,
error: res.error.issues
});
} });
/**
* Hono app instace for add hono custom routes or middlewars or etc
*/
honoInstance = this.app;
/**
* Logs a string of data, determining if it's JSON and formatting accordingly.
* This method can be overridden to customize how logging is handled within the application.
* By default, it logs data to the console, parsing JSON strings if necessary.
* @param {string} data - The string to log. If it's JSON, it will be parsed and logged as an object.
* @example
*
* // Customizing logger
* app.logger = (data: string) => {
* // Custom logging logic, e.g., writing to a file
* fs.appendFileSync('app.log', data + '\n');
* };
*/
logger = (data) => {
const isJsonString = (str) => {
try {
JSON.parse(str);
return true;
} catch (_e) {
return false;
}
};
const isJSON = isJsonString(data);
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
console.log(
isJSON ? {
...JSON.parse(data),
time: timestamp
} : data
);
};
/**
* Adds a route to the BACKAN application instance.
* @param {Route} app - The route to add, containing the path and the associated app.
* @param route
* @deprecated
*/
addRoute(route) {
this.app.route(route.path, route.app);
}
/**
* Adds a route to the BACKAN application instance.
* @param {AppSuper} app - The route to add, containing the path and the associated app.
* @param path
* @param route
*/
route(path, route) {
this.app.route(path, route.app);
}
// eslint-disable-next-line jsdoc/require-param
/**
* Registers an OpenAPI component within the app's OpenAPI registry.
* @returns {void}
*/
addComponent(type, name, component) {
return this.app.openAPIRegistry.registerComponent(type, name, component);
}
/**
* Retrieves a list of unique paths from the application's routes.
* @returns {string[]} - An array of unique paths as strings.
*/
getPaths() {
const uniquePaths = new Set(
this.app.all().routes.filter((d) => d.path && !d.path.endsWith("*")).map((d) => d.path)
);
return Array.from(uniquePaths);
}
}
class App extends AppSuper {
/**
* The version of the application.
*/
version;
/**
* The title of the application.
*/
title;
/**
* A brief description of the application.
*/
description;
/**
* Contact information for the application.
*/
contact;
#jsonPretty;
#docs = {
path: "/docs",
active: true
};
#health = {
path: "/health",
active: true,
opts: void 0
};
#openApiConfig;
#setNotFound() {
this.app.notFound((c) => {
const url = this.getDocUrl(c);
return add404Error(c, {
id: this.ERROR_ID.PAGE_NOT_FOUND,
message: "Page not found.",
help: url ? `Go to: ${url}` : this.ERROR_ID.NO_DOCS_PROVIDED
});
});
}
#setDocs() {
if (!this.#docs.path || !this.#docs.active) return;
const optsJson = `${this.#docs.path}.json`;
this.app.doc(optsJson, this.#openApiConfig);
this.app.get(this.#docs.path, swaggerUI({ url: optsJson }));
}
#setHealthPath() {
if (!this.#health.path || !this.#health.active) return;
setHealthRoute(
this,
this.#health.path,
this.#health.opts
);
}
#setJSONPrettify() {
if (typeof this.#jsonPretty === "string") this.app.use(prettyJSON({
space: 4,
query: this.#jsonPretty
}));
else this.app.use(async (c, next) => {
await next();
if (c.res.headers.get("Content-Type")?.startsWith("application/json")) {
const obj = await c.res.json();
c.res = new Response(JSON.stringify(obj, null, 2), c.res);
}
});
}
constructor(data) {
const {
jsonPretty = true,
cors: cors$1,
version,
title,
description,
docs,
health,
contact,
cache: cache$1,
trailingSlash,
hook,
poweredBy: poweredBy$1
} = data || {};
const isRoute = version && title && description;
super();
this.#jsonPretty = jsonPretty;
this.version = version || "";
this.title = title || "";
this.description = description || "";
if (contact) this.contact = contact;
if (hook) hook.beforeAll?.(this.app);
if (poweredBy$1 !== false) this.app.use(
"*",
poweredBy({ serverName: poweredBy$1 || "backan" })
);
if (trailingSlash) this.app.use(
"*",
trailingSlash === "trim" ? trimTrailingSlash() : appendTrailingSlash()
);
if (this.#jsonPretty) this.#setJSONPrettify();
if (cors$1) this.app.use("*", cors(cors$1));
if (docs?.active || docs?.active == false) this.#docs.active = docs.active;
if (docs?.path) this.#docs.path = docs.path;
if (health?.active || health?.active == false)
this.#health.active = health.active;
if (health?.path) this.#health.path = health.path;
if (health?.opts) this.#health.opts = {
...this.#health.opts,
...health.opts
};
this.#openApiConfig = {
openapi: "3.0.0",
info: {
version: this.version,
title: this.title,
description: this.description,
contact: this.contact
}
};
if (cache$1) this.app.use("*", cache(cache$1));
if (isRoute) {
this.#setDocs();
this.#setNotFound();
} else {
if (health?.active === true) this.#setHealthPath();
}
this.fetch = this.app.fetch;
}
setIpRestriction({
pattern = "*",
getIP,
rules,
onError
}) {
this.app.use(
pattern,
ipRestriction(getIP, rules || {}, onError)
);
}
/**
* The fetch method for the application.
*
* Will be entry point of your application..
* @type {Function}
*/
fetch;
/**
* Generates the full URL for the OpenAPI documentation endpoint.
* @param {Context} c - The Hono context object.
* @returns {string | undefined} - The full URL for the OpenAPI documentation, or undefined if not configured.
*/
getDocUrl(c) {
const url = new URL(c.req.url);
const protocol = url.protocol ? `${url.protocol}//` : "";
const port = url.port ? `:${url.port}` : "";
return this.#docs.path && this.#docs.active ? `${protocol}${url.hostname}${port}${this.#docs.path}` : void 0;
}
/**
* Retrieves the OpenAPI configuration object.
* @returns {object} - The OpenAPI document object.
*/
getOpenApiObject() {
return this.app.getOpenAPIDocument(this.#openApiConfig);
}
}
class Route extends AppSuper {
/**
* The path of the route.
*/
path;
/**
* Method to add OpenAPI configuration to the route.
*/
add;
constructor(params) {
super();
this.path = params.path;
this.add = this.app.openapi;
}
}
export {
App,
Route
};