refactor(api): share owned resource read access

This commit is contained in:
2026-04-01 07:35:34 +02:00
parent a0c98cf24d
commit 41916a4e46
7 changed files with 336 additions and 118 deletions
+2 -2
View File
@@ -31,14 +31,14 @@ Done
- `project` - `project`
Ready next Ready next
- none in the conflict-safe backlog - `resource-read-shared`
Deferred or blocked Deferred or blocked
- `assistant-tools` - `assistant-tools`
- `resource-read-shared`
Notes Notes
- Router files should contain only tRPC wiring. - Router files should contain only tRPC wiring.
- Procedure orchestration and router input schemas belong in `*-procedure-support.ts`. - Procedure orchestration and router input schemas belong in `*-procedure-support.ts`.
- Pure builders and domain helpers remain in existing `*-support.ts` modules. - Pure builders and domain helpers remain in existing `*-support.ts` modules.
- Targeted router verification passed on 2026-03-31 for `timeline-router` and `vacation-router`. - Targeted router verification passed on 2026-03-31 for `timeline-router` and `vacation-router`.
- Der naechste konfliktarme Architektur-Slice ist die Zerlegung von `resource-read-shared.ts` in fokussierte Read-Helper bei stabilem API-Vertrag.
+4 -2
View File
@@ -156,9 +156,11 @@ Abgedeckt werden damit insbesondere:
Der aktuelle Split-Runner wurde gegen die genannten Batches, die dedizierten Gap-Closure-Suiten und den API-Typecheck validiert. Der aktuelle Split-Runner wurde gegen die genannten Batches, die dedizierten Gap-Closure-Suiten und den API-Typecheck validiert.
## Bekannte Restlücken ## Migration abgeschlossen
Aktuell sind für den migrierten Legacy-Scope keine weiteren isolierten Split-Lücken dokumentiert. Der Legacy-Scope der entfernten Monolith-Suiten ist vollständig auf fachlich geschnittene Split-Suiten gemappt.
Neue Assistant-Regressionen sollen direkt in die fachlich passende Split-Suite eingeordnet werden.
Der explizite Split-Runner bleibt für diesen Scope der kanonische Regressionspfad; breite Glob-Runs und eine Wiederbelebung der alten Monolith-Dateien sind bewusst nicht vorgesehen.
Bewusst noch nicht Teil dieses Dokuments: Bewusst noch nicht Teil dieses Dokuments:
+160 -35
View File
@@ -6,7 +6,7 @@ Dieses Backlog gruppiert die offenen Arbeiten nach fachlichen/domänischen Blöc
Die Schätzungen sind Restaufwand auf Basis des aktuellen Worktrees und können sich nach Befund leicht ändern. Die Schätzungen sind Restaufwand auf Basis des aktuellen Worktrees und können sich nach Befund leicht ändern.
Aktuell geschnittene Domänenslices: `7` Aktuell geschnittene Domänenslices: `7`
Grobe Gesamtdauer über alle noch offenen Slices: `28-45h` Grobe Gesamtdauer über alle noch offenen Slices: `25-41h`
## Statuslegende ## Statuslegende
@@ -20,12 +20,12 @@ Grobe Gesamtdauer über alle noch offenen Slices: `28-45h`
| # | Slice | Status | Restaufwand | Kernziel | | # | Slice | Status | Restaufwand | Kernziel |
|---|---|---|---:|---| |---|---|---|---:|---|
| 1 | Timeline Interaction, Overlays, Hover, SSE-State | in_progress | 6-10h | Stabile Timeline ohne Render-/Overlay-/Drag-Artefakte, auch bei View-Wechseln und Live-Updates | | 1 | Timeline Interaction, Overlays, Hover, SSE-State | in_progress | 6-10h | Stabile Timeline ohne Render-/Overlay-/Drag-Artefakte, auch bei View-Wechseln und Live-Updates |
| 2 | Holiday/Vacation Correctness and Explainability | ready | 4-6h | Feiertage und Urlaube konsistent in Timeline, Chargeability, Forecasts, Assistant und UI-Erklärungen | | 2 | Holiday/Vacation Correctness and Explainability | in_progress | 1-3h | Feiertage und Urlaube konsistent in Timeline, Chargeability, Forecasts, Assistant und UI-Erklärungen |
| 3 | Assistant Parity, Policy Auto-Exposure, Advanced Queries | ready | 5-8h | Assistant kennt und nutzt denselben Berechtigungs- und Datenraum wie der Nutzer, inkl. komplexer Queries | | 3 | Assistant Parity, Policy Auto-Exposure, Advanced Queries | in_progress | 5-8h | Assistant kennt und nutzt denselben Berechtigungs- und Datenraum wie der Nutzer, inkl. komplexer Queries |
| 4 | Reports, Templates/Blueprints, Export Completeness | ready | 4-6h | Report Builder mit speicherbaren Vorlagen und vollständigen Exportgrößen wie SAH und Bezugsfaktoren | | 4 | Reports, Templates/Blueprints, Export Completeness | in_progress | 4-6h | Report Builder mit speicherbaren Vorlagen und vollständigen Exportgrößen wie SAH und Bezugsfaktoren |
| 5 | Notifications, Tasks, Broadcasts, Reminder Reliability | ready | 3-5h | Robuste Inbox-/Broadcast-/Reminder-Flows ohne leere Fanout- oder Persistenzkanten | | 5 | Notifications, Tasks, Broadcasts, Reminder Reliability | in_progress | 3-5h | Robuste Inbox-/Broadcast-/Reminder-Flows ohne leere Fanout- oder Persistenzkanten |
| 6 | Dashboard, Widgets, Explainability, First-Load Stability | ready | 4-6h | Dashboard zuverlässig ladbar, Widgets kompakt, nachvollziehbar und per Detail-Toggle kontrollierbar | | 6 | Dashboard, Widgets, Explainability, First-Load Stability | in_progress | 4-6h | Dashboard zuverlässig ladbar, Widgets kompakt, nachvollziehbar und per Detail-Toggle kontrollierbar |
| 7 | DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup | ready | 2-4h | Keine Raw-Prisma/Env-Fallen, kein versehentliches Seeding, keine operativen `planarchy`-Reste | | 7 | DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup | in_progress | 2-4h | Keine Raw-Prisma/Env-Fallen, kein versehentliches Seeding, keine operativen `planarchy`-Reste |
## Umsetzungshinweise ## Umsetzungshinweise
@@ -34,21 +34,138 @@ Grobe Gesamtdauer über alle noch offenen Slices: `28-45h`
- Slice 4 profitiert von Slice 2, weil Feiertags-/Urlaubslogik als Export- und Reportgröße sichtbar werden muss. - Slice 4 profitiert von Slice 2, weil Feiertags-/Urlaubslogik als Export- und Reportgröße sichtbar werden muss.
- Slice 7 bleibt als Guardrail-Block parallel im Hintergrund wichtig, sollte aber keine Produktarbeit blockieren. - Slice 7 bleibt als Guardrail-Block parallel im Hintergrund wichtig, sollte aber keine Produktarbeit blockieren.
## Arbeitsliste
1. Slice 1: Timeline Interaction, Overlays, Hover, SSE-State
2. Slice 2: Holiday/Vacation Correctness and Explainability
3. Slice 3: Assistant Parity, Policy Auto-Exposure, Advanced Queries
4. Slice 4: Reports, Templates/Blueprints, Export Completeness
5. Slice 5: Notifications, Tasks, Broadcasts, Reminder Reliability
6. Slice 6: Dashboard, Widgets, Explainability, First-Load Stability
7. Slice 7: DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup
## Abhaklog ## Abhaklog
- [x] Assistant-Testlandschaft entlang echter Domänengrenzen weiter zerlegt und vollständig grün validiert - [x] Assistant-Testlandschaft entlang echter Domänengrenzen weiter zerlegt und vollständig grün validiert
- [x] Staffing Read Pipeline Split: gemeinsame holiday-/absence-aware Capacity-Zusammenfassung aus `staffing-suggestions-read.ts` und `staffing-best-project-resource.ts` in `staffing-capacity-summary.ts` extrahiert und gegen fokussierte Staffing-/Assistant-Regressionen validiert
- [x] Vacation Read Separation: Holiday-/Deduction-Preview-Shaping aus `vacation-read.ts` in `vacation-read-support.ts` extrahiert und gegen Vacation-Auth-/Router-Regressionen validiert
- [ ] Slice 1: Timeline Interaction, Overlays, Hover, SSE-State - [ ] Slice 1: Timeline Interaction, Overlays, Hover, SSE-State
Fortschritt: Fortschritt:
Live `cellWidthRef` statt stale Snapshot im Drag-Pfad verdrahtet. Live `cellWidthRef` statt stale Snapshot im Drag-Pfad verdrahtet.
Projekt-Drag zentralisiert finalisiert, sodass MouseUp/Canvas-Up nicht mehr parallel mutieren. Projekt-Drag zentralisiert finalisiert, sodass MouseUp/Canvas-Up nicht mehr parallel mutieren.
`document`-Listener für Projekt-Drag, Allocation-Drag und Multi-Select werden beim Unmount sauber entfernt. `document`-Listener für Projekt-Drag, Allocation-Drag und Multi-Select werden beim Unmount sauber entfernt.
Timeline-Popovers schließen bei Scroll/Resize nicht mehr sofort, sondern bleiben stabil sichtbar. Timeline-Popovers schließen bei Scroll/Resize nicht mehr sofort, sondern bleiben stabil sichtbar.
Query-Refresh wird pro aktivem Timeline-Kontext statt nur einmal pro Mount erzwungen.
Session-Loading und SSE-Reconnect werden gehärtet, damit erste Route-Loads nicht in einem leeren Zwischenzustand hängen bleiben.
Vacation-SSE und lokale Planning-Invalidierung laden jetzt auch `vacation.list` nach, damit Urlaubsbalken ohne Reload konsistent bleiben.
Allocation-Handles und Move-Flächen stoppen jetzt `mousedown` sauber, damit Resource-/Project-Row-Canvases nicht parallel Range-Selektion starten.
- [ ] Slice 2: Holiday/Vacation Correctness and Explainability - [ ] Slice 2: Holiday/Vacation Correctness and Explainability
Fortschritt:
Vacation-Deduction-Snapshots werden im UI bereits in `MyVacationsClient` erklärt.
Dieselbe Ableitung wird jetzt auch im Vacation-Kalender per Hover-Tooltip genutzt.
Holiday-Editor-Vorschau zeigt jetzt Scope-/Source-Zusammenfassung plus aufgeloeste Feiertage mit Scope.
`entitlement.getBalanceDetail`, Assistant-Tool `get_vacation_balance` und `BalanceCard` nutzen jetzt denselben holiday-/deduction-aware Explainability-Pfad inkl. Vacation-Breakdown, Holiday-Basis und ausgeschlossenen Feiertagen.
Fokussierte Validierung für Entitlement-/Assistant-Balance und Web-Typecheck ist grün; Alt-Snapshots respektieren weiter persistierte `deductedDays`, während fehlende Snapshots wieder korrekt auf kanonische Feiertagsauflösung zurückfallen.
Entitlement-Jahresübersicht, Assistant-`get_entitlement_summary` und die Manager-Übersicht tragen jetzt auch `countryCode`, `countryName`, `federalState` und `metroCityName`, damit regionale Feiertagsunterschiede als Bezugsgröße sichtbar bleiben.
Assistant-`run_report` liefert für `resource_month` jetzt zusätzlich einen Explainability-Block mit Holiday-/Absence-/Location-Basis, fehlenden empfohlenen Bezugsgrößen und Interpretationshinweisen zur SAH-Ableitung.
Chargeability-Detailpfad und Assistant-`get_chargeability_report` tragen jetzt ebenfalls einen Explainability-Block mit Ortsbezug, Herleitungsfeldern, Formeln und aktiven Filtern, damit dieselbe SAH-/Holiday-/Absence-Logik auch im Forecast-Readmodel transparent ist.
Chargeability-Frontend zeigt dieselbe Berechnungsbasis jetzt kompakt an; der Excel-Export enthält zusätzlich ein Explainability-Sheet mit Formeln, Filtern und Bezugsgrößen.
Budget-Forecast-Readmodel, Dashboard-Detail und Assistant-`get_budget_forecast` tragen jetzt ebenfalls eine kompakte Burn-Herleitung mit Base-vs-Adjusted-Burn, Holiday-/Absence-Abzügen, Holiday-aware-vs-Fallback-Assignments und Anzahl der Kalenderkontexte; das Widget zeigt diese Herleitung nur im Detailmodus an.
Project-Health-Readmodel, Dashboard-Detail und Assistant-`get_project_health` tragen jetzt ebenfalls eine explizite Budget-Herleitung mit Base-vs-Adjusted-Spend, Holiday-/Absence-Abzügen, Holiday-aware-vs-Fallback-Assignments und Kalenderkontexten; das Widget zeigt diese Basis nur im Detailmodus an.
Nächster Fokus: verbleibende Forecast-/Widget-Parität derselben Ableitungen schließen.
- [ ] Slice 3: Assistant Parity, Policy Auto-Exposure, Advanced Queries - [ ] Slice 3: Assistant Parity, Policy Auto-Exposure, Advanced Queries
Fortschritt:
Tool-Sichtbarkeit und Laufzeit-Guarding auf gemeinsame Registry-/Access-Metadaten gezogen.
Estimate-Tools tragen jetzt explizite Rollen-/Permission-/Cost-Metadaten statt impliziter Alt-Policy.
Fokussierte Assistant-Policy-Regressionstests laufen gegen dieselbe zentrale Quelle.
Tool-Selektion priorisiert Holiday-/Vacation-/Entitlement- sowie Dashboard-/Report-Anfragen jetzt gezielter, damit diese Tools unter dem OpenAI-128-Limit erhalten bleiben.
Top-Level-Chat-/Approval-/Prompt-Orchestrierung ist jetzt in `assistant-procedure-support.ts` und `assistant-system-prompt.ts` extrahiert; API-Typecheck plus fokussierte Assistant-Suite laufen wieder grün.
Produktive AI-Fallbacks, Admin-UI-Platzhalter und Settings-Default im Frontend zeigen jetzt konsistent `gpt-5.4` statt alter `gpt-4o-mini`-Defaults; die fokussierte Assistant-/Settings-/Insights-Suite dafür ist grün validiert.
- [ ] Slice 4: Reports, Templates/Blueprints, Export Completeness - [ ] Slice 4: Reports, Templates/Blueprints, Export Completeness
Fokus:
Report-Templates/Blueprints, SAH-/Bezugsgrößen-Vollständigkeit und Export-Parität zur UI weiterziehen.
Fortschritt:
Report Builder exponiert Template-Beschreibung und Shared-Status jetzt auch im Frontend statt nur im Backend-Vertrag.
Gespeicherter Template-Status vs. lokal geänderte Builder-Konfiguration wird jetzt explizit angezeigt, damit lokale Draft-Änderungen nicht mehr wie persistierte Vorlagen wirken.
Duplicate-Template-Saves werden jetzt fachlich als `CONFLICT` mit verständlicher Meldung statt als roher DB-Fehler zurückgegeben.
`resource_month`-Explainability ist jetzt im gemeinsamen Report-Pfad statt nur im Assistant-Sonderfall verdrahtet; der Builder zeigt dieselbe Holiday-/Absence-/SAH-Basis kompakt im Result-Header an.
`resource_month`-Exporte laufen jetzt als XLSX mit separatem Explainability-Sheet, damit SAH-/Kalender-/Feiertagslogik im Export nachvollziehbar bleibt ohne Logik in Excel neu aufzubauen.
Der `resource_month`-Blueprint-/Preset-Katalog kommt jetzt aus einem gemeinsamen Backend-Vertrag inklusive Runtime-Output-Schema statt aus lokalem Frontend-Hardcode; fokussierte Router-/Typecheck-Validierung dafür ist grün.
`resource`, `project` und `assignment` tragen jetzt zusätzliche Bezugsgrößen wie Blueprint, Client Unit, Enterprise ID, Value Score, Shoring-/Onshore-Felder sowie Resource-/Project-Kontext an Assignments in den Report-Spaltenkatalog.
Nächster Fokus: verbleibende Report-/Export-Vollständigkeit an echten Nutzerflows prüfen, insbesondere Frontend-/Export-Verhalten außerhalb von `resource_month`.
- [ ] Slice 5: Notifications, Tasks, Broadcasts, Reminder Reliability - [ ] Slice 5: Notifications, Tasks, Broadcasts, Reminder Reliability
Fortschritt:
Notification-/Reminder-/Webhook-Testtrio ist grün validiert.
Zukünftige Scheduled Broadcasts mit Task-/Approval-Metadaten werden jetzt hart abgewiesen, damit `taskAction`/`dueDate` nicht still verloren gehen.
Broadcast-Fanout mappt jetzt auch verlorene `sourceId`-/Sender-Referenzen stabil auf fachliche `NOT_FOUND`-Fehler statt rohe FK-Fehler.
Nicht-terminale Task-Statuswechsel bereinigen jetzt `completedAt`/`completedBy` deterministisch, damit wiedergeöffnete Tasks fachlich und technisch konsistent bleiben.
Wiederkehrende Reminder ziehen `nextRemindAt` nach Downtime jetzt direkt auf den ersten Termin nach `now` vor, damit überfällige Serien-Erinnerungen nicht auf jedem Scheduler-Tick erneut feuern.
Task-Reassignment mappt fehlende `assigneeId`-Referenzen jetzt über den gemeinsamen Notification-Fehlerpfad stabil auf `NOT_FOUND`, statt rohe FK-Fehler aus dem Core-Router nach oben zu reichen.
Immediate Broadcasts werden jetzt ohne `$transaction` hart abgewiesen, damit es keinen nicht-atomaren Fan-out-Fallback mit Teilpersistenz mehr gibt.
Broadcast-Fanout gegen fehlende Empfänger ist jetzt ebenfalls fokussiert abgesichert: `userId`-FK-Verlust im Recipient-Create mappt stabil auf fachliches `NOT_FOUND`, ohne Broadcast-Finalisierung, SSE oder E-Mail-Nebenwirkungen.
Direkte Router-Writes für `notification.create` und `notification.createTask` nutzen jetzt denselben kontextsensitiven Notification-Fehlerpfad wie Assistant/Broadcast, sodass fehlende Recipient-/Sender-Referenzen als fachliche `NOT_FOUND`-Fehler statt rohe Prisma-FKs herauskommen.
`executeTaskAction` blockt jetzt auch `DISMISSED` als terminalen Zustand und läuft nur noch transaktional, damit Domain-Aktion und Task-Abschluss nicht mehr auseinanderlaufen; die Assistant-Test-Helpers spiegeln denselben Transaction-Contract jetzt ebenfalls.
Reminder-Updates und -Deletes sind jetzt auch gegen Missing-/Wrong-Owner-Fälle fokussiert regressionsgesichert, damit die fachliche `NOT_FOUND`-Oberfläche stabil bleibt.
`assignTask` fängt jetzt auch den Read-then-Update-Race auf gelöschte Tasks sauber als fachliches `NOT_FOUND` ab; Missing-Task- und Wrong-Category-Pfade sind auf Router-Ebene zusätzlich abgesichert.
Nächster Fokus: verbleibende Persistenz-/Broadcast-Kanten schließen.
- [ ] Slice 6: Dashboard, Widgets, Explainability, First-Load Stability - [ ] Slice 6: Dashboard, Widgets, Explainability, First-Load Stability
Fortschritt:
Peak Times komprimiert die Summary-Leiste jetzt in drei kompakte Pills statt großer redundanter Karten.
Zusatztexte und Erklärinfos bleiben dort jetzt konsequent hinter dem Details-Toggle.
Budget Forecast reduziert Summary-Helfertexte und sekundäre Zeilendetails jetzt ebenfalls sauber bei `Details off`.
Dashboard-Layout-Hydration überschreibt beim First Load lokale Nutzeraktionen nicht mehr, wenn DB-Layout und erste Interaktion race-condition-artig aufeinandertreffen; fokussierte Hook-/Widget-Tests dafür sind grün.
Top-Value-Ressourcen tragen jetzt auch `countryName` durch Readmodel, API-Detailpfad und Widget-Lokationsanzeige, damit Kalender-/Feiertagsbezug nicht nur als Kürzel sichtbar ist.
Top-Value trägt jetzt zusätzlich `valueScoreBreakdown` und `valueScoreUpdatedAt` bis in API-/Assistant-Detailpfade und Widget-Hover durch, damit die Score-Herleitung nachvollziehbar bleibt ohne den Default-View mit Text zu überladen.
Peak Times trägt jetzt pro Periode `calendarContextCount` plus kompakte `calendarLocations` aus dem Application-Layer bis in Widget-Hover, Detailpanel und `get_dashboard_detail`, damit regionale Feiertags-/Standortbasis auch für Auslastungsspitzen sichtbar bleibt.
- [ ] Slice 7: DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup - [ ] Slice 7: DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup
Fortschritt:
Repo-Wrapper für Env-Laden sind gesetzt; aktuelle Assistant-, Import/Export- und Insights-Tests sowie der Web-Typecheck laufen grün.
Operative Altspuren `planarchy` kommen derzeit noch über einen Symlink-Pfad und einzelne Logs/Artefakte hinein und werden weiter bereinigt.
`pnpm db:prisma -- ...` prüft jetzt für destruktive oder schemawirksame Prisma-Kommandos hart, dass `DATABASE_URL` wirklich auf `capakraken` zeigt; falsche Ziele wie `planarchy` werden vor jedem Prisma-Zugriff blockiert.
Aktive Worker:
Worker A: Slice 5 Notification-/Broadcast-Persistenzkanten.
Worker B: Slice 2 Holiday-/Vacation-Explainability in Forecast-/Widget-Parität.
Lokal: Slice 1 Timeline-Stabilität, Slice 3 Assistant-Parity, Slice 7 DB-/Env-Guardrails und Naming-Cleanup.
## Validierter Stand
- Assistant-Regressionen: `8` fokussierte Testdateien, `36` Tests grün.
- Assistant-/Settings-/Insights-/AI-Defaults: `8` fokussierte Testdateien, `30` Tests grün.
- Web-Typecheck: `pnpm --filter @capakraken/web exec tsc -p tsconfig.typecheck.json --noEmit` grün.
- Timeline/SSE-Regressionen: `pnpm --filter @capakraken/api exec vitest run src/__tests__/timeline-router.test.ts src/__tests__/sse-subscription-policy.test.ts` grün (`15` Tests).
- Dashboard WidgetContainer: `pnpm --filter @capakraken/web exec vitest run src/components/dashboard/WidgetContainer.test.tsx` grün (`2` Tests).
- Insights Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/insights-router.test.ts src/__tests__/insights-procedure-support.test.ts` grün (`9` Tests).
- Country Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/country-router.test.ts src/__tests__/country-procedure-support.test.ts src/__tests__/country-support.test.ts` grün (`12` Tests).
- Holiday Calendar Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/holiday-calendar-router.test.ts src/__tests__/holiday-calendar-router-auth.test.ts src/__tests__/holiday-calendar-procedure-support.test.ts src/__tests__/holiday-calendar-support.test.ts src/__tests__/holiday-calendar-write-support.test.ts` grün (`26` Tests).
- Dispo Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/dispo-router.test.ts src/__tests__/dispo-procedure-support.test.ts src/__tests__/dispo-management-support.test.ts` grün (`10` Tests).
- Org Unit + Import/Export Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/org-unit-router.test.ts src/__tests__/org-unit-procedure-support.test.ts src/__tests__/org-unit-support.test.ts src/__tests__/import-export-router.test.ts src/__tests__/import-export-procedure-support.test.ts` grün (`19` Tests).
- Estimate Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/estimate-router.test.ts` grün (`45` Tests).
- Import/Export Procedure Support: `pnpm --filter @capakraken/api exec vitest run src/__tests__/import-export-procedure-support.test.ts` grün (`5` Tests).
- Assistant Tool Selection: `pnpm --filter @capakraken/api exec vitest run src/__tests__/assistant-tool-selection.test.ts` grün (`3` Tests).
- Assistant Split Regression Runner: `pnpm --filter @capakraken/api test:assistant-split` als explizite Fünf-Batch-Regression plus API-Typecheck etabliert; deckt jetzt auch `export_projects_csv`, Holiday-Resolution-Fehlerpfade und `get_timeline_holiday_overlays` ab.
- Assistant Report Read: `pnpm --filter @capakraken/api exec vitest run src/__tests__/assistant-tools-report-read.test.ts` grün (`3` Tests).
- Report Router: `pnpm --filter @capakraken/api exec vitest run src/__tests__/report-router.test.ts` grün (`8` Tests).
- Report Explainability/Export Parity: `pnpm --filter @capakraken/api exec vitest run src/__tests__/report-router.test.ts src/__tests__/assistant-tools-report-read.test.ts` grün (`11` Tests), `pnpm --filter @capakraken/web exec vitest run src/components/reports/reportBuilderExplainability.test.ts` grün (`2` Tests).
- Report Blueprint Catalog Contract: `pnpm --filter @capakraken/api exec vitest run src/__tests__/report-router.test.ts` grün (`9` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün, `pnpm --filter @capakraken/web exec vitest run src/components/reports/reportBuilderExplainability.test.ts` grün (`2` Tests).
- Report Column Coverage Expansion: `pnpm --filter @capakraken/api exec vitest run src/__tests__/report-router.test.ts` grün (`11` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün.
- Chargeability Report Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/chargeability-report-router.test.ts src/__tests__/assistant-tools-chargeability-report.test.ts` grün (`7` Tests).
- Notification Router + Reminder Scheduler: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts src/__tests__/reminder-scheduler.test.ts` grün (`41` Tests).
- Project Health Explainability: `pnpm --filter @capakraken/application exec vitest run src/__tests__/dashboard.test.ts -t "excludes regional public holidays from project health budget usage"` grün (`1` Test), `pnpm --filter @capakraken/api exec vitest run src/__tests__/dashboard-router.test.ts -t "getProjectHealthDetail"` grün (`1` Test), `pnpm --filter @capakraken/api exec vitest run src/__tests__/assistant-tools-dashboard-project-health.test.ts` grün (`1` Test).
- Dashboard Top Value Country Context: `pnpm --filter @capakraken/api exec vitest run src/__tests__/dashboard-procedure-support.test.ts src/__tests__/dashboard-router.test.ts` grün (`19` Tests).
- Dashboard Top Value Explainability: `pnpm --filter @capakraken/application exec vitest run src/__tests__/dashboard.test.ts -t "enforces visible-role filtering for top value resources"` grün (`1` Test), `pnpm --filter @capakraken/api exec vitest run src/__tests__/dashboard-procedure-support.test.ts src/__tests__/dashboard-router.test.ts src/__tests__/assistant-tools-dashboard-detail.test.ts` grün (`20` Tests), `pnpm --filter @capakraken/web exec vitest run src/components/dashboard/widgets/TopValueWidget.test.tsx` grün (`1` Test).
- Peak Times Calendar Explainability: `pnpm --filter @capakraken/application exec vitest run src/__tests__/dashboard.test.ts -t "peak times"` grün (`4` Tests aktiv, `17` geskippt), `pnpm --filter @capakraken/web exec vitest run src/components/dashboard/widgets/PeakTimesWidget.test.tsx` grün (`1` Test), `pnpm --filter @capakraken/api exec vitest run src/__tests__/dashboard-procedure-support.test.ts src/__tests__/assistant-tools-dashboard-detail.test.ts` grün (`4` Tests).
- Notification Task Reopen Reliability: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts` grün (`37` Tests).
- Notification Task Assignment Reference Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-procedure-support.test.ts src/__tests__/notification-router.test.ts` grün (`46` Tests).
- Notification Immediate Broadcast Transaction Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts` grün (`40` Tests).
- Notification Broadcast Recipient Reference Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-procedure-support.test.ts src/__tests__/notification-router.test.ts` grün (`49` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün.
- Notification Direct Create/CreateTask Reference Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-procedure-support.test.ts src/__tests__/notification-router.test.ts src/__tests__/assistant-tools-notification-create-errors.test.ts src/__tests__/assistant-tools-task-create-errors.test.ts` grün (`60` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün.
- Notification Task Action Transaction Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts src/__tests__/assistant-tools-task-action-guards.test.ts src/__tests__/assistant-tools-task-action-execution.test.ts src/__tests__/assistant-tools-task-action-assignment-errors.test.ts src/__tests__/assistant-tools-task-action-vacation-errors.test.ts` grün (`56` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün.
- Notification Reminder Ownership + AssignTask Race Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts` grün (`51` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün.
- Notification Support + Assistant Router: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-procedure-support.test.ts src/__tests__/notification-router.test.ts src/__tests__/notification-router-auth.test.ts src/__tests__/assistant-router.test.ts src/__tests__/assistant-router-auth.test.ts src/__tests__/assistant-chat-loop.test.ts src/__tests__/assistant-chat-response.test.ts src/__tests__/assistant-tool-selection.test.ts src/__tests__/assistant-approvals.test.ts src/__tests__/assistant-procedure-support.test.ts` grün (`74` Tests).
- Assistant Broadcast Validation: `pnpm --filter @capakraken/api exec vitest run src/__tests__/assistant-tools-broadcast-send-validation-errors.test.ts` grün (`3` Tests).
- Staffing Read Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/staffing-router.test.ts src/__tests__/assistant-tools-holiday-staffing-suggestions.test.ts src/__tests__/assistant-tools-advanced-resource-ranking.test.ts` grün (`32` Tests).
- Vacation Read Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/vacation-router.test.ts src/__tests__/vacation-router-auth.test.ts` grün (`58` Tests).
- API-Typecheck: `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün.
## API Router Slice Inventory ## API Router Slice Inventory
@@ -71,29 +188,29 @@ oder benachbarte Support-Module, Verhalten und öffentliche Contracts bleiben st
| Router | Status | Priorität | Sinnvolle Slices | | Router | Status | Priorität | Sinnvolle Slices |
|---|---|---:|---| |---|---|---:|---|
| `allocation.ts` | done | - | Aggregator bereits dünn; nur Support-Module weiter pflegen | | `allocation.ts` | done | - | Aggregator bereits dünn; nur Support-Module weiter pflegen |
| `assistant.ts` | later | 3 | Chat loop, confirmation flow, insight generation, response shaping | | `assistant.ts` | done | - | top-level chat, approval payloads und system prompt in Support-Module extrahiert |
| `audit-log.ts` | monitor | 4 | Optional: list/detail reads und filter-building trennen, falls Auth/Wiring wächst | | `audit-log.ts` | monitor | 4 | Optional: list/detail reads und filter-building trennen, falls Auth/Wiring wächst |
| `blueprint.ts` | ready_now | 2 | summary/list reads, identifier reads, CRUD, role-preset mutation, global-field procedures | | `blueprint.ts` | done | - | summary/list reads, identifier reads, CRUD, role-preset mutation und global-field procedures in `blueprint-procedure-support.ts` extrahiert |
| `calculation-rules.ts` | monitor | 4 | Aktuell klein; nur bei zusätzlicher Write-/Audit-Logik extrahieren | | `calculation-rules.ts` | monitor | 4 | Aktuell klein; nur bei zusätzlicher Write-/Audit-Logik extrahieren |
| `chargeability-report.ts` | excluded_for_now | - | Bewusst zurückgestellt; erst nach Freigabe des geblockten Bereichs | | `chargeability-report.ts` | excluded_for_now | - | Bewusst zurückgestellt; erst nach Freigabe des geblockten Bereichs |
| `client.ts` | ready_now | 2 | list/tree reads, identifier reads, CRUD/deactivate, sort-order batch mutation | | `client.ts` | done | - | list/tree reads, identifier reads, CRUD/deactivate und sort-order batch mutation in `client-procedure-support.ts` extrahiert |
| `comment.ts` | done | - | Bereits in `comment-procedure-support.ts` ausgelagert | | `comment.ts` | done | - | Bereits in `comment-procedure-support.ts` ausgelagert |
| `computation-graph.ts` | monitor | 4 | Bereits Kompositionsrouter; nur bei zusätzlicher Orchestrierung weiter schneiden | | `computation-graph.ts` | monitor | 4 | Bereits Kompositionsrouter; nur bei zusätzlicher Orchestrierung weiter schneiden |
| `country.ts` | ready_now | 2 | list/identifier reads, country CRUD, metro-city CRUD | | `country.ts` | done | - | list/identifier reads, country CRUD, metro-city CRUD |
| `dashboard.ts` | excluded_for_now | - | Bewusst zurückgestellt; hoher Querschnitt und paralleler Scope | | `dashboard.ts` | excluded_for_now | - | Bewusst zurückgestellt; hoher Querschnitt und paralleler Scope |
| `dispo.ts` | ready_now | 2 | workbook input schemas, staging/validation, staged-read procedures, resolve/commit/cancel mutations | | `dispo.ts` | done | - | workbook input schemas, staging/validation, staged-read procedures, resolve/commit/cancel mutations |
| `effort-rule.ts` | ready_now | 1 | list/detail reads, CRUD/default toggle, preview, apply-rules mutation | | `effort-rule.ts` | done | - | list/detail reads, CRUD/default toggle, preview und apply-rules mutation in `effort-rule-procedure-support.ts` extrahiert |
| `entitlement.ts` | excluded_for_now | - | Bewusst zurückgestellt; größerer Fachblock | | `entitlement.ts` | excluded_for_now | - | Bewusst zurückgestellt; größerer Fachblock |
| `estimate.ts` | later | 3 | commercial reads, demand line writes, phasing, version workflow, aggregate read models | | `estimate.ts` | done | - | commercial reads, demand line writes, phasing, version workflow, aggregate read models |
| `experience-multiplier.ts` | ready_now | 1 | list/detail reads, CRUD/default toggle, preview, apply-rules mutation | | `experience-multiplier.ts` | done | - | list/detail reads, CRUD/default toggle, preview und apply-rules mutation in `experience-multiplier-procedure-support.ts` extrahiert |
| `holiday-calendar.ts` | ready_now | 2 | calendar CRUD, entry CRUD; catalog/resolution reads sind schon separat | | `holiday-calendar.ts` | done | - | calendar CRUD, entry CRUD; catalog/resolution reads sind schon separat |
| `import-export.ts` | ready_now | 3 | export reads, import preview/validation, file-bound orchestration | | `import-export.ts` | done | - | export reads, import preview/validation, file-bound orchestration |
| `insights.ts` | ready_now | 3 | anomaly/summary reads, cached narrative read, AI narrative mutation | | `insights.ts` | done | - | anomaly/summary reads, cached narrative read, AI narrative mutation |
| `management-level.ts` | ready_now | 1 | group reads/writes, level writes/delete, audit helper extraction | | `management-level.ts` | done | - | group reads/writes, level writes/delete und Audit-Orchestrierung in `management-level-procedure-support.ts` extrahiert |
| `notification.ts` | excluded_for_now | - | Bewusst zurückgestellt; zu groß für den aktuellen Batch | | `notification.ts` | later | 3 | Top-Level ist tragbar; nächster sinnvoller Schritt ist die weitere Zerlegung von `notification-procedure-support.ts` |
| `org-unit.ts` | ready_now | 3 | list/tree reads, identifier reads, CRUD/deactivate | | `org-unit.ts` | done | - | list/tree reads, identifier reads, CRUD/deactivate |
| `project.ts` | monitor | 4 | Bereits stark komponiert; optional nur `list` und `getById` separat ziehen | | `project.ts` | monitor | 4 | Bereits stark komponiert; optional nur `list` und `getById` separat ziehen |
| `rate-card.ts` | ready_now | 1 | list/detail/match reads, card CRUD, line CRUD, replace-lines/batch operations | | `rate-card.ts` | done | - | list/detail/match reads, card CRUD, line CRUD und replace-lines in `rate-card-procedure-support.ts` extrahiert |
| `report.ts` | done | - | Bereits in `report-template-procedure-support.ts` ausgelagert | | `report.ts` | done | - | Bereits in `report-template-procedure-support.ts` ausgelagert |
| `resource.ts` | monitor | 4 | Top-Level dünn; tieferliegende Shared-Read-Module sind separat zu betrachten | | `resource.ts` | monitor | 4 | Top-Level dünn; tieferliegende Shared-Read-Module sind separat zu betrachten |
| `role.ts` | done | - | Bereits in `role-procedure-support.ts` ausgelagert | | `role.ts` | done | - | Bereits in `role-procedure-support.ts` ausgelagert |
@@ -109,18 +226,12 @@ oder benachbarte Support-Module, Verhalten und öffentliche Contracts bleiben st
### Empfohlene Reihenfolge ### Empfohlene Reihenfolge
1. `rate-card.ts` 1. Phase-2-Hotspots mit `Vacation Read Separation`
2. `effort-rule.ts` 2. Phase-2-Hotspots mit `Staffing Read Pipeline Split`
3. `experience-multiplier.ts` 3. `notification.ts` via weiterer Zerlegung von `notification-procedure-support.ts`
4. `management-level.ts` 4. `entitlement.ts` nach Abschluss der Holiday-/Vacation-Parität
5. `blueprint.ts` 5. `dashboard.ts` nur bei erneutem Wachstum im API-Router
6. `client.ts` 6. `user.ts` nur, falls dort wieder Top-Level-Orchestrierung anwächst
7. `country.ts`
8. `holiday-calendar.ts`
9. `dispo.ts`
10. `insights.ts`
11. `import-export.ts`
12. `org-unit.ts`
### Phase 2: Tieferliegende Hotspots Hinter Bereits Dünnen Routern ### Phase 2: Tieferliegende Hotspots Hinter Bereits Dünnen Routern
@@ -131,6 +242,7 @@ im selben Architektur-Track. Sie sind sinnvoll, sobald der aktuelle Router-Arbei
|---|---:|---|---| |---|---:|---|---|
| Allocation Mutation Decomposition | 1 | `allocation-assignment-procedures.ts` | Assignment-Transaktionen, Audit, Events, Webhooks und Budget-Folgelogik weiter trennen | | Allocation Mutation Decomposition | 1 | `allocation-assignment-procedures.ts` | Assignment-Transaktionen, Audit, Events, Webhooks und Budget-Folgelogik weiter trennen |
| Vacation Read Separation | 1 | `vacation-read.ts`, `vacation-management-procedures.ts` | Ownership, Anonymisierung, Holiday-Preview und Query-Varianten sauberer schneiden | | Vacation Read Separation | 1 | `vacation-read.ts`, `vacation-management-procedures.ts` | Ownership, Anonymisierung, Holiday-Preview und Query-Varianten sauberer schneiden |
| Resource Shared Read Decomposition | 1 | `resource-read-shared.ts`, `resource-summary-read-procedure-support.ts`, `resource-identifier-read.ts` | Summary-/Detail-Selects, Identifier-Selects und Mapper aus `resource-read-shared.ts` in fokussierte Helper extrahieren; oeffentliche Read-Vertraege stabil halten |
| Estimate Write Router Slimming | 1 | `estimate.ts` plus benachbarte Estimate-Module | Write-Orchestrierung, version workflow und error translation aus dem Router ziehen | | Estimate Write Router Slimming | 1 | `estimate.ts` plus benachbarte Estimate-Module | Write-Orchestrierung, version workflow und error translation aus dem Router ziehen |
| Staffing Read Pipeline Split | 1 | `staffing-suggestions-read.ts`, `staffing-capacity-read.ts`, `staffing-best-project-resource.ts` | gemeinsame Load-/Ranking-/Response-Pipeline extrahieren | | Staffing Read Pipeline Split | 1 | `staffing-suggestions-read.ts`, `staffing-capacity-read.ts`, `staffing-best-project-resource.ts` | gemeinsame Load-/Ranking-/Response-Pipeline extrahieren |
| Report Engine by Execution Mode | 2 | `report-query-engine.ts`, `report-query-config.ts` | generische Reports, `resource_month`-Sonderfall und Exportpfade klar trennen | | Report Engine by Execution Mode | 2 | `report-query-engine.ts`, `report-query-config.ts` | generische Reports, `resource_month`-Sonderfall und Exportpfade klar trennen |
@@ -144,7 +256,6 @@ im selben Architektur-Track. Sie sind sinnvoll, sobald der aktuelle Router-Arbei
- `packages/api/src/router/dashboard.ts` - `packages/api/src/router/dashboard.ts`
- `packages/api/src/router/entitlement.ts` - `packages/api/src/router/entitlement.ts`
- `packages/api/src/router/notification.ts` - `packages/api/src/router/notification.ts`
- `packages/api/src/router/resource-read-shared.ts`
- `packages/api/src/router/resource-summary-read.ts` - `packages/api/src/router/resource-summary-read.ts`
- `packages/api/src/router/user.ts` - `packages/api/src/router/user.ts`
- `packages/api/src/__tests__/timeline-router.test.ts` - `packages/api/src/__tests__/timeline-router.test.ts`
@@ -159,3 +270,17 @@ im selben Architektur-Track. Sie sind sinnvoll, sobald der aktuelle Router-Arbei
- `report`: report template procedures extrahiert - `report`: report template procedures extrahiert
- `role`: role write procedures extrahiert - `role`: role write procedures extrahiert
- `settings`: settings procedures extrahiert - `settings`: settings procedures extrahiert
- `rate-card`: rate-card procedures extrahiert
- `effort-rule`: effort-rule procedures extrahiert
- `experience-multiplier`: experience-multiplier procedures extrahiert
- `management-level`: management-level procedures extrahiert
- `blueprint`: blueprint procedures extrahiert
- `client`: client procedures extrahiert
- `country`: read-/write-procedures in `country-procedure-support.ts` validiert
- `holiday-calendar`: write-procedures in `holiday-calendar-procedure-support.ts` validiert
- `dispo`: staged import/read/write procedures in `dispo-procedure-support.ts` validiert
- `import-export`: file-bound orchestration in `import-export-procedure-support.ts` validiert
- `insights`: procedure support und nullable-settings-Haertung validiert
- `org-unit`: read-/write-procedures in `org-unit-procedure-support.ts` validiert
- `estimate`: write/orchestration, rate lookup und Fehlertranslation in `estimate-procedure-support.ts` validiert
- `assistant`: chat-/approval-/prompt orchestration in `assistant-procedure-support.ts` und `assistant-system-prompt.ts` validiert
@@ -0,0 +1,80 @@
import { describe, expect, it, vi } from "vitest";
import {
assertCanReadOwnedResource,
canManageOwnedResourceReads,
findOwnedReadResourceId,
resolveOwnedResourceReadFilter,
} from "../router/resource-owned-read-access.js";
function createContext(options: {
role?: string | null;
userId?: string | null;
resourceFindFirst?: ReturnType<typeof vi.fn>;
} = {}) {
return {
dbUser: options.userId === null
? null
: {
id: options.userId ?? "user_1",
systemRole: options.role ?? "USER",
},
db: {
resource: options.resourceFindFirst
? { findFirst: options.resourceFindFirst }
: {
findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
},
},
};
}
describe("resource-owned-read-access", () => {
it("treats admins and managers as broad readers", () => {
expect(canManageOwnedResourceReads(createContext({ role: "ADMIN" }))).toBe(true);
expect(canManageOwnedResourceReads(createContext({ role: "MANAGER" }))).toBe(true);
expect(canManageOwnedResourceReads(createContext({ role: "USER" }))).toBe(false);
});
it("finds the signed-in user's owned resource id", async () => {
const resourceFindFirst = vi.fn().mockResolvedValue({ id: "res_123" });
await expect(findOwnedReadResourceId(createContext({ resourceFindFirst }))).resolves.toBe("res_123");
expect(resourceFindFirst).toHaveBeenCalledWith({
where: { userId: "user_1" },
select: { id: true },
});
});
it("scopes regular readers to their own resource filter", async () => {
await expect(
resolveOwnedResourceReadFilter(
createContext({ role: "USER" }),
undefined,
"forbidden",
),
).resolves.toBe("res_own");
});
it("preserves the requested filter for managers", async () => {
await expect(
resolveOwnedResourceReadFilter(
createContext({ role: "MANAGER" }),
"res_other",
"forbidden",
),
).resolves.toBe("res_other");
});
it("rejects access to another resource for regular readers", async () => {
await expect(
assertCanReadOwnedResource(
createContext({ role: "USER" }),
"res_other",
"forbidden",
),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "forbidden",
});
});
});
@@ -1,48 +1,20 @@
import { PreviewResolvedHolidaysSchema } from "@capakraken/shared"; import { PreviewResolvedHolidaysSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js"; import { findUniqueOrThrow } from "../db/helpers.js";
import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js"; import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js";
import { protectedProcedure } from "../trpc.js"; import { protectedProcedure } from "../trpc.js";
import { type HolidayReadContext } from "./holiday-calendar-shared.js"; import { type HolidayReadContext } from "./holiday-calendar-shared.js";
import { assertCanReadOwnedResource } from "./resource-owned-read-access.js";
function canManageHolidayResourceReads(ctx: HolidayReadContext): boolean {
const role = ctx.dbUser?.systemRole;
return role === "ADMIN" || role === "MANAGER";
}
async function findOwnedHolidayResourceId(ctx: HolidayReadContext): Promise<string | null> {
if (!ctx.dbUser?.id) {
return null;
}
if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
return null;
}
const resource = await ctx.db.resource.findFirst({
where: { userId: ctx.dbUser.id },
select: { id: true },
});
return resource?.id ?? null;
}
async function assertCanReadHolidayResource( async function assertCanReadHolidayResource(
ctx: HolidayReadContext, ctx: HolidayReadContext,
resourceId: string, resourceId: string,
): Promise<void> { ): Promise<void> {
if (canManageHolidayResourceReads(ctx)) { await assertCanReadOwnedResource(
return; ctx,
} resourceId,
"You can only view holiday data for your own resource",
const ownedResourceId = await findOwnedHolidayResourceId(ctx); );
if (!ownedResourceId || ownedResourceId !== resourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only view holiday data for your own resource",
});
}
} }
function formatResolvedHolidayDetail(holiday: { function formatResolvedHolidayDetail(holiday: {
@@ -0,0 +1,66 @@
import { TRPCError } from "@trpc/server";
import type { TRPCContext } from "../trpc.js";
export type OwnedResourceReadContext = Pick<TRPCContext, "db" | "dbUser">;
export function canManageOwnedResourceReads(ctx: { dbUser: { systemRole: string } | null }): boolean {
const role = ctx.dbUser?.systemRole;
return role === "ADMIN" || role === "MANAGER";
}
export async function findOwnedReadResourceId(
ctx: OwnedResourceReadContext,
): Promise<string | null> {
if (!ctx.dbUser?.id) {
return null;
}
if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
return null;
}
const resource = await ctx.db.resource.findFirst({
where: { userId: ctx.dbUser.id },
select: { id: true },
});
return resource?.id ?? null;
}
export async function assertCanReadOwnedResource(
ctx: OwnedResourceReadContext,
resourceId: string,
forbiddenMessage: string,
): Promise<void> {
if (canManageOwnedResourceReads(ctx)) {
return;
}
const ownedResourceId = await findOwnedReadResourceId(ctx);
if (!ownedResourceId || ownedResourceId !== resourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: forbiddenMessage,
});
}
}
export async function resolveOwnedResourceReadFilter(
ctx: OwnedResourceReadContext,
requestedResourceId: string | undefined,
forbiddenMessage: string,
): Promise<string | null | undefined> {
if (canManageOwnedResourceReads(ctx)) {
return requestedResourceId;
}
const ownedResourceId = await findOwnedReadResourceId(ctx);
if (requestedResourceId && requestedResourceId !== ownedResourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: forbiddenMessage,
});
}
return ownedResourceId;
}
+18 -45
View File
@@ -11,48 +11,27 @@ import {
findVacationResourceChapter, findVacationResourceChapter,
listChapterVacationOverlaps, listChapterVacationOverlaps,
} from "./vacation-read-support.js"; } from "./vacation-read-support.js";
import {
assertCanReadOwnedResource,
canManageOwnedResourceReads,
resolveOwnedResourceReadFilter,
} from "./resource-owned-read-access.js";
type VacationReadContext = Pick<TRPCContext, "db" | "dbUser">; type VacationReadContext = Pick<TRPCContext, "db" | "dbUser">;
export function canManageVacationReads(ctx: { dbUser: { systemRole: string } | null }): boolean { export function canManageVacationReads(ctx: { dbUser: { systemRole: string } | null }): boolean {
const role = ctx.dbUser?.systemRole; return canManageOwnedResourceReads(ctx);
return role === "ADMIN" || role === "MANAGER";
}
export async function findOwnedResourceId(
ctx: VacationReadContext,
): Promise<string | null> {
if (!ctx.dbUser?.id) {
return null;
}
if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
return null;
}
const resource = await ctx.db.resource.findFirst({
where: { userId: ctx.dbUser.id },
select: { id: true },
});
return resource?.id ?? null;
} }
export async function assertCanReadVacationResource( export async function assertCanReadVacationResource(
ctx: VacationReadContext, ctx: VacationReadContext,
resourceId: string, resourceId: string,
): Promise<void> { ): Promise<void> {
if (canManageVacationReads(ctx)) { await assertCanReadOwnedResource(
return; ctx,
} resourceId,
"You can only view vacation data for your own resource",
const ownedResourceId = await findOwnedResourceId(ctx); );
if (!ownedResourceId || ownedResourceId !== resourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only view vacation data for your own resource",
});
}
} }
export function isSameUtcDay(left: Date, right: Date): boolean { export function isSameUtcDay(left: Date, right: Date): boolean {
@@ -159,20 +138,14 @@ export const vacationReadProcedures = {
}), }),
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
let resourceIdFilter = input.resourceId; const resourceIdFilter = await resolveOwnedResourceReadFilter(
ctx,
input.resourceId,
"You can only view vacation data for your own resource",
);
if (!canManageVacationReads(ctx)) { if (!canManageVacationReads(ctx) && !resourceIdFilter) {
const ownedResourceId = await findOwnedResourceId(ctx); return [];
if (input.resourceId && input.resourceId !== ownedResourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only view vacation data for your own resource",
});
}
if (!ownedResourceId) {
return [];
}
resourceIdFilter = ownedResourceId;
} }
const vacations = await ctx.db.vacation.findMany({ const vacations = await ctx.db.vacation.findMany({