Services refactor
This commit is contained in:
+88
-111
@@ -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}`);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user