diff --git a/.changeset/four-carpets-watch.md b/.changeset/four-carpets-watch.md new file mode 100644 index 00000000..487fccd0 --- /dev/null +++ b/.changeset/four-carpets-watch.md @@ -0,0 +1,5 @@ +--- +"@saleor/app-sdk": minor +--- + +Saleor Cloud APL will now use built-in cache by default. diff --git a/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts b/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts index e26e0b7e..6bf25ac8 100644 --- a/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts +++ b/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts @@ -4,3 +4,11 @@ export class SaleorCloudAplError extends Error { this.name = "SaleorCloudAplError"; } } + +export const 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", +}; diff --git a/src/APL/saleor-cloud/saleor-cloud-apl.test.ts b/src/APL/saleor-cloud/saleor-cloud-apl.test.ts index d97f5d09..198ea2e3 100644 --- a/src/APL/saleor-cloud/saleor-cloud-apl.test.ts +++ b/src/APL/saleor-cloud/saleor-cloud-apl.test.ts @@ -161,6 +161,37 @@ describe("APL", () => { expect(await apl.get("http://unknown-domain.example.com/graphql/")).toBe(undefined); }); + + it("Uses cache when GET call is called 2nd time", async () => { + fetchMock.mockResolvedValue({ + status: 200, + ok: true, + json: async () => ({ + saleor_app_id: stubAuthData.appId, + saleor_api_url: stubAuthData.saleorApiUrl, + jwks: stubAuthData.jwks, + domain: stubAuthData.domain, + token: stubAuthData.token, + }), + }); + + const apl = new SaleorCloudAPL(aplConfig); + + expect(await apl.get(stubAuthData.saleorApiUrl)).toStrictEqual(stubAuthData); + expect(await apl.get(stubAuthData.saleorApiUrl)).toStrictEqual(stubAuthData); + + expect(fetchMock).toBeCalledTimes(1); + expect(fetchMock).toBeCalledWith( + "https://example.com/aHR0cHM6Ly9leGFtcGxlLmNvbS9ncmFwaHFsLw", // base64 encoded api url + { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer token", + }, + method: "GET", + } + ); + }); }); }); diff --git a/src/APL/saleor-cloud/saleor-cloud-apl.ts b/src/APL/saleor-cloud/saleor-cloud-apl.ts index efecdcde..8330be81 100644 --- a/src/APL/saleor-cloud/saleor-cloud-apl.ts +++ b/src/APL/saleor-cloud/saleor-cloud-apl.ts @@ -6,13 +6,14 @@ import { getOtelTracer, OTEL_APL_SERVICE_NAME } from "../../open-telemetry"; import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "../apl"; import { createAPLDebug } from "../apl-debug"; import { authDataFromObject } from "../auth-data-from-object"; -import { SaleorCloudAplError } from "./saleor-cloud-apl-errors"; +import { CloudAplError, SaleorCloudAplError } from "./saleor-cloud-apl-errors"; const debug = createAPLDebug("SaleorCloudAPL"); export type SaleorCloudAPLConfig = { resourceUrl: string; token: string; + cacheManager?: Map; }; type CloudAPLAuthDataShape = { @@ -28,14 +29,6 @@ export type GetAllAplResponseShape = { results: CloudAPLAuthDataShape[]; }; -export const 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", -}; - const validateResponseStatus = (response: Response) => { if (!response.ok) { debug("Response failed with status %s", response.status); @@ -92,6 +85,8 @@ export class SaleorCloudAPL implements APL { private tracer: Tracer; + private cacheManager: Map; + constructor(config: SaleorCloudAPLConfig) { this.resourceUrl = config.resourceUrl; this.headers = { @@ -99,6 +94,7 @@ export class SaleorCloudAPL implements APL { }; this.tracer = getOtelTracer(); + this.cacheManager = config.cacheManager ?? new Map(); } private getUrlForDomain(saleorApiUrl: string) { @@ -107,6 +103,13 @@ export class SaleorCloudAPL implements APL { } async get(saleorApiUrl: string): Promise { + const cachedData = this.cacheManager.get(saleorApiUrl); + + if (cachedData) { + debug("Returning authData from cache for saleorApiUrl %s", saleorApiUrl) + return cachedData; + } + debug("Will fetch data from SaleorCloudAPL for saleorApiUrl %s", saleorApiUrl); return this.tracer.startActiveSpan( @@ -224,6 +227,8 @@ export class SaleorCloudAPL implements APL { span.setAttribute("appId", authData.appId); + this.cacheManager.set(saleorApiUrl, authData); + span.end(); return authData; @@ -270,6 +275,8 @@ export class SaleorCloudAPL implements APL { debug("Set command finished successfully for saleorApiUrl: %", authData.saleorApiUrl); + this.cacheManager.set(authData.saleorApiUrl, authData); + span.setStatus({ code: SpanStatusCode.OK, }); @@ -289,6 +296,8 @@ export class SaleorCloudAPL implements APL { headers: { "Content-Type": "application/json", ...this.headers }, }); + this.cacheManager.delete(saleorApiUrl); + debug(`Delete responded with ${response.status} code`); } catch (error) { const errorMessage = extractErrorMessage(error);