diff options
Diffstat (limited to 'scripts')
-rw-r--r-- | scripts/openapi.js | 186 | ||||
-rw-r--r-- | scripts/schema.js | 48 | ||||
-rw-r--r-- | scripts/util/getRouteDescriptions.js | 98 |
3 files changed, 160 insertions, 172 deletions
diff --git a/scripts/openapi.js b/scripts/openapi.js index 49d5dfde..8258a76c 100644 --- a/scripts/openapi.js +++ b/scripts/openapi.js @@ -27,34 +27,46 @@ require("missing-native-js-functions"); const openapiPath = path.join(__dirname, "..", "assets", "openapi.json"); const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json"); -let schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); - -for (var schema in schemas) { - const part = schemas[schema]; - for (var key in part.properties) { - if (part.properties[key].anyOf) { - const nullIndex = part.properties[key].anyOf.findIndex( - (x) => x.type == "null", - ); - if (nullIndex != -1) { - part.properties[key].nullable = true; - part.properties[key].anyOf.splice(nullIndex, 1); - - if (part.properties[key].anyOf.length == 1) { - Object.assign( - part.properties[key], - part.properties[key].anyOf[0], - ); - delete part.properties[key].anyOf; - } - } - } - } -} - -const specification = JSON.parse( - fs.readFileSync(openapiPath, { encoding: "utf8" }), -); +const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); +// const specification = JSON.parse( +// fs.readFileSync(openapiPath, { encoding: "utf8" }), +// ); +let specification = { + openapi: "3.1.0", + info: { + title: "Spacebar Server", + description: + "Spacebar is a free open source selfhostable discord compatible chat, voice and video platform", + license: { + name: "AGPLV3", + url: "https://www.gnu.org/licenses/agpl-3.0.en.html", + }, + version: "1.0.0", + }, + externalDocs: { + description: "Spacebar Docs", + url: "https://docs.spacebar.chat", + }, + servers: [ + { + url: "https://old.server.spacebar.chat/api/", + description: "Official Spacebar Instance", + }, + ], + components: { + securitySchemes: { + bearer: { + type: "http", + scheme: "bearer", + description: "Bearer/Bot prefixes are not required.", + bearerFormat: "JWT", + in: "header", + }, + }, + }, + tags: [], + paths: {}, +}; function combineSchemas(schemas) { var definitions = {}; @@ -72,6 +84,11 @@ function combineSchemas(schemas) { } for (const key in definitions) { + const reg = new RegExp(/^[a-zA-Z0-9.\-_]+$/, "gm"); + if (!reg.test(key)) { + console.error(`Invalid schema name: ${key} (${reg.test(key)})`); + continue; + } specification.components = specification.components || {}; specification.components.schemas = specification.components.schemas || {}; @@ -102,30 +119,20 @@ function getTag(key) { 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.", - }, - }; + // populate tags + const tags = Array.from(routes.keys()) + .map((x) => getTag(x)) + .sort((a, b) => a.localeCompare(b)); + specification.tags = tags.unique().map((x) => ({ name: x })); 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; + obj["x-fires-event"] = route.event; if ( !NO_AUTHORIZATION_ROUTES.some((x) => { @@ -136,48 +143,56 @@ function apiRoutes() { obj.security = [{ bearer: [] }]; } - if (route.body) { + if (route.description) obj.description = route.description; + if (route.summary) obj.summary = route.summary; + if (route.deprecated) obj.deprecated = route.deprecated; + + if (route.requestBody) { obj.requestBody = { required: true, content: { "application/json": { - schema: { $ref: `#/components/schemas/${route.body}` }, + schema: { + $ref: `#/components/schemas/${route.requestBody}`, + }, }, }, }.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.responses) { + for (const [k, v] of Object.entries(route.responses)) { + let schema = { + $ref: `#/components/schemas/${v.body}`, + }; + + obj.responses = { + [k]: { + ...(v.body + ? { + description: + obj?.responses?.[k]?.description || "", + content: { + "application/json": { + schema: schema, + }, + }, + } + : { + description: "No description available", + }), }, - ], - }; - if (!route.test.body) schema = schema.allOf[0]; - + }.merge(obj.responses); + } + } else { obj.responses = { - [status]: { - ...(route.test.response.body - ? { - description: - obj?.responses?.[status]?.description || "", - content: { - "application/json": { - schema: schema, - }, - }, - } - : {}), + default: { + description: "No description available", }, - }.merge(obj.responses); - delete obj.responses.default; + }; } + + // handles path parameters if (p.includes(":")) { obj.parameters = p.match(/:\w+/g)?.map((x) => ({ name: x.replace(":", ""), @@ -187,16 +202,33 @@ function apiRoutes() { description: x.replace(":", ""), })); } + + if (route.query) { + // map to array + const query = Object.entries(route.query).map(([k, v]) => ({ + name: k, + in: "query", + required: v.required, + schema: { type: v.type }, + description: v.description, + })); + + obj.parameters = [...(obj.parameters || []), ...query]; + } + obj.tags = [...(obj.tags || []), getTag(p)].unique(); - specification.paths[path] = { - ...specification.paths[path], - [method]: obj, - }; + specification.paths[path] = Object.assign( + specification.paths[path] || {}, + { + [method]: obj, + }, + ); }); } function main() { + console.log("Generating OpenAPI Specification..."); combineSchemas(schemas); apiRoutes(); diff --git a/scripts/schema.js b/scripts/schema.js index c29d5bab..ff3280ac 100644 --- a/scripts/schema.js +++ b/scripts/schema.js @@ -34,9 +34,7 @@ const settings = { noExtraProps: true, defaultProps: false, }; -const compilerOptions = { - strictNullChecks: true, -}; + const Excluded = [ "DefaultSchema", "Schema", @@ -57,16 +55,10 @@ const Excluded = [ "PropertiesSchema", "AsyncSchema", "AnySchema", + "SMTPConnection.CustomAuthenticationResponse", + "TransportMakeRequestResponse", ]; -function modify(obj) { - for (var k in obj) { - if (typeof obj[k] === "object" && obj[k] !== null) { - modify(obj[k]); - } - } -} - function main() { const program = TJS.programFromConfig( path.join(__dirname, "..", "tsconfig.json"), @@ -75,14 +67,14 @@ function main() { const generator = TJS.buildGenerator(program, settings); if (!generator || !program) return; - let schemas = generator - .getUserSymbols() - .filter( - (x) => - (x.endsWith("Schema") || x.endsWith("Response")) && - !Excluded.includes(x), + let schemas = generator.getUserSymbols().filter((x) => { + return ( + (x.endsWith("Schema") || + x.endsWith("Response") || + x.startsWith("API")) && + !Excluded.includes(x) ); - console.log(schemas); + }); var definitions = {}; @@ -109,32 +101,12 @@ function main() { delete part.properties[key]; continue; } - - // if (part.properties[key].anyOf) { - // const nullIndex = part.properties[key].anyOf.findIndex( - // (x) => x.type == "null", - // ); - // if (nullIndex != -1) { - // part.properties[key].nullable = true; - // part.properties[key].anyOf.splice(nullIndex, 1); - - // if (part.properties[key].anyOf.length == 1) { - // Object.assign( - // part.properties[key], - // part.properties[key].anyOf[0], - // ); - // delete part.properties[key].anyOf; - // } - // } - // } } } definitions = { ...definitions, [name]: { ...part } }; } - modify(definitions); - fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4)); } diff --git a/scripts/util/getRouteDescriptions.js b/scripts/util/getRouteDescriptions.js index fe36c238..a79dac96 100644 --- a/scripts/util/getRouteDescriptions.js +++ b/scripts/util/getRouteDescriptions.js @@ -1,80 +1,64 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -const { traverseDirectory } = require("lambert-server"); -const path = require("path"); const express = require("express"); +const path = require("path"); +const { traverseDirectory } = require("lambert-server"); const RouteUtility = require("../../dist/api/util/handlers/route.js"); -const Router = express.Router; +const methods = ["get", "post", "put", "delete", "patch"]; 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); - opts.file = sourceFile; - // console.log(method, urlPath, opts); - } else { - console.log( - `${sourceFile}\nrouter.${method}("${path}") is missing the "route()" description middleware\n`, - ); - } -} +let currentPath = ""; -function routeOptions(opts) { - return opts; -} +/* + For some reason, if a route exports multiple functions, it won't be registered here! + If someone could fix that I'd really appreciate it, but for now just, don't do that :p +*/ -RouteUtility.route = routeOptions; +const proxy = (file, method, prefix, path, ...args) => { + const opts = args.find((x) => x?.prototype?.OPTS_MARKER == true); + if (!opts) + return console.error( + `${file} has route without route() description middleware`, + ); -express.Router = (opts) => { - const path = currentPath; - const file = currentFile; - const router = Router(opts); + console.log(prefix + path + " - " + method); + opts.file = file.replace("/dist/", "/src/").replace(".js", ".ts"); + routes.set(prefix + path + "|" + method, opts()); +}; - for (const method of methods) { - router[method] = registerPath.bind(null, file, method, path); - } +express.Router = () => { + return Object.fromEntries( + methods.map((method) => [ + method, + proxy.bind(null, currentFile, method, currentPath), + ]), + ); +}; - return router; +RouteUtility.route = (opts) => { + const func = function () { + return opts; + }; + func.prototype.OPTS_MARKER = true; + return func; }; 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; + + currentPath = file.replace(root.slice(0, -1), ""); + currentPath = currentPath.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path + currentPath = currentPath.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes + if (currentPath.endsWith("/index")) + currentPath = currentPath.slice(0, "/index".length * -1); // delete index from path try { require(file); - } catch (error) { - console.error("error loading file " + file, error); + } catch (e) { + console.error(e); } }); + return routes; }; |