feat(260911-e2s): Benutzerzaehler im TenantController binden (Fan-out je Mandant)
findAll/findOne/remove zaehlen Benutzer je Mandant jetzt ueber drei
gebundene Aufrufstellen (tenantPrisma.user.count mit where: { tenantId
}, in remove zusaetzlich isActive: true) statt ueber den
Relationszaehler, der nach dem Scharfschalten unbemerkt unter der
Regel von User gelaufen waere (260911-e2s, Aufgabe 1, Pruefungen 5-7).
Fan-out-Muster aus UserService.findAllForPlatformAdmin uebernommen; die
vier tenant-Zugriffe bleiben ungebunden (Tenant ohne Regel). Antwortform,
Meldungen und Statuscodes unveraendert.
tenant.controller.spec.ts legt die Testlage aus dem Nichts an (20
Faelle): Zwei-Klienten-Nachweis ueber __makeBoundClient, Rollen-
Metadaten-Test (Klasse SUPER_ADMIN, kein Handler ueberschreibt), Wachhund
gegen mehrfache Klientenerzeugung. Falsifizierungsnachweis durchgefuehrt:
der probeweise ungebundene Zaehler in findOne macht 2 Faelle rot mit
"Cannot read properties of undefined (reading 'count')" — die dkv-Form
der Falsifizierung, nicht nur eine falsche Zahl —, danach zurueckgenommen.
Klassifikation und Entwicklungsanleitung nachgezogen: 64 Paare (ein
neues, tenant.controller.ts/user), Uebersichtszeile 8/3, Klassen-
Verteilung 32 muss-mandantengebunden, Erkennungsluecke fuer
Relationseinbindungen im Kopf der Bestandsaufnahme benannt, "Zwei
belegte Befunde" und "Was diese Etappe NICHT entscheidet" (erster
Punkt aufgeloest). Beide Dokument-Falsifizierungsnachweise durchgefuehrt
(falsche Klasse macht rls-access-inventory.spec.ts rot, falsche
Uebersichtszahl macht das herleitende Gate rot), zurueckgenommen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
import 'reflect-metadata';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { Role } from '@prisma/client';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ROLES_KEY } from '../auth/decorators/roles.decorator';
|
||||
import { TenantController } from './tenant.controller';
|
||||
|
||||
/**
|
||||
* TenantController.spec — legt die Testlage fuer diesen Bereich aus dem
|
||||
* Nichts an (260911-e2s, Aufgabe 3, Befund I: vorher gab es nur
|
||||
* `tenant.service.spec.ts` mit zwei Faellen zu `create`).
|
||||
*
|
||||
* Zwei-Klienten-Nachweis (Muster aus `../dkv/dkv.service.spec.ts`):
|
||||
* `__makeBoundClient(tenantId)` bietet ein `user.count`-Modell, das
|
||||
* zusaetzlich nach der Mandantenkennung filtert und jeden Aufruf in ein
|
||||
* Bindungsprotokoll schreibt. Der UNGEBUNDENE Nachbau (`prisma.tenant.*`)
|
||||
* hat absichtlich KEIN `user`-Modell — ein versehentlich ungebundener
|
||||
* Zaehler scheitert dadurch mit "Cannot read properties of undefined"
|
||||
* (die `dkv`-Form der Falsifizierung), nicht mit einer nur falschen Zahl.
|
||||
*/
|
||||
|
||||
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
|
||||
}));
|
||||
|
||||
interface FakeTenantRow {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface FakeUserRow {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
function makeFakePrisma(tenantRows: FakeTenantRow[], userRows: FakeUserRow[]) {
|
||||
const tenants = new Map(tenantRows.map((t) => [t.id, { ...t }]));
|
||||
const users = [...userRows];
|
||||
const boundCallLog: { tenantId: string; model: string; method: string; where: any }[] = [];
|
||||
|
||||
const tenantModel = {
|
||||
findMany: vi.fn(async ({ orderBy }: { orderBy?: { name?: 'asc' | 'desc' } } = {}) => {
|
||||
const rows = [...tenants.values()];
|
||||
if (orderBy?.name === 'asc') rows.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return rows;
|
||||
}),
|
||||
findUnique: vi.fn(async ({ where }: { where: { id: string } }) => tenants.get(where.id) ?? null),
|
||||
delete: vi.fn(async ({ where }: { where: { id: string } }) => {
|
||||
const row = tenants.get(where.id) ?? null;
|
||||
tenants.delete(where.id);
|
||||
return row;
|
||||
}),
|
||||
};
|
||||
|
||||
const fake: any = {
|
||||
tenant: tenantModel,
|
||||
__boundCallLog: boundCallLog,
|
||||
__makeBoundClient(tenantId: string) {
|
||||
return {
|
||||
user: {
|
||||
count: async ({
|
||||
where,
|
||||
}: {
|
||||
where: { tenantId: string; isActive?: boolean };
|
||||
}) => {
|
||||
boundCallLog.push({ tenantId, model: 'user', method: 'count', where });
|
||||
return users.filter(
|
||||
(u) =>
|
||||
u.tenantId === where.tenantId &&
|
||||
(where.isActive === undefined || u.isActive === where.isActive),
|
||||
).length;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return fake;
|
||||
}
|
||||
|
||||
function expectBoundCall(prisma: any, tenantId: string, model: string, method: string) {
|
||||
const found = prisma.__boundCallLog.some(
|
||||
(c: any) => c.tenantId === tenantId && c.model === model && c.method === method,
|
||||
);
|
||||
expect(
|
||||
found,
|
||||
`erwarteter gebundener Aufruf ${model}.${method}(tenant=${tenantId}) fehlt im Protokoll: ${JSON.stringify(prisma.__boundCallLog)}`,
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
function makeFakeTenantService() {
|
||||
return {
|
||||
create: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
} as any;
|
||||
}
|
||||
|
||||
const TENANT_A: FakeTenantRow = {
|
||||
id: 'TENANT-A',
|
||||
name: 'A GmbH',
|
||||
slug: 'tenant-a',
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
};
|
||||
const TENANT_B: FakeTenantRow = {
|
||||
id: 'TENANT-B',
|
||||
name: 'B GmbH',
|
||||
slug: 'tenant-b',
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-01-02'),
|
||||
};
|
||||
const TENANT_C: FakeTenantRow = {
|
||||
id: 'TENANT-C',
|
||||
name: 'C GmbH',
|
||||
slug: 'tenant-c',
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-01-03'),
|
||||
};
|
||||
|
||||
describe('TenantController.findAll', () => {
|
||||
it('drei Mandanten (A: zwei Benutzer, B: ein Benutzer, C: keiner): liefert drei Eintraege mit userCount 2/1/0, genau drei gebundene Zaehlaufrufe je Mandantenkennung', async () => {
|
||||
const prisma = makeFakePrisma(
|
||||
[TENANT_A, TENANT_B, TENANT_C],
|
||||
[
|
||||
{ id: 'u-a1', tenantId: 'TENANT-A', isActive: true },
|
||||
{ id: 'u-a2', tenantId: 'TENANT-A', isActive: true },
|
||||
{ id: 'u-b1', tenantId: 'TENANT-B', isActive: true },
|
||||
],
|
||||
);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
const result = await controller.findAll();
|
||||
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({ id: 'TENANT-A', userCount: 2 }),
|
||||
expect.objectContaining({ id: 'TENANT-B', userCount: 1 }),
|
||||
expect.objectContaining({ id: 'TENANT-C', userCount: 0 }),
|
||||
]);
|
||||
expect(prisma.tenant.findMany).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.__boundCallLog).toHaveLength(3);
|
||||
expectBoundCall(prisma, 'TENANT-A', 'user', 'count');
|
||||
expectBoundCall(prisma, 'TENANT-B', 'user', 'count');
|
||||
expectBoundCall(prisma, 'TENANT-C', 'user', 'count');
|
||||
for (const call of prisma.__boundCallLog) {
|
||||
expect(call.where.tenantId).toBe(call.tenantId);
|
||||
}
|
||||
});
|
||||
|
||||
it('ohne Mandanten: leere Liste, KEIN gebundener Klient erzeugt', async () => {
|
||||
const prisma = makeFakePrisma([], []);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
const result = await controller.findAll();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(prisma.__boundCallLog).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('die Antwort traegt genau die Felder id, name, slug, isActive, createdAt, userCount', async () => {
|
||||
const prisma = makeFakePrisma(
|
||||
[TENANT_A],
|
||||
[{ id: 'u-a1', tenantId: 'TENANT-A', isActive: true }],
|
||||
);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
const [entry] = await controller.findAll();
|
||||
|
||||
expect(Object.keys(entry).sort()).toEqual(
|
||||
['createdAt', 'id', 'isActive', 'name', 'slug', 'userCount'].sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TenantController.findOne', () => {
|
||||
it('unbekannte Kennung: NotFoundException, KEIN gebundener Klient', async () => {
|
||||
const prisma = makeFakePrisma([TENANT_A], []);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
await expect(controller.findOne('unknown')).rejects.toThrow(
|
||||
new NotFoundException('Tenant not found'),
|
||||
);
|
||||
expect(prisma.__boundCallLog).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('bekannte Kennung: userCount aus dem gebundenen Klienten UNTER DIESER Kennung', async () => {
|
||||
const prisma = makeFakePrisma(
|
||||
[TENANT_A, TENANT_B],
|
||||
[
|
||||
{ id: 'u-a1', tenantId: 'TENANT-A', isActive: true },
|
||||
{ id: 'u-b1', tenantId: 'TENANT-B', isActive: true },
|
||||
{ id: 'u-b2', tenantId: 'TENANT-B', isActive: true },
|
||||
],
|
||||
);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
const result = await controller.findOne('TENANT-B');
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ id: 'TENANT-B', userCount: 2 }));
|
||||
expectBoundCall(prisma, 'TENANT-B', 'user', 'count');
|
||||
expect(prisma.__boundCallLog).toHaveLength(1);
|
||||
expect(prisma.__boundCallLog[0].where.tenantId).toBe('TENANT-B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TenantController.remove', () => {
|
||||
it('unbekannte Kennung: NotFoundException, kein gebundener Klient, tenant.delete nicht aufgerufen', async () => {
|
||||
const prisma = makeFakePrisma([TENANT_A], []);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
await expect(controller.remove('unknown')).rejects.toThrow(
|
||||
new NotFoundException('Tenant not found'),
|
||||
);
|
||||
expect(prisma.__boundCallLog).toHaveLength(0);
|
||||
expect(prisma.tenant.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('mit aktiven Benutzern: BadRequestException mit der heutigen Meldung, tenant.delete NICHT aufgerufen, der gebundene Zaehlaufruf traegt isActive=true', async () => {
|
||||
const prisma = makeFakePrisma(
|
||||
[TENANT_A],
|
||||
[{ id: 'u-a1', tenantId: 'TENANT-A', isActive: true }],
|
||||
);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
await expect(controller.remove('TENANT-A')).rejects.toThrow(
|
||||
new BadRequestException(
|
||||
'Cannot delete tenant with active users. Deactivate or reassign users first.',
|
||||
),
|
||||
);
|
||||
expect(prisma.tenant.delete).not.toHaveBeenCalled();
|
||||
expectBoundCall(prisma, 'TENANT-A', 'user', 'count');
|
||||
expect(prisma.__boundCallLog[0].where).toEqual({ tenantId: 'TENANT-A', isActive: true });
|
||||
});
|
||||
|
||||
it('mit ausschliesslich inaktiven Benutzern: der Riegel laesst durch, tenant.delete wird ungebunden mit { where: { id } } aufgerufen, Antwort { message: "Tenant deleted" } (heutiges Verhalten — der Fremdschluessel, der das in der echten Datenbank abfaengt, existiert im Nachbau nicht)', async () => {
|
||||
const prisma = makeFakePrisma(
|
||||
[TENANT_A],
|
||||
[{ id: 'u-a1', tenantId: 'TENANT-A', isActive: false }],
|
||||
);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
const result = await controller.remove('TENANT-A');
|
||||
|
||||
expect(result).toEqual({ message: 'Tenant deleted' });
|
||||
expect(prisma.tenant.delete).toHaveBeenCalledWith({ where: { id: 'TENANT-A' } });
|
||||
});
|
||||
|
||||
it('ohne Benutzer: geloescht, Antwort wie oben', async () => {
|
||||
const prisma = makeFakePrisma([TENANT_C], []);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
const result = await controller.remove('TENANT-C');
|
||||
|
||||
expect(result).toEqual({ message: 'Tenant deleted' });
|
||||
expect(prisma.tenant.delete).toHaveBeenCalledWith({ where: { id: 'TENANT-C' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('TenantController.create / update — delegieren an die Dienst-Attrappe', () => {
|
||||
it('create: kein gebundener Klient, kein Aufruf des ungebundenen Nachbaus, delegiert an den Dienst', async () => {
|
||||
const prisma = makeFakePrisma([], []);
|
||||
const tenantService = makeFakeTenantService();
|
||||
tenantService.create.mockResolvedValue(TENANT_A);
|
||||
const controller = new TenantController(tenantService, prisma as any);
|
||||
|
||||
const result = await controller.create({ name: 'A GmbH', slug: 'tenant-a' } as any);
|
||||
|
||||
expect(result).toBe(TENANT_A);
|
||||
expect(tenantService.create).toHaveBeenCalledWith({ name: 'A GmbH', slug: 'tenant-a' });
|
||||
expect(prisma.tenant.findMany).not.toHaveBeenCalled();
|
||||
expect(prisma.tenant.findUnique).not.toHaveBeenCalled();
|
||||
expect(prisma.__boundCallLog).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('update: kein gebundener Klient, kein Aufruf des ungebundenen Nachbaus (ausser der Existenzpruefung ueber den Dienst), delegiert an den Dienst', async () => {
|
||||
const prisma = makeFakePrisma([], []);
|
||||
const tenantService = makeFakeTenantService();
|
||||
tenantService.findById.mockResolvedValue(TENANT_A);
|
||||
tenantService.update.mockResolvedValue({ ...TENANT_A, name: 'Neuer Name' });
|
||||
const controller = new TenantController(tenantService, prisma as any);
|
||||
|
||||
const result = await controller.update('TENANT-A', { name: 'Neuer Name' });
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ name: 'Neuer Name' }));
|
||||
expect(tenantService.update).toHaveBeenCalledWith('TENANT-A', {
|
||||
name: 'Neuer Name',
|
||||
isActive: undefined,
|
||||
});
|
||||
expect(prisma.tenant.findMany).not.toHaveBeenCalled();
|
||||
expect(prisma.__boundCallLog).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TenantController — Rollen-Metadaten (Befund E, T-E2S-01)', () => {
|
||||
it('klassenweit ist genau [Role.SUPER_ADMIN] gesetzt', () => {
|
||||
const roles = Reflect.getMetadata(ROLES_KEY, TenantController);
|
||||
expect(roles).toEqual([Role.SUPER_ADMIN]);
|
||||
});
|
||||
|
||||
it.each(['findAll', 'findOne', 'create', 'update', 'remove'] as const)(
|
||||
'Handler %s traegt KEINE eigene Rollenmetadaten — eine schwaechere Handler-Rolle wuerde die Klassenrolle via getAllAndOverride ueberschreiben',
|
||||
(handlerName) => {
|
||||
const handlerRoles = Reflect.getMetadata(
|
||||
ROLES_KEY,
|
||||
(TenantController.prototype as any)[handlerName],
|
||||
);
|
||||
expect(handlerRoles).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('TenantController — Wachhund: hoechstens ein gebundener Klient je Aufruf und Mandant', () => {
|
||||
it('findOne erzeugt genau EINEN gebundenen Klienten je Aufruf', async () => {
|
||||
const prisma = makeFakePrisma(
|
||||
[TENANT_A],
|
||||
[{ id: 'u-a1', tenantId: 'TENANT-A', isActive: true }],
|
||||
);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
await controller.findOne('TENANT-A');
|
||||
|
||||
expect(prisma.__boundCallLog).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('remove erzeugt genau EINEN gebundenen Klienten je Aufruf', async () => {
|
||||
const prisma = makeFakePrisma([TENANT_A], []);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
await controller.remove('TENANT-A');
|
||||
|
||||
expect(prisma.__boundCallLog).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('findAll erzeugt genau so viele gebundene Klienten wie Mandanten vorhanden sind', async () => {
|
||||
const prisma = makeFakePrisma([TENANT_A, TENANT_B, TENANT_C], []);
|
||||
const controller = new TenantController(makeFakeTenantService(), prisma as any);
|
||||
|
||||
await controller.findAll();
|
||||
|
||||
expect(prisma.__boundCallLog).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Role } from '@prisma/client';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateTenantDto } from './dto/create-tenant.dto';
|
||||
import { TenantService } from './tenant.service';
|
||||
@@ -21,6 +22,30 @@ import { TenantService } from './tenant.service';
|
||||
* Tenant CRUD controller.
|
||||
* D-10: Only Super-Admin can manage tenants.
|
||||
* T-02-09: Tenant deletion blocked if active users exist.
|
||||
*
|
||||
* User counts (260911-e2s, Aufgabe 3): `findAll`/`findOne`/`remove` used to
|
||||
* read the user count through a relation include on the four unbound
|
||||
* tenant reads below. Prisma renders that as a single statement with a
|
||||
* LEFT JOIN into the protected `User` table — which carries a row-level
|
||||
* security rule. After the switch flips, an unbound relation count reads
|
||||
* zero for every tenant (measured, 260911-e2s Aufgabe 1, Pruefungen 5-7),
|
||||
* which would make the platform-admin tenant list show zero users
|
||||
* everywhere and let the delete gate below pass through with active users
|
||||
* still present. Fixed by the fan-out pattern `UserService
|
||||
* .findAllForPlatformAdmin` already uses: read tenants unbound, then bind
|
||||
* ONE user count per tenant via the tenant-binding helper. The explicit
|
||||
* `where: { tenantId }` on each bound count is TODAY (role runs with
|
||||
* BYPASSRLS, WINDOWS #18) the only filter actually in effect.
|
||||
*
|
||||
* The four tenant reads/writes below stay unbound on purpose — `Tenant`
|
||||
* carries no row-level security rule in any shipped migration (measured,
|
||||
* 260911-e2s Aufgabe 1, Pruefungen 1/2); nothing on `Tenant` itself needs
|
||||
* binding.
|
||||
*
|
||||
* This controller keeps talking to Prisma directly rather than going
|
||||
* through a service method (same pattern as `user.controller.ts`) — a
|
||||
* deliberate choice, not an oversight; see
|
||||
* docs/mandantentrennung-zugriffsklassifikation.md, "(n5)".
|
||||
*/
|
||||
@Controller('tenants')
|
||||
@UseGuards(RolesGuard)
|
||||
@@ -38,22 +63,26 @@ export class TenantController {
|
||||
@Get()
|
||||
async findAll() {
|
||||
const tenants = await this.prisma.tenant.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: { users: true },
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
|
||||
return tenants.map((t: any) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
slug: t.slug,
|
||||
isActive: t.isActive,
|
||||
createdAt: t.createdAt,
|
||||
userCount: t._count.users,
|
||||
}));
|
||||
const results: any[] = [];
|
||||
for (const tenant of tenants) {
|
||||
const tenantPrisma = forTenant(this.prisma, tenant.id) as any;
|
||||
const userCount = await tenantPrisma.user.count({
|
||||
where: { tenantId: tenant.id },
|
||||
});
|
||||
results.push({
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
slug: tenant.slug,
|
||||
isActive: tenant.isActive,
|
||||
createdAt: tenant.createdAt,
|
||||
userCount,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,24 +93,24 @@ export class TenantController {
|
||||
async findOne(@Param('id') id: string) {
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: { users: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
throw new NotFoundException('Tenant not found');
|
||||
}
|
||||
|
||||
const tenantPrisma = forTenant(this.prisma, id) as any;
|
||||
const userCount = await tenantPrisma.user.count({
|
||||
where: { tenantId: id },
|
||||
});
|
||||
|
||||
return {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
slug: tenant.slug,
|
||||
isActive: tenant.isActive,
|
||||
createdAt: tenant.createdAt,
|
||||
userCount: tenant._count.users,
|
||||
userCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,20 +154,18 @@ export class TenantController {
|
||||
async remove(@Param('id') id: string) {
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
users: { where: { isActive: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
throw new NotFoundException('Tenant not found');
|
||||
}
|
||||
|
||||
if (tenant._count.users > 0) {
|
||||
const tenantPrisma = forTenant(this.prisma, id) as any;
|
||||
const activeUserCount = await tenantPrisma.user.count({
|
||||
where: { tenantId: id, isActive: true },
|
||||
});
|
||||
|
||||
if (activeUserCount > 0) {
|
||||
throw new BadRequestException(
|
||||
'Cannot delete tenant with active users. Deactivate or reassign users first.',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user