From a46773887f10690b7914a252ca939ae49e1c420e Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Tue, 2 May 2017 14:27:16 -0500 Subject: [PATCH] Initial working editComment mutation --- graph/mutators/comment.js | 47 ++++++++--- graph/resolvers/root_mutation.js | 3 + graph/typeDefs.graphql | 14 ++++ models/comment.js | 18 ++++ services/comments.js | 48 +++++++++-- test/server/graph/mutations/editComment.js | 95 ++++++++++++++++++++++ test/server/graph/mutations/ignoreUser.js | 1 - 7 files changed, 208 insertions(+), 18 deletions(-) create mode 100644 test/server/graph/mutations/editComment.js diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 5cfc23794..cf5382d4f 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -55,11 +55,11 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = /** * Filters the comment object and outputs wordlist results. - * @param {Object} context graphql context - * @param {String} body body of a comment + * @param {String} body body of a comment + * @param {String} [asset_id] id of asset comment is posted on * @return {Object} resolves to the wordlist results */ -const filterNewComment = (context, {body, asset_id}) => { +const filterNewComment = ({body, asset_id}) => { // Create a new instance of the Wordlist. const wl = new Wordlist(); @@ -67,20 +67,19 @@ const filterNewComment = (context, {body, asset_id}) => { // Load the wordlist and filter the comment content. return Promise.all([ wl.load().then(() => wl.scan('body', body)), - AssetsService.rectifySettings(AssetsService.findById(asset_id)) + asset_id && AssetsService.rectifySettings(AssetsService.findById(asset_id)) ]); }; /** * This resolves a given comment's status to take into account moderator actions * are applied. - * @param {Object} context graphql context * @param {String} body body of the comment - * @param {String} asset_id asset for the comment + * @param {String} [asset_id] asset for the comment * @param {Object} [wordlist={}] the results of the wordlist scan * @return {Promise} resolves to the comment's status */ -const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}, settings) => { +const resolveNewCommentStatus = ({asset_id, body}, wordlist = {}, settings = {}) => { // Decide the status based on whether or not the current asset/settings // has pre-mod enabled or not. If the comment was rejected based on the @@ -92,7 +91,7 @@ const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}, setti status = Promise.resolve('REJECTED'); } else if (settings.premodLinksEnable && linkify.test(body)) { status = Promise.resolve('PREMOD'); - } else { + } else if (asset_id) { status = AssetsService .rectifySettings(AssetsService.findById(asset_id).then((asset) => { if (!asset) { @@ -119,6 +118,8 @@ const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}, setti } return moderation === 'PRE' ? 'PREMOD' : 'NONE'; }); + } else { + status = 'NONE'; } return status; @@ -136,12 +137,12 @@ const createPublicComment = (context, commentInput) => { // First we filter the comment contents to ensure that we note any validation // issues. - return filterNewComment(context, commentInput) + return filterNewComment(commentInput) // We then take the wordlist and the comment into consideration when // considering what status to assign the new comment, and resolve the new // status to set the comment to. - .then(([wordlist, settings]) => resolveNewCommentStatus(context, commentInput, wordlist, settings) + .then(([wordlist, settings]) => resolveNewCommentStatus(commentInput, wordlist, settings) // Then we actually create the comment with the new status. .then((status) => createComment(context, commentInput, status)) @@ -213,13 +214,32 @@ const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { /** * Removes a tag from a Comment - * @param {String} id identifier of the comment (uuid) + * @param {String} id identifier of the comment (uuid) * @param {String} tag name of the tag */ const removeCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { return CommentsService.removeTag(id, tag); }; +/** + * Edit a Comment + * @param {String} id identifier of the comment (uuid) + * @param {Object} edit describes how to edit the comment + * @param {String} edit.body the new Comment body + */ +const editComment = async ({user, loaders: {Comments}}, {id, edit}) => { + const {body} = edit; + const comment = await CommentsService.findById(id); + const {asset_id} = comment; + const determineStatusForComment = async ({body, asset_id}) => { + const [wordlist, settings] = await filterNewComment({asset_id, body}); + const status = await resolveNewCommentStatus({asset_id, body}, wordlist, settings); + return status; + }; + const status = await determineStatusForComment({body, asset_id}); + await CommentsService.edit(id, Object.assign({status}, edit)); +}; + module.exports = (context) => { let mutators = { Comment: { @@ -227,6 +247,7 @@ module.exports = (context) => { setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized), addCommentTag: () => Promise.reject(errors.ErrNotAuthorized), removeCommentTag: () => Promise.reject(errors.ErrNotAuthorized), + editComment: () => Promise.reject(errors.ErrNotAuthorized), } }; @@ -246,5 +267,9 @@ module.exports = (context) => { mutators.Comment.removeCommentTag = (action) => removeCommentTag(context, action); } + if (context.user) { + mutators.Comment.editComment = (action) => editComment(context, action); + } + return mutators; }; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 661cc9f96..6a91ec3f2 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -5,6 +5,9 @@ const RootMutation = { createComment(_, {comment}, {mutators: {Comment}}) { return wrapResponse('comment')(Comment.create(comment)); }, + editComment(_, args, {mutators: {Comment}}) { + return wrapResponse(null)(Comment.editComment(args)); + }, createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) { return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'})); }, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 32c2c16c2..a88baa2e8 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -780,6 +780,17 @@ type StopIgnoringUserResponse implements Response { errors: [UserError] } +# Input to editComment mutation +input EditCommentInput { + # Update body of the comment + body: String! +} + +type EditCommentResponse implements Response { + # An array of errors relating to the mutation that occured. + errors: [UserError] +} + # All mutations for the application are defined on this object. type RootMutation { @@ -798,6 +809,9 @@ type RootMutation { # Delete an action based on the action id. deleteAction(id: ID!): DeleteActionResponse + # Edit a comment + editComment(id: ID!, edit: EditCommentInput): EditCommentResponse + # Sets User status. Requires the `ADMIN` role. setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse diff --git a/models/comment.js b/models/comment.js index 0984a0832..68a8f7526 100644 --- a/models/comment.js +++ b/models/comment.js @@ -51,6 +51,23 @@ const TagSchema = new Schema({ _id: false }); +/** + * A record of old body values for a Comment + */ +const BodyHistoryItemSchema = new Schema({ + body: { + required: true, + type: String, + }, + + // datetime until the comment body value was this.body + created_at: { + required: true, + type: Date, + default: Date, + } +}); + /** * The Mongo schema for a Comment. * @type {Schema} @@ -66,6 +83,7 @@ const CommentSchema = new Schema({ required: [true, 'The body is required.'], minlength: 2 }, + body_history: [BodyHistoryItemSchema], asset_id: String, author_id: String, status_history: [StatusSchema], diff --git a/services/comments.js b/services/comments.js index d77118705..e505251a4 100644 --- a/services/comments.js +++ b/services/comments.js @@ -33,16 +33,52 @@ module.exports = class CommentsService { status = 'NONE', } = comment; - comment.status_history = status ? [{ - type: status, - created_at: new Date() - }] : []; - - let commentModel = new CommentModel(comment); + const commentModel = new CommentModel(Object.assign({ + status_history: status ? [{ + type: status, + created_at: new Date() + }] : [], + body_history: [{ + body: comment.body, + created_at: new Date() + }] + }, comment)); return commentModel.save(); } + /** + * Edit a Comment + * @param {String} body the new Comment body + * @param {String} status the new Comment status + */ + static async edit(id, {body, status}) { + if (status && ! STATUSES.includes(status)) { + throw new Error(`status ${status} is not supported`); + } + const {nModified} = await CommentModel.update({id}, { + $set: { + body, + status, + }, + $push: { + body_history: { + body, + created_at: new Date(), + }, + status_history: { + type: status, + created_at: new Date(), + } + }, + }); + switch (nModified) { + case 0: + throw new Error(`Couldn't edit comment. There is no Comment with id "${id}"`); + } + + } + /** * Adds a tag if it doesn't already exist on the comment. * @throws if tag is already added to the comment diff --git a/test/server/graph/mutations/editComment.js b/test/server/graph/mutations/editComment.js new file mode 100644 index 000000000..9fdceb1f1 --- /dev/null +++ b/test/server/graph/mutations/editComment.js @@ -0,0 +1,95 @@ +const expect = require('chai').expect; +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UsersService = require('../../../../services/users'); +const AssetModel = require('../../../../models/asset'); +const SettingsService = require('../../../../services/settings'); +const CommentsService = require('../../../../services/comments'); + +describe('graph.mutations.editComment', () => { + let asset; + let user; + beforeEach(async () => { + await SettingsService.init(); + asset = await AssetModel.create({}); + user = await UsersService.createLocalUser( + 'usernameA@example.com', 'password', 'usernameA'); + }); + afterEach(async () => { + await asset.remove(); + await user.remove(); + }); + + const editCommentMutation = ` + mutation EditComment ($id: ID!, $edit: EditCommentInput) { + editComment(id:$id, edit:$edit) { + errors { + translation_key + } + } + } + `; + + // const queryCommentById = ` + // query commentQuery($id: ID!) { + // comment(id: $id) { + // body, + // status + // } + // } + // `; + + it('a user can edit their own comment', async () => { + const context = new Context({user}); + const testStartedAt = new Date(); + const comment = await CommentsService.publicCreate({ + asset_id: asset.id, + author_id: user.id, + body: `hello there! ${ String(Math.random()).slice(2)}`, + }); + + // body_history should be there + expect(comment.body_history.length).to.equal(1); + expect(comment.body_history[0].body).to.equal(comment.body); + expect(comment.body_history[0].created_at).to.be.instanceOf(Date); + expect(comment.body_history[0].created_at).to.be.at.least(testStartedAt); + + // now edit + const newBody = 'I have been edited.'; + const response = await graphql(schema, editCommentMutation, {}, context, { + id: comment.id, + edit: { + body: newBody + } + }); + if (response.errors && response.errors.length) { + console.error(response.errors); + } + expect(response.errors).to.be.empty; + + // assert body has changed + const commentAfterEdit = await CommentsService.findById(comment.id); + expect(commentAfterEdit.body).to.equal(newBody); + expect(commentAfterEdit.body_history).to.be.instanceOf(Array); + expect(commentAfterEdit.body_history.length).to.equal(2); + expect(commentAfterEdit.body_history[1].body).to.equal(newBody); + expect(commentAfterEdit.body_history[1].created_at).to.be.instanceOf(Date); + expect(commentAfterEdit.body_history[1].created_at).to.be.at.least(testStartedAt); + expect(commentAfterEdit.status).to.equal('NONE'); + }); + + /** + Server: When an Edit is sent to the server + -- The (old) comment.body and (current) timestamp are pushed onto the comment.body_history array. + -- The status is set to the same status as if the comment is posted for the first time.* + -- The body of the comment is updated. + */ + // user can't edit outside of edit window + // can't edit comment id that doesn't exist + // user cant edit comments by others + + // should BANNED users be able to edit their comments? + +}); diff --git a/test/server/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js index 54d6305a1..c2d9ebdd6 100644 --- a/test/server/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -30,7 +30,6 @@ describe('graph.mutations.ignoreUser', () => { await SettingsService.init(); }); - // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB');