Product sync

This commit is contained in:
Dominic Ferrando
2025-09-13 13:52:53 -04:00
parent 3f9c2ae2e6
commit bdacfb535e
7 changed files with 93 additions and 50 deletions
-6
View File
@@ -1,6 +0,0 @@
PRINTFUL_AUTH=RGI4WiWeQkrEYek3obwxZL6c424LAJsUimDX1bHF
PRINTFUL_STORE_ID=15619272
WEBFLOW_AUTH=115ded1e97162f5d4835bcb87e236fe4bce1ad602f519bca8179394092a8c6f6
WEBFLOW_SITE_ID=6737d2c9511d414861f1726e
WEBFLOW_COLLECTION_ID=675a3fbe966e58ea9aa7110f
+1 -1
View File
@@ -1,3 +1,3 @@
node_modules/ node_modules/
.env.* .env
database/data.db database/data.db
+37 -15
View File
@@ -46,11 +46,16 @@ export namespace Printful {
} }
export namespace Products { export namespace Products {
interface MetaData<T = any> { export interface MetaDataSingle<T = any> {
code: number; code: number;
result: T; result: T;
} }
export interface MetaDataMulti<T = any> {
code: number;
result: T[];
}
interface Option { interface Option {
id: string; id: string;
value: string; value: string;
@@ -77,7 +82,7 @@ export namespace Printful {
stitch_count_tier: string; stitch_count_tier: string;
} }
interface SyncProductData { export interface SyncProduct {
sync_product: { sync_product: {
id: number; id: number;
external_id: string; external_id: string;
@@ -115,20 +120,37 @@ export namespace Printful {
}[] }[]
} }
export type SyncProduct = MetaData<SyncProductData>;
}
export async function getSyncProduct(syncProductId: number): Promise<Products.SyncProduct> { export async function getSyncProduct(syncProductId: number): Promise<SyncProduct> {
const payload: Products.SyncProduct = await ( const payload: MetaDataSingle<SyncProduct> = await (
await fetch(`${Printful.API_URL}/store/products/${syncProductId}`, { await fetch(`${Printful.API_URL}/store/products/${syncProductId}`, {
method: "GET", method: "GET",
headers: { headers: {
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`, "Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}` "X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
} }
}) })
).json(); ).json();
return payload; return payload.result;
}
export async function getSyncProducts(): Promise<SyncProduct[]> {
const payload: MetaDataMulti<SyncProduct["sync_product"]> = await (
await fetch(`${Printful.API_URL}/store/products`, {
method: "GET",
headers: {
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
}
})
).json();
// need to fetch variants
const syncProducts = await Promise.all(
payload.result.map(p => getSyncProduct(p.id))
);
return syncProducts;
}
} }
} }
+4 -4
View File
@@ -16,7 +16,7 @@ class ProductRecords {
this.db = new Database(dbPath); this.db = new Database(dbPath);
} }
findFromWebflow(webflowProductId: number): ProductRecord | undefined { findUsingWebflow(webflowProductId: number): ProductRecord | undefined {
const row = this.db.query(` const row = this.db.query(`
SELECT * FROM product_records WHERE webflowProductId = ? SELECT * FROM product_records WHERE webflowProductId = ?
`).get(webflowProductId); `).get(webflowProductId);
@@ -24,7 +24,7 @@ class ProductRecords {
return row as ProductRecord | undefined; return row as ProductRecord | undefined;
} }
findFromPrintful(printfulProductId: number): ProductRecord | undefined { findUsingPrintful(printfulProductId: number): ProductRecord | undefined {
const row = this.db.query(` const row = this.db.query(`
SELECT * FROM product_records WHERE printfulProductId = ? SELECT * FROM product_records WHERE printfulProductId = ?
`).get(printfulProductId); `).get(printfulProductId);
@@ -32,13 +32,13 @@ class ProductRecords {
return row as ProductRecord | undefined; return row as ProductRecord | undefined;
} }
deleteFromWebflow(webflowProductId: number) { deleteUsingWebflow(webflowProductId: number) {
this.db.run(` this.db.run(`
DELETE FROM product_records WHERE webflowProductId = ? DELETE FROM product_records WHERE webflowProductId = ?
`, [webflowProductId]); `, [webflowProductId]);
} }
deleteFromPrintful(printfulProductId: number) { deleteUsingPrintful(printfulProductId: number) {
this.db.run(` this.db.run(`
DELETE FROM product_records WHERE printfulProductId = ? DELETE FROM product_records WHERE printfulProductId = ?
`, [printfulProductId]); `, [printfulProductId]);
+15 -16
View File
@@ -26,7 +26,7 @@ export namespace Webflow {
unit: string; unit: string;
currency: string; currency: string;
}; };
"main-image": string; "main-image"?: string;
} }
} }
@@ -52,8 +52,8 @@ export namespace Webflow {
} }
} }
export async function deleteProductFromPrintful(printfulProductId: number) { export async function deleteProductUsingPrintful(printfulProductId: number) {
const webflowProductId = productRecords.findFromPrintful(printfulProductId)?.webflowProductId; const webflowProductId = productRecords.findUsingPrintful(printfulProductId)?.webflowProductId;
if (webflowProductId) { if (webflowProductId) {
const res = await fetch(`${Webflow.API_COLLECTIONS_URL}/items/${webflowProductId}`, { const res = await fetch(`${Webflow.API_COLLECTIONS_URL}/items/${webflowProductId}`, {
method: "DELETE", method: "DELETE",
@@ -63,17 +63,17 @@ export namespace Webflow {
}, },
body: JSON.stringify({}) body: JSON.stringify({})
}); });
productRecords.deleteFromWebflow(webflowProductId); productRecords.deleteUsingWebflow(webflowProductId);
} }
} }
export async function updateProductFromPrintful(printfulProduct: Printful.Products.SyncProduct) { export async function updateProductUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const webflowProductId = productRecords.findFromPrintful(printfulProduct.result.sync_product.id)?.webflowProductId; const webflowProductId = productRecords.findUsingPrintful(printfulProduct.sync_product.id)?.webflowProductId;
if (!webflowProductId) { if (!webflowProductId) {
throw new Error("Product is not recognized"); throw new Error("Product is not recognized");
} }
const printfulVariants = printfulProduct.result.sync_variants; const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = []; const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) { for (const printfulVariant of printfulVariants) {
webflowVariants.push({ webflowVariants.push({
@@ -107,8 +107,8 @@ export namespace Webflow {
body: JSON.stringify({ body: JSON.stringify({
"product": { "product": {
"fieldData": { "fieldData": {
"name": printfulProduct.result.sync_product.name, "name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.result.sync_product.name), "slug": formatSlug(printfulProduct.sync_product.name),
"sku-properties": [ "sku-properties": [
{ {
"id": "color", "id": "color",
@@ -157,12 +157,11 @@ export namespace Webflow {
"sku": webflowVariants[i] "sku": webflowVariants[i]
}) })
}); });
console.log(JSON.stringify((await res.json())));
} }
} }
export async function createProductFromPrintful(printfulProduct: Printful.Products.SyncProduct) { export async function createProductUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const printfulVariants = printfulProduct.result.sync_variants; const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = []; const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) { for (const printfulVariant of printfulVariants) {
@@ -180,7 +179,7 @@ export namespace Webflow {
"currency": printfulVariant.currency "currency": printfulVariant.currency
}, },
// TODO: sync image // TODO: sync image
"main-image": "https://www.example.com/image.jpg" // "main-image": "https://www.example.com/image.jpg"
} }
}); });
} }
@@ -198,8 +197,8 @@ export namespace Webflow {
body: JSON.stringify({ body: JSON.stringify({
"product": { "product": {
"fieldData": { "fieldData": {
"name": printfulProduct.result.sync_product.name, "name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.result.sync_product.name), "slug": formatSlug(printfulProduct.sync_product.name),
"sku-properties": [ "sku-properties": [
{ {
"id": "color", "id": "color",
@@ -232,7 +231,7 @@ export namespace Webflow {
} }
const webflowProductId = addProductResponse["product"]["id"]; const webflowProductId = addProductResponse["product"]["id"];
const printfulProductId = printfulProduct.result.sync_product.id; const printfulProductId = printfulProduct.sync_product.id;
// CREATE WEBFLOW PRODUCT SKUs // CREATE WEBFLOW PRODUCT SKUs
let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, { let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, {
+1 -1
View File
@@ -26,4 +26,4 @@ primary_region = 'ord'
[[mounts]] [[mounts]]
source = "data" source = "data"
destination = "/data" destination = "/data"
+35 -7
View File
@@ -61,32 +61,60 @@ const server = Bun.serve({
}) })
} }
}, },
"/products/sync": {
OPTIONS() {
return new ClientResponse(undefined, { status: 204 });
},
async POST(req) {
const printfulProducts = await Printful.Products.getSyncProducts();
// handle update/create
for (const printfulProduct of printfulProducts) {
const productRecord = productRecords.findUsingPrintful(printfulProduct.sync_product.id);
if (productRecord) {
await Webflow.updateProductUsingPrintful(printfulProduct);
}
else {
await Webflow.createProductUsingPrintful(printfulProduct);
}
}
// handle delete
// for (const productRecord of productRecords.all()) {
// const existsInPrintful = printfulProducts
// .some(printfulProduct => printfulProduct.sync_product.id === productRecord.printfulProductId);
// if (!existsInPrintful) {
// await Webflow.deleteProductUsingPrintful(productRecord.printfulProductId);
// }
// }
return ClientResponse.json({});
}
},
"/webhook/printful": { "/webhook/printful": {
OPTIONS() { OPTIONS() {
return new ClientResponse(undefined, { status: 204 }); return new ClientResponse(undefined, { status: 204 });
}, },
async POST(req) { async POST(req) {
const payload: Printful.Webhook.EventPayload = await req.json() const payload: Printful.Webhook.EventPayload = await req.json()
console.log(JSON.stringify(payload));
console.log("PRINTFUL WEBHOOK: " + payload.type);
try { try {
switch (payload.type) { switch (payload.type) {
// ADD/UPDATE // ADD/UPDATE
case Printful.Webhook.Event.ProductUpdated: { case Printful.Webhook.Event.ProductUpdated: {
const printfulProduct = await Printful.getSyncProduct(payload.data.sync_product.id); const printfulProduct = await Printful.Products.getSyncProduct(payload.data.sync_product.id);
const productRecord = productRecords.findFromPrintful(payload.data.sync_product.id); const productRecord = productRecords.findUsingPrintful(payload.data.sync_product.id);
if (productRecord) { if (productRecord) {
await Webflow.updateProductFromPrintful(printfulProduct); await Webflow.updateProductUsingPrintful(printfulProduct);
} }
else { else {
await Webflow.createProductFromPrintful(printfulProduct); await Webflow.createProductUsingPrintful(printfulProduct);
} }
break; break;
} }
// DELETE // DELETE
case Printful.Webhook.Event.ProductDeleted: { case Printful.Webhook.Event.ProductDeleted: {
await Webflow.deleteProductFromPrintful(payload.data.sync_product.id); await Webflow.deleteProductUsingPrintful(payload.data.sync_product.id);
break; break;
} }
} }