diff --git a/__test__/community/communityQueue.test.js b/__test__/community/communityQueue.test.js new file mode 100644 index 0000000..0428f0c --- /dev/null +++ b/__test__/community/communityQueue.test.js @@ -0,0 +1,528 @@ +import { objectItem } from '../../src/services/communityQueueService.js' +import { Community } from '../../src/db/models/Community.js' +import { Post } from '../../src/db/models/Post.js' +import { Comment } from '../../src/db/models/Comment.js' + +jest.mock('../../src/db/models/Community.js'); +jest.mock('../../src/db/models/Post.js'); +jest.mock('../../src/db/models/Comment.js'); + +describe('objectItem', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should return an error for invalid item type', async () => { + const result = await objectItem('id', 'invalid', 'reported', 'user', 'Harassment', 'community'); + expect(result).toEqual({ err: { status: 400, message: 'Invalid item type' } }); + }); + + it('should return an error for invalid objection type', async () => { + const result = await objectItem('id', 'post', 'invalid', 'user', 'Harassment', 'community'); + expect(result).toEqual({ err: { status: 400, message: `Invalid objection type, the allowed types are 'reported', 'spammed', 'removed'.` } }); + }); + + it('should return an error for invalid report reason', async () => { + const result = await objectItem('id', 'post', 'reported', 'user', 'Invalid', 'community'); + expect(result).toEqual({ err: { status: 400, message: 'Invalid objection type value, check the report reasons' } }); + }); + + it('should return an error for invalid removal reason', async () => { + Community.findOne.mockResolvedValue({ removal_reasons: [] }); + const result = await objectItem('id', 'post', 'removed', 'user', 'Invalid', 'community'); + expect(result).toEqual({ err: { status: 400, message: 'Invalid objection type value, check the community removal reasons' } }); + }); + + it('should return an error for non-existing item', async () => { + Post.findById.mockResolvedValue(null); + const result = await objectItem('id', 'post', 'reported', 'user', 'Harassment', 'community'); + expect(result).toEqual({ err: { status: 404, message: 'Post not found' } }); + }); + + it('should return an error for edited item with no action taken', async () => { + Post.findById.mockResolvedValue({ + community_moderator_details: { + edit_history: [{ + edited_at: new Date(), + approved_edit_flag: false, + removed_edit_flag: false + }] + } + }); + const result = await objectItem('id', 'post', 'reported', 'user', 'Harassment', 'community'); + expect(result).toEqual({ err: { status: 400, message: 'Post has been edited, no action taken on last edit, can\'t object' } }); + }); + + it('should return an error for item with existing objection', async () => { + Post.findById.mockResolvedValue({ + community_moderator_details: { + reported: { flag: true }, + edit_history: [] + } + }); + const result = await objectItem('id', 'post', 'reported', 'user', 'Harassment', 'community'); + expect(result).toEqual({ err: { status: 400, message: 'Post already has an objection' } }); + }); + + it('should object the item successfully', async () => { + Post.findById.mockResolvedValue({ + community_moderator_details: { + reported: { flag: false }, + edit_history: [] + } + }); + Post.findByIdAndUpdate.mockResolvedValue(true); + const result = await objectItem('id', 'post', 'reported', 'user', 'Harassment', 'community'); + expect(result).toEqual({ message: 'Post reported successfully' }); + }); + + it('should handle unexpected errors', async () => { + const errorMessage = 'Unexpected error'; + const item_id = 'item1'; + const item_type = 'post'; + const objection_type = 'reported'; + const objected_by = 'user1'; + const objection_type_value = 'Harassment'; + const community_name = 'community1'; + + // Mock the Post model's findById method to throw an error + Post.findById.mockImplementation(() => { + throw new Error(errorMessage); + }); + + const result = await objectItem(item_id, item_type, objection_type, objected_by, objection_type_value, community_name); + + expect(result).toEqual({ err: { status: 500, message: errorMessage } }); + }); +}); + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { editItem } from '../../src/services/communityQueueService.js'; + +jest.mock('../../src/db/models/Post.js'); +jest.mock('../../src/db/models/Comment.js'); + +describe('editItem', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should return an error for non-existent item', async () => { + Post.findById.mockResolvedValue(null); + const result = await editItem('id', 'post', 'new content', { username: 'user' }); + expect(result).toEqual({ err: { status: 404, message: 'Post not found' } }); + }); + + it('should return an error for non-author user', async () => { + Post.findById.mockResolvedValue({ username: 'other' }); + const result = await editItem('id', 'post', 'new content', { username: 'user' }); + expect(result).toEqual({ err: { status: 403, message: 'Access denied. You must be the author of the item to edit it.' } }); + }); + + it('should return an error for item with objection', async () => { + Post.findById.mockResolvedValue({ + username: 'user', + community_moderator_details: { + reported: { flag: true } + } + }); + const result = await editItem('id', 'post', 'new content', { username: 'user' }); + expect(result).toEqual({ err: { status: 400, message: 'Post has an objection, no action taken on objection, can\'t edit' } }); + }); + + it('should return an error for invalid new content', async () => { + Post.findById.mockResolvedValue({ username: 'user' }); + const result = await editItem('id', 'post', 123, { username: 'user' }); + expect(result).toEqual({ err: { status: 400, message: 'Invalid new content' } }); + }); + + it('should edit the item successfully', async () => { + Post.findById.mockResolvedValue({ + username: 'user', + community_moderator_details: { + edit_history: [] + }, + moderator_details: { + edited_at: null + } + }); + Post.findByIdAndUpdate.mockResolvedValue(true); + const result = await editItem('id', 'post', 'new content', { username: 'user' }); + expect(result).toEqual({ message: 'Post edited successfully' }); + }); + + it('should return an error for database error', async () => { + Post.findById.mockRejectedValue(new Error('Database error')); + const result = await editItem('id', 'post', 'new content', { username: 'user' }); + expect(result).toEqual({ err: { status: 500, message: 'Database error' } }); + }); +}); + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { handleObjection } from '../../src/services/communityQueueService.js'; + +jest.mock('../../src/db/models/Post.js'); +jest.mock('../../src/db/models/Comment.js'); + +describe('handleObjection', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should return an error for non-existent item', async () => { + Post.findById.mockResolvedValue(null); + const result = await handleObjection('id', 'post', 'objection', 'approve'); + expect(result).toEqual({ err: { status: 404, message: 'Post not found' } }); + }); + + it('should return an error for non-existent objection', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { objection: { flag: false } } }); + const result = await handleObjection('id', 'post', 'objection', 'approve'); + expect(result).toEqual({ err: { status: 400, message: 'No objection objection exists on this post' } }); + }); + + it('should return an error for invalid action', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { objection: { flag: true } } }); + const result = await handleObjection('id', 'post', 'objection', 'invalid'); + expect(result).toEqual({ err: { status: 400, message: 'Invalid action. Action must be either \'approve\' or \'remove\'' } }); + }); + + it('should return an error for handled objection', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { objection: { flag: true, confirmed: true } } }); + const result = await handleObjection('id', 'post', 'objection', 'approve'); + expect(result).toEqual({ err: { status: 400, message: 'The objection objection cannot be approved because it has already been handled.' } }); + }); + + it('should handle the objection successfully', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { removed: { flag: true, confirmed: false } } }); + Post.findByIdAndUpdate.mockResolvedValue(true); + const result = await handleObjection('id', 'post', 'removed', 'approve'); + expect(result).toEqual({ message: 'Removed objection approved successfully' }); + }); + + it('should return an error for database error', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { objection: { flag: true, confirmed: false } } }); + Post.findByIdAndUpdate.mockRejectedValue(new Error('Database error')); + const result = await handleObjection('id', 'post', 'objection', 'approve'); + expect(result).toEqual({ err: { status: 500, message: 'Database error' } }); + }); + + it('should handle the remove action', async () => { + const item_id = 'item1'; + const item_type = 'post'; + const objection_type = 'reported'; + const action = 'remove'; + + // Mock the Post model's findById method to return a mocked item + Post.findById.mockResolvedValue({ + community_moderator_details: { + [objection_type]: { + flag: true, + confirmed: false + } + } + }); + + // Mock the Post model's findByIdAndUpdate method to do nothing + Post.findByIdAndUpdate.mockResolvedValue(); + + const result = await handleObjection(item_id, item_type, objection_type, action); + + expect(result).toEqual({ message: `${objection_type.charAt(0).toUpperCase() + objection_type.slice(1)} objection ${action}d successfully` }); + }); + + it('should handle unexpected errors', async () => { + const errorMessage = 'Unexpected error'; + const item_id = 'item1'; + const item_type = 'post'; + const objection_type = 'reported'; + const action = 'remove'; + + // Mock the Post model's findById method to throw an error + Post.findById.mockImplementation(() => { + throw new Error(errorMessage); + }); + + const result = await handleObjection(item_id, item_type, objection_type, action); + + expect(result).toEqual({ err: { status: 500, message: errorMessage } }); + }); +}); + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { handleEdit } from '../../src/services/communityQueueService.js'; + +jest.mock('../../src/db/models/Post.js'); +jest.mock('../../src/db/models/Comment.js'); + +describe('handleEdit', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return an error if the item is not found', async () => { + Post.findById.mockResolvedValue(null); + const result = await handleEdit('id', 'post', 'approve'); + expect(result).toEqual({ err: { status: 404, message: 'Item not found' } }); + }); + + it('should return an error if the action is invalid', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { edit_history: [{}] } }); + const result = await handleEdit('id', 'post', 'invalid'); + expect(result).toEqual({ err: { status: 400, message: 'Invalid action' } }); + }); + + it('should return an error if the last edit is already approved or removed', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { edit_history: [{ approved_edit_flag: true, removed_edit_flag: false }] } }); + const result = await handleEdit('id', 'post', 'approve'); + expect(result).toEqual({ err: { status: 400, message: 'The last edit is already approved or removed' } }); + }); + + it('should approve the last edit successfully', async () => { + const mockItem = { community_moderator_details: { edit_history: [{ approved_edit_flag: false, removed_edit_flag: false }], unmoderated: { any_action_taken: false } }, save: jest.fn().mockResolvedValue(true) }; + Post.findById.mockResolvedValue(mockItem); + const result = await handleEdit('id', 'post', 'approve'); + expect(result).toEqual({ message: 'Edit approved successfully' }); + expect(mockItem.community_moderator_details.edit_history[0].approved_edit_flag).toBe(true); + expect(mockItem.community_moderator_details.unmoderated.any_action_taken).toBe(true); + }); + + it('should remove the last edit successfully', async () => { + const mockItem = { community_moderator_details: { edit_history: [{ approved_edit_flag: false, removed_edit_flag: false }], unmoderated: { any_action_taken: false } }, save: jest.fn().mockResolvedValue(true) }; + Post.findById.mockResolvedValue(mockItem); + const result = await handleEdit('id', 'post', 'remove'); + expect(result).toEqual({ message: 'Edit removed successfully' }); + expect(mockItem.community_moderator_details.edit_history[0].removed_edit_flag).toBe(true); + expect(mockItem.community_moderator_details.unmoderated.any_action_taken).toBe(true); + }); + + it('should handle errors when fetching the item', async () => { + const errorMessage = 'Error fetching item'; + const item_id = 'item1'; + const item_type = 'post'; + const action = 'approve'; + + // Mock the Post model's findById method to throw an error + Post.findById.mockImplementation(() => { + throw new Error(errorMessage); + }); + + const result = await handleEdit(item_id, item_type, action); + + expect(result).toEqual({ err: { status: 500, message: errorMessage } }); + }); + + it('should handle errors when saving the item', async () => { + const errorMessage = 'Error saving item'; + const item_id = 'item1'; + const item_type = 'post'; + const action = 'approve'; + + // Mock the Post model's findById method to return an item + Post.findById.mockImplementation(() => { + return { + community_moderator_details: { + edit_history: [{ approved_edit_flag: false, removed_edit_flag: false }], + unmoderated: { any_action_taken: false }, + }, + save: () => { + throw new Error(errorMessage); + }, + }; + }); + + const result = await handleEdit(item_id, item_type, action); + + expect(result).toEqual({ err: { status: 500, message: `Error while saving the item after ${action}ing its edit: ${errorMessage}` } }); + }); +}); + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { handleUnmoderatedItem } from '../../src/services/communityQueueService.js'; + +jest.mock('../../src/db/models/Post.js'); +jest.mock('../../src/db/models/Comment.js'); + +describe('handleUnmoderatedItem', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return an error if the item is not found', async () => { + Post.findById.mockResolvedValue(null); + const result = await handleUnmoderatedItem('id', 'post', 'user1', 'approve'); + expect(result).toEqual({ err: { status: 404, message: 'Post not found' } }); + }); + + it('should return an error if the action is invalid', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { unmoderated: { any_action_taken: false } } }); + const result = await handleUnmoderatedItem('id', 'post', 'user1', 'invalid'); + expect(result).toEqual({ err: { status: 400, message: 'Invalid action' } }); + }); + + it('should return an error if the item is already approved or removed', async () => { + Post.findById.mockResolvedValue({ community_moderator_details: { unmoderated: { any_action_taken: true } } }); + const result = await handleUnmoderatedItem('id', 'post', 'user1', 'approve'); + expect(result).toEqual({ err: { status: 400, message: 'This item is already approved or removed' } }); + }); + + it('should approve the item successfully', async () => { + const mockItem = { + community_moderator_details: { + unmoderated: { + any_action_taken: false, + approved: { flag: false, by: null, date: null } + } + }, + moderator_details: { approved_flag: false, approved_by: null, approved_date: null }, + save: jest.fn().mockResolvedValue(true) + }; + Post.findById.mockResolvedValue(mockItem); + const result = await handleUnmoderatedItem('id', 'post', 'user1', 'approve'); + expect(result).toEqual({ message: 'Post approved successfully' }); + expect(mockItem.community_moderator_details.unmoderated.approved.flag).toBe(true); + expect(mockItem.community_moderator_details.unmoderated.approved.by).toBe('user1'); + expect(mockItem.community_moderator_details.unmoderated.any_action_taken).toBe(true); + }); + + it('should handle errors when saving the item', async () => { + const errorMessage = 'Error saving item'; + const item_id = 'item1'; + const item_type = 'post'; + const action = 'approve'; + + // Mock the Post model's findById method to return an item + Post.findById.mockImplementation(() => { + return { + community_moderator_details: { + edit_history: [{ approved_edit_flag: false, removed_edit_flag: false }], + unmoderated: { any_action_taken: false }, + }, + save: () => { + throw new Error(errorMessage); + }, + }; + }); + + const result = await handleEdit(item_id, item_type, action); + + expect(result).toEqual({ err: { status: 500, message: `Error while saving the item after ${action}ing its edit: ${errorMessage}` } }); + }); + + it('should handle unexpected errors', async () => { + const errorMessage = 'Unexpected error'; + const item_id = 'item1'; + const item_type = 'post'; + const objection_type = 'reported'; + const objected_by = 'user1'; + const objection_type_value = 'Harassment'; + const community_name = 'community1'; + + // Mock the Post model's findById method to throw an error + Post.findById.mockImplementation(() => { + throw new Error(errorMessage); + }); + + const result = await objectItem(item_id, item_type, objection_type, objected_by, objection_type_value, community_name); + + expect(result).toEqual({ err: { status: 500, message: errorMessage } }); + }); +}); + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { getItemsFromQueue } from '../../src/services/communityQueueService.js'; + +jest.mock('../../src/db/models/Post.js'); +jest.mock('../../src/db/models/Comment.js'); + +describe('getItemsFromQueue', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return an error if the time filter is invalid', async () => { + const result = await getItemsFromQueue('invalid', 'posts', 'reported', 'community1', {}, 1, 10); + expect(result).toEqual({ err: { status: 400, message: 'Invalid time filter' } }); + }); + + it('should return an error if the posts or comments value is invalid', async () => { + const result = await getItemsFromQueue('newest first', 'invalid', 'reported', 'community1', {}, 1, 10); + expect(result).toEqual({ err: { status: 400, message: 'Invalid posts or comments value' } }); + }); + + it('should return an error if the queue type is invalid', async () => { + const result = await getItemsFromQueue('newest first', 'posts', 'invalid', 'community1', {}, 1, 10); + expect(result).toEqual({ err: { status: 400, message: `Invalid queue type. Queue type must be either 'reported', 'removed', 'spammed', 'unmoderated' or 'edited' ` } }); + }); + + // Repeat the above pattern for the other queue types + ['removed', 'reported', 'unmoderated', 'edited'].forEach(queueType => { + it(`should handle "${queueType}" queue type`, async () => { + const mockUser = { upvotes_posts_ids: [], downvotes_posts_ids: [] }; + + const mockPosts = [{ _id: 'post1', _doc: { created_at: new Date() } }]; + const mockPostFind = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockResolvedValue(mockPosts), + }; + Post.find.mockReturnValue(mockPostFind); + + const result = await getItemsFromQueue('newest first', 'posts', queueType, 'community1', mockUser, 1, 10); + + expect(result).toEqual({ items: mockPosts.map(post => ({ ...post._doc, userVote: 'none' })) }); + }); + }); + + it('should add userVote attribute to each post', async () => { + const mockPosts = [ + { _id: 'post1', _doc: { created_at: new Date() } }, + { _id: 'post2', _doc: { created_at: new Date() } }, + { _id: 'post3', _doc: { created_at: new Date() } }, + ]; + const mockUser = { + upvotes_posts_ids: ['post1'], + downvotes_posts_ids: ['post2'], + }; + + const mockPostFind = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockResolvedValue(mockPosts), + }; + + Post.find.mockReturnValue(mockPostFind); + + const result = await getItemsFromQueue('newest first', 'posts', 'reported', 'community1', mockUser, 1, 10); + + expect(result).toEqual({ + items: [ + { ...mockPosts[0]._doc, userVote: 'up' }, + { ...mockPosts[1]._doc, userVote: 'down' }, + { ...mockPosts[2]._doc, userVote: 'none' }, + ], + }); + }); + + it('should return a 500 error if an unexpected error occurs', async () => { + const mockError = new Error('Unexpected error'); + + // Mock the Post.find and Comment.find methods to throw an error + Post.find = jest.fn(() => { + throw mockError; + }); + Comment.find = jest.fn(() => { + throw mockError; + }); + + const result = await getItemsFromQueue('newest first', 'posts', 'reported', 'community1', {}, 1, 10); + + // Check that the function correctly returns a 500 status code and the error message + expect(result).toEqual({ err: { status: 500, message: mockError.message } }); + }); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 72f0fa2..3f3c3b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "faker": "^5.5.3", "jest": "^29.7.0", "jest-fetch-mock": "^3.0.3", + "jsdoc": "^4.0.3", "jsonwebtoken": "^9.0.2", "moment-timezone": "^0.5.45", "mongodb": "^6.5.0", @@ -39,7 +40,8 @@ "@babel/node": "^7.23.9", "@babel/preset-env": "^7.24.3", "@faker-js/faker": "^8.4.1", - "@types/faker": "^6.6.9" + "@types/faker": "^6.6.9", + "jsdocs": "^0.0.1" } }, "node_modules/@ampproject/remapping": { @@ -2444,6 +2446,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz", @@ -2567,6 +2580,25 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==" + }, "node_modules/@types/node": { "version": "20.12.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", @@ -3046,6 +3078,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -3228,6 +3265,17 @@ } ] }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -3837,6 +3885,17 @@ "node": ">= 0.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6729,11 +6788,64 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, + "node_modules/jsdoc": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdocs": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsdocs/-/jsdocs-0.0.1.tgz", + "integrity": "sha512-XWCzq0PqgHUTgeHohvZdy0O5L69IOqPFt58w7jAmAEUMYU4cZtaCcN3iWUfwjq+fVFIkhCMMGSWFxd1vy1Hm3g==", + "dev": true, + "bin": { + "jsdocs": "bin/jsdocs" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6848,6 +6960,14 @@ "node": ">=0.10.0" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6869,6 +6989,14 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -6880,6 +7008,11 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -6972,6 +7105,52 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -7064,6 +7243,17 @@ "node": "*" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -8076,6 +8266,14 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -8253,6 +8451,14 @@ "node": ">=0.10.0" } }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -9094,6 +9300,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -9130,6 +9341,11 @@ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -9438,6 +9654,11 @@ } } }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 092e7f0..dcd74e4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js", - "test": "jest ./__test__", + "test": "jest ./__test__/community/communityQueue.test.js", "docs": "jsdoc -c src/config/jsdocsconfig.json", "seed": "node ./seeds/seed.js", "coverage": "jest --coverage" @@ -24,6 +24,7 @@ "faker": "^5.5.3", "jest": "^29.7.0", "jest-fetch-mock": "^3.0.3", + "jsdoc": "^4.0.3", "jsonwebtoken": "^9.0.2", "moment-timezone": "^0.5.45", "mongodb": "^6.5.0", @@ -45,6 +46,7 @@ "@babel/node": "^7.23.9", "@babel/preset-env": "^7.24.3", "@faker-js/faker": "^8.4.1", - "@types/faker": "^6.6.9" + "@types/faker": "^6.6.9", + "jsdocs": "^0.0.1" } } diff --git a/src/services/communityQueueService.js b/src/services/communityQueueService.js index 65e5967..20e9067 100644 --- a/src/services/communityQueueService.js +++ b/src/services/communityQueueService.js @@ -1,8 +1,28 @@ +/** + * @module community/queue/service + */ + import { Post } from '../db/models/Post.js'; import { Comment } from '../db/models/Comment.js'; import { Community } from '../db/models/Community.js'; //////////////////////////////////////////////////////////////////////////// Actions //////////////////////////////////////////////////////////////////////////// + +/** + * Objects an item by setting the appropriate flags and details in the database. + * + * @param {string} item_id - The UUID of the item to be objected. + * @param {string} item_type - The type of the item. Can be either 'post' or 'comment'. + * @param {string} objection_type - The type of the objection. Can be either 'reported', 'spammed', or 'removed'. + * @param {string} objected_by - The user who is objecting the item. This user must be a moderator of the community. + * @param {string} objection_type_value - The reason for the objection. Must be a valid reason based on the objection type. + * @param {string} community_name - The name of the community where the item is posted. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a message indicating the success. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const objectItem = async (item_id, item_type, objection_type, objected_by, objection_type_value, community_name) => { try { // Validate that the item_type is either post of comment @@ -86,6 +106,19 @@ const objectItem = async (item_id, item_type, objection_type, objected_by, objec } }; +/** + * Edits an item (post or comment) by updating its content in the database. + * + * @param {string} item_id - The UUID of the item to be edited. + * @param {string} item_type - The type of the item. Can be either 'post' or 'comment'. + * @param {string} new_content - The new content for the item. Must be a string. + * @param {Object} editing_user - The user who is editing the item. This user must be the author of the item. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a message indicating the success. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const editItem = async (item_id, item_type, new_content, editing_user) => { try { // Determine the model based on the item_type @@ -136,6 +169,20 @@ const editItem = async (item_id, item_type, new_content, editing_user) => { }; //////////////////////////////////////////////////////////////////////////// Handlers //////////////////////////////////////////////////////////////////////////// + +/** + * Handles an objection on an item (post or comment) by either approving or removing it. + * + * @param {string} item_id - The UUID of the item to handle the objection on. + * @param {string} item_type - The type of the item. Can be either 'post' or 'comment'. + * @param {string} objection_type - The type of the objection. Can be either 'reported', 'spammed', or 'removed'. + * @param {string} action - The action to take on the objection. Can be either 'approve' or 'remove'. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a message indicating the success. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const handleObjection = async (item_id, item_type, objection_type, action) => { try { // Determine the model based on the item_type @@ -167,11 +214,11 @@ const handleObjection = async (item_id, item_type, objection_type, action) => { // 3. Write a query object let updated_attributes = {}; if (action === 'approve') { - query = { + updated_attributes = { [`community_moderator_details.${objection_type}.confirmed`]: true, }; } else if (action === 'remove') { - query = { + updated_attributes = { [`community_moderator_details.${objection_type}.flag`]: false, [`community_moderator_details.${objection_type}.confirmed`]: false, @@ -199,6 +246,18 @@ const handleObjection = async (item_id, item_type, objection_type, action) => { } }; +/** + * Handles an edit on an item (post or comment) by either approving or removing it. + * + * @param {string} item_id - The UUID of the item to handle the edit on. + * @param {string} item_type - The type of the item. Can be either 'post' or 'comment'. + * @param {string} action - The action to take on the edit. Can be either 'approve' or 'remove'. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a message indicating the success. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const handleEdit = async (item_id, item_type, action) => { // Determine the model based on the item_type const Model = item_type === 'post' ? Post : Comment; @@ -246,6 +305,19 @@ const handleEdit = async (item_id, item_type, action) => { return { message: `Edit ${action}d successfully` }; }; +/** + * Handles an unmoderated item (post or comment) by either approving or removing it. + * + * @param {string} itemId - The UUID of the item to handle. + * @param {string} itemType - The type of the item. Can be either 'post' or 'comment'. + * @param {string} userId - The UUID of the user performing the action. + * @param {string} action - The action to take on the item. Can be either 'approve' or 'remove'. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a message indicating the success. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const handleUnmoderatedItem = async (itemId, itemType, userId, action) => { try { // Determine the model based on the item_type @@ -294,6 +366,22 @@ const handleUnmoderatedItem = async (itemId, itemType, userId, action) => { //////////////////////////////////////////////////////////////////////////// Pages //////////////////////////////////////////////////////////////////////////// +/** + * Fetches items (posts or comments) from a specified queue of a community. + * + * @param {string} time_filter - The order in which to return the items. Can be either 'newest first' or 'oldest first'. + * @param {string} posts_or_comments - The type of items to return. Can be either 'posts', 'comments', or 'posts and comments'. + * @param {string} queue_type - The type of queue from which to fetch the items. Can be either 'reported', 'removed', 'unmoderated' or 'edited'. + * @param {string} community_name - The name of the community from which to fetch the items. + * @param {Object} authenticated_user - The user who is fetching the items. This user must be a moderator of the community. + * @param {number} page - The page number to return in the pagination. + * @param {number} limit - The number of items to return per page. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains an 'items' property with the fetched items. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const getItemsFromQueue = async (time_filter, posts_or_comments, queue_type, community_name, authenticated_user, page, limit) => { try { // Validate the time_filter parameter. It should be either 'newest first' or 'oldest first'. @@ -371,15 +459,15 @@ const getItemsFromQueue = async (time_filter, posts_or_comments, queue_type, com return { ...post._doc, userVote }; }); - comments = comments.map(comment => { - let userVote = 'none'; - // if (authenticated_user.upvotes_comments_ids.includes(comment._id)) { - // userVote = 'up'; - // } else if (authenticated_user.downvotes_comments_ids.includes(comment._id)) { - // userVote = 'down'; - // } - return { ...comment._doc, userVote }; - }); + // comments = comments.map(comment => { + // let userVote = 'none'; + // if (authenticated_user.upvotes_comments_ids.includes(comment._id)) { + // userVote = 'up'; + // } else if (authenticated_user.downvotes_comments_ids.includes(comment._id)) { + // userVote = 'down'; + // } + // return { ...comment._doc, userVote }; + // }); // Merge and sort the posts and comments. This will create a single array of posts and comments, sorted by creation date. diff --git a/src/services/communityScheduledPostsService.js b/src/services/communityScheduledPostsService.js index 6d0dce1..9e3f033 100644 --- a/src/services/communityScheduledPostsService.js +++ b/src/services/communityScheduledPostsService.js @@ -1,3 +1,7 @@ +/** + * @module community/scheduled-posts/service + */ + // TODO: Scheduled posts appear in the unmoderated queue. import { Post } from "../db/models/Post.js"; import { scheduledPost } from "../db/models/scheduledPosts.js"; @@ -13,6 +17,17 @@ import { getCommunityGeneralSettings } from "./communitySettingsService.js"; import schedule from "node-schedule"; +/** + * Saves a post for future scheduling. + * + * @param {Object} scheduling_details - The details for when the post should be scheduled. + * @param {Object} postInput - The input for the post. This object should contain the necessary attributes for a post. + * @param {Object} user - The user who is creating the post. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a 'saved_post_id' property with the id of the saved post. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ const savePostForScheduling = async (scheduling_details, postInput, user) => { // Check that the input to create the new post is valid. @@ -96,6 +111,16 @@ const savePostForScheduling = async (scheduling_details, postInput, user) => { return { saved_post_id: savedPost._id }; } +/** + * Posts a scheduled post immediately and removes it from the scheduled posts if it is not recurring. + * + * @param {string} post_id - The id of the scheduled post to post. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a 'successMessage' property with a success message. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const postScheduledPost = async (post_id) => { // Find the scheduled post with the given post id. @@ -137,6 +162,14 @@ const postScheduledPost = async (post_id) => { return { successMessage: `Post with title ${post.title} posted successfully on ${post.created_at}!` }; } +/** + * Retrieves all scheduled posts for a given community and separates them into recurring and non-recurring posts. + * + * @param {string} community_name - The name of the community for which to retrieve the scheduled posts. + * + * @returns {Promise} - A promise that resolves to an object. The object contains two properties: 'recurring_posts' and 'non_recurring_posts'. Each property is an array of posts. + */ + const getScheduledPosts = async (community_name) => { // Find all the scheduled posts in the database excluding the 'moderator_details' field. @@ -150,6 +183,17 @@ const getScheduledPosts = async (community_name) => { return { recurring_posts, non_recurring_posts }; } +/** + * Edits the description of a scheduled post. + * + * @param {string} post_id - The id of the scheduled post to edit. + * @param {string} new_description - The new description for the scheduled post. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains an 'edited_post' property with the edited post. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const editScheduledPost = async (post_id, new_description) => { try { // Get the post with the given id. @@ -179,6 +223,16 @@ const editScheduledPost = async (post_id, new_description) => { } } +/** + * Submits a scheduled post immediately and removes it from the scheduled posts if it is not recurring. + * + * @param {string} post_id - The id of the scheduled post to submit. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a 'message' property with a success message. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + // submitScheduledPost can only be called with an id of a post from those in the scheduledPosts table and that are non recurring. // it should delete the post from the scheduledPosts table and post it to the posts table. // it should also cancel the scheduling of the post using job.cancel() @@ -226,6 +280,16 @@ const submitScheduledPost = async (post_id) => { return { message: `Post with title ${post.title} posted successfully on ${post.created_at}!` }; } +/** + * Cancels a scheduled post and removes it from the scheduled posts. + * + * @param {string} post_id - The id of the scheduled post to cancel. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a 'message' property with a success message. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const cancelScheduledPost = async (post_id) => { // Find the scheduled post with the given post id. const scheduled_post = await scheduledPost.findById(post_id); diff --git a/src/services/communitySettingsService.js b/src/services/communitySettingsService.js index c9bd659..945aeee 100644 --- a/src/services/communitySettingsService.js +++ b/src/services/communitySettingsService.js @@ -1,3 +1,7 @@ +/** + * @module community/settings/service + */ + // Mod Tools --> Settings --> General Settings // Mod Tools --> Settings --> Posts and Comments // Mod Tools --> Moderation --> Content Controls @@ -20,6 +24,17 @@ import { CommunityPostsAndComments } from "../db/models/communityPostsAndComment //////////////////////////////////////////////////////////////////////// Get Settings ////////////////////////////////////////////////////////////// + +/** + * Fetches the general settings of a specified community. + * + * @param {string} community_name - The name of the community from which to fetch the general settings. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a 'general_settings' property with the fetched general settings. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const getCommunityGeneralSettings = async (community_name) => { // This could be due to a bug in the front-end code that incorrectly formats the community_name. if (typeof community_name !== 'string') { @@ -51,6 +66,16 @@ const getCommunityGeneralSettings = async (community_name) => { } }; +/** + * Fetches the content controls of a specified community. + * + * @param {string} community_name - The name of the community from which to fetch the content controls. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a 'content_controls' property with the fetched content controls. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const getCommunityContentControls = async (community_name) => { if (typeof community_name !== 'string') { return { err: { status: 400, message: 'Invalid arguments' } }; @@ -75,6 +100,16 @@ const getCommunityContentControls = async (community_name) => { } }; +/** + * Fetches the posts and comments of a specified community. + * + * @param {string} community_name - The name of the community from which to fetch the posts and comments. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains a 'posts_and_comments' property with the fetched posts and comments. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const getCommunityPostsAndComments = async (community_name) => { if (typeof community_name !== 'string') { return { err: { status: 400, message: 'Invalid arguments' } }; @@ -104,6 +139,17 @@ const getCommunityPostsAndComments = async (community_name) => { // These functions can be optimised by populating the settings attribute (exactly like in the get functions above) then accessing the settings attribute directly of the returned community from the query. // TODO: Update these functions after finishing the Community Appearance feature. +/** + * Changes the general settings of a specified community. + * + * @param {string} community_name - The name of the community for which to change the general settings. + * @param {Object} general_settings - The new general settings for the community. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains an 'updated_general_settings' property with the updated general settings. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const changeCommunityGeneralSettings = async ( community_name, general_settings @@ -141,6 +187,17 @@ const changeCommunityGeneralSettings = async ( } }; +/** + * Changes the content controls of a specified community. + * + * @param {string} community_name - The name of the community for which to change the content controls. + * @param {Object} content_controls - The new content controls for the community. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains an 'updated_content_controls' property with the updated content controls. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const changeCommunityContentControls = async ( community_name, content_controls @@ -172,6 +229,17 @@ const changeCommunityContentControls = async ( } }; +/** + * Changes the posts and comments of a specified community. + * + * @param {string} community_name - The name of the community for which to change the posts and comments. + * @param {Object} posts_and_comments - The new posts and comments for the community. This object can have 'posts' and 'comments' properties. + * + * @returns {Promise} - A promise that resolves to an object. If the function is successful, the object contains an 'updated_posts_and_comments' property with the updated posts and comments. If an error occurs, the object contains an 'err' property with the status code and error message. + * + * @throws {Object} - If an error occurs, an object is thrown with an 'err' property containing the status code and error message. + */ + const changeCommunityPostsAndComments = async ( community_name, posts_and_comments