- 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>
357 lines
10 KiB
JavaScript
357 lines
10 KiB
JavaScript
const couchdbService = require("../services/couchdbService");
|
|
const {
|
|
ValidationError,
|
|
NotFoundError,
|
|
DatabaseError,
|
|
withErrorHandling,
|
|
createErrorContext
|
|
} = require("../utils/modelErrors");
|
|
|
|
class Task {
|
|
constructor(data) {
|
|
// 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 = data.type || "task";
|
|
this.street = data.street || null;
|
|
this.description = data.description ? data.description.trim() : '';
|
|
this.completedBy = data.completedBy || null;
|
|
this.status = data.status || "pending";
|
|
this.pointsAwarded = data.pointsAwarded || 10;
|
|
this.createdAt = data.createdAt || new Date().toISOString();
|
|
this.updatedAt = data.updatedAt || new Date().toISOString();
|
|
this.completedAt = data.completedAt || null;
|
|
}
|
|
|
|
// Static methods for MongoDB-like interface
|
|
static async find(filter = {}) {
|
|
const errorContext = createErrorContext('Task', 'find', { filter });
|
|
|
|
return await withErrorHandling(async () => {
|
|
await couchdbService.initialize();
|
|
|
|
// Convert MongoDB filter to CouchDB selector
|
|
const selector = { type: "task", ...filter };
|
|
|
|
// Handle special cases
|
|
if (filter._id) {
|
|
selector._id = filter._id;
|
|
}
|
|
|
|
if (filter.status) {
|
|
selector.status = filter.status;
|
|
}
|
|
|
|
if (filter.street) {
|
|
selector["street.streetId"] = filter.street;
|
|
}
|
|
|
|
if (filter.completedBy) {
|
|
selector["completedBy.userId"] = filter.completedBy;
|
|
}
|
|
|
|
const query = {
|
|
selector,
|
|
sort: filter.sort || [{ createdAt: "desc" }]
|
|
};
|
|
|
|
// Add pagination if specified
|
|
if (filter.skip) query.skip = filter.skip;
|
|
if (filter.limit) query.limit = filter.limit;
|
|
|
|
const docs = await couchdbService.find(query);
|
|
|
|
// Convert to Task instances
|
|
return docs.map(doc => new Task(doc));
|
|
}, errorContext);
|
|
}
|
|
|
|
static async findById(id) {
|
|
const errorContext = createErrorContext('Task', 'findById', { taskId: id });
|
|
|
|
return await withErrorHandling(async () => {
|
|
await couchdbService.initialize();
|
|
const doc = await couchdbService.getDocument(id);
|
|
|
|
if (!doc || doc.type !== "task") {
|
|
return null;
|
|
}
|
|
|
|
return new Task(doc);
|
|
}, errorContext);
|
|
}
|
|
|
|
static async findOne(filter = {}) {
|
|
const errorContext = createErrorContext('Task', 'findOne', { filter });
|
|
|
|
return await withErrorHandling(async () => {
|
|
const tasks = await Task.find(filter);
|
|
return tasks.length > 0 ? tasks[0] : null;
|
|
}, errorContext);
|
|
}
|
|
|
|
static async countDocuments(filter = {}) {
|
|
const errorContext = createErrorContext('Task', 'countDocuments', { filter });
|
|
|
|
return await withErrorHandling(async () => {
|
|
await couchdbService.initialize();
|
|
|
|
const selector = { type: "task", ...filter };
|
|
|
|
// Use Mango query with count
|
|
const query = {
|
|
selector,
|
|
fields: ["_id"]
|
|
};
|
|
|
|
const docs = await couchdbService.find(query);
|
|
return docs.length;
|
|
}, errorContext);
|
|
}
|
|
|
|
static async create(data) {
|
|
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);
|
|
}, errorContext);
|
|
}
|
|
|
|
static async deleteMany(filter = {}) {
|
|
const errorContext = createErrorContext('Task', 'deleteMany', { filter });
|
|
|
|
return await withErrorHandling(async () => {
|
|
await couchdbService.initialize();
|
|
|
|
const tasks = await Task.find(filter);
|
|
const deletePromises = tasks.map(task => task.delete());
|
|
|
|
await Promise.all(deletePromises);
|
|
return { deletedCount: tasks.length };
|
|
}, errorContext);
|
|
}
|
|
|
|
// Instance methods
|
|
async save() {
|
|
const errorContext = createErrorContext('Task', 'save', {
|
|
taskId: this._id,
|
|
description: this.description
|
|
});
|
|
|
|
return await withErrorHandling(async () => {
|
|
await couchdbService.initialize();
|
|
|
|
this.updatedAt = new Date().toISOString();
|
|
|
|
if (this._id && this._rev) {
|
|
// Update existing document
|
|
const doc = await couchdbService.updateDocument(this.toJSON());
|
|
this._rev = doc._rev;
|
|
} else {
|
|
// Create new document
|
|
const doc = await couchdbService.createDocument(this.toJSON());
|
|
this._id = doc._id;
|
|
this._rev = doc._rev;
|
|
}
|
|
|
|
// Handle post-save operations
|
|
await this._handlePostSave();
|
|
|
|
return this;
|
|
}, errorContext);
|
|
}
|
|
|
|
async delete() {
|
|
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 ValidationError("Task must have _id and _rev to delete", '_id', this._id);
|
|
}
|
|
|
|
// Handle cascade operations
|
|
await this._handleCascadeDelete();
|
|
|
|
await couchdbService.deleteDocument(this._id, this._rev);
|
|
return this;
|
|
}, errorContext);
|
|
}
|
|
|
|
async _handlePostSave() {
|
|
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");
|
|
const user = await User.findById(this.completedBy.userId);
|
|
|
|
if (user && !user.completedTasks.includes(this._id)) {
|
|
user.completedTasks.push(this._id);
|
|
user.stats.tasksCompleted = user.completedTasks.length;
|
|
await user.save();
|
|
}
|
|
|
|
// Update street stats
|
|
if (this.street && this.street.streetId) {
|
|
const Street = require("./Street");
|
|
const street = await Street.findById(this.street.streetId);
|
|
|
|
if (street) {
|
|
street.stats.completedTasksCount = (street.stats.completedTasksCount || 0) + 1;
|
|
await street.save();
|
|
}
|
|
}
|
|
}
|
|
}, errorContext);
|
|
}
|
|
|
|
async _handleCascadeDelete() {
|
|
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");
|
|
const user = await User.findById(this.completedBy.userId);
|
|
|
|
if (user) {
|
|
user.completedTasks = user.completedTasks.filter(id => id !== this._id);
|
|
user.stats.tasksCompleted = user.completedTasks.length;
|
|
await user.save();
|
|
}
|
|
}
|
|
}, errorContext);
|
|
}
|
|
|
|
// Populate method for compatibility
|
|
async populate(paths) {
|
|
const errorContext = createErrorContext('Task', 'populate', {
|
|
taskId: this._id,
|
|
paths
|
|
});
|
|
|
|
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) {
|
|
const errorContext = createErrorContext('Task', '_populatePath', {
|
|
taskId: this._id,
|
|
path
|
|
});
|
|
|
|
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
|
|
toJSON() {
|
|
return {
|
|
_id: this._id,
|
|
_rev: this._rev,
|
|
type: this.type,
|
|
street: this.street,
|
|
description: this.description,
|
|
completedBy: this.completedBy,
|
|
status: this.status,
|
|
pointsAwarded: this.pointsAwarded,
|
|
createdAt: this.createdAt,
|
|
updatedAt: this.updatedAt,
|
|
completedAt: this.completedAt
|
|
};
|
|
}
|
|
|
|
// Convert to MongoDB-like format for API responses
|
|
toObject() {
|
|
const obj = this.toJSON();
|
|
|
|
// Remove CouchDB-specific fields for API compatibility
|
|
delete obj._rev;
|
|
delete obj.type;
|
|
|
|
// Add _id field for compatibility
|
|
if (obj._id) {
|
|
obj.id = obj._id;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
// Export both the class and static methods for compatibility
|
|
module.exports = Task;
|