feat(fase-15): item_frequency.weight + admin RPCs (15.1)

Adds a per-collective curation knob on top of the existing use_count
ordering. The UI exposes a 3-way Hide/Normal/Boost control that writes
weight=-50/0/50 via the new SECURITY DEFINER RPC; the column itself is
unconstrained so a future granularity tier doesn't need a schema diff.

Both new RPCs (set_item_frequency_weight, purge_item_frequency) gate on
collective_members.role='admin'. The all-deny RLS from migration 006 is
preserved — the RPCs are the only write path. The trigger that fires on
shopping_items INSERT keeps backfilling weight=0 via the column default.

Re-create the suggestion index to lead with weight DESC so the dropdown
query can scan in order without a sort step.

pgTAP test 018 covers the column/index/RPC schema invariants + the
default-from-trigger behaviour (11 assertions). Role-gate denial-for-non-
admin is covered by the Vitest integration suite (next commit) — pgTAP
runs as postgres so the auth.uid() guard can't be exercised here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 11:15:20 +02:00
parent b994fc7c95
commit 5563afd04c
4 changed files with 224 additions and 0 deletions

View File

@@ -416,18 +416,21 @@ export interface Database {
collective_id: string;
name: string;
use_count: number;
weight: number;
last_used_at: string;
};
Insert: {
collective_id: string;
name: string;
use_count?: number;
weight?: number;
last_used_at?: string;
};
Update: {
collective_id?: string;
name?: string;
use_count?: number;
weight?: number;
last_used_at?: string;
};
Relationships: [];
@@ -618,6 +621,14 @@ export interface Database {
Args: { p_section: string };
Returns: void;
};
set_item_frequency_weight: {
Args: { p_collective_id: string; p_name: string; p_weight: number };
Returns: void;
};
purge_item_frequency: {
Args: { p_collective_id: string; p_name: string };
Returns: void;
};
};
Enums: {
language_code: LanguageCode;

View File

@@ -92,6 +92,7 @@ export interface ItemFrequency {
collective_id: string;
name: string; // normalized: lower(trim())
use_count: number;
weight: number; // Fase 15: admin curation knob (-50/0/50 via the UI, any int in DB)
last_used_at: string;
}