diff --git a/.gitignore b/.gitignore index eedd89b..282fabf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ api +node_modules/ +.env.* +./database/data.db diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..af2db4f --- /dev/null +++ b/bun.lock @@ -0,0 +1,19 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "devDependencies": { + "@types/bun": "^1.2.11", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.11", "", { "dependencies": { "bun-types": "1.2.11" } }, "sha512-ZLbbI91EmmGwlWTRWuV6J19IUiUC5YQ3TCEuSHI3usIP75kuoA8/0PVF+LTrbEnVc8JIhpElWOxv1ocI1fJBbw=="], + + "@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="], + + "bun-types": ["bun-types@1.2.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-dbkp5Lo8HDrXkLrONm6bk+yiiYQSntvFUzQp0v3pzTAsXk6FtgVMjdQ+lzFNVAmQFUkPQZ3WMZqH5tTo+Dp/IA=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/database/data.db b/database/data.db new file mode 100644 index 0000000..db8db7d Binary files /dev/null and b/database/data.db differ diff --git a/database/seed.sql b/database/seed.sql new file mode 100644 index 0000000..92e8498 --- /dev/null +++ b/database/seed.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS products ( + webflowProductId INTEGER NOT NULL, + printfulProductId INTEGER NOT NULL, + UNIQUE(webflowProductId, printfulProductId) +) diff --git a/main.ts b/main.ts index b179ee9..c0486f7 100644 --- a/main.ts +++ b/main.ts @@ -1 +1,33 @@ -console.log("Hello world"); +import { Printful } from "./printful.ts" +import { ProductRecords } from "./storage.ts" + +const productRecords = new ProductRecords(); + +const server = Bun.serve({ + routes: { + "/webhook/printful": { + async POST(req) { + const payload: Printful.Webhook.EventPayload = await req.json() + + switch (payload.type) { + case Printful.Webhook.Event.ProductUpdated: + const productRecord = productRecords.findFromPrintful(payload.data.sync_product.id) + if (productRecord) { + + } + else { + + } + break; + case Printful.Webhook.Event.ProductDeleted: + break; + } + + return Response.json("Hello world") + } + } + }, + development: true +}) + +console.log(`Listening on ${server.url}`); diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f5b278 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": {}, + "devDependencies": { + "@types/bun": "^1.2.11" + }, + "scripts": { + "db:seed": "sqlite3 ./database/data.db < ./database/seed.sql" + } +} diff --git a/printful.ts b/printful.ts new file mode 100644 index 0000000..2127e60 --- /dev/null +++ b/printful.ts @@ -0,0 +1,122 @@ +export namespace Printful { + const API_ROUTE = "https://api.printful.com/store"; + + export namespace Webhook { + // meta + interface MetaData { + created: number; + retries: number; + store: number; + } + + export enum Event { + ProductUpdated = "product_updated", + ProductDeleted = "product_deleted" + } + + // product updated + export interface ProductUpdated extends MetaData { + type: Event.ProductUpdated, + data: { + sync_product: { + id: number; + external_id: string; + name: string; + variants: number; + synced: number; + thumbnail_url: string; + is_ignored: boolean; + }; + } + } + + // product deleted + export interface ProductDeleted extends MetaData { + type: Event.ProductDeleted, + data: { + sync_product: { + id: number; + external_id: string; + name: string; + }; + } + } + + export type EventPayload = ProductUpdated | ProductDeleted + } + + export namespace Products { + interface MetaData { + code: number; + result: T; + } + + interface Option { + id: string; + value: string; + } + + interface File { + type: string; + id: number; + url: string; + options: Option[]; + hash: string; + filename: string; + mime_type: string; + size: number; + width: number; + height: number; + dpi: number; + status: string; + created: number; + thumbnail_url: string; + preview_url: string; + visible: boolean; + is_temporary: boolean; + stitch_count_tier: string; + } + + interface SyncProductData { + id: number; + external_id: string; + sync_product_id: number; + name: string; + synced: boolean; + variant_id: number; + retail_price: string; + currency: string; + is_ignored: boolean; + sku: string; + product: { + variant_id: number; + product_id: number; + image: string; + name: string; + }; + files: File[]; + options: Option[]; + main_category_id: number; + warehouse_product_id: number; + warehouse_product_variant_id: number; + size: string; + color: string; + availability_status: string; + } + export type SyncProduct = MetaData; + } + + export async function getSyncProduct(syncProductId: String): Promise { + const payload: Products.SyncProduct = await ( + await fetch(`${API_ROUTE}/products/${syncProductId}`, { + method: "GET", + headers: { + "Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`, + "X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}` + } + }) + ).json(); + + return payload; + } +} diff --git a/storage.ts b/storage.ts new file mode 100644 index 0000000..9452b16 --- /dev/null +++ b/storage.ts @@ -0,0 +1,42 @@ +import { Database } from "bun:sqlite"; + +interface ProductsRecord { + webflowProductId: number; + printfulProductId: number; +} + +export class ProductRecords { + private db: Database; + + constructor() { + this.db = new Database("./database/data.db"); + } + + findFromWebflow(webflowProductId: number): ProductsRecord | undefined { + const row = this.db.query(` + SELECT * FROM product_records WHERE webflowProductId = ? + `).get(webflowProductId); + + return row as ProductsRecord | undefined; + } + + findFromPrintful(printfulProductId: number): ProductsRecord | undefined { + const row = this.db.query(` + SELECT * FROM product_records WHERE printfulProductId = ? + `).get(printfulProductId); + + return row as ProductsRecord | undefined; + } + + add(record: ProductsRecord): void { + this.db.run(` + INSERT OR IGNORE INTO product_records (webflowProductId, printfulProductId) + VALUES (?, ?) + `, [record.webflowProductId, record.printfulProductId]); + } + + all(): ProductsRecord[] { + return this.db.query(`SELECT * FROM product_records`).all() as ProductsRecord[]; + } +} +