const bcrypt = require("bcryptjs"); const couchdbService = require("../services/couchdbService"); class User { constructor(data) { // Validate required fields if (!data.name) { throw new Error('Name is required'); } if (!data.email) { throw new Error('Email is required'); } if (!data.password) { throw new Error('Password is required'); } 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) { 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; } static async findById(id) { const user = await couchdbService.findUserById(id); return user ? new User(user) : null; } static async findByIdAndUpdate(id, update, options = {}) { 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; } static async findByIdAndDelete(id) { const user = await couchdbService.findUserById(id); if (!user) return null; await couchdbService.delete(id); return user; } static async find(query = {}) { const selector = { type: "user", ...query }; return await couchdbService.find({ selector }); } static async create(userData) { 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); } // Instance methods async save() { 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; } } async comparePassword(candidatePassword) { return await bcrypt.compare(candidatePassword, this.password); } // 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 users = await couchdbService.find({ selector: { type: "user" }, fields: fields }); return users.map(user => new User(user)); } } // 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;