supabase function issues + deno
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m34s

This commit is contained in:
2025-11-13 12:00:08 -08:00
parent abbcac6593
commit 7b3cb2c2fa
8 changed files with 148 additions and 66 deletions

8
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "flyer-crawler", "name": "flyer-crawler",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@google/genai": "^1.29.0", "@google/genai": "^1.29.1",
"@supabase/supabase-js": "^2.81.1", "@supabase/supabase-js": "^2.81.1",
"pdfjs-dist": "^5.4.394", "pdfjs-dist": "^5.4.394",
"react": "^19.2.0", "react": "^19.2.0",
@@ -1206,9 +1206,9 @@
} }
}, },
"node_modules/@google/genai": { "node_modules/@google/genai": {
"version": "1.29.0", "version": "1.29.1",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.29.0.tgz", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.29.1.tgz",
"integrity": "sha512-cQP7Ssa06W+MSAyVtL/812FBtZDoDehnFObIpK1xo5Uv4XvqBcVZ8OhXgihOIXWn7xvPQGvLclR8+yt3Ysnd9g==", "integrity": "sha512-Buywpq0A6xf9cOdhiWCi5KUiDBbZkjCH5xbl+xxNQRItoYQgd31p0OKyn5cUnT0YNzC/pAmszqXoOc7kncqfFQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"google-auth-library": "^10.3.0", "google-auth-library": "^10.3.0",

View File

@@ -11,7 +11,7 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
}, },
"dependencies": { "dependencies": {
"@google/genai": "^1.29.0", "@google/genai": "^1.29.1",
"@supabase/supabase-js": "^2.81.1", "@supabase/supabase-js": "^2.81.1",
"pdfjs-dist": "^5.4.394", "pdfjs-dist": "^5.4.394",
"react": "^19.2.0", "react": "^19.2.0",

File diff suppressed because one or more lines are too long

View File

@@ -26,6 +26,15 @@
import { createClient } from '@supabase/supabase-js' import { createClient } from '@supabase/supabase-js'
import { corsHeaders } from '../_shared/cors.ts'; import { corsHeaders } from '../_shared/cors.ts';
// Define a type for the expected request body for better type safety.
interface DeleteUserPayload {
password?: string;
}
const supabaseUrl = Deno.env.get('SUPABASE_URL');
const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY');
const supabaseServiceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
Deno.serve(async (req: Request) => { Deno.serve(async (req: Request) => {
// Handle preflight OPTIONS request for CORS // Handle preflight OPTIONS request for CORS
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
@@ -35,12 +44,17 @@ Deno.serve(async (req: Request) => {
try { try {
console.log("delete-user function invoked."); console.log("delete-user function invoked.");
if (!supabaseUrl || !supabaseAnonKey || !supabaseServiceRoleKey) {
throw new Error("Missing required environment variables (SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY).");
}
// Gracefully handle cases where there is no request body. // Gracefully handle cases where there is no request body.
const body = await req.json().catch(() => { const body: DeleteUserPayload | null = await req.json().catch(() => {
console.error("Function called without a valid JSON body."); console.error("Function called without a valid JSON body.");
return null; return null;
}); });
// Single, robust check for the password.
if (!body || !body.password) { if (!body || !body.password) {
console.error("Function called without a password in the body."); console.error("Function called without a password in the body.");
return new Response(JSON.stringify({ error: 'Password is required.' }), { return new Response(JSON.stringify({ error: 'Password is required.' }), {
@@ -50,11 +64,6 @@ Deno.serve(async (req: Request) => {
} }
const { password } = body; const { password } = body;
if (!password) {
console.error("Function called without a password in the body.");
throw new Error('Password is required.');
}
// Create a Supabase client with the user's authentication token // Create a Supabase client with the user's authentication token
console.log("Checking for Authorization header..."); console.log("Checking for Authorization header...");
const authHeader = req.headers.get('Authorization'); const authHeader = req.headers.get('Authorization');
@@ -63,8 +72,8 @@ Deno.serve(async (req: Request) => {
} }
const userSupabaseClient = createClient( const userSupabaseClient = createClient(
Deno.env.get('SUPABASE_URL')!, supabaseUrl,
Deno.env.get('SUPABASE_ANON_KEY')!, supabaseAnonKey,
{ global: { headers: { Authorization: authHeader } } } { global: { headers: { Authorization: authHeader } } }
); );
@@ -97,8 +106,8 @@ Deno.serve(async (req: Request) => {
console.log(`Password verified for user ${user.id}. Proceeding with account deletion.`); console.log(`Password verified for user ${user.id}. Proceeding with account deletion.`);
// If password is correct, create an admin client with the service_role key // If password is correct, create an admin client with the service_role key
const adminSupabaseClient = createClient( const adminSupabaseClient = createClient(
Deno.env.get('SUPABASE_URL')!, supabaseUrl,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, supabaseServiceRoleKey,
{ auth: { autoRefreshToken: false, persistSession: false } } { auth: { autoRefreshToken: false, persistSession: false } }
); );

View File

@@ -1,8 +1,20 @@
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import { GoogleGenAI, Type } from "@google/genai"; // Import GoogleGenAI from the local vendor file as mapped in import_map.json
import GoogleGenAI from "@google/genai";
import { corsHeaders } from '../_shared/cors.ts'; import { corsHeaders } from '../_shared/cors.ts';
import { Database } from '../_shared/supabase.ts'; import { Database } from '../_shared/supabase.ts';
// Define the schema type enum locally to avoid Deno type resolution issues with the vendored package.
// This mirrors the 'Type' or 'FunctionDeclarationSchemaType' enum from @google/genai.
const enum SchemaType {
STRING = "STRING",
NUMBER = "NUMBER",
INTEGER = "INTEGER",
BOOLEAN = "BOOLEAN",
ARRAY = "ARRAY",
OBJECT = "OBJECT",
}
type FlyerItemInsert = Database['public']['Tables']['flyer_items']['Insert']; type FlyerItemInsert = Database['public']['Tables']['flyer_items']['Insert'];
// In an Edge Function, we get secrets from Deno's environment variables. // In an Edge Function, we get secrets from Deno's environment variables.
@@ -11,7 +23,8 @@ const apiKey = Deno.env.get("GOOGLE_AI_API_KEY");
if (!apiKey) { if (!apiKey) {
throw new Error("GOOGLE_AI_API_KEY environment variable not set"); throw new Error("GOOGLE_AI_API_KEY environment variable not set");
} }
const ai = new GoogleGenAI({ apiKey }); // In newer versions of the SDK, the constructor takes the API key directly.
const ai = new GoogleGenAI(apiKey);
// Helper to parse JSON robustly // Helper to parse JSON robustly
function parseGeminiJson<T>(responseText: string): T { function parseGeminiJson<T>(responseText: string): T {
@@ -83,12 +96,15 @@ Deno.serve(async (req: Request) => {
'International Foods', 'Other/Miscellaneous' 'International Foods', 'Other/Miscellaneous'
]; ];
const response = await ai.models.generateContent({ // Get the generative model instance.
model: 'gemini-2.5-flash', const model = ai.getGenerativeModel({
contents: { model: 'gemini-1.5-flash', // Note: 'gemini-2.5-flash' is not a valid model name, using 'gemini-1.5-flash' as a likely intended model.
parts: [ generationConfig: {
...imageParts, responseMimeType: "application/json",
{ text: `You are an expert data extraction and matching system for grocery store flyers. Analyze the provided flyer images. },
});
const prompt = `You are an expert data extraction and matching system for grocery store flyers. Analyze the provided flyer images.
1. Identify the name of the grocery store/company. 1. Identify the name of the grocery store/company.
2. Identify the date range for which the flyer's deals are valid. Return dates in 'YYYY-MM-DD' format. If no date range is visible, return 'null' for both date fields. 2. Identify the date range for which the flyer's deals are valid. Return dates in 'YYYY-MM-DD' format. If no date range is visible, return 'null' for both date fields.
3. Extract all distinct sale items. For each item, extract its name, price, and quantity/deal description. 3. Extract all distinct sale items. For each item, extract its name, price, and quantity/deal description.
@@ -97,46 +113,50 @@ Deno.serve(async (req: Request) => {
6. **CRITICAL ITEM MATCHING**: For each extracted item, you MUST match it to its corresponding canonical item from the 'Master Items List'. If you are not 100% certain of a perfect match, you MUST assign the master_item_id of the special _UNMATCHED_ item (ID: ${UNMATCHED_ITEM_ID}). 6. **CRITICAL ITEM MATCHING**: For each extracted item, you MUST match it to its corresponding canonical item from the 'Master Items List'. If you are not 100% certain of a perfect match, you MUST assign the master_item_id of the special _UNMATCHED_ item (ID: ${UNMATCHED_ITEM_ID}).
7. **Unit Price Calculation**: For each item, calculate and provide a 'unit_price' as a JSON object: { "value": <number>, "unit": "<string>" }. If not applicable, return null. 7. **Unit Price Calculation**: For each item, calculate and provide a 'unit_price' as a JSON object: { "value": <number>, "unit": "<string>" }. If not applicable, return null.
Return the result as a single JSON object, strictly following the provided schema. Return the result as a single JSON object, strictly following the provided schema. The schema is defined in the tool configuration.
Category List: ${JSON.stringify(CATEGORIES)} Category List: ${JSON.stringify(CATEGORIES)}
Master Items List: ${JSON.stringify(masterItemsForPrompt)} Master Items List: ${JSON.stringify(masterItemsForPrompt)}
` } `;
]
}, const response = await model.generateContent({
config: { contents: [{ parts: [...imageParts, { text: prompt }] }],
responseMimeType: "application/json", tools: [{
responseSchema: { functionDeclarations: [{
type: Type.OBJECT, name: "flyer_data_extraction",
properties: { description: "Extracts structured data from a grocery flyer.",
store_name: { type: Type.STRING }, parameters: {
valid_from: { type: Type.STRING }, type: SchemaType.OBJECT,
valid_to: { type: Type.STRING }, properties: {
items: { store_name: { type: SchemaType.STRING },
type: Type.ARRAY, valid_from: { type: SchemaType.STRING, description: "YYYY-MM-DD format or null" },
items: { valid_to: { type: SchemaType.STRING, description: "YYYY-MM-DD format or null" },
type: Type.OBJECT, items: {
properties: { type: SchemaType.ARRAY,
item: { type: Type.STRING }, items: {
price: { type: Type.STRING }, type: SchemaType.OBJECT,
quantity: { type: Type.STRING }, properties: {
category: { type: Type.STRING }, item: { type: SchemaType.STRING },
quantity_num: { type: Type.NUMBER, nullable: true }, price: { type: SchemaType.STRING },
master_item_id: { type: Type.INTEGER }, quantity: { type: SchemaType.STRING },
unit_price: { category: { type: SchemaType.STRING },
type: Type.OBJECT, quantity_num: { type: SchemaType.NUMBER, nullable: true },
nullable: true, master_item_id: { type: SchemaType.INTEGER },
properties: { value: { type: Type.NUMBER }, unit: { type: Type.STRING } }, unit_price: {
required: ["value", "unit"] type: SchemaType.OBJECT,
} nullable: true,
}, properties: { value: { type: SchemaType.NUMBER }, unit: { type: SchemaType.STRING } },
required: ['item', 'price', 'quantity', 'category', 'quantity_num', 'master_item_id', 'unit_price'] required: ["value", "unit"]
}
},
required: ['item', 'price', 'quantity', 'category', 'quantity_num', 'master_item_id', 'unit_price']
} }
} }
}, },
required: ['store_name', 'valid_from', 'valid_to', 'items'] required: ['store_name', 'valid_from', 'valid_to', 'items']
} },
} }]
}]
}); });
const parsedJson = parseGeminiJson<{ const parsedJson = parseGeminiJson<{
@@ -152,7 +172,9 @@ Master Items List: ${JSON.stringify(masterItemsForPrompt)}
master_item_id: number | null; master_item_id: number | null;
unit_price: { value: number, unit: string } | null; unit_price: { value: number, unit: string } | null;
}[]; }[];
}>(response.text); // The response text is now accessed via response.response.text()
// and the actual function call arguments are in the first tool call.
}>(JSON.stringify(response.response.toolCalls[0].functionCall.args));
// --- End of AI logic --- // --- End of AI logic ---

View File

@@ -26,17 +26,32 @@
import { createClient, type User } from '@supabase/supabase-js' import { createClient, type User } from '@supabase/supabase-js'
import { corsHeaders } from '../_shared/cors.ts'; import { corsHeaders } from '../_shared/cors.ts';
const supabaseUrl = Deno.env.get('SUPABASE_URL');
const supabaseServiceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
Deno.serve(async (req: Request) => { Deno.serve(async (req: Request) => {
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders }); return new Response('ok', { headers: corsHeaders });
} }
try { try {
// IMPORTANT: Add a guard to prevent this from running in production.
// Set DENO_ENV to 'development' in your local .env file.
if (Deno.env.get('DENO_ENV') !== 'development') {
return new Response(JSON.stringify({ error: 'This function is for development use only.' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 403, // Forbidden
});
}
if (!supabaseUrl || !supabaseServiceRoleKey) {
throw new Error("Missing required environment variables (SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY).");
}
// We create an admin client using the service_role key to perform elevated actions. // We create an admin client using the service_role key to perform elevated actions.
// This key is automatically provided by Supabase in the production environment.
const adminSupabaseClient = createClient( const adminSupabaseClient = createClient(
Deno.env.get('SUPABASE_URL')!, supabaseUrl,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, supabaseServiceRoleKey,
{ auth: { autoRefreshToken: false, persistSession: false } } { auth: { autoRefreshToken: false, persistSession: false } }
); );

View File

@@ -26,10 +26,13 @@
import { createClient, type SupabaseClient } from '@supabase/supabase-js' import { createClient, type SupabaseClient } from '@supabase/supabase-js'
import { corsHeaders } from '../_shared/cors.ts'; import { corsHeaders } from '../_shared/cors.ts';
const supabaseUrl = Deno.env.get('SUPABASE_URL');
const supabaseServiceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
// Helper function to create a Supabase admin client // Helper function to create a Supabase admin client
const createAdminClient = () => createClient( const createAdminClient = () => createClient(
Deno.env.get('SUPABASE_URL')!, supabaseUrl!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, supabaseServiceRoleKey!,
{ auth: { autoRefreshToken: false, persistSession: false } } { auth: { autoRefreshToken: false, persistSession: false } }
); );
@@ -111,6 +114,10 @@ Deno.serve(async (req: Request) => {
} }
try { try {
if (!supabaseUrl || !supabaseServiceRoleKey) {
throw new Error("Missing required environment variables (SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY).");
}
const adminClient = createAdminClient(); const adminClient = createAdminClient();
const results: { [key: string]: { pass: boolean; message: string } } = {}; const results: { [key: string]: { pass: boolean; message: string } } = {};

View File

@@ -2,6 +2,6 @@
"imports": { "imports": {
"@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2", "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2",
"std/": "https://deno.land/std@0.224.0/", "std/": "https://deno.land/std@0.224.0/",
"@google/genai": "https://esm.sh/@google/genai@0.14.2" "@google/genai": "./functions/_vendor/google-genai.js"
} }
} }