diff --git a/scripts/openapi.js b/scripts/openapi.js
new file mode 100644
index 00000000..6f066e94
--- /dev/null
+++ b/scripts/openapi.js
@@ -0,0 +1,169 @@
+require("module-alias/register");
+const getRouteDescriptions = require("./util/getRouteDescriptions");
+const path = require("path");
+const fs = require("fs");
+const {
+ NO_AUTHORIZATION_ROUTES,
+} = require("../dist/api/middlewares/Authentication");
+require("missing-native-js-functions");
+
+const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
+const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json");
+const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
+const specification = JSON.parse(
+ fs.readFileSync(openapiPath, { encoding: "utf8" }),
+);
+
+function combineSchemas(schemas) {
+ var definitions = {};
+
+ for (const name in schemas) {
+ definitions = {
+ ...definitions,
+ ...schemas[name].definitions,
+ [name]: {
+ ...schemas[name],
+ definitions: undefined,
+ $schema: undefined,
+ },
+ };
+ }
+
+ for (const key in definitions) {
+ specification.components = specification.components || {};
+ specification.components.schemas =
+ specification.components.schemas || {};
+ specification.components.schemas[key] = definitions[key];
+ delete definitions[key].additionalProperties;
+ delete definitions[key].$schema;
+ const definition = definitions[key];
+
+ if (typeof definition.properties === "object") {
+ for (const property of Object.values(definition.properties)) {
+ if (Array.isArray(property.type)) {
+ if (property.type.includes("null")) {
+ property.type = property.type.find((x) => x !== "null");
+ property.nullable = true;
+ }
+ }
+ }
+ }
+ }
+
+ return definitions;
+}
+
+function getTag(key) {
+ return key.match(/\/([\w-]+)/)[1];
+}
+
+function apiRoutes() {
+ const routes = getRouteDescriptions();
+
+ const tags = Array.from(routes.keys()).map((x) => getTag(x));
+ specification.tags = specification.tags || [];
+ specification.tags = [...specification.tags.map((x) => x.name), ...tags]
+ .unique()
+ .map((x) => ({ name: x }));
+
+ specification.components = specification.components || {};
+ specification.components.securitySchemes = {
+ bearer: {
+ type: "http",
+ scheme: "bearer",
+ description: "Bearer/Bot prefixes are not required.",
+ },
+ };
+
+ routes.forEach((route, pathAndMethod) => {
+ const [p, method] = pathAndMethod.split("|");
+ const path = p.replace(/:(\w+)/g, "{$1}");
+
+ specification.paths = specification.paths || {};
+ let obj = specification.paths[path]?.[method] || {};
+ obj["x-right-required"] = route.right;
+ obj["x-permission-required"] = route.permission;
+ obj["x-fires-event"] = route.test?.event;
+
+ if (
+ !NO_AUTHORIZATION_ROUTES.some((x) => {
+ if (typeof x === "string") return path.startsWith(x);
+ return x.test(path);
+ })
+ ) {
+ obj.security = [{ bearer: true }];
+ }
+
+ if (route.body) {
+ obj.requestBody = {
+ required: true,
+ content: {
+ "application/json": {
+ schema: { $ref: `#/components/schemas/${route.body}` },
+ },
+ },
+ }.merge(obj.requestBody);
+ }
+
+ if (route.test?.response) {
+ const status = route.test.response.status || 200;
+ let schema = {
+ allOf: [
+ {
+ $ref: `#/components/schemas/${route.test.response.body}`,
+ },
+ {
+ example: route.test.body,
+ },
+ ],
+ };
+ if (!route.test.body) schema = schema.allOf[0];
+
+ obj.responses = {
+ [status]: {
+ ...(route.test.response.body
+ ? {
+ description:
+ obj?.responses?.[status]?.description || "",
+ content: {
+ "application/json": {
+ schema: schema,
+ },
+ },
+ }
+ : {}),
+ },
+ }.merge(obj.responses);
+ delete obj.responses.default;
+ }
+ if (p.includes(":")) {
+ obj.parameters = p.match(/:\w+/g)?.map((x) => ({
+ name: x.replace(":", ""),
+ in: "path",
+ required: true,
+ schema: { type: "string" },
+ description: x.replace(":", ""),
+ }));
+ }
+ obj.tags = [...(obj.tags || []), getTag(p)].unique();
+
+ specification.paths[path] = {
+ ...specification.paths[path],
+ [method]: obj,
+ };
+ });
+}
+
+function main() {
+ combineSchemas(schemas);
+ apiRoutes();
+
+ fs.writeFileSync(
+ openapiPath,
+ JSON.stringify(specification, null, 4)
+ .replaceAll("#/definitions", "#/components/schemas")
+ .replaceAll("bigint", "number"),
+ );
+}
+
+main();
diff --git a/scripts/util/getRouteDescriptions.js b/scripts/util/getRouteDescriptions.js
new file mode 100644
index 00000000..21c36ca7
--- /dev/null
+++ b/scripts/util/getRouteDescriptions.js
@@ -0,0 +1,63 @@
+const { traverseDirectory } = require("lambert-server");
+const path = require("path");
+const express = require("express");
+const RouteUtility = require("../../dist/api/util/handlers/route.js");
+const Router = express.Router;
+
+const routes = new Map();
+let currentPath = "";
+let currentFile = "";
+const methods = ["get", "post", "put", "delete", "patch"];
+
+function registerPath(file, method, prefix, path, ...args) {
+ const urlPath = prefix + path;
+ const sourceFile = file.replace("/dist/", "/src/").replace(".js", ".ts");
+ const opts = args.find((x) => typeof x === "object");
+ if (opts) {
+ routes.set(urlPath + "|" + method, opts); // @ts-ignore
+ opts.file = sourceFile;
+ // console.log(method, urlPath, opts);
+ } else {
+ console.log(
+ `${sourceFile}\nrouter.${method}("${path}") is missing the "route()" description middleware\n`,
+ );
+ }
+}
+
+function routeOptions(opts) {
+ return opts;
+}
+
+// @ts-ignore
+RouteUtility.route = routeOptions;
+
+express.Router = (opts) => {
+ const path = currentPath;
+ const file = currentFile;
+ const router = Router(opts);
+
+ for (const method of methods) {
+ router[method] = registerPath.bind(null, file, method, path);
+ }
+
+ return router;
+};
+
+module.exports = function getRouteDescriptions() {
+ const root = path.join(__dirname, "..", "..", "dist", "api", "routes", "/");
+ traverseDirectory({ dirname: root, recursive: true }, (file) => {
+ currentFile = file;
+ let path = file.replace(root.slice(0, -1), "");
+ path = path.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path
+ path = path.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes
+ if (path.endsWith("/index")) path = path.slice(0, "/index".length * -1); // delete index from path
+ currentPath = path;
+
+ try {
+ require(file);
+ } catch (error) {
+ console.error("error loading file " + file, error);
+ }
+ });
+ return routes;
+};
|