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:
William Valentin
2025-11-03 09:43:46 -08:00
parent 97f794fca5
commit 07a80b718b
6 changed files with 1044 additions and 339 deletions

View File

@@ -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);
}
}