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:
2026-09-14 11:32:54 +02:00
parent 02016e19eb
commit 3d645674f0
12 changed files with 1272 additions and 139 deletions
@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import { forTenant, withTenantTransaction } from './prisma-tenant.extension';
import { forSystem, forTenant, withTenantTransaction } from './prisma-tenant.extension';
/**
* Prueft ohne laufende Datenbank die FORM des Aufrufs, nicht seinen mit
@@ -199,6 +199,80 @@ describe('forTenant() — Array-Form von $transaction (WINDOWS #20)', () => {
expect(transactionCalls).toHaveLength(1);
expect((transactionCalls[0] as unknown[]).length).toBe(2);
});
// Systemkontext (Etappe 3c, 260914-eym): forTenant() setzt app.system_context
// AUSDRUECKLICH auf den Leerstring — als Literal im Template-Text, nicht als
// Parameter (die Parameterliste bleibt [tenantId, userId ?? '']).
it('setzt app.system_context im Template-Text ausdruecklich auf den Leerstring (kein Erben aus einem Systemkontext, 260914-eym)', async () => {
const fakePrisma: any = {
$transaction: vi.fn(() => Promise.resolve(['set-config-result', 'query-result'])),
$extends: (config: any) => ({
async __invoke(args: unknown, query: (args: unknown) => unknown) {
return config.query.$allOperations({ args, query });
},
}),
$executeRaw: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
const text = strings.join('');
expect(text).toContain("set_config('app.system_context', '', true)");
expect(values).toEqual(['tenant-a', '']);
return 'set-config-promise';
}),
};
const scoped = forTenant(fakePrisma, 'tenant-a') as any;
await scoped.__invoke({}, () => 'query-result');
expect(fakePrisma.$executeRaw).toHaveBeenCalledTimes(1);
});
});
describe('forSystem() — Systemkontext fuer Hintergrunddienste (Etappe 3c, 260914-eym)', () => {
it("setzt alle drei Variablen als Literale im Template-Text ('true'/''/''), values leer, $transaction-Feld mit genau zwei Eintraegen", async () => {
const transactionCalls: unknown[] = [];
const fakeQueryResult = [{ id: 'row-1' }];
const fakePrisma: any = {
$transaction: vi.fn((arg: unknown) => {
transactionCalls.push(arg);
return Promise.resolve(['set-config-result', fakeQueryResult]);
}),
$extends: (config: any) => ({
async __invoke(args: unknown, query: (args: unknown) => unknown) {
return config.query.$allOperations({ args, query });
},
}),
$executeRaw: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
const text = strings.join('');
expect(text).toContain("set_config('app.system_context', 'true', true)");
expect(text).toContain("set_config('app.current_tenant', '', true)");
expect(text).toContain("set_config('app.current_user', '', true)");
expect(values).toEqual([]);
return 'set-config-promise';
}),
};
const system = forSystem(fakePrisma) as any;
let queryCallCount = 0;
const result = await system.__invoke({ where: { isActive: true } }, () => {
queryCallCount += 1;
return fakeQueryResult;
});
expect(fakePrisma.$executeRaw).toHaveBeenCalledTimes(1);
expect(transactionCalls).toHaveLength(1);
expect(Array.isArray(transactionCalls[0])).toBe(true);
expect((transactionCalls[0] as unknown[]).length).toBe(2);
expect(result).toBe(fakeQueryResult);
expect(queryCallCount).toBe(1);
});
it('nutzt im tatsaechlichen Code die Array-Form von $transaction — innerhalb von forSystem() selbst (WINDOWS-#20-Bauart)', () => {
const source = stripComments(readFileSync(EXTENSION_SOURCE_PATH, 'utf-8'));
const forSystemSource = extractFunctionSource(source, 'forSystem');
expect(forSystemSource).not.toBe('');
expect(forSystemSource).toMatch(/\$transaction\(\s*\[/);
expect(forSystemSource).not.toMatch(/\$transaction\(\s*async/);
expect(forSystemSource).not.toContain('$executeRawUnsafe');
});
});
describe('withTenantTransaction() — interaktive Callback-Form auf dem UNgebundenen Client (260909-jts, Aufgabe 1)', () => {
@@ -273,6 +347,24 @@ describe('withTenantTransaction() — interaktive Callback-Form auf dem UNgebund
await withTenantTransaction(fakePrisma, "tenant-with-quote-' OR 1=1", async () => 'ok');
expect(fakeTx.$executeRaw).toHaveBeenCalledTimes(1);
});
it('setzt app.system_context im Template-Text auf tx ausdruecklich auf den Leerstring (260914-eym)', async () => {
const fakeTx: any = {
$executeRaw: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
const text = strings.join('');
expect(text).toContain("set_config('app.current_tenant', ");
expect(text).toContain("set_config('app.system_context', '', true)");
expect(values).toEqual(['tenant-a']);
return Promise.resolve(1);
}),
};
const fakePrisma: any = {
$transaction: vi.fn((fn: (tx: unknown) => unknown) => fn(fakeTx)),
};
await withTenantTransaction(fakePrisma, 'tenant-a', async () => 'ok');
expect(fakeTx.$executeRaw).toHaveBeenCalledTimes(1);
});
});