feat(260805-fok): GroupsService.ensureDefaultGroup(tenantId)
- Neue Methode ensureDefaultGroup: legt fuer einen Mandanten ohne jede Gruppe die Standardgruppe 'Alle Benutzer' (isDefault:true) an, nimmt alle Bestandsbenutzer als MANUAL-Mitglieder auf und erzeugt Grants fuer alle aktiven Module — derselbe Endzustand wie die drei Backfill-INSERTs der Migration 20260804130130 - Waechter prueft ausschliesslich group.count === 0, niemals die fehlende isDefault-Markierung (D-13) - P2002 aus dem partiellen Index Group_one_default_per_tenant wird abgefangen und liefert null statt zu werfen (Race-Sicherheit) - groups.service.spec.ts: Fake erweitert um group.count, tenantModuleActivation, moduleGrant.findMany/createMany, $transaction mit Callback-Form, plus voller ensureDefaultGroup-Testblock
This commit is contained in:
@@ -250,6 +250,83 @@ export class GroupsService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt für einen Mandanten OHNE JEDE Gruppe denselben Endzustand her,
|
||||
* den die drei Backfill-INSERTs der Migration
|
||||
* 20260804130130_add_groups_and_module_grants pro Mandant herstellen:
|
||||
* eine Gruppe 'Alle Benutzer' (isDefault:true), alle Bestandsbenutzer als
|
||||
* MANUAL-Mitglieder und Grants für alle aktiven Module. Aufrufer:
|
||||
* TenantService.create (frischer Mandant) und
|
||||
* AdminSeedService.ensureDefaultGroupsForAllTenants (Startup-Reparatur
|
||||
* für Installationen, die die Migration ohne Mandanten durchlaufen haben).
|
||||
*
|
||||
* Der Wächter prüft AUSSCHLIESSLICH auf group.count === 0 — niemals auf
|
||||
* das Fehlen der isDefault-Markierung. D-13 erlaubt dem Admin
|
||||
* ausdrücklich, die Markierung abzuhängen oder auf eine andere Gruppe
|
||||
* umzuhängen; ein Mandant mit mindestens einer Gruppe hat diese
|
||||
* Entscheidung bereits getroffen und wird hier nie wieder angefasst.
|
||||
* Rückgabe null bedeutet in jedem Fall "nichts zu tun".
|
||||
*
|
||||
* Race-Sicherheit: zwei gleichzeitige Aufrufe (z.B. Startup-Reparatur und
|
||||
* eine parallele Mandanten-Anlage) können beide group.count === 0 lesen.
|
||||
* Der partielle Unique-Index Group_one_default_per_tenant (15-01) bleibt
|
||||
* der eigentliche Durchsetzungspunkt; hier wird nur der resultierende
|
||||
* P2002 des Verlierers abgefangen und in null übersetzt, statt ihn zu
|
||||
* propagieren.
|
||||
*/
|
||||
async ensureDefaultGroup(tenantId: string) {
|
||||
const existingCount = await this.prisma.group.count({ where: { tenantId } });
|
||||
if (existingCount > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
const group = await tx.group.create({
|
||||
data: { tenantId, name: 'Alle Benutzer', isDefault: true },
|
||||
});
|
||||
|
||||
const users = await tx.user.findMany({
|
||||
where: { tenantId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (users.length > 0) {
|
||||
await tx.groupMembership.createMany({
|
||||
data: users.map((u) => ({
|
||||
groupId: group.id,
|
||||
userId: u.id,
|
||||
source: MembershipSource.MANUAL,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
const activations = await tx.tenantModuleActivation.findMany({
|
||||
where: { tenantId, isActive: true },
|
||||
select: { moduleId: true },
|
||||
});
|
||||
if (activations.length > 0) {
|
||||
await tx.moduleGrant.createMany({
|
||||
data: activations.map((a) => ({
|
||||
tenantId,
|
||||
moduleId: a.moduleId,
|
||||
groupId: group.id,
|
||||
userId: null,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
return group;
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'P2002') {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt eine Mitgliedschaft in der als Standard markierten Gruppe des
|
||||
* Mandanten an (D-11/D-12/D-13). Existiert keine markierte Standardgruppe,
|
||||
|
||||
Reference in New Issue
Block a user