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>
119 lines
5.2 KiB
SQL
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 $$;
|