feat: implement chat persistence with MongoDB

- Add full chat persistence to database (conversations and messages)
- Implement MongoChatRepository with cursor-based pagination
- Add getConversations/getConversation endpoints in ChatController
- Save user and assistant messages in ChatService.processMessage()
- Track respondedAction (confirm/reject) on proposed event messages
- Load existing messages on chat screen mount
- Add addMessages() bulk action and chatMessageToMessageData() helper to ChatStore
- Add RespondedAction type and UpdateMessageDTO to shared types
This commit is contained in:
2026-01-09 16:21:01 +01:00
parent d86b18173f
commit c897b6d680
11 changed files with 245 additions and 45 deletions

View File

@@ -3,6 +3,7 @@ import {
Conversation,
CreateMessageDTO,
GetMessagesOptions,
UpdateMessageDTO,
} from "@caldav/shared";
import { ChatRepository } from "../../services/interfaces";
import { ChatMessageModel, ConversationModel } from "./models";
@@ -10,11 +11,15 @@ import { ChatMessageModel, ConversationModel } from "./models";
export class MongoChatRepository implements ChatRepository {
// Conversations
async getConversationsByUser(userId: string): Promise<Conversation[]> {
throw new Error("Not implemented");
const conversations = await ConversationModel.find({ userId });
return conversations.map((c) => c.toJSON() as unknown as Conversation);
}
async createConversation(userId: string): Promise<Conversation> {
throw new Error("Not implemented");
const conversation = await ConversationModel.create({
userId,
});
return conversation.toJSON() as unknown as Conversation;
}
// Messages (cursor-based pagination)
@@ -22,13 +27,44 @@ export class MongoChatRepository implements ChatRepository {
conversationId: string,
options?: GetMessagesOptions,
): Promise<ChatMessage[]> {
throw new Error("Not implemented");
const limit = options?.limit ?? 20;
const query: Record<string, unknown> = { conversationId };
// Cursor: load messages before this ID (for "load more" scrolling up)
if (options?.before) {
query._id = { $lt: options.before };
}
// Fetch newest first, then reverse for chronological order
const docs = await ChatMessageModel.find(query)
.sort({ _id: -1 })
.limit(limit);
return docs.reverse().map((doc) => doc.toJSON() as unknown as ChatMessage);
}
async createMessage(
conversationId: string,
message: CreateMessageDTO,
): Promise<ChatMessage> {
throw new Error("Not implemented");
const repoMessage = await ChatMessageModel.create({
conversationId: conversationId,
sender: message.sender,
content: message.content,
proposedChange: message.proposedChange,
});
return repoMessage.toJSON() as unknown as ChatMessage;
}
async updateMessage(
messageId: string,
updates: UpdateMessageDTO,
): Promise<ChatMessage | null> {
const doc = await ChatMessageModel.findByIdAndUpdate(
messageId,
{ $set: updates },
{ new: true },
);
return doc ? (doc.toJSON() as unknown as ChatMessage) : null;
}
}

View File

@@ -80,6 +80,10 @@ const ChatMessageSchema = new Schema<
proposedChange: {
type: ProposedChangeSchema,
},
respondedAction: {
type: String,
enum: ["confirm", "reject"],
},
},
{
timestamps: true,