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/
.env.*
.env
database/data.db
+37 -15
View File
@@ -46,11 +46,16 @@ export namespace Printful {
}
export namespace Products {
interface MetaData<T = any> {
export interface MetaDataSingle<T = any> {
code: number;
result: T;
}
export interface MetaDataMulti<T = any> {
code: number;
result: T[];
}
interface Option {
id: string;
value: string;
@@ -77,7 +82,7 @@ export namespace Printful {
stitch_count_tier: string;
}
interface SyncProductData {
export interface SyncProduct {
sync_product: {
id: number;
external_id: string;
@@ -115,20 +120,37 @@ export namespace Printful {
}[]
}
export type SyncProduct = MetaData<SyncProductData>;
}
export async function getSyncProduct(syncProductId: number): Promise<Products.SyncProduct> {
const payload: Products.SyncProduct = await (
await fetch(`${Printful.API_URL}/store/products/${syncProductId}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
}
})
).json();
export async function getSyncProduct(syncProductId: number): Promise<SyncProduct> {
const payload: MetaDataSingle<SyncProduct> = await (
await fetch(`${Printful.API_URL}/store/products/${syncProductId}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
}
})
).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);
}
findFromWebflow(webflowProductId: number): ProductRecord | undefined {
findUsingWebflow(webflowProductId: number): ProductRecord | undefined {
const row = this.db.query(`
SELECT * FROM product_records WHERE webflowProductId = ?
`).get(webflowProductId);
@@ -24,7 +24,7 @@ class ProductRecords {
return row as ProductRecord | undefined;
}
findFromPrintful(printfulProductId: number): ProductRecord | undefined {
findUsingPrintful(printfulProductId: number): ProductRecord | undefined {
const row = this.db.query(`
SELECT * FROM product_records WHERE printfulProductId = ?
`).get(printfulProductId);
@@ -32,13 +32,13 @@ class ProductRecords {
return row as ProductRecord | undefined;
}
deleteFromWebflow(webflowProductId: number) {
deleteUsingWebflow(webflowProductId: number) {
this.db.run(`
DELETE FROM product_records WHERE webflowProductId = ?
`, [webflowProductId]);
}
deleteFromPrintful(printfulProductId: number) {
deleteUsingPrintful(printfulProductId: number) {
this.db.run(`
DELETE FROM product_records WHERE printfulProductId = ?
`, [printfulProductId]);
+15 -16
View File
@@ -26,7 +26,7 @@ export namespace Webflow {
unit: string;
currency: string;
};
"main-image": string;
"main-image"?: string;
}
}
@@ -52,8 +52,8 @@ export namespace Webflow {
}
}
export async function deleteProductFromPrintful(printfulProductId: number) {
const webflowProductId = productRecords.findFromPrintful(printfulProductId)?.webflowProductId;
export async function deleteProductUsingPrintful(printfulProductId: number) {
const webflowProductId = productRecords.findUsingPrintful(printfulProductId)?.webflowProductId;
if (webflowProductId) {
const res = await fetch(`${Webflow.API_COLLECTIONS_URL}/items/${webflowProductId}`, {
method: "DELETE",
@@ -63,17 +63,17 @@ export namespace Webflow {
},
body: JSON.stringify({})
});
productRecords.deleteFromWebflow(webflowProductId);
productRecords.deleteUsingWebflow(webflowProductId);
}
}
export async function updateProductFromPrintful(printfulProduct: Printful.Products.SyncProduct) {
const webflowProductId = productRecords.findFromPrintful(printfulProduct.result.sync_product.id)?.webflowProductId;
export async function updateProductUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const webflowProductId = productRecords.findUsingPrintful(printfulProduct.sync_product.id)?.webflowProductId;
if (!webflowProductId) {
throw new Error("Product is not recognized");
}
const printfulVariants = printfulProduct.result.sync_variants;
const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) {
webflowVariants.push({
@@ -107,8 +107,8 @@ export namespace Webflow {
body: JSON.stringify({
"product": {
"fieldData": {
"name": printfulProduct.result.sync_product.name,
"slug": formatSlug(printfulProduct.result.sync_product.name),
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"sku-properties": [
{
"id": "color",
@@ -157,12 +157,11 @@ export namespace Webflow {
"sku": webflowVariants[i]
})
});
console.log(JSON.stringify((await res.json())));
}
}
export async function createProductFromPrintful(printfulProduct: Printful.Products.SyncProduct) {
const printfulVariants = printfulProduct.result.sync_variants;
export async function createProductUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) {
@@ -180,7 +179,7 @@ export namespace Webflow {
"currency": printfulVariant.currency
},
// 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({
"product": {
"fieldData": {
"name": printfulProduct.result.sync_product.name,
"slug": formatSlug(printfulProduct.result.sync_product.name),
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"sku-properties": [
{
"id": "color",
@@ -232,7 +231,7 @@ export namespace Webflow {
}
const webflowProductId = addProductResponse["product"]["id"];
const printfulProductId = printfulProduct.result.sync_product.id;
const printfulProductId = printfulProduct.sync_product.id;
// CREATE WEBFLOW PRODUCT SKUs
let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, {
+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": {
OPTIONS() {
return new ClientResponse(undefined, { status: 204 });
},
async POST(req) {
const payload: Printful.Webhook.EventPayload = await req.json()
console.log(JSON.stringify(payload));
console.log("PRINTFUL WEBHOOK: " + payload.type);
try {
switch (payload.type) {
// ADD/UPDATE
case Printful.Webhook.Event.ProductUpdated: {
const printfulProduct = await Printful.getSyncProduct(payload.data.sync_product.id);
const productRecord = productRecords.findFromPrintful(payload.data.sync_product.id);
const printfulProduct = await Printful.Products.getSyncProduct(payload.data.sync_product.id);
const productRecord = productRecords.findUsingPrintful(payload.data.sync_product.id);
if (productRecord) {
await Webflow.updateProductFromPrintful(printfulProduct);
await Webflow.updateProductUsingPrintful(printfulProduct);
}
else {
await Webflow.createProductFromPrintful(printfulProduct);
await Webflow.createProductUsingPrintful(printfulProduct);
}
break;
}
// DELETE
case Printful.Webhook.Event.ProductDeleted: {
await Webflow.deleteProductFromPrintful(payload.data.sync_product.id);
await Webflow.deleteProductUsingPrintful(payload.data.sync_product.id);
break;
}
}