parent
b4e0a053cb
commit
b9aa9008e7
4 changed files with 181 additions and 0 deletions
24
docs/apl.md
24
docs/apl.md
|
@ -184,3 +184,27 @@ const apl = new UpstashAPL({
|
||||||
```
|
```
|
||||||
|
|
||||||
Or using environment variables: `UPSTASH_TOKEN`, `UPSTASH_URL`.
|
Or using environment variables: `UPSTASH_TOKEN`, `UPSTASH_URL`.
|
||||||
|
|
||||||
|
### RestAPL
|
||||||
|
|
||||||
|
RestAPL is a simple CRUD client that performs basic operations on resource provided in constructor
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { RestAPL } from "@saleor/app-sdk/APL";
|
||||||
|
|
||||||
|
const apl = new RestAPL({
|
||||||
|
resourceUrl: "https://crudcrud.com/api/2c35cdbf2a2c48669b2feda4ab260e1c", // Required, should point to CRUD resource
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer XYZ", // Optional. Headers will be merged into request, allowing to pass auth.
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Target resource must accept "standard" CRUD operations:
|
||||||
|
|
||||||
|
- `GET /` - get all
|
||||||
|
- `GET /:domain` - get AuthData for given domain
|
||||||
|
- `POST /` - set AuthData (`{domain: string, token: string}`) from BODY
|
||||||
|
- `DELETE /:domain` - deletes AuthData for given domain
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from "./apl";
|
export * from "./apl";
|
||||||
export * from "./file-apl";
|
export * from "./file-apl";
|
||||||
|
export * from "./rest-apl";
|
||||||
export * from "./upstash-apl";
|
export * from "./upstash-apl";
|
||||||
export * from "./vercel-apl";
|
export * from "./vercel-apl";
|
||||||
|
|
145
src/APL/rest-apl.ts
Normal file
145
src/APL/rest-apl.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import { hasProp } from "../has-prop";
|
||||||
|
import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "./apl";
|
||||||
|
import { createAPLDebug } from "./apl-debug";
|
||||||
|
|
||||||
|
const debug = createAPLDebug("RestAPL");
|
||||||
|
|
||||||
|
export type RestAPLConfig = {
|
||||||
|
resourceUrl: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateResponseStatus = (response: Response) => {
|
||||||
|
if (response.status < 200 || response.status >= 400) {
|
||||||
|
debug("Response failed with status %s", response.status);
|
||||||
|
|
||||||
|
throw new Error(`Fetch returned with non 200 status code ${response.status}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO Add test
|
||||||
|
*/
|
||||||
|
export class RestAPL implements APL {
|
||||||
|
private readonly resourceUrl: string;
|
||||||
|
|
||||||
|
private headers?: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(config: RestAPLConfig) {
|
||||||
|
this.resourceUrl = config.resourceUrl;
|
||||||
|
this.headers = config.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUrlForDomain(domain: string) {
|
||||||
|
return `${this.resourceUrl}/${domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(domain: string): Promise<AuthData | undefined> {
|
||||||
|
debug("Will fetch data from RestAPL for domain %s", domain);
|
||||||
|
|
||||||
|
const response = await fetch(this.getUrlForDomain(domain), {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/json", ...this.headers },
|
||||||
|
}).catch((error) => {
|
||||||
|
debug("Failed to reach API call: %s", error?.message ?? "Unknown error");
|
||||||
|
throw new Error(`Attempt in fetch the data resulted with error: ${error}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
validateResponseStatus(response);
|
||||||
|
|
||||||
|
const parsedResponse = (await response.json().catch((e) => {
|
||||||
|
debug("Failed to parse response: %s", e?.message ?? "Unknown error");
|
||||||
|
})) as unknown;
|
||||||
|
|
||||||
|
if (hasProp(parsedResponse, "domain") && hasProp(parsedResponse, "token")) {
|
||||||
|
return { domain: parsedResponse.domain as string, token: parsedResponse.token as string };
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("Response had no domain and token.");
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(authData: AuthData) {
|
||||||
|
debug("Saving data to RestAPL for domain: %s", authData.domain);
|
||||||
|
|
||||||
|
const response = await fetch(this.resourceUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", ...this.headers },
|
||||||
|
body: JSON.stringify(authData),
|
||||||
|
}).catch((e) => {
|
||||||
|
debug("Failed to reach API call: %s", e?.message ?? "Unknown error");
|
||||||
|
|
||||||
|
throw new Error(`Error during saving the data: ${e}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
validateResponseStatus(response);
|
||||||
|
|
||||||
|
debug("Set command finished successfully for domain: %", authData.domain);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(domain: string) {
|
||||||
|
debug("Deleting data from Rest for domain: %s", domain);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.getUrlForDomain(domain), {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json", ...this.headers },
|
||||||
|
body: JSON.stringify({ domain }),
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(`Delete responded with ${response.status} code`);
|
||||||
|
} catch (error) {
|
||||||
|
debug("Error during deleting the data: %s", error);
|
||||||
|
|
||||||
|
throw new Error(`Error during saving the data: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
debug("Get all data from Rest");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.resourceUrl, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/json", ...this.headers },
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(`Get all responded with ${response.status} code`);
|
||||||
|
|
||||||
|
return ((await response.json()) as AuthData[]) || [];
|
||||||
|
} catch (error) {
|
||||||
|
debug("Error during getting all the data:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async isReady(): Promise<AplReadyResult> {
|
||||||
|
const configured = await this.isConfigured();
|
||||||
|
|
||||||
|
return configured
|
||||||
|
? {
|
||||||
|
ready: true,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
ready: false,
|
||||||
|
error: new Error("App is not configured"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async isConfigured(): Promise<AplConfiguredResult> {
|
||||||
|
if (!this.resourceUrl) {
|
||||||
|
return {
|
||||||
|
configured: false,
|
||||||
|
error: new Error("RestAPL required resourceUrl param"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
configured: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
11
src/has-prop.ts
Normal file
11
src/has-prop.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* Safely narrow unknown object and infer property existence
|
||||||
|
* @param obj
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
export function hasProp<K extends PropertyKey>(
|
||||||
|
obj: unknown,
|
||||||
|
key: K | null | undefined
|
||||||
|
): obj is Record<K, unknown> {
|
||||||
|
return key != null && obj != null && typeof obj === "object" && key in obj;
|
||||||
|
}
|
Loading…
Reference in a new issue