-- Data model v2 (t-paliad-024) + full English rename (t-paliad-025). -- -- Replaces paliad.akten with a single self-referential paliad.projects tree, -- and renames every remaining German table/column in paliad.* to English. -- Only user-facing i18n strings stay bilingual (owned by the frontend). -- -- Visibility is purely team-based (direct + inherited up the path) + admin. -- Office becomes an informational attribute on users only — no project-level -- office gate anymore. Cases are associated with lead partners, not offices. -- -- Migration is one-shot: creates the new schema, renames child tables in -- place and rewrites their FKs (same UUIDs as akten.id), drops paliad.akten, -- replaces can_see_akte() with can_see_project(). All existing akten rows -- survive as projects rows of type='case' with parent_id=NULL (orphan cases — -- admin/partners reparent them under real clients through the new UI). -- ============================================================================ -- 1. users: primary office stays; add additional_offices for partners -- who work across offices. -- ============================================================================ ALTER TABLE paliad.users ADD COLUMN IF NOT EXISTS additional_offices text[] NOT NULL DEFAULT '{}'; -- ============================================================================ -- 2. paliad.projects — the single hierarchical tree. -- type in (client, litigation, patent, case, project). -- Roots (parent_id NULL) are typically type='client' but not enforced — -- a generic 'project' root is also valid (e.g., internal knowledge project). -- ============================================================================ CREATE TABLE paliad.projects ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), type text NOT NULL CHECK (type IN ( 'client','litigation','patent','case','project' )), parent_id uuid REFERENCES paliad.projects(id) ON DELETE CASCADE, -- Materialised path of UUID labels joined by '.'; always includes self -- as the last label. Root: path = id::text. Child: path = parent.path -- || '.' || id::text. Maintained by the trigger below; never write -- directly from the service layer. path text NOT NULL, title text NOT NULL, reference text, description text, status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','archived','closed')), -- created_by is nullable (matches akten; users can be deleted from -- auth.users). A NULL creator does NOT grant visibility — the row is -- only visible via explicit team membership (or admin). created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL, -- Client-specific (type='client'). Nullable for other types. industry text, country text, billing_reference text, -- ClientMatter numbers — external HLC billing/DMS identifiers, not -- generated by Paliad. Format: CCCCCCC.MMMMMMM (7+7 digits). -- * client_number lives on the Client-level Project and is inherited -- (by convention in the UI, not enforced) down the tree. -- * matter_number is assigned independently at any sub-level -- (litigation / patent / case). Children may override the inherited -- client_number — rare but allowed. -- Both are nullable; search/filter is across the tree. client_number text CHECK (client_number IS NULL OR client_number ~ '^[0-9]{7}$'), matter_number text CHECK (matter_number IS NULL OR matter_number ~ '^[0-9]{7}$'), -- netDocuments: HLC's DMS. We can't integrate via API, so we store a -- bookmark URL per project. UI renders as an external-link button. netdocuments_url text, -- Patent-specific (type='patent'). Nullable for other types. patent_number text, filing_date date, grant_date date, -- Case-specific (type='case'). Nullable for other types. court text, case_number text, proceeding_type_id integer REFERENCES paliad.proceeding_types(id) ON DELETE SET NULL, metadata jsonb NOT NULL DEFAULT '{}', ai_summary text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT projects_parent_self_differs CHECK (parent_id IS NULL OR parent_id <> id) ); -- text_pattern_ops index supports LIKE 'prefix.%' for fast descendant lookup. CREATE INDEX projects_path_prefix_idx ON paliad.projects (path text_pattern_ops); CREATE INDEX projects_parent_idx ON paliad.projects (parent_id); CREATE INDEX projects_type_status_idx ON paliad.projects (type, status); CREATE INDEX projects_reference_idx ON paliad.projects (reference) WHERE reference IS NOT NULL; CREATE INDEX projects_client_number_idx ON paliad.projects (client_number) WHERE client_number IS NOT NULL; CREATE INDEX projects_matter_number_idx ON paliad.projects (matter_number) WHERE matter_number IS NOT NULL; -- ============================================================================ -- 3. Path maintenance triggers. -- BEFORE INSERT / BEFORE UPDATE OF parent_id: recompute this row's path. -- AFTER UPDATE OF path: propagate the new prefix to every descendant. -- ============================================================================ CREATE OR REPLACE FUNCTION paliad.projects_sync_path() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE parent_path text; BEGIN IF NEW.parent_id IS NULL THEN NEW.path := NEW.id::text; ELSE SELECT path INTO parent_path FROM paliad.projects WHERE id = NEW.parent_id; IF parent_path IS NULL THEN RAISE EXCEPTION 'parent project % not found', NEW.parent_id; END IF; -- Reject cycles: parent's path cannot contain this row's id. IF parent_path = NEW.id::text OR parent_path LIKE '%.' || NEW.id::text OR parent_path LIKE NEW.id::text || '.%' OR parent_path LIKE '%.' || NEW.id::text || '.%' THEN RAISE EXCEPTION 'cannot set parent to own descendant'; END IF; NEW.path := parent_path || '.' || NEW.id::text; END IF; RETURN NEW; END; $$; CREATE TRIGGER projects_sync_path_before BEFORE INSERT OR UPDATE OF parent_id ON paliad.projects FOR EACH ROW EXECUTE FUNCTION paliad.projects_sync_path(); CREATE OR REPLACE FUNCTION paliad.projects_rewrite_subtree() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF OLD.path IS DISTINCT FROM NEW.path THEN UPDATE paliad.projects SET path = NEW.path || substring(path FROM length(OLD.path) + 1) WHERE path LIKE OLD.path || '.%'; END IF; RETURN NEW; END; $$; CREATE TRIGGER projects_rewrite_subtree_after AFTER UPDATE OF path ON paliad.projects FOR EACH ROW EXECUTE FUNCTION paliad.projects_rewrite_subtree(); -- ============================================================================ -- 4. paliad.project_teams — membership. inherited=false rows are writes; -- inherited=true is a flag the service layer may set on read to annotate -- rows derived from an ancestor's team. Writes always use inherited=false. -- ============================================================================ CREATE TABLE paliad.project_teams ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), project_id uuid NOT NULL REFERENCES paliad.projects(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, role text NOT NULL CHECK (role IN ( 'lead','associate','pa','of_counsel', 'local_counsel','expert','observer' )), inherited boolean NOT NULL DEFAULT false, added_by uuid REFERENCES auth.users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (project_id, user_id) ); CREATE INDEX project_teams_project_idx ON paliad.project_teams (project_id); CREATE INDEX project_teams_user_idx ON paliad.project_teams (user_id); -- ============================================================================ -- 5. paliad.departments — structural partner units (distinct from project teams). -- A user's Department membership is orthogonal to their project-team roles. -- ============================================================================ CREATE TABLE paliad.departments ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, lead_user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL, office text NOT NULL CHECK (office IN ( 'munich','duesseldorf','hamburg', 'amsterdam','london','paris','milan' )), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX departments_office_idx ON paliad.departments (office); CREATE INDEX departments_lead_idx ON paliad.departments (lead_user_id) WHERE lead_user_id IS NOT NULL; CREATE TABLE paliad.department_members ( department_id uuid NOT NULL REFERENCES paliad.departments(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (department_id, user_id) ); CREATE INDEX department_members_user_idx ON paliad.department_members (user_id); -- ============================================================================ -- 6. Data migration: akten → projects (same UUIDs), collaborators+created_by -- → project_teams. -- All existing akten become type='case' orphans (parent_id NULL). Admins -- reparent under real clients via the new UI. -- ============================================================================ INSERT INTO paliad.projects ( id, type, parent_id, path, title, reference, status, court, case_number, created_by, metadata, ai_summary, created_at, updated_at ) SELECT a.id, 'case'::text, NULL::uuid, a.id::text, -- root path = id (no parent) a.title, NULLIF(a.aktenzeichen, ''), -- aktenzeichen → reference a.status, a.court, a.court_ref, a.created_by, a.metadata, a.ai_summary, a.created_at, a.updated_at FROM paliad.akten a; -- Creator → team lead (skip NULL creators; they remain admin-only orphans). INSERT INTO paliad.project_teams (project_id, user_id, role, inherited, added_by) SELECT a.id, a.created_by, 'lead', false, a.created_by FROM paliad.akten a WHERE a.created_by IS NOT NULL ON CONFLICT (project_id, user_id) DO NOTHING; -- Collaborators → team associates (dedup against the creator row). INSERT INTO paliad.project_teams (project_id, user_id, role, inherited, added_by) SELECT a.id, collab_id::uuid, 'associate', false, a.created_by FROM paliad.akten a CROSS JOIN LATERAL unnest(a.collaborators) AS collab_id WHERE collab_id IS NOT NULL ON CONFLICT (project_id, user_id) DO NOTHING; -- ============================================================================ -- 7. Rename child tables to English + rewrite FK columns (akte_id → project_id). -- Tables: parteien→parties, fristen→deadlines, termine→appointments, -- dokumente→documents, akten_events→project_events, notizen→notes. -- Column renames: termin_type→appointment_type, akten_event_id→ -- project_event_id, frist_id→deadline_id, termin_id→appointment_id. -- ============================================================================ -- Drop dependent RLS policies on children (rebuilt below against project_id). DROP POLICY IF EXISTS parteien_all ON paliad.parteien; DROP POLICY IF EXISTS fristen_all ON paliad.fristen; DROP POLICY IF EXISTS termine_select ON paliad.termine; DROP POLICY IF EXISTS termine_insert ON paliad.termine; DROP POLICY IF EXISTS termine_update ON paliad.termine; DROP POLICY IF EXISTS termine_delete ON paliad.termine; DROP POLICY IF EXISTS dokumente_all ON paliad.dokumente; DROP POLICY IF EXISTS akten_events_all ON paliad.akten_events; DROP POLICY IF EXISTS notizen_all ON paliad.notizen; DROP POLICY IF EXISTS checklist_instances_select ON paliad.checklist_instances; DROP POLICY IF EXISTS checklist_instances_insert ON paliad.checklist_instances; DROP POLICY IF EXISTS checklist_instances_update ON paliad.checklist_instances; DROP POLICY IF EXISTS checklist_instances_delete ON paliad.checklist_instances; -- akten itself — drop policies before we drop the table. DROP POLICY IF EXISTS akten_select ON paliad.akten; DROP POLICY IF EXISTS akten_insert ON paliad.akten; DROP POLICY IF EXISTS akten_update ON paliad.akten; DROP POLICY IF EXISTS akten_delete ON paliad.akten; -- parteien → parties ALTER TABLE paliad.parteien DROP CONSTRAINT IF EXISTS parteien_akte_id_fkey; ALTER TABLE paliad.parteien RENAME TO parties; ALTER TABLE paliad.parties RENAME COLUMN akte_id TO project_id; ALTER TABLE paliad.parties ADD CONSTRAINT parties_project_id_fkey FOREIGN KEY (project_id) REFERENCES paliad.projects(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.parteien_akte_idx; CREATE INDEX parties_project_idx ON paliad.parties (project_id); -- fristen → deadlines ALTER TABLE paliad.fristen DROP CONSTRAINT IF EXISTS fristen_akte_id_fkey; ALTER TABLE paliad.fristen RENAME TO deadlines; ALTER TABLE paliad.deadlines RENAME COLUMN akte_id TO project_id; ALTER TABLE paliad.deadlines ADD CONSTRAINT deadlines_project_id_fkey FOREIGN KEY (project_id) REFERENCES paliad.projects(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.fristen_akte_idx; DROP INDEX IF EXISTS paliad.fristen_status_due_date_idx; DROP INDEX IF EXISTS paliad.fristen_due_date_idx; CREATE INDEX deadlines_project_idx ON paliad.deadlines (project_id); CREATE INDEX deadlines_status_due_date_idx ON paliad.deadlines (status, due_date); CREATE INDEX deadlines_due_date_idx ON paliad.deadlines (due_date); -- termine → appointments (akte_id was nullable) ALTER TABLE paliad.termine DROP CONSTRAINT IF EXISTS termine_akte_id_fkey; ALTER TABLE paliad.termine RENAME TO appointments; ALTER TABLE paliad.appointments RENAME COLUMN akte_id TO project_id; ALTER TABLE paliad.appointments RENAME COLUMN termin_type TO appointment_type; ALTER TABLE paliad.appointments DROP CONSTRAINT IF EXISTS termine_termin_type_check; ALTER TABLE paliad.appointments ADD CONSTRAINT appointments_appointment_type_check CHECK (appointment_type IS NULL OR appointment_type IN ( 'hearing', 'meeting', 'consultation', 'deadline_hearing' )); ALTER TABLE paliad.appointments ADD CONSTRAINT appointments_project_id_fkey FOREIGN KEY (project_id) REFERENCES paliad.projects(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.termine_akte_idx; DROP INDEX IF EXISTS paliad.termine_start_at_idx; CREATE INDEX appointments_project_idx ON paliad.appointments (project_id) WHERE project_id IS NOT NULL; CREATE INDEX appointments_start_at_idx ON paliad.appointments (start_at); -- dokumente → documents ALTER TABLE paliad.dokumente DROP CONSTRAINT IF EXISTS dokumente_akte_id_fkey; ALTER TABLE paliad.dokumente RENAME TO documents; ALTER TABLE paliad.documents RENAME COLUMN akte_id TO project_id; ALTER TABLE paliad.documents ADD CONSTRAINT documents_project_id_fkey FOREIGN KEY (project_id) REFERENCES paliad.projects(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.dokumente_akte_idx; CREATE INDEX documents_project_idx ON paliad.documents (project_id); -- akten_events → project_events ALTER TABLE paliad.akten_events DROP CONSTRAINT IF EXISTS akten_events_akte_id_fkey; ALTER TABLE paliad.akten_events RENAME TO project_events; ALTER TABLE paliad.project_events RENAME COLUMN akte_id TO project_id; ALTER TABLE paliad.project_events ADD CONSTRAINT project_events_project_id_fkey FOREIGN KEY (project_id) REFERENCES paliad.projects(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.akten_events_akte_created_idx; CREATE INDEX project_events_project_created_idx ON paliad.project_events (project_id, created_at DESC); -- notizen → notes. Polymorphic parent stays; all FK columns get English names. ALTER TABLE paliad.notizen DROP CONSTRAINT IF EXISTS notizen_exactly_one_parent; ALTER TABLE paliad.notizen DROP CONSTRAINT IF EXISTS notizen_akte_id_fkey; ALTER TABLE paliad.notizen DROP CONSTRAINT IF EXISTS notizen_frist_id_fkey; ALTER TABLE paliad.notizen DROP CONSTRAINT IF EXISTS notizen_termin_id_fkey; ALTER TABLE paliad.notizen DROP CONSTRAINT IF EXISTS notizen_akten_event_id_fkey; ALTER TABLE paliad.notizen RENAME TO notes; ALTER TABLE paliad.notes RENAME COLUMN akte_id TO project_id; ALTER TABLE paliad.notes RENAME COLUMN frist_id TO deadline_id; ALTER TABLE paliad.notes RENAME COLUMN termin_id TO appointment_id; ALTER TABLE paliad.notes RENAME COLUMN akten_event_id TO project_event_id; ALTER TABLE paliad.notes ADD CONSTRAINT notes_project_id_fkey FOREIGN KEY (project_id) REFERENCES paliad.projects(id) ON DELETE CASCADE, ADD CONSTRAINT notes_deadline_id_fkey FOREIGN KEY (deadline_id) REFERENCES paliad.deadlines(id) ON DELETE CASCADE, ADD CONSTRAINT notes_appointment_id_fkey FOREIGN KEY (appointment_id) REFERENCES paliad.appointments(id) ON DELETE CASCADE, ADD CONSTRAINT notes_project_event_id_fkey FOREIGN KEY (project_event_id) REFERENCES paliad.project_events(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.notizen_akte_idx; DROP INDEX IF EXISTS paliad.notizen_frist_idx; DROP INDEX IF EXISTS paliad.notizen_termin_idx; DROP INDEX IF EXISTS paliad.notizen_akten_event_idx; CREATE INDEX notes_project_idx ON paliad.notes (project_id) WHERE project_id IS NOT NULL; CREATE INDEX notes_deadline_idx ON paliad.notes (deadline_id) WHERE deadline_id IS NOT NULL; CREATE INDEX notes_appointment_idx ON paliad.notes (appointment_id) WHERE appointment_id IS NOT NULL; CREATE INDEX notes_project_event_idx ON paliad.notes (project_event_id) WHERE project_event_id IS NOT NULL; ALTER TABLE paliad.notes ADD CONSTRAINT notes_exactly_one_parent CHECK ( (CASE WHEN project_id IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN deadline_id IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN appointment_id IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN project_event_id IS NOT NULL THEN 1 ELSE 0 END) = 1 ); -- checklist_instances — akte_id → project_id (table name already English). ALTER TABLE paliad.checklist_instances DROP CONSTRAINT IF EXISTS checklist_instances_akte_id_fkey; ALTER TABLE paliad.checklist_instances RENAME COLUMN akte_id TO project_id; ALTER TABLE paliad.checklist_instances ADD CONSTRAINT checklist_instances_project_id_fkey FOREIGN KEY (project_id) REFERENCES paliad.projects(id) ON DELETE SET NULL; DROP INDEX IF EXISTS paliad.checklist_instances_akte_idx; CREATE INDEX checklist_instances_project_idx ON paliad.checklist_instances (project_id) WHERE project_id IS NOT NULL; -- ============================================================================ -- 8. Drop paliad.akten (and its visibility helpers). -- ============================================================================ DROP TABLE paliad.akten; DROP FUNCTION IF EXISTS paliad.can_see_akte(uuid); DROP FUNCTION IF EXISTS paliad.notiz_is_visible(uuid, uuid, uuid, uuid); -- ============================================================================ -- 9. Visibility: can_see_project(id) — team-based only. -- A user sees a project iff: -- - admin, or -- - direct team member (project_teams.project_id = target), or -- - inherited team member on any ancestor of target (walk UP path). -- ============================================================================ CREATE OR REPLACE FUNCTION paliad.can_see_project(_project_id uuid) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = paliad, public AS $$ SELECT EXISTS ( SELECT 1 FROM paliad.users u WHERE u.id = auth.uid() AND u.role = 'admin' ) OR EXISTS ( -- Any team membership (direct or ancestor). `path` always includes -- the target's own id as the last label, so this handles direct too. SELECT 1 FROM paliad.projects target JOIN paliad.project_teams pt ON pt.user_id = auth.uid() AND pt.project_id = ANY(string_to_array(target.path, '.')::uuid[]) WHERE target.id = _project_id ); $$; COMMENT ON FUNCTION paliad.can_see_project(uuid) IS 'Team-based visibility predicate for paliad.projects. Direct or inherited ' '(ancestor) membership grants access. Admins see all.'; -- Helper: note visibility — dispatches by whichever parent FK is set. CREATE OR REPLACE FUNCTION paliad.note_is_visible( _project_id uuid, _deadline_id uuid, _appointment_id uuid, _project_event_id uuid ) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = paliad, public AS $$ SELECT CASE WHEN _project_id IS NOT NULL THEN paliad.can_see_project(_project_id) WHEN _deadline_id IS NOT NULL THEN paliad.can_see_project( (SELECT project_id FROM paliad.deadlines WHERE id = _deadline_id)) WHEN _appointment_id IS NOT NULL THEN CASE WHEN (SELECT project_id FROM paliad.appointments WHERE id = _appointment_id) IS NULL THEN (SELECT created_by FROM paliad.appointments WHERE id = _appointment_id) = auth.uid() ELSE paliad.can_see_project( (SELECT project_id FROM paliad.appointments WHERE id = _appointment_id)) END WHEN _project_event_id IS NOT NULL THEN paliad.can_see_project( (SELECT project_id FROM paliad.project_events WHERE id = _project_event_id)) ELSE false END; $$; -- ============================================================================ -- 10. RLS: enable + policies on the new tables. -- ============================================================================ ALTER TABLE paliad.projects ENABLE ROW LEVEL SECURITY; ALTER TABLE paliad.project_teams ENABLE ROW LEVEL SECURITY; ALTER TABLE paliad.departments ENABLE ROW LEVEL SECURITY; ALTER TABLE paliad.department_members ENABLE ROW LEVEL SECURITY; -- projects CREATE POLICY projects_select ON paliad.projects FOR SELECT TO authenticated USING (paliad.can_see_project(id)); -- INSERT: creating a root project is open to any authenticated user. -- Creating a child requires visibility on the parent. The service must -- add the creator to project_teams (role='lead') in the same transaction -- so the creator can see the row afterwards. CREATE POLICY projects_insert ON paliad.projects FOR INSERT TO authenticated WITH CHECK ( parent_id IS NULL OR paliad.can_see_project(parent_id) ); CREATE POLICY projects_update ON paliad.projects FOR UPDATE TO authenticated USING (paliad.can_see_project(id)) WITH CHECK (paliad.can_see_project(id)); -- Delete: team visibility + admin/lead role. Cascade walks down the tree. CREATE POLICY projects_delete ON paliad.projects FOR DELETE TO authenticated USING ( paliad.can_see_project(id) AND EXISTS ( SELECT 1 FROM paliad.users u WHERE u.id = auth.uid() AND u.role IN ('partner','admin') ) ); -- project_teams: everyone on the team (including ancestor-inherited members) -- can list and modify the team. Anyone with visibility on the project can -- insert themselves as the initial member (used by the creator-add-self -- flow). Partner/admin can remove. CREATE POLICY project_teams_select ON paliad.project_teams FOR SELECT TO authenticated USING (paliad.can_see_project(project_id)); CREATE POLICY project_teams_insert ON paliad.project_teams FOR INSERT TO authenticated WITH CHECK ( -- creator adding self, or already-a-member adding someone else user_id = auth.uid() OR paliad.can_see_project(project_id) ); CREATE POLICY project_teams_update ON paliad.project_teams FOR UPDATE TO authenticated USING (paliad.can_see_project(project_id)) WITH CHECK (paliad.can_see_project(project_id)); CREATE POLICY project_teams_delete ON paliad.project_teams FOR DELETE TO authenticated USING ( paliad.can_see_project(project_id) AND ( user_id = auth.uid() -- remove self OR EXISTS ( SELECT 1 FROM paliad.users u WHERE u.id = auth.uid() AND u.role IN ('partner','admin') ) ) ); -- departments: any authenticated user can read. Only admins write. CREATE POLICY departments_select ON paliad.departments FOR SELECT TO authenticated USING (true); CREATE POLICY departments_write ON paliad.departments FOR ALL TO authenticated USING ( EXISTS (SELECT 1 FROM paliad.users u WHERE u.id = auth.uid() AND u.role = 'admin') ) WITH CHECK ( EXISTS (SELECT 1 FROM paliad.users u WHERE u.id = auth.uid() AND u.role = 'admin') ); CREATE POLICY department_members_select ON paliad.department_members FOR SELECT TO authenticated USING (true); CREATE POLICY department_members_write ON paliad.department_members FOR ALL TO authenticated USING ( user_id = auth.uid() OR EXISTS (SELECT 1 FROM paliad.users u WHERE u.id = auth.uid() AND u.role = 'admin') ) WITH CHECK ( user_id = auth.uid() OR EXISTS (SELECT 1 FROM paliad.users u WHERE u.id = auth.uid() AND u.role = 'admin') ); -- ============================================================================ -- 11. RLS: rebuild child-table policies against project_id / can_see_project. -- ============================================================================ CREATE POLICY parties_all ON paliad.parties FOR ALL TO authenticated USING (paliad.can_see_project(project_id)) WITH CHECK (paliad.can_see_project(project_id)); CREATE POLICY deadlines_all ON paliad.deadlines FOR ALL TO authenticated USING (paliad.can_see_project(project_id)) WITH CHECK (paliad.can_see_project(project_id)); -- appointments — project_id nullable; personal (NULL) = creator-only. CREATE POLICY appointments_select ON paliad.appointments FOR SELECT TO authenticated USING ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) ); CREATE POLICY appointments_insert ON paliad.appointments FOR INSERT TO authenticated WITH CHECK ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) ); CREATE POLICY appointments_update ON paliad.appointments FOR UPDATE TO authenticated USING ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) ) WITH CHECK ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) ); CREATE POLICY appointments_delete ON paliad.appointments FOR DELETE TO authenticated USING ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) ); CREATE POLICY documents_all ON paliad.documents FOR ALL TO authenticated USING (paliad.can_see_project(project_id)) WITH CHECK (paliad.can_see_project(project_id)); ALTER TABLE paliad.project_events ENABLE ROW LEVEL SECURITY; CREATE POLICY project_events_all ON paliad.project_events FOR ALL TO authenticated USING (paliad.can_see_project(project_id)) WITH CHECK (paliad.can_see_project(project_id)); -- notes — polymorphic parent dispatch. CREATE POLICY notes_all ON paliad.notes FOR ALL TO authenticated USING (paliad.note_is_visible(project_id, deadline_id, appointment_id, project_event_id)) WITH CHECK (paliad.note_is_visible(project_id, deadline_id, appointment_id, project_event_id)); -- checklist_instances — mirrors appointments: personal (NULL) = creator-only. CREATE POLICY checklist_instances_select ON paliad.checklist_instances FOR SELECT TO authenticated USING ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) ); CREATE POLICY checklist_instances_insert ON paliad.checklist_instances FOR INSERT TO authenticated WITH CHECK ( created_by = auth.uid() AND (project_id IS NULL OR paliad.can_see_project(project_id)) ); CREATE POLICY checklist_instances_update ON paliad.checklist_instances FOR UPDATE TO authenticated USING ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) ) WITH CHECK ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) ); CREATE POLICY checklist_instances_delete ON paliad.checklist_instances FOR DELETE TO authenticated USING ( (project_id IS NULL AND created_by = auth.uid()) OR (project_id IS NOT NULL AND paliad.can_see_project(project_id)) );