Overhaul logging
This commit is contained in:
@@ -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
@@ -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")
|
||||
};
|
||||
|
||||
+41
-34
@@ -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",
|
||||
method: request.method,
|
||||
path: new URL(request.url).pathname,
|
||||
status: set.status ?? 200,
|
||||
}),
|
||||
);
|
||||
.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) => ({
|
||||
external_variant_id: webflowOrderSku.variantId,
|
||||
quantity: webflowOrderSku.count,
|
||||
}),
|
||||
),
|
||||
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"));
|
||||
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "^10.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,144 +23,149 @@ 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
|
||||
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 specialVariant of mainPrintfulProduct.variants) {
|
||||
const sizes = specialVariant.product.sync_variants.map(
|
||||
(v) => v.size,
|
||||
);
|
||||
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);
|
||||
foundSizes.add(printfulVariant.size);
|
||||
|
||||
webflowSkus.push({
|
||||
id: printfulVariant.external_id,
|
||||
fieldData: {
|
||||
name: printfulVariant.name,
|
||||
slug: formatSlug(printfulVariant.name),
|
||||
"sku-values": {
|
||||
color: specialVariant.color,
|
||||
size: printfulVariant.size,
|
||||
},
|
||||
price: {
|
||||
value: Math.floor(
|
||||
+printfulVariant.retail_price * 100,
|
||||
),
|
||||
unit: printfulVariant.currency,
|
||||
currency: printfulVariant.currency,
|
||||
},
|
||||
"main-image": PrintfulClient.Util.getVariantMainImage(
|
||||
printfulVariant,
|
||||
),
|
||||
webflowSkus.push({
|
||||
id: printfulVariant.external_id,
|
||||
fieldData: {
|
||||
name: printfulVariant.name,
|
||||
slug: formatSlug(printfulVariant.name),
|
||||
"sku-values": {
|
||||
color: specialVariant.color,
|
||||
size: printfulVariant.size,
|
||||
},
|
||||
});
|
||||
}
|
||||
price: {
|
||||
value: Math.floor(
|
||||
+printfulVariant.retail_price * 100,
|
||||
),
|
||||
unit: printfulVariant.currency,
|
||||
currency: printfulVariant.currency,
|
||||
},
|
||||
"main-image": PrintfulClient.Util.getVariantMainImage(
|
||||
printfulVariant,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user