import type { Printful } from "./util/types"; import { type DeepPartial, type PagingOptions } from "./util/misc"; const parsePayload = (res: Response): Promise => res.json().catch(() => res.text().catch(() => undefined)); type ClientOptions = { token: string; storeId: string; }; type Credentials = { apiUrl: string; authHeaders: Record; }; export class PrintfulError extends Error { constructor( message: string, public upstreamStatus: number, public payload: unknown, public status: number = 502 ) { super(message); this.name = "PrintfulError"; } } class VariantsClient { constructor(private creds: Credentials) { } async get(variantId: number | string): Promise { const res = await fetch(`${this.creds.apiUrl}/store/variants/${variantId}`, { method: "GET", headers: this.creds.authHeaders, }); 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.MetaDataSingle; return payload.result; } } class ProductsClient { readonly Variants: VariantsClient; constructor(private creds: Credentials) { this.Variants = new VariantsClient(creds); } async list(opt: PagingOptions = {}): Promise { const allSyncProducts: Printful.Products.SyncProduct[] = []; 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 list Printful products", res.status, await parsePayload(res)); 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; } async get(productId: number | string): Promise { const res = await fetch(`${this.creds.apiUrl}/store/products/${productId}`, { method: "GET", headers: this.creds.authHeaders, }); 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.MetaDataSingle; return payload.result; } async update(pProductId: number, pProduct: DeepPartial) { const res = await fetch(`${this.creds.apiUrl}/store/products/${pProductId}`, { method: "PUT", headers: { "Content-Type": "application/json", ...this.creds.authHeaders, }, body: JSON.stringify(pProduct), }); if (!res.ok) throw new PrintfulError("Failed to update Printful product", res.status, await parsePayload(res)); } } class OrdersClient { constructor(private creds: Credentials) { } async create(pOrder: Printful.Orders.Order) { const res = await fetch(`${this.creds.apiUrl}/orders`, { method: "POST", headers: { "Content-Type": "application/json", ...this.creds.authHeaders, }, body: JSON.stringify(pOrder), }); if (!res.ok) throw new PrintfulError("Failed to create Printful order", res.status, await parsePayload(res)); } async list(opt: PagingOptions = {}): Promise { const allPOrders: Printful.Orders.Order[] = []; 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 list Printful orders", res.status, await parsePayload(res)); const payload = (await res.json()) as Printful.MetaDataMulti; allPOrders.push(...payload.result); offset = allPOrders.length; if (allPOrders.length >= payload.paging.total) break; } while (opt.forceAll); return allPOrders; } } class WebhooksClient { constructor(private creds: Credentials) { } async get(): Promise { const res = await fetch(`${this.creds.apiUrl}/webhooks`, { method: "GET", headers: { "Content-Type": "application/json", ...this.creds.authHeaders, } }); if (res.status === 404 || res.status === 400) return; if (!res.ok) throw new PrintfulError("Failed to get webhook configuration", res.status, await parsePayload(res)); const payload = (await res.json()) as Printful.Webhook.Config; return payload; } /** * 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: { "Content-Type": "application/json", ...this.creds.authHeaders, }, body: JSON.stringify({ "url": url, "types": events }), }); if (!res.ok) throw new PrintfulError("Failed to register webhook URL", res.status, await parsePayload(res)); } } export class PrintfulClient { readonly Products: ProductsClient; readonly Orders: OrdersClient; readonly Webhooks: WebhooksClient; constructor(options: ClientOptions) { const creds: Credentials = { apiUrl: "https://api.printful.com", authHeaders: { Authorization: `Bearer ${options.token}`, "X-PF-Store-Id": options.storeId, }, }; this.Products = new ProductsClient(creds); this.Orders = new OrdersClient(creds); this.Webhooks = new WebhooksClient(creds); } static Util = { getVariantMainImage(syncVariant: Printful.Products.SyncVariant): string { const previewFile = syncVariant.files.find((f) => f.type === "preview"); return previewFile?.preview_url ?? syncVariant.product.image; }, }; }