implement event persistence and improve Mongoose TypeScript patterns

- Add event persistence: confirmed events are now saved to MongoDB
- Refactor Mongoose models to use virtuals for id field with IdVirtual interface
- Update repositories to use toJSON() with consistent type casting
- Add more test responses for chat (doctor, birthday, gym, etc.)
- Show event description in ProposedEventCard
- Change mongo-express port to 8083
- Update CLAUDE.md with Mongoose model pattern documentation
This commit is contained in:
2026-01-04 11:52:05 +01:00
parent c33508a227
commit 9fecf94c7d
13 changed files with 240 additions and 48 deletions

View File

@@ -16,7 +16,9 @@ export class MongoEventRepository implements EventRepository {
}
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
throw new Error('Not implemented');
const event = await EventModel.create({ userId, ...data });
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
return event.toJSON() as unknown as CalendarEvent;
}
async update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null> {

View File

@@ -1,29 +1,21 @@
import { User } from '@caldav/shared';
import { UserRepository, CreateUserData } from '../../services/interfaces';
import { UserModel, UserDocument } from './models';
import { UserModel } from './models';
export class MongoUserRepository implements UserRepository {
private toUser(doc: UserDocument): User {
return {
id: doc._id.toString(),
email: doc.email,
displayName: doc.displayName,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
}
async findById(id: string): Promise<User | null> {
throw new Error('Not implemented');
}
async findByEmail(email: string): Promise<User | null> {
const user = await UserModel.findOne({ email: email.toLowerCase() });
return user ? this.toUser(user) : null;
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
return (user?.toJSON() as unknown as User) ?? null;
}
async create(data: CreateUserData): Promise<User> {
const user = await UserModel.create(data);
return this.toUser(user);
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
return user.toJSON() as unknown as User;
}
}

View File

@@ -1,8 +1,13 @@
import mongoose, { Schema, Document } from 'mongoose';
import mongoose, { Schema, Document, Model } from 'mongoose';
import { ChatMessage, Conversation, CreateEventDTO, UpdateEventDTO, ProposedEventChange } from '@caldav/shared';
import { IdVirtual } from './types';
export interface ChatMessageDocument extends Omit<ChatMessage, 'id'>, Document {}
export interface ConversationDocument extends Omit<Conversation, 'id'>, Document {}
export interface ChatMessageDocument extends Omit<ChatMessage, 'id'>, Document {
toJSON(): ChatMessage;
}
export interface ConversationDocument extends Omit<Conversation, 'id'>, Document {
toJSON(): Conversation;
}
const EventSchema = new Schema<CreateEventDTO>(
{
@@ -40,7 +45,7 @@ const ProposedChangeSchema = new Schema<ProposedEventChange>(
{ _id: false }
);
const ChatMessageSchema = new Schema<ChatMessageDocument>(
const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
{
conversationId: {
type: String,
@@ -61,9 +66,16 @@ const ChatMessageSchema = new Schema<ChatMessageDocument>(
},
{
timestamps: true,
virtuals: {
id: {
get() {
return this._id.toString();
},
},
},
toJSON: {
virtuals: true,
transform: (_, ret: Record<string, unknown>) => {
ret.id = String(ret._id);
delete ret._id;
delete ret.__v;
return ret;
@@ -72,7 +84,7 @@ const ChatMessageSchema = new Schema<ChatMessageDocument>(
}
);
const ConversationSchema = new Schema<ConversationDocument>(
const ConversationSchema = new Schema<ConversationDocument, Model<ConversationDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
{
userId: {
type: String,
@@ -82,9 +94,16 @@ const ConversationSchema = new Schema<ConversationDocument>(
},
{
timestamps: true,
virtuals: {
id: {
get() {
return this._id.toString();
},
},
},
toJSON: {
virtuals: true,
transform: (_, ret: Record<string, unknown>) => {
ret.id = String(ret._id);
delete ret._id;
delete ret.__v;
return ret;

View File

@@ -1,9 +1,12 @@
import mongoose, { Schema, Document } from 'mongoose';
import mongoose, { Schema, Document, Model } from 'mongoose';
import { CalendarEvent } from '@caldav/shared';
import { IdVirtual } from './types';
export interface EventDocument extends Omit<CalendarEvent, 'id'>, Document {}
export interface EventDocument extends Omit<CalendarEvent, 'id'>, Document {
toJSON(): CalendarEvent;
}
const EventSchema = new Schema<EventDocument>(
const EventSchema = new Schema<EventDocument, Model<EventDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
{
userId: {
type: String,
@@ -40,9 +43,16 @@ const EventSchema = new Schema<EventDocument>(
},
{
timestamps: true,
virtuals: {
id: {
get() {
return this._id.toString();
},
},
},
toJSON: {
virtuals: true,
transform: (_, ret: Record<string, unknown>) => {
ret.id = String(ret._id);
delete ret._id;
delete ret.__v;
return ret;

View File

@@ -1,9 +1,12 @@
import mongoose, { Schema, Document } from 'mongoose';
import mongoose, { Schema, Document, Model } from 'mongoose';
import { User } from '@caldav/shared';
import { IdVirtual } from './types';
export interface UserDocument extends Omit<User, 'id'>, Document {}
export interface UserDocument extends Omit<User, 'id'>, Document {
toJSON(): User;
}
const UserSchema = new Schema<UserDocument>(
const UserSchema = new Schema<UserDocument, Model<UserDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
{
email: {
type: String,
@@ -24,9 +27,16 @@ const UserSchema = new Schema<UserDocument>(
},
{
timestamps: true,
virtuals: {
id: {
get() {
return this._id.toString();
},
},
},
toJSON: {
virtuals: true,
transform: (_, ret: Record<string, unknown>) => {
ret.id = String(ret._id);
delete ret._id;
delete ret.__v;
delete ret.passwordHash;

View File

@@ -0,0 +1,4 @@
// Common virtual interface for all Mongoose models
export interface IdVirtual {
id: string;
}