DB: shopping_lists + shopping_items tables with full RLS, pg_cron jobs (archive completed lists after 7 days, purge trash after 7 days). item_frequency table with SECURITY DEFINER trigger that normalises and upserts on every shopping_items INSERT. Frontend: /lists overview (featured card + 2-col grid + trash toggle), /lists/[id] detail (qty stepper, inline edit, swipe-to-reveal-delete, drag & drop reorder via svelte-dnd-action, ItemSuggestions chips). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
71 lines
2.8 KiB
PL/PgSQL
71 lines
2.8 KiB
PL/PgSQL
-- Migration 006: item_frequency — per-collective item usage tracking.
|
|
--
|
|
-- Write path: SECURITY DEFINER trigger only. The app role has SELECT access only.
|
|
-- Names are normalized (lower + trim) before upsert so "Milk" and "milk" are the same entry.
|
|
|
|
-- ── item_frequency ────────────────────────────────────────────────────────────
|
|
|
|
CREATE TABLE public.item_frequency (
|
|
collective_id uuid NOT NULL REFERENCES public.collectives(id) ON DELETE CASCADE,
|
|
name text NOT NULL, -- normalized: lower(trim(original name))
|
|
use_count integer NOT NULL DEFAULT 1,
|
|
last_used_at timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (collective_id, name)
|
|
);
|
|
|
|
ALTER TABLE public.item_frequency ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Index for the suggestion query: ordered by use_count descending per collective
|
|
CREATE INDEX item_frequency_collective_count_idx
|
|
ON public.item_frequency (collective_id, use_count DESC);
|
|
|
|
-- ── RLS ───────────────────────────────────────────────────────────────────────
|
|
|
|
-- Members (any role) can read frequency data for collectives they belong to.
|
|
CREATE POLICY item_frequency_select
|
|
ON public.item_frequency FOR SELECT
|
|
USING (public.is_member(collective_id));
|
|
|
|
-- All direct writes are blocked; the SECURITY DEFINER trigger below is the only
|
|
-- write path. This prevents clients from inflating suggestion scores artificially.
|
|
CREATE POLICY item_frequency_insert_deny
|
|
ON public.item_frequency FOR INSERT
|
|
WITH CHECK (false);
|
|
|
|
CREATE POLICY item_frequency_update_deny
|
|
ON public.item_frequency FOR UPDATE
|
|
USING (false);
|
|
|
|
CREATE POLICY item_frequency_delete_deny
|
|
ON public.item_frequency FOR DELETE
|
|
USING (false);
|
|
|
|
-- ── Trigger: upsert frequency on shopping_items insert ───────────────────────
|
|
|
|
CREATE OR REPLACE FUNCTION public.fn_record_item_frequency()
|
|
RETURNS TRIGGER
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
v_collective_id uuid;
|
|
BEGIN
|
|
SELECT collective_id INTO v_collective_id
|
|
FROM public.shopping_lists
|
|
WHERE id = NEW.list_id;
|
|
|
|
INSERT INTO public.item_frequency (collective_id, name, use_count, last_used_at)
|
|
VALUES (v_collective_id, lower(trim(NEW.name)), 1, now())
|
|
ON CONFLICT (collective_id, name) DO UPDATE SET
|
|
use_count = item_frequency.use_count + 1,
|
|
last_used_at = now();
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
CREATE TRIGGER trg_item_frequency
|
|
AFTER INSERT ON public.shopping_items
|
|
FOR EACH ROW EXECUTE FUNCTION public.fn_record_item_frequency();
|