mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
feat(contacts): add filter_contacts_by_tags RPC migration
Server-side tag filter used by the Contacts page tag filter: join + DISTINCT (OR across tags) + windowed total count + LIMIT/OFFSET in one query, so a tag covering many contacts can't silently truncate results or skew the count. SECURITY INVOKER, so existing account RLS applies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1bacaf6372
commit
450233de89
75
supabase/migrations/025_filter_contacts_by_tags.sql
Normal file
75
supabase/migrations/025_filter_contacts_by_tags.sql
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 025_filter_contacts_by_tags.sql — server-side tag filter
|
||||||
|
--
|
||||||
|
-- Why an RPC
|
||||||
|
--
|
||||||
|
-- The Contacts page filters by tag by resolving the selected
|
||||||
|
-- tags to contact ids and paging the result. Doing that on the
|
||||||
|
-- client (SELECT contact_id FROM contact_tags WHERE tag_id IN …,
|
||||||
|
-- then .in('id', ids) on contacts) hits two PostgREST limits for
|
||||||
|
-- accounts where a tag covers many contacts:
|
||||||
|
-- - the unbounded contact_tags select is silently capped
|
||||||
|
-- (~1000 rows), dropping contacts from the filter, and
|
||||||
|
-- - the follow-up .in('id', ids) pushes every matching id into
|
||||||
|
-- one IN-clause (the ~1000-value cap the broadcast sender
|
||||||
|
-- already pages around) and bloats the request URL.
|
||||||
|
--
|
||||||
|
-- Both break the total count and pagination. This function does
|
||||||
|
-- the join, de-duplication (OR across tags), ordering, windowed
|
||||||
|
-- total count, and LIMIT/OFFSET in one query so the result is
|
||||||
|
-- always complete and correctly counted.
|
||||||
|
--
|
||||||
|
-- Security
|
||||||
|
--
|
||||||
|
-- SECURITY INVOKER (the default): the function runs as the
|
||||||
|
-- caller, so the existing RLS on `contacts` and `contact_tags`
|
||||||
|
-- (account membership, migration 017) scopes the result to the
|
||||||
|
-- caller's account. No privilege bypass — unlike the SECURITY
|
||||||
|
-- DEFINER member RPCs in 018/019.
|
||||||
|
--
|
||||||
|
-- Idempotent — safe to run multiple times.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.filter_contacts_by_tags(
|
||||||
|
p_tag_ids UUID[],
|
||||||
|
p_search TEXT DEFAULT NULL,
|
||||||
|
p_limit INT DEFAULT 25,
|
||||||
|
p_offset INT DEFAULT 0
|
||||||
|
)
|
||||||
|
RETURNS TABLE (contact contacts, total_count BIGINT)
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
SECURITY INVOKER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
WITH matched AS (
|
||||||
|
-- Distinct contacts having ANY of the selected tags (OR),
|
||||||
|
-- narrowed by the same name/phone/email search as the list.
|
||||||
|
SELECT DISTINCT c.id, c.created_at
|
||||||
|
FROM contacts c
|
||||||
|
JOIN contact_tags ct ON ct.contact_id = c.id
|
||||||
|
WHERE ct.tag_id = ANY(p_tag_ids)
|
||||||
|
AND (
|
||||||
|
p_search IS NULL
|
||||||
|
OR c.name ILIKE '%' || p_search || '%'
|
||||||
|
OR c.phone ILIKE '%' || p_search || '%'
|
||||||
|
OR c.email ILIKE '%' || p_search || '%'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
page AS (
|
||||||
|
-- count(*) OVER() is evaluated before LIMIT, so it is the full
|
||||||
|
-- match total regardless of the page being returned.
|
||||||
|
SELECT id, count(*) OVER() AS total_count
|
||||||
|
FROM matched
|
||||||
|
ORDER BY created_at DESC, id
|
||||||
|
LIMIT p_limit OFFSET p_offset
|
||||||
|
)
|
||||||
|
SELECT c AS contact, page.total_count
|
||||||
|
FROM page
|
||||||
|
JOIN contacts c ON c.id = page.id
|
||||||
|
ORDER BY c.created_at DESC, c.id;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
ALTER FUNCTION public.filter_contacts_by_tags(UUID[], TEXT, INT, INT) OWNER TO postgres;
|
||||||
|
REVOKE ALL ON FUNCTION public.filter_contacts_by_tags(UUID[], TEXT, INT, INT) FROM PUBLIC;
|
||||||
|
GRANT EXECUTE ON FUNCTION public.filter_contacts_by_tags(UUID[], TEXT, INT, INT) TO authenticated;
|
||||||
Loading…
Reference in New Issue
Block a user