feat(260805-fok): beide Mandanten-Entstehungspfade verdrahten + Startup-Reparatur
- TenantService.create ruft nach prisma.tenant.create ensureDefaultGroup auf; Fehler werden protokolliert, nicht propagiert (Muster aus UserService.create) - tenant.module.ts importiert GroupsModule (keine Zirkularitaet, wie UserModule bereits vormacht) - AdminSeedService.onApplicationBootstrap besteht jetzt aus zwei sequenziellen await-Schritten: seedAdmin() (bisheriger Rumpf, plus ensureDefaultGroup nach dem Tenant-Upsert und VOR user.create), dann ensureDefaultGroupsForAllTenants() als abschliessende Reparatur ueber ALLE Mandanten — laeuft unabhaengig von seedAdmin()s fruehen Rueckkehrpfaden (fehlende ENV / Admin existiert bereits) und ist je Mandant sowie insgesamt try/catch-gekapselt, blockiert den API-Start nie - Reparatur sitzt bewusst NICHT als eigener onApplicationBootstrap-Hook in GroupsModule (Ordering-Falle aus tender-scheduler.service.ts) - tenant.service.spec.ts, admin-seed.service.spec.ts (neu): Reihenfolge, beide fruehen Rueckkehrpfade, Fehlerisolation je Mandant, Idempotenz ueber zwei Bootstrap-Laeufe
This commit is contained in:
@@ -1,8 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { GroupsModule } from '../groups/groups.module';
|
||||||
import { TenantController } from './tenant.controller';
|
import { TenantController } from './tenant.controller';
|
||||||
import { TenantService } from './tenant.service';
|
import { TenantService } from './tenant.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importiert GroupsModule für TenantService.create's Standardgruppen-
|
||||||
|
* Anlage (D-06/D-13). GroupsModule importiert seinerseits nichts,
|
||||||
|
* deshalb entsteht keine Zirkularität — UserModule bindet GroupsModule
|
||||||
|
* bereits nach demselben Muster ein.
|
||||||
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [GroupsModule],
|
||||||
controllers: [TenantController],
|
controllers: [TenantController],
|
||||||
providers: [TenantService],
|
providers: [TenantService],
|
||||||
exports: [TenantService],
|
exports: [TenantService],
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { TenantService } from './tenant.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TenantService.create — Standardgruppen-Anlage (D-06/D-13, quick-260805-fok).
|
||||||
|
*
|
||||||
|
* TenantService.create ist neben AdminSeedService.seedAdmin der zweite
|
||||||
|
* Mandanten-Entstehungspfad. Diese Tests decken ausschließlich die neue
|
||||||
|
* ensureDefaultGroup-Anbindung ab, mit gemocktem PrismaService und
|
||||||
|
* gemocktem GroupsService — im Stil von user.service.spec.ts.
|
||||||
|
*/
|
||||||
|
describe('TenantService.create — Standardgruppen-Anlage (D-06/D-13)', () => {
|
||||||
|
let prisma: any;
|
||||||
|
let groupsService: any;
|
||||||
|
let service: TenantService;
|
||||||
|
|
||||||
|
const createdTenant = { id: 't1', name: 'Acme', slug: 'acme' };
|
||||||
|
const baseData = { name: 'Acme', slug: 'acme' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
prisma = {
|
||||||
|
tenant: {
|
||||||
|
create: vi.fn().mockResolvedValue(createdTenant),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
groupsService = {
|
||||||
|
ensureDefaultGroup: vi.fn().mockResolvedValue({ id: 'g1' }),
|
||||||
|
};
|
||||||
|
service = new TenantService(prisma, groupsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ruft nach der Mandanten-Anlage genau einmal ensureDefaultGroup mit der id des neu angelegten Mandanten auf; der Rueckgabewert bleibt der Mandant', async () => {
|
||||||
|
const result = await service.create(baseData);
|
||||||
|
|
||||||
|
expect(result).toEqual(createdTenant);
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledTimes(1);
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith('t1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gibt den Mandanten trotz werfendem ensureDefaultGroup zurueck — der Fehler wird protokolliert, nicht propagiert', async () => {
|
||||||
|
groupsService.ensureDefaultGroup.mockRejectedValue(new Error('boom'));
|
||||||
|
|
||||||
|
await expect(service.create(baseData)).resolves.toEqual(createdTenant);
|
||||||
|
expect(prisma.tenant.create).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { GroupsService } from '../groups/groups.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TenantService {
|
export class TenantService {
|
||||||
constructor(private prisma: PrismaService) {}
|
private readonly logger = new Logger(TenantService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
private readonly groupsService: GroupsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async findAll() {
|
async findAll() {
|
||||||
return this.prisma.tenant.findMany();
|
return this.prisma.tenant.findMany();
|
||||||
@@ -13,8 +19,30 @@ export class TenantService {
|
|||||||
return this.prisma.tenant.findUnique({ where: { id } });
|
return this.prisma.tenant.findUnique({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legt einen Mandanten an und stellt unmittelbar danach dessen
|
||||||
|
* Standardgruppe her (D-06/D-13), damit ein frisch angelegter Mandant
|
||||||
|
* ab dem ersten Benutzer nutzbar ist. Schlägt ensureDefaultGroup fehl,
|
||||||
|
* wird der Fehler protokolliert statt propagiert — die Mandanten-Anlage
|
||||||
|
* selbst war erfolgreich, und die Startup-Reparatur in
|
||||||
|
* AdminSeedService.ensureDefaultGroupsForAllTenants holt eine
|
||||||
|
* gescheiterte Gruppenanlage beim nächsten API-Start nach (Muster aus
|
||||||
|
* UserService.create).
|
||||||
|
*/
|
||||||
async create(data: { name: string; slug: string }) {
|
async create(data: { name: string; slug: string }) {
|
||||||
return this.prisma.tenant.create({ data });
|
const tenant = await this.prisma.tenant.create({ data });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.groupsService.ensureDefaultGroup(tenant.id);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Standardgruppen-Anlage fehlgeschlagen für Mandant '${tenant.id}': ${
|
||||||
|
err instanceof Error ? err.message : String(err)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, data: { name?: string; slug?: string; isActive?: boolean }) {
|
async update(id: string, data: { name?: string; slug?: string; isActive?: boolean }) {
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { AdminSeedService } from './admin-seed.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AdminSeedService — Reihenfolge der Mandanten-/Admin-Anlage und
|
||||||
|
* Startup-Reparatur (quick-260805-fok).
|
||||||
|
*
|
||||||
|
* Deckt ab: seedAdmin() ruft ensureDefaultGroup NACH tenant.upsert und VOR
|
||||||
|
* user.create auf; beide frühen Rückkehrpfade (fehlende ENV, Admin existiert
|
||||||
|
* bereits) überspringen den Benutzer, lassen aber die Reparatur über ALLE
|
||||||
|
* Mandanten laufen; die Reparatur ist fehlerisoliert je Mandant; ein zweiter
|
||||||
|
* Bootstrap-Lauf löst keinen zusätzlichen user.create aus.
|
||||||
|
*
|
||||||
|
* Die Reihenfolgeprüfung läuft über ein gemeinsames Aufruf-Log-Array, in das
|
||||||
|
* jeder Mock beim Aufruf seinen Namen schiebt (nicht über bloße
|
||||||
|
* Aufrufzähler) — Mock-invocationCallOrder ist über drei unabhängige vi.fn's
|
||||||
|
* (tenant.upsert / ensureDefaultGroup / user.create) weniger lesbar als ein
|
||||||
|
* geteiltes Log.
|
||||||
|
*/
|
||||||
|
describe('AdminSeedService', () => {
|
||||||
|
let prisma: any;
|
||||||
|
let configService: any;
|
||||||
|
let groupsService: any;
|
||||||
|
let callLog: string[];
|
||||||
|
let service: AdminSeedService;
|
||||||
|
|
||||||
|
const tenant = { id: 't-default', slug: 'default' };
|
||||||
|
const envValues: Record<string, string> = {
|
||||||
|
TESSERA_ADMIN_USER: 'admin',
|
||||||
|
TESSERA_ADMIN_EMAIL: 'admin@example.com',
|
||||||
|
TESSERA_ADMIN_PASSWORD: 'secret',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
callLog = [];
|
||||||
|
|
||||||
|
configService = {
|
||||||
|
get: vi.fn((key: string) => envValues[key]),
|
||||||
|
};
|
||||||
|
|
||||||
|
prisma = {
|
||||||
|
user: {
|
||||||
|
findUnique: vi.fn(async () => {
|
||||||
|
callLog.push('user.findUnique');
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
create: vi.fn(async () => {
|
||||||
|
callLog.push('user.create');
|
||||||
|
return { id: 'u1' };
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
tenant: {
|
||||||
|
upsert: vi.fn(async () => {
|
||||||
|
callLog.push('tenant.upsert');
|
||||||
|
return tenant;
|
||||||
|
}),
|
||||||
|
findMany: vi.fn(async () => {
|
||||||
|
callLog.push('tenant.findMany');
|
||||||
|
return [tenant];
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
groupsService = {
|
||||||
|
ensureDefaultGroup: vi.fn(async (tenantId: string) => {
|
||||||
|
callLog.push(`ensureDefaultGroup:${tenantId}`);
|
||||||
|
return { id: 'g1' };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service = new AdminSeedService(prisma, configService, groupsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ruft auf einer frischen Installation in genau dieser Reihenfolge auf: tenant.upsert, ensureDefaultGroup, dann user.create', async () => {
|
||||||
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
|
const seedOrder = callLog.filter(
|
||||||
|
(c) => c === 'tenant.upsert' || c === 'user.create',
|
||||||
|
);
|
||||||
|
expect(seedOrder).toEqual(['tenant.upsert', 'user.create']);
|
||||||
|
|
||||||
|
const upsertIdx = callLog.indexOf('tenant.upsert');
|
||||||
|
const ensureIdx = callLog.indexOf(`ensureDefaultGroup:${tenant.id}`);
|
||||||
|
const createIdx = callLog.indexOf('user.create');
|
||||||
|
expect(upsertIdx).toBeLessThan(ensureIdx);
|
||||||
|
expect(ensureIdx).toBeLessThan(createIdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fehlen die ENV-Variablen, wird kein Benutzer angelegt, die Reparatur ueber alle Mandanten laeuft aber trotzdem', async () => {
|
||||||
|
configService.get = vi.fn(() => undefined);
|
||||||
|
|
||||||
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
|
expect(prisma.tenant.upsert).not.toHaveBeenCalled();
|
||||||
|
expect(prisma.user.create).not.toHaveBeenCalled();
|
||||||
|
expect(prisma.tenant.findMany).toHaveBeenCalledTimes(1);
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('existiert der Admin-Benutzer bereits, wird kein Benutzer angelegt, die Reparatur ueber alle Mandanten laeuft aber trotzdem', async () => {
|
||||||
|
prisma.user.findUnique = vi.fn(async () => ({ id: 'existing-admin' }));
|
||||||
|
|
||||||
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
|
expect(prisma.tenant.upsert).not.toHaveBeenCalled();
|
||||||
|
expect(prisma.user.create).not.toHaveBeenCalled();
|
||||||
|
expect(prisma.tenant.findMany).toHaveBeenCalledTimes(1);
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('die Reparatur ruft ensureDefaultGroup fuer JEDEN von tenant.findMany gelieferten Mandanten auf, auch fuer nicht-default-Mandanten', async () => {
|
||||||
|
prisma.user.findUnique = vi.fn(async () => ({ id: 'existing-admin' }));
|
||||||
|
const otherTenant = { id: 't-other', slug: 'other' };
|
||||||
|
prisma.tenant.findMany = vi.fn(async () => [tenant, otherTenant]);
|
||||||
|
|
||||||
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id);
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(otherTenant.id);
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wirft ensureDefaultGroup fuer einen Mandanten, werden die uebrigen Mandanten trotzdem abgearbeitet und onApplicationBootstrap wirft nicht', async () => {
|
||||||
|
prisma.user.findUnique = vi.fn(async () => ({ id: 'existing-admin' }));
|
||||||
|
const failing = { id: 't-fail', slug: 'fail' };
|
||||||
|
const ok = { id: 't-ok', slug: 'ok' };
|
||||||
|
prisma.tenant.findMany = vi.fn(async () => [failing, ok]);
|
||||||
|
groupsService.ensureDefaultGroup = vi.fn(async (tenantId: string) => {
|
||||||
|
if (tenantId === failing.id) {
|
||||||
|
throw new Error('boom');
|
||||||
|
}
|
||||||
|
return { id: 'g-ok' };
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(service.onApplicationBootstrap()).resolves.not.toThrow();
|
||||||
|
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(failing.id);
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(ok.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein zweiter onApplicationBootstrap-Lauf ruft ensureDefaultGroup erneut auf, loest aber keinen zusaetzlichen user.create aus', async () => {
|
||||||
|
await service.onApplicationBootstrap();
|
||||||
|
expect(prisma.user.create).toHaveBeenCalledTimes(1);
|
||||||
|
// Erster Lauf: seedAdmin() ruft ensureDefaultGroup einmal vor
|
||||||
|
// user.create auf, die Reparatur einmal danach fuer denselben Mandanten.
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
// Zweiter Lauf: Admin existiert jetzt bereits (Neustart-Semantik), also
|
||||||
|
// ruft nur noch die Reparatur ensureDefaultGroup auf — kein weiterer
|
||||||
|
// user.create.
|
||||||
|
prisma.user.findUnique = vi.fn(async () => ({ id: 'u1' }));
|
||||||
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
|
expect(prisma.user.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,27 @@
|
|||||||
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
|
import { GroupsService } from '../groups/groups.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the initial Super-Admin account from Docker ENV variables on first boot.
|
* Creates the initial Super-Admin account from Docker ENV variables on first boot.
|
||||||
* Per D-05, D-07, D-13.
|
* Per D-05, D-07, D-13.
|
||||||
|
*
|
||||||
|
* onApplicationBootstrap läuft in genau zwei sequenziellen await-Schritten:
|
||||||
|
* erst seedAdmin(), danach ensureDefaultGroupsForAllTenants(). Diese
|
||||||
|
* sequenzielle Reihenfolge — nicht Nests Hook-Reihenfolge über Module
|
||||||
|
* hinweg — ist der Ordering-Garant. Die Reparatur sitzt bewusst NICHT als
|
||||||
|
* eigener onApplicationBootstrap-Hook in GroupsModule: GroupsModule ist
|
||||||
|
* eine Dependency von UserModule, seine eigenen Hooks liefen deshalb VOR
|
||||||
|
* dem Admin-Seed und würden auf einer frischen Installation den noch gar
|
||||||
|
* nicht existierenden Default-Mandanten übergehen — dieselbe Falle, die
|
||||||
|
* tenders/tender-scheduler.service.ts für den DÖE-Poll-Config-Seed
|
||||||
|
* ausführlich dokumentiert (onModuleInit-Reihenfolge zwischen Modulen ist
|
||||||
|
* unspezifiziert, onApplicationBootstrap läuft garantiert nach jedem
|
||||||
|
* onModuleInit). seedAdmin() bleibt bewusst UNgekapselt: schlägt der
|
||||||
|
* Admin-Seed fehl, soll der Start weiterhin laut scheitern — bestehendes
|
||||||
|
* Verhalten, hier nicht aufgeweicht.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminSeedService implements OnApplicationBootstrap {
|
export class AdminSeedService implements OnApplicationBootstrap {
|
||||||
@@ -14,9 +30,15 @@ export class AdminSeedService implements OnApplicationBootstrap {
|
|||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private readonly groupsService: GroupsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onApplicationBootstrap() {
|
async onApplicationBootstrap() {
|
||||||
|
await this.seedAdmin();
|
||||||
|
await this.ensureDefaultGroupsForAllTenants();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async seedAdmin() {
|
||||||
const username = this.configService
|
const username = this.configService
|
||||||
.get<string>('TESSERA_ADMIN_USER')
|
.get<string>('TESSERA_ADMIN_USER')
|
||||||
?.toLowerCase();
|
?.toLowerCase();
|
||||||
@@ -49,6 +71,12 @@ export class AdminSeedService implements OnApplicationBootstrap {
|
|||||||
create: { name: 'Default', slug: 'default' },
|
create: { name: 'Default', slug: 'default' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure the default group exists BEFORE the Super-Admin user is
|
||||||
|
// created, so user.create's addUserToDefaultGroup path (D-11/D-12)
|
||||||
|
// finds a marked default group to join instead of relying on the
|
||||||
|
// repair below to backfill the membership afterwards.
|
||||||
|
await this.groupsService.ensureDefaultGroup(tenant.id);
|
||||||
|
|
||||||
// Create Super-Admin user
|
// Create Super-Admin user
|
||||||
const passwordHash = await argon2.hash(password);
|
const passwordHash = await argon2.hash(password);
|
||||||
await this.prisma.user.create({
|
await this.prisma.user.create({
|
||||||
@@ -67,4 +95,50 @@ export class AdminSeedService implements OnApplicationBootstrap {
|
|||||||
`Admin user "${username}" seeded as SUPER_ADMIN in tenant "${tenant.slug}"`,
|
`Admin user "${username}" seeded as SUPER_ADMIN in tenant "${tenant.slug}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-time startup repair for installations that already exist without a
|
||||||
|
* default group — e.g. a fresh database where migration
|
||||||
|
* 20260804130130_add_groups_and_module_grants' backfill ran before any
|
||||||
|
* tenant existed, or an install whose admin already exists so seedAdmin()
|
||||||
|
* returned early without ever touching a tenant. Runs per-tenant in its
|
||||||
|
* own try/catch and is additionally wrapped as a whole, so a failing
|
||||||
|
* tenant.findMany or a single tenant's ensureDefaultGroup call can never
|
||||||
|
* block the API from starting.
|
||||||
|
*/
|
||||||
|
private async ensureDefaultGroupsForAllTenants() {
|
||||||
|
try {
|
||||||
|
const tenants = await this.prisma.tenant.findMany({
|
||||||
|
select: { id: true, slug: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
let repaired = 0;
|
||||||
|
for (const tenant of tenants) {
|
||||||
|
try {
|
||||||
|
const group = await this.groupsService.ensureDefaultGroup(tenant.id);
|
||||||
|
if (group !== null) {
|
||||||
|
repaired += 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Default-group repair failed for tenant "${tenant.slug}": ${
|
||||||
|
err instanceof Error ? err.message : String(err)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repaired > 0) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Default-group repair created a default group for ${repaired} tenant(s) that had none`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Default-group repair could not load tenants: ${
|
||||||
|
err instanceof Error ? err.message : String(err)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user