Files
tower/apps/api/src/modules/routes/routes.service.ts
T
2026-06-09 02:02:40 +05:30

180 lines
6.1 KiB
TypeScript

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, tenantId: true } },
targetGroup: { select: { name: true, tenantId: true } },
} as const;
@Injectable()
export class RoutesService {
constructor(
private readonly prisma: PrismaService,
private readonly audit: AuditService,
) {}
list(tenantId: string, sourceGroupId?: string) {
return this.prisma.syncRoute.findMany({
where: {
tenantId,
...(sourceGroupId ? { sourceGroupId } : {}),
},
include: {
sourceGroup: { select: { name: true } },
targetGroup: { select: { name: true } },
},
orderBy: { createdAt: 'desc' },
});
}
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 {
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');
}
}
throw e;
}
}
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`);
}
throw e;
}
}
}