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,
|
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
||||||
// development
|
// development
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
|
"http://dev.portal.bladeandbrawn.com/"
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user