/* eslint-disable class-methods-use-this */ // eslint-disable-next-line max-classes-per-file import fetch, { Response } from "node-fetch"; import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "./apl"; import { createAPLDebug } from "./apl-debug"; const debug = createAPLDebug("UpstashAPL"); type SuccessResponse = { result: string }; type ErrorResponse = { error: string }; type UpstashResponse = SuccessResponse | ErrorResponse; export const UpstashAPLVariables = { UPSTASH_TOKEN: "UPSTASH_TOKEN", UPSTASH_URL: "UPSTASH_URL", }; export class UpstashAplMisconfiguredError extends Error { constructor(public missingVars: string[]) { super( `Configuration values for: ${missingVars .map((v) => `"${v}"`) .join(", ")} not found or is empty. Pass values to constructor of use env variables.` ); } } export class UpstashAplNotConfiguredError extends Error {} export type UpstashAPLConfig = { restURL: string; restToken: string; }; /** * Upstash APL * * Use [Upstash](https://upstash.com) which is SaaS Redis provider, to store auth data. * You can create free developer account and use it to develop multi-tenant apps. * * Configuration require 2 elements: URL to the REST service and auth token. Both can be * found in the Upstash dashboard. You can choose to store them using environment variables * (`UPSTASH_URL` and `UPSTASH_TOKEN`) or pass directly to APL's constructor. */ export class UpstashAPL implements APL { private restURL?: string; private restToken?: string; constructor(config?: UpstashAPLConfig) { const restURL = config?.restURL || process.env[UpstashAPLVariables.UPSTASH_URL]; const restToken = config?.restToken || process.env[UpstashAPLVariables.UPSTASH_TOKEN]; this.restURL = restURL; this.restToken = restToken; } private async upstashRequest(requestBody: string) { debug("Sending request to Upstash"); if (!this.restURL || !this.restToken) { throw new Error( "UpstashAPL is not configured. See https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md" ); } let response: Response; try { response = await fetch(this.restURL, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.restToken}` }, body: requestBody, }); } catch (error) { debug("Error during sending the data:", error); throw new Error(`UpstashAPL was unable to perform a request ${error}`); } if (response.status >= 400 || response.status < 200) { debug("Non 200 response code. Upstash responded with %j", response); throw new Error(`Upstash APL responded with the code ${response.status}`); } const parsedResponse = (await response.json()) as UpstashResponse; if ("error" in parsedResponse) { debug("Upstash API responded with error: %s", parsedResponse.error); throw new Error("Upstash APL was not able to perform operation"); } debug("Register service responded successfully"); return parsedResponse.result; } private async saveDataToUpstash(authData?: AuthData) { debug("saveDataToUpstash() called with: %j", { domain: authData?.domain, token: authData?.token.substring(0, 4), }); await this.upstashRequest(`["SET", "${authData?.domain}", "${authData?.token}"]`); } private async deleteDataFromUpstash(domain: string) { await this.upstashRequest(`["DEL", "${domain}"]`); } private async fetchDataFromUpstash(domain: string) { const result = await this.upstashRequest(`["GET", "${domain}"]`); if (result) { return { domain, token: result }; } return undefined; } async get(domain: string) { return this.fetchDataFromUpstash(domain); } async set(authData: AuthData) { await this.saveDataToUpstash(authData); } async delete(domain: string) { await this.deleteDataFromUpstash(domain); } async getAll() { throw new Error("UpstashAPL does not support getAll method"); return []; } // eslint-disable-next-line class-methods-use-this async isReady(): Promise { const missingConf: string[] = []; if (!this.restToken) { missingConf.push("restToken"); } if (!this.restURL) { missingConf.push("restURL"); } if (missingConf.length > 0) { return { ready: false, error: new UpstashAplMisconfiguredError(missingConf), }; } return { ready: true, }; } async isConfigured(): Promise { return this.restToken && this.restURL ? { configured: true, } : { configured: false, error: new UpstashAplNotConfiguredError( "UpstashAPL not configured. Check if REST URL and token provided in constructor or env" ), }; } }