Improve webhook client api and make script to register webhooks
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
import { Printful, PrintfulClient } from "@blade-and-brawn/commerce";
|
||||
import { env } from "../env";
|
||||
|
||||
const printful = new PrintfulClient({
|
||||
token: env.PRINTFUL_AUTH,
|
||||
storeId: env.PRINTFUL_STORE_ID,
|
||||
});
|
||||
|
||||
await printful.Webhooks.register("https://dev.bladeandbrawn.com/webhook/printful", [
|
||||
Printful.Webhook.Event.ProductUpdated,
|
||||
Printful.Webhook.Event.ProductDeleted,
|
||||
Printful.Webhook.Event.PackageShipped,
|
||||
]);
|
||||
|
||||
const config = await printful.Webhooks.getConfig();
|
||||
console.log(config);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Printful, PrintfulClient, PrintfulError, Webflow, WebflowClient, WebflowError } from "@blade-and-brawn/commerce";
|
||||
import { env } from "../env";
|
||||
|
||||
const BASE_URL = "https://dev.api.bladeandbrawn.com";
|
||||
|
||||
const PRINTFUL_WEBHOOK_URL = `${BASE_URL}/webhook/printful`;
|
||||
const WEBFLOW_WEBHOOK_URL = `${BASE_URL}/webhook/webflow`;
|
||||
|
||||
function printError(err: unknown): never {
|
||||
if (err instanceof WebflowError || err instanceof PrintfulError)
|
||||
console.error(`${err.message}\n`, JSON.stringify(err.payload, null, 2));
|
||||
else
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Register printful webhook
|
||||
try {
|
||||
console.log("Registering printful webhook...");
|
||||
const printful = new PrintfulClient({
|
||||
token: env.PRINTFUL_AUTH,
|
||||
storeId: env.PRINTFUL_STORE_ID,
|
||||
});
|
||||
|
||||
await printful.Webhooks.create(PRINTFUL_WEBHOOK_URL, [
|
||||
Printful.Webhook.Event.ProductUpdated,
|
||||
Printful.Webhook.Event.ProductDeleted,
|
||||
Printful.Webhook.Event.PackageShipped,
|
||||
]);
|
||||
|
||||
const webhook = await printful.Webhooks.get();
|
||||
console.log("Finished: ");
|
||||
console.log(webhook);
|
||||
} catch (err) {
|
||||
printError(err);
|
||||
}
|
||||
|
||||
// Register webflow webhooks
|
||||
try {
|
||||
console.log("Registering webflow webhooks...");
|
||||
const webflow = new WebflowClient({
|
||||
siteId: env.WEBFLOW_SITE_ID,
|
||||
collectionsId: env.WEBFLOW_COLLECTION_ID,
|
||||
token: env.WEBFLOW_AUTH,
|
||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
const events: Webflow.Webhook.Event[] = [
|
||||
Webflow.Webhook.Event.OrderCreated,
|
||||
];
|
||||
|
||||
const webhooksToDelete = await webflow.Webhooks.list({ forceAll: true });
|
||||
for (const webhook of webhooksToDelete)
|
||||
await webflow.Webhooks.remove(webhook.id);
|
||||
|
||||
for (const event of events)
|
||||
await webflow.Webhooks.create(WEBFLOW_WEBHOOK_URL, event);
|
||||
|
||||
const webhooks = await webflow.Webhooks.list({ forceAll: true });
|
||||
console.log("Finished: ");
|
||||
console.log(webhooks);
|
||||
} catch (err) {
|
||||
printError(err);
|
||||
}
|
||||
@@ -72,6 +72,7 @@ export const app = new Elysia()
|
||||
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
||||
// development
|
||||
"http://localhost:5173",
|
||||
"http://dev.portal.bladeandbrawn.com/"
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ class ProductsClient {
|
||||
method: "GET",
|
||||
headers: this.creds.authHeaders,
|
||||
});
|
||||
if (!res.ok) throw new PrintfulError("Failed to get all Printful products", res.status, await parsePayload(res));
|
||||
if (!res.ok) throw new PrintfulError("Failed to list Printful products", res.status, await parsePayload(res));
|
||||
const payload = (await res.json()) as Printful.MetaDataMulti<Printful.Products.SyncProduct>;
|
||||
allSyncProducts.push(...payload.result);
|
||||
offset = allSyncProducts.length;
|
||||
@@ -111,7 +111,7 @@ class OrdersClient {
|
||||
method: "GET",
|
||||
headers: this.creds.authHeaders,
|
||||
});
|
||||
if (!res.ok) throw new PrintfulError("Failed to get all Printful orders", res.status, await parsePayload(res));
|
||||
if (!res.ok) throw new PrintfulError("Failed to list Printful orders", res.status, await parsePayload(res));
|
||||
const payload = (await res.json()) as Printful.MetaDataMulti<Printful.Orders.Order>;
|
||||
allPrintfulOrders.push(...payload.result);
|
||||
offset = allPrintfulOrders.length;
|
||||
@@ -124,7 +124,7 @@ class OrdersClient {
|
||||
class WebhooksClient {
|
||||
constructor(private creds: Credentials) { }
|
||||
|
||||
async getConfig(): Promise<Printful.Webhook.Config | undefined> {
|
||||
async get(): Promise<Printful.Webhook.Config | undefined> {
|
||||
const res = await fetch(`${this.creds.apiUrl}/webhooks`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -138,7 +138,11 @@ class WebhooksClient {
|
||||
return payload;
|
||||
}
|
||||
|
||||
async register(url: string, events: Printful.Webhook.Event[]) {
|
||||
|
||||
/**
|
||||
* Printful only supports one URL at a time, so subsequent calls will overwrite the previous webhook
|
||||
*/
|
||||
async create(url: string, events: Printful.Webhook.Event[]) {
|
||||
const res = await fetch(`${this.creds.apiUrl}/webhooks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -197,7 +197,7 @@ export namespace Webflow {
|
||||
export type MetaDataMulti<K extends string, T = any> = {
|
||||
[P in K]: T[];
|
||||
} & {
|
||||
pagination: {
|
||||
pagination?: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
@@ -442,6 +442,16 @@ export namespace Webflow {
|
||||
OrderUpdated = "ecomm_order_changed",
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
id: string,
|
||||
triggerType: Event,
|
||||
url: string,
|
||||
workspaceId: string,
|
||||
siteId: string,
|
||||
lastTriggered: string,
|
||||
createdOn: string
|
||||
}
|
||||
|
||||
export interface OrderCreated extends MetaData<
|
||||
Event.OrderCreated,
|
||||
Orders.Order
|
||||
|
||||
@@ -13,6 +13,7 @@ type ClientOptions = {
|
||||
};
|
||||
|
||||
type Credentials = {
|
||||
apiBaseUrl: string;
|
||||
apiSitesUrl: string;
|
||||
apiCollectionsUrl: string;
|
||||
authHeader: Record<string, string>;
|
||||
@@ -103,11 +104,11 @@ class ProductsClient {
|
||||
method: "GET",
|
||||
headers: this.creds.authHeader,
|
||||
});
|
||||
if (!res.ok) throw new WebflowError("Failed to get all Webflow products", res.status, await parsePayload(res));
|
||||
if (!res.ok) throw new WebflowError("Failed to list Webflow products", res.status, await parsePayload(res));
|
||||
const payload = (await res.json()) as Webflow.MetaDataMulti<"items", Webflow.Products.ProductAndSkus>;
|
||||
allWebflowProducts.push(...payload.items);
|
||||
offset = allWebflowProducts.length;
|
||||
if (allWebflowProducts.length >= payload.pagination.total) break;
|
||||
if (allWebflowProducts.length >= (payload?.pagination?.total ?? 0)) break;
|
||||
} while (opt.forceAll);
|
||||
return allWebflowProducts;
|
||||
}
|
||||
@@ -168,11 +169,11 @@ class OrdersClient {
|
||||
method: "GET",
|
||||
headers: this.creds.authHeader,
|
||||
});
|
||||
if (!res.ok) throw new WebflowError("Failed to get all Webflow orders", res.status, await parsePayload(res));
|
||||
if (!res.ok) throw new WebflowError("Failed to list Webflow orders", res.status, await parsePayload(res));
|
||||
const payload = (await res.json()) as Webflow.MetaDataMulti<"orders", Webflow.Orders.Order>;
|
||||
allOrders.push(...payload.orders);
|
||||
offset = allOrders.length;
|
||||
if (allOrders.length >= payload.pagination.total) break;
|
||||
if (allOrders.length >= (payload?.pagination?.total ?? 0)) break;
|
||||
} while (opt.forceAll);
|
||||
return allOrders;
|
||||
}
|
||||
@@ -213,6 +214,60 @@ class OrdersClient {
|
||||
}
|
||||
}
|
||||
|
||||
class WebhooksClient {
|
||||
constructor(private creds: Credentials) { }
|
||||
|
||||
async list(opt: PagingOptions = {}): Promise<Webflow.Webhook.Config[]> {
|
||||
const allWebhooks: Webflow.Webhook.Config[] = [];
|
||||
let offset = opt.offset ?? 0;
|
||||
do {
|
||||
const res = await fetch(`${this.creds.apiSitesUrl}/webhooks?offset=${offset}&limit=${opt.limit ?? 100}`, {
|
||||
method: "GET",
|
||||
headers: this.creds.authHeader,
|
||||
});
|
||||
if (!res.ok) throw new WebflowError("Failed to list Webflow webhooks", res.status, await parsePayload(res));
|
||||
const payload = (await res.json()) as Webflow.MetaDataMulti<"webhooks", Webflow.Webhook.Config>;
|
||||
allWebhooks.push(...payload.webhooks);
|
||||
offset = allWebhooks.length;
|
||||
if (allWebhooks.length >= (payload?.pagination?.total ?? 0)) break;
|
||||
} while (opt.forceAll);
|
||||
return allWebhooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit of 75 registrations per event
|
||||
*/
|
||||
async create(url: string, event: Webflow.Webhook.Event) {
|
||||
const res = await fetch(`${this.creds.apiSitesUrl}/webhooks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...this.creds.authHeader,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"url": url,
|
||||
"triggerType": event
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new WebflowError("Failed to create Webflow webhook", res.status, await parsePayload(res));
|
||||
}
|
||||
|
||||
async remove(webhookId: string): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${this.creds.apiBaseUrl}/webhooks/${webhookId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...this.creds.authHeader,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
if (!res.ok) throw new WebflowError("Failed to remove Webflow webhook", res.status, await parsePayload(res));
|
||||
}
|
||||
}
|
||||
|
||||
class UtilClient {
|
||||
constructor(private webhookSecret: string) { }
|
||||
|
||||
@@ -248,16 +303,19 @@ class UtilClient {
|
||||
export class WebflowClient {
|
||||
readonly Products: ProductsClient;
|
||||
readonly Orders: OrdersClient;
|
||||
readonly Webhooks: WebhooksClient;
|
||||
readonly Util: UtilClient;
|
||||
|
||||
constructor(options: ClientOptions) {
|
||||
const creds: Credentials = {
|
||||
apiBaseUrl: `https://api.webflow.com/v2`,
|
||||
apiSitesUrl: `https://api.webflow.com/v2/sites/${options.siteId}`,
|
||||
apiCollectionsUrl: `https://api.webflow.com/v2/collections/${options.collectionsId}`,
|
||||
authHeader: { Authorization: `bearer ${options.token}` },
|
||||
};
|
||||
this.Products = new ProductsClient(creds);
|
||||
this.Orders = new OrdersClient(creds);
|
||||
this.Webhooks = new WebhooksClient(creds);
|
||||
this.Util = new UtilClient(options.webhookSecret);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user