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,78 @@
|
||||
const couchdbService = require("../services/couchdbService");
|
||||
const {
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
DatabaseError,
|
||||
withErrorHandling,
|
||||
createErrorContext
|
||||
} = require("../utils/modelErrors");
|
||||
|
||||
class Street {
|
||||
constructor(data) {
|
||||
// Validate required fields
|
||||
if (!data.name) {
|
||||
throw new Error('Name is required');
|
||||
}
|
||||
if (!data.location) {
|
||||
throw new Error('Location is required');
|
||||
// Handle both new documents and database documents
|
||||
const isNew = !data._id;
|
||||
|
||||
// For new documents, validate required fields
|
||||
if (isNew) {
|
||||
if (!data.name || data.name.trim() === '') {
|
||||
throw new ValidationError('Street name is required', 'name', data.name);
|
||||
}
|
||||
|
||||
// Handle GeoJSON format: { type: 'Point', coordinates: [lng, lat] }
|
||||
let coordinates;
|
||||
if (data.location) {
|
||||
if (data.location.type === 'Point' && Array.isArray(data.location.coordinates)) {
|
||||
coordinates = data.location.coordinates;
|
||||
} else if (Array.isArray(data.location)) {
|
||||
coordinates = data.location;
|
||||
}
|
||||
}
|
||||
|
||||
if (!coordinates || !Array.isArray(coordinates) || coordinates.length !== 2) {
|
||||
throw new ValidationError('Valid location coordinates [longitude, latitude] are required', 'location', data.location);
|
||||
}
|
||||
if (typeof coordinates[0] !== 'number' || typeof coordinates[1] !== 'number') {
|
||||
throw new ValidationError('Location coordinates must be numbers', 'location', coordinates);
|
||||
}
|
||||
if (coordinates[0] < -180 || coordinates[0] > 180) {
|
||||
throw new ValidationError('Longitude must be between -180 and 180', 'location[0]', coordinates[0]);
|
||||
}
|
||||
if (coordinates[1] < -90 || coordinates[1] > 90) {
|
||||
throw new ValidationError('Latitude must be between -90 and 90', 'location[1]', coordinates[1]);
|
||||
}
|
||||
if (data.status && !['available', 'adopted', 'maintenance'].includes(data.status)) {
|
||||
throw new ValidationError('Status must be one of: available, adopted, maintenance', 'status', data.status);
|
||||
}
|
||||
}
|
||||
|
||||
this._id = data._id || `street_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
// Assign properties
|
||||
this._id = data._id || null;
|
||||
this._rev = data._rev || null;
|
||||
this.type = "street";
|
||||
this.name = data.name;
|
||||
this.location = data.location;
|
||||
this.type = data.type || "street";
|
||||
this.name = data.name ? data.name.trim() : '';
|
||||
|
||||
// Handle location - ensure GeoJSON format
|
||||
if (data.location) {
|
||||
if (data.location.type === 'Point' && Array.isArray(data.location.coordinates)) {
|
||||
this.location = data.location;
|
||||
} else if (Array.isArray(data.location)) {
|
||||
this.location = {
|
||||
type: 'Point',
|
||||
coordinates: data.location
|
||||
};
|
||||
} else {
|
||||
this.location = {
|
||||
type: 'Point',
|
||||
coordinates: [0, 0]
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this.location = {
|
||||
type: 'Point',
|
||||
coordinates: [0, 0]
|
||||
};
|
||||
}
|
||||
|
||||
this.adoptedBy = data.adoptedBy || null;
|
||||
this.status = data.status || "available";
|
||||
this.createdAt = data.createdAt || new Date().toISOString();
|
||||
@@ -26,9 +84,10 @@ class Street {
|
||||
};
|
||||
}
|
||||
|
||||
// Static methods for MongoDB-like interface
|
||||
static async find(filter = {}) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'find', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
// Extract pagination and sorting options from filter
|
||||
@@ -59,19 +118,17 @@ class Street {
|
||||
if (skip !== undefined) query.skip = skip;
|
||||
if (limit !== undefined) query.limit = limit;
|
||||
|
||||
console.log("Street.find query:", JSON.stringify(query, null, 2));
|
||||
const docs = await couchdbService.find(query);
|
||||
|
||||
// Convert to Street instances
|
||||
return docs.map(doc => new Street(doc));
|
||||
} catch (error) {
|
||||
console.error("Error finding streets:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findById(id) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'findById', { streetId: id });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
const doc = await couchdbService.getDocument(id);
|
||||
|
||||
@@ -80,27 +137,22 @@ class Street {
|
||||
}
|
||||
|
||||
return new Street(doc);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404) {
|
||||
return null;
|
||||
}
|
||||
console.error("Error finding street by ID:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findOne(filter = {}) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'findOne', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const streets = await Street.find(filter);
|
||||
return streets.length > 0 ? streets[0] : null;
|
||||
} catch (error) {
|
||||
console.error("Error finding one street:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async countDocuments(filter = {}) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'countDocuments', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
const selector = { type: "street", ...filter };
|
||||
@@ -113,28 +165,29 @@ class Street {
|
||||
|
||||
const docs = await couchdbService.find(query);
|
||||
return docs.length;
|
||||
} catch (error) {
|
||||
console.error("Error counting streets:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async create(data) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'create', {
|
||||
name: data.name,
|
||||
location: data.location
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
const street = new Street(data);
|
||||
const doc = await couchdbService.createDocument(street.toJSON());
|
||||
|
||||
return new Street(doc);
|
||||
} catch (error) {
|
||||
console.error("Error creating street:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async deleteMany(filter = {}) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'deleteMany', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
const streets = await Street.find(filter);
|
||||
@@ -142,15 +195,17 @@ class Street {
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
return { deletedCount: streets.length };
|
||||
} catch (error) {
|
||||
console.error("Error deleting many streets:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// Instance methods
|
||||
async save() {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'save', {
|
||||
streetId: this._id,
|
||||
name: this.name
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
this.updatedAt = new Date().toISOString();
|
||||
@@ -167,18 +222,20 @@ class Street {
|
||||
}
|
||||
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error("Error saving street:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
async delete() {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'delete', {
|
||||
streetId: this._id,
|
||||
name: this.name
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
if (!this._id || !this._rev) {
|
||||
throw new Error("Street must have _id and _rev to delete");
|
||||
throw new ValidationError("Street must have _id and _rev to delete", '_id', this._id);
|
||||
}
|
||||
|
||||
// Handle cascade operations
|
||||
@@ -186,14 +243,16 @@ class Street {
|
||||
|
||||
await couchdbService.deleteDocument(this._id, this._rev);
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error("Error deleting street:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
async _handleCascadeDelete() {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', '_handleCascadeDelete', {
|
||||
streetId: this._id,
|
||||
adoptedBy: this.adoptedBy
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
// Remove street from user's adoptedStreets
|
||||
if (this.adoptedBy && this.adoptedBy.userId) {
|
||||
const User = require("./User");
|
||||
@@ -208,37 +267,54 @@ class Street {
|
||||
// Delete all tasks associated with this street
|
||||
const Task = require("./Task");
|
||||
await Task.deleteMany({ "street.streetId": this._id });
|
||||
} catch (error) {
|
||||
console.error("Error handling cascade delete:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// Populate method for compatibility
|
||||
async populate(path) {
|
||||
if (path === "adoptedBy" && this.adoptedBy && this.adoptedBy.userId) {
|
||||
const User = require("./User");
|
||||
const user = await User.findById(this.adoptedBy.userId);
|
||||
|
||||
if (user) {
|
||||
this.adoptedBy = {
|
||||
userId: user._id,
|
||||
name: user.name,
|
||||
profilePicture: user.profilePicture
|
||||
};
|
||||
}
|
||||
}
|
||||
const errorContext = createErrorContext('Street', 'populate', {
|
||||
streetId: this._id,
|
||||
path
|
||||
});
|
||||
|
||||
return this;
|
||||
return await withErrorHandling(async () => {
|
||||
if (path === "adoptedBy" && this.adoptedBy && this.adoptedBy.userId) {
|
||||
const User = require("./User");
|
||||
const user = await User.findById(this.adoptedBy.userId);
|
||||
|
||||
if (user) {
|
||||
this.adoptedBy = {
|
||||
userId: user._id,
|
||||
name: user.name,
|
||||
profilePicture: user.profilePicture
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// Geospatial query helper
|
||||
// Geospatial query helper
|
||||
static async findNearby(coordinates, maxDistance = 1000) {
|
||||
try {
|
||||
const errorContext = createErrorContext('Street', 'findNearby', {
|
||||
coordinates,
|
||||
maxDistance
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
await couchdbService.initialize();
|
||||
|
||||
// Validate coordinates
|
||||
if (!Array.isArray(coordinates) || coordinates.length !== 2) {
|
||||
throw new ValidationError('Coordinates must be [longitude, latitude]', 'coordinates', coordinates);
|
||||
}
|
||||
if (typeof coordinates[0] !== 'number' || typeof coordinates[1] !== 'number') {
|
||||
throw new ValidationError('Coordinates must be numbers', 'coordinates', coordinates);
|
||||
}
|
||||
|
||||
// For CouchDB, we'll use a bounding box approach
|
||||
// Calculate bounding box around the point
|
||||
// Calculate bounding box around point
|
||||
const [lng, lat] = coordinates;
|
||||
const earthRadius = 6371000; // Earth's radius in meters
|
||||
const latDelta = (maxDistance / earthRadius) * (180 / Math.PI);
|
||||
@@ -251,10 +327,7 @@ class Street {
|
||||
|
||||
const streets = await couchdbService.findStreetsByLocation(bounds);
|
||||
return streets.map(doc => new Street(doc));
|
||||
} catch (error) {
|
||||
console.error("Error finding nearby streets:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// Convert to plain object
|
||||
|
||||
Reference in New Issue
Block a user