feat(24-01): add search, bookmark, branch, and export Express route handlers
- GET /companies/:companyId/messages/search: FTS search with ZodError 400 guard - POST /conversations/:id/bookmarks: toggle bookmark with UUID validation - GET /companies/:companyId/bookmarks: list bookmarks with optional conversationId filter - POST /conversations/:id/branch: branch conversation from message point - GET /conversations/:id/branches: list child conversations - GET /conversations/:id/export: download Markdown or JSON with Content-Disposition header
This commit is contained in:
parent
430588a54e
commit
2af9de913d
1 changed files with 68 additions and 0 deletions
|
|
@ -3,11 +3,14 @@ import type { Db } from "@paperclipai/db";
|
||||||
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
||||||
import { chatService } from "../services/chat.js";
|
import { chatService } from "../services/chat.js";
|
||||||
import { issueService } from "../services/issues.js";
|
import { issueService } from "../services/issues.js";
|
||||||
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
createConversationSchema,
|
createConversationSchema,
|
||||||
updateConversationSchema,
|
updateConversationSchema,
|
||||||
createMessageSchema,
|
createMessageSchema,
|
||||||
handoffSchema,
|
handoffSchema,
|
||||||
|
searchMessagesSchema,
|
||||||
|
branchConversationSchema,
|
||||||
} from "@paperclipai/shared";
|
} from "@paperclipai/shared";
|
||||||
|
|
||||||
export function chatRoutes(db: Db): Router {
|
export function chatRoutes(db: Db): Router {
|
||||||
|
|
@ -207,5 +210,70 @@ export function chatRoutes(db: Db): Router {
|
||||||
res.status(201).json(message);
|
res.status(201).json(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/companies/:companyId/messages/search
|
||||||
|
router.get("/companies/:companyId/messages/search", async (req, res) => {
|
||||||
|
assertBoard(req);
|
||||||
|
assertCompanyAccess(req, req.params.companyId!);
|
||||||
|
let parsed: { q: string; limit?: number };
|
||||||
|
try {
|
||||||
|
parsed = searchMessagesSchema.parse({ q: req.query.q, limit: req.query.limit });
|
||||||
|
} catch {
|
||||||
|
res.status(400).json({ error: "Query must be at least 2 characters" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await svc.searchMessages(req.params.companyId!, parsed.q, { limit: parsed.limit });
|
||||||
|
res.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/conversations/:id/bookmarks -- Toggle bookmark for a message
|
||||||
|
router.post("/conversations/:id/bookmarks", async (req, res) => {
|
||||||
|
assertBoard(req);
|
||||||
|
const messageId = z.string().uuid().safeParse(req.body.messageId);
|
||||||
|
if (!messageId.success) {
|
||||||
|
res.status(400).json({ error: "messageId must be a valid UUID" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conversationId = req.params.id!;
|
||||||
|
const conversation = await svc.getConversation(conversationId);
|
||||||
|
const result = await svc.toggleBookmark(conversation.companyId, messageId.data, conversationId);
|
||||||
|
res.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/companies/:companyId/bookmarks
|
||||||
|
router.get("/companies/:companyId/bookmarks", async (req, res) => {
|
||||||
|
assertBoard(req);
|
||||||
|
assertCompanyAccess(req, req.params.companyId!);
|
||||||
|
const conversationId = req.query.conversationId as string | undefined;
|
||||||
|
const result = await svc.getBookmarks(req.params.companyId!, { conversationId });
|
||||||
|
res.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/conversations/:id/branch -- Branch conversation from a message
|
||||||
|
router.post("/conversations/:id/branch", async (req, res) => {
|
||||||
|
assertBoard(req);
|
||||||
|
const parsed = branchConversationSchema.parse(req.body);
|
||||||
|
const conv = await svc.getConversation(req.params.id!);
|
||||||
|
const newConversation = await svc.branchConversation(req.params.id!, parsed.branchFromMessageId, conv.companyId);
|
||||||
|
res.status(201).json(newConversation);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/conversations/:id/branches -- List child conversations
|
||||||
|
router.get("/conversations/:id/branches", async (req, res) => {
|
||||||
|
assertBoard(req);
|
||||||
|
const branches = await svc.listBranches(req.params.id!);
|
||||||
|
res.json({ items: branches });
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/conversations/:id/export -- Export conversation as Markdown or JSON file
|
||||||
|
router.get("/conversations/:id/export", async (req, res) => {
|
||||||
|
assertBoard(req);
|
||||||
|
const format = req.query.format === "json" ? "json" : "markdown";
|
||||||
|
const { content, filename } = await svc.exportConversation(req.params.id!, format);
|
||||||
|
const mime = format === "json" ? "application/json" : "text/markdown";
|
||||||
|
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||||
|
res.setHeader("Content-Type", mime);
|
||||||
|
res.send(content);
|
||||||
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue