Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
124 lines
4.2 KiB
TypeScript
124 lines
4.2 KiB
TypeScript
import { isInputField, type TemplateField } from '@/lib/templates/fields'
|
|
import { redactForLevel, type Segment } from '@/lib/redaction'
|
|
import { AccessError, ConflictError } from '@/lib/errors'
|
|
import type { EffectivePermissions } from '@/lib/auth/permissions'
|
|
import type { Prisma } from '@afis/db'
|
|
|
|
/**
|
|
* Abschnitte: der ausgefüllte Inhalt einer Vorlage.
|
|
*
|
|
* Eine Fallakte trägt sie direkt, ein Dokument ebenso. Die Regeln sind
|
|
* dieselben, deshalb stehen sie hier einmal und nicht zweimal: welche Stufe
|
|
* ein Feld verlangt, was ein zu niedrig eingestufter Leser bekommt, und wann
|
|
* ein Speichern ein Konflikt ist.
|
|
*/
|
|
|
|
/** Was ein Leser sieht, dessen Stufe für ein Feld nicht reicht. */
|
|
export type Placeholder = { redacted: true; requiredLevel: number }
|
|
|
|
/** Die Spalten, die `CaseSection` und `DocumentSection` gemeinsam haben. */
|
|
export type SectionRow = {
|
|
fieldKey: string
|
|
value: unknown
|
|
minClearance: number
|
|
currentVersion: number
|
|
updatedAt: Date
|
|
updatedById: string | null
|
|
}
|
|
|
|
export type RenderedSection = {
|
|
fieldKey: string
|
|
field: TemplateField
|
|
version: number
|
|
updatedAt: Date | null
|
|
updatedById: string | null
|
|
} & (
|
|
| { visible: true; value: unknown; segments: Segment[] | null }
|
|
| { visible: false; value: Placeholder }
|
|
)
|
|
|
|
/**
|
|
* Prisma trennt „Feld nicht setzen" von „JSON null". Ein leerer Abschnitt ist
|
|
* inhaltlich JSON null, nicht das Fehlen des Feldes.
|
|
*/
|
|
export function asJson(value: unknown): Prisma.InputJsonValue {
|
|
return (value ?? null) as Prisma.InputJsonValue
|
|
}
|
|
|
|
/**
|
|
* Wendet die Stufe des Lesers auf die Abschnitte an.
|
|
*
|
|
* Felder über seiner Stufe werden **hier**, also serverseitig, durch einen
|
|
* Platzhalter ersetzt. Der echte Wert verlässt den Server nicht - nie im
|
|
* Client ausblenden. Dasselbe gilt für Schwärzungen im Fließtext, die als
|
|
* Segmente zurückkommen.
|
|
*/
|
|
export function renderSections(
|
|
fields: TemplateField[],
|
|
rows: SectionRow[],
|
|
p: EffectivePermissions,
|
|
): RenderedSection[] {
|
|
const byKey = new Map(rows.map((row) => [row.fieldKey, row]))
|
|
|
|
return fields.map((field): RenderedSection => {
|
|
const row = byKey.get(field.key)
|
|
const required = Math.max(field.minClearance ?? 0, row?.minClearance ?? 0)
|
|
const common = {
|
|
fieldKey: field.key,
|
|
field,
|
|
version: row?.currentVersion ?? 0,
|
|
updatedAt: row?.updatedAt ?? null,
|
|
updatedById: row?.updatedById ?? null,
|
|
}
|
|
|
|
if (!p.isAdmin && required > p.clearanceLevel) {
|
|
return { ...common, visible: false, value: { redacted: true, requiredLevel: required } }
|
|
}
|
|
|
|
const value = row?.value ?? null
|
|
const segments =
|
|
isInputField(field) && field.type === 'longtext' && typeof value === 'string'
|
|
? redactForLevel(value, p.clearanceLevel)
|
|
: null
|
|
|
|
return { ...common, visible: true, value, segments }
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Prüft ein Speichern, bevor es geschrieben wird, und liefert die neue
|
|
* Feldstufe und Versionsnummer.
|
|
*
|
|
* Wirft `AccessError`, wenn die Stufe nicht reicht, und `ConflictError`, wenn
|
|
* der Abschnitt sich seit dem Laden verändert hat. Beides gilt für Fallakten
|
|
* und Dokumente gleichermaßen.
|
|
*/
|
|
export function planSectionSave(input: {
|
|
p: EffectivePermissions
|
|
field: TemplateField
|
|
existing: Pick<SectionRow, 'minClearance' | 'currentVersion' | 'value' | 'updatedById'> | null
|
|
baseVersion: number
|
|
minClearance?: number
|
|
}): { nextClearance: number; nextVersion: number } {
|
|
const { p, field, existing } = input
|
|
|
|
if (!isInputField(field)) throw new AccessError('field-unknown')
|
|
|
|
// Wer ein Feld nicht lesen darf, darf es auch nicht überschreiben.
|
|
const required = Math.max(field.minClearance ?? 0, existing?.minClearance ?? 0)
|
|
if (!p.isAdmin && required > p.clearanceLevel) throw new AccessError('clearance-too-low')
|
|
|
|
const nextClearance = input.minClearance ?? existing?.minClearance ?? field.minClearance ?? 0
|
|
if (nextClearance > p.clearanceLevel && !p.isAdmin) throw new AccessError('clearance-too-low')
|
|
|
|
if ((existing?.currentVersion ?? 0) !== input.baseVersion) {
|
|
throw new ConflictError({
|
|
value: existing?.value ?? null,
|
|
version: existing?.currentVersion ?? 0,
|
|
updatedById: existing?.updatedById ?? null,
|
|
})
|
|
}
|
|
|
|
return { nextClearance, nextVersion: (existing?.currentVersion ?? 0) + 1 }
|
|
}
|