From 521e3f552f39ceb28732fb1e550f0143da3f54a3 Mon Sep 17 00:00:00 2001 From: Pantry Lead Agent Date: Tue, 24 Feb 2026 00:02:53 +0000 Subject: [PATCH] feat: add product-lookup Edge Function (#24) - Fetch product data from Open Food Facts API - Cache results in products table - Handle product not found gracefully - CORS enabled for frontend access - Returns cached data for performance Closes #24 --- supabase/functions/product-lookup/README.md | 83 ++++++++++++ supabase/functions/product-lookup/index.ts | 140 ++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 supabase/functions/product-lookup/README.md create mode 100644 supabase/functions/product-lookup/index.ts diff --git a/supabase/functions/product-lookup/README.md b/supabase/functions/product-lookup/README.md new file mode 100644 index 0000000..abe44af --- /dev/null +++ b/supabase/functions/product-lookup/README.md @@ -0,0 +1,83 @@ +# Product Lookup Edge Function + +Fetches product data from Open Food Facts API by barcode and caches results in the database. + +## Endpoint + +`POST /functions/v1/product-lookup` + +## Request + +```json +{ + "barcode": "8000500310427" +} +``` + +## Response + +### Success (200) + +```json +{ + "barcode": "8000500310427", + "name": "Nutella", + "brand": "Ferrero", + "quantity": "750g", + "image_url": "https://...", + "category": "spreads", + "cached": false +} +``` + +### Not Found (404) + +```json +{ + "barcode": "1234567890123", + "name": "Unknown Product (1234567890123)", + "cached": false +} +``` + +### Error (500) + +```json +{ + "error": "Error message", + "barcode": null, + "name": null +} +``` + +## Features + +- ✅ Queries Open Food Facts API +- ✅ Caches results in `products` table +- ✅ Returns cached data for subsequent requests +- ✅ Handles product not found gracefully +- ✅ CORS enabled for frontend access + +## Environment Variables + +- `SUPABASE_URL`: Auto-injected by Supabase +- `SUPABASE_SERVICE_ROLE_KEY`: Auto-injected by Supabase + +## Testing + +```bash +# Local (with Supabase CLI) +supabase functions serve product-lookup + +# Test request +curl -X POST http://localhost:54321/functions/v1/product-lookup \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_ANON_KEY" \ + -d '{"barcode":"8000500310427"}' +``` + +## Deployment + +```bash +supabase functions deploy product-lookup +``` diff --git a/supabase/functions/product-lookup/index.ts b/supabase/functions/product-lookup/index.ts new file mode 100644 index 0000000..3d04b1c --- /dev/null +++ b/supabase/functions/product-lookup/index.ts @@ -0,0 +1,140 @@ +// Product Lookup Edge Function +// Fetches product data from Open Food Facts API by barcode + +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +interface ProductData { + barcode: string + name: string + brand?: string + quantity?: string + image_url?: string + category?: string + cached?: boolean +} + +serve(async (req) => { + // Handle CORS preflight + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + const { barcode } = await req.json() + + if (!barcode) { + throw new Error('Barcode is required') + } + + // Initialize Supabase client + const supabaseUrl = Deno.env.get('SUPABASE_URL')! + const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! + const supabase = createClient(supabaseUrl, supabaseKey) + + // Check cache first (products table can store known products) + const { data: cachedProduct } = await supabase + .from('products') + .select('*') + .eq('barcode', barcode) + .single() + + if (cachedProduct) { + console.log(`Cache HIT for barcode: ${barcode}`) + return new Response( + JSON.stringify({ + barcode: cachedProduct.barcode, + name: cachedProduct.name, + brand: cachedProduct.brand, + quantity: cachedProduct.quantity, + image_url: cachedProduct.image_url, + category: cachedProduct.category, + cached: true, + } as ProductData), + { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ) + } + + console.log(`Cache MISS for barcode: ${barcode}, fetching from Open Food Facts...`) + + // Fetch from Open Food Facts + const offResponse = await fetch( + `https://world.openfoodfacts.org/api/v2/product/${barcode}.json`, + { + headers: { + 'User-Agent': 'Pantry/1.0 (https://github.com/pantry-app/pantry)', + }, + } + ) + + if (!offResponse.ok) { + throw new Error(`Open Food Facts API error: ${offResponse.status}`) + } + + const offData = await offResponse.json() + + if (offData.status !== 1 || !offData.product) { + // Product not found in Open Food Facts + return new Response( + JSON.stringify({ + barcode, + name: `Unknown Product (${barcode})`, + cached: false, + } as ProductData), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 404 + } + ) + } + + const product = offData.product + + // Extract relevant data + const productData: ProductData = { + barcode, + name: product.product_name || product.generic_name || `Product ${barcode}`, + brand: product.brands || undefined, + quantity: product.quantity || undefined, + image_url: product.image_url || product.image_front_url || undefined, + category: product.categories || undefined, + cached: false, + } + + // Cache the product in our database (upsert) + await supabase.from('products').upsert({ + barcode: productData.barcode, + name: productData.name, + brand: productData.brand, + quantity: productData.quantity, + image_url: productData.image_url, + category: productData.category, + }, { onConflict: 'barcode' }) + + console.log(`Successfully fetched and cached product: ${productData.name}`) + + return new Response( + JSON.stringify(productData), + { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ) + + } catch (error) { + console.error('Error in product-lookup:', error) + return new Response( + JSON.stringify({ + error: error instanceof Error ? error.message : 'Unknown error', + barcode: null, + name: null, + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500 + } + ) + } +}) -- 2.49.1