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>
This commit is contained in:
@@ -16,13 +16,22 @@ export async function saveHireDraft(payload: {
|
||||
try {
|
||||
const id = await withUser(userId, async (tx) => {
|
||||
if (payload.id) {
|
||||
// Ob die Zeile der aufrufenden Person gehört, entscheidet die
|
||||
// Policy hire_drafts_owner — nicht eine Prüfung hier.
|
||||
await tx
|
||||
// Ob die Zeile der aufrufenden Person gehört, entscheidet die Regel
|
||||
// hire_drafts_update — nicht eine Prüfung hier.
|
||||
//
|
||||
// Die Zahl der geänderten Zeilen wird trotzdem gelesen, und zwar
|
||||
// seit fremde Entwürfe sichtbar sind: eine Regel weist ein UPDATE
|
||||
// nicht mit einem Fehler ab, sie lässt es ins Leere laufen. Ohne
|
||||
// diese Prüfung meldete die Anwendung „gespeichert", und gespeichert
|
||||
// wäre nichts.
|
||||
const ergebnis = await tx
|
||||
.updateTable("hire_drafts")
|
||||
.set({ step: payload.step, payload: payload.data, updated_at: new Date().toISOString() })
|
||||
.where("id", "=", payload.id)
|
||||
.execute();
|
||||
.executeTakeFirst();
|
||||
if (ergebnis.numUpdatedRows === 0n) {
|
||||
throw new Error("Der Entwurf liess sich nicht speichern: er gehört jemand anderem oder wurde inzwischen gelöscht.");
|
||||
}
|
||||
return payload.id;
|
||||
}
|
||||
|
||||
@@ -43,7 +52,15 @@ export async function saveHireDraft(payload: {
|
||||
|
||||
export async function deleteHireDraft(id: string): Promise<ActionResult> {
|
||||
try {
|
||||
await withUser(await currentUserId(), (tx) => tx.deleteFrom("hire_drafts").where("id", "=", id).execute());
|
||||
// Auch hier die Zeilenzahl: die Regel hire_drafts_delete lässt ein
|
||||
// fremdes Löschen leer laufen statt es abzuweisen. „Entwurf gelöscht"
|
||||
// über einem Entwurf, der noch dasteht, wäre die schlechtere Auskunft.
|
||||
await withUser(await currentUserId(), async (tx) => {
|
||||
const ergebnis = await tx.deleteFrom("hire_drafts").where("id", "=", id).executeTakeFirst();
|
||||
if (ergebnis.numDeletedRows === 0n) {
|
||||
throw new Error("Der Entwurf liess sich nicht löschen: er gehört jemand anderem oder war schon weg.");
|
||||
}
|
||||
});
|
||||
revalidatePath("/");
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
|
||||
@@ -6,7 +6,12 @@ import { withUser } from "@/lib/db";
|
||||
import type { ActionResult } from "@/lib/db/rpc";
|
||||
|
||||
/**
|
||||
* Notizen einer Kollegin oder eines Kollegen hinzuwählen oder abwählen.
|
||||
* Eine Kollegin oder einen Kollegen hinzuwählen oder abwählen.
|
||||
*
|
||||
* Die Auswahl steuert zwei Listen: die Notizen in der Glocke und die
|
||||
* Entwürfe auf der Übersicht. Der Name der Funktion nennt nur die erste,
|
||||
* weil die Einstellung in der Glocke sitzt — was sie bewirkt, steht an der
|
||||
* Einstellung selbst.
|
||||
*
|
||||
* Kein Aufruf einer SQL-Funktion und kein Protokolleintrag, anders als bei
|
||||
* allem, was Personaldaten ändert: das hier ist eine persönliche
|
||||
@@ -15,7 +20,7 @@ import type { ActionResult } from "@/lib/db/rpc";
|
||||
* Dasselbe Muster wie bei gespeicherten Auswertungen und Entwürfen
|
||||
* (actions/reports.ts, actions/hireDrafts.ts).
|
||||
*
|
||||
* Abgesichert ist es trotzdem: die Regel `note_subscriptions_owner` lässt nur Zeilen
|
||||
* Abgesichert ist es trotzdem: die Regel `colleague_subscriptions_owner` lässt nur Zeilen
|
||||
* zu, deren `user_id` die angemeldete Person ist. Eine fremde Einstellung
|
||||
* liesse sich auch mit erfundenen Werten nicht schreiben.
|
||||
*/
|
||||
@@ -38,13 +43,13 @@ export async function setNotizSichtbarkeit(payload: {
|
||||
// `on conflict do nothing`: zweimal dasselbe Hinzuwählen ist kein
|
||||
// Fehler, sondern derselbe Wunsch — etwa wenn zwei Reiter offen sind.
|
||||
await tx
|
||||
.insertInto("note_subscriptions")
|
||||
.insertInto("colleague_subscriptions")
|
||||
.values({ user_id: userId, author_user_id: payload.kollegeId })
|
||||
.onConflict((oc) => oc.columns(["user_id", "author_user_id"]).doNothing())
|
||||
.execute();
|
||||
} else {
|
||||
await tx
|
||||
.deleteFrom("note_subscriptions")
|
||||
.deleteFrom("colleague_subscriptions")
|
||||
.where("user_id", "=", userId)
|
||||
.where("author_user_id", "=", payload.kollegeId)
|
||||
.execute();
|
||||
|
||||
Reference in New Issue
Block a user