feat(22-01): DB schema, shared types, validators, service methods for streaming and editing
- Add editedContent and editedAt columns to chat_messages schema - Update ChatMessage type with editedContent/editedAt fields - Add editMessageSchema and streamMessageSchema validators - Add agentId to updateConversationSchema - Export new schemas from @paperclipai/shared - Add editMessage and getMessageHistory service methods - Update updateConversation to accept agentId field - Add PUT /conversations/:id/messages/:messageId route - Extend test coverage for agentId patching and message editing - Generate migration 0048 for editedContent/editedAt columns
This commit is contained in:
parent
e8c70d6c8d
commit
5db6fe7af7
10 changed files with 12240 additions and 4 deletions
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "chat_messages" ADD COLUMN "edited_content" text;--> statement-breakpoint
|
||||
ALTER TABLE "chat_messages" ADD COLUMN "edited_at" timestamp with time zone;
|
||||
12109
packages/db/src/migrations/meta/0048_snapshot.json
Normal file
12109
packages/db/src/migrations/meta/0048_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -337,6 +337,13 @@
|
|||
"when": 1775041195907,
|
||||
"tag": "0047_fixed_johnny_storm",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 48,
|
||||
"version": "7",
|
||||
"when": 1775047784481,
|
||||
"tag": "0048_flat_stepford_cuckoos",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ export const chatMessages = pgTable(
|
|||
role: text("role").notNull(),
|
||||
content: text("content").notNull(),
|
||||
agentId: uuid("agent_id"),
|
||||
editedContent: text("edited_content"),
|
||||
editedAt: timestamp("edited_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
|
|
|
|||
|
|
@ -608,4 +608,6 @@ export {
|
|||
createConversationSchema,
|
||||
updateConversationSchema,
|
||||
createMessageSchema,
|
||||
editMessageSchema,
|
||||
streamMessageSchema,
|
||||
} from "./validators/chat.js";
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export interface ChatMessage {
|
|||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
agentId: string | null;
|
||||
editedContent: string | null;
|
||||
editedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const createConversationSchema = z.object({
|
|||
|
||||
export const updateConversationSchema = z.object({
|
||||
title: z.string().max(200).optional(),
|
||||
agentId: z.string().uuid().optional().nullable(),
|
||||
});
|
||||
|
||||
export const createMessageSchema = z.object({
|
||||
|
|
@ -13,3 +14,12 @@ export const createMessageSchema = z.object({
|
|||
content: z.string().min(1),
|
||||
agentId: z.string().uuid().optional().nullable(),
|
||||
});
|
||||
|
||||
export const editMessageSchema = z.object({
|
||||
content: z.string().min(1),
|
||||
});
|
||||
|
||||
export const streamMessageSchema = z.object({
|
||||
content: z.string().min(1),
|
||||
agentId: z.string().uuid().optional().nullable(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ const mockChatService = vi.hoisted(() => ({
|
|||
unpinConversation: vi.fn(),
|
||||
listMessages: vi.fn(),
|
||||
addMessage: vi.fn(),
|
||||
editMessage: vi.fn(),
|
||||
getMessageHistory: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../services/chat.js", () => ({
|
||||
|
|
@ -204,6 +206,8 @@ describe("chat routes", () => {
|
|||
role: "user",
|
||||
content: "hello",
|
||||
agentId: null,
|
||||
editedContent: null,
|
||||
editedAt: null,
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
mockChatService.addMessage.mockResolvedValue(message);
|
||||
|
|
@ -216,4 +220,64 @@ describe("chat routes", () => {
|
|||
expect(res.body).toMatchObject({ id: "msg-1", content: "hello" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/conversations/:id with agentId", () => {
|
||||
it("returns 200 with agentId set", async () => {
|
||||
const agentId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
||||
const updated = {
|
||||
id: "conv-1",
|
||||
companyId: "company-1",
|
||||
title: "Test",
|
||||
agentId,
|
||||
pinnedAt: null,
|
||||
archivedAt: null,
|
||||
deletedAt: null,
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
mockChatService.updateConversation.mockResolvedValue(updated);
|
||||
|
||||
const res = await request(createApp())
|
||||
.patch("/api/conversations/conv-1")
|
||||
.send({ agentId });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({ agentId });
|
||||
expect(mockChatService.updateConversation).toHaveBeenCalledWith("conv-1", { agentId });
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/conversations/:id/messages/:messageId", () => {
|
||||
it("returns 200 with editedContent set", async () => {
|
||||
const editedMessage = {
|
||||
id: "msg-1",
|
||||
conversationId: "conv-1",
|
||||
role: "user",
|
||||
content: "original",
|
||||
agentId: null,
|
||||
editedContent: "edited",
|
||||
editedAt: "2024-01-01T00:00:00.000Z",
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
mockChatService.editMessage.mockResolvedValue(editedMessage);
|
||||
|
||||
const res = await request(createApp())
|
||||
.put("/api/conversations/conv-1/messages/msg-1")
|
||||
.send({ content: "edited" });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({ editedContent: "edited", editedAt: expect.any(String) });
|
||||
expect(mockChatService.editMessage).toHaveBeenCalledWith("msg-1", { content: "edited" });
|
||||
});
|
||||
|
||||
it("returns 404 when message not found", async () => {
|
||||
mockChatService.editMessage.mockResolvedValue(null);
|
||||
|
||||
const res = await request(createApp())
|
||||
.put("/api/conversations/conv-1/messages/nonexistent")
|
||||
.send({ content: "edited" });
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Db } from "@paperclipai/db";
|
|||
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
||||
import { chatService } from "../services/chat.js";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { createConversationSchema, updateConversationSchema, createMessageSchema } from "@paperclipai/shared";
|
||||
import { createConversationSchema, updateConversationSchema, createMessageSchema, editMessageSchema, streamMessageSchema } from "@paperclipai/shared";
|
||||
|
||||
export function chatRoutes(db: Db) {
|
||||
const router = Router();
|
||||
|
|
@ -97,5 +97,16 @@ export function chatRoutes(db: Db) {
|
|||
res.status(201).json(message);
|
||||
});
|
||||
|
||||
// PUT /api/conversations/:id/messages/:messageId
|
||||
router.put("/conversations/:id/messages/:messageId", validate(editMessageSchema), async (req, res) => {
|
||||
assertBoard(req);
|
||||
const message = await svc.editMessage(req.params.messageId as string, req.body);
|
||||
if (!message) {
|
||||
res.status(404).json({ error: "Not found" });
|
||||
return;
|
||||
}
|
||||
res.json(message);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { and, desc, eq, isNull, lt } from "drizzle-orm";
|
||||
import { and, asc, desc, eq, isNull, lt } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { chatConversations, chatMessages } from "@paperclipai/db";
|
||||
|
||||
|
|
@ -55,11 +55,12 @@ export function chatService(db: Db) {
|
|||
return row;
|
||||
},
|
||||
|
||||
async updateConversation(id: string, data: { title?: string }) {
|
||||
async updateConversation(id: string, data: { title?: string; agentId?: string | null }) {
|
||||
const [updated] = await db
|
||||
.update(chatConversations)
|
||||
.set({
|
||||
title: data.title,
|
||||
...(data.title !== undefined ? { title: data.title } : {}),
|
||||
...(data.agentId !== undefined ? { agentId: data.agentId } : {}),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(chatConversations.id, id))
|
||||
|
|
@ -141,6 +142,32 @@ export function chatService(db: Db) {
|
|||
return { items, hasMore };
|
||||
},
|
||||
|
||||
async editMessage(messageId: string, data: { content: string }) {
|
||||
const [updated] = await db
|
||||
.update(chatMessages)
|
||||
.set({
|
||||
editedContent: data.content,
|
||||
editedAt: new Date(),
|
||||
})
|
||||
.where(eq(chatMessages.id, messageId))
|
||||
.returning();
|
||||
|
||||
return updated ?? null;
|
||||
},
|
||||
|
||||
async getMessageHistory(conversationId: string) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(chatMessages)
|
||||
.where(eq(chatMessages.conversationId, conversationId))
|
||||
.orderBy(asc(chatMessages.createdAt));
|
||||
|
||||
return rows.map((m) => ({
|
||||
...m,
|
||||
effectiveContent: m.editedContent ?? m.content,
|
||||
}));
|
||||
},
|
||||
|
||||
async addMessage(conversationId: string, data: { role: string; content: string; agentId?: string | null }) {
|
||||
const [message] = await db
|
||||
.insert(chatMessages)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue