bookwiz.io / supabase / migrations / 017_fix_search_path.sql
017_fix_search_path.sql
Raw
-- Fix search path to include pg_catalog for proper operator resolution
-- The previous search_path = public, pg_temp was too restrictive

-- 1. Fix cleanup_old_ai_operations function
CREATE OR REPLACE FUNCTION cleanup_old_ai_operations()
RETURNS INTEGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
DECLARE
    deleted_count INTEGER;
BEGIN
    -- Delete AI operations older than 30 days
    DELETE FROM ai_operations 
    WHERE created_at < NOW() - INTERVAL '30 days';
    
    GET DIAGNOSTICS deleted_count = ROW_COUNT;
    
    RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;

-- 2. Fix set_billing_month function
CREATE OR REPLACE FUNCTION set_billing_month()
RETURNS TRIGGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    NEW.billing_month := DATE_TRUNC('month', NEW.created_at)::DATE;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 3. Fix get_monthly_ai_usage function
CREATE OR REPLACE FUNCTION get_monthly_ai_usage(p_user_id UUID, p_tier TEXT DEFAULT NULL)
RETURNS TABLE (
    tier TEXT,
    usage_count BIGINT,
    total_tokens BIGINT,
    total_cost DECIMAL
) 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    RETURN QUERY
    SELECT 
        au.model_tier as tier,
        COUNT(*)::BIGINT as usage_count,
        COALESCE(SUM(au.total_tokens), 0)::BIGINT as total_tokens,
        COALESCE(SUM(au.cost_usd), 0)::DECIMAL as total_cost
    FROM ai_usage au
    WHERE au.user_id = p_user_id
        AND au.billing_month = DATE_TRUNC('month', NOW())
        AND au.success = true -- Only count successful requests
        AND (p_tier IS NULL OR au.model_tier = p_tier)
    GROUP BY au.model_tier;
END;
$$ LANGUAGE plpgsql;

-- 4. Fix get_user_usage_stats function
CREATE OR REPLACE FUNCTION get_user_usage_stats(p_user_id UUID)
RETURNS JSON 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
DECLARE
    result JSON;
BEGIN
    SELECT json_build_object(
        'current_month', json_build_object(
            'advanced', COALESCE((
                SELECT json_build_object(
                    'count', usage_count,
                    'tokens', total_tokens,
                    'cost', total_cost
                )
                FROM get_monthly_ai_usage(p_user_id, 'advanced')
                WHERE tier = 'advanced'
            ), json_build_object('count', 0, 'tokens', 0, 'cost', 0)),
            'auto', COALESCE((
                SELECT json_build_object(
                    'count', usage_count,
                    'tokens', total_tokens,
                    'cost', total_cost
                )
                FROM get_monthly_ai_usage(p_user_id, 'auto')
                WHERE tier = 'auto'
            ), json_build_object('count', 0, 'tokens', 0, 'cost', 0))
        ),
        'total_books', (
            SELECT COUNT(*) FROM books WHERE user_id = p_user_id
        ),
        'total_words', (
            SELECT COALESCE(SUM(word_count), 0) FROM books WHERE user_id = p_user_id
        )
    ) INTO result;
    
    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- 5. Fix can_user_make_request function
CREATE OR REPLACE FUNCTION can_user_make_request(
    p_user_id UUID, 
    p_tier TEXT, 
    p_limit INTEGER
) RETURNS JSON 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
DECLARE
    current_usage BIGINT;
    result JSON;
BEGIN
    -- Get current month usage for the tier
    SELECT COALESCE(usage_count, 0) INTO current_usage
    FROM get_monthly_ai_usage(p_user_id, p_tier)
    WHERE tier = p_tier;
    
    -- Return usage info and whether request can proceed
    SELECT json_build_object(
        'can_proceed', (current_usage < p_limit OR p_limit = 0),
        'current_usage', current_usage,
        'limit', p_limit,
        'remaining', GREATEST(0, p_limit - current_usage)
    ) INTO result;
    
    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- 6. Fix handle_new_user_email_preferences function
CREATE OR REPLACE FUNCTION handle_new_user_email_preferences()
RETURNS TRIGGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    INSERT INTO email_preferences (user_id)
    VALUES (NEW.id);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 7. Fix get_user_subscription function
CREATE OR REPLACE FUNCTION get_user_subscription(user_uuid UUID)
RETURNS TABLE (
    id UUID,
    status subscription_status,
    price_id TEXT,
    current_period_end TIMESTAMPTZ,
    cancel_at_period_end BOOLEAN
)
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    RETURN QUERY
    SELECT 
        s.id,
        s.status::subscription_status,
        s.price_id,
        s.current_period_end,
        s.cancel_at_period_end
    FROM subscriptions s
    WHERE s.user_id = user_uuid
    AND s.status IN ('active', 'trialing', 'past_due')
    ORDER BY s.created_at DESC
    LIMIT 1;
