const couchdbService = require("../services/couchdbService"); const { ValidationError, NotFoundError, DatabaseError, withErrorHandling, createErrorContext, logModelError } = require("../utils/modelErrors"); class Event { constructor(data) { // Handle both new documents and database documents const isNew = !data._id; // For new documents, validate required fields if (isNew) { if (!data.title || data.title.trim() === '') { throw new ValidationError('Title is required', 'title', data.title); } if (!data.description || data.description.trim() === '') { throw new ValidationError('Description is required', 'description', data.description); } if (!data.date) { throw new ValidationError('Date is required', 'date', data.date); } if (!data.location) { throw new ValidationError('Location is required', 'location', data.location); } // Validate status const validStatuses = ["upcoming", "ongoing", "completed", "cancelled"]; if (data.status && !validStatuses.includes(data.status)) { throw new ValidationError('Invalid status', 'status', data.status); } } this._id = data._id || null; this._rev = data._rev || null; this.type = data.type || "event"; this.title = data.title; this.description = data.description; this.date = data.date; this.location = data.location; this.organizer = data.organizer || null; this.participants = data.participants || []; this.participantsCount = data.participantsCount || 0; this.status = data.status || "upcoming"; this.createdAt = data.createdAt || new Date().toISOString(); this.updatedAt = data.updatedAt || new Date().toISOString(); } static async create(eventData) { const errorContext = createErrorContext('Event', 'create', { title: eventData?.title, date: eventData?.date, location: eventData?.location }); return await withErrorHandling(async () => { const event = new Event(eventData); event._id = `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const createdEvent = await couchdbService.createDocument(event.toJSON()); return new Event(createdEvent); }, errorContext); } static async findById(eventId) { const errorContext = createErrorContext('Event', 'findById', { eventId }); return await withErrorHandling(async () => { const doc = await couchdbService.getById(eventId); if (doc && doc.type === "event") { return new Event(doc); } return null; }, errorContext); } static async find(query = {}, options = {}) { const errorContext = createErrorContext('Event', 'find', { query, options }); return await withErrorHandling(async () => { const defaultQuery = { type: "event", ...query }; return await couchdbService.find({ selector: defaultQuery, ...options }); }, errorContext); } static async findOne(query) { const errorContext = createErrorContext('Event', 'findOne', { query }); return await withErrorHandling(async () => { const events = await this.find(query, { limit: 1 }); return events[0] || null; }, errorContext); } static async update(eventId, updateData) { const errorContext = createErrorContext('Event', 'update', { eventId, updateData }); return await withErrorHandling(async () => { const event = await this.findById(eventId); if (!event) { throw new NotFoundError('Event', eventId); } const updatedEvent = { ...event.toJSON(), ...updateData, updatedAt: new Date().toISOString() }; const result = await couchdbService.update(eventId, updatedEvent); return new Event(result); }, errorContext); } static async delete(eventId) { const errorContext = createErrorContext('Event', 'delete', { eventId }); return await withErrorHandling(async () => { const event = await this.findById(eventId); if (!event) { throw new NotFoundError('Event', eventId); } return await couchdbService.delete(eventId); }, errorContext); } static async addParticipant(eventId, userId, userName, userProfilePicture) { const errorContext = createErrorContext('Event', 'addParticipant', { eventId, userId }); return await withErrorHandling(async () => { const event = await this.findById(eventId); if (!event) { throw new NotFoundError('Event', eventId); } // Check if user is already a participant const existingParticipant = event.participants.find(p => p.userId === userId); if (existingParticipant) { throw new ValidationError('User already participating in this event', 'userId', userId); } // Add participant with embedded user data const newParticipant = { userId: userId, name: userName, profilePicture: userProfilePicture || "", joinedAt: new Date().toISOString() }; event.participants.push(newParticipant); event.participantsCount = event.participants.length; event.updatedAt = new Date().toISOString(); const result = await couchdbService.update(eventId, event.toJSON()); return new Event(result); }, errorContext); } static async removeParticipant(eventId, userId) { const errorContext = createErrorContext('Event', 'removeParticipant', { eventId, userId }); return await withErrorHandling(async () => { const event = await this.findById(eventId); if (!event) { throw new NotFoundError('Event', eventId); } // Remove participant event.participants = event.participants.filter(p => p.userId !== userId); event.participantsCount = event.participants.length; event.updatedAt = new Date().toISOString(); const result = await couchdbService.update(eventId, event.toJSON()); return new Event(result); }, errorContext); } static async updateStatus(eventId, newStatus) { const errorContext = createErrorContext('Event', 'updateStatus', { eventId, newStatus }); return await withErrorHandling(async () => { const validStatuses = ["upcoming", "ongoing", "completed", "cancelled"]; if (!validStatuses.includes(newStatus)) { throw new ValidationError('Invalid status', 'status', newStatus); } return await this.update(eventId, { status: newStatus }); }, errorContext); } static async findByStatus(status) { const errorContext = createErrorContext('Event', 'findByStatus', { status }); return await withErrorHandling(async () => { return await this.find({ status }); }, errorContext); } static async findByDateRange(startDate, endDate) { const errorContext = createErrorContext('Event', 'findByDateRange', { startDate, endDate }); return await withErrorHandling(async () => { return await couchdbService.find({ selector: { type: "event", date: { $gte: startDate.toISOString(), $lte: endDate.toISOString() } }, sort: [{ date: "asc" }] }); }, errorContext); } static async findByParticipant(userId) { const errorContext = createErrorContext('Event', 'findByParticipant', { userId }); return await withErrorHandling(async () => { return await couchdbService.view("events", "by-participant", { key: userId, include_docs: true }); }, errorContext); } static async getUpcomingEvents(limit = 10) { const errorContext = createErrorContext('Event', 'getUpcomingEvents', { limit }); return await withErrorHandling(async () => { const now = new Date().toISOString(); return await couchdbService.find({ selector: { type: "event", status: "upcoming", date: { $gte: now } }, sort: [{ date: "asc" }], limit }); }, errorContext); } static async getAllPaginated(page = 1, limit = 10) { const errorContext = createErrorContext('Event', 'getAllPaginated', { page, limit }); return await withErrorHandling(async () => { const skip = (page - 1) * limit; const events = await couchdbService.find({ selector: { type: "event" }, sort: [{ date: "desc" }], skip, limit }); // Get total count const totalCount = await couchdbService.find({ selector: { type: "event" }, fields: ["_id"] }); return { events, pagination: { page, limit, totalCount: totalCount.length, totalPages: Math.ceil(totalCount.length / limit) } }; }, errorContext); } static async getEventsByUser(userId) { const errorContext = createErrorContext('Event', 'getEventsByUser', { userId }); return await withErrorHandling(async () => { return await this.find({ "participants": { $elemMatch: { userId: userId } } }); }, errorContext); } // Convert to CouchDB document format toJSON() { return { _id: this._id, _rev: this._rev, type: this.type, title: this.title, description: this.description, date: this.date, location: this.location, organizer: this.organizer, participants: this.participants, participantsCount: this.participantsCount, status: this.status, createdAt: this.createdAt, updatedAt: this.updatedAt }; } // Instance save method async save() { const errorContext = createErrorContext('Event', 'save', { id: this._id, isNew: !this._id }); return await withErrorHandling(async () => { if (!this._id) { // New document this._id = `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const created = await couchdbService.createDocument(this.toJSON()); this._rev = created._rev; return this; } else { // Update existing document this.updatedAt = new Date().toISOString(); const updated = await couchdbService.updateDocument(this.toJSON()); this._rev = updated._rev; return this; } }, errorContext); } // Migration helper static async migrateFromMongo(mongoEvent) { const errorContext = createErrorContext('Event', 'migrateFromMongo', { mongoEventId: mongoEvent._id }); return await withErrorHandling(async () => { const eventData = { title: mongoEvent.title, description: mongoEvent.description, date: mongoEvent.date, location: mongoEvent.location, status: mongoEvent.status || "upcoming" }; // Create event without participants first const event = await this.create(eventData); // If there are participants, add them with embedded user data if (mongoEvent.participants && mongoEvent.participants.length > 0) { for (const participantId of mongoEvent.participants) { try { // Get user data to embed const user = await couchdbService.findUserById(participantId.toString()); if (user) { await this.addParticipant(event._id, participantId.toString(), user.name, user.profilePicture); } } catch (error) { console.error(`Error migrating participant ${participantId}:`, error.message); } } } return event; }, errorContext); } } module.exports = Event;