Yesterday's version had it the other way — everyone visible, untick to
hide. The decision from the business side is the opposite: you see your
own notes, and you tick the colleagues you also want. So note_mutes
becomes note_subscriptions and the predicate flips from `not exists` to
`exists`.
The existing rows are not carried over. The meaning inverts rather than
the sign: converting faithfully ("everyone except the muted") would write
almost the whole roster into the new table and reproduce exactly the state
the change is meant to end. Anyone opening the setting tomorrow would
think it had not taken effect. The table is a day old; what is lost is a
few ticks from trying it out.
What this costs is worth saying plainly: the silent case that could not
happen under exceptions can happen now. Do not tick a colleague and you
will not see her follow-ups — not while she is on holiday either. That is
the flip side of the decision, and it is written down in the migration
rather than discovered later.
Each note now says who wrote it. Own notes read "von mir" rather than
repeating your own name, which would sit on every second line and tell
nobody anything. The flag is computed on the server: the user id is
already there, and threading it through four components for one word is a
poor trade. The counter on the button follows the same turn — "+2" for
what you added, nothing when you added nothing.
Verified: 21 tests, five mutation-checked (restoring `not exists`,
dropping the own-notes clause, inverting the default, hiding the author,
and printing your own name instead of "von mir" each turn them red). 489
tests, typecheck, lint, schema drift and build clean. The migration is
reviewed but not run — no reachable database here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from "kysely";
|
|
import { describe, expect, it } from "vitest";
|
|
import type { Schema } from "@/lib/db/schema";
|
|
import { sichtbareNotizen } from "@/lib/notes";
|
|
import { baueKollegen } from "@/lib/shell-data";
|
|
|
|
// Wessen Notizen jemand sieht, ist die eine Stelle, an der ein Fehler nicht
|
|
// auffällt: die Glocke zeigt weiter eine Zahl, nur die falsche. Zu wenig, und
|
|
// eine Wiedervorlage bleibt liegen; zu viel, und die Einstellung wirkt nicht.
|
|
// Deshalb wird hier die Abfrage gelesen, die tatsächlich herauskommt.
|
|
|
|
const db = new Kysely<Schema>({
|
|
dialect: {
|
|
createAdapter: () => new PostgresAdapter(),
|
|
createDriver: () => new DummyDriver(),
|
|
createIntrospector: (d) => new PostgresIntrospector(d),
|
|
createQueryCompiler: () => new PostgresQueryCompiler(),
|
|
},
|
|
});
|
|
|
|
const ICH = "11111111-1111-1111-1111-111111111111";
|
|
|
|
function abfrage(userId = ICH) {
|
|
return db
|
|
.selectFrom("employee_notes as n")
|
|
.select("n.id")
|
|
.where(sichtbareNotizen(userId))
|
|
.compile();
|
|
}
|
|
|
|
const sql = (userId?: string) => abfrage(userId).sql.replace(/\s+/g, " ");
|
|
|
|
describe("sichtbareNotizen", () => {
|
|
it("lässt Notizen ohne Verfasser durch", () => {
|
|
// author_user_id kann leer sein. Eine Notiz auszublenden, weil niemand
|
|
// weiss, von wem sie ist, wäre genau der stille Verlust, den die Liste
|
|
// verhindern soll.
|
|
expect(sql()).toContain('"n"."author_user_id" is null');
|
|
});
|
|
|
|
it("lässt die eigenen Notizen immer durch", () => {
|
|
expect(sql()).toContain('"n"."author_user_id" = $');
|
|
});
|
|
|
|
it("holt dazu, wer ausdrücklich hinzugewählt wurde", () => {
|
|
// Vorzeichen und Tabelle zusammen: ein `not exists` auf derselben
|
|
// Tabelle kehrte die Bedeutung um, ohne dass ein Wort sich änderte.
|
|
const s = sql();
|
|
expect(s).toContain("exists");
|
|
expect(s).not.toContain("not exists");
|
|
expect(s).toContain("from note_subscriptions s");
|
|
expect(s).toContain("s.user_id = $");
|
|
expect(s).toContain("s.author_user_id = \"n\".\"author_user_id\"");
|
|
});
|
|
|
|
it("verknüpft die drei Fälle mit ODER, nicht mit UND", () => {
|
|
// Mit UND sähe niemand mehr etwas: keine Notiz ist gleichzeitig ohne
|
|
// Verfasser und von mir.
|
|
const s = sql();
|
|
expect(s).toMatch(/is null\s+or/);
|
|
expect(s).not.toMatch(/is null\s+and/);
|
|
});
|
|
|
|
it("bindet die Kennung als Parameter, nicht in den Text", () => {
|
|
// Sie kommt aus der Sitzung, nicht aus der Adresse — trotzdem hat sie im
|
|
// Abfragetext nichts verloren.
|
|
const { sql: text, parameters } = abfrage("bösartig'; drop table employee_notes; --");
|
|
expect(text).not.toContain("drop table");
|
|
expect(parameters).toContain("bösartig'; drop table employee_notes; --");
|
|
});
|
|
|
|
it("nennt die Kennung zweimal — für die eigenen Notizen und für die Ausnahmen", () => {
|
|
expect(abfrage().parameters.filter((p) => p === ICH)).toHaveLength(2);
|
|
});
|
|
|
|
it("lässt sich auf einen anderen Aliasnamen setzen", () => {
|
|
// Die Übersicht bindet dieselbe Tabelle ein; käme sie je unter anderem
|
|
// Namen, muss die Regel mitkönnen statt kopiert zu werden.
|
|
const s = db
|
|
.selectFrom("employee_notes as notiz")
|
|
.select("notiz.id")
|
|
.where(sichtbareNotizen(ICH, "notiz.author_user_id"))
|
|
.compile().sql;
|
|
expect(s).toContain('"notiz"."author_user_id"');
|
|
});
|
|
});
|
|
|
|
describe("baueKollegen", () => {
|
|
const leute = [
|
|
{ id: "a", full_name: "Anna Berger", email: "a@example.test" },
|
|
{ id: "b", full_name: null, email: "b@example.test" },
|
|
{ id: "c", full_name: " ", email: "c@example.test" },
|
|
];
|
|
|
|
it("lässt ohne Auswahl alle Haken leer", () => {
|
|
// Die Vorgabe ist die eigene Person; wer mehr will, wählt hinzu.
|
|
expect(baueKollegen(leute, []).map((k) => k.sichtbar)).toEqual([false, false, false]);
|
|
});
|
|
|
|
it("setzt den Haken, wo ein Abo steht", () => {
|
|
expect(baueKollegen(leute, ["b"]).map((k) => [k.id, k.sichtbar])).toEqual([
|
|
["a", false],
|
|
["b", true],
|
|
["c", false],
|
|
]);
|
|
});
|
|
|
|
it("fällt ohne Namen auf die E-Mail zurück", () => {
|
|
// Ein Haken ohne Beschriftung wäre einer, von dem niemand weiss, wen er
|
|
// betrifft. Auch ein Name aus Leerzeichen zählt als keiner.
|
|
expect(baueKollegen(leute, []).map((k) => k.name)).toEqual([
|
|
"Anna Berger",
|
|
"b@example.test",
|
|
"c@example.test",
|
|
]);
|
|
});
|
|
|
|
it("kommt mit einem Abo zurecht, zu dem es niemanden mehr gibt", () => {
|
|
// Wer die Personalabteilung verlässt, verschwindet aus der Liste; die
|
|
// Zeile bleibt, bis der Fremdschlüssel sie räumt.
|
|
expect(baueKollegen(leute, ["längst-weg"])).toHaveLength(3);
|
|
});
|
|
});
|