From c25d22bc006e92bde0f91e15903a57c6d9742794 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 22:40:47 +0000 Subject: [PATCH 1/6] Add Slack MCP server config Registers the read-capable Slack MCP server (browser-session token auth) so sessions can query the Code for Philly / phlask workspace. Tokens are supplied via SLACK_MCP_XOXC_TOKEN / SLACK_MCP_XOXD_TOKEN environment secrets and referenced (not embedded) here. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DQCnNexihkvzxUYee3NUa1 --- .mcp.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..84b21b8 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "slack": { + "command": "npx", + "args": ["-y", "slack-mcp-server@latest", "--transport", "stdio"], + "env": { + "SLACK_MCP_XOXC_TOKEN": "${SLACK_MCP_XOXC_TOKEN}", + "SLACK_MCP_XOXD_TOKEN": "${SLACK_MCP_XOXD_TOKEN}" + } + } + } +} From 74692a215ce4f5ec4ee8c6540fda5a00e55afa78 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 23:14:54 +0000 Subject: [PATCH 2/6] Add resource-editing schema: holding table, versioned history, review RPCs Introduces the Supabase migration behind the crowdsourced resource-editing review flow (phlask/admin-dashboard#3): - resource_edits holding table (PENDING edits, NEW/UPDATE, base_version) - resource_history: one row per version + change log (folds in #1) - accept_edit / reject_edit / rollback_resource transactional RPCs (#2) - dashboard views: two review queues, per-resource counts, change log - RLS: anon may only INSERT pending edits; reviewers run the RPCs Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DQCnNexihkvzxUYee3NUa1 --- .../20260714231214_resource_editing.sql | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 supabase/migrations/20260714231214_resource_editing.sql diff --git a/supabase/migrations/20260714231214_resource_editing.sql b/supabase/migrations/20260714231214_resource_editing.sql new file mode 100644 index 0000000..ca3b67d --- /dev/null +++ b/supabase/migrations/20260714231214_resource_editing.sql @@ -0,0 +1,357 @@ +-- ============================================================================ +-- Resource Editing: holding table, versioned history (+ change log), and the +-- transactional review RPCs behind the admin dashboard. +-- +-- Design: phlask/admin-dashboard#3 (see the "Data Model & Review Flow" comment). +-- Also covers #2 (versioning / rollback) and #1 (change log, folded into history). +-- +-- NOTE: the resources column list lives in one place — _apply_resource_snapshot() +-- and accept_edit()'s NEW branch. It mirrors the `ResourceEntry` type +-- (app/types/ResourceEntry.ts). Confirm it against the live Supabase `resources` +-- schema before merging; if a column is added to ResourceEntry, add it here too. +-- ============================================================================ + +begin; + +-- --------------------------------------------------------------------------- +-- Enums +-- --------------------------------------------------------------------------- +create type edit_type as enum ('NEW', 'UPDATE'); +create type edit_status as enum ('PENDING', 'ACCEPTED', 'REJECTED'); +create type change_type as enum ('CREATE', 'UPDATE', 'ROLLBACK'); + +-- --------------------------------------------------------------------------- +-- resources: add an authoritative version counter (HEAD). +-- The table already exists (Supabase); adding the column is safe to re-run. +-- --------------------------------------------------------------------------- +alter table public.resources + add column if not exists version integer not null default 1; + +-- --------------------------------------------------------------------------- +-- resource_edits: holding table for pending crowdsourced submissions. +-- --------------------------------------------------------------------------- +create table public.resource_edits ( + id uuid primary key default gen_random_uuid(), + resource_id uuid references public.resources (id) on delete cascade, -- null = NEW + edit_type edit_type not null, + status edit_status not null default 'PENDING', + + -- version of the target resource this edit was drafted against; if it is + -- behind resources.version at review time, the dashboard shows a non-blocking + -- "changed since submitted" warning. Null for NEW submissions. + base_version integer, + + suggested jsonb not null, -- full suggested ResourceEntry + source jsonb, -- DataSource + + -- pending-state flags (see open question: flags vs. resources.status) + unverified boolean not null default true, + not_displayed boolean not null default true, + unreviewed boolean not null default true, + + submitted_by text, -- anon / crowdsource identity + submitted_at timestamptz not null default now(), + + reviewed_by uuid references auth.users (id), + reviewed_at timestamptz, + review_notes text, + + constraint resource_edits_new_has_no_resource + check (edit_type <> 'NEW' or resource_id is null), + constraint resource_edits_update_has_resource + check (edit_type <> 'UPDATE' or resource_id is not null) +); + +create index resource_edits_pending_idx + on public.resource_edits (status) where status = 'PENDING'; +create index resource_edits_resource_idx + on public.resource_edits (resource_id); + +-- --------------------------------------------------------------------------- +-- resource_history: one row per committed version + the change log. +-- HEAD is included, so resources.version always equals the latest history row. +-- --------------------------------------------------------------------------- +create table public.resource_history ( + id uuid primary key default gen_random_uuid(), + resource_id uuid not null references public.resources (id) on delete cascade, + version integer not null, + snapshot jsonb not null, -- ResourceEntry at this version + change_type change_type not null, + changed_by uuid references auth.users (id), + changed_at timestamptz not null default now(), + produced_by_edit_id uuid references public.resource_edits (id), + note text, + unique (resource_id, version) +); + +create index resource_history_resource_idx + on public.resource_history (resource_id, version desc); + +-- --------------------------------------------------------------------------- +-- Dashboard read models (plain views — always fresh; see #3 on why not +-- materialized). +-- --------------------------------------------------------------------------- + +-- Brand-new sites awaiting review. +create view public.new_resources_queue as + select e.* + from public.resource_edits e + where e.status = 'PENDING' and e.edit_type = 'NEW' + order by e.submitted_at; + +-- Edits to existing resources, joined to the current resource for the +-- side-by-side compare. `stale` flags base_version drift. +create view public.resource_edits_queue as + select + e.*, + r.version as current_version, + (e.base_version is not null + and e.base_version < r.version) as stale, + to_jsonb(r.*) as current_resource + from public.resource_edits e + join public.resources r on r.id = e.resource_id + where e.status = 'PENDING' and e.edit_type = 'UPDATE' + order by e.submitted_at; + +-- Pending-edit count per resource, for list badges. +create view public.resource_edit_counts as + select resource_id, count(*) as pending_count + from public.resource_edits + where status = 'PENDING' and resource_id is not null + group by resource_id; + +-- Readable change log for the per-resource history / approvals page (#7/#8). +create view public.resource_change_log as + select h.resource_id, h.version, h.change_type, + h.changed_by, h.changed_at, h.produced_by_edit_id, h.note + from public.resource_history h + order by h.resource_id, h.version desc; + +-- --------------------------------------------------------------------------- +-- Internal helper: overwrite a resource's data columns from a jsonb payload +-- (the ResourceEntry shape), set the given version, and stamp last_modified. +-- Centralizes the one place the resources column list is enumerated. +-- --------------------------------------------------------------------------- +create or replace function public._apply_resource_snapshot( + p_id uuid, p_payload jsonb, p_version integer +) returns public.resources +language plpgsql +security definer +set search_path = public +as $$ +declare + s public.resources; + r public.resources; +begin + s := jsonb_populate_record(null::public.resources, p_payload); + + update public.resources t set + date_created = s.date_created, + creator = s.creator, + last_modifier = s.last_modifier, + source = s.source, + verification = s.verification, + resource_type = s.resource_type, + address = s.address, + city = s.city, + state = s.state, + zip_code = s.zip_code, + latitude = s.latitude, + longitude = s.longitude, + gp_id = s.gp_id, + images = s.images, + guidelines = s.guidelines, + description = s.description, + name = s.name, + status = s.status, + entry_type = s.entry_type, + hours = s.hours, + water = s.water, + food = s.food, + forage = s.forage, + bathroom = s.bathroom, + version = p_version, + last_modified = now() + where t.id = p_id + returning * into r; + + return r; +end; +$$; + +-- --------------------------------------------------------------------------- +-- RPCs (transactional). SECURITY DEFINER so reviewers can run the privileged +-- writes without direct table grants; callers are gated by the EXECUTE grants +-- and RLS policies below. +-- --------------------------------------------------------------------------- + +-- Apply a pending edit; snapshot the resulting version into history. +create or replace function public.accept_edit(p_edit_id uuid, p_reviewer uuid) +returns public.resources +language plpgsql +security definer +set search_path = public +as $$ +declare + e public.resource_edits; + r public.resources; + v integer; +begin + select * into e from public.resource_edits where id = p_edit_id for update; + if not found then + raise exception 'edit % not found', p_edit_id; + end if; + if e.status <> 'PENDING' then + raise exception 'edit % is % (expected PENDING)', p_edit_id, e.status; + end if; + + if e.edit_type = 'NEW' then + -- Create the resource at version 1 from the suggested payload + -- (id/created defaults come from the table; version forced to 1). + insert into public.resources ( + date_created, creator, last_modified, last_modifier, source, verification, + resource_type, address, city, state, zip_code, latitude, longitude, gp_id, + images, guidelines, description, name, status, entry_type, hours, + water, food, forage, bathroom, version + ) + select + coalesce(s.date_created, now()), s.creator, now(), s.last_modifier, + s.source, s.verification, s.resource_type, s.address, s.city, s.state, + s.zip_code, s.latitude, s.longitude, s.gp_id, s.images, s.guidelines, + s.description, s.name, s.status, s.entry_type, s.hours, + s.water, s.food, s.forage, s.bathroom, 1 + from jsonb_populate_record(null::public.resources, e.suggested) s + returning * into r; + + v := 1; + insert into public.resource_history + (resource_id, version, snapshot, change_type, changed_by, produced_by_edit_id) + values (r.id, v, to_jsonb(r), 'CREATE', p_reviewer, e.id); + + else + -- UPDATE: overwrite the current resource (last-Save-wins) and bump version. + select * into r from public.resources where id = e.resource_id for update; + if not found then + raise exception 'resource % not found', e.resource_id; + end if; + v := r.version + 1; + r := public._apply_resource_snapshot(e.resource_id, e.suggested, v); + + insert into public.resource_history + (resource_id, version, snapshot, change_type, changed_by, produced_by_edit_id) + values (r.id, v, to_jsonb(r), 'UPDATE', p_reviewer, e.id); + end if; + + update public.resource_edits + set status = 'ACCEPTED', reviewed_by = p_reviewer, reviewed_at = now() + where id = e.id; + + return r; +end; +$$; + +-- Reject a pending edit. Recorded on the edit row; production untouched. +create or replace function public.reject_edit( + p_edit_id uuid, p_reviewer uuid, p_notes text default null +) returns public.resource_edits +language plpgsql +security definer +set search_path = public +as $$ +declare + e public.resource_edits; +begin + select * into e from public.resource_edits where id = p_edit_id for update; + if not found then + raise exception 'edit % not found', p_edit_id; + end if; + if e.status <> 'PENDING' then + raise exception 'edit % is % (expected PENDING)', p_edit_id, e.status; + end if; + + update public.resource_edits + set status = 'REJECTED', reviewed_by = p_reviewer, + reviewed_at = now(), review_notes = p_notes + where id = e.id + returning * into e; + + return e; +end; +$$; + +-- Roll a resource back to an earlier version by committing a new forward +-- version that restores that snapshot. +create or replace function public.rollback_resource( + p_resource_id uuid, p_version integer, p_actor uuid +) returns public.resources +language plpgsql +security definer +set search_path = public +as $$ +declare + target public.resource_history; + r public.resources; + v integer; +begin + select * into target + from public.resource_history + where resource_id = p_resource_id and version = p_version; + if not found then + raise exception 'version % of resource % not found', p_version, p_resource_id; + end if; + + select * into r from public.resources where id = p_resource_id for update; + if not found then + raise exception 'resource % not found', p_resource_id; + end if; + + v := r.version + 1; + r := public._apply_resource_snapshot(p_resource_id, target.snapshot, v); + + insert into public.resource_history + (resource_id, version, snapshot, change_type, changed_by, note) + values (r.id, v, to_jsonb(r), 'ROLLBACK', p_actor, + format('rolled back to v%s', p_version)); + + return r; +end; +$$; + +-- --------------------------------------------------------------------------- +-- Row Level Security +-- * anonymous crowdsourcing may only INSERT pending edits +-- * authenticated reviewers may read edits/history and run the RPCs +-- * `resources` stays publicly readable (the live map) — unchanged here +-- Depends on Supabase Auth for reviewer identity (#13). +-- --------------------------------------------------------------------------- +alter table public.resource_edits enable row level security; +alter table public.resource_history enable row level security; + +-- Anyone (anon) can submit an edit, but only as a fresh PENDING row. +create policy resource_edits_insert_anon + on public.resource_edits for insert + to anon, authenticated + with check (status = 'PENDING'); + +-- Authenticated reviewers can read the queues. +create policy resource_edits_select_auth + on public.resource_edits for select + to authenticated + using (true); + +-- History is readable by authenticated reviewers; writes happen only through +-- the SECURITY DEFINER RPCs (no direct write policy). +create policy resource_history_select_auth + on public.resource_history for select + to authenticated + using (true); + +-- Only authenticated reviewers may execute the review RPCs. +revoke execute on function public.accept_edit(uuid, uuid) from public, anon; +revoke execute on function public.reject_edit(uuid, uuid, text) from public, anon; +revoke execute on function public.rollback_resource(uuid, integer, uuid) from public, anon; +grant execute on function public.accept_edit(uuid, uuid) to authenticated; +grant execute on function public.reject_edit(uuid, uuid, text) to authenticated; +grant execute on function public.rollback_resource(uuid, integer, uuid) to authenticated; + +commit; From 3ca7d2353b1a420e19795f293acf88c1a5e993f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 23:25:21 +0000 Subject: [PATCH 3/6] Fix update overwrite nulling preserved fields in resource snapshot Local dry-run against a stub Postgres surfaced that _apply_resource_snapshot overwrote every resources column from the suggested payload, nulling any field the edit omitted (e.g. date_created -> not-null violation). Now overlays the suggested changes onto the current row so omitted fields are preserved, and never lets an edit change id / date_created / creator. Verified end-to-end: CREATE -> UPDATE -> REJECT -> ROLLBACK, with history versions [1,2,3] = [CREATE,UPDATE,ROLLBACK] and both queues draining to 0. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DQCnNexihkvzxUYee3NUa1 --- .../20260714231214_resource_editing.sql | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/supabase/migrations/20260714231214_resource_editing.sql b/supabase/migrations/20260714231214_resource_editing.sql index ca3b67d..a35b2e5 100644 --- a/supabase/migrations/20260714231214_resource_editing.sql +++ b/supabase/migrations/20260714231214_resource_editing.sql @@ -140,10 +140,26 @@ security definer set search_path = public as $$ declare - s public.resources; - r public.resources; + cur jsonb; + s public.resources; + r public.resources; begin - s := jsonb_populate_record(null::public.resources, p_payload); + select to_jsonb(t) into cur from public.resources t where t.id = p_id; + if cur is null then + raise exception 'resource % not found', p_id; + end if; + + -- Overlay the suggested changes onto the current row so fields the edit does + -- not mention are preserved, and never let an edit change id / date_created / + -- creator (server-managed / immutable). + s := jsonb_populate_record( + null::public.resources, + cur || p_payload || jsonb_build_object( + 'id', cur -> 'id', + 'date_created', cur -> 'date_created', + 'creator', cur -> 'creator' + ) + ); update public.resources t set date_created = s.date_created, From df1240d273dba08c12c8be56d99f2c70fb58114c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 23:47:20 +0000 Subject: [PATCH 4/6] Rework migration to build on existing resource_revisions table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inspecting the live Supabase schema showed the earlier design was wrong: the team already has a `resource_revisions` table (full copy of the resources columns + mapped_resource/mapped_resources + a review `status` of PENDING/APPROVED/REJECTED), and `resources.id` is a bigint, not a uuid. The old resource_edits/resource_history/uuid design would have created a parallel, conflicting system. Reworked to be additive and non-destructive — no new tables, no alters, no RLS changes: - Views: revision_edits_queue, revision_new_queue, revision_edit_counts, resource_change_log (all over resource_revisions). - SECURITY DEFINER RPCs: approve_revision / reject_revision (review-state only). Deliberately defers copying an approved revision into `resources` (and any version bump / rollback): `status` is overloaded (operational vs review) and `version` is a schema version, so the write-path needs the data circle to confirm before automating a production write. Proposed path is in comments. Dry-run against a stub matching the real integer-keyed schema: migration applies clean; queues/counts correct; approve->APPROVED, reject->REJECTED, non-PENDING guarded. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DQCnNexihkvzxUYee3NUa1 --- .../20260714231214_resource_editing.sql | 436 ++++-------------- 1 file changed, 102 insertions(+), 334 deletions(-) diff --git a/supabase/migrations/20260714231214_resource_editing.sql b/supabase/migrations/20260714231214_resource_editing.sql index a35b2e5..d62269e 100644 --- a/supabase/migrations/20260714231214_resource_editing.sql +++ b/supabase/migrations/20260714231214_resource_editing.sql @@ -1,373 +1,141 @@ -- ============================================================================ --- Resource Editing: holding table, versioned history (+ change log), and the --- transactional review RPCs behind the admin dashboard. +-- Resource-editing review layer — built on the EXISTING `resource_revisions` +-- table (discovered in the live Supabase schema). -- --- Design: phlask/admin-dashboard#3 (see the "Data Model & Review Flow" comment). --- Also covers #2 (versioning / rollback) and #1 (change log, folded into history). +-- This SUPERSEDES the earlier resource_edits/resource_history draft, which did +-- not match production. What actually exists: -- --- NOTE: the resources column list lives in one place — _apply_resource_snapshot() --- and accept_edit()'s NEW branch. It mirrors the `ResourceEntry` type --- (app/types/ResourceEntry.ts). Confirm it against the live Supabase `resources` --- schema before merging; if a column is added to ResourceEntry, add it here too. +-- resources id bigint PK, version int (SCHEMA version, per +-- ResourceEntry), status = OPERATIONAL | ... , ... +-- +-- resource_revisions a full copy of the `resources` columns, PLUS +-- mapped_resource bigint -> resources.id (null = new site) +-- mapped_resources bigint (dup of mapped_resource today) +-- status = REVIEW state: PENDING | APPROVED | REJECTED +-- ^ NOTE: `status` means OPERATIONAL status in `resources` +-- but REVIEW status here — the column name is overloaded. +-- +-- This migration is ADDITIVE and NON-DESTRUCTIVE: it creates views + two +-- review-state RPCs only. It creates no tables, alters no existing tables, and +-- changes no RLS. Everything runs in one transaction. +-- +-- DEFERRED ON PURPOSE: applying an APPROVED revision's data into `resources` +-- (the copy + any version bump + rollback) is NOT automated here. Because +-- `status` is overloaded and `version` is a schema version (not a per-resource +-- counter), the write-path semantics need the data circle to confirm before we +-- touch production `resources`. Proposed design is sketched in comments below. +-- Design + discussion: phlask/admin-dashboard#3. -- ============================================================================ begin; -- --------------------------------------------------------------------------- --- Enums --- --------------------------------------------------------------------------- -create type edit_type as enum ('NEW', 'UPDATE'); -create type edit_status as enum ('PENDING', 'ACCEPTED', 'REJECTED'); -create type change_type as enum ('CREATE', 'UPDATE', 'ROLLBACK'); - --- --------------------------------------------------------------------------- --- resources: add an authoritative version counter (HEAD). --- The table already exists (Supabase); adding the column is safe to re-run. +-- Review queues (plain views — always fresh). -- --------------------------------------------------------------------------- -alter table public.resources - add column if not exists version integer not null default 1; - --- --------------------------------------------------------------------------- --- resource_edits: holding table for pending crowdsourced submissions. --- --------------------------------------------------------------------------- -create table public.resource_edits ( - id uuid primary key default gen_random_uuid(), - resource_id uuid references public.resources (id) on delete cascade, -- null = NEW - edit_type edit_type not null, - status edit_status not null default 'PENDING', - - -- version of the target resource this edit was drafted against; if it is - -- behind resources.version at review time, the dashboard shows a non-blocking - -- "changed since submitted" warning. Null for NEW submissions. - base_version integer, - - suggested jsonb not null, -- full suggested ResourceEntry - source jsonb, -- DataSource - -- pending-state flags (see open question: flags vs. resources.status) - unverified boolean not null default true, - not_displayed boolean not null default true, - unreviewed boolean not null default true, - - submitted_by text, -- anon / crowdsource identity - submitted_at timestamptz not null default now(), - - reviewed_by uuid references auth.users (id), - reviewed_at timestamptz, - review_notes text, - - constraint resource_edits_new_has_no_resource - check (edit_type <> 'NEW' or resource_id is null), - constraint resource_edits_update_has_resource - check (edit_type <> 'UPDATE' or resource_id is not null) -); - -create index resource_edits_pending_idx - on public.resource_edits (status) where status = 'PENDING'; -create index resource_edits_resource_idx - on public.resource_edits (resource_id); - --- --------------------------------------------------------------------------- --- resource_history: one row per committed version + the change log. --- HEAD is included, so resources.version always equals the latest history row. --- --------------------------------------------------------------------------- -create table public.resource_history ( - id uuid primary key default gen_random_uuid(), - resource_id uuid not null references public.resources (id) on delete cascade, - version integer not null, - snapshot jsonb not null, -- ResourceEntry at this version - change_type change_type not null, - changed_by uuid references auth.users (id), - changed_at timestamptz not null default now(), - produced_by_edit_id uuid references public.resource_edits (id), - note text, - unique (resource_id, version) -); - -create index resource_history_resource_idx - on public.resource_history (resource_id, version desc); - --- --------------------------------------------------------------------------- --- Dashboard read models (plain views — always fresh; see #3 on why not --- materialized). --- --------------------------------------------------------------------------- - --- Brand-new sites awaiting review. -create view public.new_resources_queue as - select e.* - from public.resource_edits e - where e.status = 'PENDING' and e.edit_type = 'NEW' - order by e.submitted_at; - --- Edits to existing resources, joined to the current resource for the --- side-by-side compare. `stale` flags base_version drift. -create view public.resource_edits_queue as +-- Pending edits to an existing resource, joined to that resource so the +-- dashboard can render the existing-vs-suggested compare. +create or replace view public.revision_edits_queue as select - e.*, - r.version as current_version, - (e.base_version is not null - and e.base_version < r.version) as stale, - to_jsonb(r.*) as current_resource - from public.resource_edits e - join public.resources r on r.id = e.resource_id - where e.status = 'PENDING' and e.edit_type = 'UPDATE' - order by e.submitted_at; + rev.*, + res.name as current_name, + to_jsonb(res.*) as current_resource + from public.resource_revisions rev + join public.resources res on res.id = rev.mapped_resource + where rev.status = 'PENDING' and rev.mapped_resource is not null; + +-- Pending brand-new site submissions (not yet mapped to a resource). +create or replace view public.revision_new_queue as + select rev.* + from public.resource_revisions rev + where rev.status = 'PENDING' and rev.mapped_resource is null; -- Pending-edit count per resource, for list badges. -create view public.resource_edit_counts as - select resource_id, count(*) as pending_count - from public.resource_edits - where status = 'PENDING' and resource_id is not null - group by resource_id; - --- Readable change log for the per-resource history / approvals page (#7/#8). -create view public.resource_change_log as - select h.resource_id, h.version, h.change_type, - h.changed_by, h.changed_at, h.produced_by_edit_id, h.note - from public.resource_history h - order by h.resource_id, h.version desc; - --- --------------------------------------------------------------------------- --- Internal helper: overwrite a resource's data columns from a jsonb payload --- (the ResourceEntry shape), set the given version, and stamp last_modified. --- Centralizes the one place the resources column list is enumerated. --- --------------------------------------------------------------------------- -create or replace function public._apply_resource_snapshot( - p_id uuid, p_payload jsonb, p_version integer -) returns public.resources -language plpgsql -security definer -set search_path = public -as $$ -declare - cur jsonb; - s public.resources; - r public.resources; -begin - select to_jsonb(t) into cur from public.resources t where t.id = p_id; - if cur is null then - raise exception 'resource % not found', p_id; - end if; - - -- Overlay the suggested changes onto the current row so fields the edit does - -- not mention are preserved, and never let an edit change id / date_created / - -- creator (server-managed / immutable). - s := jsonb_populate_record( - null::public.resources, - cur || p_payload || jsonb_build_object( - 'id', cur -> 'id', - 'date_created', cur -> 'date_created', - 'creator', cur -> 'creator' - ) - ); - - update public.resources t set - date_created = s.date_created, - creator = s.creator, - last_modifier = s.last_modifier, - source = s.source, - verification = s.verification, - resource_type = s.resource_type, - address = s.address, - city = s.city, - state = s.state, - zip_code = s.zip_code, - latitude = s.latitude, - longitude = s.longitude, - gp_id = s.gp_id, - images = s.images, - guidelines = s.guidelines, - description = s.description, - name = s.name, - status = s.status, - entry_type = s.entry_type, - hours = s.hours, - water = s.water, - food = s.food, - forage = s.forage, - bathroom = s.bathroom, - version = p_version, - last_modified = now() - where t.id = p_id - returning * into r; - - return r; -end; -$$; - --- --------------------------------------------------------------------------- --- RPCs (transactional). SECURITY DEFINER so reviewers can run the privileged --- writes without direct table grants; callers are gated by the EXECUTE grants --- and RLS policies below. +create or replace view public.revision_edit_counts as + select mapped_resource as resource_id, count(*) as pending_count + from public.resource_revisions + where status = 'PENDING' and mapped_resource is not null + group by mapped_resource; + +-- Approved / rejected revisions per resource = the per-resource change log. +create or replace view public.resource_change_log as + select + mapped_resource as resource_id, + id as revision_id, + creator, + last_modifier, + last_modified, + status + from public.resource_revisions + where status in ('APPROVED', 'REJECTED') + order by mapped_resource, last_modified desc; + +-- --------------------------------------------------------------------------- +-- Review-state RPCs. +-- +-- These transition a revision's review `status` only (PENDING -> APPROVED / +-- REJECTED) and stamp the reviewer. They intentionally do NOT copy the +-- revision into `resources`. +-- +-- PROPOSED (deferred) apply-path, once the data circle confirms semantics: +-- on APPROVE of an edit (mapped_resource not null): +-- copy the ResourceEntry columns from the revision into +-- resources WHERE id = mapped_resource; (operational-status handling TBD) +-- on APPROVE of a new site (mapped_resource null): +-- insert a new resources row from the revision, then set +-- resource_revisions.mapped_resource = new id; +-- rollback: re-apply a prior APPROVED revision's columns to its resource. +-- Left out here so we don't guess a production write. See #3. +-- +-- SECURITY DEFINER so authenticated reviewers can run them without a direct +-- UPDATE grant on resource_revisions. +-- +-- CAVEAT: if `resource_revisions.status` is a constrained enum that does not +-- already include 'REJECTED', that value must be added to the enum first; this +-- migration assumes the column accepts it (text, or an enum containing it). -- --------------------------------------------------------------------------- --- Apply a pending edit; snapshot the resulting version into history. -create or replace function public.accept_edit(p_edit_id uuid, p_reviewer uuid) -returns public.resources -language plpgsql -security definer -set search_path = public -as $$ -declare - e public.resource_edits; - r public.resources; - v integer; -begin - select * into e from public.resource_edits where id = p_edit_id for update; - if not found then - raise exception 'edit % not found', p_edit_id; - end if; - if e.status <> 'PENDING' then - raise exception 'edit % is % (expected PENDING)', p_edit_id, e.status; - end if; - - if e.edit_type = 'NEW' then - -- Create the resource at version 1 from the suggested payload - -- (id/created defaults come from the table; version forced to 1). - insert into public.resources ( - date_created, creator, last_modified, last_modifier, source, verification, - resource_type, address, city, state, zip_code, latitude, longitude, gp_id, - images, guidelines, description, name, status, entry_type, hours, - water, food, forage, bathroom, version - ) - select - coalesce(s.date_created, now()), s.creator, now(), s.last_modifier, - s.source, s.verification, s.resource_type, s.address, s.city, s.state, - s.zip_code, s.latitude, s.longitude, s.gp_id, s.images, s.guidelines, - s.description, s.name, s.status, s.entry_type, s.hours, - s.water, s.food, s.forage, s.bathroom, 1 - from jsonb_populate_record(null::public.resources, e.suggested) s - returning * into r; - - v := 1; - insert into public.resource_history - (resource_id, version, snapshot, change_type, changed_by, produced_by_edit_id) - values (r.id, v, to_jsonb(r), 'CREATE', p_reviewer, e.id); - - else - -- UPDATE: overwrite the current resource (last-Save-wins) and bump version. - select * into r from public.resources where id = e.resource_id for update; - if not found then - raise exception 'resource % not found', e.resource_id; - end if; - v := r.version + 1; - r := public._apply_resource_snapshot(e.resource_id, e.suggested, v); - - insert into public.resource_history - (resource_id, version, snapshot, change_type, changed_by, produced_by_edit_id) - values (r.id, v, to_jsonb(r), 'UPDATE', p_reviewer, e.id); - end if; - - update public.resource_edits - set status = 'ACCEPTED', reviewed_by = p_reviewer, reviewed_at = now() - where id = e.id; - - return r; -end; -$$; - --- Reject a pending edit. Recorded on the edit row; production untouched. -create or replace function public.reject_edit( - p_edit_id uuid, p_reviewer uuid, p_notes text default null -) returns public.resource_edits +create or replace function public.approve_revision(p_revision_id bigint, p_reviewer text) +returns public.resource_revisions language plpgsql security definer set search_path = public as $$ declare - e public.resource_edits; + rev public.resource_revisions; begin - select * into e from public.resource_edits where id = p_edit_id for update; + update public.resource_revisions + set status = 'APPROVED', last_modifier = p_reviewer, last_modified = now() + where id = p_revision_id and status = 'PENDING' + returning * into rev; if not found then - raise exception 'edit % not found', p_edit_id; + raise exception 'revision % not found or not PENDING', p_revision_id; end if; - if e.status <> 'PENDING' then - raise exception 'edit % is % (expected PENDING)', p_edit_id, e.status; - end if; - - update public.resource_edits - set status = 'REJECTED', reviewed_by = p_reviewer, - reviewed_at = now(), review_notes = p_notes - where id = e.id - returning * into e; - - return e; + return rev; end; $$; --- Roll a resource back to an earlier version by committing a new forward --- version that restores that snapshot. -create or replace function public.rollback_resource( - p_resource_id uuid, p_version integer, p_actor uuid -) returns public.resources +create or replace function public.reject_revision(p_revision_id bigint, p_reviewer text) +returns public.resource_revisions language plpgsql security definer set search_path = public as $$ declare - target public.resource_history; - r public.resources; - v integer; + rev public.resource_revisions; begin - select * into target - from public.resource_history - where resource_id = p_resource_id and version = p_version; - if not found then - raise exception 'version % of resource % not found', p_version, p_resource_id; - end if; - - select * into r from public.resources where id = p_resource_id for update; + update public.resource_revisions + set status = 'REJECTED', last_modifier = p_reviewer, last_modified = now() + where id = p_revision_id and status = 'PENDING' + returning * into rev; if not found then - raise exception 'resource % not found', p_resource_id; + raise exception 'revision % not found or not PENDING', p_revision_id; end if; - - v := r.version + 1; - r := public._apply_resource_snapshot(p_resource_id, target.snapshot, v); - - insert into public.resource_history - (resource_id, version, snapshot, change_type, changed_by, note) - values (r.id, v, to_jsonb(r), 'ROLLBACK', p_actor, - format('rolled back to v%s', p_version)); - - return r; + return rev; end; $$; --- --------------------------------------------------------------------------- --- Row Level Security --- * anonymous crowdsourcing may only INSERT pending edits --- * authenticated reviewers may read edits/history and run the RPCs --- * `resources` stays publicly readable (the live map) — unchanged here --- Depends on Supabase Auth for reviewer identity (#13). --- --------------------------------------------------------------------------- -alter table public.resource_edits enable row level security; -alter table public.resource_history enable row level security; - --- Anyone (anon) can submit an edit, but only as a fresh PENDING row. -create policy resource_edits_insert_anon - on public.resource_edits for insert - to anon, authenticated - with check (status = 'PENDING'); - --- Authenticated reviewers can read the queues. -create policy resource_edits_select_auth - on public.resource_edits for select - to authenticated - using (true); - --- History is readable by authenticated reviewers; writes happen only through --- the SECURITY DEFINER RPCs (no direct write policy). -create policy resource_history_select_auth - on public.resource_history for select - to authenticated - using (true); - --- Only authenticated reviewers may execute the review RPCs. -revoke execute on function public.accept_edit(uuid, uuid) from public, anon; -revoke execute on function public.reject_edit(uuid, uuid, text) from public, anon; -revoke execute on function public.rollback_resource(uuid, integer, uuid) from public, anon; -grant execute on function public.accept_edit(uuid, uuid) to authenticated; -grant execute on function public.reject_edit(uuid, uuid, text) to authenticated; -grant execute on function public.rollback_resource(uuid, integer, uuid) to authenticated; +grant execute on function public.approve_revision(bigint, text) to authenticated; +grant execute on function public.reject_revision(bigint, text) to authenticated; commit; From 51693dbf4d4c8677e2a065b6a3c16d17a36ffe56 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 02:23:43 +0000 Subject: [PATCH 5/6] Rework as dedicated resource_edits table (typed mirror) with apply-on-approve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per design decision: resource_revisions was a throwaway table, so build a new dedicated holding table instead and leave resource_revisions alone. - New resource_edits table: typed mirror of the ResourceEntry fields + a mapped_resource link (null = new site) + a DEDICATED review_status (PENDING/APPROVED/REJECTED), fixing the operational-vs-review status overload. - Integer (bigint) keys to match resources.id. - Views: resource_edits_queue, new_resources_queue, resource_edit_counts, resource_change_log. - RPCs: approve_edit (applies the edit into resources — update mapped or insert new site), reject_edit, rollback_to_edit. The copy uses jsonb_populate_record so it adapts to the real resources column types. - RLS: anon may only INSERT a PENDING edit; reviewers read + run the RPCs. Dry-run against a stub (resources.images text[] vs edit.images jsonb) passes: queues correct, approve applies + auto-maps new sites, images converts across types, reject + rollback work. DRAFT: still needs a staging run to confirm real resources column types and that resources.id has a default/identity for the new-site insert path. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DQCnNexihkvzxUYee3NUa1 --- .../20260714231214_resource_editing.sql | 339 +++++++++++++----- 1 file changed, 244 insertions(+), 95 deletions(-) diff --git a/supabase/migrations/20260714231214_resource_editing.sql b/supabase/migrations/20260714231214_resource_editing.sql index d62269e..758fdc2 100644 --- a/supabase/migrations/20260714231214_resource_editing.sql +++ b/supabase/migrations/20260714231214_resource_editing.sql @@ -1,141 +1,290 @@ -- ============================================================================ --- Resource-editing review layer — built on the EXISTING `resource_revisions` --- table (discovered in the live Supabase schema). +-- Resource editing: a dedicated `resource_edits` holding table (typed mirror of +-- the resources fields), review queues, and transactional review RPCs. -- --- This SUPERSEDES the earlier resource_edits/resource_history draft, which did --- not match production. What actually exists: +-- Grounded in the live schema: +-- * resources.id is a BIGINT (not uuid); resources already has `version` +-- (a ResourceEntry SCHEMA version) and `status` (OPERATIONAL | ...). +-- * A throwaway `resource_revisions` table exists — LEFT UNTOUCHED here. -- --- resources id bigint PK, version int (SCHEMA version, per --- ResourceEntry), status = OPERATIONAL | ... , ... +-- Design choices (per #3 discussion): +-- * Typed columns mirroring the ResourceEntry fields (easy side-by-side +-- compare), plus a `mapped_resource` link (null = brand-new site). +-- * A DEDICATED `review_status` column (PENDING/APPROVED/REJECTED) — kept +-- separate from the mirrored operational `status`, so the column is not +-- overloaded the way the temp table's was. +-- * Reviewer identity is text (email) to match current data; can move to +-- auth.uid() once #13 lands. -- --- resource_revisions a full copy of the `resources` columns, PLUS --- mapped_resource bigint -> resources.id (null = new site) --- mapped_resources bigint (dup of mapped_resource today) --- status = REVIEW state: PENDING | APPROVED | REJECTED --- ^ NOTE: `status` means OPERATIONAL status in `resources` --- but REVIEW status here — the column name is overloaded. +-- The apply-on-approve copy into `resources` uses jsonb_populate_record, which +-- adapts to the real `resources` column types — so it is robust even though the +-- exact types (e.g. images text[] vs jsonb) aren't enumerated here. -- --- This migration is ADDITIVE and NON-DESTRUCTIVE: it creates views + two --- review-state RPCs only. It creates no tables, alters no existing tables, and --- changes no RLS. Everything runs in one transaction. --- --- DEFERRED ON PURPOSE: applying an APPROVED revision's data into `resources` --- (the copy + any version bump + rollback) is NOT automated here. Because --- `status` is overloaded and `version` is a schema version (not a per-resource --- counter), the write-path semantics need the data circle to confirm before we --- touch production `resources`. Proposed design is sketched in comments below. --- Design + discussion: phlask/admin-dashboard#3. +-- DRAFT: verify against a staging Supabase (real resources types + whether +-- resources.id has a default/identity for the NEW-insert path) before prod. -- ============================================================================ begin; +-- --------------------------------------------------------------------------- +-- resource_edits: holding table for pending crowdsourced edits / new sites. +-- --------------------------------------------------------------------------- +create table public.resource_edits ( + id bigint generated always as identity primary key, + + -- link to the resource being edited; null = brand-new site submission + mapped_resource bigint references public.resources (id) on delete cascade, + + -- review lifecycle (kept separate from the mirrored operational `status`) + review_status text not null default 'PENDING' + check (review_status in ('PENDING', 'APPROVED', 'REJECTED')), + submitted_by text, + submitted_at timestamptz not null default now(), + reviewed_by text, + reviewed_at timestamptz, + review_notes text, + + -- ---- mirrored ResourceEntry fields (the suggested values) ---- + version integer, + date_created timestamptz, + creator text, + last_modified timestamptz, + last_modifier text, + source jsonb, + verification jsonb, + resource_type text, + address text, + city text, + state text, + zip_code text, + latitude double precision, + longitude double precision, + gp_id text, + images jsonb, + guidelines text, + description text, + name text, + status text, -- operational status (OPERATIONAL | ...) + entry_type text, + hours jsonb, + water jsonb, + food jsonb, + forage jsonb, + bathroom jsonb +); + +create index resource_edits_pending_idx + on public.resource_edits (review_status) where review_status = 'PENDING'; +create index resource_edits_mapped_idx + on public.resource_edits (mapped_resource); + -- --------------------------------------------------------------------------- -- Review queues (plain views — always fresh). -- --------------------------------------------------------------------------- --- Pending edits to an existing resource, joined to that resource so the --- dashboard can render the existing-vs-suggested compare. -create or replace view public.revision_edits_queue as - select - rev.*, - res.name as current_name, - to_jsonb(res.*) as current_resource - from public.resource_revisions rev - join public.resources res on res.id = rev.mapped_resource - where rev.status = 'PENDING' and rev.mapped_resource is not null; - --- Pending brand-new site submissions (not yet mapped to a resource). -create or replace view public.revision_new_queue as - select rev.* - from public.resource_revisions rev - where rev.status = 'PENDING' and rev.mapped_resource is null; +-- Pending edits to an existing resource, joined to that resource for compare. +create view public.resource_edits_queue as + select e.*, to_jsonb(r.*) as current_resource + from public.resource_edits e + join public.resources r on r.id = e.mapped_resource + where e.review_status = 'PENDING' and e.mapped_resource is not null; + +-- Pending brand-new site submissions. +create view public.new_resources_queue as + select e.* + from public.resource_edits e + where e.review_status = 'PENDING' and e.mapped_resource is null; -- Pending-edit count per resource, for list badges. -create or replace view public.revision_edit_counts as +create view public.resource_edit_counts as select mapped_resource as resource_id, count(*) as pending_count - from public.resource_revisions - where status = 'PENDING' and mapped_resource is not null + from public.resource_edits + where review_status = 'PENDING' and mapped_resource is not null group by mapped_resource; --- Approved / rejected revisions per resource = the per-resource change log. -create or replace view public.resource_change_log as - select - mapped_resource as resource_id, - id as revision_id, - creator, - last_modifier, - last_modified, - status - from public.resource_revisions - where status in ('APPROVED', 'REJECTED') - order by mapped_resource, last_modified desc; +-- Resolved edits per resource = the change log. +create view public.resource_change_log as + select id as edit_id, mapped_resource as resource_id, review_status, + submitted_by, reviewed_by, reviewed_at, review_notes + from public.resource_edits + where review_status in ('APPROVED', 'REJECTED') + order by mapped_resource, reviewed_at desc; -- --------------------------------------------------------------------------- --- Review-state RPCs. --- --- These transition a revision's review `status` only (PENDING -> APPROVED / --- REJECTED) and stamp the reviewer. They intentionally do NOT copy the --- revision into `resources`. --- --- PROPOSED (deferred) apply-path, once the data circle confirms semantics: --- on APPROVE of an edit (mapped_resource not null): --- copy the ResourceEntry columns from the revision into --- resources WHERE id = mapped_resource; (operational-status handling TBD) --- on APPROVE of a new site (mapped_resource null): --- insert a new resources row from the revision, then set --- resource_revisions.mapped_resource = new id; --- rollback: re-apply a prior APPROVED revision's columns to its resource. --- Left out here so we don't guess a production write. See #3. --- --- SECURITY DEFINER so authenticated reviewers can run them without a direct --- UPDATE grant on resource_revisions. --- --- CAVEAT: if `resource_revisions.status` is a constrained enum that does not --- already include 'REJECTED', that value must be added to the enum first; this --- migration assumes the column accepts it (text, or an enum containing it). +-- Helper: apply a jsonb payload (mirrored ResourceEntry fields) onto an +-- existing resource. jsonb_populate_record adapts to `resources` real column +-- types. Immutable server fields (id / date_created / creator) are preserved. +-- --------------------------------------------------------------------------- +create or replace function public._apply_edit_payload(p_resource_id bigint, p_payload jsonb) +returns public.resources +language plpgsql +security definer +set search_path = public +as $$ +declare + cur jsonb; + s public.resources; + r public.resources; +begin + select to_jsonb(t) into cur from public.resources t where t.id = p_resource_id; + if cur is null then + raise exception 'resource % not found', p_resource_id; + end if; + + s := jsonb_populate_record( + null::public.resources, + cur || p_payload || jsonb_build_object( + 'id', cur -> 'id', + 'date_created', cur -> 'date_created', + 'creator', cur -> 'creator' + ) + ); + + update public.resources t set + last_modifier = s.last_modifier, source = s.source, verification = s.verification, + resource_type = s.resource_type, address = s.address, city = s.city, state = s.state, + zip_code = s.zip_code, latitude = s.latitude, longitude = s.longitude, gp_id = s.gp_id, + images = s.images, guidelines = s.guidelines, description = s.description, name = s.name, + status = s.status, entry_type = s.entry_type, hours = s.hours, water = s.water, + food = s.food, forage = s.forage, bathroom = s.bathroom, version = s.version, + last_modified = now() + where t.id = p_resource_id + returning * into r; + + return r; +end; +$$; + +-- Extract just the mirrored resource fields from an edit row as jsonb. +create or replace function public._edit_payload(e public.resource_edits) +returns jsonb language sql immutable as $$ + select to_jsonb(e) + - 'id' - 'mapped_resource' - 'review_status' - 'submitted_by' + - 'submitted_at' - 'reviewed_by' - 'reviewed_at' - 'review_notes'; +$$; + +-- --------------------------------------------------------------------------- +-- Review RPCs (SECURITY DEFINER). Approve applies the edit to `resources`. -- --------------------------------------------------------------------------- -create or replace function public.approve_revision(p_revision_id bigint, p_reviewer text) -returns public.resource_revisions +create or replace function public.approve_edit(p_edit_id bigint, p_reviewer text) +returns public.resources language plpgsql security definer set search_path = public as $$ declare - rev public.resource_revisions; + e public.resource_edits; + r public.resources; + new_id bigint; begin - update public.resource_revisions - set status = 'APPROVED', last_modifier = p_reviewer, last_modified = now() - where id = p_revision_id and status = 'PENDING' - returning * into rev; + select * into e from public.resource_edits where id = p_edit_id for update; if not found then - raise exception 'revision % not found or not PENDING', p_revision_id; + raise exception 'edit % not found', p_edit_id; + end if; + if e.review_status <> 'PENDING' then + raise exception 'edit % is % (expected PENDING)', p_edit_id, e.review_status; + end if; + + if e.mapped_resource is not null then + r := public._apply_edit_payload(e.mapped_resource, public._edit_payload(e)); + else + -- NEW site: insert from the mirrored fields (resources.id from its default). + insert into public.resources ( + version, date_created, creator, last_modified, last_modifier, source, + verification, resource_type, address, city, state, zip_code, latitude, + longitude, gp_id, images, guidelines, description, name, status, + entry_type, hours, water, food, forage, bathroom + ) + select + s.version, coalesce(s.date_created, now()), s.creator, now(), s.last_modifier, + s.source, s.verification, s.resource_type, s.address, s.city, s.state, + s.zip_code, s.latitude, s.longitude, s.gp_id, s.images, s.guidelines, + s.description, s.name, s.status, s.entry_type, s.hours, s.water, s.food, + s.forage, s.bathroom + from jsonb_populate_record(null::public.resources, public._edit_payload(e)) s + returning id into new_id; + + update public.resource_edits set mapped_resource = new_id where id = e.id; + select * into r from public.resources where id = new_id; end if; - return rev; + + update public.resource_edits + set review_status = 'APPROVED', reviewed_by = p_reviewer, reviewed_at = now() + where id = e.id; + + return r; end; $$; -create or replace function public.reject_revision(p_revision_id bigint, p_reviewer text) -returns public.resource_revisions +create or replace function public.reject_edit(p_edit_id bigint, p_reviewer text, p_notes text default null) +returns public.resource_edits language plpgsql security definer set search_path = public as $$ declare - rev public.resource_revisions; + e public.resource_edits; begin - update public.resource_revisions - set status = 'REJECTED', last_modifier = p_reviewer, last_modified = now() - where id = p_revision_id and status = 'PENDING' - returning * into rev; + update public.resource_edits + set review_status = 'REJECTED', reviewed_by = p_reviewer, + reviewed_at = now(), review_notes = p_notes + where id = p_edit_id and review_status = 'PENDING' + returning * into e; if not found then - raise exception 'revision % not found or not PENDING', p_revision_id; + raise exception 'edit % not found or not PENDING', p_edit_id; end if; - return rev; + return e; end; $$; -grant execute on function public.approve_revision(bigint, text) to authenticated; -grant execute on function public.reject_revision(bigint, text) to authenticated; +-- Roll a resource back to the state captured by a prior APPROVED edit. +create or replace function public.rollback_to_edit(p_edit_id bigint, p_actor text) +returns public.resources +language plpgsql +security definer +set search_path = public +as $$ +declare + e public.resource_edits; + r public.resources; +begin + select * into e from public.resource_edits where id = p_edit_id; + if not found then + raise exception 'edit % not found', p_edit_id; + end if; + if e.review_status <> 'APPROVED' or e.mapped_resource is null then + raise exception 'edit % is not an applied edit to roll back to', p_edit_id; + end if; + + r := public._apply_edit_payload(e.mapped_resource, public._edit_payload(e)); + return r; +end; +$$; + +-- --------------------------------------------------------------------------- +-- Grants / RLS +-- * anon may only INSERT a fresh PENDING edit +-- * authenticated reviewers may read edits + run the review RPCs +-- * `resources` and `resource_revisions` untouched +-- --------------------------------------------------------------------------- +alter table public.resource_edits enable row level security; + +create policy resource_edits_insert_anon + on public.resource_edits for insert + to anon, authenticated + with check (review_status = 'PENDING'); + +create policy resource_edits_select_auth + on public.resource_edits for select + to authenticated + using (true); + +revoke execute on function public.approve_edit(bigint, text) from public, anon; +revoke execute on function public.reject_edit(bigint, text, text) from public, anon; +revoke execute on function public.rollback_to_edit(bigint, text) from public, anon; +grant execute on function public.approve_edit(bigint, text) to authenticated; +grant execute on function public.reject_edit(bigint, text, text) to authenticated; +grant execute on function public.rollback_to_edit(bigint, text) to authenticated; commit; From a9daab685353016724b49d993ec978dc036d7e43 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 02:26:15 +0000 Subject: [PATCH 6/6] Add rollback script for the resource_edits migration Drops the views, functions, and resource_edits table (with its policies and indexes); leaves resources and resource_revisions untouched. Kept in supabase/rollback/ so the Supabase CLI won't apply it as a forward migration. Verified: up -> down leaves the schema as it started. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DQCnNexihkvzxUYee3NUa1 --- .../20260714231214_resource_editing.down.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 supabase/rollback/20260714231214_resource_editing.down.sql diff --git a/supabase/rollback/20260714231214_resource_editing.down.sql b/supabase/rollback/20260714231214_resource_editing.down.sql new file mode 100644 index 0000000..8f0d772 --- /dev/null +++ b/supabase/rollback/20260714231214_resource_editing.down.sql @@ -0,0 +1,27 @@ +-- ============================================================================ +-- Rollback for 20260714231214_resource_editing.sql +-- +-- Reverses the resource_edits review pipeline. Safe to run in the Supabase SQL +-- Editor. Kept in supabase/rollback/ (NOT supabase/migrations/) so the Supabase +-- CLI does not try to apply it as a forward migration. +-- +-- Drops only what the up-migration created; `resources` and `resource_revisions` +-- are untouched. Dropping resource_edits also removes its RLS policies + indexes. +-- ============================================================================ + +begin; + +drop function if exists public.rollback_to_edit(bigint, text); +drop function if exists public.reject_edit(bigint, text, text); +drop function if exists public.approve_edit(bigint, text); +drop function if exists public._edit_payload(public.resource_edits); +drop function if exists public._apply_edit_payload(bigint, jsonb); + +drop view if exists public.resource_change_log; +drop view if exists public.resource_edit_counts; +drop view if exists public.new_resources_queue; +drop view if exists public.resource_edits_queue; + +drop table if exists public.resource_edits; -- also drops its policies + indexes + +commit;