Overhaul logging

This commit is contained in:
Dominic Ferrando
2026-06-23 22:30:56 -04:00
parent 449b08971f
commit d415685a81
6 changed files with 170 additions and 115 deletions
+1
View File
@@ -15,6 +15,7 @@
"@elysiajs/cors": "^1.4.2",
"elysia": "^1.4.29",
"ml-levenberg-marquardt": "^5.0.1",
"pino": "^10.3.1",
"zipcodes-us": "^1.1.3",
"zod": "^4.4.3"
}
+8 -1
View File
@@ -4,11 +4,18 @@ function requireEnv(key: string): string {
return val;
}
function optionEnv(key: string, fallback: string): string {
const val = Bun.env[key];
if (!val) return fallback;
return val;
}
export const env = {
PRINTFUL_AUTH: requireEnv("PRINTFUL_AUTH"),
PRINTFUL_STORE_ID: requireEnv("PRINTFUL_STORE_ID"),
WEBFLOW_SITE_ID: requireEnv("WEBFLOW_SITE_ID"),
WEBFLOW_COLLECTION_ID: requireEnv("WEBFLOW_COLLECTION_ID"),
WEBFLOW_AUTH: requireEnv("WEBFLOW_AUTH"),
WEBFLOW_WEBHOOK_SECRET: requireEnv("WEBFLOW_WEBHOOK_SECRET")
WEBFLOW_WEBHOOK_SECRET: requireEnv("WEBFLOW_WEBHOOK_SECRET"),
LOG_LEVEL: optionEnv("LOG_LEVEL", "info")
};
+36 -29
View File
@@ -21,8 +21,11 @@ import {
} from "@blade-and-brawn/commerce";
import zipcodesUs from "zipcodes-us";
import z from "zod";
import pino from "pino";
import { env } from "./env";
const log = pino({ level: env.LOG_LEVEL });
// SERVICES
// -----------
const levelCalculator = new LevelCalculator(
@@ -41,11 +44,15 @@ const webflow = new WebflowClient({
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
});
const productSyncer = new ProductSyncer(printful, webflow);
const productSyncer = new ProductSyncer(printful, webflow, log);
// ELYSIA
// -----------
export const app = new Elysia()
.derive(() => ({
startTime: Date.now(),
requestId: crypto.randomUUID(),
}))
.use(
cors({
origin: [
@@ -71,27 +78,29 @@ export const app = new Elysia()
WebflowError,
})
.onError(({ code, error }) => {
.onError(({ code, error, startTime, requestId }) => {
const duration = Date.now() - (startTime ?? Date.now());
switch (code) {
case "PrintfulError":
case "WebflowError":
console.error(error.message, error.status, error.payload);
log.error(
{ requestId, duration, upstreamStatus: error.status, payload: error.payload },
error.message,
);
return status(502, { error: error.message });
default:
console.error(error);
log.error({ requestId, duration, err: error }, "unhandled error");
}
})
.onAfterHandle(({ request, set }) => {
console.log(
JSON.stringify({
lvl: "info",
msg: "req",
.onAfterHandle(({ request, set, startTime, requestId }) => {
log.info({
requestId,
method: request.method,
path: new URL(request.url).pathname,
status: set.status ?? 200,
}),
);
duration: Date.now() - startTime,
}, "request");
})
.get("/", () => ({ status: "ok" }))
@@ -148,18 +157,14 @@ export const app = new Elysia()
await productSyncer.sync(params.printfulProductId);
}, { params: z.object({ printfulProductId: z.coerce.number() }) }),
)
// TODO: add schema validation
.get("/:printfulProductId", async ({ params }) => {
const printfulProduct =
await printful.Products.get(params.printfulProductId);
const printfulProduct = await printful.Products.get(params.printfulProductId);
if (!printfulProduct) throw new NotFoundError("Missing printful product");
const webflowProductId =
printfulProduct.sync_product.external_id.split("-")[0];
const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0];
if (!webflowProductId) throw new NotFoundError("Missing webflow product ID");
const webflowProduct =
await webflow.Products.get(webflowProductId);
const webflowProduct = await webflow.Products.get(webflowProductId);
return { printfulProduct, webflowProduct };
}, {
@@ -174,18 +179,19 @@ export const app = new Elysia()
switch (payload.type) {
case Printful.Webhook.Event.ProductUpdated: {
const printfulProduct = payload.data.sync_product;
log.info({ productId: printfulProduct.id }, "printful webhook: product updated");
await productSyncer.sync(printfulProduct.id);
break;
}
case Printful.Webhook.Event.ProductDeleted: {
await webflow.Products.remove(
payload.data.sync_product.external_id,
);
log.info({ externalId: payload.data.sync_product.external_id }, "printful webhook: product deleted");
await webflow.Products.remove(payload.data.sync_product.external_id);
break;
}
case Printful.Webhook.Event.PackageShipped: {
const webflowOrderId = payload.data.order.external_id;
const shipInfo = payload.data.shipment;
log.info({ webflowOrderId, carrier: shipInfo.carrier, tracking: shipInfo.tracking_number }, "printful webhook: package shipped");
await webflow.Orders.update(webflowOrderId, {
shippingTrackingURL: shipInfo.tracking_url,
@@ -197,6 +203,8 @@ export const app = new Elysia()
});
break;
}
default:
log.warn({ type: (payload as any).type }, "printful webhook: unhandled event type");
}
})
.post("/webhook/webflow", async ({ request, body }) => {
@@ -208,6 +216,7 @@ export const app = new Elysia()
switch (payload.triggerType) {
case Webflow.Webhook.Event.OrderCreated: {
const webflowOrder = payload.payload;
log.info({ orderId: webflowOrder.orderId }, "webflow webhook: order created");
await printful.Orders.create({
external_id: webflowOrder.orderId,
@@ -219,24 +228,22 @@ export const app = new Elysia()
address2: webflowOrder.shippingAddress.line2,
city: webflowOrder.shippingAddress.city,
state_code: zipcodesUs.find(
webflowOrder.shippingAddress.postalCode.split(
"-",
)[0] ?? "",
webflowOrder.shippingAddress.postalCode.split("-")[0] ?? "",
).stateCode,
country_code: webflowOrder.shippingAddress.country,
zip: webflowOrder.shippingAddress.postalCode,
},
items: webflowOrder.purchasedItems.map(
(webflowOrderSku) => ({
items: webflowOrder.purchasedItems.map((webflowOrderSku) => ({
external_variant_id: webflowOrderSku.variantId,
quantity: webflowOrderSku.count,
}),
),
})),
});
break;
}
default:
log.warn({ triggerType: (payload as any).triggerType }, "webflow webhook: unhandled event type");
}
});
app.listen(3000);
app.listen(3000, () => log.info({ port: 3000 }, "server started"));
+32
View File
@@ -20,6 +20,7 @@
"@elysiajs/cors": "^1.4.2",
"elysia": "^1.4.29",
"ml-levenberg-marquardt": "^5.0.1",
"pino": "^10.3.1",
"zipcodes-us": "^1.1.3",
"zod": "^4.4.3",
},
@@ -56,6 +57,9 @@
"packages/commerce": {
"name": "@blade-and-brawn/commerce",
"version": "0.0.0",
"dependencies": {
"pino": "^10.3.1",
},
},
"packages/domain": {
"name": "@blade-and-brawn/domain",
@@ -143,6 +147,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
@@ -257,6 +263,8 @@
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
@@ -363,26 +371,46 @@
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="],
"pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
"svelte": ["svelte@5.56.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.11", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA=="],
@@ -395,6 +423,8 @@
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="],
"tiny-glob": ["tiny-glob@0.2.9", "", { "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" } }, "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -432,5 +462,7 @@
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
}
}
+3
View File
@@ -5,5 +5,8 @@
"type": "module",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"pino": "^10.3.1"
}
}
+64 -59
View File
@@ -3,12 +3,10 @@ import { formatSlug, type DeepPartial } from "./util/misc";
import { WebflowClient } from "./webflow";
import { PrintfulClient } from "./printful";
import { redis } from "bun";
import pino from "pino";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const R2_WORKER_URL = "https://r2-worker.xominus.workers.dev";
export const WEBSITE_MEDIA_URL = "https://website-media.bladeandbrawn.com";
type MainProduct = {
name: string;
externalId: string;
@@ -25,94 +23,103 @@ type SyncState = {
};
export class ProductSyncer {
private log: pino.Logger;
constructor(
private printful: PrintfulClient,
private webflow: WebflowClient,
) { }
log?: pino.Logger,
) {
this.log = (log ?? pino({ level: "silent" })).child({ component: "ProductSyncer" });
}
async sync(printfulProductId?: number) {
const syncStart = Date.now();
try {
if (printfulProductId) this.log.info({ printfulProductId }, "sync started");
else this.log.info("sync started");
await this.setState({ isSyncing: true, syncingIds: [] });
// Populate products
const mainPrintfulProducts: MainProduct[] = [];
const webflowProducts: Webflow.Products.ProductAndSkus[] = [];
this.log.debug("populating webflow products");
const webflowProducts: Webflow.Products.ProductAndSkus[] = await this.webflow.Products.getAll();
this.log.debug("populating printful products");
let printfulProducts = await this.printful.Products.getAll();
if (printfulProductId) {
const printfulProduct = printfulProducts.find(
(p) => p.id === printfulProductId,
);
const printfulProduct = printfulProducts.find((p) => p.id === printfulProductId);
if (printfulProduct) {
const mainProductName = this.getMainProductName(
const metaProductName = this.getMainProductName(
printfulProduct.name,
);
printfulProducts = printfulProducts.filter((p) =>
p.name.includes(mainProductName),
p.name.includes(metaProductName),
);
}
}
webflowProducts.push(...(await this.webflow.Products.getAll()));
// Generate main printful products
console.log("Generating main products...");
this.log.info({ count: printfulProducts.length }, "generating meta printful products");
const metaPrintfulProducts: MainProduct[] = [];
for (const printfulProduct of printfulProducts) {
const mainProductName = this.getMainProductName(
printfulProduct.name,
);
let mainPrintfulProduct = mainPrintfulProducts.find((mp) =>
let metaPrintfulProduct = metaPrintfulProducts.find((mp) =>
mp.name.includes(mainProductName),
);
if (!mainPrintfulProduct) {
mainPrintfulProduct = {
if (!metaPrintfulProduct) {
metaPrintfulProduct = {
name: mainProductName,
variants: [],
externalId: printfulProduct.external_id,
colors: new Set(),
};
mainPrintfulProducts.push(mainPrintfulProduct);
metaPrintfulProducts.push(metaPrintfulProduct);
}
const productColor = this.findColorInProductName(
printfulProduct.name,
);
mainPrintfulProduct.variants.push({
metaPrintfulProduct.variants.push({
color: productColor,
product: (await this.printful.Products.get(printfulProduct.id))!,
});
mainPrintfulProduct.colors.add(productColor);
metaPrintfulProduct.colors.add(productColor);
}
// TODO: Validate main printful products
// Sync the main printful products
console.log("Syncing main products...");
for (const mainPrintfulProduct of mainPrintfulProducts) {
for (const metaPrintfulProduct of metaPrintfulProducts) {
this.log.info({ name: metaPrintfulProduct.name }, "syncing meta printful product");
await this.setState({
isSyncing: true,
syncingIds: mainPrintfulProduct.variants.map(
syncingIds: metaPrintfulProduct.variants.map(
(v) => v.product.sync_product.id,
),
});
const foundColors = mainPrintfulProduct.colors;
const foundColors = metaPrintfulProduct.colors;
const foundSizes: Set<string> = new Set();
const webflowSkus: DeepPartial<Webflow.Products.Skus.Sku>[] = [];
for (const specialVariant of mainPrintfulProduct.variants) {
for (const specialVariant of metaPrintfulProduct.variants) {
for (const printfulVariant of specialVariant.product.sync_variants) {
// check if this variant size is in all other special variants
let sizeIsInAllVariants = true;
for (const specialVariant of mainPrintfulProduct.variants) {
const sizes = specialVariant.product.sync_variants.map(
(v) => v.size,
this.log.debug(
{ size: printfulVariant.size, color: printfulVariant.color },
"generating webflow SKU from printful variant"
);
// check if this variant size is in all other special variants, if its not then skip it
let sizeIsInAllVariants = true;
for (const metaVariant of metaPrintfulProduct.variants) {
const sizes = metaVariant.product.sync_variants.map((v) => v.size);
if (!sizes.includes(printfulVariant.size))
sizeIsInAllVariants = false;
}
if (!sizeIsInAllVariants) continue;
if (sizeIsInAllVariants) {
foundSizes.add(printfulVariant.size);
webflowSkus.push({
@@ -138,31 +145,27 @@ export class ProductSyncer {
});
}
}
}
// SYNC PRODUCT DATA
let existingWebflowProduct = webflowProducts.find(
(webflowProduct) =>
webflowProduct.product.id ===
mainPrintfulProduct.externalId.split("-")[0],
(webflowProduct) => webflowProduct.product.id === metaPrintfulProduct.externalId.split("-")[0],
);
if (existingWebflowProduct) {
// SYNC PRODUCT UPDATE
this.log.info("existing webflow product found, updating it");
// do not update images
for (const webflowSku of webflowSkus) {
for (const webflowSku of webflowSkus)
delete webflowSku.fieldData?.["main-image"];
}
const webflowProductId =
mainPrintfulProduct.externalId.split("-")[0];
if (!webflowProductId)
throw new Error("Malformed printful product ID");
const webflowProductId = metaPrintfulProduct.externalId.split("-")[0];
if (!webflowProductId) throw new Error("Malformed printful product ID");
await this.webflow.Products.update(webflowProductId, {
product: {
fieldData: {
name: mainPrintfulProduct.name,
slug: formatSlug(mainPrintfulProduct.name),
name: metaPrintfulProduct.name,
slug: formatSlug(metaPrintfulProduct.name),
shippable: true,
"tax-category": "standard-taxable",
"sku-properties": [
@@ -209,11 +212,13 @@ export class ProductSyncer {
}
} else {
// SYNC PRODUCT CREATE
this.log.info("existing webflow product not found, creating it");
const webflowProductId = await this.webflow.Products.create({
product: {
fieldData: {
name: mainPrintfulProduct.name,
slug: formatSlug(mainPrintfulProduct.name),
name: metaPrintfulProduct.name,
slug: formatSlug(metaPrintfulProduct.name),
shippable: true,
"tax-category": "standard-taxable",
"sku-properties": [
@@ -249,12 +254,11 @@ export class ProductSyncer {
existingWebflowProduct = await this.webflow.Products.get(webflowProductId);
if (!existingWebflowProduct)
throw new Error("Webflow product missing after create");
throw new Error("webflow product missing after create");
for (const specialVariant of mainPrintfulProduct.variants) {
const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] =
[];
for (const printfulVariant of specialVariant.product.sync_variants) {
for (const metaVariant of metaPrintfulProduct.variants) {
const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] = [];
for (const printfulVariant of metaVariant.product.sync_variants) {
const associatedWebflowSku = existingWebflowProduct.skus.find(
(sku) =>
sku.fieldData["sku-values"]?.["color"] === printfulVariant.color &&
@@ -269,12 +273,13 @@ export class ProductSyncer {
}
await sleep(10000);
this.log.info({ printfulProduct: metaVariant.product.sync_product.id }, "updating printful product");
await this.printful.Products.update(
specialVariant.product.sync_product.id,
metaVariant.product.sync_product.id,
{
sync_product: {
id: specialVariant.product.sync_product.id,
external_id: `${webflowProductId}-${specialVariant.color}`,
id: metaVariant.product.sync_product.id,
external_id: `${webflowProductId}-${metaVariant.color}`,
},
sync_variants: newPrintfulVariants,
},
@@ -282,13 +287,13 @@ export class ProductSyncer {
}
}
}
console.log("Done");
}
finally {
this.log.info({ durationMs: Date.now() - syncStart }, "sync complete");
} finally {
try {
await this.setState({ isSyncing: false, syncingIds: [] });
} catch {
console.error("Failed to reset sync state");
this.log.error("failed to reset sync state");
}
}
}