feat(quick-260910-exd): ModuleRegistryService binden, Klassifikation abschliessen

- module-registry.service.ts: findActiveForTenant, activateForTenant,
  isModuleActive binden je einen Aktivierungszugriff, deactivateForTenant
  bindet beide (Lesen+Schreiben) ueber EINEN Klienten unter tenantPrisma;
  alle sechs Katalogzugriffe (findAll/findBySlug/beide
  Existenzpruefungen/isModuleActive-Katalogsuche/seedModule) bleiben
  bewusst ungebunden, mit Kommentar der Messung von Bedingung trennt
- isModuleActive-Kopfkommentar richtiggestellt: der Waechter ruft sie
  nicht auf (0 Aufrufer, TEIL 3 von Aufgabe 1) — Waechter nimmt findBySlug
  + ModuleAccessService.getAccessibleModuleIds
- module-registry.service.spec.ts: NEU, Zwei-Klienten-Nachweis, deckt die
  bislang ungetestete Datei mit elf der siebzehn Zugriffe des Bereichs ab,
  inkl. der lauten (deactivate ohne Aktivierung) und stillen (isModuleActive
  ohne Aktivierung) Richtung und dem Katalog-Wachhund
- tender-scheduler.service.spec.ts: forTenant() auf Identitaet gemockt
  (dieselbe Konvention wie ldap.service.spec.ts) — cross-area Bruch durch
  die Umstellung von activateForTenant behoben (Rule 1/3)
- docs/mandantentrennung-zugriffsklassifikation.md: alle fuenf
  handgepflegten Stellen nachgezogen (Bestandsaufnahme, Uebersichtszeile
  7/10, Summenzeile 108/134, Klassen-Verteilung unveraendert bei 63 Paaren,
  Hintergrunddienst-Abschnitt haelt die Abwesenheit eines sechsten Falls
  fest) — alle gemessen, nicht abgeschrieben, Befund K haelt exakt
- docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag mit
  tatsaechlich umgesetzten Pfaden, beiden Falsifizierungsnachweisen
  (Testname+Meldung), und der Feststellung zum unveraenderten
  Controller-Kommentar
- .planning/WINDOWS.md: neuer offener Eintrag #23 (deviation) — kein Signal
  unterscheidet "keine Freigabe" von "Abfrage fand nichts", mit
  Vorabpruefung fuer Etappe 4 und begruendeter Verwerfung einer
  Laufzeitwarnung
