good forst commit
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { RoutesController } from './routes.controller';
|
||||
import { RoutesService } from './routes.service';
|
||||
import type { TenantContext } from '../../common/tenant-context';
|
||||
|
||||
const ctx: TenantContext = { tenantId: 'tnt_1', adminId: 'adm_1', role: 'OWNER' };
|
||||
|
||||
const mockRoute = {
|
||||
id: 'rt_1',
|
||||
@@ -12,6 +15,7 @@ const mockRoute = {
|
||||
const mockService = {
|
||||
list: jest.fn().mockResolvedValue([mockRoute]),
|
||||
create: jest.fn().mockResolvedValue(mockRoute),
|
||||
createBatch: jest.fn().mockResolvedValue([mockRoute, { ...mockRoute, id: 'rt_2', targetGroupId: 'grp_3', targetGroup: { name: 'Gamma' } }]),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
@@ -27,25 +31,31 @@ describe('RoutesController', () => {
|
||||
controller = module.get<RoutesController>(RoutesController);
|
||||
});
|
||||
|
||||
it('list() delegates to service with no filter', async () => {
|
||||
const result = await controller.list(undefined);
|
||||
it('list() delegates to service with tenantId and no filter', async () => {
|
||||
const result = await controller.list(ctx, undefined);
|
||||
expect(result).toEqual([mockRoute]);
|
||||
expect(mockService.list).toHaveBeenCalledWith(undefined);
|
||||
expect(mockService.list).toHaveBeenCalledWith('tnt_1', undefined);
|
||||
});
|
||||
|
||||
it('list() passes sourceGroupId filter to service', async () => {
|
||||
await controller.list('grp_1');
|
||||
expect(mockService.list).toHaveBeenCalledWith('grp_1');
|
||||
it('list() passes sourceGroupId filter to service along with tenantId', async () => {
|
||||
await controller.list(ctx, 'grp_1');
|
||||
expect(mockService.list).toHaveBeenCalledWith('tnt_1', 'grp_1');
|
||||
});
|
||||
|
||||
it('create() extracts body fields and delegates to service', async () => {
|
||||
const result = await controller.create({ sourceGroupId: 'grp_1', targetGroupId: 'grp_2' });
|
||||
it('create() extracts body fields and delegates to service with tenantId', async () => {
|
||||
const result = await controller.create(ctx, { sourceGroupId: 'grp_1', targetGroupId: 'grp_2' });
|
||||
expect(result).toEqual(mockRoute);
|
||||
expect(mockService.create).toHaveBeenCalledWith('grp_1', 'grp_2');
|
||||
expect(mockService.create).toHaveBeenCalledWith('tnt_1', 'grp_1', 'grp_2');
|
||||
});
|
||||
|
||||
it('remove() delegates id to service', async () => {
|
||||
await controller.remove('rt_1');
|
||||
expect(mockService.remove).toHaveBeenCalledWith('rt_1');
|
||||
it('remove() delegates tenantId and id to service', async () => {
|
||||
await controller.remove(ctx, 'rt_1');
|
||||
expect(mockService.remove).toHaveBeenCalledWith('tnt_1', 'rt_1');
|
||||
});
|
||||
|
||||
it('createBatch() extracts body fields and delegates to service with tenantId', async () => {
|
||||
const result = await controller.createBatch(ctx, { sourceGroupId: 'grp_1', targetGroupIds: ['grp_2', 'grp_3'] });
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockService.createBatch).toHaveBeenCalledWith('tnt_1', 'grp_1', ['grp_2', 'grp_3']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,44 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Query } from '@nestjs/common';
|
||||
import { RoutesService } from './routes.service';
|
||||
import { CurrentTenantContext } from '../auth/current-tenant.decorator';
|
||||
import { TenantContext } from '../../common/tenant-context';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
class CreateRouteDto {
|
||||
@IsString() sourceGroupId!: string;
|
||||
@IsString() targetGroupId!: string;
|
||||
}
|
||||
|
||||
class BatchCreateRouteDto {
|
||||
@IsString() sourceGroupId!: string;
|
||||
@IsString({ each: true }) targetGroupIds!: string[];
|
||||
}
|
||||
|
||||
@Controller('routes')
|
||||
export class RoutesController {
|
||||
constructor(private readonly routesService: RoutesService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query('sourceGroupId') sourceGroupId?: string) {
|
||||
return this.routesService.list(sourceGroupId);
|
||||
list(
|
||||
@CurrentTenantContext() ctx: TenantContext,
|
||||
@Query('sourceGroupId') sourceGroupId?: string,
|
||||
) {
|
||||
return this.routesService.list(ctx.tenantId, sourceGroupId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: { sourceGroupId: string; targetGroupId: string }) {
|
||||
return this.routesService.create(body.sourceGroupId, body.targetGroupId);
|
||||
create(@CurrentTenantContext() ctx: TenantContext, @Body() body: CreateRouteDto) {
|
||||
return this.routesService.create(ctx.tenantId, body.sourceGroupId, body.targetGroupId);
|
||||
}
|
||||
|
||||
@Post('batch')
|
||||
createBatch(@CurrentTenantContext() ctx: TenantContext, @Body() body: BatchCreateRouteDto) {
|
||||
return this.routesService.createBatch(ctx.tenantId, body.sourceGroupId, body.targetGroupIds);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
remove(@Param('id') id: string) {
|
||||
return this.routesService.remove(id);
|
||||
async remove(@CurrentTenantContext() ctx: TenantContext, @Param('id') id: string) {
|
||||
await this.routesService.remove(ctx.tenantId, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BadRequestException, ConflictException, NotFoundException } from '@nest
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { RoutesService } from './routes.service';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
|
||||
const mockRoute = {
|
||||
id: 'rt_1',
|
||||
@@ -14,15 +15,32 @@ const mockRoute = {
|
||||
targetGroup: { name: 'Beta' },
|
||||
};
|
||||
|
||||
const mockRoute2 = {
|
||||
id: 'rt_2',
|
||||
sourceGroupId: 'grp_1',
|
||||
targetGroupId: 'grp_3',
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
sourceGroup: { name: 'Alpha' },
|
||||
targetGroup: { name: 'Gamma' },
|
||||
};
|
||||
|
||||
describe('RoutesService', () => {
|
||||
let service: RoutesService;
|
||||
const mockPrisma = {
|
||||
const mockPrisma: any = {
|
||||
$transaction: jest.fn((ops: any[]) => Promise.all(ops)),
|
||||
syncRoute: {
|
||||
findMany: jest.fn().mockResolvedValue([mockRoute]),
|
||||
findFirst: jest.fn().mockResolvedValue(mockRoute),
|
||||
create: jest.fn().mockResolvedValue(mockRoute),
|
||||
delete: jest.fn().mockResolvedValue(mockRoute),
|
||||
},
|
||||
group: {
|
||||
findFirst: jest.fn().mockResolvedValue({ id: 'g' }),
|
||||
findMany: jest.fn().mockResolvedValue([{ id: 'grp_2', name: 'Beta' }, { id: 'grp_3', name: 'Gamma' }]),
|
||||
},
|
||||
};
|
||||
const mockAudit: any = { log: jest.fn().mockResolvedValue(undefined) };
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
@@ -30,53 +48,51 @@ describe('RoutesService', () => {
|
||||
providers: [
|
||||
RoutesService,
|
||||
{ provide: PrismaService, useValue: mockPrisma },
|
||||
{ provide: AuditService, useValue: mockAudit },
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<RoutesService>(RoutesService);
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('returns all routes with group names', async () => {
|
||||
const result = await service.list();
|
||||
it('returns routes for tenant', async () => {
|
||||
const result = await service.list('tnt-1');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].sourceGroup.name).toBe('Alpha');
|
||||
expect(mockPrisma.syncRoute.findMany).toHaveBeenCalledWith({
|
||||
where: undefined,
|
||||
include: {
|
||||
sourceGroup: { select: { name: true } },
|
||||
targetGroup: { select: { name: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
expect(mockPrisma.syncRoute.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { tenantId: 'tnt-1' } }),
|
||||
);
|
||||
});
|
||||
|
||||
it('filters by sourceGroupId when provided', async () => {
|
||||
await service.list('grp_1');
|
||||
await service.list('tnt-1', 'grp_1');
|
||||
expect(mockPrisma.syncRoute.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { sourceGroupId: 'grp_1' } }),
|
||||
expect.objectContaining({ where: { tenantId: 'tnt-1', sourceGroupId: 'grp_1' } }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('creates a route and returns it with group names', async () => {
|
||||
const result = await service.create('grp_1', 'grp_2');
|
||||
it('creates a route within the tenant and writes audit', async () => {
|
||||
const result = await service.create('tnt-1', 'grp_1', 'grp_2');
|
||||
expect(result).toEqual(mockRoute);
|
||||
expect(mockPrisma.syncRoute.create).toHaveBeenCalledWith({
|
||||
data: { sourceGroupId: 'grp_1', targetGroupId: 'grp_2' },
|
||||
data: { tenantId: 'tnt-1', sourceGroupId: 'grp_1', targetGroupId: 'grp_2' },
|
||||
include: {
|
||||
sourceGroup: { select: { name: true } },
|
||||
targetGroup: { select: { name: true } },
|
||||
},
|
||||
});
|
||||
expect(mockAudit.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: 'ROUTE_CREATED', resourceId: 'rt_1' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when sourceGroupId is empty', async () => {
|
||||
await expect(service.create('', 'grp_2')).rejects.toThrow(BadRequestException);
|
||||
await expect(service.create('tnt-1', '', 'grp_2')).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when targetGroupId is empty', async () => {
|
||||
await expect(service.create('grp_1', '')).rejects.toThrow(BadRequestException);
|
||||
await expect(service.create('tnt-1', 'grp_1', '')).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws ConflictException when route already exists (Prisma P2002)', async () => {
|
||||
@@ -85,36 +101,87 @@ describe('RoutesService', () => {
|
||||
clientVersion: '6.0.0',
|
||||
});
|
||||
mockPrisma.syncRoute.create.mockRejectedValueOnce(p2002);
|
||||
await expect(service.create('grp_1', 'grp_2')).rejects.toThrow(ConflictException);
|
||||
await expect(service.create('tnt-1', 'grp_1', 'grp_2')).rejects.toThrow(ConflictException);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when source and target are the same group', async () => {
|
||||
await expect(service.create('grp_1', 'grp_1')).rejects.toThrow(BadRequestException);
|
||||
await expect(service.create('tnt-1', 'grp_1', 'grp_1')).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when a group ID does not exist (Prisma P2003)', async () => {
|
||||
const p2003 = new Prisma.PrismaClientKnownRequestError('Foreign key constraint', {
|
||||
code: 'P2003',
|
||||
clientVersion: '6.0.0',
|
||||
});
|
||||
mockPrisma.syncRoute.create.mockRejectedValueOnce(p2003);
|
||||
await expect(service.create('grp_1', 'bad_grp')).rejects.toThrow(BadRequestException);
|
||||
it('throws BadRequestException when a group is not in this tenant', async () => {
|
||||
mockPrisma.group.findFirst.mockResolvedValueOnce(null);
|
||||
await expect(service.create('tnt-1', 'grp_1', 'grp_x')).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBatch', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockPrisma.syncRoute.create
|
||||
.mockResolvedValueOnce(mockRoute)
|
||||
.mockResolvedValueOnce(mockRoute2);
|
||||
mockPrisma.syncRoute.findMany.mockResolvedValue([]);
|
||||
mockPrisma.group.findMany.mockResolvedValue([
|
||||
{ id: 'grp_2', name: 'Beta' },
|
||||
{ id: 'grp_3', name: 'Gamma' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates multiple routes in batch', async () => {
|
||||
const result = await service.createBatch('tnt-1', 'grp_1', ['grp_2', 'grp_3']);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockPrisma.$transaction).toHaveBeenCalled();
|
||||
expect(mockAudit.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'ROUTE_CREATED',
|
||||
payload: expect.objectContaining({ count: 2 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when targetGroupIds is empty', async () => {
|
||||
await expect(service.createBatch('tnt-1', 'grp_1', [])).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when source is also a target', async () => {
|
||||
await expect(service.createBatch('tnt-1', 'grp_1', ['grp_1'])).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when a target group is not in this tenant', async () => {
|
||||
mockPrisma.group.findMany.mockResolvedValueOnce([{ id: 'grp_2', name: 'Beta' }]);
|
||||
await expect(service.createBatch('tnt-1', 'grp_1', ['grp_2', 'grp_x'])).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws ConflictException when any route already exists', async () => {
|
||||
mockPrisma.syncRoute.findMany.mockResolvedValue([{ targetGroupId: 'grp_2' }]);
|
||||
await expect(service.createBatch('tnt-1', 'grp_1', ['grp_2', 'grp_3'])).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('deletes a route by id', async () => {
|
||||
await service.remove('rt_1');
|
||||
it('deletes a route and writes audit', async () => {
|
||||
await service.remove('tnt-1', 'rt_1');
|
||||
expect(mockPrisma.syncRoute.findFirst).toHaveBeenCalledWith({
|
||||
where: { id: 'rt_1', tenantId: 'tnt-1' },
|
||||
});
|
||||
expect(mockPrisma.syncRoute.delete).toHaveBeenCalledWith({ where: { id: 'rt_1' } });
|
||||
expect(mockAudit.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: 'ROUTE_DELETED', resourceId: 'rt_1' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotFoundException when route does not exist (Prisma P2025)', async () => {
|
||||
it('throws NotFoundException when route is not in this tenant', async () => {
|
||||
mockPrisma.syncRoute.findFirst.mockResolvedValueOnce(null);
|
||||
await expect(service.remove('tnt-1', 'bad_id')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('throws NotFoundException on Prisma P2025', async () => {
|
||||
const p2025 = new Prisma.PrismaClientKnownRequestError('Record not found', {
|
||||
code: 'P2025',
|
||||
clientVersion: '6.0.0',
|
||||
});
|
||||
mockPrisma.syncRoute.delete.mockRejectedValueOnce(p2025);
|
||||
await expect(service.remove('bad_id')).rejects.toThrow(NotFoundException);
|
||||
await expect(service.remove('tnt-1', 'rt_1')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,52 +1,174 @@
|
||||
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { AuditAction } from '../audit/audit.types';
|
||||
|
||||
export interface BatchCreateResult {
|
||||
created: Array<{ id: string; sourceGroupId: string; targetGroupId: string; sourceGroup: { name: string }; targetGroup: { name: string } }>;
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
const routeInclude = {
|
||||
sourceGroup: { select: { name: true } },
|
||||
targetGroup: { select: { name: true } },
|
||||
sourceGroup: { select: { name: true, tenantId: true } },
|
||||
targetGroup: { select: { name: true, tenantId: true } },
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export class RoutesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
list(sourceGroupId?: string) {
|
||||
list(tenantId: string, sourceGroupId?: string) {
|
||||
return this.prisma.syncRoute.findMany({
|
||||
where: sourceGroupId ? { sourceGroupId } : undefined,
|
||||
include: routeInclude,
|
||||
where: {
|
||||
tenantId,
|
||||
...(sourceGroupId ? { sourceGroupId } : {}),
|
||||
},
|
||||
include: {
|
||||
sourceGroup: { select: { name: true } },
|
||||
targetGroup: { select: { name: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(sourceGroupId: string, targetGroupId: string) {
|
||||
async create(tenantId: string, sourceGroupId: string, targetGroupId: string) {
|
||||
if (!sourceGroupId || !targetGroupId) {
|
||||
throw new BadRequestException('sourceGroupId and targetGroupId are required');
|
||||
}
|
||||
if (sourceGroupId === targetGroupId) {
|
||||
throw new BadRequestException('Source and target groups cannot be the same');
|
||||
}
|
||||
|
||||
// Source must be owned by this tenant AND active; target can be owned OR shared AND active
|
||||
const [source, target] = await Promise.all([
|
||||
this.prisma.group.findFirst({ where: { id: sourceGroupId, tenantId, isActive: true }, select: { id: true } }),
|
||||
this.prisma.group.findFirst({
|
||||
where: {
|
||||
id: targetGroupId,
|
||||
isActive: true,
|
||||
OR: [
|
||||
{ tenantId },
|
||||
{ groupAccesses: { some: { tenantId } } },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
}),
|
||||
]);
|
||||
if (!source) throw new BadRequestException('Source group is not available or the bot has been removed from it');
|
||||
if (!target) throw new BadRequestException('Target group is not available or the bot has been removed from it');
|
||||
|
||||
try {
|
||||
return await this.prisma.syncRoute.create({
|
||||
data: { sourceGroupId, targetGroupId },
|
||||
include: routeInclude,
|
||||
const route = await this.prisma.syncRoute.create({
|
||||
data: { tenantId, sourceGroupId, targetGroupId },
|
||||
include: {
|
||||
sourceGroup: { select: { name: true } },
|
||||
targetGroup: { select: { name: true } },
|
||||
},
|
||||
});
|
||||
await this.audit.log({
|
||||
tenantId,
|
||||
action: AuditAction.ROUTE_CREATED,
|
||||
resourceType: 'SyncRoute',
|
||||
resourceId: route.id,
|
||||
payload: { sourceGroupId, targetGroupId },
|
||||
});
|
||||
return route;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (e.code === 'P2002') {
|
||||
throw new ConflictException('A route between these groups already exists');
|
||||
}
|
||||
if (e.code === 'P2003') {
|
||||
throw new BadRequestException('One or both group IDs do not exist');
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
async createBatch(tenantId: string, sourceGroupId: string, targetGroupIds: string[]) {
|
||||
if (!sourceGroupId || !targetGroupIds.length) {
|
||||
throw new BadRequestException('sourceGroupId and at least one targetGroupId are required');
|
||||
}
|
||||
|
||||
if (targetGroupIds.includes(sourceGroupId)) {
|
||||
throw new BadRequestException('Source and target groups cannot be the same');
|
||||
}
|
||||
|
||||
// Source group must exist in this tenant AND be active
|
||||
const source = await this.prisma.group.findFirst({ where: { id: sourceGroupId, tenantId, isActive: true }, select: { id: true } });
|
||||
if (!source) {
|
||||
throw new BadRequestException('Source group is not available or the bot has been removed from it');
|
||||
}
|
||||
|
||||
// All target groups must be owned OR shared AND active
|
||||
const targets = await this.prisma.group.findMany({
|
||||
where: {
|
||||
id: { in: targetGroupIds },
|
||||
isActive: true,
|
||||
OR: [
|
||||
{ tenantId },
|
||||
{ groupAccesses: { some: { tenantId } } },
|
||||
],
|
||||
},
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
if (targets.length !== targetGroupIds.length) {
|
||||
const found = new Set(targets.map((t) => t.id));
|
||||
const missing = targetGroupIds.filter((id) => !found.has(id));
|
||||
throw new BadRequestException(`Target groups not found or not shared: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
// Check for existing conflicts — reject the whole batch
|
||||
const existing = await this.prisma.syncRoute.findMany({
|
||||
where: { tenantId, sourceGroupId, targetGroupId: { in: targetGroupIds } },
|
||||
select: { targetGroupId: true },
|
||||
});
|
||||
if (existing.length > 0) {
|
||||
const names = existing.map((e) => {
|
||||
const t = targets.find((t) => t.id === e.targetGroupId);
|
||||
return t?.name ?? e.targetGroupId;
|
||||
});
|
||||
throw new ConflictException(`Routes already exist for: ${names.join(', ')}`);
|
||||
}
|
||||
|
||||
const created = await this.prisma.$transaction(
|
||||
targetGroupIds.map((targetGroupId) =>
|
||||
this.prisma.syncRoute.create({
|
||||
data: { tenantId, sourceGroupId, targetGroupId },
|
||||
include: {
|
||||
sourceGroup: { select: { name: true } },
|
||||
targetGroup: { select: { name: true } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await this.audit.log({
|
||||
tenantId,
|
||||
action: AuditAction.ROUTE_CREATED,
|
||||
resourceType: 'SyncRoute',
|
||||
resourceId: created.map((r) => r.id).join(','),
|
||||
payload: { sourceGroupId, targetGroupIds, count: created.length },
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
async remove(tenantId: string, id: string) {
|
||||
// Verify ownership before delete
|
||||
const existing = await this.prisma.syncRoute.findFirst({ where: { id, tenantId } });
|
||||
if (!existing) throw new NotFoundException(`Route ${id} not found`);
|
||||
|
||||
try {
|
||||
await this.prisma.syncRoute.delete({ where: { id } });
|
||||
await this.audit.log({
|
||||
tenantId,
|
||||
action: AuditAction.ROUTE_DELETED,
|
||||
resourceType: 'SyncRoute',
|
||||
resourceId: id,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') {
|
||||
throw new NotFoundException(`Route ${id} not found`);
|
||||
|
||||
Reference in New Issue
Block a user