UNPKG

@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
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