-- ===================================================== -- Audit Timeline RPCs for Courses, Events, Bookings -- -- Paginated, filterable read layer on the audit logs. -- Mirrors get_member_timeline from 20260416000007. -- ===================================================== -- ------------------------------------------------------- -- 1. Course timeline RPC -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_course_timeline( p_course_id uuid, p_page integer DEFAULT 1, p_page_size integer DEFAULT 50, p_action_filter text DEFAULT NULL ) RETURNS TABLE ( id bigint, action text, changes jsonb, metadata jsonb, user_id uuid, user_email text, created_at timestamptz, total_count bigint ) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_account_id uuid; v_total bigint; v_offset integer; BEGIN -- Get course's account for access check SELECT c.account_id INTO v_account_id FROM public.courses c WHERE c.id = p_course_id; IF v_account_id IS NULL THEN RAISE EXCEPTION 'Course not found'; END IF; IF NOT public.has_role_on_account(v_account_id) THEN RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501'; END IF; -- Clamp page size to prevent unbounded queries p_page_size := LEAST(GREATEST(p_page_size, 1), 200); v_offset := GREATEST(0, (p_page - 1)) * p_page_size; -- Get total count SELECT count(*) INTO v_total FROM public.course_audit_log cal WHERE cal.course_id = p_course_id AND (p_action_filter IS NULL OR cal.action = p_action_filter); -- Return paginated results with user email RETURN QUERY SELECT cal.id, cal.action, cal.changes, cal.metadata, cal.user_id, u.email::text AS user_email, cal.created_at, v_total AS total_count FROM public.course_audit_log cal LEFT JOIN auth.users u ON u.id = cal.user_id WHERE cal.course_id = p_course_id AND (p_action_filter IS NULL OR cal.action = p_action_filter) ORDER BY cal.created_at DESC LIMIT p_page_size OFFSET v_offset; END; $$; GRANT EXECUTE ON FUNCTION public.get_course_timeline(uuid, integer, integer, text) TO authenticated, service_role; -- ------------------------------------------------------- -- 2. Event timeline RPC -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_event_timeline( p_event_id uuid, p_page integer DEFAULT 1, p_page_size integer DEFAULT 50, p_action_filter text DEFAULT NULL ) RETURNS TABLE ( id bigint, action text, changes jsonb, metadata jsonb, user_id uuid, user_email text, created_at timestamptz, total_count bigint ) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_account_id uuid; v_total bigint; v_offset integer; BEGIN -- Get event's account for access check SELECT e.account_id INTO v_account_id FROM public.events e WHERE e.id = p_event_id; IF v_account_id IS NULL THEN RAISE EXCEPTION 'Event not found'; END IF; IF NOT public.has_role_on_account(v_account_id) THEN RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501'; END IF; -- Clamp page size to prevent unbounded queries p_page_size := LEAST(GREATEST(p_page_size, 1), 200); v_offset := GREATEST(0, (p_page - 1)) * p_page_size; -- Get total count SELECT count(*) INTO v_total FROM public.event_audit_log eal WHERE eal.event_id = p_event_id AND (p_action_filter IS NULL OR eal.action = p_action_filter); -- Return paginated results with user email RETURN QUERY SELECT eal.id, eal.action, eal.changes, eal.metadata, eal.user_id, u.email::text AS user_email, eal.created_at, v_total AS total_count FROM public.event_audit_log eal LEFT JOIN auth.users u ON u.id = eal.user_id WHERE eal.event_id = p_event_id AND (p_action_filter IS NULL OR eal.action = p_action_filter) ORDER BY eal.created_at DESC LIMIT p_page_size OFFSET v_offset; END; $$; GRANT EXECUTE ON FUNCTION public.get_event_timeline(uuid, integer, integer, text) TO authenticated, service_role; -- ------------------------------------------------------- -- 3. Booking timeline RPC -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_booking_timeline( p_booking_id uuid, p_page integer DEFAULT 1, p_page_size integer DEFAULT 50, p_action_filter text DEFAULT NULL ) RETURNS TABLE ( id bigint, action text, changes jsonb, metadata jsonb, user_id uuid, user_email text, created_at timestamptz, total_count bigint ) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_account_id uuid; v_total bigint; v_offset integer; BEGIN -- Get booking's account for access check SELECT b.account_id INTO v_account_id FROM public.bookings b WHERE b.id = p_booking_id; IF v_account_id IS NULL THEN RAISE EXCEPTION 'Booking not found'; END IF; IF NOT public.has_role_on_account(v_account_id) THEN RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501'; END IF; -- Clamp page size to prevent unbounded queries p_page_size := LEAST(GREATEST(p_page_size, 1), 200); v_offset := GREATEST(0, (p_page - 1)) * p_page_size; -- Get total count SELECT count(*) INTO v_total FROM public.booking_audit_log bal WHERE bal.booking_id = p_booking_id AND (p_action_filter IS NULL OR bal.action = p_action_filter); -- Return paginated results with user email RETURN QUERY SELECT bal.id, bal.action, bal.changes, bal.metadata, bal.user_id, u.email::text AS user_email, bal.created_at, v_total AS total_count FROM public.booking_audit_log bal LEFT JOIN auth.users u ON u.id = bal.user_id WHERE bal.booking_id = p_booking_id AND (p_action_filter IS NULL OR bal.action = p_action_filter) ORDER BY bal.created_at DESC LIMIT p_page_size OFFSET v_offset; END; $$; GRANT EXECUTE ON FUNCTION public.get_booking_timeline(uuid, integer, integer, text) TO authenticated, service_role;