feat: route-centric rules, approve popup, emoji picker, 5-column sync table
- Schema: SyncRouteRuleMap junction table, ruleIntent on TenantRule, ForwardFormat/Frequency/ApprovalMode columns on SyncRoute, INTEREST match type - Worker: remove global AUTO_APPROVE forwarding; per-route rule evaluation in ingest processor — POSITIVE rules auto-forward that route, NEGATIVE rules block it, fallback to approvalMode - API: PATCH /routes/:id, rule assign/unassign endpoints, approve now accepts targetGroupIds, new GET /messages/:id/routes for popup data - Web: 5-column route table with settings + rules dialogs, ForwardTarget popup with rule-based pre-selection, ruleIntent toggle + emoji dropdown (👍/👎 only) + INTEREST type in rules manager Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,14 @@ export interface BatchCreateResult {
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
export interface UpdateRouteInput {
|
||||
forwardFormat?: 'THREADED' | 'DIGEST' | 'SUMMARY';
|
||||
withAttachment?: boolean;
|
||||
frequency?: 'INSTANT' | 'FIVE_MIN' | 'FIFTEEN_MIN' | 'ONE_HOUR' | 'ONE_DAY' | 'SEVEN_DAYS';
|
||||
approvalMode?: 'MANUAL' | 'AUTO';
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
const routeInclude = {
|
||||
sourceGroup: { select: { name: true, tenantId: true } },
|
||||
targetGroup: { select: { name: true, tenantId: true } },
|
||||
@@ -30,11 +38,70 @@ export class RoutesService {
|
||||
include: {
|
||||
sourceGroup: { select: { name: true } },
|
||||
targetGroup: { select: { name: true } },
|
||||
assignedRules: {
|
||||
include: {
|
||||
tenantRule: {
|
||||
select: { id: true, matchType: true, matchValue: true, action: true, ruleIntent: true, isActive: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async update(tenantId: string, id: string, dto: UpdateRouteInput) {
|
||||
const existing = await this.prisma.syncRoute.findFirst({ where: { id, tenantId } });
|
||||
if (!existing) throw new NotFoundException(`Route ${id} not found`);
|
||||
return this.prisma.syncRoute.update({ where: { id }, data: dto as any });
|
||||
}
|
||||
|
||||
async assignRule(tenantId: string, routeId: string, tenantRuleId: string) {
|
||||
const route = await this.prisma.syncRoute.findFirst({ where: { id: routeId, tenantId } });
|
||||
if (!route) throw new NotFoundException(`Route ${routeId} not found`);
|
||||
const rule = await this.prisma.tenantRule.findFirst({ where: { id: tenantRuleId, tenantId } });
|
||||
if (!rule) throw new NotFoundException(`Rule ${tenantRuleId} not found`);
|
||||
|
||||
try {
|
||||
return await this.prisma.syncRouteRuleMap.create({
|
||||
data: { id: `${routeId}_${tenantRuleId}`.slice(0, 25) + Math.random().toString(36).slice(2, 8), syncRouteId: routeId, tenantRuleId },
|
||||
include: { tenantRule: { select: { id: true, matchType: true, matchValue: true, action: true, ruleIntent: true, isActive: true } } },
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new ConflictException('Rule is already assigned to this route');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async unassignRule(tenantId: string, routeId: string, tenantRuleId: string) {
|
||||
const route = await this.prisma.syncRoute.findFirst({ where: { id: routeId, tenantId } });
|
||||
if (!route) throw new NotFoundException(`Route ${routeId} not found`);
|
||||
const map = await this.prisma.syncRouteRuleMap.findUnique({
|
||||
where: { syncRouteId_tenantRuleId: { syncRouteId: routeId, tenantRuleId } },
|
||||
});
|
||||
if (!map) throw new NotFoundException('Assignment not found');
|
||||
await this.prisma.syncRouteRuleMap.delete({
|
||||
where: { syncRouteId_tenantRuleId: { syncRouteId: routeId, tenantRuleId } },
|
||||
});
|
||||
}
|
||||
|
||||
async listAssignedRules(tenantId: string, routeId: string) {
|
||||
const route = await this.prisma.syncRoute.findFirst({ where: { id: routeId, tenantId } });
|
||||
if (!route) throw new NotFoundException(`Route ${routeId} not found`);
|
||||
const maps = await this.prisma.syncRouteRuleMap.findMany({
|
||||
where: { syncRouteId: routeId },
|
||||
include: {
|
||||
tenantRule: {
|
||||
select: { id: true, matchType: true, matchValue: true, action: true, ruleIntent: true, priority: true, isActive: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
return maps.map((m: any) => ({ ...m.tenantRule, assignedAt: m.createdAt }));
|
||||
}
|
||||
|
||||
async create(tenantId: string, sourceGroupId: string, targetGroupId: string) {
|
||||
if (!sourceGroupId || !targetGroupId) {
|
||||
throw new BadRequestException('sourceGroupId and targetGroupId are required');
|
||||
|
||||
Reference in New Issue
Block a user