@aep_dev/aep-lib-ts
Version:
Utility libraries for AEP TypeScript-based tools including case conversion, OpenAPI utilities, and API clients
311 lines • 13.3 kB
JavaScript
import { pascalCaseToKebabCase } from "../cases/cases.js";
import { logger } from "../utils/logger.js";
export class APIClient {
api;
constructor(api) {
this.api = api;
}
static async fromOpenAPI(openAPI, serverURL = "", pathPrefix = "") {
if (!openAPI.openapi && !openAPI.openapi) {
throw new Error("Unable to detect OAS openapi. Please add an openapi field or a openapi field");
}
logger.info(`reading openapi file: ${openAPI.openapi}`);
const resourceBySingular = {};
const customMethodsByPattern = {};
// Parse paths to find possible resources
for (const [path, pathItem] of Object.entries(openAPI.paths)) {
const trimmedPath = path.slice(pathPrefix.length);
const patternInfo = getPatternInfo(trimmedPath);
if (!patternInfo) {
continue;
}
let schemaRef = null;
const r = {};
if (patternInfo.customMethodName && patternInfo.isResourcePattern) {
const pattern = trimmedPath.split(":")[0].slice(1);
if (!customMethodsByPattern[pattern]) {
customMethodsByPattern[pattern] = [];
}
if (pathItem.post) {
const response = pathItem.post.responses["200"];
if (response) {
const schema = getSchemaFromResponse(response, openAPI);
const responseSchema = schema
? await dereferenceSchema(schema, openAPI)
: null;
if (!pathItem.post.requestBody) {
throw new Error(`Custom method ${patternInfo.customMethodName} has a POST response but no request body`);
}
const requestSchema = await dereferenceSchema(getSchemaFromRequestBody(pathItem.post.requestBody, openAPI), openAPI);
customMethodsByPattern[pattern].push({
name: patternInfo.customMethodName,
method: "POST",
request: requestSchema,
response: responseSchema,
});
}
}
if (pathItem.get) {
const response = pathItem.get.responses["200"];
if (response) {
const schema = getSchemaFromResponse(response, openAPI);
const responseSchema = schema
? await dereferenceSchema(schema, openAPI)
: null;
customMethodsByPattern[pattern].push({
name: patternInfo.customMethodName,
method: "GET",
request: null,
response: responseSchema,
});
}
}
}
else if (patternInfo.isResourcePattern) {
if (pathItem.delete) {
r.deleteMethod = {};
}
if (pathItem.get) {
const response = pathItem.get.responses["200"];
if (response) {
schemaRef = getSchemaFromResponse(response, openAPI);
r.getMethod = {};
}
}
if (pathItem.patch) {
const response = pathItem.patch.responses["200"];
if (response) {
schemaRef = getSchemaFromResponse(response, openAPI);
r.updateMethod = {};
}
}
}
else {
if (pathItem.post) {
const response = pathItem.post.responses["200"];
if (response) {
schemaRef = getSchemaFromResponse(response, openAPI);
const supportsUserSettableCreate = pathItem.post.parameters?.some((param) => param.name === "id") ??
false;
r.createMethod = { supportsUserSettableCreate };
}
}
if (pathItem.get) {
const response = pathItem.get.responses["200"];
if (response) {
const respSchema = getSchemaFromResponse(response, openAPI);
if (!respSchema) {
console.warn(`Resource ${path} has a LIST method with a response schema, but the response schema is null.`);
}
else {
const resolvedSchema = await dereferenceSchema(respSchema, openAPI);
const arrayProperty = Object.entries(resolvedSchema.properties || {}).find(([_, prop]) => prop.type === "array");
if (arrayProperty) {
schemaRef = arrayProperty[1].items || null;
r.listMethod = {
hasUnreachableResources: false,
supportsFilter: false,
supportsSkip: false,
};
for (const param of pathItem.get.parameters || []) {
if (param.name === "skip") {
r.listMethod.supportsSkip = true;
}
else if (param.name === "unreachable") {
r.listMethod.hasUnreachableResources = true;
}
else if (param.name === "filter") {
r.listMethod.supportsFilter = true;
}
}
}
else {
console.warn(`Resource ${path} has a LIST method with a response schema, but the items field is not present or is not an array.`);
}
}
}
}
}
if (schemaRef) {
const parts = schemaRef.$ref?.split("/") || [];
const key = parts[parts.length - 1];
const singular = pascalCaseToKebabCase(key);
const pattern = trimmedPath.split("/").slice(1);
if (!patternInfo.isResourcePattern) {
let finalSingular = singular;
if (pattern.length >= 3) {
const parent = pattern[pattern.length - 3].slice(1, -1); // Remove curly braces
if (singular.startsWith(parent)) {
finalSingular = singular.slice(parent.length + 1);
}
}
pattern.push(`{${finalSingular}}`);
}
const dereferencedSchema = await dereferenceSchema(schemaRef, openAPI);
const resource = await getOrPopulateResource(singular, pattern, dereferencedSchema, resourceBySingular, openAPI);
if (r.getMethod)
resource.getMethod = r.getMethod;
if (r.listMethod)
resource.listMethod = r.listMethod;
if (r.createMethod)
resource.createMethod = r.createMethod;
if (r.updateMethod)
resource.updateMethod = r.updateMethod;
if (r.deleteMethod)
resource.deleteMethod = r.deleteMethod;
}
}
// Map custom methods to resources
for (const [pattern, customMethods] of Object.entries(customMethodsByPattern)) {
const resource = Object.values(resourceBySingular).find((r) => r.patternElems.join("/") === pattern);
if (resource) {
resource.customMethods = customMethods;
}
}
if (!serverURL) {
serverURL = openAPI.servers[0]?.url + pathPrefix;
}
if (serverURL == "" || serverURL == "undefined") {
throw new Error("No server URL found in openapi, and none was provided");
}
// Add non-resource schemas to API's schemas
const schemas = {};
for (const [key, schema] of Object.entries(openAPI.components.schemas)) {
if (!resourceBySingular[key]) {
schemas[key] = schema;
}
}
return new APIClient({
serverURL,
name: openAPI.info.title,
contact: getContact(openAPI.info.contact),
resources: resourceBySingular,
schemas,
});
}
resources() {
return this.api.resources;
}
getResource(resource) {
const r = this.api.resources[resource];
if (!r) {
throw new Error(`Resource "${resource}" not found`);
}
return r;
}
serverUrl() {
return this.api.serverURL;
}
}
function getPatternInfo(path) {
let customMethodName = "";
if (path.includes(":")) {
const parts = path.split(":");
path = parts[0];
customMethodName = parts[1];
}
const pattern = path.split("/").slice(1);
for (let i = 0; i < pattern.length; i++) {
const segment = pattern[i];
const wrapped = segment.startsWith("{") && segment.endsWith("}");
const wantWrapped = i % 2 === 1;
if (wrapped !== wantWrapped) {
return null;
}
}
return {
isResourcePattern: pattern.length % 2 === 0,
customMethodName,
};
}
async function getOrPopulateResource(singular, pattern, schema, resourceBySingular, openAPI) {
if (resourceBySingular[singular]) {
return resourceBySingular[singular];
}
let resource;
if (schema["x-aep-resource"]) {
resource = {
singular: schema["x-aep-resource"].singular,
plural: schema["x-aep-resource"].plural,
// Parents will be set later on.
parents: [],
children: [],
patternElems: schema["x-aep-resource"].patterns[0].split("/").filter(Boolean),
schema,
customMethods: [],
};
}
else {
resource = {
singular,
plural: "",
parents: [],
children: [],
patternElems: pattern,
schema,
customMethods: [],
};
}
if (schema) {
if (schema["x-aep-resource"] && schema["x-aep-resource"].parents) {
for (const parentSingular of schema["x-aep-resource"].parents) {
const parentSchema = openAPI.components.schemas[parentSingular];
if (!parentSchema) {
throw new Error(`Resource "${singular}" parent "${parentSingular}" not found`);
}
const parentResource = await getOrPopulateResource(parentSingular, [], parentSchema, resourceBySingular, openAPI);
resource.parents.push(parentResource);
parentResource.children.push(resource);
}
}
}
resourceBySingular[singular] = resource;
return resource;
}
function getContact(contact) {
if (!contact || (!contact.name && !contact.email && !contact.url)) {
return null;
}
return contact;
}
function getSchemaFromResponse(response, openAPI) {
if (openAPI.openapi === "2.0") {
return response.schema || null;
}
return response.content?.["application/json"]?.schema || null;
}
function getSchemaFromRequestBody(requestBody, openAPI) {
if (openAPI.openapi === "2.0") {
return requestBody.schema;
}
return requestBody.content["application/json"].schema;
}
async function dereferenceSchema(schema, openAPI) {
if (!schema.$ref) {
return schema;
}
if (schema.$ref.startsWith("http://") || schema.$ref.startsWith("https://")) {
logger.debug(`Fetching external schema from ${schema.$ref}...`);
const response = await fetch(schema.$ref);
if (!response.ok) {
throw new Error(`Failed to fetch external schema: ${schema.$ref}`);
}
const externalSchema = await response.json();
logger.debug(`Final schema fetched from ${schema.$ref}: ${JSON.stringify(externalSchema)}`);
return dereferenceSchema(externalSchema, openAPI);
}
const parts = schema.$ref.split("/");
const key = parts[parts.length - 1];
let childSchema;
if (openAPI.openapi === "2.0") {
childSchema = openAPI.definitions[key];
}
else {
childSchema = openAPI.components.schemas[key];
}
if (!childSchema) {
throw new Error(`Schema "${schema.$ref}" not found`);
}
return dereferenceSchema(childSchema, openAPI);
}
//# sourceMappingURL=api.js.map