UNPKG

@saleor/app-sdk

Version:
330 lines (322 loc) 11 kB
import { OTEL_APL_SERVICE_NAME, getOtelTracer } from "../../chunk-UCTHLSSD.mjs"; import { createAPLDebug } from "../../chunk-ORQVZRNL.mjs"; import "../../chunk-CPDLIPGD.mjs"; // src/APL/saleor-cloud/saleor-cloud-apl.ts import { SpanKind, SpanStatusCode } from "@opentelemetry/api"; import { SemanticAttributes } from "@opentelemetry/semantic-conventions"; // src/has-prop.ts function hasProp(obj, key) { return key != null && obj != null && typeof obj === "object" && key in obj; } // src/APL/has-auth-data.ts var hasAuthData = (data) => hasProp(data, "token") && data.token && hasProp(data, "appId") && data.appId && hasProp(data, "saleorApiUrl") && data.saleorApiUrl; // src/APL/auth-data-from-object.ts var debug = createAPLDebug("authDataFromObject"); var authDataFromObject = (parsed) => { if (!hasAuthData(parsed)) { debug("Given object did not contained AuthData"); return void 0; } const { saleorApiUrl, appId, token, jwks } = parsed; return { saleorApiUrl, appId, token, jwks }; }; // src/APL/saleor-cloud/paginator.ts var debug2 = createAPLDebug("Paginator"); var Paginator = class { constructor(url, fetchOptions, fetchFn = fetch) { this.url = url; this.fetchOptions = fetchOptions; this.fetchFn = fetchFn; } async fetchAll() { debug2("Fetching all pages for url", this.url); const response = await this.fetchFn(this.url, this.fetchOptions); debug2("%0", response); const json = await response.json(); if (json.next) { const remainingPages = await this.fetchNext(json.next); const allResults = [...json.results, ...remainingPages.flatMap((page) => page.results)]; debug2("Fetched all pages, total length: %d", allResults.length); return { next: null, previous: null, count: allResults.length, results: allResults }; } debug2("No more pages to fetch, returning first page"); return json; } async fetchNext(nextUrl) { debug2("Fetching next page with url %s", nextUrl); const response = await this.fetchFn(nextUrl, this.fetchOptions); debug2("%0", response); const json = await response.json(); if (json.next) { return [json, ...await this.fetchNext(json.next)]; } return [json]; } }; // src/APL/saleor-cloud/saleor-cloud-apl-errors.ts var SaleorCloudAplError = class extends Error { constructor(code, message) { super(message); this.code = code; this.name = "SaleorCloudAplError"; } }; var CloudAplError = { FAILED_TO_REACH_API: "FAILED_TO_REACH_API", RESPONSE_BODY_INVALID: "RESPONSE_BODY_INVALID", RESPONSE_NON_200: "RESPONSE_NON_200", ERROR_SAVING_DATA: "ERROR_SAVING_DATA", ERROR_DELETING_DATA: "ERROR_DELETING_DATA" }; // src/APL/saleor-cloud/saleor-cloud-apl.ts var debug3 = createAPLDebug("SaleorCloudAPL"); var validateResponseStatus = (response) => { if (!response.ok) { debug3("Response failed with status %s", response.status); debug3("%O", response); throw new SaleorCloudAplError( CloudAplError.RESPONSE_NON_200, `Fetch returned with non 200 status code ${response.status}` ); } }; var mapAuthDataToAPIBody = (authData) => ({ saleor_app_id: authData.appId, saleor_api_url: authData.saleorApiUrl, jwks: authData.jwks, token: authData.token, domain: new URL(authData.saleorApiUrl).hostname }); var mapAPIResponseToAuthData = (response) => ({ appId: response.saleor_app_id, jwks: response.jwks, saleorApiUrl: response.saleor_api_url, token: response.token }); var extractErrorMessage = (error) => { if (typeof error === "string") { return error; } if (hasProp(error, "message")) { return error.message; } return "Unknown error"; }; var SaleorCloudAPL = class { constructor(config) { this.resourceUrl = config.resourceUrl; this.headers = { Authorization: `Bearer ${config.token}` }; this.tracer = getOtelTracer(); this.cacheManager = config?.experimental?.cacheManager; this.pageLimit = config.pageLimit ?? 1e3; } getUrlForDomain(saleorApiUrl) { return `${this.resourceUrl}/${Buffer.from(saleorApiUrl).toString("base64url")}`; } getUrlWithLimit() { return `${this.resourceUrl}?limit=${this.pageLimit}`; } setToCacheIfExists(saleorApiUrl, authData) { if (!this.cacheManager) { return; } this.cacheManager.set(authData.saleorApiUrl, authData); } deleteFromCacheIfExists(saleorApiUrl) { if (!this.cacheManager) { return; } this.cacheManager.delete(saleorApiUrl); } getFromCacheIfExists(saleorApiUrl) { return this.cacheManager?.get(saleorApiUrl); } async get(saleorApiUrl) { const cachedData = this.getFromCacheIfExists(saleorApiUrl); if (cachedData) { debug3("Returning authData from cache for saleorApiUrl %s", saleorApiUrl); return cachedData; } debug3("Will fetch data from SaleorCloudAPL for saleorApiUrl %s", saleorApiUrl); return this.tracer.startActiveSpan( "SaleorCloudAPL.get", { attributes: { saleorApiUrl, [SemanticAttributes.PEER_SERVICE]: OTEL_APL_SERVICE_NAME }, kind: SpanKind.CLIENT }, async (span) => { const response = await fetch(this.getUrlForDomain(saleorApiUrl), { method: "GET", headers: { "Content-Type": "application/json", ...this.headers } }).catch((error) => { debug3("Failed to reach API call: %s", extractErrorMessage(error)); debug3("%O", error); span.recordException(CloudAplError.FAILED_TO_REACH_API); span.setStatus({ code: SpanStatusCode.ERROR, message: extractErrorMessage(error) }).end(); throw new SaleorCloudAplError( CloudAplError.FAILED_TO_REACH_API, `${extractErrorMessage(error)}` ); }); if (!response) { debug3("No response from the API"); span.recordException(CloudAplError.FAILED_TO_REACH_API); span.setStatus({ code: SpanStatusCode.ERROR, message: "Response couldn't be resolved" }).end(); throw new SaleorCloudAplError( CloudAplError.FAILED_TO_REACH_API, "Response couldn't be resolved" ); } if (response.status >= 500) { const message = `Api responded with ${response.status}`; span.recordException(CloudAplError.FAILED_TO_REACH_API); span.setStatus({ code: SpanStatusCode.ERROR, message }).end(); throw new SaleorCloudAplError(CloudAplError.FAILED_TO_REACH_API, message); } if (response.status === 404) { debug3("No auth data for given saleorApiUrl"); span.addEvent("Missing auth data for given saleorApiUrl"); span.setStatus({ code: SpanStatusCode.OK }).end(); return void 0; } const parsedResponse = await response.json().catch((e) => { debug3("Failed to parse response: %s", extractErrorMessage(e)); debug3("%O", e); const message = `Cant parse response body: ${extractErrorMessage(e)}`; span.recordException(CloudAplError.RESPONSE_BODY_INVALID); span.setStatus({ code: SpanStatusCode.ERROR, message }).end(); throw new SaleorCloudAplError(CloudAplError.RESPONSE_BODY_INVALID, message); }); const authData = authDataFromObject(mapAPIResponseToAuthData(parsedResponse)); if (!authData) { debug3("No auth data for given saleorApiUrl"); span.addEvent("Missing auth data for given saleorApiUrl"); span.setStatus({ code: SpanStatusCode.OK }).end(); return void 0; } span.setAttribute("appId", authData.appId); this.setToCacheIfExists(authData.saleorApiUrl, authData); span.setStatus({ code: SpanStatusCode.OK }).end(); return authData; } ); } async set(authData) { debug3("Saving data to SaleorCloudAPL for saleorApiUrl: %s", authData.saleorApiUrl); return this.tracer.startActiveSpan( "SaleorCloudAPL.set", { attributes: { saleorApiUrl: authData.saleorApiUrl, appId: authData.appId, [SemanticAttributes.PEER_SERVICE]: OTEL_APL_SERVICE_NAME }, kind: SpanKind.CLIENT }, async (span) => { const response = await fetch(this.resourceUrl, { method: "POST", headers: { "Content-Type": "application/json", ...this.headers }, body: JSON.stringify(mapAuthDataToAPIBody(authData)) }).catch((e) => { debug3("Failed to reach API call: %s", extractErrorMessage(e)); debug3("%O", e); span.recordException(`Failed to reach API call: ${extractErrorMessage(e)}`); span.setStatus({ code: SpanStatusCode.ERROR }).end(); throw new SaleorCloudAplError( CloudAplError.ERROR_SAVING_DATA, `Error during saving the data: ${extractErrorMessage(e)}` ); }); validateResponseStatus(response); debug3("Set command finished successfully for saleorApiUrl: %", authData.saleorApiUrl); this.setToCacheIfExists(authData.saleorApiUrl, authData); span.setStatus({ code: SpanStatusCode.OK }); span.end(); return void 0; } ); } async delete(saleorApiUrl) { debug3("Deleting data from SaleorCloud for saleorApiUrl: %s", saleorApiUrl); try { const response = await fetch(this.getUrlForDomain(saleorApiUrl), { method: "DELETE", headers: { "Content-Type": "application/json", ...this.headers } }); this.deleteFromCacheIfExists(saleorApiUrl); debug3(`Delete responded with ${response.status} code`); } catch (error) { const errorMessage = extractErrorMessage(error); debug3("Error during deleting the data: %s", errorMessage); debug3("%O", error); throw new SaleorCloudAplError( CloudAplError.ERROR_DELETING_DATA, `Error during deleting the data: ${errorMessage}` ); } } async getAll() { debug3("Get all data from SaleorCloud"); try { const paginator = new Paginator(this.getUrlWithLimit(), { method: "GET", headers: { "Content-Type": "application/json", ...this.headers } }); const responses = await paginator.fetchAll(); return responses.results.map(mapAPIResponseToAuthData); } catch (error) { const errorMessage = extractErrorMessage(error); debug3("Error during getting all the data:", errorMessage); debug3("%O", error); } return []; } }; export { CloudAplError, SaleorCloudAPL, SaleorCloudAplError };