Spaces:
Running
on
Zero
Running
on
Zero
File size: 10,556 Bytes
18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 416c2e2 05035ed 416c2e2 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 7c6a236 18b9531 7c6a236 18b9531 dbe3108 18b9531 7c6a236 18b9531 9f411df 18b9531 7c6a236 9f411df 18b9531 ffc5e7c b24c2ae ffc5e7c b24c2ae ffc5e7c b24c2ae ffc5e7c 18b9531 f565e50 18b9531 f565e50 18b9531 7c6a236 18b9531 7c6a236 18b9531 dbe3108 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 dbe3108 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c 18b9531 ffc5e7c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
-- Portfolio Intelligence Platform Database Schema
-- For Supabase PostgreSQL with Auth Integration
-- Enable UUID extension (if not already enabled)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Helper function to convert strings to UUIDs (for demo portfolio IDs)
CREATE OR REPLACE FUNCTION string_to_portfolio_uuid(input_string TEXT)
RETURNS UUID
LANGUAGE plpgsql
IMMUTABLE
SECURITY INVOKER
SET search_path = ''
AS $$
BEGIN
-- Use URL namespace UUID constant (6ba7b811-9dad-11d1-80b4-00c04fd430c8)
-- uuid_ns_url() is a constant in PostgreSQL, not a function
RETURN public.uuid_generate_v5('6ba7b811-9dad-11d1-80b4-00c04fd430c8'::uuid, input_string);
END;
$$;
-- Grant execute to authenticated and anon roles
GRANT EXECUTE ON FUNCTION string_to_portfolio_uuid TO authenticated, anon;
-- Users table (linked to Supabase Auth - CRITICAL FIX)
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100),
is_active BOOLEAN DEFAULT true,
is_demo BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_id ON users(id);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- Portfolios table
CREATE TABLE IF NOT EXISTS portfolios (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
description TEXT,
risk_tolerance VARCHAR(20) DEFAULT 'moderate',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_portfolios_user_id ON portfolios(user_id);
CREATE INDEX IF NOT EXISTS idx_portfolios_created_at ON portfolios(created_at DESC);
-- Portfolio inputs table (for auto-save history - keeps last 3 per user)
CREATE TABLE IF NOT EXISTS portfolio_inputs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
description TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_portfolio_inputs_user_created ON portfolio_inputs(user_id, created_at DESC);
-- Trigger function to limit portfolio inputs to 3 per user
CREATE OR REPLACE FUNCTION limit_portfolio_inputs_per_user()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = ''
AS $$
BEGIN
-- Keep only the 3 most recent entries per user
DELETE FROM public.portfolio_inputs
WHERE id IN (
SELECT id
FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC
) AS row_num
FROM public.portfolio_inputs
WHERE user_id = NEW.user_id
) ranked
WHERE row_num > 3
);
RETURN NULL;
END;
$$;
-- Create trigger for automatic cleanup
DROP TRIGGER IF EXISTS trigger_limit_portfolio_inputs ON portfolio_inputs;
CREATE TRIGGER trigger_limit_portfolio_inputs
AFTER INSERT ON portfolio_inputs
FOR EACH ROW EXECUTE FUNCTION limit_portfolio_inputs_per_user();
-- Portfolio holdings table
CREATE TABLE IF NOT EXISTS portfolio_holdings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE,
ticker VARCHAR(20) NOT NULL,
quantity NUMERIC(20, 8) NOT NULL,
cost_basis NUMERIC(20, 2),
asset_type VARCHAR(20) DEFAULT 'stock',
added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_holdings_portfolio_id ON portfolio_holdings(portfolio_id);
CREATE INDEX IF NOT EXISTS idx_holdings_ticker ON portfolio_holdings(ticker);
-- Portfolio analyses table
CREATE TABLE IF NOT EXISTS portfolio_analyses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE,
analysis_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Analysis results (JSONB for flexibility)
holdings_snapshot JSONB NOT NULL,
market_data JSONB,
risk_metrics JSONB,
optimisation_results JSONB,
ai_synthesis TEXT,
recommendations JSONB,
-- Metadata
reasoning_steps JSONB,
mcp_calls JSONB,
sentiment_data JSONB,
execution_time_ms INTEGER,
model_version VARCHAR(50),
-- Computed fields
total_value NUMERIC(20, 2),
health_score INTEGER CHECK (health_score >= 0 AND health_score <= 10),
risk_level VARCHAR(20),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_analyses_portfolio_id ON portfolio_analyses(portfolio_id);
CREATE INDEX IF NOT EXISTS idx_analyses_date ON portfolio_analyses(analysis_date DESC);
CREATE INDEX IF NOT EXISTS idx_analyses_health_score ON portfolio_analyses(health_score);
CREATE INDEX IF NOT EXISTS idx_analyses_sentiment_data ON portfolio_analyses USING GIN (sentiment_data);
-- Function to automatically create user profile on signup (CRITICAL FOR AUTH)
-- Uses SECURITY DEFINER with empty search_path to bypass RLS on INSERT
-- Fully qualifies table name (public.users) to work with empty search_path
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER SET search_path = ''
AS $$
BEGIN
INSERT INTO public.users (id, email, username, is_active, is_demo)
VALUES (
NEW.id,
NEW.email,
COALESCE(
NEW.raw_user_meta_data ->> 'username',
split_part(NEW.email, '@', 1)
),
true,
false
);
RETURN NEW;
EXCEPTION
WHEN OTHERS THEN
-- Log error but don't block signup
RAISE WARNING 'Failed to create user profile: %', SQLERRM;
RETURN NEW;
END;
$$;
-- Create trigger for automatic profile creation
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = ''
AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;
-- Triggers for updated_at
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_portfolios_updated_at ON portfolios;
CREATE TRIGGER update_portfolios_updated_at BEFORE UPDATE ON portfolios
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Grant permissions (Supabase uses RLS - Row Level Security)
-- These are basic permissions; adjust based on your auth setup
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolios ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolio_inputs ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolio_holdings ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolio_analyses ENABLE ROW LEVEL SECURITY;
-- RLS policies with performance optimizations (wrapped auth.uid() = 20x faster)
-- Users table policies
DROP POLICY IF EXISTS users_read_own ON users;
CREATE POLICY users_read_own ON users
FOR SELECT
TO authenticated
USING ((SELECT auth.uid()) = id);
DROP POLICY IF EXISTS users_update_own ON users;
CREATE POLICY users_update_own ON users
FOR UPDATE
TO authenticated
USING ((SELECT auth.uid()) = id)
WITH CHECK ((SELECT auth.uid()) = id);
DROP POLICY IF EXISTS users_insert_own ON users;
CREATE POLICY users_insert_own ON users
FOR INSERT
TO authenticated
WITH CHECK ((SELECT auth.uid()) = id);
-- Portfolios table policies
DROP POLICY IF EXISTS portfolios_read_own ON portfolios;
CREATE POLICY portfolios_read_own ON portfolios
FOR SELECT
TO authenticated
USING ((SELECT auth.uid()) = user_id);
DROP POLICY IF EXISTS portfolios_insert_own ON portfolios;
CREATE POLICY portfolios_insert_own ON portfolios
FOR INSERT
TO authenticated
WITH CHECK ((SELECT auth.uid()) = user_id);
DROP POLICY IF EXISTS portfolios_update_own ON portfolios;
CREATE POLICY portfolios_update_own ON portfolios
FOR UPDATE
TO authenticated
USING ((SELECT auth.uid()) = user_id)
WITH CHECK ((SELECT auth.uid()) = user_id);
DROP POLICY IF EXISTS portfolios_delete_own ON portfolios;
CREATE POLICY portfolios_delete_own ON portfolios
FOR DELETE
TO authenticated
USING ((SELECT auth.uid()) = user_id);
-- Portfolio inputs table policies
DROP POLICY IF EXISTS portfolio_inputs_read_own ON portfolio_inputs;
CREATE POLICY portfolio_inputs_read_own ON portfolio_inputs
FOR SELECT
TO authenticated
USING ((SELECT auth.uid()) = user_id);
DROP POLICY IF EXISTS portfolio_inputs_insert_own ON portfolio_inputs;
CREATE POLICY portfolio_inputs_insert_own ON portfolio_inputs
FOR INSERT
TO authenticated
WITH CHECK ((SELECT auth.uid()) = user_id);
DROP POLICY IF EXISTS portfolio_inputs_delete_own ON portfolio_inputs;
CREATE POLICY portfolio_inputs_delete_own ON portfolio_inputs
FOR DELETE
TO authenticated
USING ((SELECT auth.uid()) = user_id);
-- Holdings policies (optimized subquery pattern)
DROP POLICY IF EXISTS holdings_access_own ON portfolio_holdings;
CREATE POLICY holdings_access_own ON portfolio_holdings
FOR ALL
TO authenticated
USING (
portfolio_id IN (
SELECT id FROM portfolios
WHERE user_id = (SELECT auth.uid())
)
)
WITH CHECK (
portfolio_id IN (
SELECT id FROM portfolios
WHERE user_id = (SELECT auth.uid())
)
);
-- Analyses policies
DROP POLICY IF EXISTS analyses_access_own ON portfolio_analyses;
CREATE POLICY analyses_access_own ON portfolio_analyses
FOR ALL
TO authenticated
USING (
portfolio_id IN (
SELECT id FROM portfolios
WHERE user_id = (SELECT auth.uid())
)
)
WITH CHECK (
portfolio_id IN (
SELECT id FROM portfolios
WHERE user_id = (SELECT auth.uid())
)
);
-- Grant permissions
GRANT USAGE ON SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL TABLES IN SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated;
|