feat(16-02): standard group handoff building block (D-06)
- DEFAULT_GROUP_NAME extracted as shared constant between ensureDefaultGroup() and the new reassignDefaultBeforeDelete() - reassignDefaultBeforeDelete(tenantId, groupId) moves the default marker deterministically (DEFAULT_GROUP_NAME first, else oldest other group by createdAt asc), never deletes, never throws - 6 test cases covering handoff, fallback ordering, no-other-group, non-default no-op, cross-tenant no-op, and P2002 race
This commit is contained in:
@@ -7,6 +7,15 @@ import {
|
||||
import { MembershipSource } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
/**
|
||||
* Name der automatisch angelegten Standardgruppe (D-13). Geteilte Wahrheit
|
||||
* zwischen ensureDefaultGroup() (legt sie an, falls der Mandant noch keine
|
||||
* Gruppe hat) und reassignDefaultBeforeDelete() (bevorzugtes Handoff-Ziel,
|
||||
* D-06) — zwei getrennte Literale würden bei einer Umbenennung
|
||||
* auseinanderlaufen.
|
||||
*/
|
||||
export const DEFAULT_GROUP_NAME = 'Alle Benutzer';
|
||||
|
||||
/**
|
||||
* Service für Gruppen-CRUD, Mitgliederverwaltung und die automatische
|
||||
* Standardgruppen-Mitgliedschaft (PERM-01/PERM-06, D-11/D-12/D-13/D-17/D-19).
|
||||
@@ -254,7 +263,7 @@ 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
|
||||
* eine Gruppe DEFAULT_GROUP_NAME (isDefault:true), alle Bestandsbenutzer als
|
||||
* MANUAL-Mitglieder und Grants für alle aktiven Module. Aufrufer:
|
||||
* TenantService.create (frischer Mandant) und
|
||||
* AdminSeedService.ensureDefaultGroupsForAllTenants (Startup-Reparatur
|
||||
@@ -283,7 +292,7 @@ export class GroupsService {
|
||||
try {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
const group = await tx.group.create({
|
||||
data: { tenantId, name: 'Alle Benutzer', isDefault: true },
|
||||
data: { tenantId, name: DEFAULT_GROUP_NAME, isDefault: true },
|
||||
});
|
||||
|
||||
const users = await tx.user.findMany({
|
||||
@@ -327,6 +336,72 @@ export class GroupsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschiebt die Standardmarkierung weg von der übergebenen Gruppe, BEVOR
|
||||
* der Aufrufer sie löscht (D-06). Löscht selbst nichts — meldet
|
||||
* ausschließlich per Rückgabewert, ob sie etwas verschoben hat.
|
||||
*
|
||||
* Bewusst NICHT findOwned(): dessen NotFoundException ist für HTTP
|
||||
* gebaut und würde einen Batch-Sync-Lauf (Plan 16-03) abbrechen. Eine
|
||||
* Gruppen-ID aus einem fremden Mandanten oder eine nicht markierte
|
||||
* Gruppe ist stattdessen ein folgenloses No-Op mit Rückgabe false.
|
||||
*
|
||||
* Zielauswahl: zuerst DEFAULT_GROUP_NAME (falls im Mandanten vorhanden
|
||||
* und nicht die zu löschende Gruppe selbst), sonst die älteste andere
|
||||
* Gruppe (orderBy createdAt asc) — dieser Determinismus garantiert, dass
|
||||
* zwei Läufe über denselben Bestand dieselbe Gruppe wählen. Existiert
|
||||
* keine andere Gruppe, gibt die Methode false zurück; der Aufrufer ruft
|
||||
* danach ensureDefaultGroup(tenantId), um den Mandanten neu aufzubauen.
|
||||
*
|
||||
* Dieselbe Zwei-Schritt-Transaktionsform wie update() (isDefault:true):
|
||||
* erst updateMany auf isDefault:false für den ganzen Mandanten, dann
|
||||
* update der Zielgruppe auf isDefault:true. Der partielle Unique-Index
|
||||
* Group_one_default_per_tenant (15-01) bleibt der eigentliche
|
||||
* Durchsetzungspunkt; ein daraus resultierender P2002 wird als "hat sich
|
||||
* schon jemand anderes gekümmert" behandelt und liefert false statt zu
|
||||
* werfen — exakt das Muster aus ensureDefaultGroup().
|
||||
*/
|
||||
async reassignDefaultBeforeDelete(tenantId: string, groupId: string): Promise<boolean> {
|
||||
const group = await this.prisma.group.findFirst({
|
||||
where: { id: groupId, tenantId },
|
||||
});
|
||||
if (!group || !group.isDefault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target =
|
||||
(await this.prisma.group.findFirst({
|
||||
where: { tenantId, id: { not: groupId }, name: DEFAULT_GROUP_NAME },
|
||||
})) ??
|
||||
(await this.prisma.group.findFirst({
|
||||
where: { tenantId, id: { not: groupId } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
}));
|
||||
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.group.updateMany({
|
||||
where: { tenantId, isDefault: true },
|
||||
data: { isDefault: false },
|
||||
}),
|
||||
this.prisma.group.update({
|
||||
where: { id: target.id },
|
||||
data: { isDefault: true },
|
||||
}),
|
||||
]);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'P2002') {
|
||||
return false;
|
||||
}
|
||||
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