Files
tessera-ctl/apps/api/src/module-registry/module-registry.service.ts
T
schalli b188946e31 refactor(quick-260921-m34): Aufgabe 1 - Mandantenbindung entzaubert, 105 unnoetige any-Zusicherungen entfernt
- prisma-tenant.extension.ts: (prisma as any) und die Handannotation an
  $allOperations in forTenant()/forSystem() entfernt; Kopfkommentar
  unveraendert. .then((results: any[]) => ...) auf unknown[] umgestellt.
- 105 Aufrufstellen `const X = forTenant(...) as any` / `forSystem(...) as
  any` von der Zusicherung befreit, Zuweisungsform woertlich erhalten
  (rls-access-inventory.spec.ts bleibt scharf, 30/30 gruen einzeln
  geprueft).
- withTenantTransaction(): Prisma.TransactionClient fuer tx probiert,
  gemessen verworfen - bricht das Testdoppel in
  prisma-tenant.extension.spec.ts (TS2322 auf einem absichtlich
  unvollstaendigen Fake-Objekt). tx bleibt any, mit Begruendung am Typ.
- Gefolge des jetzt getypten Klienten entfernt: any[]-Annotationen und
  .map((x: any) => ...) in groups.service.ts, module-grants.service.ts,
  dkv.service.ts, ldap-config.service.ts, tenders.controller.ts:270.
- Befund (D-03): tender-matching.service.ts:159 trug eine Handannotation
  (match: { tender: unknown }), die den Wert nur deshalb auf unknown
  verengte, um TS7006 unter dem alten any-Klienten zu vermeiden - mit dem
  getypten Klienten war das falsch. Annotation geloescht, kein Ersatz
  durch Zusicherung.
- Zwei any bleiben gezielt in groups.service.ts (u/a in
  ensureDefaultGroup(), gefolge von tx: any) - Begruendung am Code.

noExplicitAny apps/api/src: 288 -> 149 (Schranke 155). type-check 4/4,
lint 5/5 (0 error). apps/api 72/1143 gruen, apps/web 73/531 gruen,
rls-access-inventory.spec.ts 30/30 gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 16:19:16 +02:00

228 lines
6.9 KiB
TypeScript

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.
*
* Modules are registered centrally; tenants activate/deactivate them
* independently via TenantModuleActivation records (per D-07, D-08).
*/
@Injectable()
export class ModuleRegistryService {
constructor(private readonly prisma: PrismaService) {}
/**
* 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({
orderBy: { name: 'asc' },
});
}
/**
* 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({
where: { slug },
});
}
/**
* Returns all active modules for a given tenant.
*/
async findActiveForTenant(tenantId: string) {
const tenantPrisma = forTenant(this.prisma, tenantId);
const activations = await tenantPrisma.tenantModuleActivation.findMany({
where: {
tenantId,
isActive: true,
},
include: {
module: true,
},
});
return activations.map((a: { module: unknown }) => a.module);
}
/**
* Activates a module for a tenant (upsert: creates or re-activates).
* Per D-08: dynamic activation without restart.
*/
async activateForTenant(tenantId: string, moduleId: string) {
// 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 },
});
if (!moduleExists) {
throw new NotFoundException(`Module with id '${moduleId}' not found`);
}
const tenantPrisma = forTenant(this.prisma, tenantId);
return tenantPrisma.tenantModuleActivation.upsert({
where: {
tenantId_moduleId: {
tenantId,
moduleId,
},
},
update: {
isActive: true,
activatedAt: new Date(),
},
create: {
tenantId,
moduleId,
isActive: true,
},
include: {
module: true,
},
});
}
/**
* Deactivates a module for a tenant (soft-delete: sets isActive=false).
* Does not remove the activation record, preserving audit trail.
*/
async deactivateForTenant(tenantId: string, moduleId: string) {
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
// `findAll` oben.
const moduleExists = await this.prisma.module.findUnique({
where: { id: moduleId },
});
if (!moduleExists) {
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);
// Check if activation record exists
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
where: {
tenantId_moduleId: {
tenantId,
moduleId,
},
},
});
if (!activation) {
throw new NotFoundException(
`Module '${moduleId}' is not activated for this tenant`,
);
}
return tenantPrisma.tenantModuleActivation.update({
where: {
tenantId_moduleId: {
tenantId,
moduleId,
},
},
data: {
isActive: false,
},
include: {
module: true,
},
});
}
/**
* Checks whether a module (by slug) is active for a given tenant.
*
* 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({
where: { slug: moduleSlug },
});
if (!module) {
return false;
}
const tenantPrisma = forTenant(this.prisma, tenantId);
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
where: {
tenantId_moduleId: {
tenantId,
moduleId: module.id,
},
},
});
return activation?.isActive === true;
}
/**
* 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;
name: string;
version: string;
category: string;
description: Record<string, string>;
icon?: string;
isSystem?: boolean;
}) {
return this.prisma.module.upsert({
where: { slug: manifest.slug },
update: {
name: manifest.name,
version: manifest.version,
category: manifest.category,
description: manifest.description,
icon: manifest.icon ?? null,
isSystem: manifest.isSystem ?? false,
},
create: {
slug: manifest.slug,
name: manifest.name,
version: manifest.version,
category: manifest.category,
description: manifest.description,
icon: manifest.icon ?? null,
isSystem: manifest.isSystem ?? false,
},
});
}
}