Fix Supabase credentials error - use API routes

- Create /api/experiments endpoint for server-side Supabase access
- Update experiments service to use fetch instead of direct Supabase client
- Fixes 'Supabase credentials not configured' error in browser

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Christian Vidal Wolf
2026-02-21 00:21:57 +01:00
co-authored by Qwen-Coder
parent 2b62bef707
commit edbde19f3a
2 changed files with 179 additions and 73 deletions
+112
View File
@@ -0,0 +1,112 @@
import type { VercelRequest, VercelResponse } from '@vercel/node';
import { createClient } from '@supabase/supabase-js';
import { Experiment, ExperimentCreateInput } from '../types';
const supabase = createClient(
process.env.SUPABASE_URL || '',
process.env.SUPABASE_SERVICE_KEY || ''
);
export default async function handler(req: VercelRequest, res: VercelResponse) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') return res.status(200).end();
try {
// GET - List experiments or get single experiment
if (req.method === 'GET') {
const { id, status, type, marketplace, asin, dateFrom, dateTo } = req.query;
let query = supabase.from('experiments').select('*');
if (id) {
const { data, error } = await query.eq('id', id).single();
if (error) throw error;
return res.status(200).json(data);
}
// List with filters
if (status) {
const statuses = Array.isArray(status) ? status : [status];
query = query.in('status', statuses);
}
if (type) {
const types = Array.isArray(type) ? type : [type];
query = query.in('type', types);
}
if (marketplace) {
const markets = Array.isArray(marketplace) ? marketplace : [marketplace];
query = query.in('marketplace', markets);
}
if (asin) {
query = query.contains('asins', [asin]);
}
if (dateFrom) {
query = query.gte('start_date', dateFrom as string);
}
if (dateTo) {
query = query.lte('end_date', dateTo as string);
}
const { data, error } = await query.order('created_at', { ascending: false });
if (error) throw error;
return res.status(200).json(data || []);
}
// POST - Create experiment
if (req.method === 'POST') {
const input: ExperimentCreateInput = req.body;
const { data, error } = await supabase
.from('experiments')
.insert([{
...input,
status: 'planned',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}])
.select()
.single();
if (error) throw error;
return res.status(201).json(data);
}
// PUT - Update experiment
if (req.method === 'PUT') {
const { id } = req.query;
const updates = req.body;
const { data, error } = await supabase
.from('experiments')
.update({ ...updates, updated_at: new Date().toISOString() })
.eq('id', id)
.select()
.single();
if (error) throw error;
return res.status(200).json(data);
}
// DELETE - Delete experiment
if (req.method === 'DELETE') {
const { id } = req.query;
const { error } = await supabase
.from('experiments')
.delete()
.eq('id', id);
if (error) throw error;
return res.status(200).json({ success: true });
}
return res.status(405).json({ error: 'Method not allowed' });
} catch (error: any) {
console.error('Experiments API error:', error);
return res.status(500).json({ error: error.message || 'Internal server error' });
}
}
+64 -70
View File
@@ -1,4 +1,3 @@
import { createClient } from '@supabase/supabase-js';
import { import {
Experiment, Experiment,
ExperimentCreateInput, ExperimentCreateInput,
@@ -9,75 +8,60 @@ import {
} from '../types'; } from '../types';
import { SalesRecord } from '../types'; import { SalesRecord } from '../types';
const supabaseUrl = process.env.SUPABASE_URL || ''; const API_BASE = '/api/experiments';
const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY || '';
const getSupabase = () => {
if (!supabaseUrl || !supabaseServiceKey) {
throw new Error('Supabase credentials not configured');
}
return createClient(supabaseUrl, supabaseServiceKey);
};
// ============ CRUD Operations ============ // ============ CRUD Operations ============
export const createExperiment = async (input: ExperimentCreateInput): Promise<Experiment> => { export const createExperiment = async (input: ExperimentCreateInput): Promise<Experiment> => {
const supabase = getSupabase(); const response = await fetch(API_BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
const { data, error } = await supabase if (!response.ok) {
.from('experiments') const error = await response.json();
.insert([{ throw new Error(error.error || 'Failed to create experiment');
...input, }
status: 'planned',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}])
.select()
.single();
if (error) throw error; return response.json();
return data as Experiment;
}; };
export const updateExperiment = async ( export const updateExperiment = async (
id: string, id: string,
updates: Partial<Experiment> updates: Partial<Experiment>
): Promise<Experiment> => { ): Promise<Experiment> => {
const supabase = getSupabase(); const response = await fetch(`${API_BASE}?id=${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
const { data, error } = await supabase if (!response.ok) {
.from('experiments') const error = await response.json();
.update({ ...updates, updated_at: new Date().toISOString() }) throw new Error(error.error || 'Failed to update experiment');
.eq('id', id) }
.select()
.single();
if (error) throw error; return response.json();
return data as Experiment;
}; };
export const deleteExperiment = async (id: string): Promise<void> => { export const deleteExperiment = async (id: string): Promise<void> => {
const supabase = getSupabase(); const response = await fetch(`${API_BASE}?id=${id}`, {
method: 'DELETE',
});
const { error } = await supabase if (!response.ok) {
.from('experiments') const error = await response.json();
.delete() throw new Error(error.error || 'Failed to delete experiment');
.eq('id', id); }
if (error) throw error;
}; };
export const getExperiment = async (id: string): Promise<Experiment | null> => { export const getExperiment = async (id: string): Promise<Experiment | null> => {
const supabase = getSupabase(); const response = await fetch(`${API_BASE}?id=${id}`);
const { data, error } = await supabase if (!response.ok) return null;
.from('experiments')
.select('*')
.eq('id', id)
.single();
if (error) return null; return response.json();
return data as Experiment;
}; };
export const listExperiments = async ( export const listExperiments = async (
@@ -90,35 +74,38 @@ export const listExperiments = async (
dateTo?: string; dateTo?: string;
} }
): Promise<ExperimentListItem[]> => { ): Promise<ExperimentListItem[]> => {
const supabase = getSupabase(); const params = new URLSearchParams();
let query = supabase.from('experiments').select('*');
if (filters?.status?.length) { if (filters?.status?.length) {
query = query.in('status', filters.status); filters.status.forEach(s => params.append('status', s));
} }
if (filters?.type?.length) { if (filters?.type?.length) {
query = query.in('type', filters.type); filters.type.forEach(t => params.append('type', t));
} }
if (filters?.marketplace?.length) { if (filters?.marketplace?.length) {
query = query.in('marketplace', filters.marketplace); filters.marketplace.forEach(m => params.append('marketplace', m));
} }
if (filters?.asin) { if (filters?.asin) {
query = query.contains('asins', [filters.asin]); params.append('asin', filters.asin);
} }
if (filters?.dateFrom) { if (filters?.dateFrom) {
query = query.gte('start_date', filters.dateFrom); params.append('dateFrom', filters.dateFrom);
} }
if (filters?.dateTo) { if (filters?.dateTo) {
query = query.lte('end_date', filters.dateTo); params.append('dateTo', filters.dateTo);
} }
const { data, error } = await query.order('created_at', { ascending: false }); const response = await fetch(`${API_BASE}?${params.toString()}`);
if (error) throw error; if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to list experiments');
}
const data = await response.json();
// Transform to ExperimentListItem // Transform to ExperimentListItem
return (data || []).map((exp: any) => { return data.map((exp: any) => {
const today = new Date(); const today = new Date();
const startDate = new Date(exp.start_date); const startDate = new Date(exp.start_date);
const endDate = exp.end_date ? new Date(exp.end_date) : null; const endDate = exp.end_date ? new Date(exp.end_date) : null;
@@ -149,24 +136,31 @@ export const listExperiments = async (
export const getActiveExperimentsForASINs = async ( export const getActiveExperimentsForASINs = async (
asins: string[] asins: string[]
): Promise<Map<string, ActiveExperiment[]>> => { ): Promise<Map<string, ActiveExperiment[]>> => {
const supabase = getSupabase(); const params = new URLSearchParams();
asins.forEach(asin => params.append('asin', asin));
const response = await fetch(`${API_BASE}?${params.toString()}&status=active`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to fetch active experiments');
}
const data = await response.json();
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
const { data, error } = await supabase // Filter experiments that are currently active
.from('experiments') const activeExperiments = data.filter((exp: any) =>
.select('*') exp.status === 'active' &&
.eq('status', 'active') exp.start_date <= today &&
.lte('start_date', today) (!exp.end_date || exp.end_date >= today)
.or(`end_date.is.null,end_date.gte.${today}`); );
if (error) throw error;
const map = new Map<string, ActiveExperiment[]>(); const map = new Map<string, ActiveExperiment[]>();
for (const asin of asins) { for (const asin of asins) {
const experiments = (data || []) const experiments = activeExperiments
.filter((exp: any) => Array.isArray(exp.asins) && exp.asins.includes(asin)) .filter((exp: any) => exp.asins?.includes(asin))
.map((exp: any) => { .map((exp: any) => {
const endDate = exp.end_date ? new Date(exp.end_date) : null; const endDate = exp.end_date ? new Date(exp.end_date) : null;
const daysRemaining = endDate const daysRemaining = endDate