feat: complete Post model standardized error handling
- Add comprehensive error handling to Post model with ValidationError, NotFoundError - Fix Post model toJSON method duplicate type field bug - Update Post test suite with proper mocking for all CouchDB service methods - All 23 Post model tests now passing - Complete standardized error handling implementation for User, Report, and Post models - Add modelErrors utility with structured error classes and logging 🤖 Generated with AI Assistant Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
@@ -1,17 +1,25 @@
|
||||
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 Error('Name is required');
|
||||
throw new ValidationError('Name is required', 'name', data.name);
|
||||
}
|
||||
if (!data.email) {
|
||||
throw new Error('Email is required');
|
||||
throw new ValidationError('Email is required', 'email', data.email);
|
||||
}
|
||||
if (!data.password) {
|
||||
throw new Error('Password is required');
|
||||
throw new ValidationError('Password is required', 'password', data.password);
|
||||
}
|
||||
|
||||
this._id = data._id || null;
|
||||
@@ -42,97 +50,136 @@ class User {
|
||||
|
||||
// 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;
|
||||
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 user = await couchdbService.findUserById(id);
|
||||
return user ? new User(user) : null;
|
||||
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 user = await couchdbService.findUserById(id);
|
||||
if (!user) return null;
|
||||
|
||||
const updatedUser = { ...user, ...update, updatedAt: new Date().toISOString() };
|
||||
const saved = await couchdbService.update(id, updatedUser);
|
||||
const errorContext = createErrorContext('User', 'findByIdAndUpdate', { id, update, options });
|
||||
|
||||
if (options.new) {
|
||||
return saved;
|
||||
}
|
||||
return user;
|
||||
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 user = await couchdbService.findUserById(id);
|
||||
if (!user) return null;
|
||||
const errorContext = createErrorContext('User', 'findByIdAndDelete', { id });
|
||||
|
||||
await couchdbService.delete(id);
|
||||
return user;
|
||||
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 selector = { type: "user", ...query };
|
||||
return await couchdbService.find({ selector });
|
||||
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 user = new User(userData);
|
||||
const errorContext = createErrorContext('User', 'create', { userData: { ...userData, password: '[REDACTED]' } });
|
||||
|
||||
// Hash password if provided
|
||||
if (user.password) {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
user.password = await bcrypt.hash(user.password, salt);
|
||||
}
|
||||
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)}`;
|
||||
}
|
||||
// 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);
|
||||
const created = await couchdbService.createDocument(user.toJSON());
|
||||
return new User(created);
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
return await bcrypt.compare(candidatePassword, this.password);
|
||||
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
|
||||
@@ -168,11 +215,15 @@ class User {
|
||||
|
||||
// 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));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user