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>
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { currentUserId } from "@/lib/auth/session";
|
|
import { withUser } from "@/lib/db";
|
|
import type { ActionResult } from "@/lib/db/rpc";
|
|
|
|
export async function saveHireDraft(payload: {
|
|
id?: string;
|
|
step: number;
|
|
data: Record<string, unknown>;
|
|
}): Promise<ActionResult & { id?: string }> {
|
|
const userId = await currentUserId();
|
|
if (!userId) return { success: false, error: "Nicht angemeldet." };
|
|
|
|
try {
|
|
const id = await withUser(userId, async (tx) => {
|
|
if (payload.id) {
|
|
// 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)
|
|
.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;
|
|
}
|
|
|
|
const row = await tx
|
|
.insertInto("hire_drafts")
|
|
.values({ created_by: userId, step: payload.step, payload: payload.data })
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow();
|
|
return row.id;
|
|
});
|
|
|
|
revalidatePath("/");
|
|
return { success: true, id };
|
|
} catch (err) {
|
|
return { success: false, error: err instanceof Error ? err.message : "Unbekannter Fehler." };
|
|
}
|
|
}
|
|
|
|
export async function deleteHireDraft(id: string): Promise<ActionResult> {
|
|
try {
|
|
// 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) {
|
|
return { success: false, error: err instanceof Error ? err.message : "Unbekannter Fehler." };
|
|
}
|
|
}
|