Improve webhook client api and make script to register webhooks

This commit is contained in:
Dominic Ferrando
2026-06-26 14:53:56 -04:00
parent 62b651c476
commit b3e7d9a251
6 changed files with 146 additions and 25 deletions
-16
View File
@@ -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);
+64
View File
@@ -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);
}
+1
View File
@@ -72,6 +72,7 @@ export const app = new Elysia()
/^https?:\/\/blade-and-brawn\.fly\.dev$/i, /^https?:\/\/blade-and-brawn\.fly\.dev$/i,
// development // development
"http://localhost:5173", "http://localhost:5173",
"http://dev.portal.bladeandbrawn.com/"
], ],
}), }),
) )
+8 -4
View File
@@ -55,7 +55,7 @@ class ProductsClient {
method: "GET", method: "GET",
headers: this.creds.authHeaders, 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>; const payload = (await res.json()) as Printful.MetaDataMulti<Printful.Products.SyncProduct>;
allSyncProducts.push(...payload.result); allSyncProducts.push(...payload.result);
offset = allSyncProducts.length; offset = allSyncProducts.length;
@@ -111,7 +111,7 @@ class OrdersClient {
method: "GET", method: "GET",
headers: this.creds.authHeaders, 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>; const payload = (await res.json()) as Printful.MetaDataMulti<Printful.Orders.Order>;
allPrintfulOrders.push(...payload.result); allPrintfulOrders.push(...payload.result);
offset = allPrintfulOrders.length; offset = allPrintfulOrders.length;
@@ -124,7 +124,7 @@ class OrdersClient {
class WebhooksClient { class WebhooksClient {
constructor(private creds: Credentials) { } 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`, { const res = await fetch(`${this.creds.apiUrl}/webhooks`, {
method: "GET", method: "GET",
headers: { headers: {
@@ -138,7 +138,11 @@ class WebhooksClient {
return payload; 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`, { const res = await fetch(`${this.creds.apiUrl}/webhooks`, {
method: "POST", method: "POST",
headers: { headers: {
+11 -1
View File
@@ -197,7 +197,7 @@ export namespace Webflow {
export type MetaDataMulti<K extends string, T = any> = { export type MetaDataMulti<K extends string, T = any> = {
[P in K]: T[]; [P in K]: T[];
} & { } & {
pagination: { pagination?: {
limit: number; limit: number;
offset: number; offset: number;
total: number; total: number;
@@ -442,6 +442,16 @@ export namespace Webflow {
OrderUpdated = "ecomm_order_changed", 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< export interface OrderCreated extends MetaData<
Event.OrderCreated, Event.OrderCreated,
Orders.Order Orders.Order
+62 -4
View File
@@ -13,6 +13,7 @@ type ClientOptions = {
}; };
type Credentials = { type Credentials = {
apiBaseUrl: string;
apiSitesUrl: string; apiSitesUrl: string;
apiCollectionsUrl: string; apiCollectionsUrl: string;
authHeader: Record<string, string>; authHeader: Record<string, string>;
@@ -103,11 +104,11 @@ class ProductsClient {
method: "GET", method: "GET",
headers: this.creds.authHeader, 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>; const payload = (await res.json()) as Webflow.MetaDataMulti<"items", Webflow.Products.ProductAndSkus>;
allWebflowProducts.push(...payload.items); allWebflowProducts.push(...payload.items);
offset = allWebflowProducts.length; offset = allWebflowProducts.length;
if (allWebflowProducts.length >= payload.pagination.total) break; if (allWebflowProducts.length >= (payload?.pagination?.total ?? 0)) break;
} while (opt.forceAll); } while (opt.forceAll);
return allWebflowProducts; return allWebflowProducts;
} }
@@ -168,11 +169,11 @@ class OrdersClient {
method: "GET", method: "GET",
headers: this.creds.authHeader, 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>; const payload = (await res.json()) as Webflow.MetaDataMulti<"orders", Webflow.Orders.Order>;
allOrders.push(...payload.orders); allOrders.push(...payload.orders);
offset = allOrders.length; offset = allOrders.length;
if (allOrders.length >= payload.pagination.total) break; if (allOrders.length >= (payload?.pagination?.total ?? 0)) break;
} while (opt.forceAll); } while (opt.forceAll);
return allOrders; 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 { class UtilClient {
constructor(private webhookSecret: string) { } constructor(private webhookSecret: string) { }
@@ -248,16 +303,19 @@ class UtilClient {
export class WebflowClient { export class WebflowClient {
readonly Products: ProductsClient; readonly Products: ProductsClient;
readonly Orders: OrdersClient; readonly Orders: OrdersClient;
readonly Webhooks: WebhooksClient;
readonly Util: UtilClient; readonly Util: UtilClient;
constructor(options: ClientOptions) { constructor(options: ClientOptions) {
const creds: Credentials = { const creds: Credentials = {
apiBaseUrl: `https://api.webflow.com/v2`,
apiSitesUrl: `https://api.webflow.com/v2/sites/${options.siteId}`, apiSitesUrl: `https://api.webflow.com/v2/sites/${options.siteId}`,
apiCollectionsUrl: `https://api.webflow.com/v2/collections/${options.collectionsId}`, apiCollectionsUrl: `https://api.webflow.com/v2/collections/${options.collectionsId}`,
authHeader: { Authorization: `bearer ${options.token}` }, authHeader: { Authorization: `bearer ${options.token}` },
}; };
this.Products = new ProductsClient(creds); this.Products = new ProductsClient(creds);
this.Orders = new OrdersClient(creds); this.Orders = new OrdersClient(creds);
this.Webhooks = new WebhooksClient(creds);
this.Util = new UtilClient(options.webhookSecret); this.Util = new UtilClient(options.webhookSecret);
} }
} }