Services refactor

This commit is contained in:
Dominic Ferrando
2026-06-23 15:36:53 -04:00
parent 5bae0f067d
commit 720aa61c66
6 changed files with 502 additions and 638 deletions
+88 -111
View File
@@ -1,7 +1,7 @@
import type { Printful, Webflow } from "./util/types";
import { FetchError, formatSlug, type DeepPartial } from "./util/misc";
import { WebflowService } from "./webflow";
import { PrintfulService } from "./printful";
import { WebflowClient } from "./webflow";
import { PrintfulClient } from "./printful";
import { redis } from "bun";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -24,15 +24,20 @@ type SyncState = {
syncingIds: number[];
};
export abstract class SyncService {
static async sync(printfulProductId?: number) {
export class ProductSyncer {
constructor(
private printful: PrintfulClient,
private webflow: WebflowClient,
) { }
async sync(printfulProductId?: number) {
await this.setState({ isSyncing: true, syncingIds: [] });
// Populate products
const mainPrintfulProducts: MainProduct[] = [];
const webflowProducts: Webflow.Products.ProductAndSkus[] = [];
try {
let printfulProducts = await PrintfulService.Products.getAll();
let printfulProducts = await this.printful.Products.getAll();
if (printfulProductId) {
const printfulProduct = printfulProducts.find(
(p) => p.id === printfulProductId,
@@ -46,7 +51,7 @@ export abstract class SyncService {
);
}
}
webflowProducts.push(...(await WebflowService.Products.getAll()));
webflowProducts.push(...(await this.webflow.Products.getAll()));
// Generate main printful products
console.log("Generating main products...");
@@ -73,9 +78,7 @@ export abstract class SyncService {
);
mainPrintfulProduct.variants.push({
color: productColor,
product: (await PrintfulService.Products.get(
printfulProduct.id,
))!,
product: (await this.printful.Products.get(printfulProduct.id))!,
});
mainPrintfulProduct.colors.add(productColor);
}
@@ -94,25 +97,22 @@ export abstract class SyncService {
isSyncing: true,
syncingIds: mainPrintfulProduct.variants.map(
(v) => v.product.sync_product.id,
)
),
});
const foundColors = mainPrintfulProduct.colors;
const foundSizes: Set<string> = new Set();
const webflowSkus: DeepPartial<Webflow.Products.Skus.Sku>[] =
[];
const webflowSkus: DeepPartial<Webflow.Products.Skus.Sku>[] = [];
for (const specialVariant of mainPrintfulProduct.variants) {
for (const printfulVariant of specialVariant.product
.sync_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,
);
const sizes = specialVariant.product.sync_variants.map(
(v) => v.size,
);
if (!sizes.includes(printfulVariant.size))
sizeIsInAllVariants = false;
}
@@ -136,10 +136,9 @@ export abstract class SyncService {
unit: printfulVariant.currency,
currency: printfulVariant.currency,
},
"main-image":
PrintfulService.Util.getVariantMainImage(
printfulVariant,
),
"main-image": PrintfulClient.Util.getVariantMainImage(
printfulVariant,
),
},
});
}
@@ -165,7 +164,7 @@ export abstract class SyncService {
if (!webflowProductId) {
throw new Error("Malformed printful product ID");
}
await WebflowService.Products.update(webflowProductId, {
await this.webflow.Products.update(webflowProductId, {
product: {
fieldData: {
name: mainPrintfulProduct.name,
@@ -176,24 +175,20 @@ export abstract class SyncService {
{
id: "color",
name: "Color",
enum: Array.from(foundColors).map(
(color) => ({
id: color,
slug: formatSlug(color),
name: color,
}),
),
enum: Array.from(foundColors).map((color) => ({
id: color,
slug: formatSlug(color),
name: color,
})),
},
{
id: "size",
name: "Size",
enum: Array.from(foundSizes).map(
(size) => ({
id: size,
slug: formatSlug(size),
name: size,
}),
),
enum: Array.from(foundSizes).map((size) => ({
id: size,
slug: formatSlug(size),
name: size,
})),
},
],
},
@@ -202,18 +197,17 @@ export abstract class SyncService {
});
for (const webflowSku of webflowSkus.slice(1)) {
const existingWebflowSku =
existingWebflowProduct.skus.find(
(sku) => sku.id === webflowSku.id,
);
const existingWebflowSku = existingWebflowProduct.skus.find(
(sku) => sku.id === webflowSku.id,
);
if (webflowSku.id && existingWebflowSku) {
await WebflowService.Products.Skus.update(
await this.webflow.Products.Skus.update(
webflowProductId,
webflowSku.id,
webflowSku,
);
} else {
await WebflowService.Products.Skus.create(
await this.webflow.Products.Skus.create(
webflowProductId,
[webflowSku],
);
@@ -221,51 +215,45 @@ export abstract class SyncService {
}
} else {
// SYNC PRODUCT CREATE
const webflowProductId =
await WebflowService.Products.create({
product: {
fieldData: {
name: mainPrintfulProduct.name,
slug: formatSlug(mainPrintfulProduct.name),
shippable: true,
"tax-category": "standard-taxable",
"sku-properties": [
{
id: "color",
name: "Color",
enum: Array.from(foundColors).map(
(color) => ({
id: color,
slug: formatSlug(color),
name: color,
}),
),
},
{
id: "size",
name: "Size",
enum: Array.from(foundSizes).map(
(size) => ({
id: size,
slug: formatSlug(size),
name: size,
}),
),
},
],
},
const webflowProductId = await this.webflow.Products.create({
product: {
fieldData: {
name: mainPrintfulProduct.name,
slug: formatSlug(mainPrintfulProduct.name),
shippable: true,
"tax-category": "standard-taxable",
"sku-properties": [
{
id: "color",
name: "Color",
enum: Array.from(foundColors).map((color) => ({
id: color,
slug: formatSlug(color),
name: color,
})),
},
{
id: "size",
name: "Size",
enum: Array.from(foundSizes).map((size) => ({
id: size,
slug: formatSlug(size),
name: size,
})),
},
],
},
sku: webflowSkus[0],
});
},
sku: webflowSkus[0],
});
// create webflow product SKUs
await WebflowService.Products.Skus.create(
await this.webflow.Products.Skus.create(
webflowProductId,
webflowSkus.slice(1),
);
existingWebflowProduct =
await WebflowService.Products.get(webflowProductId);
existingWebflowProduct = await this.webflow.Products.get(webflowProductId);
if (!existingWebflowProduct) {
console.error("Missing webflow product");
throw new Error("Failed to create Webflow product");
@@ -274,30 +262,22 @@ export abstract class SyncService {
for (const specialVariant of mainPrintfulProduct.variants) {
const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] =
[];
for (const printfulVariant of specialVariant.product
.sync_variants) {
const associatedWebflowSku =
existingWebflowProduct.skus.find(
(sku) =>
sku.fieldData["sku-values"]?.[
"color"
] === printfulVariant.color &&
sku.fieldData["sku-values"]?.[
"size"
] === printfulVariant.size,
);
for (const printfulVariant of specialVariant.product.sync_variants) {
const associatedWebflowSku = existingWebflowProduct.skus.find(
(sku) =>
sku.fieldData["sku-values"]?.["color"] === printfulVariant.color &&
sku.fieldData["sku-values"]?.["size"] === printfulVariant.size,
);
if (associatedWebflowSku) {
newPrintfulVariants.push({
id: printfulVariant.id, // printful variant id
external_id: String(
associatedWebflowSku.id,
), // webflow variant id
id: printfulVariant.id,
external_id: String(associatedWebflowSku.id),
});
}
}
await sleep(10000);
await PrintfulService.Products.update(
await this.printful.Products.update(
specialVariant.product.sync_product.id,
{
sync_product: {
@@ -319,41 +299,38 @@ export abstract class SyncService {
console.log("Done");
}
public static async setState(state: SyncState) {
async setState(state: SyncState) {
await redis.set("commerce:sync:state", JSON.stringify(state));
await redis.expire("commerce:sync:state", 1800);
}
public static async getState(): Promise<SyncState> {
async getState(): Promise<SyncState> {
const val = await redis.get("commerce:sync:state");
if (val) return JSON.parse(val);
return { isSyncing: false, syncingIds: [] }
return { isSyncing: false, syncingIds: [] };
}
public static findColorInProductName(productName: string): string {
findColorInProductName(productName: string): string {
const m = productName.match(/\[([^\]]+)\]/);
return m?.[1] ? m[1] : "N/A";
};
}
public static getMainProductName(productName: string) {
getMainProductName(productName: string): string {
const colorInName = this.findColorInProductName(productName);
if (colorInName) {
return productName.replace(`[${colorInName}]`, "").trimEnd();
} else {
return productName;
}
};
}
private static async getProductImageUrls(slug: string) {
private async getProductImageUrls(slug: string): Promise<string[]> {
const res = await fetch(`${R2_WORKER_URL}/product-images/${slug}`);
if (!res.ok) {
console.error(
"Failed to get product image keys:",
await res.json(),
);
console.error("Failed to get product image keys:", await res.json());
throw new Error("Failed to get product image keys");
}
const productImageKeys: string[] = (await res.json()) as string[];
return productImageKeys.map((key) => `${WEBSITE_MEDIA_URL}/${key}`);
};
}
}