feat(quick-260914-eym): forSystem(), is_system_context(), Systemleseregel auf fuenf Tabellen, DKV-Planer je Mandant — ein Pfad (WINDOWS #21)
- Helfer forSystem(prisma) in prisma-tenant.extension.ts (Array-Form, setzt app.system_context='true' und die beiden anderen Variablen ausdruecklich leer); forTenant()/withTenantTransaction() setzen app.system_context='' als Literal (4 neue Spec-Tests) - Migration 20260914120000_rls_system_context_read: is_system_context() (COALESCE, STABLE) und system_read_policy FOR SELECT auf DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch — lokal angewendet (36 Migrationen, pg_proc 1, 5 system_read_policy, 34 Regeln) - migration-sql.spec.ts: describe-Block fuer die neue Migration (6 Tests) - rls-scratch-check.mjs: Funktion aus der Migration geschnitten, forSystemQuery/buildInlineSystemClient, Reset in forTenantQuery/ buildInlineExtendedClient, runSystemContextChecks (4 Funktionsfaelle + 9 Kennungen DkvModuleConfig) -> Alle 216 Pruefungen bestanden - rls-access-inventory.spec.ts: fuenfte Erkennungsform const X = forSystem(, Stand system-gebunden mit Vorrangregel, FORSYSTEM_ALLOWED_CALL_SITES (exakte Zahl je Datei, 3 Tests), Proben C/D/E - DKV: loadActiveConfigsForScheduler() ueber forSystem (findMany isActive, CONFIG_SAFE_SELECT, orderBy tenantId); DkvSchedulerService mit Auftrag je Mandant dkv-inbox-poll:<tenantId>, activeTenantId ersatzlos entfernt, setInterval/stopJob je Mandant, registeredTenantIds(); Controller stopJob(tenantId); neue dkv-scheduler.service.spec.ts (7 Tests), dkv.service.spec.ts Tests 6/7 umgestellt - Klassifikation: dkv.service.ts/dkvModuleConfig system-gebunden, Header mit fuenfter Erkennungsform und viertem Stand-Wert - Baseline: 63 Dateien / 1051 Tests, tsc 0, Werkzeug 216 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -9,7 +9,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { forSystem, forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { DkvExportService } from './dkv-export.service';
|
||||
import { DkvMailService } from './dkv-mail.service';
|
||||
import { DkvParserService } from './dkv-parser.service';
|
||||
@@ -56,13 +56,17 @@ const CONFIG_SAFE_SELECT = {
|
||||
* - T-07-09: Export filename validated against safe pattern before reading (traversal guard)
|
||||
* - Single-flight guard: prevents concurrent inbox processing (Pitfall 7)
|
||||
*
|
||||
* Multi-tenant note (v1): The scheduler loads its startup config via
|
||||
* loadAnyActiveConfigForScheduler(), which stays bewusst UNGEBUNDEN
|
||||
* (WINDOWS #21, see that method's own doc comment). Each processInbox(tenantId)
|
||||
* call is per-tenant and fully forTenant()-bound (260909-mir). The Controller
|
||||
* scopes all operations to req.tenantId. Full per-tenant scheduling (one cron
|
||||
* per active tenant) is deferred to a future plan — v1 covers single-tenant
|
||||
* deployments.
|
||||
* Multi-tenant note (seit 260914-eym, Etappe 3c): Der Planer laedt seinen
|
||||
* Startpfad ueber `loadActiveConfigsForScheduler()` — SYSTEMGEBUNDEN
|
||||
* (`forSystem()`, liest ALLE aktiven Konfigurationen ueber alle Mandanten,
|
||||
* nur lesend) — und registriert je aktivem Mandanten einen eigenen
|
||||
* Cron-Auftrag (einmal-abfragen-viele-bedienen, WINDOWS #21 geschlossen).
|
||||
* Each processInbox(tenantId) call is per-tenant and fully forTenant()-bound
|
||||
* (260909-mir). The Controller scopes all operations to req.tenantId.
|
||||
*
|
||||
* Bewusst NICHT angefasst (260914-eym): der Single-Flight-Riegel
|
||||
* `processing` ist EIN prozessweites Boolean, nicht je Mandant — siehe
|
||||
* Kommentar am Feld und WINDOWS-Eintrag (Ledger).
|
||||
*/
|
||||
@Injectable()
|
||||
export class DkvService {
|
||||
@@ -72,6 +76,14 @@ export class DkvService {
|
||||
* Single-flight guard: if processing is already in progress, any concurrent
|
||||
* call to processInbox() returns early without starting a second pipeline
|
||||
* run (Pitfall 7 — prevents the prune race condition and duplicate records).
|
||||
*
|
||||
* PROZESSWEIT, nicht je Mandant (260914-eym, bewusst unangetastet): seit
|
||||
* je aktivem Mandanten ein eigener Cron-Auftrag laeuft, koennen sich zwei
|
||||
* Ticks verschiedener Mandanten ueberschneiden — der zweite bricht dann
|
||||
* still ab und wartet bis zum naechsten Intervall (Verzoegerung, kein
|
||||
* Datenverlust; mit EINEM Mandanten unveraendert). Loesungsweg: Riegel je
|
||||
* Mandant (Set<tenantId>) — als Ledger-Eintrag in .planning/WINDOWS.md
|
||||
* gefuehrt, nicht in diesem Durchlauf gebaut (Auftrag: Tick unangetastet).
|
||||
*/
|
||||
private processing = false;
|
||||
|
||||
@@ -102,7 +114,8 @@ export class DkvService {
|
||||
* optionalen Parameter, hinter dem der eine Zweig gebunden werden MUSSTE
|
||||
* und der andere gebunden werden DURFTE NICHT — genau die Form, die
|
||||
* dieser Umbau aufloest. Der uebergreifende Zweig ist jetzt eine eigene,
|
||||
* benannte Methode: `loadAnyActiveConfigForScheduler()` unten.
|
||||
* benannte Methode: `loadActiveConfigsForScheduler()` unten (seit
|
||||
* 260914-eym systemgebunden, eine Zeile je aktivem Mandanten).
|
||||
*/
|
||||
async loadConfig(tenantId: string) {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
@@ -113,40 +126,39 @@ export class DkvService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull the DKV module config for a single, ARBITRARY tenant that has one
|
||||
* configured — used EXCLUSIVELY by DkvSchedulerService.onModuleInit() to
|
||||
* seed the one (v1, single-tenant) cron job at boot time.
|
||||
* Alle AKTIVEN DKV-Konfigurationen ueber ALLE Mandanten — verwendet
|
||||
* AUSSCHLIESSLICH von DkvSchedulerService.onModuleInit(), das je Zeile
|
||||
* einen eigenen Cron-Auftrag `dkv-inbox-poll:<tenantId>` registriert.
|
||||
*
|
||||
* BLEIBT bewusst UNGEBUNDEN (WINDOWS #21 Etappe 2, 260909-mir, Befund D
|
||||
* — siehe .planning/WINDOWS.md und den Abschnitt "Bereich dkv" in
|
||||
* docs/mandantentrennung-etappe2-fehlerrichtung.md fuer die vollstaendige
|
||||
* Begruendung, hier nur die Kurzfassung):
|
||||
* SYSTEMGEBUNDEN (Etappe 3c, 260914-eym, WINDOWS #21 GESCHLOSSEN): liest
|
||||
* ueber `forSystem()` (Sitzungsvariable `app.system_context = 'true'`,
|
||||
* Regel `system_read_policy ... FOR SELECT` auf "DkvModuleConfig",
|
||||
* Migration 20260914120000). Warum VIELE statt EINER:
|
||||
*
|
||||
* - HEUTE bereits falsch, nicht nur ungenau: `findFirst()` ohne jede
|
||||
* Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient
|
||||
* die uebrigen NIE. Ist ausgerechnet die gezogene Zeile inaktiv,
|
||||
* registriert der Planer gar nichts, obwohl ein zweiter Mandant aktiv
|
||||
* waere.
|
||||
* - NACH DEM SCHARFSCHALTEN (Etappe 4, WINDOWS #18) verstummt dieselbe
|
||||
* Abfrage zusaetzlich: sie liefert dann `null` statt einer beliebigen
|
||||
* Zeile, der Planer protokolliert das als Normalfall und richtet fuer
|
||||
* JEDEN Mandanten nichts ein — ohne Fehler, ohne Alarm.
|
||||
* - Binden wuerde diesen Pfad garantiert leer laufen lassen (es gibt beim
|
||||
* Boot strukturell keinen Mandantenkontext). Umbau auf
|
||||
* einmal-abfragen-viele-bedienen ist die in 07-04 zurueckgestellte
|
||||
* Mehrmandanten-Planung — eine Funktionsaenderung, kein Bindungsumbau,
|
||||
* und deshalb hier NICHT vorgenommen.
|
||||
* - Praezedenzfall: `LdapConfigService.getAllActiveConfigs()`
|
||||
* (260909-ipc, Befund B) — mit der einen Unsymmetrie, die dieser
|
||||
* Praezedenzfall NICHT deckt: `getAllActiveConfigs` ist heute korrekt
|
||||
* und verstummt erst spaeter, dieser Pfad ist HEUTE bereits falsch UND
|
||||
* verstummt zusaetzlich spaeter.
|
||||
* - Die Vorgaengerform `findFirst()` ohne Bedingung zog bei mehreren
|
||||
* Mandanten EINEN beliebigen und bediente die uebrigen NIE — HEUTE
|
||||
* schon falsch (260909-mir, Befund D). `findMany({ where: { isActive:
|
||||
* true } })` liefert jeden aktiven Mandanten genau einmal, sortiert nach
|
||||
* tenantId (deterministische Reihenfolge der Auftraege).
|
||||
* - Das VERSTUMMEN nach dem Scharfschalten (Etappe 4) ist strukturell
|
||||
* ausgeschlossen: ohne Systemkontext saehe dieser Pfad unter einer Rolle
|
||||
* ohne BYPASSRLS NULL Zeilen; `system_read_policy` oeffnet genau diese
|
||||
* Tabelle fuer genau diesen Kontext, nur lesend (Werkzeugbeleg
|
||||
* `dkvmoduleconfig-systemkontext-sieht-beide-mandanten`).
|
||||
* - Mit EINEM Mandanten ist das Ergebnis beobachtbar identisch zur
|
||||
* Vorgaengerform: eine Zeile, derselbe Auftrag, dieselbe Cron-Expression
|
||||
* (dkv-scheduler.service.spec.ts, Test 1).
|
||||
*
|
||||
* Das Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4
|
||||
* (`apps/api/scripts/rls-preflight.mjs`), NICHT in diesen Durchlauf.
|
||||
* `CONFIG_SAFE_SELECT`: die verschluesselten Zugangsdaten bleiben draussen
|
||||
* (T-07-12) — der Planer braucht nur tenantId und pollIntervalMin.
|
||||
*/
|
||||
async loadAnyActiveConfigForScheduler() {
|
||||
return this.prisma.dkvModuleConfig.findFirst({ select: CONFIG_SAFE_SELECT });
|
||||
async loadActiveConfigsForScheduler() {
|
||||
const systemPrisma = forSystem(this.prisma) as any;
|
||||
return systemPrisma.dkvModuleConfig.findMany({
|
||||
where: { isActive: true },
|
||||
select: CONFIG_SAFE_SELECT,
|
||||
orderBy: { tenantId: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user