Files
alpenwerk-hr/db/migrations/20260910120000_abos_gelten_auch_fuer_entwuerfe.sql
Maximilian Stubhan eeaf210e78
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m47s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m10s
Let the same choice open notes and drafts
The picker in the bell now governs both lists, so note_subscriptions is
renamed to colleague_subscriptions -- a name that only mentions notes would
mislead the next reader.

Reading and writing a draft now reach differently far. hire_drafts_owner
(for all) is split into four policies: select lets in your own drafts and
those of the people you added, while insert/update/delete stay with the
owner. A draft is unfinished work with no lock and no history; two people
writing into the same row would overwrite each other silently.

That split forces a change in the actions: a policy does not reject a write,
it lets it hit no rows. saveHireDraft and deleteHireDraft now read the row
count instead of reporting success over a row that never changed.

The card shows a foreign draft with its author and without Fortsetzen or
Loeschen -- offering a button that reliably ends in a database error is a
promise without cover.

check-schema-types.mjs learns `alter table ... rename to`; without it the
drift check reports one rename as two errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:05:40 +02:00

119 lines
5.2 KiB
SQL

-- Die Auswahl der Kolleg:innen gilt jetzt für Notizen **und** Entwürfe.
--
-- ═══ 1. Der Name stimmt nicht mehr ═══════════════════════════════
--
-- `note_subscriptions` hiess nach dem, wofür die Auswahl gestern allein
-- galt. Sie steuert ab jetzt zwei Listen; ein Name, der nur eine davon
-- nennt, führt beim nächsten Lesen in die Irre.
alter table note_subscriptions rename to colleague_subscriptions;
alter index idx_note_subscriptions_user rename to idx_colleague_subscriptions_user;
alter table colleague_subscriptions
rename constraint chk_note_abos_nicht_selbst to chk_kollegen_abos_nicht_selbst;
drop policy if exists "note_subscriptions_owner" on colleague_subscriptions;
drop policy if exists "colleague_subscriptions_owner" on colleague_subscriptions;
create policy "colleague_subscriptions_owner" on colleague_subscriptions
for all
using (user_id = app_current_user_id() and is_hr_user())
with check (user_id = app_current_user_id() and is_hr_user());
comment on table colleague_subscriptions is
'Hinzugewählte Kolleg:innen je Person. Eine Zeile holt deren Notizen und Entwürfe in die Ansicht von user_id. Ohne Zeile sieht man nur die eigenen.';
-- ═══ 2. Entwürfe: lesen weiter, schreiben eng ════════════════════
--
-- Bisher stand über hire_drafts eine einzige Regel `for all`. Sie wird in
-- vier zerlegt, weil Lesen und Schreiben ab jetzt verschieden weit reichen:
--
-- lesen — die eigenen und die der hinzugewählten Kolleg:innen
-- anlegen — nur auf den eigenen Namen
-- ändern — nur die eigenen
-- löschen — nur die eigenen
--
-- Warum das Schreiben eng bleibt: ein Entwurf ist unfertige Arbeit. Zwei
-- Personen, die abwechselnd in derselben Zeile schreiben, überschreiben
-- einander lautlos — es gibt keine Sperre und keine Historie, die das
-- auffangen könnte. Wer einen fremden Entwurf übernehmen soll, braucht dafür
-- einen eigenen Vorgang, keine stillschweigend geöffnete Regel.
--
-- ═══ Was das erzwingt ═══
--
-- Solange fremde Entwürfe unsichtbar waren, konnte niemand versuchen, einen
-- zu speichern. Jetzt schon — und ein UPDATE, das die Regel abweist, trifft
-- keine Zeile und meldet trotzdem keinen Fehler. Die Anwendung muss die Zahl
-- der geänderten Zeilen prüfen (actions/hireDrafts.ts), sonst sähe die
-- Person „gespeichert", und nichts wäre gespeichert.
drop policy if exists "hire_drafts_owner" on hire_drafts;
drop policy if exists "hire_drafts_select" on hire_drafts;
create policy "hire_drafts_select" on hire_drafts
for select
using (
is_hr_user()
and (
created_by = app_current_user_id()
or exists (
select 1 from colleague_subscriptions s
where s.user_id = app_current_user_id()
and s.author_user_id = hire_drafts.created_by
)
)
);
drop policy if exists "hire_drafts_insert" on hire_drafts;
create policy "hire_drafts_insert" on hire_drafts
for insert
with check (created_by = app_current_user_id() and is_hr_user());
drop policy if exists "hire_drafts_update" on hire_drafts;
create policy "hire_drafts_update" on hire_drafts
for update
using (created_by = app_current_user_id() and is_hr_user())
with check (created_by = app_current_user_id() and is_hr_user());
drop policy if exists "hire_drafts_delete" on hire_drafts;
create policy "hire_drafts_delete" on hire_drafts
for delete
using (created_by = app_current_user_id() and is_hr_user());
-- ═══ Gegenprobe ═══════════════════════════════════════════════════
do $$
declare
anzahl int;
begin
if exists (select 1 from pg_tables where tablename = 'note_subscriptions') then
raise exception 'note_subscriptions steht noch — die Umbenennung hat nicht gegriffen.';
end if;
if not exists (
select 1 from pg_policies
where tablename = 'colleague_subscriptions' and policyname = 'colleague_subscriptions_owner'
) then
raise exception 'Die Eigentuemerregel auf colleague_subscriptions fehlt.';
end if;
-- Die alte Sammelregel darf nicht übrigbleiben: sie erlaubte `for all`
-- und machte die vier neuen wirkungslos, weil Policies sich addieren.
if exists (select 1 from pg_policies where tablename = 'hire_drafts' and policyname = 'hire_drafts_owner') then
raise exception 'hire_drafts_owner steht noch — die alte Sammelregel wuerde die neuen aushebeln.';
end if;
select count(*) into anzahl from pg_policies where tablename = 'hire_drafts';
if anzahl <> 4 then
raise exception 'hire_drafts hat % Regeln, erwartet werden 4 (select, insert, update, delete).', anzahl;
end if;
-- Keine der drei Schreibregeln darf die Abos kennen. Stünde dort dieselbe
-- Bedingung wie beim Lesen, liesse sich ein fremder Entwurf überschreiben.
if exists (
select 1 from pg_policies
where tablename = 'hire_drafts'
and cmd in ('INSERT', 'UPDATE', 'DELETE')
and coalesce(qual, '') || coalesce(with_check, '') like '%colleague_subscriptions%'
) then
raise exception 'Eine Schreibregel auf hire_drafts kennt die Abos — Schreiben muss eigentuemergebunden bleiben.';
end if;
end $$;