- 833 Tests gruen (56 Dateien), Typpruefung sauber, Wegwerf-Werkzeug 66/66

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-10 11:30:37 +02:00
parent 3df72687c1
commit 9c0eefee90
6 changed files with 515 additions and 17 deletions
@@ -1,5 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { forTenant } from '../prisma/prisma-tenant.extension';
/**
* Service managing the module registry and per-tenant activations.
@@ -13,6 +14,13 @@ export class ModuleRegistryService {
/**
* Returns all registered modules.
*
* Bewusst UNGEBUNDEN (260910-exd, Aufgabe 1, Befund E): "Module" traegt
* heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht
* katastrophal. Katastrophal wuerde sie erst, WENN Etappe 3 dieser
* Tabelle eine Regel gibt — dann verschwaende der gesamte Katalog fuer
* jeden Mandanten. Diese Bedingung steht hier als Bedingung, nicht als
* heute beobachtbare Tatsache.
*/
async findAll() {
return this.prisma.module.findMany({
@@ -22,6 +30,12 @@ export class ModuleRegistryService {
/**
* Finds a module by its unique slug.
*
* Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben. Diese
* Methode ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER
* Modulanfrage aufruft — eine Bindung wuerde jede Modulanfrage mit einer
* Meldung abweisen, die faelschlich von einer fehlenden Aktivierung
* spricht.
*/
async findBySlug(slug: string) {
return this.prisma.module.findUnique({
@@ -33,7 +47,8 @@ export class ModuleRegistryService {
* Returns all active modules for a given tenant.
*/
async findActiveForTenant(tenantId: string) {
const activations = await this.prisma.tenantModuleActivation.findMany({
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const activations = await tenantPrisma.tenantModuleActivation.findMany({
where: {
tenantId,
isActive: true,
@@ -43,7 +58,7 @@ export class ModuleRegistryService {
},
});
return activations.map((a) => a.module);
return activations.map((a: { module: unknown }) => a.module);
}
/**
@@ -51,7 +66,10 @@ export class ModuleRegistryService {
* Per D-08: dynamic activation without restart.
*/
async activateForTenant(tenantId: string, moduleId: string) {
// Verify module exists
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
// `findAll` oben (Aufgabe 1, Befund E). Laeuft VOR jedem gebundenen
// Schreibzugriff: eine unbekannte moduleId wirft, bevor der gebundene
// Klient ueberhaupt erzeugt wird.
const moduleExists = await this.prisma.module.findUnique({
where: { id: moduleId },
});
@@ -59,7 +77,8 @@ export class ModuleRegistryService {
throw new NotFoundException(`Module with id '${moduleId}' not found`);
}
return this.prisma.tenantModuleActivation.upsert({
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
return tenantPrisma.tenantModuleActivation.upsert({
where: {
tenantId_moduleId: {
tenantId,
@@ -86,7 +105,8 @@ export class ModuleRegistryService {
* Does not remove the activation record, preserving audit trail.
*/
async deactivateForTenant(tenantId: string, moduleId: string) {
// Verify module exists
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
// `findAll` oben.
const moduleExists = await this.prisma.module.findUnique({
where: { id: moduleId },
});
@@ -94,8 +114,13 @@ export class ModuleRegistryService {
throw new NotFoundException(`Module with id '${moduleId}' not found`);
}
// EIN gebundener Klient fuer beide Aktivierungszugriffe dieser Methode
// (Lesen, Schreiben) — nicht ein Klient je Zugriff (260910-exd,
// Aufgabe 3, dieselbe Konvention wie `module-access.service.ts`).
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
// Check if activation record exists
const activation = await this.prisma.tenantModuleActivation.findUnique({
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
where: {
tenantId_moduleId: {
tenantId,
@@ -110,7 +135,7 @@ export class ModuleRegistryService {
);
}
return this.prisma.tenantModuleActivation.update({
return tenantPrisma.tenantModuleActivation.update({
where: {
tenantId_moduleId: {
tenantId,
@@ -128,7 +153,15 @@ export class ModuleRegistryService {
/**
* Checks whether a module (by slug) is active for a given tenant.
* Used by ModuleGuard to gate access to module-specific endpoints.
*
* Richtiggestellt (260910-exd, Aufgabe 1, Befund G): der vorherige
* Kommentar behauptete, `ModuleGuard` benutze diese Methode — er tut es
* NICHT. Gemessen (Aufgabe 1, TEIL 3, `grep -rn "isModuleActive"
* apps/api/src apps/web/src packages`): genau EIN Treffer, die Definition
* selbst, kein Aufrufer. Der Waechter nimmt stattdessen `findBySlug` plus
* `ModuleAccessService.getAccessibleModuleIds`. Diese Methode bleibt
* TROTZDEM umgestellt: heute toter, ungebunden gelassener Code ist die
* Falle fuer den, der ihn morgen verdrahtet.
*/
async isModuleActive(tenantId: string, moduleSlug: string): Promise<boolean> {
const module = await this.prisma.module.findUnique({
@@ -139,7 +172,8 @@ export class ModuleRegistryService {
return false;
}
const activation = await this.prisma.tenantModuleActivation.findUnique({
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
where: {
tenantId_moduleId: {
tenantId,
@@ -154,6 +188,11 @@ export class ModuleRegistryService {
/**
* Registers or updates a module in the registry by slug (upsert).
* Used during application startup to seed built-in modules.
*
* Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben — mit einem
* zusaetzlichen Grund, den nur diese Methode hat: sie laeuft beim
* Anwendungsstart aus vier Seed-Dateien, ohne Anfrage und ohne Mandanten
* — ein gebundener Aufruf haette dort strukturell keinen Kontext.
*/
async seedModule(manifest: {
slug: string;