const couchdbService = require("../services/couchdbService"); const { ValidationError, NotFoundError, DatabaseError, withErrorHandling, createErrorContext } = require("../utils/modelErrors"); class Task { constructor(data) { // Handle both new documents and database documents const isNew = !data._id; // For new documents, validate required fields if (isNew) { if (!data.street || (!data.street.streetId && !data.street._id)) { throw new ValidationError('Street reference is required', 'street', data.street); } if (!data.description || data.description.trim() === '') { throw new ValidationError('Task description is required', 'description', data.description); } if (data.status && !['pending', 'in_progress', 'completed', 'cancelled'].includes(data.status)) { throw new ValidationError('Status must be one of: pending, in_progress, completed, cancelled', 'status', data.status); } if (data.pointsAwarded !== undefined && (typeof data.pointsAwarded !== 'number' || data.pointsAwarded < 0)) { throw new ValidationError('Points awarded must be a non-negative number', 'pointsAwarded', data.pointsAwarded); } } // Assign properties this._id = data._id || null; this._rev = data._rev || null; this.type = data.type || "task"; this.street = data.street || null; this.description = data.description ? data.description.trim() : ''; this.completedBy = data.completedBy || null; this.status = data.status || "pending"; this.pointsAwarded = data.pointsAwarded || 10; this.createdAt = data.createdAt || new Date().toISOString(); this.updatedAt = data.updatedAt || new Date().toISOString(); this.completedAt = data.completedAt || null; } // Static methods for MongoDB-like interface static async find(filter = {}) { const errorContext = createErrorContext('Task', 'find', { filter }); return await withErrorHandling(async () => { await couchdbService.initialize(); // Convert MongoDB filter to CouchDB selector const selector = { type: "task", ...filter }; // Handle special cases if (filter._id) { selector._id = filter._id; } if (filter.status) { selector.status = filter.status; } if (filter.street) { selector["street.streetId"] = filter.street; } if (filter.completedBy) { selector["completedBy.userId"] = filter.completedBy; } const query = { selector, sort: filter.sort || [{ createdAt: "desc" }] }; // Add pagination if specified if (filter.skip) query.skip = filter.skip; if (filter.limit) query.limit = filter.limit; const docs = await couchdbService.find(query); // Convert to Task instances return docs.map(doc => new Task(doc)); }, errorContext); } static async findById(id) { const errorContext = createErrorContext('Task', 'findById', { taskId: id }); return await withErrorHandling(async () => { await couchdbService.initialize(); const doc = await couchdbService.getDocument(id); if (!doc || doc.type !== "task") { return null; } return new Task(doc); }, errorContext); } static async findOne(filter = {}) { const errorContext = createErrorContext('Task', 'findOne', { filter }); return await withErrorHandling(async () => { const tasks = await Task.find(filter); return tasks.length > 0 ? tasks[0] : null; }, errorContext); } static async countDocuments(filter = {}) { const errorContext = createErrorContext('Task', 'countDocuments', { filter }); return await withErrorHandling(async () => { await couchdbService.initialize(); const selector = { type: "task", ...filter }; // Use Mango query with count const query = { selector, fields: ["_id"] }; const docs = await couchdbService.find(query); return docs.length; }, errorContext); } static async create(data) { const errorContext = createErrorContext('Task', 'create', { description: data.description, streetId: data.street?.streetId || data.street?._id }); return await withErrorHandling(async () => { await couchdbService.initialize(); const task = new Task(data); const doc = await couchdbService.createDocument(task.toJSON()); return new Task(doc); }, errorContext); } static async deleteMany(filter = {}) { const errorContext = createErrorContext('Task', 'deleteMany', { filter }); return await withErrorHandling(async () => { await couchdbService.initialize(); const tasks = await Task.find(filter); const deletePromises = tasks.map(task => task.delete()); await Promise.all(deletePromises); return { deletedCount: tasks.length }; }, errorContext); } // Instance methods async save() { const errorContext = createErrorContext('Task', 'save', { taskId: this._id, description: this.description }); return await withErrorHandling(async () => { await couchdbService.initialize(); this.updatedAt = new Date().toISOString(); if (this._id && this._rev) { // Update existing document const doc = await couchdbService.updateDocument(this.toJSON()); this._rev = doc._rev; } else { // Create new document const doc = await couchdbService.createDocument(this.toJSON()); this._id = doc._id; this._rev = doc._rev; } // Handle post-save operations await this._handlePostSave(); return this; }, errorContext); } async delete() { const errorContext = createErrorContext('Task', 'delete', { taskId: this._id, description: this.description }); return await withErrorHandling(async () => { await couchdbService.initialize(); if (!this._id || !this._rev) { throw new ValidationError("Task must have _id and _rev to delete", '_id', this._id); } // Handle cascade operations await this._handleCascadeDelete(); await couchdbService.deleteDocument(this._id, this._rev); return this; }, errorContext); } async _handlePostSave() { const errorContext = createErrorContext('Task', '_handlePostSave', { taskId: this._id, status: this.status, completedBy: this.completedBy }); return await withErrorHandling(async () => { // Update user relationship when task is completed if (this.completedBy && this.completedBy.userId && this.status === "completed") { const User = require("./User"); const user = await User.findById(this.completedBy.userId); if (user && !user.completedTasks.includes(this._id)) { user.completedTasks.push(this._id); user.stats.tasksCompleted = user.completedTasks.length; await user.save(); } // Update street stats if (this.street && this.street.streetId) { const Street = require("./Street"); const street = await Street.findById(this.street.streetId); if (street) { street.stats.completedTasksCount = (street.stats.completedTasksCount || 0) + 1; await street.save(); } } } }, errorContext); } async _handleCascadeDelete() { const errorContext = createErrorContext('Task', '_handleCascadeDelete', { taskId: this._id, completedBy: this.completedBy }); return await withErrorHandling(async () => { // Remove task from user's completedTasks if (this.completedBy && this.completedBy.userId) { const User = require("./User"); const user = await User.findById(this.completedBy.userId); if (user) { user.completedTasks = user.completedTasks.filter(id => id !== this._id); user.stats.tasksCompleted = user.completedTasks.length; await user.save(); } } }, errorContext); } // Populate method for compatibility async populate(paths) { const errorContext = createErrorContext('Task', 'populate', { taskId: this._id, paths }); return await withErrorHandling(async () => { if (Array.isArray(paths)) { for (const path of paths) { await this._populatePath(path); } } else { await this._populatePath(paths); } return this; }, errorContext); } async _populatePath(path) { const errorContext = createErrorContext('Task', '_populatePath', { taskId: this._id, path }); return await withErrorHandling(async () => { if (path === "street" && this.street && this.street.streetId) { const Street = require("./Street"); const street = await Street.findById(this.street.streetId); if (street) { this.street = { streetId: street._id, name: street.name, location: street.location }; } } if (path === "completedBy" && this.completedBy && this.completedBy.userId) { const User = require("./User"); const user = await User.findById(this.completedBy.userId); if (user) { this.completedBy = { userId: user._id, name: user.name, profilePicture: user.profilePicture }; } } }, errorContext); } // Convert to plain object toJSON() { return { _id: this._id, _rev: this._rev, type: this.type, street: this.street, description: this.description, completedBy: this.completedBy, status: this.status, pointsAwarded: this.pointsAwarded, createdAt: this.createdAt, updatedAt: this.updatedAt, completedAt: this.completedAt }; } // Convert to MongoDB-like format for API responses toObject() { const obj = this.toJSON(); // Remove CouchDB-specific fields for API compatibility delete obj._rev; delete obj.type; // Add _id field for compatibility if (obj._id) { obj.id = obj._id; } return obj; } } // Export both the class and static methods for compatibility module.exports = Task;