const couchdbService = require("../services/couchdbService"); const { ValidationError, NotFoundError, DatabaseError, withErrorHandling, createErrorContext } = require("../utils/modelErrors"); class Post { constructor(data) { // Handle both new documents and database documents const isNew = !data._id; // 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); } } 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 || "" }; 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); return createdPost; }, errorContext); } static async findById(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 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 }); 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); }, errorContext); } static async countDocuments() { 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 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() }; return await couchdbService.update(postId, updatedPost); }, errorContext); } static async deletePost(postId) { const errorContext = createErrorContext('Post', 'deletePost', { postId }); return await withErrorHandling(async () => { const post = await couchdbService.getById(postId); if (!post) { throw new NotFoundError('Post', 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 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); } return post; }, errorContext); } static async removeLike(postId, userId) { 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); } return post; }, errorContext); } static async incrementCommentsCount(postId) { 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); }, errorContext); } static async decrementCommentsCount(postId) { 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); }, errorContext); } static async findByUserId(userId, options = {}) { const { skip = 0, limit = 20 } = options; const errorContext = createErrorContext('Post', 'findByUserId', { userId, options }); return await withErrorHandling(async () => { const query = { selector: { type: "post", "user.userId": userId }, sort: [["createdAt", "desc"]], skip, limit }; 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 static async populate(posts, fields) { // In CouchDB, user data is already embedded in posts // This method is for compatibility with existing code if (fields && fields.includes("user")) { // User data is already embedded, so just return posts as-is return posts; } return posts; } } module.exports = Post;