summary refs log tree commit diff
path: root/scripts
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-01-06 19:30:03 +1100
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-01-06 19:30:03 +1100
commit98d318fd88cc42d290add88a81b2d5d5915de019 (patch)
treebbf740cd8154144956f17a0c0251428efb569949 /scripts
parentDon't allow BaseClass props through schema (diff)
downloadserver-98d318fd88cc42d290add88a81b2d5d5915de019.tar.xz
add back openapi generation. todo: find way to keep route text descriptions in code, and find way to get usages of right/permission .hasThrow
Diffstat (limited to 'scripts')
-rw-r--r--scripts/openapi.js169
-rw-r--r--scripts/util/getRouteDescriptions.js63
2 files changed, 232 insertions, 0 deletions
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;
+};