From 43089c03b65eb76d17e7bb8015d6552e4ab96a77 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Wed, 24 Jun 2026 16:32:03 -0400 Subject: [PATCH] Improve getAll webflow/printful client api --- packages/commerce/src/printful.ts | 28 +++++------ packages/commerce/src/sync.ts | 4 +- packages/commerce/src/util/misc.ts | 6 +++ packages/commerce/src/util/types.ts | 72 ++++++++++++++++++----------- packages/commerce/src/webflow.ts | 55 ++++++++++++++-------- 5 files changed, 104 insertions(+), 61 deletions(-) diff --git a/packages/commerce/src/printful.ts b/packages/commerce/src/printful.ts index f5726db..3c049e4 100644 --- a/packages/commerce/src/printful.ts +++ b/packages/commerce/src/printful.ts @@ -1,5 +1,5 @@ import type { Printful } from "./util/types"; -import { type DeepPartial } from "./util/misc"; +import { type DeepPartial, type PagingOptions } from "./util/misc"; const parsePayload = (res: Response): Promise => res.json().catch(() => res.text().catch(() => undefined)); @@ -35,7 +35,7 @@ class VariantsClient { }); if (res.status === 404 || res.status === 400) return; if (!res.ok) throw new PrintfulError("Failed to get Printful variant", res.status, await parsePayload(res)); - const payload = (await res.json()) as Printful.Products.MetaDataSingle; + const payload = (await res.json()) as Printful.MetaDataSingle; return payload.result; } } @@ -47,19 +47,20 @@ class ProductsClient { this.Variants = new VariantsClient(creds); } - async getAll(offset = 0): Promise { + async getAll(opt: PagingOptions): Promise { const allSyncProducts: Printful.Products.SyncProduct[] = []; - while (true) { - const res = await fetch(`${this.creds.apiUrl}/store/products?offset=${offset}`, { + let offset = opt.offset ?? 0; + do { + const res = await fetch(`${this.creds.apiUrl}/store/products?offset=${offset}&limit=${opt.limit ?? 100}`, { method: "GET", headers: this.creds.authHeaders, }); if (!res.ok) throw new PrintfulError("Failed to get all Printful products", res.status, await parsePayload(res)); - const payload = (await res.json()) as Printful.Products.MetaDataMulti; + const payload = (await res.json()) as Printful.MetaDataMulti; allSyncProducts.push(...payload.result); offset = allSyncProducts.length; if (allSyncProducts.length >= payload.paging.total) break; - } + } while (opt.forceAll); return allSyncProducts; } @@ -70,7 +71,7 @@ class ProductsClient { }); if (res.status === 404 || res.status === 400) return; if (!res.ok) throw new PrintfulError("Failed to get Printful product", res.status, await parsePayload(res)); - const payload = (await res.json()) as Printful.Products.MetaDataSingle; + const payload = (await res.json()) as Printful.MetaDataSingle; return payload.result; } @@ -102,19 +103,20 @@ class OrdersClient { if (!res.ok) throw new PrintfulError("Failed to create Printful order", res.status, await parsePayload(res)); } - async getAll(offset = 0): Promise { + async getAll(opt: PagingOptions): Promise { const allPrintfulOrders: Printful.Orders.Order[] = []; - while (true) { - const res = await fetch(`${this.creds.apiUrl}/orders?offset=${offset}`, { + let offset = opt.offset ?? 0; + do { + const res = await fetch(`${this.creds.apiUrl}/orders?offset=${offset}&limit=${opt.limit ?? 100}`, { method: "GET", headers: this.creds.authHeaders, }); if (!res.ok) throw new PrintfulError("Failed to get all Printful orders", res.status, await parsePayload(res)); - const payload = (await res.json()) as Printful.Products.MetaDataMulti; + const payload = (await res.json()) as Printful.MetaDataMulti; allPrintfulOrders.push(...payload.result); offset = allPrintfulOrders.length; if (allPrintfulOrders.length >= payload.paging.total) break; - } + } while (opt.forceAll); return allPrintfulOrders; } } diff --git a/packages/commerce/src/sync.ts b/packages/commerce/src/sync.ts index 1377ddc..524a7ed 100644 --- a/packages/commerce/src/sync.ts +++ b/packages/commerce/src/sync.ts @@ -42,10 +42,10 @@ export class ProductSyncer { await this.setState({ isSyncing: true, syncingIds: [] }); this.log.debug("populating webflow products"); - const webflowProducts: Webflow.Products.ProductAndSkus[] = await this.webflow.Products.getAll(); + const webflowProducts: Webflow.Products.ProductAndSkus[] = await this.webflow.Products.getAll({ forceAll: true }); this.log.debug("populating printful products"); - let printfulProducts = await this.printful.Products.getAll(); + let printfulProducts = await this.printful.Products.getAll({ forceAll: true }); if (printfulProductId) { const printfulProduct = printfulProducts.find((p) => p.id === printfulProductId); if (printfulProduct) { diff --git a/packages/commerce/src/util/misc.ts b/packages/commerce/src/util/misc.ts index 5fdbd1a..87e0c76 100644 --- a/packages/commerce/src/util/misc.ts +++ b/packages/commerce/src/util/misc.ts @@ -13,3 +13,9 @@ export const formatSlug = (name: string): string => { .replace(/--+/g, "-") // collapse multiple dashes .replace(/^([^a-z0-9_])/, "_$1"); // if it starts with an invalid char, prefix underscore }; + +export type PagingOptions = { + offset?: number, + limit?: number, + forceAll?: boolean +}; diff --git a/packages/commerce/src/util/types.ts b/packages/commerce/src/util/types.ts index 89ac387..c693cb2 100644 --- a/packages/commerce/src/util/types.ts +++ b/packages/commerce/src/util/types.ts @@ -1,20 +1,20 @@ export namespace Printful { + export interface MetaDataSingle { + code: number; + result: T; + } + + export interface MetaDataMulti { + code: number; + result: T[]; + paging: { + total: number; + offset: number; + limit: number; + }; + } + export namespace Products { - export interface MetaDataSingle { - code: number; - result: T; - } - - export interface MetaDataMulti { - code: number; - result: T[]; - paging: { - total: number; - offset: number; - limit: number; - }; - } - type Option = { id: string; value: string; @@ -85,6 +85,16 @@ export namespace Printful { } export namespace Orders { + export interface MetaDataMulti { + code: number; + result: T[]; + paging: { + total: number; + offset: number; + limit: number; + }; + } + export type Order = { external_id: string; shipping: string; @@ -136,7 +146,7 @@ export namespace Printful { is_ignored: boolean; }; } - > {} + > { } export interface ProductDeleted extends MetaData< Event.ProductDeleted, @@ -147,7 +157,7 @@ export namespace Printful { name: string; }; } - > {} + > { } export interface PackageShipped extends MetaData< Event.PackageShipped, @@ -169,7 +179,7 @@ export namespace Printful { status: string; }; } - > {} + > { } export type EventPayload = | ProductUpdated @@ -179,6 +189,16 @@ export namespace Printful { } export namespace Webflow { + export type MetaDataMulti = { + [P in K]: T[]; + } & { + pagination: { + limit: number; + offset: number; + total: number; + }; + }; + export namespace Products { export namespace Skus { export type Sku = { @@ -238,12 +258,12 @@ export namespace Webflow { export type Order = { orderId: string; status: - | "pending" - | "unfulfilled" - | "fulfilled" - | "disputed" - | "dispute-lost" - | "refunded"; + | "pending" + | "unfulfilled" + | "fulfilled" + | "disputed" + | "dispute-lost" + | "refunded"; comment: string; orderComment: string; acceptedOn: string; @@ -420,11 +440,11 @@ export namespace Webflow { export interface OrderCreated extends MetaData< Event.OrderCreated, Orders.Order - > {} + > { } export interface OrderUpdated extends MetaData< Event.OrderUpdated, Orders.Order - > {} + > { } export type EventPayload = OrderCreated | OrderUpdated; } diff --git a/packages/commerce/src/webflow.ts b/packages/commerce/src/webflow.ts index 6f87c8f..553c089 100644 --- a/packages/commerce/src/webflow.ts +++ b/packages/commerce/src/webflow.ts @@ -1,5 +1,5 @@ import type { Webflow } from "./util/types"; -import { type DeepPartial } from "./util/misc"; +import { type DeepPartial, type PagingOptions } from "./util/misc"; import crypto from "node:crypto"; const parsePayload = (res: Response): Promise => @@ -95,14 +95,21 @@ class ProductsClient { return payload.product.id; } - async getAll(): Promise { - const res = await fetch(`${this.creds.apiSitesUrl}/products`, { - method: "GET", - headers: this.creds.authHeader, - }); - if (!res.ok) throw new WebflowError("Failed to get all Webflow products", res.status, await parsePayload(res)); - const payload = (await res.json()) as { items: Webflow.Products.ProductAndSkus[] }; - return payload.items; + async getAll(opt: PagingOptions): Promise { + const allWebflowProducts: Webflow.Products.ProductAndSkus[] = []; + let offset = opt.offset ?? 0; + do { + const res = await fetch(`${this.creds.apiSitesUrl}/products?offset=${offset}&limit=${opt.limit ?? 100}`, { + method: "GET", + headers: this.creds.authHeader, + }); + if (!res.ok) throw new WebflowError("Failed to get all 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; + } while (opt.forceAll); + return allWebflowProducts; } async get(webflowProductId: string): Promise { @@ -149,17 +156,25 @@ class ProductsClient { class OrdersClient { constructor(private creds: Credentials) { } - async getAll(opt: { status?: Webflow.Orders.Order["status"] } = {}): Promise { - // TODO: pagination - const params = new URLSearchParams(); - if (opt.status !== undefined) params.set("status", opt.status); - const res = await fetch(`${this.creds.apiSitesUrl}/orders?${params.toString()}`, { - method: "GET", - headers: this.creds.authHeader, - }); - if (!res.ok) throw new WebflowError("Failed to get all Webflow orders", res.status, await parsePayload(res)); - const payload = (await res.json()) as { orders: Webflow.Orders.Order[] }; - return payload.orders; + async getAll(opt: PagingOptions & { status?: Webflow.Orders.Order["status"] }): Promise { + const allOrders: Webflow.Orders.Order[] = []; + let offset = opt.offset ?? 0; + do { + const params = new URLSearchParams(); + if (opt.status !== undefined) params.set("status", opt.status); + params.set("offset", String(offset)); + params.set("limit", String(opt.limit ?? 100)); + const res = await fetch(`${this.creds.apiSitesUrl}/orders?${params.toString()}`, { + method: "GET", + headers: this.creds.authHeader, + }); + if (!res.ok) throw new WebflowError("Failed to get all 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; + } while (opt.forceAll); + return allOrders; } async update(