const bcrypt = require("bcryptjs"); const couchdbService = require("../services/couchdbService"); const { ValidationError, NotFoundError, DatabaseError, DuplicateError, withErrorHandling, createErrorContext } = require("../utils/modelErrors"); class User { constructor(data) { // Validate required fields if (!data.name) { throw new ValidationError('Name is required', 'name', data.name); } if (!data.email) { throw new ValidationError('Email is required', 'email', data.email); } if (!data.password) { throw new ValidationError('Password is required', 'password', data.password); } this._id = data._id || null; this._rev = data._rev || null; this.type = "user"; this.name = data.name; this.email = data.email; this.password = data.password; this.isPremium = data.isPremium || false; this.points = Math.max(0, data.points || 0); // Ensure non-negative this.adoptedStreets = data.adoptedStreets || []; this.completedTasks = data.completedTasks || []; this.posts = data.posts || []; this.events = data.events || []; this.profilePicture = data.profilePicture || null; this.cloudinaryPublicId = data.cloudinaryPublicId || null; this.earnedBadges = data.earnedBadges || []; this.stats = data.stats || { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }; this.createdAt = data.createdAt || new Date().toISOString(); this.updatedAt = data.updatedAt || new Date().toISOString(); } // Static methods for MongoDB compatibility static async findOne(query) { const errorContext = createErrorContext('User', 'findOne', { query }); return await withErrorHandling(async () => { let user; if (query.email) { user = await couchdbService.findUserByEmail(query.email); } else if (query._id) { user = await couchdbService.findUserById(query._id); } else { // Generic query fallback const docs = await couchdbService.find({ selector: { type: "user", ...query }, limit: 1 }); user = docs[0] || null; } return user ? new User(user) : null; }, errorContext); } static async findById(id) { const errorContext = createErrorContext('User', 'findById', { id }); return await withErrorHandling(async () => { const user = await couchdbService.findUserById(id); return user ? new User(user) : null; }, errorContext); } static async findByIdAndUpdate(id, update, options = {}) { const errorContext = createErrorContext('User', 'findByIdAndUpdate', { id, update, options }); return await withErrorHandling(async () => { const user = await couchdbService.findUserById(id); if (!user) return null; const updatedUser = { ...user, ...update, updatedAt: new Date().toISOString() }; const saved = await couchdbService.update(id, updatedUser); if (options.new) { return saved; } return user; }, errorContext); } static async findByIdAndDelete(id) { const errorContext = createErrorContext('User', 'findByIdAndDelete', { id }); return await withErrorHandling(async () => { const user = await couchdbService.findUserById(id); if (!user) return null; await couchdbService.delete(id); return user; }, errorContext); } static async find(query = {}) { const errorContext = createErrorContext('User', 'find', { query }); return await withErrorHandling(async () => { const selector = { type: "user", ...query }; return await couchdbService.find({ selector }); }, errorContext); } static async create(userData) { const errorContext = createErrorContext('User', 'create', { userData: { ...userData, password: '[REDACTED]' } }); return await withErrorHandling(async () => { const user = new User(userData); // Hash password if provided if (user.password) { const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(user.password, salt); } // Generate ID if not provided if (!user._id) { user._id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } const created = await couchdbService.createDocument(user.toJSON()); return new User(created); }, errorContext); } // Instance methods async save() { const errorContext = createErrorContext('User', 'save', { id: this._id, email: this.email, isNew: !this._id }); return await withErrorHandling(async () => { if (!this._id) { // New document this._id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Hash password if not already hashed if (this.password && !this.password.startsWith('$2')) { const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); } 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); } async comparePassword(candidatePassword) { const errorContext = createErrorContext('User', 'comparePassword', { id: this._id, email: this.email }); return await withErrorHandling(async () => { return await bcrypt.compare(candidatePassword, this.password); }, errorContext); } // Helper method to get user without password toSafeObject() { const obj = this.toJSON(); delete obj.password; return obj; } // Convert to CouchDB document format toJSON() { return { _id: this._id, _rev: this._rev, type: this.type, name: this.name, email: this.email, password: this.password, isPremium: this.isPremium, points: this.points, adoptedStreets: this.adoptedStreets, completedTasks: this.completedTasks, posts: this.posts, events: this.events, profilePicture: this.profilePicture, cloudinaryPublicId: this.cloudinaryPublicId, earnedBadges: this.earnedBadges, stats: this.stats, createdAt: this.createdAt, updatedAt: this.updatedAt }; } // Static method for select functionality static async select(fields) { const errorContext = createErrorContext('User', 'select', { fields }); return await withErrorHandling(async () => { const users = await couchdbService.find({ selector: { type: "user" }, fields: fields }); return users.map(user => new User(user)); }, errorContext); } } // Add select method to instance for chaining User.prototype.select = function(fields) { const obj = this.toJSON(); const selected = {}; if (fields.includes('-password')) { // Exclude password fields = fields.filter(f => f !== '-password'); fields.forEach(field => { if (obj[field] !== undefined) { selected[field] = obj[field]; } }); } else { // Include only specified fields fields.forEach(field => { if (obj[field] !== undefined) { selected[field] = obj[field]; } }); } return selected; }; module.exports = User;