diff options
author | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2023-01-06 19:30:03 +1100 |
---|---|---|
committer | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2023-01-06 19:30:03 +1100 |
commit | 98d318fd88cc42d290add88a81b2d5d5915de019 (patch) | |
tree | bbf740cd8154144956f17a0c0251428efb569949 /scripts | |
parent | Don't allow BaseClass props through schema (diff) | |
download | server-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.js | 169 | ||||
-rw-r--r-- | scripts/util/getRouteDescriptions.js | 63 |
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; +}; |