good forst commit
This commit is contained in:
@@ -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