import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { asc, eq } from 'drizzle-orm'; import { nanoid } from 'nanoid'; import { z } from 'zod'; import { groupInputSchema, type GroupDto } from '@proxy-station/shared'; import { db } from '../db/index.js'; import { groupMembers, policyGroups, rules, nodes } from '../db/schema.js'; import { groupError } from '../services/group-validation.js'; function validate(input: z.output, id?: string): string | null { return groupError(input, loadGroups(), db.select({ id: nodes.id, name: nodes.name }).from(nodes).all(), id); } function loadGroups(): GroupDto[] { const groups = db.select().from(policyGroups).orderBy(asc(policyGroups.sortOrder)).all(); const members = db.select().from(groupMembers).orderBy(asc(groupMembers.sortOrder)).all(); return groups.map((g) => ({ ...g, members: members .filter((m) => m.groupId === g.id) .map((m) => ({ id: m.id, kind: m.memberKind, nodeId: m.nodeId, refName: m.refName, sortOrder: m.sortOrder })), })); } function replaceMembers(groupId: string, members: z.output['members']) { db.delete(groupMembers).where(eq(groupMembers.groupId, groupId)).run(); members.forEach((m, i) => { db.insert(groupMembers) .values({ groupId, memberKind: m.kind, nodeId: m.kind === 'node' ? (m.nodeId ?? null) : null, refName: m.kind === 'node' ? null : (m.refName ?? null), sortOrder: i, }) .run(); }); } export const groupsRoute = new Hono() .get('/', (c) => c.json(loadGroups())) .post('/', zValidator('json', groupInputSchema), (c) => { const input = c.req.valid('json'); const error = validate(input); if (error) return c.json({ error }, error === '策略组名称已存在' ? 409 : 400); const id = nanoid(10); try { db.transaction(() => { db.insert(policyGroups) .values({ id, name: input.name, type: input.type, testUrl: input.testUrl ?? null, interval: input.interval ?? null, tolerance: input.tolerance ?? null, filterRegex: input.filterRegex || null, includeAllNodes: input.includeAllNodes, sortOrder: input.sortOrder, source: 'custom', }) .run(); replaceMembers(id, input.members); }); } catch (e: any) { if (String(e.message).includes('UNIQUE')) return c.json({ error: `策略组「${input.name}」已存在` }, 409); throw e; } return c.json({ id }, 201); }) .put('/reorder', zValidator('json', z.object({ ids: z.array(z.string()) })), (c) => { const { ids } = c.req.valid('json'); ids.forEach((id, i) => db.update(policyGroups).set({ sortOrder: i }).where(eq(policyGroups.id, id)).run()); return c.json({ ok: true }); }) .patch('/:id/enabled', zValidator('json', z.object({ enabled: z.boolean() })), (c) => { const res = db .update(policyGroups) .set({ enabled: c.req.valid('json').enabled }) .where(eq(policyGroups.id, c.req.param('id'))) .run(); if (res.changes === 0) return c.json({ error: '策略组不存在' }, 404); return c.json({ ok: true }); }) .put('/:id', zValidator('json', groupInputSchema), (c) => { const id = c.req.param('id'); const existing = db.select().from(policyGroups).where(eq(policyGroups.id, id)).get(); if (!existing) return c.json({ error: '策略组不存在' }, 404); const input = c.req.valid('json'); const error = validate(input, id); if (error) return c.json({ error }, error === '策略组名称已存在' ? 409 : 400); const oldName = existing.name; db.transaction(() => { db.update(policyGroups) .set({ name: input.name, type: input.type, testUrl: input.testUrl ?? null, interval: input.interval ?? null, tolerance: input.tolerance ?? null, filterRegex: input.filterRegex || null, includeAllNodes: input.includeAllNodes, }) .where(eq(policyGroups.id, id)) .run(); replaceMembers(id, input.members.map((m) => m.kind === 'group' && m.refName === oldName ? { ...m, refName: input.name } : m)); // 组名变更时同步引用它的成员与规则策略 if (oldName !== input.name) { db.update(groupMembers).set({ refName: input.name }).where(eq(groupMembers.refName, oldName)).run(); db.update(rules).set({ policy: input.name }).where(eq(rules.policy, oldName)).run(); } }); return c.json({ ok: true }); }) .delete('/:id', (c) => { const res = db.delete(policyGroups).where(eq(policyGroups.id, c.req.param('id'))).run(); if (res.changes === 0) return c.json({ error: '策略组不存在' }, 404); return c.json({ ok: true }); });