diff options
| author | akiyamn | 2023-10-04 23:29:19 +1100 |
|---|---|---|
| committer | akiyamn | 2023-10-04 23:29:19 +1100 |
| commit | a2d1c179f88e056265321b426a51907a3941044a (patch) | |
| tree | 3804227ee072265a1cf1121cc59eb1fbdc74a3e2 | |
| parent | 4e87195739f2a5d9a05451b48773c8afdc680765 (diff) | |
| download | price-tracker-worker-master.tar.gz price-tracker-worker-master.zip | |
Kind of worksmaster
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | schema.sql | 9 | ||||
| -rw-r--r-- | src/cron.ts | 56 | ||||
| -rw-r--r-- | src/gather.ts | 111 | ||||
| -rw-r--r-- | src/index.ts | 49 | ||||
| -rw-r--r-- | src/store.ts | 10 | ||||
| -rw-r--r-- | wrangler.toml | 7 |
7 files changed, 239 insertions, 7 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3da6d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.wrangler +.editorconfig +package-lock.json +node_modules
\ No newline at end of file diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..9416d33 --- /dev/null +++ b/schema.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS Prices; +CREATE TABLE IF NOT EXISTS Prices ( + Id INTEGER PRIMARY KEY NOT NULL, + CompanyName VARCHAR(32) NOT NULL, + ProductName TEXT NOT NULL, + Price REAL NOT NULL, + AccessDate TEXT NOT NULL, + Picture TEXT +); diff --git a/src/cron.ts b/src/cron.ts new file mode 100644 index 0000000..fb016a3 --- /dev/null +++ b/src/cron.ts @@ -0,0 +1,56 @@ +import { fetchColesProduct, fetchWooliesProduct, ProductInfo } from "./gather" +import { storePriceRecord } from "./store" + +export interface ProductRequest { + productId: string, + companyName: string +} + +export async function cacheAllPrices(db: D1Database, products: ProductRequest[]): Promise<boolean> { + // return getAllPrices(products).then().map(storePriceRecord) + return (await getAllPrices(products)) + .map(x => {console.log("getting prices", x); return x}) + .filter(x => x != null) + .map(product => storePriceRecord(db, product as ProductInfo)) + .every(async x => await x === true) + + // for (let p of products) { + // console.log(p) + // const prices = await getPrice(p) + // console.warn(prices) + // } + + // let out = [] + // for (let price of prices) { + // if (price !== null) { + // const result = await storePriceRecord(db, price) + // console.log(price, result) + // out.push(result) + // } + // } + // return out.every(x => !!x) + // return true + + +} + +async function getAllPrices(products: ProductRequest[]): Promise<(ProductInfo | null)[]> { + return await Promise.all(products.map(getPrice)) + // let out = [] + // for (let p of products) { + // out.push(await getPrice(p)) + // } + // return out + // return [await getPrice(products[0])] +} + +async function getPrice(product: ProductRequest): Promise<ProductInfo | null> { + switch (product.companyName) { + case "Coles": + return await fetchColesProduct(product.productId) + case "Woolies": + return await fetchWooliesProduct(product.productId) + default: + throw Error("Invalid store name") + } +}
\ No newline at end of file diff --git a/src/gather.ts b/src/gather.ts new file mode 100644 index 0000000..7252418 --- /dev/null +++ b/src/gather.ts @@ -0,0 +1,111 @@ + +export interface ProductInfo { + productId: string, + companyName: string, + productName: string, + price: number, + accessDate: string, + picture: string | null + +} + +interface WooliesResponse { + Product: { + Stockcode: number, + DisplayName: string, + Price: number, + LargeImageFile: string + } +} + +interface ColesResponse { + pricing: { + now: number, + } + id: number, + name: string, + imageUris: [{ + uri: string + }] +} + +// const COLES_ROOT_URL = "apigw.coles.com.au" +// const WOOLIES_ROOT_URL = "www.woolworths.com.au" + +const COLES_ROOT_URL = "localhost:3002" +const WOOLIES_ROOT_URL = "localhost:3002" + + + +export async function fetchWooliesProduct(productID: string): Promise<ProductInfo | null> { + const options = { + method: 'GET', + // credentials: 'include', + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0', + "Cookie": '_abck=8D926507FE8BB3E2030100E0A40908AD~-1~YAAQDV8wF4yrp+WKAQAAcBa1+QqOLH6Cr1PPGhoBK6dzldNoO6fT9E5Yh2JWjug69wmscVVDCxTl3AtqhLO700yB1l/aMekoSEMfXWdB1vAkiaNMKVPNjc+x4hh7Yon6NLceF4NYulFEKqcjo1BEgGtnYYv8xgJW0jcO0i4kIdNAZaszCrXxefAMTskVsvWcZvmAuqcLr72EmDY6BNBSCQgzm2KX4uAM7D79DQVKY5TDR79ITzKmCZghdYGdNrXtSxksNB8rllTWUT+5UD45fpB13jYdxmwy6Lu51gBNuYrQ/lmQuJdFIzc3czVYvQI64uDlWbA17Swz/9En3cCF0/IGtuAUx/6PfJVfAdFyfZrcD3d7Sl+vdzlhiZ3smo/1/ocITzZggDurPckyVMvy5DPmZDx1mwvkn83yOt0cP7c=~-1~-1~-1;' + } + + + }; + + const url = `http://${WOOLIES_ROOT_URL}/apis/ui/product/detail/${productID}?isMobile=false&useVariant=false` + const response = fetch(url, options) + // .json() + // .then(response => console.log(response)) + .then(response => response.json()) + .then(x => {console.log(url, x); return x}) + .then(data => data as WooliesResponse) + .then(formatWooliesProduct) + // .catch(err => console.error(err)); + + return response +} + +export async function fetchColesProduct(productID: string): Promise<ProductInfo | null> { + const options = { + method: 'GET', + headers: { + 'Ocp-Apim-Subscription-Key': 'dd6ae58532d743978508555a59a199ac', + 'User-Agent': 'okhttp/4.10.0' + } + }; + + const url = `https://${COLES_ROOT_URL}/digital/colesappbff/v3/api/2/products/${productID}?type=sku&storeId=697&shoppingMethod=clickAndCollect&includeLiquor=true` + // const url = `http://localhost:3002/apis/ui/product/detail/${productID}?isMobile=false&useVariant=false` + const response = fetch(url, options) + // .json() + // .then(response => console.log(response)) + .then(x => {console.log(x); return x}) + .then(response => response.json()) + .then(data => data as WooliesResponse) + .then(formatWooliesProduct) + // .catch(err => console.error(err)); + + return response +} + + +function formatWooliesProduct(json: WooliesResponse): ProductInfo { + const data = json.Product + return { + "productId": String(data.Stockcode), + "companyName": "Woolies", + "productName": data.DisplayName, + "price": data.Price, + "accessDate": String(Date.now()), + "picture": data.LargeImageFile + } +} + + +function formatColesProduct(json: ColesResponse): ProductInfo { + return { + "productId": String(json.id), + "companyName": "Coles", + "productName": json.name, + "price": json.pricing.now, + "accessDate": String(Date.now()), + "picture": json.imageUris[0]?.uri + } +}
\ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 674339b..3008e35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { cacheAllPrices, ProductRequest } from "./cron"; /** * Welcome to Cloudflare Workers! * @@ -29,10 +30,27 @@ export interface Env { // MY_QUEUE: Queue; // // Example binding to a D1 Database. Learn more at https://developers.cloudflare.com/workers/platform/bindings/#d1-database-bindings - // DB: D1Database + DB: D1Database } export default { + + async fetch(request: Request, env: Env, ctx: ExecutionContext) { + const { pathname } = new URL(request.url); + switch (pathname) { + case "/api/history": + const { results } = await env.DB.prepare( + "SELECT * FROM Prices" + ) + .all(); + return Response.json(results); + case "/": + return new Response('🀄'); + default: + return new Response("404 💩", { status: 404 }) + } + }, + // The scheduled handler is invoked at the interval set in our wrangler.toml's // [[triggers]] configuration. async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> { @@ -40,11 +58,30 @@ export default { // publish to a Queue, query a D1 Database, and much more. // // We'll keep it simple and make an API call to a Cloudflare API: - let resp = await fetch('https://api.cloudflare.com/client/v4/ips'); - let wasSuccessful = resp.ok ? 'success' : 'fail'; + // let resp = await fetch('https://api.cloudflare.com/client/v4/ips'); + // let wasSuccessful = resp.ok ? 'success' : 'fail'; + + + + const products : ProductRequest[] = [ + {productId: "888140", companyName: "Woolies"}, + // {productId: "8150288", companyName: "Coles"}, + {productId: "581170", companyName: "Woolies"}, + // {productId: "4901345", companyName: "Coles"}, + // {productId: "165367", companyName: "Woolies"}, + // {productId: "5178633", companyName: "Coles"}, + // {productId: "165367", companyName: "Woolies"}, + // {productId: "5178633", companyName: "Coles"}, + // {productId: "778171", companyName: "Woolies"}, + // {productId: "2271188", companyName: "Coles"}, + // {productId: "829281", companyName: "Woolies"}, + // {productId: "2236788", companyName: "Coles"}, + ] + const success = await cacheAllPrices(env.DB, products) + console.log(`Fetching and caching ${products.length} products. Success: ${success}`) - // You could store this result in KV, write to a D1 Database, or publish to a Queue. - // In this template, we'll just log the result: - console.log(`trigger fired at ${event.cron}: ${wasSuccessful}`); + // // You could store this result in KV, write to a D1 Database, or publish to a Queue. + // // In this template, we'll just log the result: + // console.log(`trigger fired at ${event.cron}: ${ok}`); }, }; diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..9e98254 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,10 @@ +import { ProductInfo } from "./gather"; + +export async function storePriceRecord(db: D1Database, product: ProductInfo): Promise<boolean> { + const query = 'INSERT INTO Prices (CompanyName, ProductName, Price, AccessDate, Picture) VALUES (?1, ?2, ?3, ?4, ?5)' + const { results, success } = await db.prepare(query) + .bind(product.companyName, product.productName, product.price, product.accessDate, product.picture) + .run(); + console.log(results, success) + return success +}
\ No newline at end of file diff --git a/wrangler.toml b/wrangler.toml index 0fd27a2..97df854 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,4 +6,9 @@ compatibility_date = "2023-05-15" # Docs: https://developers.cloudflare.com/workers/platform/triggers/cron-triggers/ # Configuration: https://developers.cloudflare.com/workers/wrangler/configuration/#triggers [triggers] -crons = ["* * * * *"] # * * * * * = run every minute +crons = ["0 4 * * Sun"] # * * * * * = run every minute + +[[d1_databases]] +binding = "DB" # i.e. available in your Worker on env.DB +database_name = "price-history" +database_id = "024c66c3-ff5f-48cb-b944-bc95fa053b1d" |
