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();
|