supabase function issues + deno
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m34s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m34s
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
29
supabase/functions/_vendor/google-genai.js
Normal file
29
supabase/functions/_vendor/google-genai.js
Normal file
File diff suppressed because one or more lines are too long
@@ -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 } }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 ---
|
||||||
|
|
||||||
|
|||||||
@@ -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 } }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 } } = {};
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user