END;
$$ LANGUAGE plpgsql;

-- 8. Fix user_has_active_subscription function
CREATE OR REPLACE FUNCTION user_has_active_subscription(user_uuid UUID)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    RETURN EXISTS(
        SELECT 1 
        FROM subscriptions 
        WHERE user_id = user_uuid 
        AND status IN ('active', 'trialing')
        AND (current_period_end IS NULL OR current_period_end > NOW())
    );
END;
$$ LANGUAGE plpgsql;

-- 9. Fix get_current_usage_period function
CREATE OR REPLACE FUNCTION get_current_usage_period(user_uuid UUID)
RETURNS TABLE (
    period_start TIMESTAMPTZ,
    period_end TIMESTAMPTZ
)
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
DECLARE
    subscription_record RECORD;
    current_time TIMESTAMPTZ := NOW();
BEGIN
    -- Get the user's active subscription
    SELECT * INTO subscription_record
    FROM subscriptions s
    WHERE s.user_id = user_uuid 
    AND s.status = 'active'
    AND s.current_period_end > current_time
    ORDER BY s.created_at DESC
    LIMIT 1;
    
    IF subscription_record IS NOT NULL THEN
        -- Return subscription period
        RETURN QUERY 
        SELECT 
            subscription_record.current_period_start,
            subscription_record.current_period_end;
    ELSE
        -- Return current month for free tier
        RETURN QUERY
        SELECT 
            DATE_TRUNC('month', current_time),
            (DATE_TRUNC('month', current_time) + INTERVAL '1 month' - INTERVAL '1 second');
    END IF;
END;
$$ LANGUAGE plpgsql;

-- 10. Fix get_or_create_current_usage function
CREATE OR REPLACE FUNCTION get_or_create_current_usage(user_uuid UUID)
RETURNS user_usage 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
DECLARE
    usage_period RECORD;
    usage_record user_usage;
BEGIN
    -- Get current usage period
    SELECT * INTO usage_period FROM get_current_usage_period(user_uuid);
    
    -- Try to find existing usage record
    SELECT * INTO usage_record
    FROM user_usage uu
    WHERE uu.user_id = user_uuid
    AND uu.period_start = usage_period.period_start
    AND uu.period_end = usage_period.period_end;
    
    -- Create if doesn't exist
    IF usage_record IS NULL THEN
        INSERT INTO user_usage (
            user_id,
            period_start,
            period_end
        ) VALUES (
            user_uuid,
            usage_period.period_start,
            usage_period.period_end
        ) RETURNING * INTO usage_record;
    END IF;
    
    RETURN usage_record;
END;
$$ LANGUAGE plpgsql;

-- 11. Fix increment_usage function (THE KEY ONE CAUSING THE ERROR)
CREATE OR REPLACE FUNCTION increment_usage(
    user_uuid UUID,
    feature_name TEXT,
    increment_amount DECIMAL DEFAULT 1
)
RETURNS user_usage 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
DECLARE
    usage_record user_usage;
BEGIN
    -- Get or create current usage record
    SELECT * INTO usage_record FROM get_or_create_current_usage(user_uuid);
    
    -- Update the appropriate counter
    UPDATE user_usage 
    SET 
        advanced_prompts_used = CASE 
            WHEN feature_name = 'advanced_prompts' THEN advanced_prompts_used + increment_amount
            ELSE advanced_prompts_used 
        END,
        auto_prompts_used = CASE 
            WHEN feature_name = 'auto_prompts' THEN auto_prompts_used + increment_amount
            ELSE auto_prompts_used 
        END,
        storage_used_gb = CASE 
            WHEN feature_name = 'storage_gb' THEN storage_used_gb + increment_amount
            ELSE storage_used_gb 
        END,
        books_created = CASE 
            WHEN feature_name = 'books' THEN books_created + increment_amount
            ELSE books_created 
        END,
        custom_templates_used = CASE 
            WHEN feature_name = 'custom_templates' THEN custom_templates_used + increment_amount
            ELSE custom_templates_used 
        END,
        updated_at = NOW()
    WHERE id = usage_record.id
    RETURNING * INTO usage_record;
    
    -- Log the usage increment
    INSERT INTO usage_logs (user_id, feature_type, action, amount, metadata)
    VALUES (user_uuid, feature_name, 'increment', increment_amount, '{}'::jsonb);
    
    RETURN usage_record;
END;
$$ LANGUAGE plpgsql;

