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,182 +1,311 @@
|
||||
const couchdbService = require("../services/couchdbService");
|
||||
const {
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
DatabaseError,
|
||||
withErrorHandling,
|
||||
createErrorContext
|
||||
} = require("../utils/modelErrors");
|
||||
|
||||
class Post {
|
||||
static async create(postData) {
|
||||
const { user, content, imageUrl, cloudinaryPublicId } = postData;
|
||||
constructor(data) {
|
||||
// Handle both new documents and database documents
|
||||
const isNew = !data._id;
|
||||
|
||||
// Get user data for embedding
|
||||
const userDoc = await couchdbService.findUserById(user);
|
||||
if (!userDoc) {
|
||||
throw new Error("User not found");
|
||||
// For new documents, validate required fields
|
||||
if (isNew) {
|
||||
if (!data.user) {
|
||||
throw new ValidationError('User is required', 'user', data.user);
|
||||
}
|
||||
// Note: Original behavior allows posts without content, so we don't validate content here
|
||||
// The validation will happen at route level if needed
|
||||
|
||||
// Validate post type
|
||||
const validTypes = ['text', 'image', 'achievement'];
|
||||
if (data.type && !validTypes.includes(data.type)) {
|
||||
throw new ValidationError('Invalid post type', 'type', data.type);
|
||||
}
|
||||
}
|
||||
|
||||
const post = {
|
||||
_id: `post_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: "post",
|
||||
user: {
|
||||
this._id = data._id || null;
|
||||
this._rev = data._rev || null;
|
||||
this.type = data.type || "post"; // Keep original type for database docs
|
||||
this.user = data.user;
|
||||
this.content = data.content;
|
||||
this.imageUrl = data.imageUrl || null;
|
||||
this.cloudinaryPublicId = data.cloudinaryPublicId || null;
|
||||
this.postType = data.type || 'text';
|
||||
this.likes = data.likes || [];
|
||||
this.likesCount = data.likesCount || 0;
|
||||
this.commentsCount = data.commentsCount || 0;
|
||||
this.createdAt = data.createdAt || new Date().toISOString();
|
||||
this.updatedAt = data.updatedAt || new Date().toISOString();
|
||||
}
|
||||
|
||||
static async create(postData) {
|
||||
const { user, content, imageUrl, cloudinaryPublicId } = postData;
|
||||
const errorContext = createErrorContext('Post', 'create', {
|
||||
user,
|
||||
content: content?.substring(0, 100) + '...',
|
||||
hasImage: !!imageUrl
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
// Validate input first before database operations
|
||||
const post = new Post(postData);
|
||||
|
||||
// Get user data for embedding
|
||||
const userDoc = await couchdbService.findUserById(user);
|
||||
if (!userDoc) {
|
||||
throw new NotFoundError('User', user);
|
||||
}
|
||||
|
||||
post._id = `post_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
post.user = {
|
||||
userId: user,
|
||||
name: userDoc.name,
|
||||
profilePicture: userDoc.profilePicture || ""
|
||||
},
|
||||
content,
|
||||
imageUrl,
|
||||
cloudinaryPublicId,
|
||||
likes: [],
|
||||
likesCount: 0,
|
||||
commentsCount: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
};
|
||||
|
||||
const createdPost = await couchdbService.create(post);
|
||||
const createdPost = await couchdbService.create(post.toJSON());
|
||||
|
||||
// Update user's posts array
|
||||
userDoc.posts.push(createdPost._id);
|
||||
userDoc.stats.postsCreated = userDoc.posts.length;
|
||||
await couchdbService.update(user, userDoc);
|
||||
// Update user's posts array
|
||||
userDoc.posts.push(createdPost._id);
|
||||
userDoc.stats.postsCreated = userDoc.posts.length;
|
||||
await couchdbService.update(user, userDoc);
|
||||
|
||||
return createdPost;
|
||||
return createdPost;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findById(postId) {
|
||||
return await couchdbService.getById(postId);
|
||||
const errorContext = createErrorContext('Post', 'findById', { postId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const doc = await couchdbService.getById(postId);
|
||||
if (doc && (doc.type === "post" || ['text', 'image', 'achievement'].includes(doc.type))) {
|
||||
return new Post(doc);
|
||||
}
|
||||
return null;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async find(query = {}) {
|
||||
const couchQuery = {
|
||||
selector: {
|
||||
type: "post",
|
||||
...query
|
||||
}
|
||||
};
|
||||
return await couchdbService.find(couchQuery);
|
||||
const errorContext = createErrorContext('Post', 'find', { query });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const couchQuery = {
|
||||
selector: {
|
||||
type: "post",
|
||||
...query
|
||||
}
|
||||
};
|
||||
return await couchdbService.find(couchQuery);
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findAll(options = {}) {
|
||||
const { skip = 0, limit = 20, sort = { createdAt: -1 } } = options;
|
||||
const errorContext = createErrorContext('Post', 'findAll', { options });
|
||||
|
||||
const query = {
|
||||
selector: { type: "post" },
|
||||
sort: Object.keys(sort).map(key => [key, sort[key] === -1 ? "desc" : "asc"]),
|
||||
skip,
|
||||
limit
|
||||
};
|
||||
return await withErrorHandling(async () => {
|
||||
const query = {
|
||||
selector: { type: "post" },
|
||||
sort: Object.keys(sort).map(key => [key, sort[key] === -1 ? "desc" : "asc"]),
|
||||
skip,
|
||||
limit
|
||||
};
|
||||
|
||||
return await couchdbService.find(query);
|
||||
return await couchdbService.find(query);
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async countDocuments() {
|
||||
const query = {
|
||||
selector: { type: "post" },
|
||||
fields: ["_id"]
|
||||
};
|
||||
const docs = await couchdbService.find(query);
|
||||
return docs.length;
|
||||
const errorContext = createErrorContext('Post', 'countDocuments');
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const query = {
|
||||
selector: { type: "post" },
|
||||
fields: ["_id"]
|
||||
};
|
||||
const docs = await couchdbService.find(query);
|
||||
return docs.length;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async updatePost(postId, updateData) {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new Error("Post not found");
|
||||
}
|
||||
const errorContext = createErrorContext('Post', 'updatePost', { postId, updateData });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundError('Post', postId);
|
||||
}
|
||||
|
||||
const updatedPost = {
|
||||
...post,
|
||||
...updateData,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
const updatedPost = {
|
||||
...post,
|
||||
...updateData,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
return await couchdbService.update(postId, updatedPost);
|
||||
return await couchdbService.update(postId, updatedPost);
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async deletePost(postId) {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new Error("Post not found");
|
||||
}
|
||||
|
||||
// Remove post from user's posts array
|
||||
if (post.user && post.user.userId) {
|
||||
const userDoc = await couchdbService.findUserById(post.user.userId);
|
||||
if (userDoc) {
|
||||
userDoc.posts = userDoc.posts.filter(id => id !== postId);
|
||||
userDoc.stats.postsCreated = userDoc.posts.length;
|
||||
await couchdbService.update(post.user.userId, userDoc);
|
||||
const errorContext = createErrorContext('Post', 'deletePost', { postId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundError('Post', postId);
|
||||
}
|
||||
}
|
||||
|
||||
return await couchdbService.delete(postId);
|
||||
// Remove post from user's posts array
|
||||
if (post.user && post.user.userId) {
|
||||
const userDoc = await couchdbService.findUserById(post.user.userId);
|
||||
if (userDoc) {
|
||||
userDoc.posts = userDoc.posts.filter(id => id !== postId);
|
||||
userDoc.stats.postsCreated = userDoc.posts.length;
|
||||
await couchdbService.update(post.user.userId, userDoc);
|
||||
}
|
||||
}
|
||||
|
||||
return await couchdbService.delete(postId);
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async addLike(postId, userId) {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new Error("Post not found");
|
||||
}
|
||||
const errorContext = createErrorContext('Post', 'addLike', { postId, userId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundError('Post', postId);
|
||||
}
|
||||
|
||||
if (!post.likes.includes(userId)) {
|
||||
post.likes.push(userId);
|
||||
post.likesCount = post.likes.length;
|
||||
post.updatedAt = new Date().toISOString();
|
||||
await couchdbService.update(postId, post);
|
||||
}
|
||||
if (!post.likes.includes(userId)) {
|
||||
post.likes.push(userId);
|
||||
post.likesCount = post.likes.length;
|
||||
post.updatedAt = new Date().toISOString();
|
||||
await couchdbService.update(postId, post);
|
||||
}
|
||||
|
||||
return post;
|
||||
return post;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async removeLike(postId, userId) {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new Error("Post not found");
|
||||
}
|
||||
const errorContext = createErrorContext('Post', 'removeLike', { postId, userId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundError('Post', postId);
|
||||
}
|
||||
|
||||
const likeIndex = post.likes.indexOf(userId);
|
||||
if (likeIndex > -1) {
|
||||
post.likes.splice(likeIndex, 1);
|
||||
post.likesCount = post.likes.length;
|
||||
post.updatedAt = new Date().toISOString();
|
||||
await couchdbService.update(postId, post);
|
||||
}
|
||||
const likeIndex = post.likes.indexOf(userId);
|
||||
if (likeIndex > -1) {
|
||||
post.likes.splice(likeIndex, 1);
|
||||
post.likesCount = post.likes.length;
|
||||
post.updatedAt = new Date().toISOString();
|
||||
await couchdbService.update(postId, post);
|
||||
}
|
||||
|
||||
return post;
|
||||
return post;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async incrementCommentsCount(postId) {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new Error("Post not found");
|
||||
}
|
||||
const errorContext = createErrorContext('Post', 'incrementCommentsCount', { postId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundError('Post', postId);
|
||||
}
|
||||
|
||||
post.commentsCount = (post.commentsCount || 0) + 1;
|
||||
post.updatedAt = new Date().toISOString();
|
||||
return await couchdbService.update(postId, post);
|
||||
post.commentsCount = (post.commentsCount || 0) + 1;
|
||||
post.updatedAt = new Date().toISOString();
|
||||
return await couchdbService.update(postId, post);
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async decrementCommentsCount(postId) {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new Error("Post not found");
|
||||
}
|
||||
const errorContext = createErrorContext('Post', 'decrementCommentsCount', { postId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const post = await couchdbService.getById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundError('Post', postId);
|
||||
}
|
||||
|
||||
post.commentsCount = Math.max(0, (post.commentsCount || 0) - 1);
|
||||
post.updatedAt = new Date().toISOString();
|
||||
return await couchdbService.update(postId, post);
|
||||
post.commentsCount = Math.max(0, (post.commentsCount || 0) - 1);
|
||||
post.updatedAt = new Date().toISOString();
|
||||
return await couchdbService.update(postId, post);
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findByUserId(userId, options = {}) {
|
||||
const { skip = 0, limit = 20 } = options;
|
||||
const errorContext = createErrorContext('Post', 'findByUserId', { userId, options });
|
||||
|
||||
const query = {
|
||||
selector: {
|
||||
type: "post",
|
||||
"user.userId": userId
|
||||
},
|
||||
sort: [["createdAt", "desc"]],
|
||||
skip,
|
||||
limit
|
||||
};
|
||||
return await withErrorHandling(async () => {
|
||||
const query = {
|
||||
selector: {
|
||||
type: "post",
|
||||
"user.userId": userId
|
||||
},
|
||||
sort: [["createdAt", "desc"]],
|
||||
skip,
|
||||
limit
|
||||
};
|
||||
|
||||
return await couchdbService.find(query);
|
||||
return await couchdbService.find(query);
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
// Convert to CouchDB document format
|
||||
toJSON() {
|
||||
return {
|
||||
_id: this._id,
|
||||
_rev: this._rev,
|
||||
type: this.type,
|
||||
user: this.user,
|
||||
content: this.content,
|
||||
imageUrl: this.imageUrl,
|
||||
cloudinaryPublicId: this.cloudinaryPublicId,
|
||||
postType: this.postType,
|
||||
likes: this.likes,
|
||||
likesCount: this.likesCount,
|
||||
commentsCount: this.commentsCount,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
// Instance save method
|
||||
async save() {
|
||||
const errorContext = createErrorContext('Post', 'save', {
|
||||
id: this._id,
|
||||
isNew: !this._id
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
if (!this._id) {
|
||||
// New document
|
||||
this._id = `post_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
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);
|
||||
}
|
||||
|
||||
// Legacy compatibility methods for mongoose-like interface
|
||||
|
||||
Reference in New Issue
Block a user