Add database schema
This commit is contained in:
@@ -1 +1,7 @@
|
||||
project_id = "hhwrnnerlwmstyqhmtrr"
|
||||
project_id = "hhwrnnerlwmstyqhmtrr"
|
||||
|
||||
[functions.check-prices]
|
||||
verify_jwt = false
|
||||
|
||||
[functions.send-discord-alert]
|
||||
verify_jwt = false
|
||||
175
supabase/functions/check-prices/index.ts
Normal file
175
supabase/functions/check-prices/index.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
console.log('Starting price check...');
|
||||
|
||||
// Get all enabled alert profiles
|
||||
const { data: alertProfiles, error: profilesError } = await supabase
|
||||
.from('alert_profiles')
|
||||
.select('*')
|
||||
.eq('enabled', true);
|
||||
|
||||
if (profilesError) {
|
||||
throw profilesError;
|
||||
}
|
||||
|
||||
console.log(`Found ${alertProfiles?.length || 0} active alert profiles`);
|
||||
|
||||
// Get all products
|
||||
const { data: products, error: productsError } = await supabase
|
||||
.from('products')
|
||||
.select('*');
|
||||
|
||||
if (productsError) {
|
||||
throw productsError;
|
||||
}
|
||||
|
||||
console.log(`Found ${products?.length || 0} products`);
|
||||
|
||||
const alerts = [];
|
||||
|
||||
// Check each alert profile against products
|
||||
for (const profile of alertProfiles || []) {
|
||||
for (const product of products || []) {
|
||||
// Match category
|
||||
if (product.category !== profile.category) continue;
|
||||
|
||||
// Check if product meets specification requirements
|
||||
const specsFilter = profile.specs_filter;
|
||||
const productSpecs = product.specs;
|
||||
|
||||
let meetsRequirements = true;
|
||||
|
||||
// Check minimum size (for harddrives/ram)
|
||||
if (specsFilter.min_size) {
|
||||
const minSizeValue = parseFloat(specsFilter.min_size);
|
||||
const productSizeValue = parseFloat(productSpecs.size || '0');
|
||||
if (productSizeValue < minSizeValue) {
|
||||
meetsRequirements = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check type (SSD/HDD/etc)
|
||||
if (specsFilter.type && productSpecs.type !== specsFilter.type) {
|
||||
meetsRequirements = false;
|
||||
}
|
||||
|
||||
// Check if price is below target
|
||||
if (!meetsRequirements || product.current_price > profile.target_price) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a match! Log the alert
|
||||
const message = `🎯 Alert: ${profile.name}\n` +
|
||||
`Product: ${product.name}\n` +
|
||||
`Price: ${product.current_price} ${product.currency}\n` +
|
||||
`Target: ${profile.target_price} ${product.currency}\n` +
|
||||
`Store: ${product.store}\n` +
|
||||
`URL: ${product.url}`;
|
||||
|
||||
const { error: logError } = await supabase
|
||||
.from('alerts_log')
|
||||
.insert({
|
||||
alert_profile_id: profile.id,
|
||||
product_id: product.id,
|
||||
triggered_price: product.current_price,
|
||||
message,
|
||||
sent_to_discord: false
|
||||
});
|
||||
|
||||
if (logError) {
|
||||
console.error('Error logging alert:', logError);
|
||||
}
|
||||
|
||||
alerts.push({
|
||||
profile,
|
||||
product,
|
||||
message
|
||||
});
|
||||
|
||||
// Send Discord notification if webhook URL is set
|
||||
if (profile.discord_webhook_url) {
|
||||
try {
|
||||
await fetch(profile.discord_webhook_url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: message,
|
||||
embeds: [{
|
||||
title: '💰 Price Alert!',
|
||||
description: message,
|
||||
color: 0x00ff00,
|
||||
fields: [
|
||||
{
|
||||
name: 'Current Price',
|
||||
value: `${product.current_price} ${product.currency}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Target Price',
|
||||
value: `${profile.target_price} ${product.currency}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Savings',
|
||||
value: `${(profile.target_price - product.current_price).toFixed(2)} ${product.currency}`,
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
timestamp: new Date().toISOString()
|
||||
}]
|
||||
})
|
||||
});
|
||||
|
||||
// Update log to mark as sent
|
||||
await supabase
|
||||
.from('alerts_log')
|
||||
.update({ sent_to_discord: true })
|
||||
.eq('alert_profile_id', profile.id)
|
||||
.eq('product_id', product.id);
|
||||
|
||||
console.log(`Sent Discord notification for ${product.name}`);
|
||||
} catch (discordError) {
|
||||
console.error('Error sending Discord notification:', discordError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Price check complete. Found ${alerts.length} matching alerts`);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
alertsTriggered: alerts.length,
|
||||
alerts
|
||||
}),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in check-prices function:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
return new Response(
|
||||
JSON.stringify({ error: errorMessage }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
83
supabase/functions/send-discord-alert/index.ts
Normal file
83
supabase/functions/send-discord-alert/index.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { webhookUrl, message, product, profile } = await req.json();
|
||||
|
||||
if (!webhookUrl) {
|
||||
throw new Error('Discord webhook URL is required');
|
||||
}
|
||||
|
||||
const embed = {
|
||||
title: '💰 Price Alert Triggered!',
|
||||
description: message,
|
||||
color: 0x00ff00,
|
||||
fields: [
|
||||
{
|
||||
name: 'Product',
|
||||
value: product.name,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'Current Price',
|
||||
value: `${product.current_price} ${product.currency}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Target Price',
|
||||
value: `${profile.target_price} ${product.currency}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Store',
|
||||
value: product.store,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Link',
|
||||
value: product.url,
|
||||
inline: false
|
||||
}
|
||||
],
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: `🎯 **${profile.name}** - Price target reached!`,
|
||||
embeds: [embed]
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Discord API error: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log('Discord notification sent successfully');
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error sending Discord alert:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
return new Response(
|
||||
JSON.stringify({ error: errorMessage }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
-- Create products table
|
||||
CREATE TABLE IF NOT EXISTS public.products (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
store TEXT NOT NULL,
|
||||
category TEXT NOT NULL, -- 'harddrive', 'ram', 'processor', 'graphics_card'
|
||||
current_price DECIMAL(10,2) NOT NULL,
|
||||
currency TEXT DEFAULT 'DKK',
|
||||
specs JSONB NOT NULL, -- Store size, brand, speed, etc.
|
||||
last_checked TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create alert profiles table (specification-based, not product-specific)
|
||||
CREATE TABLE IF NOT EXISTS public.alert_profiles (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
target_price DECIMAL(10,2) NOT NULL,
|
||||
specs_filter JSONB NOT NULL, -- e.g., {"min_size": "6TB", "type": "SSD"}
|
||||
discord_webhook_url TEXT,
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create price history table
|
||||
CREATE TABLE IF NOT EXISTS public.price_history (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
product_id UUID REFERENCES public.products(id) ON DELETE CASCADE,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
checked_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create alerts log table
|
||||
CREATE TABLE IF NOT EXISTS public.alerts_log (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
alert_profile_id UUID REFERENCES public.alert_profiles(id) ON DELETE CASCADE,
|
||||
product_id UUID REFERENCES public.products(id) ON DELETE CASCADE,
|
||||
triggered_price DECIMAL(10,2) NOT NULL,
|
||||
message TEXT,
|
||||
sent_to_discord BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- Enable Row Level Security (but make everything public since no login)
|
||||
ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.alert_profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.price_history ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.alerts_log ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create public access policies (no authentication required)
|
||||
CREATE POLICY "Public read access for products" ON public.products FOR SELECT USING (true);
|
||||
CREATE POLICY "Public insert access for products" ON public.products FOR INSERT WITH CHECK (true);
|
||||
CREATE POLICY "Public update access for products" ON public.products FOR UPDATE USING (true);
|
||||
CREATE POLICY "Public delete access for products" ON public.products FOR DELETE USING (true);
|
||||
|
||||
CREATE POLICY "Public read access for alert_profiles" ON public.alert_profiles FOR SELECT USING (true);
|
||||
CREATE POLICY "Public insert access for alert_profiles" ON public.alert_profiles FOR INSERT WITH CHECK (true);
|
||||
CREATE POLICY "Public update access for alert_profiles" ON public.alert_profiles FOR UPDATE USING (true);
|
||||
CREATE POLICY "Public delete access for alert_profiles" ON public.alert_profiles FOR DELETE USING (true);
|
||||
|
||||
CREATE POLICY "Public read access for price_history" ON public.price_history FOR SELECT USING (true);
|
||||
CREATE POLICY "Public insert access for price_history" ON public.price_history FOR INSERT WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "Public read access for alerts_log" ON public.alerts_log FOR SELECT USING (true);
|
||||
CREATE POLICY "Public insert access for alerts_log" ON public.alerts_log FOR INSERT WITH CHECK (true);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_products_category ON public.products(category);
|
||||
CREATE INDEX idx_products_last_checked ON public.products(last_checked);
|
||||
CREATE INDEX idx_alert_profiles_category ON public.alert_profiles(category);
|
||||
CREATE INDEX idx_alert_profiles_enabled ON public.alert_profiles(enabled);
|
||||
CREATE INDEX idx_price_history_product ON public.price_history(product_id);
|
||||
CREATE INDEX idx_alerts_log_profile ON public.alerts_log(alert_profile_id);
|
||||
Reference in New Issue
Block a user