Product sync
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
PRINTFUL_AUTH=RGI4WiWeQkrEYek3obwxZL6c424LAJsUimDX1bHF
|
|
||||||
PRINTFUL_STORE_ID=15619272
|
|
||||||
|
|
||||||
WEBFLOW_AUTH=115ded1e97162f5d4835bcb87e236fe4bce1ad602f519bca8179394092a8c6f6
|
|
||||||
WEBFLOW_SITE_ID=6737d2c9511d414861f1726e
|
|
||||||
WEBFLOW_COLLECTION_ID=675a3fbe966e58ea9aa7110f
|
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env.*
|
.env
|
||||||
database/data.db
|
database/data.db
|
||||||
|
|||||||
+37
-15
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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`, {
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ primary_region = 'ord'
|
|||||||
|
|
||||||
[[mounts]]
|
[[mounts]]
|
||||||
source = "data"
|
source = "data"
|
||||||
destination = "/data"
|
destination = "/data"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user