feat: Complete standardized error handling across all models
- Create comprehensive error infrastructure in backend/utils/modelErrors.js - Implement consistent error handling patterns across all 11 models - Add proper try-catch blocks, validation, and error logging - Standardize error messages and error types - Maintain 100% test compatibility (221/221 tests passing) - Update UserBadge.js with flexible validation for different use cases - Add comprehensive field validation to PointTransaction.js - Improve constructor validation in Street.js and Task.js - Enhance error handling in Badge.js with backward compatibility Models updated: - User.js, Post.js, Report.js (Phase 1) - Event.js, Reward.js, Comment.js (Phase 2) - Street.js, Task.js, Badge.js, PointTransaction.js, UserBadge.js (Phase 3) 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
@@ -1,20 +1,39 @@
|
||||
const couchdbService = require("../services/couchdbService");
|
||||
const {
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
DatabaseError,
|
||||
withErrorHandling,
|
||||
createErrorContext
|
||||
} = require("../utils/modelErrors");
|
||||
|
||||
class Task {
|
||||
constructor(data) {
|
||||
// Validate required fields
|
||||
if (!data.street) {
|
||||
throw new Error('Street is required');
|
||||
}
|
||||
if (!data.description) {
|
||||
throw new Error('Description is required');
|
||||
// Handle both new documents and database documents
|
||||
const isNew = !data._id;
|
||||
|
||||
// For new documents, validate required fields
|
||||
if (isNew) {
|
||||
if (!data.street || (!data.street.streetId && !data.street._id)) {
|
||||
throw new ValidationError('Street reference is required', 'street', data.street);
|
||||
}
|
||||
if (!data.description || data.description.trim() === '') {
|
||||
throw new ValidationError('Task description is required', 'description', data.description);
|
||||
}
|
||||
if (data.status && !['pending', 'in_progress', 'completed', 'cancelled'].includes(data.status)) {
|
||||
throw new ValidationError('Status must be one of: pending, in_progress, completed, cancelled', 'status', data.status);
|
||||
}
|
||||
if (data.pointsAwarded !== undefined && (typeof data.pointsAwarded !== 'number' || data.pointsAwarded < 0)) {
|
||||
throw new ValidationError('Points awarded must be a non-negative number', 'pointsAwarded', data.pointsAwarded);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign properties
|
||||
this._id = data._id || null;
|
||||
this._rev = data._rev || null;
|
||||
this.type = "task";
|
||||
this.type = data.type || "task";
|
||||
this.street = data.street || null;
|
||||
this.description = data.description;
|
||||
this.description = data.description ? data.description.trim() : '';
|
||||
this.completedBy = data.completedBy || null;
|
||||
this.status = data.status || "pending";
|
||||
this.pointsAwarded = data.pointsAwarded || 10;
|
||||
@@ -25,7 +44,9 @@ class Task {
|
||||
|
||||
// Static methods for MongoDB-like interface
|
||||
static async find(filter = {}) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', 'find', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
// Convert MongoDB filter to CouchDB selector
|
||||
@@ -61,14 +82,13 @@ class Task {
|
||||
|
||||
// Convert to Task instances
|
||||
return docs.map(doc => new Task(doc));
|
||||
} catch (error) {
|
||||
console.error("Error finding tasks:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findById(id) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', 'findById', { taskId: id });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
const doc = await couchdbService.getDocument(id);
|
||||
|
||||
@@ -77,27 +97,22 @@ class Task {
|
||||
}
|
||||
|
||||
return new Task(doc);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404) {
|
||||
return null;
|
||||
}
|
||||
console.error("Error finding task by ID:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findOne(filter = {}) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', 'findOne', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const tasks = await Task.find(filter);
|
||||
return tasks.length > 0 ? tasks[0] : null;
|
||||
} catch (error) {
|
||||
console.error("Error finding one task:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async countDocuments(filter = {}) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', 'countDocuments', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
const selector = { type: "task", ...filter };
|
||||
@@ -110,28 +125,29 @@ class Task {
|
||||
|
||||
const docs = await couchdbService.find(query);
|
||||
return docs.length;
|
||||
} catch (error) {
|
||||
console.error("Error counting tasks:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async create(data) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', 'create', {
|
||||
description: data.description,
|
||||
streetId: data.street?.streetId || data.street?._id
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
const task = new Task(data);
|
||||
const doc = await couchdbService.createDocument(task.toJSON());
|
||||
|
||||
return new Task(doc);
|
||||
} catch (error) {
|
||||
console.error("Error creating task:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async deleteMany(filter = {}) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', 'deleteMany', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
const tasks = await Task.find(filter);
|
||||
@@ -139,15 +155,17 @@ class Task {
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
return { deletedCount: tasks.length };
|
||||
} catch (error) {
|
||||
console.error("Error deleting many tasks:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// Instance methods
|
||||
async save() {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', 'save', {
|
||||
taskId: this._id,
|
||||
description: this.description
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
this.updatedAt = new Date().toISOString();
|
||||
@@ -167,18 +185,20 @@ class Task {
|
||||
await this._handlePostSave();
|
||||
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error("Error saving task:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
async delete() {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', 'delete', {
|
||||
taskId: this._id,
|
||||
description: this.description
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
if (!this._id || !this._rev) {
|
||||
throw new Error("Task must have _id and _rev to delete");
|
||||
throw new ValidationError("Task must have _id and _rev to delete", '_id', this._id);
|
||||
}
|
||||
|
||||
// Handle cascade operations
|
||||
@@ -186,14 +206,17 @@ class Task {
|
||||
|
||||
await couchdbService.deleteDocument(this._id, this._rev);
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error("Error deleting task:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
async _handlePostSave() {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', '_handlePostSave', {
|
||||
taskId: this._id,
|
||||
status: this.status,
|
||||
completedBy: this.completedBy
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
// Update user relationship when task is completed
|
||||
if (this.completedBy && this.completedBy.userId && this.status === "completed") {
|
||||
const User = require("./User");
|
||||
@@ -216,14 +239,16 @@ class Task {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling post-save:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
async _handleCascadeDelete() {
|
||||
try {
|
||||
const errorContext = createErrorContext('Task', '_handleCascadeDelete', {
|
||||
taskId: this._id,
|
||||
completedBy: this.completedBy
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
// Remove task from user's completedTasks
|
||||
if (this.completedBy && this.completedBy.userId) {
|
||||
const User = require("./User");
|
||||
@@ -235,51 +260,62 @@ class Task {
|
||||
await user.save();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling cascade delete:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// Populate method for compatibility
|
||||
async populate(paths) {
|
||||
if (Array.isArray(paths)) {
|
||||
for (const path of paths) {
|
||||
await this._populatePath(path);
|
||||
}
|
||||
} else {
|
||||
await this._populatePath(paths);
|
||||
}
|
||||
const errorContext = createErrorContext('Task', 'populate', {
|
||||
taskId: this._id,
|
||||
paths
|
||||
});
|
||||
|
||||
return this;
|
||||
return await withErrorHandling(async () => {
|
||||
if (Array.isArray(paths)) {
|
||||
for (const path of paths) {
|
||||
await this._populatePath(path);
|
||||
}
|
||||
} else {
|
||||
await this._populatePath(paths);
|
||||
}
|
||||
|
||||
return this;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
async _populatePath(path) {
|
||||
if (path === "street" && this.street && this.street.streetId) {
|
||||
const Street = require("./Street");
|
||||
const street = await Street.findById(this.street.streetId);
|
||||
|
||||
if (street) {
|
||||
this.street = {
|
||||
streetId: street._id,
|
||||
name: street.name,
|
||||
location: street.location
|
||||
};
|
||||
}
|
||||
}
|
||||
const errorContext = createErrorContext('Task', '_populatePath', {
|
||||
taskId: this._id,
|
||||
path
|
||||
});
|
||||
|
||||
if (path === "completedBy" && this.completedBy && this.completedBy.userId) {
|
||||
const User = require("./User");
|
||||
const user = await User.findById(this.completedBy.userId);
|
||||
|
||||
if (user) {
|
||||
this.completedBy = {
|
||||
userId: user._id,
|
||||
name: user.name,
|
||||
profilePicture: user.profilePicture
|
||||
};
|
||||
return await withErrorHandling(async () => {
|
||||
if (path === "street" && this.street && this.street.streetId) {
|
||||
const Street = require("./Street");
|
||||
const street = await Street.findById(this.street.streetId);
|
||||
|
||||
if (street) {
|
||||
this.street = {
|
||||
streetId: street._id,
|
||||
name: street.name,
|
||||
location: street.location
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path === "completedBy" && this.completedBy && this.completedBy.userId) {
|
||||
const User = require("./User");
|
||||
const user = await User.findById(this.completedBy.userId);
|
||||
|
||||
if (user) {
|
||||
this.completedBy = {
|
||||
userId: user._id,
|
||||
name: user.name,
|
||||
profilePicture: user.profilePicture
|
||||
};
|
||||
}
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// Convert to plain object
|
||||
|
||||
Reference in New Issue
Block a user