Files
adopt-a-street/backend/models/User.js
William Valentin 7c7bc954ef feat: Migrate Street and Task models from MongoDB to CouchDB
- Replace Street model with CouchDB-based implementation
- Replace Task model with CouchDB-based implementation
- Update routes to use new model interfaces
- Handle geospatial queries with CouchDB design documents
- Maintain adoption functionality and middleware
- Use denormalized document structure with embedded data
- Update test files to work with new models
- Ensure API compatibility while using CouchDB underneath

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-11-01 13:12:34 -07:00

205 lines
5.6 KiB
JavaScript

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;