-- 12. Fix handle_new_user function
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    INSERT INTO public.profiles (
        id,
        full_name,
        avatar_url,
        email,
        created_at,
        updated_at
    ) VALUES (
        NEW.id,
        COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'),
        NEW.raw_user_meta_data->>'avatar_url',
        NEW.email,
        NOW(),
        NOW()
    );
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 13. Fix handle_user_update function
CREATE OR REPLACE FUNCTION public.handle_user_update()
RETURNS TRIGGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    -- Only update if the profile exists
    IF EXISTS (SELECT 1 FROM public.profiles WHERE id = NEW.id) THEN
        UPDATE public.profiles
        SET
            full_name = COALESCE(
                NEW.raw_user_meta_data->>'full_name',
                NEW.raw_user_meta_data->>'name',
                full_name
            ),
            avatar_url = COALESCE(
                NEW.raw_user_meta_data->>'avatar_url',
                avatar_url
            ),
            email = COALESCE(NEW.email, email),
            updated_at = NOW()
        WHERE id = NEW.id;
    ELSE
        -- If profile doesn't exist, create it
        INSERT INTO public.profiles (
            id,
            full_name,
            avatar_url,
            email,
            created_at,
            updated_at
        ) VALUES (
            NEW.id,
            COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'),
            NEW.raw_user_meta_data->>'avatar_url',
            NEW.email,
            NOW(),
            NOW()
        );
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 14. Fix update_updated_at_column function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 15. Fix calculate_book_word_count function
CREATE OR REPLACE FUNCTION calculate_book_word_count(book_uuid UUID)
RETURNS INTEGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
DECLARE
    total_words INTEGER := 0;
    item_record RECORD;
BEGIN
    -- Sum word counts from all text files in the book
    FOR item_record IN 
        SELECT content 
        FROM file_system_items 
        WHERE book_id = book_uuid 
        AND item_type = 'file'
        AND file_extension IN ('txt', 'md', 'markdown')
        AND content IS NOT NULL
    LOOP
        -- Simple word count: split by whitespace and count non-empty elements
        total_words := total_words + (
            SELECT array_length(
                string_to_array(
                    regexp_replace(item_record.content, '\s+', ' ', 'g'), 
                    ' '
                ), 
                1
            )
        );
    END LOOP;
    
    RETURN COALESCE(total_words, 0);
END;
$$ LANGUAGE plpgsql;

-- 16. Fix update_book_word_count function
CREATE OR REPLACE FUNCTION update_book_word_count()
RETURNS TRIGGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
DECLARE
    book_uuid UUID;
    new_word_count INTEGER;
BEGIN
    -- Get the book_id for the file that was modified
    IF TG_OP = 'DELETE' THEN
        book_uuid := OLD.book_id;
    ELSE
        book_uuid := NEW.book_id;
    END IF;
    
    -- Only process if this is a text file
    IF (TG_OP = 'DELETE' AND OLD.item_type = 'file' AND OLD.file_extension IN ('txt', 'md', 'markdown')) OR
       (TG_OP IN ('INSERT', 'UPDATE') AND NEW.item_type = 'file' AND NEW.file_extension IN ('txt', 'md', 'markdown')) THEN
        
        -- Calculate new word count for the book
        new_word_count := calculate_book_word_count(book_uuid);
        
        -- Update the book's word count
        UPDATE books 
        SET word_count = new_word_count, 
            updated_at = NOW()
        WHERE id = book_uuid;
    END IF;
    
    -- Return appropriate record based on operation
    IF TG_OP = 'DELETE' THEN
        RETURN OLD;
    ELSE
        RETURN NEW;
    END IF;
END;
$$ LANGUAGE plpgsql;

-- 17. Fix update_chat_metadata function
CREATE OR REPLACE FUNCTION update_chat_metadata()
RETURNS TRIGGER 
SECURITY DEFINER
SET search_path = public, pg_catalog, pg_temp
AS $$
BEGIN
    -- Update metadata when a message is added/updated/deleted
    IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
        UPDATE chats SET
            last_message_at = NOW(),
            message_count = (
                SELECT COUNT(*) 
                FROM chat_messages 
                WHERE chat_id = NEW.chat_id
            ),
            updated_at = NOW()
        WHERE id = NEW.chat_id;
        
        RETURN NEW;
    ELSIF TG_OP = 'DELETE' THEN
        UPDATE chats SET
            message_count = (
                SELECT COUNT(*) 
                FROM chat_messages 
                WHERE chat_id = OLD.chat_id
            ),
            updated_at = NOW()
        WHERE id = OLD.chat_id;
        
        RETURN OLD;
    END IF;
    
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;