const couchdbService = require("../services/couchdbService"); const { ValidationError, NotFoundError, DatabaseError, withErrorHandling, createErrorContext } = require("../utils/modelErrors"); class Comment { constructor(data) { // Handle both new documents and database documents const isNew = !data._id; // For new documents, validate required fields if (isNew) { if (!data.user || !data.user.userId) { throw new ValidationError('User ID is required', 'user.userId', data.user?.userId); } if (!data.post || !data.post.postId) { throw new ValidationError('Post ID is required', 'post.postId', data.post?.postId); } const contentStr = typeof data.content === 'string' ? data.content : String(data.content || ''); if (!contentStr || contentStr.trim() === '') { throw new ValidationError('Content is required', 'content', data.content); } if (contentStr.length > 500) { throw new ValidationError('Content must be 500 characters or less', 'content', contentStr.length); } } // Assign properties this._id = data._id || null; this._rev = data._rev || null; this.type = data.type || "comment"; this.user = data.user || { userId: null, name: '', profilePicture: '' }; this.post = data.post || { postId: null, content: '', userId: null }; const contentStr = typeof data.content === 'string' ? data.content : String(data.content || ''); this.content = contentStr ? contentStr.trim() : ''; this.createdAt = data.createdAt || new Date().toISOString(); this.updatedAt = data.updatedAt || new Date().toISOString(); } toJSON() { return { _id: this._id, _rev: this._rev, type: this.type, user: this.user, post: this.post, content: this.content, createdAt: this.createdAt, updatedAt: this.updatedAt }; } async save() { const errorContext = createErrorContext('Comment', 'save', { commentId: this._id, postId: this.post?.postId }); return await withErrorHandling(async () => { if (this._id) { // Update existing document const updatedDoc = await couchdbService.updateDocument(this._id, this.toJSON()); Object.assign(this, updatedDoc); return this; } else { // Create new document this._id = `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const createdDoc = await couchdbService.createDocument(this.toJSON()); Object.assign(this, createdDoc); return this; } }, errorContext); } static async create(commentData) { const { user, post, content } = commentData; const errorContext = createErrorContext('Comment', 'create', { userId: user, postId: post, contentLength: content?.length }); return await withErrorHandling(async () => { // Validate content first const validatedContent = Comment.validateContent(content); // Get user data for embedding const userDoc = await couchdbService.findUserById(user); if (!userDoc) { throw new NotFoundError('User', user); } // Get post data for embedding const postDoc = await couchdbService.getById(post); if (!postDoc) { throw new NotFoundError('Post', post); } const comment = new Comment({ _id: `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: "comment", user: { userId: user, name: userDoc.name, profilePicture: userDoc.profilePicture || "" }, post: { postId: post, content: postDoc.content, userId: postDoc.user.userId }, content: validatedContent, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); const createdComment = await couchdbService.createDocument(comment.toJSON()); // Update post's comment count const updatedPost = { ...postDoc, commentsCount: (postDoc.commentsCount || 0) + 1 }; await couchdbService.updateDocument(post, updatedPost); return new Comment(createdComment); }, errorContext); } static async findById(commentId) { const errorContext = createErrorContext('Comment', 'findById', { commentId }); return await withErrorHandling(async () => { const doc = await couchdbService.getById(commentId); if (doc && doc.type === "comment") { return new Comment(doc); } return null; }, errorContext); } static async find(query = {}) { const errorContext = createErrorContext('Comment', 'find', { query }); return await withErrorHandling(async () => { const couchQuery = { selector: { type: "comment", ...query } }; const docs = await couchdbService.find(couchQuery); return docs.map(doc => new Comment(doc)); }, errorContext); } static async findByPostId(postId, options = {}) { const { skip = 0, limit = 50, sort = { createdAt: -1 } } = options; const errorContext = createErrorContext('Comment', 'findByPostId', { postId, options }); return await withErrorHandling(async () => { const query = { selector: { type: "comment", "post.postId": postId }, sort: Object.keys(sort).map(key => [key, sort[key] === -1 ? "desc" : "asc"]), skip, limit }; const docs = await couchdbService.find(query); return docs.map(doc => new Comment(doc)); }, errorContext); } static async countDocuments(query = {}) { const errorContext = createErrorContext('Comment', 'countDocuments', { query }); return await withErrorHandling(async () => { const couchQuery = { selector: { type: "comment", ...query }, fields: ["_id"] }; const docs = await couchdbService.find(couchQuery); return docs.length; }, errorContext); } static async deleteComment(commentId) { const errorContext = createErrorContext('Comment', 'deleteComment', { commentId }); return await withErrorHandling(async () => { const comment = await this.findById(commentId); if (!comment) { throw new NotFoundError('Comment', commentId); } // Update post's comment count if (comment.post && comment.post.postId) { const postDoc = await couchdbService.getById(comment.post.postId); if (postDoc) { await couchdbService.updatePost(comment.post.postId, { commentsCount: Math.max(0, (postDoc.commentsCount || 0) - 1) }); } } return await couchdbService.deleteDocument(commentId); }, errorContext); } static async findByUserId(userId, options = {}) { const { skip = 0, limit = 50 } = options; const errorContext = createErrorContext('Comment', 'findByUserId', { userId, options }); return await withErrorHandling(async () => { const query = { selector: { type: "comment", "user.userId": userId }, sort: [["createdAt", "desc"]], skip, limit }; const docs = await couchdbService.find(query); return docs.map(doc => new Comment(doc)); }, errorContext); } static async validateContent(content) { if (!content || content.trim().length === 0) { throw new ValidationError("Comment content is required", 'content', content); } if (content.length > 500) { throw new ValidationError("Comment content must be 500 characters or less", 'content', content.length); } return content.trim(); } // Legacy compatibility methods for mongoose-like interface static async populate(comments, fields) { const errorContext = createErrorContext('Comment', 'populate', { commentCount: comments.length, fields }); return await withErrorHandling(async () => { // In CouchDB, user and post data are already embedded in comments // This method is for compatibility with existing code if (fields) { if (fields.includes("user") || fields.includes("post")) { // Data is already embedded, so just return comments as-is return comments; } } return comments; }, errorContext); } // Helper method to check if comment belongs to a post static async belongsToPost(commentId, postId) { const errorContext = createErrorContext('Comment', 'belongsToPost', { commentId, postId }); return await withErrorHandling(async () => { const comment = await this.findById(commentId); return comment && comment.post && comment.post.postId === postId; }, errorContext); } // Helper method to check if user owns comment static async isOwnedByUser(commentId, userId) { const errorContext = createErrorContext('Comment', 'isOwnedByUser', { commentId, userId }); return await withErrorHandling(async () => { const comment = await this.findById(commentId); return comment && comment.user && comment.user.userId === userId; }, errorContext); } } module.exports = Comment;