bananas-commerce-admin
Version:
What's this, an admin for apes?
255 lines • 10.6 kB
JavaScript
import SwaggerParser from "@apidevtools/swagger-parser";
import { getCookie } from "./util/get_cookie";
export const BODY_APPLICATION_TYPES = ["application/json", "multipart/form-data"];
export class ApiLoadFailedError extends Error {
response;
constructor(response, message) {
super(message);
this.response = response;
}
}
export class ApiOperation {
id;
tags;
server;
endpoint;
method;
request;
app;
summary;
description;
contribs;
bodyType;
component;
order;
constructor(id, tags, server, endpoint, method, request, app, summary, description, bodyType, contribs, component, order) {
this.id = id;
this.tags = tags;
this.server = server;
this.endpoint = endpoint;
this.method = method;
this.request = request;
this.app = app;
this.summary = summary;
this.description = description;
this.bodyType = bodyType;
this.contribs = contribs ?? [];
this.component = component;
this.order = order;
}
/**
* Return the `URL` for `endpoint`, possibly enriched with `request` params.
* Useful for operations that generate a static url, like a file download.
*/
url(request) {
let endpoint = this.endpoint;
// Add params to url
if (request?.params !== undefined) {
for (const [name, value] of Object.entries(request.params)) {
endpoint = endpoint.replace(`{${name}}`, value);
}
}
const url = new URL(this.server + endpoint);
// Add query to url
if (request?.query) {
let query;
try {
// @ts-expect-error - TODO
query = new URLSearchParams(request.query);
}
catch {
query = request?.query;
}
url.search = query.toString();
}
return url;
}
async call(request, init) {
const csrftoken = getCookie("csrftoken");
init ??= {};
init.method ??= this.method;
init.headers = new Headers(init.headers);
if (csrftoken !== undefined)
init.headers.append("X-CSRFToken", csrftoken);
init.credentials = "include";
// Add body to request
if (request?.body !== undefined) {
// "multipart/form-data" includes information about the FormData Boundry.
// This is a bit arbitrary, so we need to have it set automatically.
// Don't try to be smart when dealing with forms.
if (this.bodyType && this.bodyType !== "multipart/form-data") {
init.headers.set("Content-Type", this.bodyType);
}
switch (this.bodyType) {
case "application/json":
init.body ??= JSON.stringify(request?.body);
break;
case "multipart/form-data":
if (init.body) {
break;
}
else if (request.body instanceof FormData) {
init.body = request.body;
break;
}
else if (request.body instanceof HTMLFormElement) {
new FormData(request.body);
break;
}
else {
throw TypeError(`Wrong type passed to ApiOperation.Call. Expected multipart/form-data as FormData or HTMLFormElement.`);
}
default:
init.body ??= request.body;
break;
}
}
return await fetch(this.url(request), init);
}
}
export class ApiClient {
document;
operations = {};
contrib = {};
static async load(source, server) {
const response = await fetch(source);
if (!response.ok) {
throw new ApiLoadFailedError(response, `Could not load schema from ${source}`);
}
const schema = await response.json().catch((e) => {
throw new ApiLoadFailedError(response, `Could not parse schema from ${source}: ${e}`);
});
const document = await SwaggerParser.dereference(schema);
return new ApiClient(document, server);
}
constructor(document, server) {
this.document = document;
// If the document uses servers and it is not manually set this will be prepended to path later
if ("servers" in this.document &&
this.document.servers !== undefined &&
this.document.servers.length >= 1) {
server ??= this.document.servers.pop().url;
}
// Make sure the server variable is an URL
if (server !== undefined && !(server instanceof URL)) {
server = new URL(server);
}
// Build operations
for (const [path, definition] of Object.entries(this.document.paths ?? {})) {
for (const [method, operation] of Object.entries(definition)) {
let appName;
const contribs = [];
// All operations require an operation id
if (!operation.operationId) {
break;
}
// Check if an tag starting with `app:` exists on the operation
operation.tags?.forEach((tag) => {
if (tag.startsWith("app:")) {
appName = tag.replace(/^app:/, "");
}
else if (tag.startsWith("contrib:")) {
contribs.push(tag.replace(/^contrib:/, ""));
}
});
if (appName === undefined) {
break;
}
const request = {};
// Fill the request object with `path`, `query` and `body` data which may be provided
for (const parameter of operation.parameters ?? []) {
if ("in" in parameter) {
switch (parameter.in) {
case "body": {
request["body"] = {
required: parameter.required ?? false,
schema: parameter.schema,
};
break;
}
case "path": {
request["params"] ??= {};
// eslint-disable-next-line
// @ts-ignore
request["params"][parameter.name] = {
// Path parameters are always required: https://swagger.io/docs/specification/describing-parameters/#path-parameters
required: true,
schema: parameter.schema,
};
break;
}
case "query": {
request["query"] ??= {};
// eslint-disable-next-line
// @ts-ignore
request["query"][parameter.name] = {
required: parameter.required ?? false,
schema: parameter.schema,
};
}
}
}
}
let bodyType;
// If `requestBody` exists then add it to the request objects body object
if ("requestBody" in operation && operation.requestBody !== undefined) {
if ("content" in operation.requestBody) {
const bodyTypes = Object.keys(operation.requestBody.content).map((bodyType) => bodyType.toLowerCase());
for (const type of bodyTypes) {
// @ts-expect-error this is actually totally fine
if (BODY_APPLICATION_TYPES.includes(type)) {
const schema = operation.requestBody.content[type].schema;
const required = operation.requestBody.required ?? false;
bodyType = type;
request["body"] = {
schema,
required,
};
break;
}
}
}
}
// If no server is set, fall back to the current origin
server = server ? server.toString() : globalThis.origin;
// Combine base url and path
if (server.endsWith("/")) {
server = server.slice(0, -1);
}
const endpoint = path.startsWith("/") ? path : "/" + path;
// Create a new `ApiOperation` with the relevant information
this.operations[operation.operationId] = new ApiOperation(operation.operationId, operation.tags || [], server, endpoint, method.toUpperCase(), request, appName, operation.summary, operation.description, bodyType, contribs, undefined, undefined);
for (const contribName of contribs) {
this.contrib[contribName] ??= {};
this.contrib[contribName][appName] = this.operations[operation.operationId];
}
}
}
}
async getAuthenticatedUser() {
try {
const response = await this.operations["bananas.me:list"].call();
return response.ok ? response.json() : null;
}
catch {
return null;
}
}
findContrib(prefix) {
const results = [];
for (const [tag, operations] of Object.entries(this.contrib)) {
for (const [_, operation] of Object.entries(operations)) {
if (tag.startsWith(prefix) && operation.component !== undefined) {
results.push(operation);
}
}
}
return results.sort((a, b) => {
if (a.order !== undefined && b.order !== undefined) {
return a.order - b.order;
}
return 0;
});
}
}
//# sourceMappingURL=api.js.map