From 2e1bc8d75a2b21f3a8ffac41653ae78ede367c6a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 9 May 2017 15:01:01 -0600 Subject: [PATCH] Adjusted query/tag flow --- graph/mutators/comment.js | 155 ++++++++++++++++-- graph/resolvers/index.js | 4 + graph/resolvers/root_mutation.js | 11 +- graph/resolvers/tag.js | 5 + graph/resolvers/tag_link.js | 9 + graph/typeDefs.graphql | 36 ++-- models/action.js | 13 +- models/comment.js | 44 +---- models/enum/action_types.js | 4 + models/enum/comment_status.js | 6 + models/enum/item_types.js | 5 + models/enum/moderation_options.js | 4 + models/enum/user_roles.js | 4 + models/enum/user_status.js | 6 + models/schema/tag.js | 53 ++++++ models/schema/tag_link.js | 31 ++++ models/setting.js | 43 +---- models/user.js | 43 +---- services/comments.js | 122 +++++--------- services/tags.js | 5 + services/users.js | 4 +- test/server/graph/mutations/addCommentTag.js | 29 ++-- test/server/graph/mutations/createComment.js | 6 +- .../graph/mutations/removeCommentTag.js | 68 ++++---- test/server/services/comments.js | 109 +++++++----- 25 files changed, 476 insertions(+), 343 deletions(-) create mode 100644 graph/resolvers/tag_link.js create mode 100644 models/enum/action_types.js create mode 100644 models/enum/comment_status.js create mode 100644 models/enum/item_types.js create mode 100644 models/enum/moderation_options.js create mode 100644 models/enum/user_roles.js create mode 100644 models/enum/user_status.js create mode 100644 models/schema/tag.js create mode 100644 models/schema/tag_link.js create mode 100644 services/tags.js diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 3413c6f4e..3a57f3b3a 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -18,18 +18,25 @@ const Wordlist = require('../../services/wordlist'); */ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null}, status = 'NONE') => { + // Add the staff tag for comments created as a staff member. + let tags = []; + if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + tags = [{ + tag: { + name: 'STAFF' + } + }]; + } + return CommentsService.publicCreate({ body, asset_id, parent_id, status, + tags, author_id: user.id }) - .then(async (comment) => { - - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user.id); - } + .then((comment) => { // If the loaders are present, clear the caches for these values because we // just added a new comment, hence the counts should be updated. We should @@ -204,21 +211,139 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { }; /** - * Adds a tag to a Comment - * @param {String} id identifier of the comment (uuid) - * @param {String} tag name of the tag + * Adds a tag to a Comment. */ -const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.addTag(id, tag, user.id); +const addCommentTag = async ({user, loaders: {Assets, Comments, Settings}}, {id, asset_id, name}) => { + + // If the current user does not have the ADMIN or MODERATOR role + if (!user || !user.can('mutation:addCommentTag')) { + throw errors.ErrNotAuthorized; + } + + // Load the settings from the dataloader, it will be cached if it was + // retrieved already. + let settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id)); + + // Try to find the tag in the asset tags. This will contain the permission + // information. + let tag = settings.tags.find((globalTag) => { + return globalTag.name === name && globalTag.models.include('COMMENTS'); + }); + + // Create the new tagLink that will be created to attach to the comment. + let tagLink = { + tag, + assigned_by: user.id, + created_at: new Date() + }; + + // If the tag was found, we need to ensure that the current user can indeed + // set this tag on the comment. + if (tag) { + + // If the tag has roles defined, and the current user has at least one of + // the required roles, then add the tag without checking for ownership. + if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { + return CommentsService.addTag(id, tagLink, false); + } + + // If the permissions allow for self assignment, then ensure that the query + // is compose with that in mind. + if (tag.permissions && tag.permissions.self) { + + // Otherwise, we assume that we have to check to see that the user indeed + // owns the resource before allowing the tag to get added. + return CommentsService.addTag(id, tagLink, true); + } + + throw errors.ErrNotAuthorized; + } + + // Only admin/moderators can add unique tags, these are tags that are not in + // the global list. + if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { + throw errors.ErrNotAuthorized; + } + + // Generate the tag in the event now that we have to create the tag for this + // specific comment. + tagLink.tag = { + name, + permissions: { + public: true, + self: false, + roles: [] + }, + models: ['COMMENTS'], + created_at: new Date() + }; + + // Actually attach the new tag that we created to the comment. + return CommentsService.addTag(id, tagLink, false); }; /** - * Removes a tag from a Comment - * @param {String} id identifier of the comment (uuid) - * @param {String} tag name of the tag + * Removes a tag from a Comment. */ -const removeCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.removeTag(id, tag); +const removeCommentTag = async ({user, loaders: {Comments}}, {id, asset_id, name}) => { + + // If the current user does not have the ADMIN or MODERATOR role + if (!user || !user.can('mutation:removeCommentTag')) { + throw errors.ErrNotAuthorized; + } + + // Load the settings from the dataloader, it will be cached if it was + // retrieved already. + let settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id)); + + // Try to find the tag in the asset tags. This will contain the permission + // information. + let tag = settings.tags.find((globalTag) => { + return globalTag.name === name && globalTag.models.include('COMMENTS'); + }); + + // Create the new tagLink that will be created to attach to the comment. + let tagLink = { + tag, + assigned_by: user.id + }; + + // If the tag was found, we need to ensure that the current user can indeed + // remove this tag on the comment. + if (tag) { + + // If the tag has roles defined, and the current user has at least one of + // the required roles, then remove the tag without checking for ownership. + if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { + return CommentsService.removeTag(id, tagLink, false); + } + + // If the permissions allow for self assignment, then ensure that the query + // is compose with that in mind. + if (tag.permissions && tag.permissions.self) { + + // Otherwise, we assume that we have to check to see that the user indeed + // owns the resource before allowing the tag to get removed. + return CommentsService.removeTag(id, tagLink, true); + } + + throw errors.ErrNotAuthorized; + } + + // Only admin/moderators can remove unique tags, these are tags that are not + // in the global list. + if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { + throw errors.ErrNotAuthorized; + } + + // Generate the tag in the event now that we have to create the tag for this + // specific comment. + tagLink.tag = { + name + }; + + // Actually attach the new tag that we created to the comment. + return CommentsService.removeTag(id, tagLink, false); }; module.exports = (context) => { diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js index cbeb6dbf9..f2707bcc8 100644 --- a/graph/resolvers/index.js +++ b/graph/resolvers/index.js @@ -16,6 +16,8 @@ const RootMutation = require('./root_mutation'); const RootQuery = require('./root_query'); const Settings = require('./settings'); const Subscription = require('./subscription'); +const TagLink = require('./tag_link'); +const Tag = require('./tag'); const UserError = require('./user_error'); const User = require('./user'); const ValidationUserError = require('./validation_user_error'); @@ -39,6 +41,8 @@ let resolvers = { RootQuery, Settings, Subscription, + TagLink, + Tag, UserError, User, ValidationUserError, diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 5b60fbaa8..442845bd2 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -1,5 +1,4 @@ const wrapResponse = require('../helpers/response'); -const CommentsService = require('../../services/comments'); const RootMutation = { createComment(_, {comment}, {mutators: {Comment}}) { @@ -29,12 +28,12 @@ const RootMutation = { setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, - addCommentTag(_, {id, tag}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.addCommentTag({id, tag}).then(() => CommentsService.findById(id))); - }, - removeCommentTag(_, {id, tag}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.removeCommentTag({id, tag}).then(() => CommentsService.findById(id))); + addCommentTag(_, {id, asset_id, name}, {mutators: {Comment}}) { + return wrapResponse('comment')(Comment.addCommentTag({id, asset_id, name})); }, + removeCommentTag(_, {id, asset_id, name}, {mutators: {Comment}}) { + return wrapResponse('comment')(Comment.removeCommentTag({id, asset_id, name})); + } }; module.exports = RootMutation; diff --git a/graph/resolvers/tag.js b/graph/resolvers/tag.js index e69de29bb..e1cd7922d 100644 --- a/graph/resolvers/tag.js +++ b/graph/resolvers/tag.js @@ -0,0 +1,5 @@ +const Tag = { + +}; + +module.exports = Tag; diff --git a/graph/resolvers/tag_link.js b/graph/resolvers/tag_link.js new file mode 100644 index 000000000..5c0d39587 --- /dev/null +++ b/graph/resolvers/tag_link.js @@ -0,0 +1,9 @@ +const TagLink = { + assigned_by({assigned_by}, _, {user, loaders: {Users}}) { + if (user && user.hasRole('ADMIN')) { + return Users.getByID.load(assigned_by); + } + } +}; + +module.exports = TagLink; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 781b3b328..bbe3e4d17 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -52,14 +52,30 @@ type User { status: USER_STATUS } +# Tag represents the underlying Tag that can be either stored in a global list +# or added uniquely to the entity. type Tag { - # the actual tag for the comment. - id: String! - # the user that assigned the tag. If NULL then the system automatically tagged it. - added_by: String + # The actual name of the tag entry. + name: String! - # the time when the tag was assigned. + # The time that this Tag was created. + created_at: Date! +} + +# TagLink is used to associate a given Tag with a Model via a TagLink. +type TagLink { + + # The underlying Tag that is either duplicated from the global list or created + # uniquely for this specific model. + tag: Tag! + + # The user that assigned the tag. This TagLink could have been created by the + # system, in which case this will be null. It could also be null if the + # current user is not an Admin/Moderator. + assigned_by: User + + # The date that the TagLink was created. created_at: Date! } @@ -176,7 +192,7 @@ type Comment { body: String! # the tags on the comment - tags: [Tag] + tags: [TagLink!] # the user who authored the comment. user: User @@ -702,15 +718,15 @@ type SetCommentStatusResponse implements Response { # Response to addCommentTag mutation type AddCommentTagResponse implements Response { + # An array of errors relating to the mutation that occured. - comment: Comment errors: [UserError] } # Response to removeCommentTag mutation type RemoveCommentTagResponse implements Response { + # An array of errors relating to the mutation that occured. - comment: Comment errors: [UserError] } @@ -751,10 +767,10 @@ type RootMutation { setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse # Add tag to comment. - addCommentTag(id: ID!, tag: String!): AddCommentTagResponse + addCommentTag(id: ID!, asset_id: ID!, name: String!): AddCommentTagResponse # Remove tag from comment. - removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse + removeCommentTag(id: ID!, asset_id: ID!, name: String!): RemoveCommentTagResponse # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse diff --git a/models/action.js b/models/action.js index 36d83574e..8aa7322e3 100644 --- a/models/action.js +++ b/models/action.js @@ -1,17 +1,8 @@ const mongoose = require('../services/mongoose'); const uuid = require('uuid'); const Schema = mongoose.Schema; - -const ACTION_TYPES = [ - 'LIKE', - 'FLAG' -]; - -const ITEM_TYPES = [ - 'ASSETS', - 'COMMENTS', - 'USERS' -]; +const ACTION_TYPES = require('./enum/action_types'); +const ITEM_TYPES = require('./enum/item_types'); const ActionSchema = new Schema({ id: { diff --git a/models/comment.js b/models/comment.js index 219ec055d..e3443376b 100644 --- a/models/comment.js +++ b/models/comment.js @@ -1,13 +1,8 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; +const TagLinkSchema = require('./schema/tag_link'); const uuid = require('uuid'); - -const STATUSES = [ - 'ACCEPTED', - 'REJECTED', - 'PREMOD', - 'NONE' -]; +const COMMENT_STATUS = require('./enum/comment_status'); /** * The Mongo schema for a Comment Status. @@ -16,7 +11,7 @@ const STATUSES = [ const StatusSchema = new Schema({ type: { type: String, - enum: STATUSES, + enum: COMMENT_STATUS, }, // The User ID of the user that assigned the status. @@ -30,29 +25,6 @@ const StatusSchema = new Schema({ _id: false }); -/** - * The Mongo schema for a Comment Tag. - * @type {Schema} - */ -const TagSchema = new Schema({ - - // This is the actual 'tag' and we only permit tags that are in Setting.tags. - id: String, - - // The User ID of the user that added the tag. - added_by: { - type: String, - default: null - }, - - created_at: { - type: Date, - default: Date - } -}, { - _id: false -}); - /** * The Mongo schema for a Comment. * @type {Schema} @@ -73,12 +45,11 @@ const CommentSchema = new Schema({ status_history: [StatusSchema], status: { type: String, - enum: STATUSES, + enum: COMMENT_STATUS, default: 'NONE' }, parent_id: String, - - tags: [TagSchema], + tags: [TagLinkSchema], // Additional metadata stored on the field. metadata: { @@ -92,10 +63,9 @@ const CommentSchema = new Schema({ } }); -// Add the indexes on the comment tag. +// Add the indexes for the id of the comment. CommentSchema.index({ - 'id': 1, - 'tags.id': 1 + 'id': 1 }, { unique: true, background: false diff --git a/models/enum/action_types.js b/models/enum/action_types.js new file mode 100644 index 000000000..3a27d3b85 --- /dev/null +++ b/models/enum/action_types.js @@ -0,0 +1,4 @@ +module.exports = [ + 'LIKE', + 'FLAG' +]; diff --git a/models/enum/comment_status.js b/models/enum/comment_status.js new file mode 100644 index 000000000..64633e9de --- /dev/null +++ b/models/enum/comment_status.js @@ -0,0 +1,6 @@ +module.exports = [ + 'ACCEPTED', + 'REJECTED', + 'PREMOD', + 'NONE' +]; diff --git a/models/enum/item_types.js b/models/enum/item_types.js new file mode 100644 index 000000000..e95aff82d --- /dev/null +++ b/models/enum/item_types.js @@ -0,0 +1,5 @@ +module.exports = [ + 'ASSETS', + 'COMMENTS', + 'USERS' +]; diff --git a/models/enum/moderation_options.js b/models/enum/moderation_options.js new file mode 100644 index 000000000..0331d59e0 --- /dev/null +++ b/models/enum/moderation_options.js @@ -0,0 +1,4 @@ +module.exports = [ + 'PRE', + 'POST' +]; diff --git a/models/enum/user_roles.js b/models/enum/user_roles.js new file mode 100644 index 000000000..0711adcd7 --- /dev/null +++ b/models/enum/user_roles.js @@ -0,0 +1,4 @@ +module.exports = [ + 'ADMIN', + 'MODERATOR' +]; diff --git a/models/enum/user_status.js b/models/enum/user_status.js new file mode 100644 index 000000000..cec930904 --- /dev/null +++ b/models/enum/user_status.js @@ -0,0 +1,6 @@ +module.exports = [ + 'ACTIVE', + 'BANNED', + 'PENDING', + 'APPROVED' // Indicates that the users' username has been approved +]; diff --git a/models/schema/tag.js b/models/schema/tag.js new file mode 100644 index 000000000..2d97185a0 --- /dev/null +++ b/models/schema/tag.js @@ -0,0 +1,53 @@ +const mongoose = require('../../services/mongoose'); +const Schema = mongoose.Schema; + +const ITEM_TYPES = require('../enum/item_types'); +const USER_ROLES = require('../enum/user_roles'); + +/** + * The Mongo schema for a Tag. + * @type {Schema} + */ +const TagSchema = new Schema({ + + // The actual name of the tag. + name: String, + + // Contains permission data. + permissions: { + + // Determines if this tag is public or not. + public: { + type: Boolean, + default: true + }, + + // Determines if the owner of the Model can add/remove this tag on their own + // resources. + self: Boolean, + + // Determines other roles that are allowed to set this tag on other + // resources. + roles: [{ + type: String, + enum: USER_ROLES, + default: [] + }] + }, + + // A list of all the model types that this tag can be added to. + models: [{ + type: String, + enum: ITEM_TYPES + }], + + // The date for when the tag was created. + created_at: { + type: Date, + default: Date + } +}, { + _id: false +}); + +module.exports = TagSchema; diff --git a/models/schema/tag_link.js b/models/schema/tag_link.js new file mode 100644 index 000000000..1cc1dbd08 --- /dev/null +++ b/models/schema/tag_link.js @@ -0,0 +1,31 @@ +const mongoose = require('../../services/mongoose'); +const Schema = mongoose.Schema; +const TagSchema = require('./tag'); + +/** + * The Mongo schema for linking a Tag to a Model. + * @type {Schema} + */ +const TagLinkSchema = new Schema({ + + // A deep copy of the tag that is the origin for this link. If the ID matches + // with existing tags in the global/asset context then content will be + // substituted. + tag: TagSchema, + + // The User ID of the user that assigned the status. + assigned_by: { + type: String, + default: null + }, + + // The date when the tag was added to the model. + created_at: { + type: Date, + default: Date + } +}, { + _id: false +}); + +module.exports = TagLinkSchema; diff --git a/models/setting.js b/models/setting.js index 44f5ad5ca..aee266054 100644 --- a/models/setting.js +++ b/models/setting.js @@ -1,45 +1,7 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; - -const PERMISSIONS = [ - 'ADMIN' -]; - -/** - * The Mongo schema for a Tag. - * @type {Schema} - */ -const TagSchema = new Schema({ - id: { - type: String, - unique: true, - default: 'STAFF' - }, - public: Boolean, - text: { - type: Schema.Types.Mixed, - default: null - }, - permissions: [{ - type: String, - enum: PERMISSIONS, - default: 'ADMIN' - }], - models: [String], - - // Additional metadata stored on the field. - metadata: Schema.Types.Mixed -}, { - timestamps: { - createdAt: 'created_at', - updatedAt: 'updated_at' - } -}); - -const MODERATION_OPTIONS = [ - 'PRE', - 'POST' -]; +const TagSchema = require('./schema/tag'); +const MODERATION_OPTIONS = require('./enum/moderation_options'); /** * SettingSchema manages application settings that get used on front and backend. @@ -156,4 +118,3 @@ SettingSchema.method('merge', function(src) { const Setting = mongoose.model('Setting', SettingSchema); module.exports = Setting; -module.exports.MODERATION_OPTIONS = MODERATION_OPTIONS; diff --git a/models/user.js b/models/user.js index 688c37c2c..b61ddd6eb 100644 --- a/models/user.js +++ b/models/user.js @@ -4,41 +4,10 @@ const Schema = mongoose.Schema; const uuid = require('uuid'); // USER_ROLES is the array of roles that is permissible as a user role. -const USER_ROLES = [ - 'ADMIN', - 'MODERATOR' -]; +const USER_ROLES = require('./enum/user_roles'); // USER_STATUS is the list of statuses that are permitted for the user status. -const USER_STATUS = [ - 'ACTIVE', - 'BANNED', - 'PENDING', - 'APPROVED' // Indicates that the users' username has been approved -]; - -// /** -// * The Mongo schema for a User Tag. -// * @type {Schema} -// */ -// const TagSchema = new Schema({ -// -// // This is the actual 'tag' and we only permit tags that are in Setting.tags. -// id: String, -// -// // The User ID of the user that added the tag. -// added_by: { -// type: String, -// default: null -// }, -// -// created_at: { -// type: Date, -// default: Date -// } -// }, { -// _id: false -// }); +const USER_STATUS = require('./enum/user_status'); // ProfileSchema is the mongoose schema defined as the representation of a // User's profile stored in MongoDB. @@ -245,12 +214,6 @@ UserSchema.method('can', function(...actions) { return false; } - // {add,remove}CommentTag - requires admin and/or moderator role - const userCanModifyTags = user => ['ADMIN', 'MODERATOR'].some(r => user.hasRoles(r)); - if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag', 'mutation:removeUserTag', 'mutation: addUserTag'].includes(a)) && ! userCanModifyTags(this)) { - return false; - } - return true; }); @@ -258,5 +221,3 @@ UserSchema.method('can', function(...actions) { const UserModel = mongoose.model('User', UserSchema); module.exports = UserModel; -module.exports.USER_ROLES = USER_ROLES; -module.exports.USER_STATUS = USER_STATUS; diff --git a/services/comments.js b/services/comments.js index e1873ba02..0ef15c552 100644 --- a/services/comments.js +++ b/services/comments.js @@ -1,23 +1,7 @@ const CommentModel = require('../models/comment'); - const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const SettingModel = require('../models/setting'); -const SettingsService = require('./settings'); -const UsersService = require('./users'); - -const errors = require('../errors'); - -const STATUSES = [ - 'ACCEPTED', - 'REJECTED', - 'PREMOD', - 'NONE', -]; - -const ALLOWED_TAGS = [ - 'STAFF' -]; +const COMMENT_STATUS = require('../models/enum/comment_status'); module.exports = class CommentsService { @@ -47,82 +31,52 @@ module.exports = class CommentsService { return commentModel.save(); } - /** - * Adds a tag if it doesn't already exist on the comment. - * @throws if tag is already added to the comment - * @throws if tag name is not in ALLOWED_TAGS - * @param {String} id the id of the comment to tag - * @param {String} name the name of the tag to add - * @param {String} added_by the user id for the user who added the tag - */ - static addTag(id, name, added_by) { + static addTag(id, tagLink, verifyOwnership = false) { - // Check that the tag is allowed by the system OR in our setting.tags. - return SettingsService.retrieve() - .then((settings) => { + // Compose the query to find the comment. + const query = { + id, + 'tags.tag.name': { + $ne: tagLink.tag.name + } + }; - UsersService.findById(added_by) - .then((user) => { + // If ownership verification is required, ensure that the person that is + // assigning the tag is the same person that owns the comment. + if (verifyOwnership) { + query['author_id'] = tagLink.assigned_by; + } - // Moderators or ADMIN can add any tag automatically. - if (user != null && (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { - SettingModel.findOneAndUpdate({id: settings.id}, { - $push: { - tags: { - id: name, - models: ['COMMENTS'] - } - } - }); - } - else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { - return Promise.reject(errors.ErrTagNotAllowed); - } - }); - - return CommentModel.findOneAndUpdate({id, 'tags.id': {$ne: name}}, { - $push: { - tags: { - id: name, - added_by: added_by - } - }, - }) - .then(({nModified}) => { - switch (nModified) { - case 0: - return Promise.reject(errors.ErrNoCommentFound); - case 1: - return; - default: - } - }); + return CommentModel.update(query, { + $push: { + tags: tagLink + } }); } - /** - * Removes a tag from a comment - * @throws if the tag is not on the comment - * @param {String} id the id of the comment to tag - * @param {String} tag_id the id of the tag to remove - */ - static removeTag(id, tag_id) { - return CommentModel.findOneAndUpdate({id, 'tags.id': tag_id}, { + static removeTag(id, {tag: {name}, assigned_by}, verifyOwnership = false) { + + // Compose the query to find the comment that has the id: `id` and a tag + // that has the name: `name`. + const query = { + id, + 'tags.tag.name': { + $eq: name + } + }; + + // If ownership verification is required, ensure that the person that is + // assigning the tag is the same person that owns the comment. + if (verifyOwnership) { + query['author_id'] = assigned_by; + } + + return CommentModel.update(query, { $pull: { tags: { - id: tag_id + name } } - } - ) - .then(({nModified}) => { - switch(nModified) { - case 0: - return Promise.reject(errors.ErrNoCommentFound); - case 1: - return; - default: - } }); } @@ -226,7 +180,7 @@ module.exports = class CommentsService { static pushStatus(id, status, assigned_by = null) { // Check to see if the comment status is in the allowable set of statuses. - if (STATUSES.indexOf(status) === -1) { + if (COMMENT_STATUS.indexOf(status) === -1) { // Comment status is not supported! Error out here. return Promise.reject(new Error(`status ${status} is not supported`)); diff --git a/services/tags.js b/services/tags.js new file mode 100644 index 000000000..94212cb13 --- /dev/null +++ b/services/tags.js @@ -0,0 +1,5 @@ +class TagsService { + +} + +module.exports = TagsService; diff --git a/services/users.js b/services/users.js index 14301ccf4..53b51b686 100644 --- a/services/users.js +++ b/services/users.js @@ -12,8 +12,8 @@ const redis = require('./redis'); const redisClient = redis.createClient(); const UserModel = require('../models/user'); -const USER_STATUS = require('../models/user').USER_STATUS; -const USER_ROLES = require('../models/user').USER_ROLES; +const USER_STATUS = require('../models/enum/user_status'); +const USER_ROLES = require('../models/enum/user_roles'); const RECAPTCHA_WINDOW_SECONDS = 60 * 10; // 10 minutes. const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha will be required. diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 83a12dc2e..5783a14c1 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -3,23 +3,25 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); +const AssetModel = require('../../../../models/asset'); const UserModel = require('../../../../models/user'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); describe('graph.mutations.addCommentTag', () => { - let comment; + let comment, asset; beforeEach(async () => { await SettingsService.init(); - comment = await CommentsService.publicCreate({body: `hello there! ${ String(Math.random()).slice(2)}`}); + + asset = new AssetModel({url: 'http://new.test.com/'}); + await asset.save(); + + comment = await CommentsService.publicCreate({asset_id: asset.id, body: `hello there! ${String(Math.random()).slice(2)}`}); }); const query = ` - mutation AddCommentTag ($id: ID!, $tag: String!) { - addCommentTag(id:$id, tag:$tag) { - comment { - id - } + mutation AddCommentTag ($id: ID!, $asset_id: ID!, $name: String!) { + addCommentTag(id: $id, asset_id: $asset_id, name: $name) { errors { translation_key } @@ -30,16 +32,14 @@ describe('graph.mutations.addCommentTag', () => { it('moderators can add tags to comments', async () => { const user = new UserModel({roles: ['MODERATOR' ]}); const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); + const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); } expect(response.errors).to.be.empty; - return CommentsService.findById(response.data.addCommentTag.comment.id) - .then(({tags}) => { - expect(tags).to.have.length(1); - }); + let {tags} = await CommentsService.findById(comment.id); + expect(tags).to.have.length(1); }); describe('users who cant add tags', () => { @@ -48,15 +48,14 @@ describe('graph.mutations.addCommentTag', () => { 'regular commenter': new UserModel({}), 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) }).forEach(([ userDescription, user ]) => { - it(userDescription, async function () { + it(userDescription, async () => { const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST', privacy_type: 'PUBLIC'}); + const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); } expect(response.errors).to.be.empty; expect(response.data.addCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); - expect(response.data.addCommentTag.comment).to.be.null; }); }); }); diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index 41822fcdb..e32943720 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -21,7 +21,9 @@ describe('graph.mutations.createComment', () => { id status tags { - id + tag { + name + } } } errors { @@ -228,7 +230,7 @@ describe('graph.mutations.createComment', () => { .then(({tags}) => { if (tag) { expect(tags).to.have.length(1); - expect(tags[0]).to.have.property('id', tag); + expect(tags[0].tag.name).to.have.equal(tag); } else { expect(tags).length(0); } diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 760887a44..cca072af2 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -6,22 +6,24 @@ const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); const SettingModel = require('../../../../models/setting'); +const AssetModel = require('../../../../models/asset'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); describe('graph.mutations.removeCommentTag', () => { - let comment; + let asset, comment; beforeEach(async () => { await SettingsService.init(); - comment = await CommentsService.publicCreate({body: `hello there! ${ String(Math.random()).slice(2)}`}); + + asset = new AssetModel({url: 'http://new.test.com/'}); + await asset.save(); + + comment = await CommentsService.publicCreate({asset_id: asset.id, body: `hello there! ${String(Math.random()).slice(2)}`}); }); const query = ` - mutation RemoveCommentTag ($id: ID!, $tag: String!) { - removeCommentTag(id:$id, tag:$tag) { - comment { - id - } + mutation RemoveCommentTag ($id: ID!, $asset_id: ID!, $name: String!) { + removeCommentTag(id: $id, asset_id: $asset_id, name: $name) { errors { translation_key } @@ -30,57 +32,53 @@ describe('graph.mutations.removeCommentTag', () => { `; it('moderators can add remove tags from comments', async () => { - const user = new UserModel({roles: ['MODERATOR' ]}); + const user = new UserModel({roles: ['MODERATOR']}); const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user.id); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); + await CommentsService.addTag(comment.id, {tag: {name: 'BEST'}}, false); + + const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); } expect(response.errors).to.be.empty; expect(response.data.removeCommentTag.errors).to.be.null; - CommentsService.findById(response.data.removeCommentTag.comment.id) - .then(({tags}) => { - expect(tags).to.deep.equal([]); - }); + let retrievedComment = await CommentsService.findById(comment.id); + expect(retrievedComment.tags).to.have.length(0); }); describe('users who cant remove tags', () => { - // allow the tag in the settings - SettingModel.findOneAndUpdate({id: 1}, { + before(() => SettingModel.findOneAndUpdate({id: 1}, { $push: { tags: { id: 'BEST', models: ['COMMENTS'] } } - }) - .then(() => { - Object.entries({ - 'anonymous': undefined, - 'regular commenter': new UserModel({}), - 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) - }).forEach(([ userDescription, user ]) => { - it(userDescription, async function () { - const context = new Context({user}); + })); - // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user.id); + Object.entries({ + 'anonymous': undefined, + 'regular commenter': new UserModel({}), + 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) + }).forEach(([userDescription, user]) => { + it(userDescription, async function () { + const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); - if (response.errors && response.errors.length) { - console.error(response.errors); - } - expect(response.errors).to.be.empty; + // add a tag first + await CommentsService.addTag(comment.id, {tag: {name: 'BEST'}}, false); - expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); - expect(response.data.removeCommentTag.comment).to.be.null; - }); + const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); + if (response.errors && response.errors.length) { + console.error(response.errors); + } + expect(response.errors).to.be.empty; + + expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); }); }); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 27657efa5..f5d82d1de 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -221,63 +221,84 @@ describe('services.CommentsService', () => { describe('#addTag', () => { it('adds a tag', async () => { - const commentId = comments[0].id; - const tagName = 'BEST'; - const userId = users[0].id; - await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); - const {tags} = await CommentsService.findById(commentId); - expect(tags.length).to.equal(1); - expect(tags[0].id).to.equal(tagName); - expect(tags[0].added_by).to.equal(userId); - expect(tags[0].created_at).to.be.an.instanceof(Date); - }); - it('can\'t add a tag to comment id that doesn\'t exist', async () => { - const commentId = 'fakenews'; - const tagName = 'BEST'; - const userId = users[0].id; - - CommentsService.addTag(commentId, tagName, userId) - .catch((error) => { - expect(error).to.not.be.null; + const id = comments[0].id; + const name = 'BEST'; + const assigned_by = users[0].id; + + await CommentsService.addTag(id, { + tag: { + name + }, + assigned_by }); + + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + expect(tags[0].tag.name).to.equal(name); + expect(tags[0].assigned_by).to.equal(assigned_by); }); + it('can\'t add same tag.id twice', async () => { - const commentId = comments[0].id; - const tagName = 'BEST'; - const userId = users[0].id; + const id = comments[0].id; + const name = 'BEST'; + const assigned_by = users[0].id; + + await CommentsService.addTag(id, { + tag: { + name + }, + assigned_by + }); - // first time - await CommentsService.addTag(commentId, tagName, userId); + { + let {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } - // second time should fail - await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; + await CommentsService.addTag(id, { + tag: { + name + }, + assigned_by + }); + + { + let {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } }); }); describe('#removeTag', () => { it('removes a tag', async () => { - const commentId = comments[0].id; - const tagName = 'BEST'; - await CommentsService.addTag(commentId, tagName, users[0].id); - const {tags} = await CommentsService.findById(commentId); - expect(tags.length).to.equal(1); + const id = comments[0].id; + const name = 'BEST'; + const assigned_by = users[0].id; + + await CommentsService.addTag(id, { + tag: { + name + }, + assigned_by + }); + + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } // ok now to remove it - await CommentsService.removeTag(commentId, tagName); - const comment = await CommentsService.findById(commentId); - expect(comment.tags.length).to.equal(0); - }); - it('throws if removing a tag that isn\'t there', async () => { - const commentId = comments[0].id; + await CommentsService.removeTag(id, { + tag: { + name + }, + assigned_by + }); - const tagName = 'BEST'; - - // just make sure it has no tags to start - const {tags} = await CommentsService.findById(commentId); - expect(tags.length).to.equal(0); - - // ok now to remove it - await expect(CommentsService.removeTag(commentId, tagName)).to.be.rejected; + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(0); + } }); });