From 1137fca3b4d5b6f059ae10cb757eed5f61da0e25 Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 18 Apr 2017 14:03:26 -0700 Subject: [PATCH 01/56] Adds Tag Model that will be apply to different models. --- graph/resolvers/tag.js | 0 models/comment.js | 22 ------ models/tag.js | 64 +++++++++++++++++ models/user.js | 6 +- services/comments.js | 67 ++++-------------- services/mongoose.js | 1 + services/tags.js | 72 ++++++++++++++++++++ test/server/graph/mutations/addCommentTag.js | 8 ++- 8 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 graph/resolvers/tag.js create mode 100644 models/tag.js create mode 100644 services/tags.js diff --git a/graph/resolvers/tag.js b/graph/resolvers/tag.js new file mode 100644 index 000000000..e69de29bb diff --git a/models/comment.js b/models/comment.js index 0984a0832..09f592eca 100644 --- a/models/comment.js +++ b/models/comment.js @@ -30,27 +30,6 @@ const StatusSchema = new Schema({ _id: false }); -/** - * The Mongo schema for a Comment Tag. - * @type {Schema} - */ -const TagSchema = new Schema({ - name: String, - - // The User ID of the user that assigned the status. - assigned_by: { - type: String, - default: null - }, - - created_at: { - type: Date, - default: Date - } -}, { - _id: false -}); - /** * The Mongo schema for a Comment. * @type {Schema} @@ -74,7 +53,6 @@ const CommentSchema = new Schema({ enum: STATUSES, default: 'NONE' }, - tags: [TagSchema], parent_id: String, // Additional metadata stored on the field. diff --git a/models/tag.js b/models/tag.js new file mode 100644 index 000000000..6991f3abb --- /dev/null +++ b/models/tag.js @@ -0,0 +1,64 @@ +const mongoose = require('../services/mongoose'); +const uuid = require('uuid'); +const Schema = mongoose.Schema; + +// in settings +// --> decide who can apply them (self, role, anyone) and + +// Who can see the tag (self, by role, anyone) +const PRIVACY_TYPES = [ + 'ADMIN', + 'SELF', + 'PUBLIC' +]; + +// The type of item that the tag is apply on. +const ITEM_TYPES = [ + 'ASSETS', + 'COMMENTS', + 'USERS' +]; + +/** + * The Mongo schema for a Comment Tag. + * @type {Schema} + */ +const TagSchema = new Schema({ + id: { + type: String, + default: uuid.v4, + unique: true + }, + + name: { + type: String, + unique: true + }, + + item_type: { + type: String, + enum: ITEM_TYPES + }, + + item_id: String, + + // The User ID of the user that assigned the status. + assigned_by: { + type: String, + default: null + }, + + privacy_type: { + type: String, + enum: PRIVACY_TYPES + }, + + // Additional metadata stored on the field. + metadata: Schema.Types.Mixed +}, { + _id: false +}); + +const Tag = mongoose.model('Tag', TagSchema); + +module.exports = Tag; diff --git a/models/user.js b/models/user.js index 2663410a7..fdd3ed74a 100644 --- a/models/user.js +++ b/models/user.js @@ -185,7 +185,9 @@ const USER_GRAPH_OPERATIONS = [ 'mutation:suspendUser', 'mutation:setCommentStatus', 'mutation:addCommentTag', - 'mutation:removeCommentTag' + 'mutation:removeCommentTag', + 'mutation:addUserTag', + 'mutation:removeUserTag' ]; /** @@ -207,7 +209,7 @@ UserSchema.method('can', function(...actions) { // {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'].includes(a)) && ! userCanModifyTags(this)) { + if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag', 'mutation:removeUserTag', 'mutation: addUserTag'].includes(a)) && ! userCanModifyTags(this)) { return false; } diff --git a/services/comments.js b/services/comments.js index 75b53873a..a7f483d2c 100644 --- a/services/comments.js +++ b/services/comments.js @@ -3,10 +3,8 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const ALLOWED_TAGS = [ - {name: 'STAFF'}, - {name: 'BEST'}, -]; +const TagModel = require('../models/tag'); +const TagsService = require('./tags'); const STATUSES = [ 'ACCEPTED', @@ -53,37 +51,14 @@ module.exports = class CommentsService { */ static addTag(id, name, assigned_by) { - if (ALLOWED_TAGS.find((t) => t.name === name) == null) { - return Promise.reject(new Error('tag not allowed')); - } + return TagsService.insertCommentTag({ + name, + item_id: id, + item_type: 'COMMENTS', + user_id: assigned_by, + }); - const filter = { - id, - 'tags.name': {$ne: name}, - }; - const update = { - $push: {tags: { - name, - assigned_by, - created_at: new Date() - }} - }; - return CommentModel.update(filter, update) - .then(({nModified}) => { - switch (nModified) { - case 0: - - // either the tag was already there, or the comment doesn't exist with that id... - throw new Error('Could not add tag to comment. Either the comment doesn\'t exist or the tag is already present.'); - case 1: - - // tag added - return; - default: - - // this should never happen because no multi parameter and unique index on id - } - }); + // Add the ID to the comment } /** @@ -93,25 +68,11 @@ module.exports = class CommentsService { * @param {String} name the name of the tag to add */ static removeTag(id, name) { - const filter = { - id, - 'tags.name': name, - }; - const update = {$pull: {tags: {name}}}; - return CommentModel.update(filter, update) - .then(({nModified}) => { - switch (nModified) { - case 0: - throw new Error('Could not remove tag from comment. Either the comment doesn\'t exist or the tag is not present'); - case 1: - - // tag removed - return; - default: - - // this should never happen because no multi parameter and unique index on id - } - }); + return TagModel.remove({ + item_type: 'COMMENTS', + item_id: id, + name + }); } /** diff --git a/services/mongoose.js b/services/mongoose.js index c7092326f..206075239 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -65,4 +65,5 @@ require('../models/action'); require('../models/asset'); require('../models/comment'); require('../models/setting'); +require('../models/tag'); require('../models/user'); diff --git a/services/tags.js b/services/tags.js new file mode 100644 index 000000000..caeb4e761 --- /dev/null +++ b/services/tags.js @@ -0,0 +1,72 @@ +const TagModel = require('../models/tag'); + +const ALLOWED_COMMENT_TAGS = [ + {name: 'STAFF'}, + {name: 'BEST'}, +]; + +module.exports = class TagsService { + + /** + * Finds an action by the id. + * @param {String} id identifier of the tag (uuid) + */ + static findById(id) { + return TagModel.findOne({id}); + } + + /** + * Add a tag. + * @param {string} name the actual tag + * @param {String} item_id identifier of the comment (uuid) + * @param {String} item_type type of the object being tag (COMMENTS) + * @param {String} user_id user id that assigned the tag (uuid) + * @param {String} privacy_type visibility of the tag on the comment + * @return {Promise} + */ + static insertCommentTag(tag) { + + if (ALLOWED_COMMENT_TAGS.find((t) => t.name === tag.name) == null) { + return Promise.reject(new Error('tag not allowed')); + } + + // Tags are made unique by using a query that can be reproducable, i.e., + // not containing user inputable values. + let query = { + name: tag.name, + item_id: tag.item_id, + item_type: tag.item_type, + assigned_by: tag.user_id, + privacy_type: tag.privacy_type + }; + + // Create/Update the tag. + return TagModel.findOneAndUpdate(query, tag, { + + // Ensure that if it's new, we return the new object created. + new: true, + + // Perform an upsert in the event that this doesn't exist. + upsert: true, + + // Set the default values if not provided based on the mongoose models. + setDefaultsOnInsert: true + }) + .then(({nModified}) => { + switch (nModified) { + case 0: + + // either the tag was already there, or the comment doesn't exist with that id... + throw new Error('Could not add tag to comment. Either the comment doesn\'t exist or the tag is already present.'); + case 1: + + // tag added + return; + default: + + // this should never happen because no multi parameter and unique index on id + } + }); + } + +}; diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 018e96631..fbb7fe8a8 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -4,6 +4,7 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); +const TagModel = require('../../../../models/tag'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); @@ -37,7 +38,12 @@ describe('graph.mutations.addCommentTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; - expect(response.data.addCommentTag.comment.tags).to.deep.equal([{name: 'BEST'}]); + TagModel.find({ + item_id: response.data.addCommentTag.comment.id, + name: 'BEST' + }).then((tags) => { + expect(tags).to.have.length(1); + }); }); describe('users who cant add tags', () => { From bd72bfc6ecd5a5f5b888aff7b97f8f069fada01e Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 20 Apr 2017 12:24:08 -0700 Subject: [PATCH 02/56] Fix tests related to tags on comments. --- graph/loaders/index.js | 2 + graph/loaders/tags.js | 37 ++++++++++++ graph/mutators/comment.js | 14 ++--- models/tag.js | 10 +++- services/comments.js | 27 ++++++--- services/tags.js | 59 +++++++++---------- test/server/graph/mutations/addCommentTag.js | 13 ++-- test/server/graph/mutations/createComment.js | 14 +++-- .../graph/mutations/removeCommentTag.js | 16 +++-- test/server/services/comments.js | 28 +++++---- 10 files changed, 139 insertions(+), 81 deletions(-) create mode 100644 graph/loaders/tags.js diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 26727940f..8bc170c6d 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -6,6 +6,7 @@ const Assets = require('./assets'); const Comments = require('./comments'); const Metrics = require('./metrics'); const Settings = require('./settings'); +const Tags = require('./tags'); const Users = require('./users'); const plugins = require('../../services/plugins'); @@ -18,6 +19,7 @@ let loaders = [ Comments, Metrics, Settings, + Tags, Users, // Load the plugin loaders from the manager. diff --git a/graph/loaders/tags.js b/graph/loaders/tags.js new file mode 100644 index 000000000..b76bc36bd --- /dev/null +++ b/graph/loaders/tags.js @@ -0,0 +1,37 @@ +const DataLoader = require('dataloader'); + +const util = require('./util'); + +const TagsService = require('../../services/tags'); +const TagModel = require('../../models/tag'); + +/** + * Gets tags based on their item id's. + */ +const genTagsByItemID = (_, item_ids) => { + return TagsService + .findByItemIdArray(item_ids) + .then(util.arrayJoinBy(item_ids, 'item_id')); +}; + +/** + * Search for tags based on their item_type and ensures that + * the tags returned have unique item id's. + * @param {String} item_type the item id to search by + * @return {Promise} resolves to distinct items tags + */ +const getItemIdsByItemType = (_, item_type) => { + return TagModel.distinct('item_id', {item_type}); +}; + +/** + * Creates a set of loaders based on a GraphQL context. + * @param {Object} context the context of the GraphQL request + * @return {Object} object of loaders + */ +module.exports = (context) => ({ + Tags: { + getByID: new DataLoader((ids) => genTagsByItemID(context, ids)), + getByTypes: ({item_type}) => getItemIdsByItemType(context, item_type) + } +}); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 728603ae2..930417c5c 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -5,6 +5,8 @@ const ActionsService = require('../../services/actions'); const CommentsService = require('../../services/comments'); const linkify = require('linkify-it')(); +const TagModel = require('../../models/tag'); + const Wordlist = require('../../services/wordlist'); /** @@ -18,20 +20,18 @@ const Wordlist = require('../../services/wordlist'); */ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = 'NONE') => { - let tags = []; - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - tags = [{name: 'STAFF'}]; - } - return CommentsService.publicCreate({ body, asset_id, parent_id, status, - tags, author_id: user.id }) - .then((comment) => { + .then(async (comment) => { + + if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + await CommentsService.addTag(comment.id, 'STAFF', user.id); + } // 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 diff --git a/models/tag.js b/models/tag.js index 6991f3abb..155e57dbb 100644 --- a/models/tag.js +++ b/models/tag.js @@ -50,13 +50,17 @@ const TagSchema = new Schema({ privacy_type: { type: String, - enum: PRIVACY_TYPES + enum: PRIVACY_TYPES, + default: 'SELF' }, // Additional metadata stored on the field. metadata: Schema.Types.Mixed -}, { - _id: false +}, { + timestamps: { + createdAt: 'created_at', + updatedAt: 'updated_at' + } }); const Tag = mongoose.model('Tag', TagSchema); diff --git a/services/comments.js b/services/comments.js index a7f483d2c..d61b6ca25 100644 --- a/services/comments.js +++ b/services/comments.js @@ -3,8 +3,8 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const TagModel = require('../models/tag'); const TagsService = require('./tags'); +const TagModel = require('../models/tag'); const STATUSES = [ 'ACCEPTED', @@ -51,14 +51,18 @@ module.exports = class CommentsService { */ static addTag(id, name, assigned_by) { - return TagsService.insertCommentTag({ - name, - item_id: id, - item_type: 'COMMENTS', - user_id: assigned_by, + return CommentModel.findOne({id}) + .then((comment) => { + if (comment == null) { + return Promise.reject(new Error('tag not allowed')); + } + return TagsService.insertCommentTag({ + name, + item_id: id, + item_type: 'COMMENTS', + user_id: assigned_by, + }); }); - - // Add the ID to the comment } /** @@ -68,10 +72,15 @@ module.exports = class CommentsService { * @param {String} name the name of the tag to add */ static removeTag(id, name) { - return TagModel.remove({ + return TagModel.findOneAndRemove({ item_type: 'COMMENTS', item_id: id, name + }) + .then((tag) => { + if (tag == null) { + return Promise.reject(new Error('tag does not exist')); + } }); } diff --git a/services/tags.js b/services/tags.js index caeb4e761..172148727 100644 --- a/services/tags.js +++ b/services/tags.js @@ -8,13 +8,26 @@ const ALLOWED_COMMENT_TAGS = [ module.exports = class TagsService { /** - * Finds an action by the id. + * Finds a tag by the id. * @param {String} id identifier of the tag (uuid) */ static findById(id) { return TagModel.findOne({id}); } + /** + * Finds atag by the item_id and name. + * @param {String} item_id identifier of the item that the tag was applied into(uuid) + * @param {string} name name of the tag + */ + static findByItemIdAndName(item_id, name, item_type) { + return TagModel.find({ + item_id, + item_type, + name + }); + } + /** * Add a tag. * @param {string} name the actual tag @@ -30,43 +43,25 @@ module.exports = class TagsService { return Promise.reject(new Error('tag not allowed')); } - // Tags are made unique by using a query that can be reproducable, i.e., - // not containing user inputable values. - let query = { + // // Tags are made unique by using a query that can be reproducable, i.e., + // // not containing user inputable values. + // let query = { + // name: tag.name, + // item_id: tag.item_id, + // item_type: tag.item_type, + // assigned_by: tag.user_id, + // privacy_type: tag.privacy_type + // }; + + // Create/Update the tag. + let newtag = new TagModel({ name: tag.name, item_id: tag.item_id, item_type: tag.item_type, assigned_by: tag.user_id, privacy_type: tag.privacy_type - }; - - // Create/Update the tag. - return TagModel.findOneAndUpdate(query, tag, { - - // Ensure that if it's new, we return the new object created. - new: true, - - // Perform an upsert in the event that this doesn't exist. - upsert: true, - - // Set the default values if not provided based on the mongoose models. - setDefaultsOnInsert: true - }) - .then(({nModified}) => { - switch (nModified) { - case 0: - - // either the tag was already there, or the comment doesn't exist with that id... - throw new Error('Could not add tag to comment. Either the comment doesn\'t exist or the tag is already present.'); - case 1: - - // tag added - return; - default: - - // this should never happen because no multi parameter and unique index on id - } }); + return newtag.save(); } }; diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index fbb7fe8a8..063537004 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -4,7 +4,7 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); -const TagModel = require('../../../../models/tag'); +const TagService = require('../../../../services/tags'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); @@ -19,9 +19,7 @@ describe('graph.mutations.addCommentTag', () => { mutation AddCommentTag ($id: ID!, $tag: String!) { addCommentTag(id:$id, tag:$tag) { comment { - tags { - name - } + id } errors { translation_key @@ -38,10 +36,9 @@ describe('graph.mutations.addCommentTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; - TagModel.find({ - item_id: response.data.addCommentTag.comment.id, - name: 'BEST' - }).then((tags) => { + + return TagService.findByItemIdAndName(response.data.addCommentTag.comment.id, 'BEST', 'COMMENTS') + .then((tags) => { expect(tags).to.have.length(1); }); }); diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index b3bdd459a..11fa666d0 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -3,11 +3,14 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); + const UserModel = require('../../../../models/user'); const AssetModel = require('../../../../models/asset'); -const SettingsService = require('../../../../services/settings'); const ActionModel = require('../../../../models/action'); +const SettingsService = require('../../../../services/settings'); +const TagService = require('../../../../services/tags'); + describe('graph.mutations.createComment', () => { beforeEach(() => SettingsService.init()); @@ -217,11 +220,14 @@ describe('graph.mutations.createComment', () => { expect(data.createComment).to.have.property('comment').not.null; expect(data.createComment).to.have.property('errors').null; + return TagService.findByItemIdAndName(data.createComment.comment.id, tag, 'COMMENTS'); + }) + .then((tags) => { if (tag) { - expect(data.createComment.comment).to.have.property('tags').length(1); - expect(data.createComment.comment.tags[0]).to.have.property('name', tag); + expect(tags).to.have.length(1); + expect(tags[0]).to.have.property('name', tag); } else { - expect(data.createComment.comment).to.have.property('tags').length(0); + expect(tags).length(0); } }); }); diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index c0d452f37..4455a0108 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -4,8 +4,10 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); + const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); +const TagService = require('../../../../services/tags'); describe('graph.mutations.removeCommentTag', () => { let comment; @@ -18,9 +20,7 @@ describe('graph.mutations.removeCommentTag', () => { mutation RemoveCommentTag ($id: ID!, $tag: String!) { removeCommentTag(id:$id, tag:$tag) { comment { - tags { - name - } + id } errors { translation_key @@ -41,7 +41,12 @@ describe('graph.mutations.removeCommentTag', () => { } expect(response.errors).to.be.empty; expect(response.data.removeCommentTag.errors).to.be.null; - expect(response.data.removeCommentTag.comment.tags).to.deep.equal([]); + + TagService.findByItemIdAndName(response.data.removeCommentTag.comment.id, 'BEST') + .then((tags) => { + expect(tags).to.deep.equal([]); + }); + }); describe('users who cant remove tags', () => { @@ -60,8 +65,9 @@ describe('graph.mutations.removeCommentTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; + expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); - expect(response.data.removeCommentTag.comment).to.be.null; + expect(response.data.removeCommentTag.comment).to.be.null; }); }); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 5b9cbf4d1..952e071a3 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -5,6 +5,7 @@ const ActionsService = require('../../../services/actions'); const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); const CommentsService = require('../../../services/comments'); +const TagService = require('../../../services/tags'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; @@ -225,16 +226,17 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; const userId = users[0].id; await CommentsService.addTag(commentId, tagName, userId); - const updatedComment = await CommentsService.findById(commentId); - expect(updatedComment.tags.length).to.equal(1); - expect(updatedComment.tags[0].name).to.equal(tagName); - expect(updatedComment.tags[0].assigned_by).to.equal(userId); - expect(updatedComment.tags[0].created_at).to.be.an.instanceof(Date); + const tags = await TagService.findByItemIdAndName(commentId, 'BEST', 'COMMENTS'); + expect(tags.length).to.equal(1); + expect(tags[0].name).to.equal(tagName); + expect(tags[0].assigned_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; + await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; }); it('can\'t add same tag.name twice', async () => { @@ -255,23 +257,23 @@ describe('services.CommentsService', () => { const commentId = comments[0].id; const tagName = 'BEST'; await CommentsService.addTag(commentId, tagName, users[0].id); - const updatedComment = await CommentsService.findById(commentId); - expect(updatedComment.tags.length).to.equal(1); + const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + expect(tags.length).to.equal(1); // ok now to remove it await CommentsService.removeTag(commentId, tagName); - const updatedComment2 = await CommentsService.findById(commentId); - expect(updatedComment2.tags.length).to.equal(0); + const tags2 = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + expect(tags2.length).to.equal(0); }); it('throws if removing a tag that isn\'t there', async () => { const commentId = comments[0].id; - // just make sure it has no tags to start - const updatedComment2 = await CommentsService.findById(commentId); - expect(updatedComment2.tags.length).to.equal(0); - const tagName = 'BEST'; + // just make sure it has no tags to start + const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + expect(tags.length).to.equal(0); + // ok now to remove it await expect(CommentsService.removeTag(commentId, tagName)).to.be.rejected; }); From 8458a9aecc160cca60e88d7809026395a2d9bb91 Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 20 Apr 2017 12:29:21 -0700 Subject: [PATCH 03/56] fixed lint error. --- graph/mutators/comment.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 930417c5c..096db4913 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -5,8 +5,6 @@ const ActionsService = require('../../services/actions'); const CommentsService = require('../../services/comments'); const linkify = require('linkify-it')(); -const TagModel = require('../../models/tag'); - const Wordlist = require('../../services/wordlist'); /** From 962a9bd6c91fed0e6674d4e7076a86e21a90bf21 Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 20 Apr 2017 14:42:57 -0700 Subject: [PATCH 04/56] That was so so so silly... --- models/tag.js | 1 - 1 file changed, 1 deletion(-) diff --git a/models/tag.js b/models/tag.js index 155e57dbb..b6e29a6ab 100644 --- a/models/tag.js +++ b/models/tag.js @@ -32,7 +32,6 @@ const TagSchema = new Schema({ name: { type: String, - unique: true }, item_type: { From 91af8c2ecec35c275d3867876f058aab6ab1832b Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 21 Apr 2017 10:34:57 -0700 Subject: [PATCH 05/56] Index on Tag for name, item id, item type. --- graph/loaders/tags.js | 4 ++-- graph/resolvers/comment.js | 7 +++++-- models/tag.js | 10 ++++++++++ services/tags.js | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/graph/loaders/tags.js b/graph/loaders/tags.js index b76bc36bd..cb767fdf6 100644 --- a/graph/loaders/tags.js +++ b/graph/loaders/tags.js @@ -8,7 +8,7 @@ const TagModel = require('../../models/tag'); /** * Gets tags based on their item id's. */ -const genTagsByItemID = (_, item_ids) => { +const getByItemID = (_, item_ids) => { return TagsService .findByItemIdArray(item_ids) .then(util.arrayJoinBy(item_ids, 'item_id')); @@ -31,7 +31,7 @@ const getItemIdsByItemType = (_, item_type) => { */ module.exports = (context) => ({ Tags: { - getByID: new DataLoader((ids) => genTagsByItemID(context, ids)), + getByID: new DataLoader((ids) => getByItemID(context, ids)), getByTypes: ({item_type}) => getItemIdsByItemType(context, item_type) } }); diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 19ea11efe..f94cfaf52 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -6,6 +6,9 @@ const Comment = { return Comments.get.load(parent_id); }, + tags({id}, _, {loaders: {Tags}}) { + return Tags.getByID.load([id]); + }, user({author_id}, _, {loaders: {Users}}) { return Users.getByID.load(author_id); }, @@ -23,9 +26,9 @@ const Comment = { }, replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) { if (user && excludeIgnored) { - return Comments.countByParentIDPersonalized({id, excludeIgnored}); + return Comments.countByParentIDPersonalized({id, excludeIgnored}); } - return Comments.countByParentID.load(id); + return Comments.countByParentID.load(id); }, actions({id}, _, {user, loaders: {Actions}}) { diff --git a/models/tag.js b/models/tag.js index b6e29a6ab..85bb0845a 100644 --- a/models/tag.js +++ b/models/tag.js @@ -62,6 +62,16 @@ const TagSchema = new Schema({ } }); +// Add the indixies on the tag on an item. +TagSchema.index({ + 'item_type': 1, + 'item_id': 1, + 'name': 1 +}, { + unique: true, + background: false +}); + const Tag = mongoose.model('Tag', TagSchema); module.exports = Tag; diff --git a/services/tags.js b/services/tags.js index 172148727..c4010e562 100644 --- a/services/tags.js +++ b/services/tags.js @@ -28,6 +28,22 @@ module.exports = class TagsService { }); } + /** + * Finds actions in an array of ids. + * @param {String} ids array of user identifiers (uuid) + */ + static async findByItemIdArray(item_ids) { + let tags = await TagModel.find({ + 'item_id': {$in: item_ids} + }); + + if (tags === null) { + return []; + } + + return tags; + } + /** * Add a tag. * @param {string} name the actual tag From c4ffe58f585e66a5a863d6020dc633677919d97f Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 21 Apr 2017 14:30:41 -0700 Subject: [PATCH 06/56] Adds privacy type to the methods to add tags. --- graph/mutators/comment.js | 6 +++--- graph/resolvers/root_mutation.js | 4 ++-- graph/typeDefs.graphql | 2 +- services/comments.js | 3 ++- services/tags.js | 18 +++++++++--------- test/server/graph/mutations/addCommentTag.js | 8 ++++---- .../server/graph/mutations/removeCommentTag.js | 4 ++-- test/server/services/comments.js | 12 ++++++------ 8 files changed, 29 insertions(+), 28 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 0290d9f16..724e71917 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -28,7 +28,7 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = .then(async (comment) => { if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user.id); + await CommentsService.addTag(comment.id, 'STAFF', user.id, 'PUBLIC'); } // If the loaders are present, clear the caches for these values because we @@ -202,8 +202,8 @@ const setCommentStatus = ({loaders: {Comments}}, {id, status}) => { * @param {String} id identifier of the comment (uuid) * @param {String} tag name of the tag */ -const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.addTag(id, tag, user.id); +const addCommentTag = ({user, loaders: {Comments}}, {id, tag, privacy_type}) => { + return CommentsService.addTag(id, tag, user.id, privacy_type); }; /** diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 661cc9f96..d669869cd 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -32,8 +32,8 @@ 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))); + addCommentTag(_, {id, tag, privacy_type}, {mutators: {Comment}}) { + return wrapResponse('comment')(Comment.addCommentTag({id, tag, privacy_type}).then(() => CommentsService.findById(id))); }, removeCommentTag(_, {id, tag}, {mutators: {Comment}}) { return wrapResponse('comment')(Comment.removeCommentTag({id, tag}).then(() => CommentsService.findById(id))); diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 32c2c16c2..200e752b6 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -808,7 +808,7 @@ type RootMutation { setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse # Add tag to comment. - addCommentTag(id: ID!, tag: String!): AddCommentTagResponse + addCommentTag(id: ID!, tag: String!, privacy_type: String!): AddCommentTagResponse # Remove tag from comment. removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse diff --git a/services/comments.js b/services/comments.js index d61b6ca25..ccb5a9131 100644 --- a/services/comments.js +++ b/services/comments.js @@ -49,7 +49,7 @@ module.exports = class CommentsService { * @param {String} name the name of the tag to add * @param {String} assigned_by the user id for the user who added the tag */ - static addTag(id, name, assigned_by) { + static addTag(id, name, assigned_by, privacy_type) { return CommentModel.findOne({id}) .then((comment) => { @@ -61,6 +61,7 @@ module.exports = class CommentsService { item_id: id, item_type: 'COMMENTS', user_id: assigned_by, + privacy_type }); }); } diff --git a/services/tags.js b/services/tags.js index c4010e562..5cd0a09e1 100644 --- a/services/tags.js +++ b/services/tags.js @@ -5,6 +5,12 @@ const ALLOWED_COMMENT_TAGS = [ {name: 'BEST'}, ]; +const ALLOWED_PRIVACY_TYPE = [ + {privacy_type: 'PUBLIC'}, + {privacy_type: 'SELF'}, + {privacy_type: 'ADMIN'} +]; + module.exports = class TagsService { /** @@ -59,15 +65,9 @@ module.exports = class TagsService { return Promise.reject(new Error('tag not allowed')); } - // // Tags are made unique by using a query that can be reproducable, i.e., - // // not containing user inputable values. - // let query = { - // name: tag.name, - // item_id: tag.item_id, - // item_type: tag.item_type, - // assigned_by: tag.user_id, - // privacy_type: tag.privacy_type - // }; + if (ALLOWED_PRIVACY_TYPE.find((p) => p.privacy_type === tag.privacy_type) == null) { + return Promise.reject(new Error('privacy type not allowed')); + } // Create/Update the tag. let newtag = new TagModel({ diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 063537004..01966bd88 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -16,8 +16,8 @@ describe('graph.mutations.addCommentTag', () => { }); const query = ` - mutation AddCommentTag ($id: ID!, $tag: String!) { - addCommentTag(id:$id, tag:$tag) { + mutation AddCommentTag ($id: ID!, $tag: String!, $privacy_type: String!) { + addCommentTag(id:$id, tag:$tag, privacy_type:$privacy_type) { comment { id } @@ -31,7 +31,7 @@ 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, tag: 'BEST', privacy_type: 'PUBLIC'}); if (response.errors && response.errors.length) { console.error(response.errors); } @@ -51,7 +51,7 @@ describe('graph.mutations.addCommentTag', () => { }).forEach(([ userDescription, user ]) => { it(userDescription, async function () { 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, tag: 'BEST', privacy_type: 'PUBLIC'}); if (response.errors && response.errors.length) { console.error(response.errors); } diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 4455a0108..7660eef35 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -34,7 +34,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST'); + await CommentsService.addTag(comment.id, 'BEST', user.id, 'PUBLIC'); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); @@ -59,7 +59,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST'); + await CommentsService.addTag(comment.id, 'BEST', user ? user.id : null, 'PUBLIC'); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 952e071a3..d4fc5f17e 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -225,7 +225,7 @@ describe('services.CommentsService', () => { const commentId = comments[0].id; const tagName = 'BEST'; const userId = users[0].id; - await CommentsService.addTag(commentId, tagName, userId); + await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); const tags = await TagService.findByItemIdAndName(commentId, 'BEST', 'COMMENTS'); expect(tags.length).to.equal(1); expect(tags[0].name).to.equal(tagName); @@ -237,7 +237,7 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; const userId = users[0].id; - await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; + await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; }); it('can\'t add same tag.name twice', async () => { const commentId = comments[0].id; @@ -245,10 +245,10 @@ describe('services.CommentsService', () => { const userId = users[0].id; // first time - await CommentsService.addTag(commentId, tagName, userId); + await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); // second time should fail - await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; + await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; }); }); @@ -256,7 +256,7 @@ describe('services.CommentsService', () => { it('removes a tag', async () => { const commentId = comments[0].id; const tagName = 'BEST'; - await CommentsService.addTag(commentId, tagName, users[0].id); + await CommentsService.addTag(commentId, tagName, users[0].id, 'PUBLIC'); const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); expect(tags.length).to.equal(1); @@ -273,7 +273,7 @@ describe('services.CommentsService', () => { // just make sure it has no tags to start const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); expect(tags.length).to.equal(0); - + // ok now to remove it await expect(CommentsService.removeTag(commentId, tagName)).to.be.rejected; }); From cb09c5082010f32e924beab7375544f7a11a8c42 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 13:11:48 -0700 Subject: [PATCH 07/56] Error on duplicate. --- graph/loaders/index.js | 2 - graph/loaders/settings.js | 17 +++- graph/loaders/tags.js | 37 --------- graph/mutators/comment.js | 8 +- graph/resolvers/comment.js | 3 - graph/resolvers/root_mutation.js | 4 +- graph/typeDefs.graphql | 17 ++-- models/comment.js | 25 ++++++ models/setting.js | 38 ++++++++- models/tag.js | 77 ----------------- models/user.js | 12 +++ services/comments.js | 54 ++++++------ services/mongoose.js | 1 - services/tags.js | 83 ------------------- test/server/graph/loaders/metrics.js | 28 +++++-- test/server/graph/mutations/addCommentTag.js | 11 ++- test/server/graph/mutations/createComment.js | 6 +- .../graph/mutations/removeCommentTag.js | 5 +- test/server/services/comments.js | 11 ++- 19 files changed, 173 insertions(+), 266 deletions(-) delete mode 100644 graph/loaders/tags.js delete mode 100644 models/tag.js delete mode 100644 services/tags.js diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 8bc170c6d..26727940f 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -6,7 +6,6 @@ const Assets = require('./assets'); const Comments = require('./comments'); const Metrics = require('./metrics'); const Settings = require('./settings'); -const Tags = require('./tags'); const Users = require('./users'); const plugins = require('../../services/plugins'); @@ -19,7 +18,6 @@ let loaders = [ Comments, Metrics, Settings, - Tags, Users, // Load the plugin loaders from the manager. diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js index 77ece165f..9d777255f 100644 --- a/graph/loaders/settings.js +++ b/graph/loaders/settings.js @@ -1,12 +1,25 @@ const SettingsService = require('../../services/settings'); +const SettingModel = require('../../models/setting'); const util = require('./util'); +/** + * Search for tags based on their item_type. + * @param {String} item_type the item type to search by + * @return {Promise} resolves to distinct items tags + */ +const getByItemType = (_, item_type) => { + return SettingModel.distinct('tags.models', {item_type}); +}; + /** * Creates a set of loaders based on a GraphQL context. * @param {Object} context the context of the GraphQL request * @return {Object} object of loaders */ -module.exports = () => ({ - Settings: new util.SingletonResolver(() => SettingsService.retrieve()) +module.exports = (context) => ({ + Settings: { + get: new util.SingletonResolver(() => SettingsService.retrieve()), + getTagsByItemType: (item_type) => getByItemType(context, item_type) + } }); diff --git a/graph/loaders/tags.js b/graph/loaders/tags.js deleted file mode 100644 index cb767fdf6..000000000 --- a/graph/loaders/tags.js +++ /dev/null @@ -1,37 +0,0 @@ -const DataLoader = require('dataloader'); - -const util = require('./util'); - -const TagsService = require('../../services/tags'); -const TagModel = require('../../models/tag'); - -/** - * Gets tags based on their item id's. - */ -const getByItemID = (_, item_ids) => { - return TagsService - .findByItemIdArray(item_ids) - .then(util.arrayJoinBy(item_ids, 'item_id')); -}; - -/** - * Search for tags based on their item_type and ensures that - * the tags returned have unique item id's. - * @param {String} item_type the item id to search by - * @return {Promise} resolves to distinct items tags - */ -const getItemIdsByItemType = (_, item_type) => { - return TagModel.distinct('item_id', {item_type}); -}; - -/** - * Creates a set of loaders based on a GraphQL context. - * @param {Object} context the context of the GraphQL request - * @return {Object} object of loaders - */ -module.exports = (context) => ({ - Tags: { - getByID: new DataLoader((ids) => getByItemID(context, ids)), - getByTypes: ({item_type}) => getItemIdsByItemType(context, item_type) - } -}); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index ec310d763..3413c6f4e 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -16,7 +16,7 @@ const Wordlist = require('../../services/wordlist'); * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => { +const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null}, status = 'NONE') => { return CommentsService.publicCreate({ body, @@ -28,7 +28,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par .then(async (comment) => { if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user.id, 'PUBLIC'); + await CommentsService.addTag(comment.id, 'STAFF', user.id); } // If the loaders are present, clear the caches for these values because we @@ -208,8 +208,8 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { * @param {String} id identifier of the comment (uuid) * @param {String} tag name of the tag */ -const addCommentTag = ({user, loaders: {Comments}}, {id, tag, privacy_type}) => { - return CommentsService.addTag(id, tag, user.id, privacy_type); +const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { + return CommentsService.addTag(id, tag, user.id); }; /** diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index f94cfaf52..88ddbee07 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -6,9 +6,6 @@ const Comment = { return Comments.get.load(parent_id); }, - tags({id}, _, {loaders: {Tags}}) { - return Tags.getByID.load([id]); - }, user({author_id}, _, {loaders: {Users}}) { return Users.getByID.load(author_id); }, diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index e3e3ae1d7..5b60fbaa8 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -29,8 +29,8 @@ const RootMutation = { setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, - addCommentTag(_, {id, tag, privacy_type}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.addCommentTag({id, tag, privacy_type}).then(() => CommentsService.findById(id))); + 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))); diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index a4ebe98b3..781b3b328 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -54,10 +54,10 @@ type User { type Tag { # the actual tag for the comment. - name: String! + id: String! # the user that assigned the tag. If NULL then the system automatically tagged it. - assigned_by: String + added_by: String # the time when the tag was assigned. created_at: Date! @@ -588,8 +588,13 @@ enum ACTION_ITEM_TYPE { USERS } -enum TAG_TYPE { - STAFF +input CreateLikeInput { + + # The item's id for which we are to create a like. + item_id: ID! + + # The type of the item for which we are to create the like. + item_type: ACTION_ITEM_TYPE! } input CreateCommentInput { @@ -604,7 +609,7 @@ input CreateCommentInput { body: String! # Tags - tags: [TAG_TYPE] + tags: [String] } @@ -746,7 +751,7 @@ type RootMutation { setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse # Add tag to comment. - addCommentTag(id: ID!, tag: String!, privacy_type: String!): AddCommentTagResponse + addCommentTag(id: ID!, tag: String!): AddCommentTagResponse # Remove tag from comment. removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse diff --git a/models/comment.js b/models/comment.js index 09f592eca..f3e239080 100644 --- a/models/comment.js +++ b/models/comment.js @@ -30,6 +30,29 @@ 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} @@ -55,6 +78,8 @@ const CommentSchema = new Schema({ }, parent_id: String, + tags: [TagSchema], + // Additional metadata stored on the field. metadata: { default: {}, diff --git a/models/setting.js b/models/setting.js index e63fa8f3e..44f5ad5ca 100644 --- a/models/setting.js +++ b/models/setting.js @@ -1,6 +1,41 @@ 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' @@ -88,7 +123,8 @@ const SettingSchema = new Schema({ type: Array, default: ['localhost'] } - } + }, + tags: [TagSchema] }, { timestamps: { createdAt: 'created_at', diff --git a/models/tag.js b/models/tag.js deleted file mode 100644 index 85bb0845a..000000000 --- a/models/tag.js +++ /dev/null @@ -1,77 +0,0 @@ -const mongoose = require('../services/mongoose'); -const uuid = require('uuid'); -const Schema = mongoose.Schema; - -// in settings -// --> decide who can apply them (self, role, anyone) and - -// Who can see the tag (self, by role, anyone) -const PRIVACY_TYPES = [ - 'ADMIN', - 'SELF', - 'PUBLIC' -]; - -// The type of item that the tag is apply on. -const ITEM_TYPES = [ - 'ASSETS', - 'COMMENTS', - 'USERS' -]; - -/** - * The Mongo schema for a Comment Tag. - * @type {Schema} - */ -const TagSchema = new Schema({ - id: { - type: String, - default: uuid.v4, - unique: true - }, - - name: { - type: String, - }, - - item_type: { - type: String, - enum: ITEM_TYPES - }, - - item_id: String, - - // The User ID of the user that assigned the status. - assigned_by: { - type: String, - default: null - }, - - privacy_type: { - type: String, - enum: PRIVACY_TYPES, - default: 'SELF' - }, - - // Additional metadata stored on the field. - metadata: Schema.Types.Mixed -}, { - timestamps: { - createdAt: 'created_at', - updatedAt: 'updated_at' - } -}); - -// Add the indixies on the tag on an item. -TagSchema.index({ - 'item_type': 1, - 'item_id': 1, - 'name': 1 -}, { - unique: true, - background: false -}); - -const Tag = mongoose.model('Tag', TagSchema); - -module.exports = Tag; diff --git a/models/user.js b/models/user.js index 159407fdf..b85f09a93 100644 --- a/models/user.js +++ b/models/user.js @@ -111,6 +111,18 @@ const UserSchema = new mongoose.Schema({ default: false }, + tags: [{ + id: { + type: String, + unique: true + }, + public: Boolean, + text: [{ + type: mongoose.Schema.Types.Mixed, + default: null + }] + }], + // User's settings settings: { bio: { diff --git a/services/comments.js b/services/comments.js index bdc878ce1..4c8f423c6 100644 --- a/services/comments.js +++ b/services/comments.js @@ -3,8 +3,7 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const TagsService = require('./tags'); -const TagModel = require('../models/tag'); +const SettingsService = require('./settings'); const STATUSES = [ 'ACCEPTED', @@ -13,6 +12,10 @@ const STATUSES = [ 'NONE', ]; +const ALLOWED_TAGS = [ + 'STAFF' +]; + module.exports = class CommentsService { /** @@ -22,6 +25,8 @@ module.exports = class CommentsService { */ static publicCreate(comment) { + console.log('-----------------> debug publicCreate'); + // Check to see if this is an array of comments, if so map it out. if (Array.isArray(comment)) { return Promise.all(comment.map(CommentsService.publicCreate)); @@ -47,22 +52,26 @@ module.exports = class CommentsService { * @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} assigned_by the user id for the user who added the tag + * @param {String} added_by the user id for the user who added the tag */ - static addTag(id, name, assigned_by, privacy_type) { + static addTag(id, name, added_by) { - return CommentModel.findOne({id}) - .then((comment) => { - if (comment == null) { + console.log('-----------------> debug addtag', name); + + SettingsService.retrieve() + .then(({tags}) => { + if (!ALLOWED_TAGS.includes(name) || tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { return Promise.reject(new Error('tag not allowed')); } - return TagsService.insertCommentTag({ - name, - item_id: id, - item_type: 'COMMENTS', - user_id: assigned_by, - privacy_type - }); + }); + + return CommentModel.findOneAndUpdate({id}, { + $push: { + tags: { + id: name, + added_by: added_by + } + } }); } @@ -70,17 +79,14 @@ module.exports = class CommentsService { * 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} name the name of the tag to add + * @param {String} tag_id the id of the tag to remove */ - static removeTag(id, name) { - return TagModel.findOneAndRemove({ - item_type: 'COMMENTS', - item_id: id, - name - }) - .then((tag) => { - if (tag == null) { - return Promise.reject(new Error('tag does not exist')); + static removeTag(id, tag_id) { + return CommentModel.findOneAndUpdate({id}, { + $pull: { + tags: { + id: tag_id + } } }); } diff --git a/services/mongoose.js b/services/mongoose.js index 206075239..c7092326f 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -65,5 +65,4 @@ require('../models/action'); require('../models/asset'); require('../models/comment'); require('../models/setting'); -require('../models/tag'); require('../models/user'); diff --git a/services/tags.js b/services/tags.js deleted file mode 100644 index 5cd0a09e1..000000000 --- a/services/tags.js +++ /dev/null @@ -1,83 +0,0 @@ -const TagModel = require('../models/tag'); - -const ALLOWED_COMMENT_TAGS = [ - {name: 'STAFF'}, - {name: 'BEST'}, -]; - -const ALLOWED_PRIVACY_TYPE = [ - {privacy_type: 'PUBLIC'}, - {privacy_type: 'SELF'}, - {privacy_type: 'ADMIN'} -]; - -module.exports = class TagsService { - - /** - * Finds a tag by the id. - * @param {String} id identifier of the tag (uuid) - */ - static findById(id) { - return TagModel.findOne({id}); - } - - /** - * Finds atag by the item_id and name. - * @param {String} item_id identifier of the item that the tag was applied into(uuid) - * @param {string} name name of the tag - */ - static findByItemIdAndName(item_id, name, item_type) { - return TagModel.find({ - item_id, - item_type, - name - }); - } - - /** - * Finds actions in an array of ids. - * @param {String} ids array of user identifiers (uuid) - */ - static async findByItemIdArray(item_ids) { - let tags = await TagModel.find({ - 'item_id': {$in: item_ids} - }); - - if (tags === null) { - return []; - } - - return tags; - } - - /** - * Add a tag. - * @param {string} name the actual tag - * @param {String} item_id identifier of the comment (uuid) - * @param {String} item_type type of the object being tag (COMMENTS) - * @param {String} user_id user id that assigned the tag (uuid) - * @param {String} privacy_type visibility of the tag on the comment - * @return {Promise} - */ - static insertCommentTag(tag) { - - if (ALLOWED_COMMENT_TAGS.find((t) => t.name === tag.name) == null) { - return Promise.reject(new Error('tag not allowed')); - } - - if (ALLOWED_PRIVACY_TYPE.find((p) => p.privacy_type === tag.privacy_type) == null) { - return Promise.reject(new Error('privacy type not allowed')); - } - - // Create/Update the tag. - let newtag = new TagModel({ - name: tag.name, - item_id: tag.item_id, - item_type: tag.item_type, - assigned_by: tag.user_id, - privacy_type: tag.privacy_type - }); - return newtag.save(); - } - -}; diff --git a/test/server/graph/loaders/metrics.js b/test/server/graph/loaders/metrics.js index a24c18f79..5d74f154d 100644 --- a/test/server/graph/loaders/metrics.js +++ b/test/server/graph/loaders/metrics.js @@ -23,11 +23,27 @@ describe('graph.loaders.Metrics', () => { describe('different comment states', () => { - beforeEach(() => CommentModel.create([ - {id: '1', body: 'a new comment!'}, - {id: '2', body: 'a new comment!'}, - {id: '3', body: 'a new comment!'} - ])); + beforeEach(() =>{ + CommentModel.create( {id: '1', body: 'a new comment!'}) + .then((comment) => { + console.log('*************** wtf comment', comment); + }) + .catch((error) => { + console.log('1 debug the rror create ', error); + }); + console.log('debug 1'); + CommentModel.create({id: '2', body: 'a new comment!'}) + .catch((error) => { + console.log('2 debug the rror create ', error); + }); + console.log('debug 2'); + CommentModel.create({id: '3', body: 'a new comment!'}) + .catch((error) => { + console.log('3 debug the rror create ', error); + }); + console.log('debug 3'); + } + ); [ {flagged: 0, actions: []}, @@ -40,6 +56,7 @@ describe('graph.loaders.Metrics', () => { ]} ].forEach(({flagged, actions}) => { + console.log('-----------------> debug forEach'); describe(`with actions=${actions.length}`, () => { beforeEach(() => ActionModel.create(actions)); @@ -52,7 +69,6 @@ describe('graph.loaders.Metrics', () => { to: (new Date()).setMinutes((new Date()).getMinutes() + 5) }) .then(({data, errors}) => { - console.log(errors); expect(errors).to.be.undefined; expect(data.flagged).to.have.length(flagged); }); diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 01966bd88..83a12dc2e 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -4,7 +4,6 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); -const TagService = require('../../../../services/tags'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); @@ -16,8 +15,8 @@ describe('graph.mutations.addCommentTag', () => { }); const query = ` - mutation AddCommentTag ($id: ID!, $tag: String!, $privacy_type: String!) { - addCommentTag(id:$id, tag:$tag, privacy_type:$privacy_type) { + mutation AddCommentTag ($id: ID!, $tag: String!) { + addCommentTag(id:$id, tag:$tag) { comment { id } @@ -31,14 +30,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', privacy_type: 'PUBLIC'}); + 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; - return TagService.findByItemIdAndName(response.data.addCommentTag.comment.id, 'BEST', 'COMMENTS') - .then((tags) => { + return CommentsService.findById(response.data.addCommentTag.comment.id) + .then(({tags}) => { expect(tags).to.have.length(1); }); }); diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index d132c46c7..31cb9872f 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -9,7 +9,7 @@ const AssetModel = require('../../../../models/asset'); const ActionModel = require('../../../../models/action'); const SettingsService = require('../../../../services/settings'); -const TagService = require('../../../../services/tags'); +const CommentsService = require('../../../../services/comments'); describe('graph.mutations.createComment', () => { beforeEach(() => SettingsService.init()); @@ -223,9 +223,9 @@ describe('graph.mutations.createComment', () => { expect(data.createComment).to.have.property('comment').not.null; expect(data.createComment).to.have.property('errors').null; - return TagService.findByItemIdAndName(data.createComment.comment.id, tag, 'COMMENTS'); + return CommentsService.findById(data.createComment.comment.id); }) - .then((tags) => { + .then(({tags}) => { if (tag) { expect(tags).to.have.length(1); expect(tags[0]).to.have.property('name', tag); diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 7660eef35..4deb4bd5e 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -7,7 +7,6 @@ const UserModel = require('../../../../models/user'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); -const TagService = require('../../../../services/tags'); describe('graph.mutations.removeCommentTag', () => { let comment; @@ -42,8 +41,8 @@ describe('graph.mutations.removeCommentTag', () => { expect(response.errors).to.be.empty; expect(response.data.removeCommentTag.errors).to.be.null; - TagService.findByItemIdAndName(response.data.removeCommentTag.comment.id, 'BEST') - .then((tags) => { + CommentsService.findById(response.data.removeCommentTag.comment.id) + .then(({tags}) => { expect(tags).to.deep.equal([]); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index d4fc5f17e..ed908d1b2 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -5,7 +5,6 @@ const ActionsService = require('../../../services/actions'); const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); const CommentsService = require('../../../services/comments'); -const TagService = require('../../../services/tags'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; @@ -226,7 +225,7 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; const userId = users[0].id; await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); - const tags = await TagService.findByItemIdAndName(commentId, 'BEST', 'COMMENTS'); + const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(1); expect(tags[0].name).to.equal(tagName); expect(tags[0].assigned_by).to.equal(userId); @@ -257,13 +256,13 @@ describe('services.CommentsService', () => { const commentId = comments[0].id; const tagName = 'BEST'; await CommentsService.addTag(commentId, tagName, users[0].id, 'PUBLIC'); - const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(1); // ok now to remove it await CommentsService.removeTag(commentId, tagName); - const tags2 = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); - expect(tags2.length).to.equal(0); + 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; @@ -271,7 +270,7 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; // just make sure it has no tags to start - const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(0); // ok now to remove it From d7e72bb1387e1732ee6a543d88efca89f029b0ce Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 5 May 2017 14:09:00 -0700 Subject: [PATCH 08/56] Fix tests. --- graph/mutators/comment.js | 4 +- services/comments.js | 39 ++++++++++++-------- test/server/graph/loaders/metrics.js | 25 ++----------- test/server/graph/mutations/createComment.js | 4 +- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 3413c6f4e..427ab4c63 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -28,7 +28,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par .then(async (comment) => { if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user.id); + await CommentsService.addTag(comment.id, 'STAFF', user); } // If the loaders are present, clear the caches for these values because we @@ -209,7 +209,7 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { * @param {String} tag name of the tag */ const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.addTag(id, tag, user.id); + return CommentsService.addTag(id, tag, user); }; /** diff --git a/services/comments.js b/services/comments.js index 4c8f423c6..41c383725 100644 --- a/services/comments.js +++ b/services/comments.js @@ -2,7 +2,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 STATUSES = [ @@ -25,8 +25,6 @@ module.exports = class CommentsService { */ static publicCreate(comment) { - console.log('-----------------> debug publicCreate'); - // Check to see if this is an array of comments, if so map it out. if (Array.isArray(comment)) { return Promise.all(comment.map(CommentsService.publicCreate)); @@ -56,22 +54,33 @@ module.exports = class CommentsService { */ static addTag(id, name, added_by) { - console.log('-----------------> debug addtag', name); - - SettingsService.retrieve() - .then(({tags}) => { - if (!ALLOWED_TAGS.includes(name) || tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { + // Check that the tag is allowed by the system OR in our setting.tags. + return SettingsService.retrieve() + .then((settings) => { + + // Moderators or ADMIN can add any tag automatically. + if (added_by != null && (added_by.hasRoles('ADMIN') || added_by.hasRoles('MODERATOR'))) { + SettingModel.findOneAndUpdate({id: settings.id}, { + $push: { + tags: { + id: name, + added_by: added_by.id + } + } + }); + } + else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { return Promise.reject(new Error('tag not allowed')); } - }); - return CommentModel.findOneAndUpdate({id}, { - $push: { - tags: { - id: name, - added_by: added_by + return CommentModel.findOneAndUpdate({id}, { + $push: { + tags: { + id: name, + added_by: added_by.id + } } - } + }); }); } diff --git a/test/server/graph/loaders/metrics.js b/test/server/graph/loaders/metrics.js index 5d74f154d..0d2741ef8 100644 --- a/test/server/graph/loaders/metrics.js +++ b/test/server/graph/loaders/metrics.js @@ -23,27 +23,11 @@ describe('graph.loaders.Metrics', () => { describe('different comment states', () => { - beforeEach(() =>{ - CommentModel.create( {id: '1', body: 'a new comment!'}) - .then((comment) => { - console.log('*************** wtf comment', comment); - }) - .catch((error) => { - console.log('1 debug the rror create ', error); - }); - console.log('debug 1'); - CommentModel.create({id: '2', body: 'a new comment!'}) - .catch((error) => { - console.log('2 debug the rror create ', error); - }); - console.log('debug 2'); + beforeEach(() =>[ + CommentModel.create( {id: '1', body: 'a new comment!'}), + CommentModel.create({id: '2', body: 'a new comment!'}), CommentModel.create({id: '3', body: 'a new comment!'}) - .catch((error) => { - console.log('3 debug the rror create ', error); - }); - console.log('debug 3'); - } - ); + ]); [ {flagged: 0, actions: []}, @@ -56,7 +40,6 @@ describe('graph.loaders.Metrics', () => { ]} ].forEach(({flagged, actions}) => { - console.log('-----------------> debug forEach'); describe(`with actions=${actions.length}`, () => { beforeEach(() => ActionModel.create(actions)); diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index 31cb9872f..41822fcdb 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -21,7 +21,7 @@ describe('graph.mutations.createComment', () => { id status tags { - name + id } } errors { @@ -228,7 +228,7 @@ describe('graph.mutations.createComment', () => { .then(({tags}) => { if (tag) { expect(tags).to.have.length(1); - expect(tags[0]).to.have.property('name', tag); + expect(tags[0]).to.have.property('id', tag); } else { expect(tags).length(0); } From c72cb2428e241dd6b97790ff22542d2b26bb137c Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 5 May 2017 14:33:02 -0700 Subject: [PATCH 09/56] Fixed errors on tags creation --- services/comments.js | 4 ++-- test/server/graph/mutations/removeCommentTag.js | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/services/comments.js b/services/comments.js index 41c383725..aae5c8248 100644 --- a/services/comments.js +++ b/services/comments.js @@ -57,14 +57,14 @@ module.exports = class CommentsService { // Check that the tag is allowed by the system OR in our setting.tags. return SettingsService.retrieve() .then((settings) => { - + // Moderators or ADMIN can add any tag automatically. if (added_by != null && (added_by.hasRoles('ADMIN') || added_by.hasRoles('MODERATOR'))) { SettingModel.findOneAndUpdate({id: settings.id}, { $push: { tags: { id: name, - added_by: added_by.id + models: ['COMMENTS'] } } }); diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 4deb4bd5e..663e0de9a 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -4,6 +4,7 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); +const SettingModel = require('../../../../models/setting'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); @@ -33,7 +34,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user.id, 'PUBLIC'); + await CommentsService.addTag(comment.id, 'BEST', user); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); @@ -49,6 +50,16 @@ describe('graph.mutations.removeCommentTag', () => { }); describe('users who cant remove tags', () => { + + // allow the tag in the settings + SettingModel.findOneAndUpdate({id: 1}, { + $push: { + tags: { + id: 'BEST', + models: ['COMMENTS'] + } + } + }); Object.entries({ 'anonymous': undefined, 'regular commenter': new UserModel({}), @@ -58,7 +69,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user ? user.id : null, 'PUBLIC'); + await CommentsService.addTag(comment.id, 'BEST', user); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); From 607ab6c0d6c998542a64746eebd3ff88fb409e86 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 5 May 2017 13:14:34 -0600 Subject: [PATCH 10/56] Added privacy fix --- models/user.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/models/user.js b/models/user.js index 159407fdf..95a1422f2 100644 --- a/models/user.js +++ b/models/user.js @@ -136,6 +136,14 @@ const UserSchema = new mongoose.Schema({ timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' + }, + + toJSON: { + transform: function (doc, ret) { + delete ret.password; + delete ret._id; + delete ret.__v; + } } }); From 85fae3104e475889cfd85316813f642e66bfc0f6 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 5 May 2017 14:05:52 -0600 Subject: [PATCH 11/56] float: right; in css is just fine. --- .../coral-configure/components/ConfigureCommentStream.css | 6 ++---- client/coral-configure/components/ConfigureCommentStream.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/client/coral-configure/components/ConfigureCommentStream.css b/client/coral-configure/components/ConfigureCommentStream.css index 335c94377..a8cbc36cb 100644 --- a/client/coral-configure/components/ConfigureCommentStream.css +++ b/client/coral-configure/components/ConfigureCommentStream.css @@ -3,10 +3,8 @@ } .apply { - position: absolute; - top: 38%; - transform: translateX(-50%); - right: 0; + float: right; + margin: 0 10px; } .wrapper ul { diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index bef53e1ef..6c00b5996 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -12,7 +12,6 @@ export default ({handleChange, handleApply, changed, ...props}) => (

{lang.t('configureCommentStream.title')}

-

{lang.t('configureCommentStream.description')}

+

{lang.t('configureCommentStream.description')}

  • From c4c0ee79cd48a3972bb2f2e8e1d690131b48c9ca Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 12:57:37 -0700 Subject: [PATCH 12/56] Fix tests. --- errors.js | 14 ++++++ graph/mutators/comment.js | 4 +- models/user.js | 42 +++++++++++------- services/comments.js | 43 ++++++++++++------- services/users.js | 1 + test/server/graph/mutations/ignoreUser.js | 11 +++++ .../graph/mutations/removeCommentTag.js | 39 +++++++++-------- test/server/services/comments.js | 6 +-- 8 files changed, 106 insertions(+), 54 deletions(-) diff --git a/errors.js b/errors.js index 67ae0693a..9f12c7835 100644 --- a/errors.js +++ b/errors.js @@ -75,6 +75,18 @@ const ErrMissingToken = new APIError('token is required', { status: 400 }); +// ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. +const ErrNoCommentFound = new APIError('comment does not exist', { + translation_key: 'COMMENT_NOT_FOUND', + status: 400 +}); + +// ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. +const ErrorTagNotAllowed = new APIError('tag not allowed', { + translation_key: 'TAG_NOT_ALLOWED', + status: 400 +}); + // ErrAssetCommentingClosed is returned when a comment or action is attempted on // a stream where commenting has been closed. class ErrAssetCommentingClosed extends APIError { @@ -161,6 +173,8 @@ module.exports = { ErrMissingEmail, ErrMissingPassword, ErrMissingToken, + ErrNoCommentFound, + ErrorTagNotAllowed, ErrEmailTaken, ErrSpecialChars, ErrMissingUsername, diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 427ab4c63..3413c6f4e 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -28,7 +28,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par .then(async (comment) => { if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user); + await CommentsService.addTag(comment.id, 'STAFF', user.id); } // If the loaders are present, clear the caches for these values because we @@ -209,7 +209,7 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { * @param {String} tag name of the tag */ const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.addTag(id, tag, user); + return CommentsService.addTag(id, tag, user.id); }; /** diff --git a/models/user.js b/models/user.js index 73e04c392..688c37c2c 100644 --- a/models/user.js +++ b/models/user.js @@ -1,5 +1,6 @@ const mongoose = require('../services/mongoose'); const bcrypt = require('bcrypt'); +const Schema = mongoose.Schema; const uuid = require('uuid'); // USER_ROLES is the array of roles that is permissible as a user role. @@ -16,9 +17,32 @@ const USER_STATUS = [ '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 +// }); + // ProfileSchema is the mongoose schema defined as the representation of a // User's profile stored in MongoDB. -const ProfileSchema = new mongoose.Schema({ +const ProfileSchema = new Schema({ // ID provides the identifier for the user profile, in the case of a local // provider, the id would be an email, in the case of a social provider, @@ -41,7 +65,7 @@ const ProfileSchema = new mongoose.Schema({ // used by the `local` provider to indicate when the email address was // confirmed. metadata: { - type: mongoose.Schema.Types.Mixed + type: Schema.Types.Mixed } }, { _id: false @@ -49,7 +73,7 @@ const ProfileSchema = new mongoose.Schema({ // UserSchema is the mongoose schema defined as the representation of a User in // MongoDB. -const UserSchema = new mongoose.Schema({ +const UserSchema = new Schema({ // This ID represents the most unique identifier for a user, it is generated // when the user is created as a random uuid. @@ -111,18 +135,6 @@ const UserSchema = new mongoose.Schema({ default: false }, - tags: [{ - id: { - type: String, - unique: true - }, - public: Boolean, - text: [{ - type: mongoose.Schema.Types.Mixed, - default: null - }] - }], - // User's settings settings: { bio: { diff --git a/services/comments.js b/services/comments.js index aae5c8248..db4dd18c1 100644 --- a/services/comments.js +++ b/services/comments.js @@ -4,6 +4,9 @@ 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', @@ -58,29 +61,37 @@ module.exports = class CommentsService { return SettingsService.retrieve() .then((settings) => { - // Moderators or ADMIN can add any tag automatically. - if (added_by != null && (added_by.hasRoles('ADMIN') || added_by.hasRoles('MODERATOR'))) { - SettingModel.findOneAndUpdate({id: settings.id}, { - $push: { - tags: { - id: name, - models: ['COMMENTS'] + UsersService.findById(added_by) + .then((user) => { + + // 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(new Error('tag not allowed')); - } + }); + } + else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { + return Promise.reject(errors.ErrorTagNotAllowed); + } + }); return CommentModel.findOneAndUpdate({id}, { $push: { tags: { id: name, - added_by: added_by.id + added_by: added_by } - } - }); + }, + }, + { + new: false, + upsert: false + }); }); } diff --git a/services/users.js b/services/users.js index 14301ccf4..c33b18780 100644 --- a/services/users.js +++ b/services/users.js @@ -339,6 +339,7 @@ module.exports = class UsersService { if (err.message.match('Username')) { return reject(errors.ErrUsernameTaken); } + console.log('DEBUG FUCKING ERROR', err); return reject(errors.ErrEmailTaken); } return reject(err); diff --git a/test/server/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js index 54d6305a1..94243a690 100644 --- a/test/server/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -32,11 +32,18 @@ describe('graph.mutations.ignoreUser', () => { // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { + UsersService.findLocalUser('usernameB@example.com') + .then((user) => { + console.log('--------- debug user', user); + }); const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); + console.log('--------------- debug aca -2'); const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); + console.log('--------------- debug aca -1'); const context = new Context({user}); const ignoreUserResponse = await graphql(schema, ignoreUserMutation, {}, context, {id: userToIgnore.id}); if (ignoreUserResponse.errors && ignoreUserResponse.errors.length) { + console.log('--------------- debug aca 0'); console.error(ignoreUserResponse.errors); } expect(ignoreUserResponse.errors).to.be.empty; @@ -44,13 +51,17 @@ describe('graph.mutations.ignoreUser', () => { // now check my ignored users const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); if (myIgnoredUsersResponse.errors && myIgnoredUsersResponse.errors.length) { + console.log('debug aca 1'); console.error(myIgnoredUsersResponse.errors); } expect(myIgnoredUsersResponse.errors).to.be.empty; const myIgnoredUsers = myIgnoredUsersResponse.data.myIgnoredUsers; expect(myIgnoredUsers.length).to.equal(1); + console.log('debug aca 2'); expect(myIgnoredUsers[0].id).to.equal(userToIgnore.id); + console.log('debug aca 3'); expect(myIgnoredUsers[0].username).to.equal(userToIgnore.username); + console.log('debug aca 4'); }); it('users cannot ignore themselves', async () => { diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 663e0de9a..760887a44 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -34,7 +34,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user); + await CommentsService.addTag(comment.id, 'BEST', user.id); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); @@ -59,25 +59,28 @@ describe('graph.mutations.removeCommentTag', () => { models: ['COMMENTS'] } } - }); - 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}); + }) + .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); - 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, 'BEST', user.id); - 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, tag: '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'}]); + expect(response.data.removeCommentTag.comment).to.be.null; + }); }); }); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index ed908d1b2..e01fc7bad 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -227,8 +227,8 @@ describe('services.CommentsService', () => { await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(1); - expect(tags[0].name).to.equal(tagName); - expect(tags[0].assigned_by).to.equal(userId); + 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 () => { @@ -238,7 +238,7 @@ describe('services.CommentsService', () => { await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; }); - it('can\'t add same tag.name twice', async () => { + it('can\'t add same tag.id twice', async () => { const commentId = comments[0].id; const tagName = 'BEST'; const userId = users[0].id; From 743a96cd217f7be2790658ca9e7455d6ece4ff81 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 13:09:17 -0700 Subject: [PATCH 13/56] Removes debug statements. --- services/users.js | 1 - test/server/graph/mutations/ignoreUser.js | 12 +----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/services/users.js b/services/users.js index c33b18780..14301ccf4 100644 --- a/services/users.js +++ b/services/users.js @@ -339,7 +339,6 @@ module.exports = class UsersService { if (err.message.match('Username')) { return reject(errors.ErrUsernameTaken); } - console.log('DEBUG FUCKING ERROR', err); return reject(errors.ErrEmailTaken); } return reject(err); diff --git a/test/server/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js index 94243a690..8de54a41d 100644 --- a/test/server/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -32,18 +32,12 @@ describe('graph.mutations.ignoreUser', () => { // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { - UsersService.findLocalUser('usernameB@example.com') - .then((user) => { - console.log('--------- debug user', user); - }); + UsersService.findLocalUser('usernameB@example.com'); const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); - console.log('--------------- debug aca -2'); const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); - console.log('--------------- debug aca -1'); const context = new Context({user}); const ignoreUserResponse = await graphql(schema, ignoreUserMutation, {}, context, {id: userToIgnore.id}); if (ignoreUserResponse.errors && ignoreUserResponse.errors.length) { - console.log('--------------- debug aca 0'); console.error(ignoreUserResponse.errors); } expect(ignoreUserResponse.errors).to.be.empty; @@ -51,17 +45,13 @@ describe('graph.mutations.ignoreUser', () => { // now check my ignored users const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); if (myIgnoredUsersResponse.errors && myIgnoredUsersResponse.errors.length) { - console.log('debug aca 1'); console.error(myIgnoredUsersResponse.errors); } expect(myIgnoredUsersResponse.errors).to.be.empty; const myIgnoredUsers = myIgnoredUsersResponse.data.myIgnoredUsers; expect(myIgnoredUsers.length).to.equal(1); - console.log('debug aca 2'); expect(myIgnoredUsers[0].id).to.equal(userToIgnore.id); - console.log('debug aca 3'); expect(myIgnoredUsers[0].username).to.equal(userToIgnore.username); - console.log('debug aca 4'); }); it('users cannot ignore themselves', async () => { From 96cd5197e3b5b0804ccfc28d27edca6c332b3377 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 16:05:40 -0700 Subject: [PATCH 14/56] Test was failing as I was not returning an error. --- errors.js | 4 ++-- models/comment.js | 9 +++++++ services/comments.js | 29 +++++++++++++++++------ test/server/graph/mutations/ignoreUser.js | 1 - test/server/services/comments.js | 11 +++++---- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/errors.js b/errors.js index 9f12c7835..5c2b05f92 100644 --- a/errors.js +++ b/errors.js @@ -82,7 +82,7 @@ const ErrNoCommentFound = new APIError('comment does not exist', { }); // ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. -const ErrorTagNotAllowed = new APIError('tag not allowed', { +const ErrTagNotAllowed = new APIError('tag not allowed', { translation_key: 'TAG_NOT_ALLOWED', status: 400 }); @@ -174,7 +174,7 @@ module.exports = { ErrMissingPassword, ErrMissingToken, ErrNoCommentFound, - ErrorTagNotAllowed, + ErrTagNotAllowed, ErrEmailTaken, ErrSpecialChars, ErrMissingUsername, diff --git a/models/comment.js b/models/comment.js index f3e239080..219ec055d 100644 --- a/models/comment.js +++ b/models/comment.js @@ -92,6 +92,15 @@ const CommentSchema = new Schema({ } }); +// Add the indexes on the comment tag. +CommentSchema.index({ + 'id': 1, + 'tags.id': 1 +}, { + unique: true, + background: false +}); + // Comment model. const Comment = mongoose.model('Comment', CommentSchema); diff --git a/services/comments.js b/services/comments.js index db4dd18c1..e1873ba02 100644 --- a/services/comments.js +++ b/services/comments.js @@ -76,21 +76,26 @@ module.exports = class CommentsService { }); } else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { - return Promise.reject(errors.ErrorTagNotAllowed); + return Promise.reject(errors.ErrTagNotAllowed); } }); - return CommentModel.findOneAndUpdate({id}, { + return CommentModel.findOneAndUpdate({id, 'tags.id': {$ne: name}}, { $push: { tags: { id: name, added_by: added_by } }, - }, - { - new: false, - upsert: false + }) + .then(({nModified}) => { + switch (nModified) { + case 0: + return Promise.reject(errors.ErrNoCommentFound); + case 1: + return; + default: + } }); }); } @@ -102,12 +107,22 @@ module.exports = class CommentsService { * @param {String} tag_id the id of the tag to remove */ static removeTag(id, tag_id) { - return CommentModel.findOneAndUpdate({id}, { + return CommentModel.findOneAndUpdate({id, 'tags.id': tag_id}, { $pull: { tags: { id: tag_id } } + } + ) + .then(({nModified}) => { + switch(nModified) { + case 0: + return Promise.reject(errors.ErrNoCommentFound); + case 1: + return; + default: + } }); } diff --git a/test/server/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js index 8de54a41d..54d6305a1 100644 --- a/test/server/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -32,7 +32,6 @@ describe('graph.mutations.ignoreUser', () => { // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { - UsersService.findLocalUser('usernameB@example.com'); const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); const context = new Context({user}); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index e01fc7bad..27657efa5 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -236,7 +236,10 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; const userId = users[0].id; - await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; + CommentsService.addTag(commentId, tagName, userId) + .catch((error) => { + expect(error).to.not.be.null; + }); }); it('can\'t add same tag.id twice', async () => { const commentId = comments[0].id; @@ -244,10 +247,10 @@ describe('services.CommentsService', () => { const userId = users[0].id; // first time - await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); + await CommentsService.addTag(commentId, tagName, userId); // second time should fail - await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; + await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; }); }); @@ -255,7 +258,7 @@ describe('services.CommentsService', () => { it('removes a tag', async () => { const commentId = comments[0].id; const tagName = 'BEST'; - await CommentsService.addTag(commentId, tagName, users[0].id, 'PUBLIC'); + await CommentsService.addTag(commentId, tagName, users[0].id); const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(1); From 7c09902294e7c9217ed751548606735c63a509ac Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 16:55:11 -0700 Subject: [PATCH 15/56] The name for the tag is called ID. --- .../src/containers/Comment.js | 2 +- .../graphql/fragments/commentView.graphql | 2 +- .../graphql/mutations/addCommentTag.graphql | 2 +- .../graphql/mutations/removeCommentTag.graphql | 2 +- graph/loaders/settings.js | 18 ++---------------- 5 files changed, 6 insertions(+), 20 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 9b48e0809..09723b973 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -28,7 +28,7 @@ export default withFragments({ created_at status tags { - name + id } user { id diff --git a/client/coral-framework/graphql/fragments/commentView.graphql b/client/coral-framework/graphql/fragments/commentView.graphql index 0ed5e00b8..71a6ae2d4 100644 --- a/client/coral-framework/graphql/fragments/commentView.graphql +++ b/client/coral-framework/graphql/fragments/commentView.graphql @@ -6,7 +6,7 @@ fragment commentView on Comment { created_at status tags { - name + id } user { id diff --git a/client/coral-framework/graphql/mutations/addCommentTag.graphql b/client/coral-framework/graphql/mutations/addCommentTag.graphql index 5fd63868e..a4ff36da4 100644 --- a/client/coral-framework/graphql/mutations/addCommentTag.graphql +++ b/client/coral-framework/graphql/mutations/addCommentTag.graphql @@ -3,7 +3,7 @@ mutation AddCommentTag ($id: ID!, $tag: String!) { comment { id tags { - name + id } } errors { diff --git a/client/coral-framework/graphql/mutations/removeCommentTag.graphql b/client/coral-framework/graphql/mutations/removeCommentTag.graphql index 3826b0703..642466f00 100644 --- a/client/coral-framework/graphql/mutations/removeCommentTag.graphql +++ b/client/coral-framework/graphql/mutations/removeCommentTag.graphql @@ -3,7 +3,7 @@ mutation RemoveCommentTag ($id: ID!, $tag: String!) { comment { id tags { - name + id } } errors { diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js index 9d777255f..83545821d 100644 --- a/graph/loaders/settings.js +++ b/graph/loaders/settings.js @@ -1,25 +1,11 @@ const SettingsService = require('../../services/settings'); -const SettingModel = require('../../models/setting'); - const util = require('./util'); -/** - * Search for tags based on their item_type. - * @param {String} item_type the item type to search by - * @return {Promise} resolves to distinct items tags - */ -const getByItemType = (_, item_type) => { - return SettingModel.distinct('tags.models', {item_type}); -}; - /** * Creates a set of loaders based on a GraphQL context. * @param {Object} context the context of the GraphQL request * @return {Object} object of loaders */ -module.exports = (context) => ({ - Settings: { - get: new util.SingletonResolver(() => SettingsService.retrieve()), - getTagsByItemType: (item_type) => getByItemType(context, item_type) - } +module.exports = () => ({ + Settings: new util.SingletonResolver(() => SettingsService.retrieve()) }); From da0cebbda72b2e6b2e8335987c8dfc8179193ae7 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 17:06:49 -0700 Subject: [PATCH 16/56] Fix the plugin TagLabel to show the staff tag. --- client/coral-embed-stream/src/components/Comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 4349fe38d..b58bf2957 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -23,7 +23,7 @@ import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import styles from './Comment.css'; -const isStaff = tags => !tags.every(t => t.name !== 'STAFF'); +const isStaff = tags => !tags.every(t => t.id !== 'STAFF'); // hold actions links (e.g. Reply) along the comment footer const ActionButton = ({children}) => { @@ -72,7 +72,7 @@ class Comment extends React.Component { id: PropTypes.string.isRequired, tags: PropTypes.arrayOf( PropTypes.shape({ - name: PropTypes.string + id: PropTypes.string }) ), replies: PropTypes.arrayOf( From 2e1bc8d75a2b21f3a8ffac41653ae78ede367c6a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 9 May 2017 15:01:01 -0600 Subject: [PATCH 17/56] 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); + } }); }); From 0d47ee895c4262fa15709578613f9797ae6d4845 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 9 May 2017 15:02:31 -0600 Subject: [PATCH 18/56] removed unused service --- services/tags.js | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 services/tags.js diff --git a/services/tags.js b/services/tags.js deleted file mode 100644 index 94212cb13..000000000 --- a/services/tags.js +++ /dev/null @@ -1,5 +0,0 @@ -class TagsService { - -} - -module.exports = TagsService; From aeaf809a0f5c9ef618b4781549d840b82490588b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 11 May 2017 14:42:06 -0600 Subject: [PATCH 19/56] Made queries more generic related to tags --- graph/mutators/comment.js | 148 +----------------- graph/mutators/index.js | 2 + graph/mutators/tag.js | 82 ++++++++++ graph/resolvers/asset.js | 5 + graph/resolvers/comment.js | 5 + graph/resolvers/root_mutation.js | 8 +- graph/resolvers/user.js | 5 + graph/resolvers/util.js | 16 ++ graph/typeDefs.graphql | 112 ++++++++----- models/asset.js | 4 + models/comment.js | 2 + models/user.js | 10 +- .../server/typeDefs.graphql | 2 +- services/tags.js | 133 ++++++++++++++++ .../mutations/{addCommentTag.js => addTag.js} | 24 +-- .../{removeCommentTag.js => removeTag.js} | 10 +- 16 files changed, 356 insertions(+), 212 deletions(-) create mode 100644 graph/mutators/tag.js create mode 100644 graph/resolvers/util.js create mode 100644 services/tags.js rename test/server/graph/mutations/{addCommentTag.js => addTag.js} (65%) rename test/server/graph/mutations/{removeCommentTag.js => removeTag.js} (87%) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 3a57f3b3a..23c906f15 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -210,149 +210,11 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { }); }; -/** - * Adds a tag to a Comment. - */ -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. - */ -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) => { let mutators = { Comment: { create: () => Promise.reject(errors.ErrNotAuthorized), - setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized), - addCommentTag: () => Promise.reject(errors.ErrNotAuthorized), - removeCommentTag: () => Promise.reject(errors.ErrNotAuthorized), + setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized) } }; @@ -364,13 +226,5 @@ module.exports = (context) => { mutators.Comment.setCommentStatus = (action) => setCommentStatus(context, action); } - if (context.user && context.user.can('mutation:addCommentTag')) { - mutators.Comment.addCommentTag = (action) => addCommentTag(context, action); - } - - if (context.user && context.user.can('mutation:removeCommentTag')) { - mutators.Comment.removeCommentTag = (action) => removeCommentTag(context, action); - } - return mutators; }; diff --git a/graph/mutators/index.js b/graph/mutators/index.js index 9975bedde..0076a5e8f 100644 --- a/graph/mutators/index.js +++ b/graph/mutators/index.js @@ -3,6 +3,7 @@ const debug = require('debug')('talk:graph:mutators'); const Comment = require('./comment'); const Action = require('./action'); +const Tag = require('./tag'); const User = require('./user'); const plugins = require('../../services/plugins'); @@ -12,6 +13,7 @@ let mutators = [ // Load in the core mutators. Comment, Action, + Tag, User, // Load the plugin mutators from the manager. diff --git a/graph/mutators/tag.js b/graph/mutators/tag.js new file mode 100644 index 000000000..3182a7d00 --- /dev/null +++ b/graph/mutators/tag.js @@ -0,0 +1,82 @@ +const TagsService = require('../../services/tags'); +const errors = require('../../errors'); + +/** + * Modifies the targeted model with the specified operation to add/remove a tag. + */ +const modify = async ({user}, operation, {name, id, item_type, asset_id}) => { + + // Try to find the tag in the global list. This will contain the permission + // information if it's found. + let tag = await TagsService.get({name, id, item_type, asset_id}); + + // Create the new tagLink that will be created to interact 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 + // modify 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 modify the tag without checking for ownership. + if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { + return operation(id, item_type, 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 modified. + return operation(id, item_type, tagLink, true); + } + + throw errors.ErrNotAuthorized; + } + + // Only admin/moderators can modify 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: [item_type], + created_at: new Date() + }; + + // Actually modify the tag on the model. + return operation(id, item_type, tagLink, false); +}; + +module.exports = (context) => { + let mutators = { + Tag: { + add: () => Promise.reject(errors.ErrNotAuthorized), + remove: () => Promise.reject(errors.ErrNotAuthorized) + } + }; + + if (context.user && context.user.can('mutation:addTag')) { + mutators.Tag.add = (tag) => modify(context, TagsService.add, tag); + } + + if (context.user && context.user.can('mutation:removeTag')) { + mutators.Tag.remove = (tag) => modify(context, TagsService.remove, tag); + } + + return mutators; +}; diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index c260d0070..012e77927 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -1,3 +1,5 @@ +const {decorateWithTags} = require('./util'); + const Asset = { lastComment({id}, _, {loaders: {Comments}}) { return Comments.getByQuery({ @@ -49,4 +51,7 @@ const Asset = { } }; +// Decorate the Asset type resolver with a tags field. +decorateWithTags(Asset); + module.exports = Asset; diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 88ddbee07..b0251f626 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -1,3 +1,5 @@ +const {decorateWithTags} = require('./util'); + const Comment = { parent({parent_id}, _, {loaders: {Comments}}) { if (parent_id == null) { @@ -48,4 +50,7 @@ const Comment = { } }; +// Decorate the Comment type resolver with a tags field. +decorateWithTags(Comment); + module.exports = Comment; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 442845bd2..3d18bb277 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -28,11 +28,11 @@ const RootMutation = { setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, - addCommentTag(_, {id, asset_id, name}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.addCommentTag({id, asset_id, name})); + addTag(_, {tag}, {mutators: {Tag}}) { + return wrapResponse(null)(Tag.add(tag)); }, - removeCommentTag(_, {id, asset_id, name}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.removeCommentTag({id, asset_id, name})); + removeTag(_, {tag}, {mutators: {Tag}}) { + return wrapResponse(null)(Tag.remove(tag)); } }; diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index d8ed7ee15..53f4f7c1b 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -1,3 +1,5 @@ +const {decorateWithTags} = require('./util'); + const User = { action_summaries({id}, _, {loaders: {Actions}}) { return Actions.getSummariesByItemID.load(id); @@ -31,4 +33,7 @@ const User = { } }; +// Decorate the User type resolver with a tags field. +decorateWithTags(User); + module.exports = User; diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js new file mode 100644 index 000000000..11cfcbb13 --- /dev/null +++ b/graph/resolvers/util.js @@ -0,0 +1,16 @@ +/** + * Decorates the typeResolver with the tags field. + */ +const decorateWithTags = (typeResolver) => { + typeResolver.tags = ({tags = []}, _, {user}) => { + if (user && user.hasRoles('ADMIN')) { + return tags; + } + + return tags.filter((tag) => tag.public); + }; +}; + +module.exports = { + decorateWithTags +}; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index bbe3e4d17..b6d29e5ef 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -39,6 +39,9 @@ type User { # the current roles of the user. roles: [USER_ROLES] + # the tags on the user + tags: [TagLink!] + # determines whether the user can edit their username canEditName: Boolean @@ -52,6 +55,38 @@ type User { status: USER_STATUS } +# UsersQuery allows the ability to query users by a specific fields. +input UsersQuery { + action_type: ACTION_TYPE + + # Limit the number of results to be returned. + limit: Int = 10 + + # Skip results from the last created_at timestamp. + cursor: Date + + # Sort the results by created_at. + sort: SORT_ORDER = REVERSE_CHRONOLOGICAL +} + + +################################################################################ +## Tags +################################################################################ + +# Used to represent the item type for a tag. +enum TAGGABLE_ITEM_TYPE { + + # The action references a entity of type Asset. + ASSETS + + # The action references a entity of type Comment. + COMMENTS + + # The action references a entity of type User. + USERS +} + # Tag represents the underlying Tag that can be either stored in a global list # or added uniquely to the entity. type Tag { @@ -79,20 +114,6 @@ type TagLink { created_at: Date! } -# UsersQuery allows the ability to query users by a specific fields. -input UsersQuery { - action_type: ACTION_TYPE - - # Limit the number of results to be returned. - limit: Int = 10 - - # Skip results from the last created_at timestamp. - cursor: Date - - # Sort the results by created_at. - sort: SORT_ORDER = REVERSE_CHRONOLOGICAL -} - ################################################################################ ## Comments ################################################################################ @@ -459,6 +480,9 @@ type Asset { # The date that the asset was created. created_at: Date + # the tags on the asset + tags: [TagLink!] + # The author(s) of the asset. author: String } @@ -495,7 +519,7 @@ type ValidationUserError implements UserError { } ################################################################################ -## Queries; +## Queries ################################################################################ # Establishes the ordering of the content by their created_at time stamp. @@ -577,7 +601,7 @@ type RootQuery { interface Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # CreateCommentResponse is returned with the comment that was created and any @@ -588,7 +612,7 @@ type CreateCommentResponse implements Response { comment: Comment # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # Used to represent the item type for an action. @@ -653,7 +677,7 @@ type CreateFlagResponse implements Response { flag: FlagAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } @@ -666,7 +690,7 @@ type CreateDontAgreeResponse implements Response { dontagree: DontAgreeAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } input CreateDontAgreeInput { @@ -689,7 +713,7 @@ input CreateDontAgreeInput { type DeleteActionResponse implements Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # SetUserStatusResponse is the response returned with possibly some errors @@ -697,7 +721,7 @@ type DeleteActionResponse implements Response { type SetUserStatusResponse implements Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # SuspendUserResponse is the response returned with possibly some errors @@ -705,7 +729,7 @@ type SetUserStatusResponse implements Response { type SuspendUserResponse implements Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # SetCommentStatusResponse is the response returned with possibly some errors @@ -713,33 +737,43 @@ type SuspendUserResponse implements Response { type SetCommentStatusResponse implements Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } -# Response to addCommentTag mutation -type AddCommentTagResponse implements Response { +# ModifyTagInput is the input used to modify a tag. +input ModifyTagInput { + + # name is the actual tag to add to the model. + name: String! + + # id is the ID of the model in question that we are modifying the tag of. + id: ID! + + # item_type is the type of item that we are modifying the tag if. + item_type: TAGGABLE_ITEM_TYPE! + + # asset_id is used when the item_type is `COMMENTS`, the is needed to rectify + # the settings to get the asset specific tags/settings. + asset_id: ID +} + +# Response to the addTag or removeTag mutations. +type ModifyTagResponse implements Response { # An array of errors relating to the mutation that occured. - errors: [UserError] -} - -# Response to removeCommentTag mutation -type RemoveCommentTagResponse implements Response { - - # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError!] } # Response to ignoreUser mutation type IgnoreUserResponse implements Response { # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError!] } # Response to stopIgnoringUser mutation type StopIgnoringUserResponse implements Response { # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError!] } # All mutations for the application are defined on this object. @@ -766,11 +800,11 @@ type RootMutation { # Sets Comment status. Requires the `ADMIN` role. setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse - # Add tag to comment. - addCommentTag(id: ID!, asset_id: ID!, name: String!): AddCommentTagResponse + # Add a tag. + addTag(tag: ModifyTagInput!): ModifyTagResponse! - # Remove tag from comment. - removeCommentTag(id: ID!, asset_id: ID!, name: String!): RemoveCommentTagResponse + # Removes a tag. + removeTag(tag: ModifyTagInput!): ModifyTagResponse! # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse diff --git a/models/asset.js b/models/asset.js index 51ff42705..1aeec34f3 100644 --- a/models/asset.js +++ b/models/asset.js @@ -1,6 +1,7 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; const uuid = require('uuid'); +const TagLinkSchema = require('./schema/tag_link'); const AssetSchema = new Schema({ id: { @@ -47,6 +48,9 @@ const AssetSchema = new Schema({ default: null }, + // Tags are added by the self or by administrators. + tags: [TagLinkSchema], + // Additional metadata stored on the field. metadata: { default: {}, diff --git a/models/comment.js b/models/comment.js index e3443376b..76065f5cb 100644 --- a/models/comment.js +++ b/models/comment.js @@ -49,6 +49,8 @@ const CommentSchema = new Schema({ default: 'NONE' }, parent_id: String, + + // Tags are added by the self or by administrators. tags: [TagLinkSchema], // Additional metadata stored on the field. diff --git a/models/user.js b/models/user.js index b61ddd6eb..d069ac457 100644 --- a/models/user.js +++ b/models/user.js @@ -2,6 +2,7 @@ const mongoose = require('../services/mongoose'); const bcrypt = require('bcrypt'); const Schema = mongoose.Schema; const uuid = require('uuid'); +const TagLinkSchema = require('./schema/tag_link'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = require('./enum/user_roles'); @@ -118,6 +119,9 @@ const UserSchema = new Schema({ type: String, }], + // Tags are added by the self or by administrators. + tags: [TagLinkSchema], + // Additional metadata stored on the field. metadata: { default: {}, @@ -191,10 +195,8 @@ const USER_GRAPH_OPERATIONS = [ 'mutation:setUserStatus', 'mutation:suspendUser', 'mutation:setCommentStatus', - 'mutation:addCommentTag', - 'mutation:removeCommentTag', - 'mutation:addUserTag', - 'mutation:removeUserTag' + 'mutation:addTag', + 'mutation:removeTag' ]; /** diff --git a/plugins/coral-plugin-respect/server/typeDefs.graphql b/plugins/coral-plugin-respect/server/typeDefs.graphql index 6639aa454..56734e543 100644 --- a/plugins/coral-plugin-respect/server/typeDefs.graphql +++ b/plugins/coral-plugin-respect/server/typeDefs.graphql @@ -44,7 +44,7 @@ type CreateRespectResponse implements Response { respect: RespectAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } type RootMutation { diff --git a/services/tags.js b/services/tags.js new file mode 100644 index 000000000..3ee3de121 --- /dev/null +++ b/services/tags.js @@ -0,0 +1,133 @@ +const CommentModel = require('../models/comment'); +const AssetModel = require('../models/asset'); +const UserModel = require('../models/user'); + +const AssetsService = require('./assets'); +const SettingsService = require('./settings'); + +const updateModel = async (item_type, query, update) => { + + // Get the model to update with. + let Model; + switch (item_type) { + case 'COMMENTS': + Model = CommentModel; + break; + case 'ASSETS': + Model = AssetModel; + break; + case 'USERS': + Model = UserModel; + break; + default: + throw new Error(`item_type ${item_type} is not a valid item_type to update a tag on`); + } + + // Execute the update operation. + return Model.update(query, update); +}; + +const ownershipQuery = async (item_type, link, query) => { + switch (item_type) { + case 'COMMENTS': + query['author_id'] = link.assigned_by; + break; + case 'USERS': + query['id'] = link.assigned_by; + break; + } +}; + +class TagsService { + + /** + * Retrives a global tag from the settings based on the input_type. + */ + static async get({name, id, item_type, asset_id = null}) { + + // Extract the settings from the database. + let settings; + switch (item_type) { + case 'COMMENTS': + settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id)); + break; + case 'ASSETS': + settings = await AssetsService.rectifySettings(AssetsService.findById(id)); + break; + case 'USERS': + settings = await SettingsService.retrieve(); + break; + default: + settings = await SettingsService.retrieve(); + break; + } + + // Extract the tags from the settings object. + let {tags = []} = settings; + + // Return the first tag that matches the requested form. + return tags.find((tag) => tag.name === name && tag.models.include(item_type)); + } + + /** + * Adds a TagLink to a Model, optionally checking for ownership. + */ + static async add(id, item_type, link, ownershipCheck) { + + // Compose the query to find the comment. + const query = { + id, + 'tags.tag.name': { + $ne: link.tag.name + } + }; + + // If ownership verification is required, ensure that the person that is + // assigning the tag is the same person that owns the comment. + if (ownershipCheck) { + + // Modify the query to support an ownership verification. + ownershipQuery(item_type, link, query); + } + + // Get the Model to perform the update. + return updateModel(item_type, query, { + $push: { + tags: link + } + }); + } + + /** + * Removes a TagLink to a Model, optionally checking for ownership. + */ + static async remove(id, item_type, link, ownershipCheck) { + + // Compose the query to find the comment. + const query = { + id, + 'tags.tag.name': { + $eq: link.tag.name + } + }; + + // If ownership verification is required, ensure that the person that is + // assigning the tag is the same person that owns the comment. + if (ownershipCheck) { + + // Modify the query to support an ownership verification. + ownershipQuery(item_type, link, query); + } + + // Get the Model to perform the update. + return updateModel(item_type, query, { + $pull: { + tags: { + name: link.tag.name + } + } + }); + } +} + +module.exports = TagsService; diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addTag.js similarity index 65% rename from test/server/graph/mutations/addCommentTag.js rename to test/server/graph/mutations/addTag.js index 5783a14c1..e6639d49c 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addTag.js @@ -8,7 +8,7 @@ const UserModel = require('../../../../models/user'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); -describe('graph.mutations.addCommentTag', () => { +describe('graph.mutations.addTag', () => { let comment, asset; beforeEach(async () => { await SettingsService.init(); @@ -21,7 +21,7 @@ describe('graph.mutations.addCommentTag', () => { const query = ` mutation AddCommentTag ($id: ID!, $asset_id: ID!, $name: String!) { - addCommentTag(id: $id, asset_id: $asset_id, name: $name) { + addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { errors { translation_key } @@ -30,13 +30,13 @@ describe('graph.mutations.addCommentTag', () => { `; it('moderators can add tags to comments', async () => { - const user = new UserModel({roles: ['MODERATOR' ]}); + const user = new UserModel({roles: ['MODERATOR']}); const context = new Context({user}); - 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); + const res = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}, 'AddCommentTag'); + if (res.errors && res.errors.length) { + console.error(res.errors); } - expect(response.errors).to.be.empty; + expect(res.errors).to.be.empty; let {tags} = await CommentsService.findById(comment.id); expect(tags).to.have.length(1); @@ -50,12 +50,12 @@ describe('graph.mutations.addCommentTag', () => { }).forEach(([ userDescription, user ]) => { it(userDescription, async () => { const context = new Context({user}); - 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); + const res = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}, 'AddCommentTag'); + if (res.errors && res.errors.length) { + console.error(res.errors); } - expect(response.errors).to.be.empty; - expect(response.data.addCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); + expect(res.errors).to.be.empty; + expect(res.data.addTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); }); }); }); diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeTag.js similarity index 87% rename from test/server/graph/mutations/removeCommentTag.js rename to test/server/graph/mutations/removeTag.js index cca072af2..1075cab49 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeTag.js @@ -10,7 +10,7 @@ const AssetModel = require('../../../../models/asset'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); -describe('graph.mutations.removeCommentTag', () => { +describe('graph.mutations.removeTag', () => { let asset, comment; beforeEach(async () => { await SettingsService.init(); @@ -22,8 +22,8 @@ describe('graph.mutations.removeCommentTag', () => { }); const query = ` - mutation RemoveCommentTag ($id: ID!, $asset_id: ID!, $name: String!) { - removeCommentTag(id: $id, asset_id: $asset_id, name: $name) { + mutation RemoveCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { errors { translation_key } @@ -43,7 +43,7 @@ describe('graph.mutations.removeCommentTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; - expect(response.data.removeCommentTag.errors).to.be.null; + expect(response.data.removeTag.errors).to.be.null; let retrievedComment = await CommentsService.findById(comment.id); @@ -78,7 +78,7 @@ describe('graph.mutations.removeCommentTag', () => { } expect(response.errors).to.be.empty; - expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); + expect(response.data.removeTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); }); }); }); From 0d957bcfb9878620c2575e92fe5e5e15bc9e6810 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 11 May 2017 14:56:36 -0600 Subject: [PATCH 20/56] Code cleanup related to old tag services --- services/comments.js | 49 ----------- test/server/graph/mutations/removeTag.js | 5 +- test/server/services/comments.js | 83 ------------------ test/server/services/tags.js | 107 +++++++++++++++++++++++ 4 files changed, 110 insertions(+), 134 deletions(-) create mode 100644 test/server/services/tags.js diff --git a/services/comments.js b/services/comments.js index e23e4e486..ca258934f 100644 --- a/services/comments.js +++ b/services/comments.js @@ -31,55 +31,6 @@ module.exports = class CommentsService { return commentModel.save(); } - static addTag(id, tagLink, verifyOwnership = false) { - - // Compose the query to find the comment. - const query = { - id, - 'tags.tag.name': { - $ne: tagLink.tag.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'] = tagLink.assigned_by; - } - - return CommentModel.update(query, { - $push: { - tags: tagLink - } - }); - } - - 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: { - name - } - } - }); - } - /** * Finds a comment by the id. * @param {String} id identifier of comment (uuid) diff --git a/test/server/graph/mutations/removeTag.js b/test/server/graph/mutations/removeTag.js index 1075cab49..a7ee4053b 100644 --- a/test/server/graph/mutations/removeTag.js +++ b/test/server/graph/mutations/removeTag.js @@ -9,6 +9,7 @@ const SettingModel = require('../../../../models/setting'); const AssetModel = require('../../../../models/asset'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); +const TagsService = require('../../../../services/tags'); describe('graph.mutations.removeTag', () => { let asset, comment; @@ -36,7 +37,7 @@ describe('graph.mutations.removeTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, {tag: {name: 'BEST'}}, false); + await TagsService.add(comment.id, 'COMMENTS', {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) { @@ -70,7 +71,7 @@ describe('graph.mutations.removeTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, {tag: {name: 'BEST'}}, false); + await TagsService.add(comment.id, 'COMMENTS', {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) { diff --git a/test/server/services/comments.js b/test/server/services/comments.js index f5d82d1de..68859077f 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -219,89 +219,6 @@ describe('services.CommentsService', () => { }); - describe('#addTag', () => { - it('adds a tag', async () => { - 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 id = comments[0].id; - const name = 'BEST'; - const assigned_by = users[0].id; - - await CommentsService.addTag(id, { - tag: { - name - }, - assigned_by - }); - - { - let {tags} = await CommentsService.findById(id); - expect(tags.length).to.equal(1); - } - - 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 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(id, { - tag: { - name - }, - assigned_by - }); - - { - const {tags} = await CommentsService.findById(id); - expect(tags.length).to.equal(0); - } - }); - }); - describe('#changeStatus', () => { it('should change the status of a comment from no status', () => { diff --git a/test/server/services/tags.js b/test/server/services/tags.js new file mode 100644 index 000000000..7cdad829e --- /dev/null +++ b/test/server/services/tags.js @@ -0,0 +1,107 @@ +const CommentsService = require('../../../services/comments'); +const TagsService = require('../../../services/tags'); +const UsersService = require('../../../services/users'); +const SettingsService = require('../../../services/settings'); + +const CommentModel = require('../../../models/comment'); + +const expect = require('chai').use(require('chai-as-promised')).expect; + +describe('services.TagsService', () => { + let comment, user; + beforeEach(async () => { + await SettingsService.init(); + user = await UsersService.createLocalUser('stampi@gmail.com', '1Coral!!', 'Stampi'); + comment = await CommentModel.create({ + id: '1', + body: 'comment 10', + asset_id: '123', + status_history: [], + parent_id: null, + author_id: user.id + }); + }); + + describe('#add', () => { + it('adds a tag', async () => { + const id = comment.id; + const name = 'BEST'; + const assigned_by = user.id; + + await TagsService.add(id, 'COMMENTS', { + 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 id = comment.id; + const name = 'BEST'; + const assigned_by = user.id; + + await TagsService.add(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + let {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } + + await TagsService.add(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + let {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } + }); + }); + + describe('#remove', () => { + it('removes a tag', async () => { + const id = comment.id; + const name = 'BEST'; + const assigned_by = user.id; + + await TagsService.add(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } + + // ok now to remove it + await TagsService.remove(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(0); + } + }); + }); +}); From e6b2ff19b8435cdc8e133cdf4e9ef1f81992dcc9 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 12 May 2017 07:51:53 -0600 Subject: [PATCH 21/56] fixing graph merge error --- graph/typeDefs.graphql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index e69a39f23..a8d13e80c 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -104,12 +104,12 @@ 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! } @@ -767,7 +767,7 @@ input ModifyTagInput { # Response to the addTag or removeTag mutations. type ModifyTagResponse implements Response { - + # An array of errors relating to the mutation that occured. errors: [UserError!] } @@ -798,7 +798,7 @@ type CommentInfoAfterEdit { type EditCommentResponse implements Response { comment: CommentInfoAfterEdit! # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError!] } # All mutations for the application are defined on this object. From b60ed166d0fd70f3aabae7b7babae7c93bac920e Mon Sep 17 00:00:00 2001 From: David Erwin Date: Fri, 12 May 2017 15:38:38 -0400 Subject: [PATCH 22/56] Allow MODERATORs to access tags --- graph/resolvers/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js index 11cfcbb13..96e125a55 100644 --- a/graph/resolvers/util.js +++ b/graph/resolvers/util.js @@ -3,7 +3,7 @@ */ const decorateWithTags = (typeResolver) => { typeResolver.tags = ({tags = []}, _, {user}) => { - if (user && user.hasRoles('ADMIN')) { + if (user && (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { return tags; } From c3d02a14a0fc43cba9ae26f2d77a37441c960363 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 15 May 2017 18:11:14 -0300 Subject: [PATCH 23/56] Updated queries and hasRoles --- .../src/containers/Comment.js | 8 ++++++- .../coral-embed-stream/src/graphql/index.js | 24 ++++++++++++++++--- graph/resolvers/tag_link.js | 2 +- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 4a8e877b5..7b5159cc1 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -28,7 +28,13 @@ export default withFragments({ created_at status tags { - id + tag { + name + created_at + } + assigned_by { + id + } } user { id diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index daa8389b3..d43bfd3d5 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -33,7 +33,13 @@ const extension = { comment { id tags { - name + tag { + name + created_at + } + assigned_by { + id + } } } errors { @@ -46,7 +52,13 @@ const extension = { comment { id tags { - name + tag { + name + created_at + } + assigned_by { + id + } } } errors { @@ -101,7 +113,13 @@ const extension = { status replyCount tags { - name + tag { + name + created_at + } + assigned_by { + id + } } user { id diff --git a/graph/resolvers/tag_link.js b/graph/resolvers/tag_link.js index 5c0d39587..11ee1350d 100644 --- a/graph/resolvers/tag_link.js +++ b/graph/resolvers/tag_link.js @@ -1,6 +1,6 @@ const TagLink = { assigned_by({assigned_by}, _, {user, loaders: {Users}}) { - if (user && user.hasRole('ADMIN')) { + if (user && user.hasRoles('ADMIN')) { return Users.getByID.load(assigned_by); } } From c59346eb0ae5b347669bb73de89f1c03b7ba8b65 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 15 May 2017 18:13:45 -0300 Subject: [PATCH 24/56] tags, and assignedby nul --- graph/resolvers/tag_link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/resolvers/tag_link.js b/graph/resolvers/tag_link.js index 11ee1350d..2fede45ec 100644 --- a/graph/resolvers/tag_link.js +++ b/graph/resolvers/tag_link.js @@ -1,6 +1,6 @@ const TagLink = { assigned_by({assigned_by}, _, {user, loaders: {Users}}) { - if (user && user.hasRoles('ADMIN')) { + if (user && user.hasRoles('ADMIN') && assigned_by != null) { return Users.getByID.load(assigned_by); } } From 793f63495915e47bc0678f6feda738d21ac41ef2 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 09:01:41 -0300 Subject: [PATCH 25/56] Working tags --- client/coral-embed-stream/src/components/Comment.js | 2 +- client/coral-embed-stream/src/containers/Comment.js | 4 ---- client/coral-framework/graphql/fragments/commentView.graphql | 4 +++- .../coral-framework/graphql/mutations/addCommentTag.graphql | 4 +++- .../graphql/mutations/removeCommentTag.graphql | 4 +++- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 9d7cb436d..dabac433c 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -25,7 +25,7 @@ import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import {getEditableUntilDate} from './util'; import styles from './Comment.css'; -const isStaff = tags => !tags.every(t => t.id !== 'STAFF'); +const isStaff = tags => !!tags.filter(i => i.tag.name === 'STAFF').length; // hold actions links (e.g. Reply) along the comment footer const ActionButton = ({children}) => { diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 7b5159cc1..7ae96e177 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -30,10 +30,6 @@ export default withFragments({ tags { tag { name - created_at - } - assigned_by { - id } } user { diff --git a/client/coral-framework/graphql/fragments/commentView.graphql b/client/coral-framework/graphql/fragments/commentView.graphql index 1832cc77d..0aa80c9cf 100644 --- a/client/coral-framework/graphql/fragments/commentView.graphql +++ b/client/coral-framework/graphql/fragments/commentView.graphql @@ -6,7 +6,9 @@ fragment commentView on Comment { created_at status tags { - id + tag { + name + } } user { id diff --git a/client/coral-framework/graphql/mutations/addCommentTag.graphql b/client/coral-framework/graphql/mutations/addCommentTag.graphql index a4ff36da4..aa4ce7f5b 100644 --- a/client/coral-framework/graphql/mutations/addCommentTag.graphql +++ b/client/coral-framework/graphql/mutations/addCommentTag.graphql @@ -3,7 +3,9 @@ mutation AddCommentTag ($id: ID!, $tag: String!) { comment { id tags { - id + tag { + name + } } } errors { diff --git a/client/coral-framework/graphql/mutations/removeCommentTag.graphql b/client/coral-framework/graphql/mutations/removeCommentTag.graphql index 642466f00..c89fc24f9 100644 --- a/client/coral-framework/graphql/mutations/removeCommentTag.graphql +++ b/client/coral-framework/graphql/mutations/removeCommentTag.graphql @@ -3,7 +3,9 @@ mutation RemoveCommentTag ($id: ID!, $tag: String!) { comment { id tags { - id + tag { + name + } } } errors { From 54eb078a131e4ac5767e2c2456fd3514eab25b20 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 09:37:26 -0300 Subject: [PATCH 26/56] Working offtopic tag --- graph/mutators/comment.js | 16 ++++++++++++---- .../client/components/OffTopicTag.js | 4 +--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 5c39f7b69..4755aa4ac 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -16,16 +16,24 @@ const Wordlist = require('../../services/wordlist'); * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null}, status = 'NONE') => { +const createComment = ({user, loaders: {Comments}, pubsub}, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { + + // Handle Tags + if (!!tags.length) { + tags = tags.map(tag => ({ + tag: { + name: tag + } + })) + } // Add the staff tag for comments created as a staff member. - let tags = []; if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - tags = [{ + tags.push({ tag: { name: 'STAFF' } - }]; + }); } return CommentsService.publicCreate({ diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js index 73e2372ed..0b50eb131 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js @@ -1,9 +1,7 @@ import React from 'react'; import styles from './styles.css'; -const isOffTopic = (tags) => { - return !!tags.filter(tag => tag.name === 'OFF_TOPIC').length -} +const isOffTopic = tags => !!tags.filter(i => i.tag.name === 'OFF_TOPIC').length; export default (props) => ( From 9dc0693530e62efcc10c108c0edb49407b7dd2b6 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 09:43:04 -0300 Subject: [PATCH 27/56] Linting --- graph/mutators/comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 4755aa4ac..615d9cd43 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -19,12 +19,12 @@ const Wordlist = require('../../services/wordlist'); const createComment = ({user, loaders: {Comments}, pubsub}, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { // Handle Tags - if (!!tags.length) { + if (tags.length) { tags = tags.map(tag => ({ tag: { name: tag } - })) + })); } // Add the staff tag for comments created as a staff member. From 9db77360b456fd62b26ab3a57f65c49e4fb8fb4f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 17 May 2017 08:16:48 -0600 Subject: [PATCH 28/56] removed unused errors --- errors.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/errors.js b/errors.js index c0d405553..19b0ec90c 100644 --- a/errors.js +++ b/errors.js @@ -75,18 +75,6 @@ const ErrMissingToken = new APIError('token is required', { status: 400 }); -// ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. -const ErrNoCommentFound = new APIError('comment does not exist', { - translation_key: 'COMMENT_NOT_FOUND', - status: 400 -}); - -// ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. -const ErrTagNotAllowed = new APIError('tag not allowed', { - translation_key: 'TAG_NOT_ALLOWED', - status: 400 -}); - // ErrAssetCommentingClosed is returned when a comment or action is attempted on // a stream where commenting has been closed. class ErrAssetCommentingClosed extends APIError { @@ -179,8 +167,6 @@ module.exports = { ErrMissingEmail, ErrMissingPassword, ErrMissingToken, - ErrNoCommentFound, - ErrTagNotAllowed, ErrEmailTaken, ErrSpecialChars, ErrMissingUsername, From b2f1707f1732d92c08c03efb3efe39fb6b8b67a5 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 11:19:06 -0300 Subject: [PATCH 29/56] Fully working offtopic --- client/coral-embed-stream/src/graphql/index.js | 10 +++++++++- client/coral-framework/graphql/mutations.js | 1 - client/coral-plugin-commentbox/actions.js | 4 ++++ client/coral-plugin-commentbox/constants.js | 1 + client/coral-plugin-commentbox/reducer.js | 4 +++- .../client/components/OffTopicCheckbox.js | 14 ++++++++++++-- 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index d43bfd3d5..77041f554 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -170,7 +170,15 @@ const extension = { parent_id, asset_id, action_summaries: [], - tags, + tags: tags.map(tag => ({ + tag: { + name: tag, + created_at: new Date().toISOString(), + }, + assigned_by: { + id: auth.toJS().user.id, + }, + })), status: null, id: 'pending' } diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 66e68be97..1d5318853 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -168,4 +168,3 @@ export const withStopIgnoringUser = withMutation( }); }}), }); - diff --git a/client/coral-plugin-commentbox/actions.js b/client/coral-plugin-commentbox/actions.js index 46bcf4b1b..879824d68 100644 --- a/client/coral-plugin-commentbox/actions.js +++ b/client/coral-plugin-commentbox/actions.js @@ -7,3 +7,7 @@ export const removeTag = idx => ({ type: 'REMOVE_TAG', idx }); + +export const clearTags = () => ({ + type: 'CLEAR_TAGS', +}); diff --git a/client/coral-plugin-commentbox/constants.js b/client/coral-plugin-commentbox/constants.js index b1290f02e..4a80bfe4f 100644 --- a/client/coral-plugin-commentbox/constants.js +++ b/client/coral-plugin-commentbox/constants.js @@ -1,2 +1,3 @@ export const ADD_TAG = 'ADD_TAG'; export const REMOVE_TAG = 'REMOVE_TAG'; +export const CLEAR_TAGS = 'CLEAR_TAGS'; diff --git a/client/coral-plugin-commentbox/reducer.js b/client/coral-plugin-commentbox/reducer.js index d9065fe87..e84f35e93 100644 --- a/client/coral-plugin-commentbox/reducer.js +++ b/client/coral-plugin-commentbox/reducer.js @@ -1,4 +1,4 @@ -import {ADD_TAG, REMOVE_TAG} from './constants'; +import {ADD_TAG, REMOVE_TAG, CLEAR_TAGS} from './constants'; const initialState = { tags: [] @@ -19,6 +19,8 @@ export default function commentBox (state = initialState, action) { ...state.tags.slice(action.idx + 1) ] }; + case CLEAR_TAGS : + return initialState; default : return state; } diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js index 4e7ef7adf..329a16dca 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js @@ -1,13 +1,23 @@ import React from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {addTag, removeTag} from 'coral-plugin-commentbox/actions'; +import {addTag, removeTag, clearTags} from 'coral-plugin-commentbox/actions'; import styles from './styles.css'; class OffTopicCheckbox extends React.Component { label = 'OFF_TOPIC'; + componentDidMount() { + this.clearTagsHook = this.props.registerHook('postSubmit', (data) => { + this.props.clearTags(); + }); + } + + componentWillUnmount() { + this.props.unregisterHook(this.clearTagsHook); + } + handleChange = (e) => { if (e.target.checked) { this.props.addTag(this.label) @@ -33,6 +43,6 @@ class OffTopicCheckbox extends React.Component { const mapStateToProps = ({commentBox}) => ({commentBox}); const mapDispatchToProps = dispatch => - bindActionCreators({addTag, removeTag}, dispatch); + bindActionCreators({addTag, removeTag, clearTags}, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox); From 6664f73d2df567ceb6c2ab8372736c09418e355f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 11:57:51 -0300 Subject: [PATCH 30/56] Best tag working, only Optimistic Missing --- .../src/services/fragmentMatcher.js | 3 +- .../src/components/Comment.js | 22 ++++++------ .../src/components/Stream.js | 12 +++---- .../src/containers/Stream.js | 7 ++-- .../coral-embed-stream/src/graphql/index.js | 35 ++----------------- client/coral-framework/graphql/mutations.js | 28 ++++++++------- .../graphql/mutations/addCommentTag.graphql | 15 -------- .../mutations/removeCommentTag.graphql | 15 -------- client/coral-plugin-best/BestButton.js | 8 ++--- 9 files changed, 44 insertions(+), 101 deletions(-) delete mode 100644 client/coral-framework/graphql/mutations/addCommentTag.graphql delete mode 100644 client/coral-framework/graphql/mutations/removeCommentTag.graphql diff --git a/client/coral-admin/src/services/fragmentMatcher.js b/client/coral-admin/src/services/fragmentMatcher.js index 3b57de0b1..231c0fa2b 100644 --- a/client/coral-admin/src/services/fragmentMatcher.js +++ b/client/coral-admin/src/services/fragmentMatcher.js @@ -26,8 +26,7 @@ const fm = new IntrospectionFragmentMatcher({ {name: 'SetUserStatusResponse'}, {name: 'SuspendUserResponse'}, {name: 'SetCommentStatusResponse'}, - {name: 'AddCommentTagResponse'}, - {name: 'RemoveCommentTagResponse'}, + {name: 'ModifyTagResponse'}, {name: 'IgnoreUserResponse'}, {name: 'StopIgnoringUserResponse'} ] diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index dabac433c..6525f528f 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -109,10 +109,10 @@ class Comment extends React.Component { commentIsIgnored: React.PropTypes.func, // dispatch action to add a tag to a comment - addCommentTag: React.PropTypes.func, + addTag: React.PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: React.PropTypes.func, + removeTag: React.PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, @@ -171,8 +171,8 @@ class Comment extends React.Component { setActiveReplyBox, activeReplyBox, deleteAction, - addCommentTag, - removeCommentTag, + addTag, + removeTag, ignoreUser, disableReply, commentIsIgnored, @@ -213,18 +213,20 @@ class Comment extends React.Component { const addBestTag = notifyOnError( () => - addCommentTag({ + addTag({ id: comment.id, - tag: BEST_TAG + name: BEST_TAG, + asset_id: asset.id }), () => 'Failed to tag comment as best' ); const removeBestTag = notifyOnError( () => - removeCommentTag({ + removeTag({ id: comment.id, - tag: BEST_TAG + name: BEST_TAG, + asset_id: asset.id }), () => 'Failed to remove best comment tag' ); @@ -394,8 +396,8 @@ class Comment extends React.Component { currentUser={currentUser} postFlag={postFlag} deleteAction={deleteAction} - addCommentTag={addCommentTag} - removeCommentTag={removeCommentTag} + addTag={addTag} + removeTag={removeTag} ignoreUser={ignoreUser} charCountEnable={charCountEnable} maxCharCount={maxCharCount} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index b7b325cf6..848427aa3 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -33,8 +33,8 @@ class Stream extends React.Component { loadMore, deleteAction, showSignInDialog, - addCommentTag, - removeCommentTag, + addTag, + removeTag, pluginProps, ignoreUser, auth: {loggedIn, isAdmin, user}, @@ -166,8 +166,8 @@ class Stream extends React.Component { currentUser={user} postFlag={postFlag} postDontAgree={postDontAgree} - addCommentTag={addCommentTag} - removeCommentTag={removeCommentTag} + addTag={addTag} + removeTag={removeTag} ignoreUser={ignoreUser} commentIsIgnored={commentIsIgnored} loadMore={loadMore} @@ -201,10 +201,10 @@ Stream.propTypes = { postComment: PropTypes.func.isRequired, // dispatch action to add a tag to a comment - addCommentTag: PropTypes.func, + addTag: PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: PropTypes.func, + removeTag: PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index f19cc0a34..69f3c5491 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -8,7 +8,7 @@ import isNil from 'lodash/isNil'; import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream'; import { withPostComment, withPostFlag, withPostDontAgree, withDeleteAction, - withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment, + withAddTag, withRemoveTag, withIgnoreUser, withEditComment, } from 'coral-framework/graphql/mutations'; import {notificationActions, authActions} from 'coral-framework'; @@ -243,10 +243,9 @@ export default compose( withPostComment, withPostFlag, withPostDontAgree, - withAddCommentTag, - withRemoveCommentTag, + withAddTag, + withRemoveTag, withIgnoreUser, withDeleteAction, withEditComment, )(StreamContainer); - diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 77041f554..201f088dc 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -28,39 +28,8 @@ const extension = { } } `, - RemoveCommentTagResponse: gql` - fragment CoralEmbedStream_RemoveCommentTagResponse on RemoveCommentTagResponse { - comment { - id - tags { - tag { - name - created_at - } - assigned_by { - id - } - } - } - errors { - translation_key - } - } - `, - AddCommentTagResponse: gql` - fragment CoralEmbedStream_AddCommentTagResponse on AddCommentTagResponse { - comment { - id - tags { - tag { - name - created_at - } - assigned_by { - id - } - } - } + ModifyTagResponse: gql` + fragment CoralEmbedStream_ModifyTagResponse on ModifyTagResponse { errors { translation_key } diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 1d5318853..ab07b4441 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -95,39 +95,43 @@ export const withDeleteAction = withMutation( }}), }); -export const withAddCommentTag = withMutation( +export const withAddTag = withMutation( gql` - mutation AddCommentTag($id: ID!, $tag: String!) { - addCommentTag(id:$id, tag:$tag) { - ...AddCommentTagResponse + mutation AddCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { + ...ModifyTagResponse } } `, { props: ({mutate}) => ({ - addCommentTag: ({id, tag}) => { + addTag: ({id, name, asset_id}) => { return mutate({ variables: { id, - tag + name, + asset_id } }); }}), }); -export const withRemoveCommentTag = withMutation( +export const withRemoveTag = withMutation( gql` - mutation RemoveCommentTag($id: ID!, $tag: String!) { - removeCommentTag(id:$id, tag:$tag) { - ...RemoveCommentTagResponse + mutation RemoveCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { + errors { + translation_key + } } } `, { props: ({mutate}) => ({ - removeCommentTag: ({id, tag}) => { + removeTag: ({id, name, asset_id}) => { return mutate({ variables: { id, - tag + name, + asset_id } }); }}), diff --git a/client/coral-framework/graphql/mutations/addCommentTag.graphql b/client/coral-framework/graphql/mutations/addCommentTag.graphql deleted file mode 100644 index aa4ce7f5b..000000000 --- a/client/coral-framework/graphql/mutations/addCommentTag.graphql +++ /dev/null @@ -1,15 +0,0 @@ -mutation AddCommentTag ($id: ID!, $tag: String!) { - addCommentTag(id:$id, tag:$tag) { - comment { - id - tags { - tag { - name - } - } - } - errors { - translation_key - } - } -} diff --git a/client/coral-framework/graphql/mutations/removeCommentTag.graphql b/client/coral-framework/graphql/mutations/removeCommentTag.graphql deleted file mode 100644 index c89fc24f9..000000000 --- a/client/coral-framework/graphql/mutations/removeCommentTag.graphql +++ /dev/null @@ -1,15 +0,0 @@ -mutation RemoveCommentTag ($id: ID!, $tag: String!) { - removeCommentTag(id:$id, tag:$tag) { - comment { - id - tags { - tag { - name - } - } - } - errors { - translation_key - } - } -} diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index 96ef45f86..691b3c889 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -6,10 +6,10 @@ import classnames from 'classnames'; // tag string for best comments export const BEST_TAG = 'BEST'; -export const commentIsBest = ({tags} = {}) => { - const isBest = Array.isArray(tags) && tags.some(t => t.name === BEST_TAG); - return isBest; -}; + +export const commentIsBest = ({tags} = {}) => tags.some(t => t.tag.name === BEST_TAG); + +// const commentIsBest = tags => !!tags.filter(i => i.tag.name === 'BEST_TAG).length; const name = 'coral-plugin-best'; const lang = new I18n(translations); From f57fa7cb7bb9753f461c244b956df3a9060efdbb Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 23:53:07 -0300 Subject: [PATCH 31/56] Update Cache --- .../src/components/Comment.js | 2 +- client/coral-framework/graphql/mutations.js | 49 +++++++++++++++++-- client/coral-plugin-best/BestButton.js | 2 - 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 6525f528f..a0d293bdb 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -216,7 +216,7 @@ class Comment extends React.Component { addTag({ id: comment.id, name: BEST_TAG, - asset_id: asset.id + assetId: asset.id }), () => 'Failed to tag comment as best' ); diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index ab07b4441..2ad2d6f50 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -95,6 +95,16 @@ export const withDeleteAction = withMutation( }}), }); + const COMMENT_FRAGMENT = gql` + fragment CoralRespect_UpdateFragment on Comment { + tags { + tag { + name + } + } + } + `; + export const withAddTag = withMutation( gql` mutation AddCommentTag($id: ID!, $asset_id: ID!, $name: String!) { @@ -104,13 +114,35 @@ export const withAddTag = withMutation( } `, { props: ({mutate}) => ({ - addTag: ({id, name, asset_id}) => { + addTag: ({id, name, assetId}) => { return mutate({ variables: { id, name, - asset_id - } + asset_id: assetId + }, + optimisticResponse: { + deleteAction: { + __typename: 'DeleteActionResponse', + errors: null, + } + }, + update: (proxy) => { + const fragmentId = `Comment_${id}`; + + // Read the data from our cache for this query. + const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); + + data.tags.push({ + tag: { + __typename: 'TagLink', + name + } + }); + + // Write our data back to the cache. + proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data}); + }, }); }}), }); @@ -132,6 +164,17 @@ export const withRemoveTag = withMutation( id, name, asset_id + }, + update: (proxy) => { + const fragmentId = `Comment_${id}`; + + // Read the data from our cache for this query. + const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); + + console.log('remove', data); + + // Write our data back to the cache. + proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data}); } }); }}), diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index 691b3c889..b279c0adc 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -9,8 +9,6 @@ export const BEST_TAG = 'BEST'; export const commentIsBest = ({tags} = {}) => tags.some(t => t.tag.name === BEST_TAG); -// const commentIsBest = tags => !!tags.filter(i => i.tag.name === 'BEST_TAG).length; - const name = 'coral-plugin-best'; const lang = new I18n(translations); From 17e7b28688f60e5780906af3b3f8200586a08cbc Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 18 May 2017 00:21:08 -0300 Subject: [PATCH 32/56] Fully working best tag --- client/coral-framework/graphql/mutations.js | 15 +++++++++------ client/coral-plugin-best/BestButton.js | 4 ---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index ab7f4ae78..cf57688ee 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -95,8 +95,8 @@ export const withDeleteAction = withMutation( }}), }); - const COMMENT_FRAGMENT = gql` - fragment CoralRespect_UpdateFragment on Comment { +const COMMENT_FRAGMENT = gql` + fragment CoraBest_UpdateFragment on Comment { tags { tag { name @@ -135,9 +135,10 @@ export const withAddTag = withMutation( data.tags.push({ tag: { - __typename: 'TagLink', - name - } + __typename: 'Tag', + name: "BEST" + }, + __typename: 'TagLink' }); // Write our data back to the cache. @@ -171,7 +172,9 @@ export const withRemoveTag = withMutation( // Read the data from our cache for this query. const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - console.log('remove', data); + const idx = data.tags.findIndex(i => i.tag.name === 'BEST'); + + data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)]; // Write our data back to the cache. proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data}); diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index acc971a05..8a17cfdf8 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -6,12 +6,8 @@ import classnames from 'classnames'; // tag string for best comments export const BEST_TAG = 'BEST'; -<<<<<<< HEAD -export const commentIsBest = ({tags} = {}) => tags.some(t => t.tag.name === BEST_TAG); -======= export const commentIsBest = ({tags} = {}) => tags.some((t) => t.tag.name === BEST_TAG); ->>>>>>> 7a256bdd2b62c5bdf6f20df76baa910ab0b44166 const name = 'coral-plugin-best'; const lang = new I18n(translations); From 132b0a854d4654fbf53c2c09964dc30be7f3f118 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 18 May 2017 00:22:39 -0300 Subject: [PATCH 33/56] linting --- client/coral-framework/graphql/mutations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index cf57688ee..6eb1b4bad 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -136,7 +136,7 @@ export const withAddTag = withMutation( data.tags.push({ tag: { __typename: 'Tag', - name: "BEST" + name: 'BEST' }, __typename: 'TagLink' }); @@ -172,7 +172,7 @@ export const withRemoveTag = withMutation( // Read the data from our cache for this query. const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - const idx = data.tags.findIndex(i => i.tag.name === 'BEST'); + const idx = data.tags.findIndex((i) => i.tag.name === 'BEST'); data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)]; From 8f219c47a763c6a4db19e06b5ce45288b2dc994e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 19 May 2017 12:23:53 -0600 Subject: [PATCH 34/56] Tag server impl --- PLUGINS.md | 24 ++++++ graph/loaders/index.js | 2 + graph/loaders/tags.js | 22 +++++ graph/mutators/comment.js | 74 +++++++++++----- graph/mutators/tag.js | 60 ++----------- plugins.default.json | 3 +- plugins/coral-plugin-offtopic/index.js | 13 ++- .../server/typeDefs.graphql | 4 - services/tags.js | 85 ++++++++++++++++++- test/server/graph/mutations/addTag.js | 2 +- 10 files changed, 209 insertions(+), 80 deletions(-) create mode 100644 graph/loaders/tags.js delete mode 100644 plugins/coral-plugin-offtopic/server/typeDefs.graphql diff --git a/PLUGINS.md b/PLUGINS.md index 0d17ccd55..4462f8577 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -307,6 +307,30 @@ module.exports = { } ``` +#### Field: `tags` + +The tags hook allows a plugin to define tags that are code controlled (added +or enabled by code). Below is an example pulled from the core off topic plugin +on how to create a hook for the `OFF_TOPIC` name: + +```js +[ + { + name: 'OFF_TOPIC', + permissions: { + public: true, + self: true, + roles: [] + }, + models: ['COMMENTS'], + created_at: new Date() + } +] +``` + +You can refer to `models/schema/tag.js` for the available schema to match when +creating models to enable/disable specific features. + #### Field: `passport` ```js diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 26727940f..8bc170c6d 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -6,6 +6,7 @@ const Assets = require('./assets'); const Comments = require('./comments'); const Metrics = require('./metrics'); const Settings = require('./settings'); +const Tags = require('./tags'); const Users = require('./users'); const plugins = require('../../services/plugins'); @@ -18,6 +19,7 @@ let loaders = [ Comments, Metrics, Settings, + Tags, Users, // Load the plugin loaders from the manager. diff --git a/graph/loaders/tags.js b/graph/loaders/tags.js new file mode 100644 index 000000000..23ab6fd43 --- /dev/null +++ b/graph/loaders/tags.js @@ -0,0 +1,22 @@ +const DataLoader = require('dataloader'); +const TagsService = require('../../services/tags'); + +/** + * Get all the tags for the context for the dataloader. + */ +const genAll = (context, queries) => { + return Promise.all(queries.map(({id, item_type, asset_id}) => { + return TagsService.getAll({id, item_type, asset_id}); + })); +}; + +/** + * Creates a set of loaders based on a GraphQL context. + * @param {Object} context the context of the GraphQL request + * @return {Object} object of loaders + */ +module.exports = (context) => ({ + Tags: { + getAll: new DataLoader((queries) => genAll(context, queries)) + } +}); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 193753275..75e7d9fc0 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -2,11 +2,61 @@ const errors = require('../../errors'); const AssetsService = require('../../services/assets'); const ActionsService = require('../../services/actions'); +const TagsService = require('../../services/tags'); const CommentsService = require('../../services/comments'); const linkify = require('linkify-it')(); - const Wordlist = require('../../services/wordlist'); +const debug = require('debug')('talk:graph:mutators:tags'); +const plugins = require('../../services/plugins'); + +const pluginTags = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => { + debug(`added plugin '${plugin.name}'`); + + acc = acc.concat(tags); + + return acc; +}, []); + +const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => { + const item_type = 'COMMENTS'; + + // Handle Tags + if (tags.length) { + + // Get the global list of tags from the dataloader. + let globalTags = await Tags.getAll.load({ + item_type, + asset_id + }); + if (!Array.isArray(globalTags)) { + globalTags = []; + } + + globalTags = globalTags.concat(pluginTags); + + // Merge in the tags for the given comment. + tags = tags.map((name) => { + + // Resolve the TagLink that we can use for the comment. + let {tagLink} = TagsService.resolveLink(user, globalTags, {name, item_type}); + + // Return the tagLink for tag insertion. + return tagLink; + }); + } + + // Add the staff tag for comments created as a staff member. + if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + tags.push(TagsService.newTagLink(user, { + name: 'STAFF', + item_type + })); + } + + return tags; +}; + /** * Creates a new comment. * @param {Object} user the user performing the request @@ -16,25 +66,11 @@ const Wordlist = require('../../services/wordlist'); * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = async ({user, loaders: {Comments}, pubsub}, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { +const createComment = async (context, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { + const {user, loaders: {Comments}, pubsub} = context; - // Handle Tags - if (tags.length) { - tags = tags.map((tag) => ({ - tag: { - name: tag - } - })); - } - - // Add the staff tag for comments created as a staff member. - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - tags.push({ - tag: { - name: 'STAFF' - } - }); - } + // Resolve the tags for the comment. + tags = await resolveTagsForComment(context, {asset_id, tags}); let comment = await CommentsService.publicCreate({ body, diff --git a/graph/mutators/tag.js b/graph/mutators/tag.js index 3182a7d00..0c93d8ea0 100644 --- a/graph/mutators/tag.js +++ b/graph/mutators/tag.js @@ -4,62 +4,18 @@ const errors = require('../../errors'); /** * Modifies the targeted model with the specified operation to add/remove a tag. */ -const modify = async ({user}, operation, {name, id, item_type, asset_id}) => { +const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type, asset_id}) => { - // Try to find the tag in the global list. This will contain the permission - // information if it's found. - let tag = await TagsService.get({name, id, item_type, asset_id}); + // Get the global list of tags from the dataloader. + const tags = await Tags.getAll.load({id, item_type, asset_id}); - // Create the new tagLink that will be created to interact 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 - // modify 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 modify the tag without checking for ownership. - if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { - return operation(id, item_type, 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 modified. - return operation(id, item_type, tagLink, true); - } - - throw errors.ErrNotAuthorized; - } - - // Only admin/moderators can modify 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: [item_type], - created_at: new Date() - }; + // Resolve the TagLink that should be used to insert to the user. This will + // addtionally return with an ownership property that can be used to determine + // that the user who adds this tag must also be the owner of the resource. + let {tagLink, ownership} = TagsService.resolveLink(user, tags, {name, item_type}); // Actually modify the tag on the model. - return operation(id, item_type, tagLink, false); + return operation(id, item_type, tagLink, ownership); }; module.exports = (context) => { diff --git a/plugins.default.json b/plugins.default.json index b3443af48..7af4eb4fe 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -1,7 +1,8 @@ { "server": [ "coral-plugin-respect", - "coral-plugin-facebook-auth" + "coral-plugin-facebook-auth", + "coral-plugin-offtopic" ], "client": [ "coral-plugin-respect" diff --git a/plugins/coral-plugin-offtopic/index.js b/plugins/coral-plugin-offtopic/index.js index 51ad1c24e..9988686ff 100644 --- a/plugins/coral-plugin-offtopic/index.js +++ b/plugins/coral-plugin-offtopic/index.js @@ -2,5 +2,16 @@ const {readFileSync} = require('fs'); const path = require('path'); module.exports = { - typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8') + tags: [ + { + name: 'OFF_TOPIC', + permissions: { + public: true, + self: true, + roles: [] + }, + models: ['COMMENTS'], + created_at: new Date() + } + ] }; diff --git a/plugins/coral-plugin-offtopic/server/typeDefs.graphql b/plugins/coral-plugin-offtopic/server/typeDefs.graphql deleted file mode 100644 index 48ba577cf..000000000 --- a/plugins/coral-plugin-offtopic/server/typeDefs.graphql +++ /dev/null @@ -1,4 +0,0 @@ -## Extending TAG_TYPE by adding OFF_TOPIC Tag -enum TAG_TYPE { - OFF_TOPIC -} diff --git a/services/tags.js b/services/tags.js index 3ee3de121..7653434c9 100644 --- a/services/tags.js +++ b/services/tags.js @@ -5,6 +5,8 @@ const UserModel = require('../models/user'); const AssetsService = require('./assets'); const SettingsService = require('./settings'); +const errors = require('../errors'); + const updateModel = async (item_type, query, update) => { // Get the model to update with. @@ -43,7 +45,7 @@ class TagsService { /** * Retrives a global tag from the settings based on the input_type. */ - static async get({name, id, item_type, asset_id = null}) { + static async getAll({id, item_type, asset_id = null}) { // Extract the settings from the database. let settings; @@ -66,7 +68,86 @@ class TagsService { let {tags = []} = settings; // Return the first tag that matches the requested form. - return tags.find((tag) => tag.name === name && tag.models.include(item_type)); + return tags; + } + + /** + * Resolves the tagLink and ownership verification requirements that should be + * used when trying to perform tag adding/removing operations. + */ + static resolveLink(user, tags, {name, item_type}) { + + // Try to find the tag in the global list. This will contain the permission + // information if it's found. + let tag = tags.find((tag) => { + return tag.name === name && Array.isArray(tag.models) && tag.models.includes(item_type); + }); + + // Create the new tagLink that will be created to interact 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 + // modify 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 modify the tag without checking for ownership. + if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { + return {tagLink, ownership: 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 modified. + return {tagLink, ownership: true}; + } + + throw errors.ErrNotAuthorized; + } + + // Only admin/moderators can modify 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 = TagsService.newTagLink(user, {name, item_type}); + + // Actually modify the tag on the model. + return {tagLink, ownership: false}; + } + + static newTag({name, item_type}) { + return { + name, + permissions: { + public: true, + self: false, + roles: [] + }, + models: [item_type], + created_at: new Date() + }; + } + + /** + * Creates a new TagLink based on the input user and the tag data. + */ + static newTagLink(user, tag) { + return { + tag: TagsService.newTag(tag), + assigned_by: user.id, + created_at: new Date() + }; } /** diff --git a/test/server/graph/mutations/addTag.js b/test/server/graph/mutations/addTag.js index e6639d49c..17e268cea 100644 --- a/test/server/graph/mutations/addTag.js +++ b/test/server/graph/mutations/addTag.js @@ -12,7 +12,7 @@ describe('graph.mutations.addTag', () => { let comment, asset; beforeEach(async () => { await SettingsService.init(); - + asset = new AssetModel({url: 'http://new.test.com/'}); await asset.save(); From 1e904584e74d79ea1596688b7913f57b4323c1f3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 6 Jun 2017 17:31:52 -0600 Subject: [PATCH 35/56] Added model migrations --- bin/cli | 1 + bin/cli-migration | 88 +++++++++++++++++++++ migrations/1496771633_tags.js | 78 +++++++++++++++++++ models/migration.js | 10 +++ package.json | 1 + services/migration.js | 142 ++++++++++++++++++++++++++++++++++ yarn.lock | 16 ++++ 7 files changed, 336 insertions(+) create mode 100755 bin/cli-migration create mode 100644 migrations/1496771633_tags.js create mode 100644 models/migration.js create mode 100644 services/migration.js diff --git a/bin/cli b/bin/cli index 7e27fa85b..1f3d50ad2 100755 --- a/bin/cli +++ b/bin/cli @@ -13,6 +13,7 @@ program .command('setup', 'setup the application') .command('jobs', 'work with the job queues') .command('users', 'work with the application auth') + .command('migration', 'provides utilities for migrating the database') .command('plugins', 'provides utilities for interacting with the plugin system') .parse(process.argv); diff --git a/bin/cli-migration b/bin/cli-migration new file mode 100755 index 000000000..9c3e077b1 --- /dev/null +++ b/bin/cli-migration @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +const program = require('./commander'); +const util = require('./util'); +const inquirer = require('inquirer'); +const mongoose = require('../services/mongoose'); +const MigrationService = require('../services/migration'); + +// Register shutdown hooks. +util.onshutdown([ + () => mongoose.disconnect() +]); + +async function createMigration(name) { + try { + + // Create the migration. + await MigrationService.create(name); + + util.shutdown(); + } catch (e) { + console.error(e); + util.shutdown(1); + } +} + +async function runMigrations() { + + try { + + // Get the migrations to run. + let migrations = await MigrationService.listPending(); + + console.log('Now going to run the following migrations:\n'); + + for (let {filename} of migrations) { + console.log(`\tmigrations/${filename}`); + } + + let answers = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Proceed with migrations', + default: false + } + ]); + + if (answers.confirm) { + + // Run the migrations. + await MigrationService.run(migrations); + } else { + console.warn('Skipping migrations'); + } + + util.shutdown(); + } catch (e) { + console.error(e); + util.shutdown(1); + } +} + +//============================================================================== +// Setting up the program command line arguments. +//============================================================================== + +program + .command('create ') + .description('creates a new migration') + .action(createMigration); + +program + .command('run') + .description('runs all pending migrations') + .action(runMigrations); + +program.parse(process.argv); + +// If there is no command listed, output help. +if (process.argv.length <= 2) { + program.outputHelp(); + util.shutdown(); +} diff --git a/migrations/1496771633_tags.js b/migrations/1496771633_tags.js new file mode 100644 index 000000000..9b2aeedf0 --- /dev/null +++ b/migrations/1496771633_tags.js @@ -0,0 +1,78 @@ +const CommentModel = require('../models/comment'); + +module.exports = { + async up() { + + // Find all comments that have tags. + let comments = await CommentModel.aggregate([ + {$match: { + tags: { + $exists: true, + $ne: [] + } + }}, + {$project: { + id: true, + tags: true + }} + ]); + + // Create a new batch operation. + let batch = CommentModel.collection.initializeUnorderedBulkOp(); + + // Loop over the comments retrieved, updating the tag structure. + for (let {id, tags} of comments) { + + // OLD + // + // [ + // { + // name: 'OFF_TOPIC', + // assigned_by: '', + // created_at: new Date() + // } + // ] + + // NEW + // + // [ + // { + // tag: { + // name: 'OFF_TOPIC', + // permissions: { + // public: true, + // self: false, + // roles: [] + // }, + // models: ['COMMENTS'], + // created_at: new Date() + // }, + // assigned_by: '', + // created_at: new Date() + // } + // ] + + // Remap the tag structure. + tags = tags.map(({name, assigned_by, created_at}) => ({ + tag: { + name, + permissions: { + public: true, + self: name === 'OFF_TOPIC', // at the time of migration, only off topic tags were self assigning + roles: [] + }, + models: ['COMMENTS'], + created_at + }, + assigned_by, + created_at + })); + + // Execute the batch operation. + batch.find({id}).updateOne({$set: {tags}}); + } + + // Execute the batch update operation. + await batch.execute(); + } +}; diff --git a/models/migration.js b/models/migration.js new file mode 100644 index 000000000..648c5594b --- /dev/null +++ b/models/migration.js @@ -0,0 +1,10 @@ +const mongoose = require('../services/mongoose'); +const Schema = mongoose.Schema; + +const MigrationSchema = new Schema({ + version: Number +}); + +const Migration = mongoose.model('Migration', MigrationSchema); + +module.exports = Migration; diff --git a/package.json b/package.json index 84d0fdf52..450764c90 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "resolve": "^1.3.2", "semver": "^5.3.0", "simplemde": "^1.11.2", + "snake-case": "^2.1.0", "subscriptions-transport-ws": "^0.5.5-alpha.0", "timekeeper": "^1.0.0", "uuid": "^3.0.1" diff --git a/services/migration.js b/services/migration.js new file mode 100644 index 000000000..bc2274de4 --- /dev/null +++ b/services/migration.js @@ -0,0 +1,142 @@ +const MigrationModel = require('../models/migration'); +const fs = require('fs'); +const path = require('path'); +const Joi = require('joi'); +const sc = require('snake-case'); + +const migrationTemplate = ` +module.exports = { + async up() { + + } +}; + +`; + +module.exports = class MigrationService { + static async create(name) { + if (!name || typeof name !== 'string' || name.length === 0) { + throw new Error('name must be defined'); + } + + let version = Math.round(Date.now() / 1000, 0); + let filename = path.join(__dirname, '..', 'migrations', `${version}_${sc(name)}.js`); + fs.writeFileSync(filename, migrationTemplate, 'utf8'); + + console.log(`Created migration ${version} in ${filename}`); + } + + static async listPending() { + + // Get all the migration files. + let migrationFiles = fs.readdirSync(path.join(__dirname, '..', 'migrations')); + + // Ensure that all migrations follow this format. + const migrationSchema = Joi.object({ + up: Joi.func().required(), + down: Joi.func() + }); + + // Extract the version from the filename with this regex. + const versionRe = /(\d+)_([\S_]+)\.js/; + + // Get the latest version. + let latestVersion = await MigrationService.latestVersion(); + + // Parse the migrations from the file listing. + let migrations = migrationFiles + .map((filename) => { + + // Parse the version from the filename. + let matches = filename.match(versionRe); + if (matches.length !== 3) { + return null; + } + let version = parseInt(matches[1]); + + // Don't rerun migrations from versions already ran. + if (version <= latestVersion) { + return null; + } + + // Read the migration from the filesystem. + let migration = require(`../migrations/${filename}`); + Joi.assert(migration, migrationSchema, `Migration ${filename} does did not pass validation`); + + return { + filename, + version, + migration + }; + }) + .filter((migration) => migration !== null) + .sort((a, b) => { + if (a.version < b.version) { + return -1; + } + + if (a.version > b.version) { + return 1; + } + + return 0; + }); + + return migrations; + } + + static async run(migrations) { + if (migrations.length === 0) { + console.log('No migrations to run!'); + return; + } + + for (let {version, migration} of migrations) { + console.log(`Starting migration ${version}.js`); + await migration.up(); + console.log(`Finished migration ${version}.js`); + + console.log(`Recording migration ${version}.js`); + + // Record that the migration was finished. + let m = new MigrationModel({version}); + await m.save(); + + console.log(`Finished recording migration ${version}.js`); + } + } + + static async latestVersion() { + + // Load the latest migration details from the database. + let latestMigration = await MigrationModel + .find({}) + .sort({version: -1}) + .limit(1) + .exec(); + + // If there weren't any migrations at all, then error out. + if (latestMigration.length === 0) { + return null; + } + + // If the latest migration does not match the required version, then error + // out. + return latestMigration[0].version; + } + + static async verify(requiredVersion) { + + // If the requiredVersion isn't specified or is 0, then don't complain. + if (typeof requiredVersion !== 'number' || requiredVersion === 0) { + return; + } + + // If the latest migration does not match the required version, then error + // out. + let latestVersion = await MigrationService.latestVersion(); + if (!latestVersion || latestVersion < requiredVersion) { + throw new Error(`A database migration is required, version required ${requiredVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``); + } + } +}; diff --git a/yarn.lock b/yarn.lock index 728687248..adf0675d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5028,6 +5028,10 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: dependencies: js-tokens "^3.0.0" +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" @@ -5409,6 +5413,12 @@ nightwatch@^0.9.11: proxy-agent "2.0.0" q "1.4.1" +no-case@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081" + dependencies: + lower-case "^1.1.1" + nocache@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.0.0.tgz#202b48021a0c4cbde2df80de15a17443c8b43980" @@ -7533,6 +7543,12 @@ smtp-connection@2.12.0: httpntlm "1.6.1" nodemailer-shared "1.1.0" +snake-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" + dependencies: + no-case "^2.2.0" + sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" From cf82f5b7079e41e36d4db3d569a2451b922b5aaf Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 6 Jun 2017 17:32:52 -0600 Subject: [PATCH 36/56] fixed enum --- bin/cli-setup | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/cli-setup b/bin/cli-setup index 1d6de4090..e9f0959ef 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -8,6 +8,7 @@ const program = require('./commander'); const inquirer = require('inquirer'); const mongoose = require('../services/mongoose'); const SettingModel = require('../models/setting'); +const MODERATION_OPTIONS = require('../models/enum/moderation_options'); const SettingsService = require('../services/settings'); const SetupService = require('../services/setup'); const UsersService = require('../services/users'); @@ -90,7 +91,7 @@ const performSetup = () => { }, { type: 'list', - choices: SettingModel.MODERATION_OPTIONS, + choices: MODERATION_OPTIONS, name: 'moderation', default: settings.moderation, message: 'Select a moderation mode' From 6827ff2f1fd723145c9e05d13523d7a68c4fa2a7 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 7 Jun 2017 10:27:42 -0600 Subject: [PATCH 37/56] Added support for version enforcing --- migrations/1496771633_tags.js | 5 +++ package.json | 5 +++ services/cache.js | 2 +- services/karma.js | 2 +- services/migration.js | 74 +++++++++++++++++++++++++++++------ services/mongoose.js | 1 + services/passport.js | 2 +- services/redis.js | 2 +- 8 files changed, 76 insertions(+), 17 deletions(-) diff --git a/migrations/1496771633_tags.js b/migrations/1496771633_tags.js index 9b2aeedf0..be3fa2577 100644 --- a/migrations/1496771633_tags.js +++ b/migrations/1496771633_tags.js @@ -17,6 +17,11 @@ module.exports = { }} ]); + // If no comments were found, nothing needes to be done! + if (comments.length <= 0) { + return; + } + // Create a new batch operation. let batch = CommentModel.collection.initializeUnorderedBulkOp(); diff --git a/package.json b/package.json index 450764c90..137249884 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,11 @@ "embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs", "heroku-postbuild": "./bin/cli plugins reconcile && yarn build" }, + "talk": { + "migration": { + "minVersion": 1496771633 + } + }, "config": { "pre-git": { "commit-msg": [], diff --git a/services/cache.js b/services/cache.js index 92da6df0a..25642e1d0 100644 --- a/services/cache.js +++ b/services/cache.js @@ -1,5 +1,5 @@ const redis = require('./redis'); -const debug = require('debug')('talk:cache'); +const debug = require('debug')('talk:services:cache'); const crypto = require('crypto'); const cache = module.exports = { diff --git a/services/karma.js b/services/karma.js index ea932c86a..c76bcebb3 100644 --- a/services/karma.js +++ b/services/karma.js @@ -1,4 +1,4 @@ -const debug = require('debug')('talk:trust'); +const debug = require('debug')('talk:services:karma'); const UserModel = require('../models/user'); /** diff --git a/services/migration.js b/services/migration.js index bc2274de4..11eec3247 100644 --- a/services/migration.js +++ b/services/migration.js @@ -2,10 +2,11 @@ const MigrationModel = require('../models/migration'); const fs = require('fs'); const path = require('path'); const Joi = require('joi'); +const debug = require('debug')('talk:services:migration'); const sc = require('snake-case'); +const {talk: {migration: {minVersion}}} = require('../package.json'); -const migrationTemplate = ` -module.exports = { +const migrationTemplate = `module.exports = { async up() { } @@ -13,12 +14,19 @@ module.exports = { `; -module.exports = class MigrationService { +class MigrationService { + + /** + * Creates a new migration file. + * + * @param {String} name name of the migration + */ static async create(name) { if (!name || typeof name !== 'string' || name.length === 0) { throw new Error('name must be defined'); } + // Create a new Migration based on the current time. let version = Math.round(Date.now() / 1000, 0); let filename = path.join(__dirname, '..', 'migrations', `${version}_${sc(name)}.js`); fs.writeFileSync(filename, migrationTemplate, 'utf8'); @@ -26,6 +34,9 @@ module.exports = class MigrationService { console.log(`Created migration ${version} in ${filename}`); } + /** + * Returns a list of all pending migrations. + */ static async listPending() { // Get all the migration files. @@ -85,27 +96,48 @@ module.exports = class MigrationService { return migrations; } + /** + * Runs an list of migrations. + * + * @param {Array} migrations a list of migrations returned by `listPending` + */ static async run(migrations) { if (migrations.length === 0) { console.log('No migrations to run!'); return; } - for (let {version, migration} of migrations) { - console.log(`Starting migration ${version}.js`); - await migration.up(); - console.log(`Finished migration ${version}.js`); + for (let {filename, version, migration} of migrations) { + try { + console.log(`Starting migration ${filename}`); + await migration.up(); + console.log(`Finished migration ${filename}`); + } catch (e) { + console.error(`Migration ${filename} failed`); + throw e; + } - console.log(`Recording migration ${version}.js`); + try { + console.log(`Recording migration ${filename}`); - // Record that the migration was finished. - let m = new MigrationModel({version}); - await m.save(); + // Record that the migration was finished. + let m = new MigrationModel({version}); + await m.save(); - console.log(`Finished recording migration ${version}.js`); + console.log(`Finished recording migration ${filename}`); + } catch (e) { + console.error(`Migration ${filename} could not be recorded`); + throw e; + } } + + console.log(`Database now at migration version ${migrations[migrations.length - 1].version}`); } + /** + * Returns the latest migration version number that has been applied to the + * database, null if none were found. + */ static async latestVersion() { // Load the latest migration details from the database. @@ -138,5 +170,21 @@ module.exports = class MigrationService { if (!latestVersion || latestVersion < requiredVersion) { throw new Error(`A database migration is required, version required ${requiredVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``); } + + return latestVersion; } -}; +} + +// Verify that the minimum migration version is met. +MigrationService + .verify(minVersion) + .then((latestVersion) => { + debug(`minimum migration version ${minVersion} was met with version ${latestVersion}`); + }) + .catch((e) => { + + // Throw the error in the catch block. + throw e; + }); + +module.exports = MigrationService; diff --git a/services/mongoose.js b/services/mongoose.js index acda2f564..c697e38ef 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -59,3 +59,4 @@ require('../models/asset'); require('../models/comment'); require('../models/setting'); require('../models/user'); +require('./migration'); diff --git a/services/passport.js b/services/passport.js index c64bb2334..826297ae5 100644 --- a/services/passport.js +++ b/services/passport.js @@ -7,7 +7,7 @@ const JWT = require('jsonwebtoken'); const LocalStrategy = require('passport-local').Strategy; const errors = require('../errors'); const uuid = require('uuid'); -const debug = require('debug')('talk:passport'); +const debug = require('debug')('talk:services:passport'); const {createClient} = require('./redis'); // Create a redis client to use for authentication. diff --git a/services/redis.js b/services/redis.js index c049b2d00..c6506eb3c 100644 --- a/services/redis.js +++ b/services/redis.js @@ -1,5 +1,5 @@ const redis = require('redis'); -const debug = require('debug')('talk:redis'); +const debug = require('debug')('talk:services:redis'); const { REDIS_URL } = require('../config'); From 4cbc8b2226ffae6c724630acf8d9765c8ffed752 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 7 Jun 2017 11:08:39 -0600 Subject: [PATCH 38/56] Updates to setup process --- bin/cli-serve | 13 +- bin/cli-setup | 309 ++++++++++++++++++++++-------------------- services/migration.js | 22 +-- 3 files changed, 183 insertions(+), 161 deletions(-) diff --git a/bin/cli-serve b/bin/cli-serve index 7978c6c85..63eda3af2 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -5,6 +5,7 @@ const app = require('../app'); const {createServer} = require('http'); const scraper = require('../services/scraper'); const mailer = require('../services/mailer'); +const MigrationService = require('../services/migration'); const kue = require('../services/kue'); const mongoose = require('../services/mongoose'); const util = require('./util'); @@ -87,7 +88,17 @@ function onListening() { /** * Start the app. */ -function startApp(program) { +async function startApp(program) { + + try { + + // Verify that the minimum migration version is met. + await MigrationService.verify(); + + } catch(e) { + console.error(e); + process.exit(1); + } /** * Listen on provided port, on all network interfaces. diff --git a/bin/cli-setup b/bin/cli-setup index e9f0959ef..add134209 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -12,6 +12,7 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options'); const SettingsService = require('../services/settings'); const SetupService = require('../services/setup'); const UsersService = require('../services/users'); +const MigrationService = require('../services/migration'); const util = require('./util'); const errors = require('../errors'); @@ -33,166 +34,186 @@ program // Setup the application //============================================================================== -const performSetup = () => { +const performSetup = async () => { if (program.defaults) { - return SettingsService - .init() - .then(() => { - console.log('Settings created.'); - console.log('\nTalk is now installed!'); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); + await SettingsService.init(); + + console.log('Settings created.'); + console.log('\nTalk is now installed!'); + + return; } // Get the current settings, we are expecing an error here. - return SettingsService - .retrieve() - .then(() => { + try { - // We should NOT have gotten a settings object, this means that the - // application is already setup. Error out here. - throw errors.ErrSettingsInit; + // Try to get the settings. + await SettingsService.retrieve(); - }) - .catch((err) => { + // We should NOT have gotten a settings object, this means that the + // application is already setup. Error out here. + throw errors.ErrSettingsInit; - // If the error is `not init`, then we're good, otherwise, it's something - // else. - if (err !== errors.ErrSettingsNotInit) { - throw err; + } catch (e) { + + // If the error is `not init`, then we're good, otherwise, it's something + // else. + if (e !== errors.ErrSettingsNotInit) { + throw e; + } + } + + // Get the migrations to run. + let migrations = await MigrationService.listPending(); + if (migrations.length > 0) { + + console.log('Now going to run the following migrations:\n'); + + for (let {filename} of migrations) { + console.log(`\tmigrations/${filename}`); + } + + console.log(''); + + let {confirm} = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Proceed with migrations', + default: false } + ]); - }) - .then(() => { + if (!confirm) { + throw new Error('migrations are needed to complete setup'); + } - // Create the base settings model. - let settings = new SettingModel(); + // Perform all migrations. + await MigrationService.run(migrations); + } else { + console.log('No migrations have to be run.'); + } - console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n'); + // Create the base settings model. + let settings = new SettingModel(); - return inquirer.prompt([ - { - type: 'input', - name: 'organizationName', - message: 'Organization Name', - default: settings.organizationName, - validate: (input) => { - if (input && input.length > 0) { - return true; - } + console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n'); - return 'Organization Name is required.'; - } - }, - { - type: 'list', - choices: MODERATION_OPTIONS, - name: 'moderation', - default: settings.moderation, - message: 'Select a moderation mode' - }, - { - type: 'confirm', - name: 'requireEmailConfirmation', - default: settings.requireEmailConfirmation, - message: 'Should emails always be confirmed' - } - ]) - .then((answers) => { - - // Update the settings that were changed. - Object.keys(answers).forEach((key) => { - if (answers[key] !== undefined) { - settings[key] = answers[key]; - } - }); - - console.log('\nWe\'ll ask you some questions about your first admin user.\n'); - - return inquirer.prompt([ - { - type: 'input', - name: 'username', - message: 'Username', - filter: (username) => { - return UsersService - .isValidUsername(username, false) - .catch((err) => { - throw err.message; - }); - } - }, - { - name: 'email', - message: 'Email', - format: 'email', - validate: (value) => { - if (value && value.length >= 3) { - return true; - } - - return 'Email is required'; - } - }, - { - name: 'password', - message: 'Password', - type: 'password', - filter: (password) => { - return UsersService - .isValidPassword(password) - .catch((err) => { - throw err.message; - }); - } - }, - { - name: 'confirmPassword', - message: 'Confirm Password', - type: 'password', - filter: (confirmPassword) => { - return UsersService - .isValidPassword(confirmPassword) - .catch((err) => { - throw err.message; - }); - } - }, - ]); - }) - .then((user) => { - - if (user.password !== user.confirmPassword) { - return Promise.reject(new Error('Passwords do not match')); + let answers = await inquirer.prompt([ + { + type: 'input', + name: 'organizationName', + message: 'Organization Name', + default: settings.organizationName, + validate: (input) => { + if (input && input.length > 0) { + return true; } - return SetupService.setup({ - settings: settings.toObject(), - user: { - email: user.email, - username: user.username, - password: user.password - } - }); - }); - }) - .then(({user}) => { - console.log('Settings created.'); - console.log(`User ${user.id} created.`); - console.log('\nTalk is now installed!'); - console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); + return 'Organization Name is required.'; + } + }, + { + type: 'list', + choices: MODERATION_OPTIONS, + name: 'moderation', + default: settings.moderation, + message: 'Select a moderation mode' + }, + { + type: 'confirm', + name: 'requireEmailConfirmation', + default: settings.requireEmailConfirmation, + message: 'Should emails always be confirmed' + } + ]); + + // Update the settings that were changed. + Object.keys(answers).forEach((key) => { + if (answers[key] !== undefined) { + settings[key] = answers[key]; + } + }); + + console.log('\nWe\'ll ask you some questions about your first admin user.\n'); + + let user = await inquirer.prompt([ + { + type: 'input', + name: 'username', + message: 'Username', + filter: (username) => { + return UsersService + .isValidUsername(username, false) + .catch((err) => { + throw err.message; + }); + } + }, + { + name: 'email', + message: 'Email', + format: 'email', + validate: (value) => { + if (value && value.length >= 3) { + return true; + } + + return 'Email is required'; + } + }, + { + name: 'password', + message: 'Password', + type: 'password', + filter: (password) => { + return UsersService + .isValidPassword(password) + .catch((err) => { + throw err.message; + }); + } + }, + { + name: 'confirmPassword', + message: 'Confirm Password', + type: 'password', + filter: (confirmPassword) => { + return UsersService + .isValidPassword(confirmPassword) + .catch((err) => { + throw err.message; + }); + } + }, + ]); + + if (user.password !== user.confirmPassword) { + return Promise.reject(new Error('Passwords do not match')); + } + + let {user: newUser} = await SetupService.setup({ + settings: settings.toObject(), + user: { + email: user.email, + username: user.username, + password: user.password + } + }); + + console.log('Settings created.'); + console.log(`User ${newUser.id} created.`); + console.log('\nTalk is now installed!'); + console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'); }; // Start tthe setup process. -performSetup(); +performSetup() + .then(() => { + util.shutdown(); + }) + .catch((e) => { + console.error(e); + util.shutdown(1); + }); diff --git a/services/migration.js b/services/migration.js index 11eec3247..8676b91be 100644 --- a/services/migration.js +++ b/services/migration.js @@ -157,34 +157,24 @@ class MigrationService { return latestMigration[0].version; } - static async verify(requiredVersion) { + static async verify() { // If the requiredVersion isn't specified or is 0, then don't complain. - if (typeof requiredVersion !== 'number' || requiredVersion === 0) { + if (typeof minVersion !== 'number' || minVersion === 0) { return; } // If the latest migration does not match the required version, then error // out. let latestVersion = await MigrationService.latestVersion(); - if (!latestVersion || latestVersion < requiredVersion) { - throw new Error(`A database migration is required, version required ${requiredVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``); + if (!latestVersion || latestVersion < minVersion) { + throw new Error(`A database migration is required, version required ${minVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``); } + debug(`minimum migration version ${minVersion} was met with version ${latestVersion}`); + return latestVersion; } } -// Verify that the minimum migration version is met. -MigrationService - .verify(minVersion) - .then((latestVersion) => { - debug(`minimum migration version ${minVersion} was met with version ${latestVersion}`); - }) - .catch((e) => { - - // Throw the error in the catch block. - throw e; - }); - module.exports = MigrationService; From 38077cf3cfc19fdda0ce843fa9ab10fd72bf479e Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 11:56:38 -0300 Subject: [PATCH 39/56] Missing fragment --- client/coral-embed-stream/src/graphql/index.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 7d20ea48b..aa6b5d9a2 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -36,18 +36,6 @@ const extension = { name } } - ModifyTagResponse: gql` - fragment CoralEmbedStream_ModifyTagResponse on ModifyTagResponse { - errors { - translation_key - } - } - `, - DeleteActionResponse: gql` - fragment CoralEmbedStream_DeleteActionResponse on DeleteActionResponse { - errors { - translation_key - } } `, CreateFlagResponse: gql` From 15b5dc2236fd52d9701df94a05049d028feb1f07 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 11:58:38 -0300 Subject: [PATCH 40/56] Linting --- client/coral-embed-stream/src/graphql/index.js | 2 +- plugins/coral-plugin-offtopic/index.js | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index aa6b5d9a2..7f62240b5 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -158,8 +158,8 @@ const extension = { assigned_by: { id: auth.toJS().user.id, }, + __typename: 'Tag' })), - tags: tags.map((t) => ({name: t, __typename: 'Tag'})), status: null, replyCount: 0, parent: parent_id diff --git a/plugins/coral-plugin-offtopic/index.js b/plugins/coral-plugin-offtopic/index.js index 9988686ff..eae6a2ef7 100644 --- a/plugins/coral-plugin-offtopic/index.js +++ b/plugins/coral-plugin-offtopic/index.js @@ -1,6 +1,3 @@ -const {readFileSync} = require('fs'); -const path = require('path'); - module.exports = { tags: [ { From 82a72e506fe9eb0542c26529e42d8fed329ba5be Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 12:04:33 -0300 Subject: [PATCH 41/56] Off-topic and Staff working --- client/coral-embed-stream/src/components/Comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index b5eca4ff2..d798b446c 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -26,8 +26,8 @@ import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import {getEditableUntilDate} from './util'; import styles from './Comment.css'; -const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF'); -const hasTag = (tags, lookupTag) => !!tags.filter((tag) => tag.name === lookupTag).length; +const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF'); +const hasTag = (tags, lookupTag) => !!tags.filter((t) => t.tag.name === lookupTag).length; const hasComment = (nodes, id) => nodes.some((node) => node.id === id); // resetCursors will return the id cursors of the first and second newest comment in From 3a285d92a5cc889cb802abd3e43470d80ecc3710 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 09:10:53 -0600 Subject: [PATCH 42/56] Added more warnings --- bin/cli-migration | 17 +++++++++++++++-- plugins.default.json | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bin/cli-migration b/bin/cli-migration index 9c3e077b1..83a84e29d 100755 --- a/bin/cli-migration +++ b/bin/cli-migration @@ -41,7 +41,20 @@ async function runMigrations() { console.log(`\tmigrations/${filename}`); } - let answers = await inquirer.prompt([ + let {backedUp} = await inquirer.prompt([ + { + type: 'confirm', + name: 'backedUp', + message: 'Did you perform a database backup', + default: false + } + ]); + + if (!backedUp) { + throw new Error('Please backup your databases prior to migrations occuring'); + } + + let {confirm} = await inquirer.prompt([ { type: 'confirm', name: 'confirm', @@ -50,7 +63,7 @@ async function runMigrations() { } ]); - if (answers.confirm) { + if (confirm) { // Run the migrations. await MigrationService.run(migrations); diff --git a/plugins.default.json b/plugins.default.json index 753eb9000..2fb3aaa21 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -4,7 +4,7 @@ "coral-plugin-like", "coral-plugin-respect", "coral-plugin-offtopic", - "coral-plugin-facebook-auth", + "coral-plugin-facebook-auth" ], "client": [ "coral-plugin-respect", From 480bab3c1a170b5d882f39812f493b9ff03ee227 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 09:12:08 -0600 Subject: [PATCH 43/56] changed some text around --- bin/cli-migration | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/cli-migration b/bin/cli-migration index 83a84e29d..036ac8b02 100755 --- a/bin/cli-migration +++ b/bin/cli-migration @@ -32,15 +32,6 @@ async function runMigrations() { try { - // Get the migrations to run. - let migrations = await MigrationService.listPending(); - - console.log('Now going to run the following migrations:\n'); - - for (let {filename} of migrations) { - console.log(`\tmigrations/${filename}`); - } - let {backedUp} = await inquirer.prompt([ { type: 'confirm', @@ -54,6 +45,15 @@ async function runMigrations() { throw new Error('Please backup your databases prior to migrations occuring'); } + // Get the migrations to run. + let migrations = await MigrationService.listPending(); + + console.log('Now going to run the following migrations:\n'); + + for (let {filename} of migrations) { + console.log(`\tmigrations/${filename}`); + } + let {confirm} = await inquirer.prompt([ { type: 'confirm', From 1f261f5216f03f1adaa7f408b0e5945d15342a19 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 12:35:42 -0300 Subject: [PATCH 44/56] Fully working --- .../src/components/Comment.js | 22 ++++++++++--------- .../src/components/Stream.js | 12 +++++----- .../coral-embed-stream/src/graphql/index.js | 8 +++---- client/coral-framework/graphql/mutations.js | 16 ++++++++------ .../client/components/OffTopicTag.js | 5 +---- test/server/graph/mutations/removeTag.js | 2 +- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index d798b446c..4233325c5 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -180,10 +180,10 @@ export default class Comment extends React.Component { commentIsIgnored: React.PropTypes.func, // dispatch action to add a tag to a comment - addCommentTag: React.PropTypes.func, + addTag: React.PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: React.PropTypes.func, + removeTag: React.PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, @@ -286,11 +286,11 @@ export default class Comment extends React.Component { deleteAction, disableReply, maxCharCount, - addCommentTag, addNotification, charCountEnable, showSignInDialog, - removeCommentTag, + addTag, + removeTag, liveUpdates, commentIsIgnored, commentClassNames = [] @@ -333,18 +333,20 @@ export default class Comment extends React.Component { const addBestTag = notifyOnError( () => - addCommentTag({ + addTag({ id: comment.id, - tag: BEST_TAG + name: BEST_TAG, + assetId: asset.id }), () => 'Failed to tag comment as best' ); const removeBestTag = notifyOnError( () => - removeCommentTag({ + removeTag({ id: comment.id, - tag: BEST_TAG + name: BEST_TAG, + assetId: asset.id }), () => 'Failed to remove best comment tag' ); @@ -547,8 +549,8 @@ export default class Comment extends React.Component { currentUser={currentUser} postFlag={postFlag} deleteAction={deleteAction} - addCommentTag={addCommentTag} - removeCommentTag={removeCommentTag} + addTag={addTag} + removeTag={removeTag} ignoreUser={ignoreUser} charCountEnable={charCountEnable} maxCharCount={maxCharCount} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 586d28f23..91e696652 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -129,10 +129,10 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - addCommentTag, + addTag, ignoreUser, auth: {loggedIn, user}, - removeCommentTag, + removeTag, pluginProps, editName } = this.props; @@ -264,8 +264,8 @@ class Stream extends React.Component { currentUser={user} postFlag={postFlag} postDontAgree={postDontAgree} - addCommentTag={addCommentTag} - removeCommentTag={removeCommentTag} + addTag={addTag} + removeTag={removeTag} ignoreUser={ignoreUser} commentIsIgnored={commentIsIgnored} loadMore={this.props.loadNewReplies} @@ -298,10 +298,10 @@ Stream.propTypes = { postComment: PropTypes.func.isRequired, // dispatch action to add a tag to a comment - addCommentTag: PropTypes.func, + addTag: PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: PropTypes.func, + removeTag: PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 7f62240b5..dab648c8b 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -18,8 +18,8 @@ const extension = { } } `, - RemoveCommentTagResponse: gql` - fragment CoralEmbedStream_RemoveCommentTagResponse on RemoveCommentTagResponse { + RemoveTagResponse: gql` + fragment CoralEmbedStream_RemoveTagResponse on RemoveTagResponse { comment { id tags { @@ -28,8 +28,8 @@ const extension = { } } `, - AddCommentTagResponse: gql` - fragment CoralEmbedStream_AddCommentTagResponse on AddCommentTagResponse { + AddTagResponse: gql` + fragment CoralEmbedStream_AddTagResponse on AddTagResponse { comment { id tags { diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 183dead09..6660af999 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -174,7 +174,7 @@ export const withDeleteAction = withMutation( }); const COMMENT_FRAGMENT = gql` - fragment CoraBest_UpdateFragment on Comment { + fragment CoralBest_UpdateFragment on Comment { tags { tag { name @@ -185,9 +185,11 @@ const COMMENT_FRAGMENT = gql` export const withAddTag = withMutation( gql` - mutation AddCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + mutation AddTag($id: ID!, $asset_id: ID!, $name: String!) { addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { - ...ModifyTagResponse + errors { + translation_key + } } } `, { @@ -228,7 +230,7 @@ export const withAddTag = withMutation( export const withRemoveTag = withMutation( gql` - mutation RemoveCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) { removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { errors { translation_key @@ -237,12 +239,12 @@ export const withRemoveTag = withMutation( } `, { props: ({mutate}) => ({ - removeTag: ({id, name, asset_id}) => { + removeTag: ({id, name, assetId}) => { return mutate({ variables: { id, name, - asset_id + asset_id: assetId }, update: (proxy) => { const fragmentId = `Comment_${id}`; @@ -250,7 +252,7 @@ export const withRemoveTag = withMutation( // Read the data from our cache for this query. const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - const idx = data.tags.findIndex((i) => i.tag.name === 'BEST'); + const idx = data.tags.findIndex(i => i.tag.name === 'BEST'); data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)]; diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js index e8029ce5e..0e028c6bf 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js @@ -1,11 +1,8 @@ import React from 'react'; import styles from './styles.css'; - import {t} from 'plugin-api/beta/client/services'; -const isOffTopic = (tags) => { - return !!tags.filter((tag) => tag.name === 'OFF_TOPIC').length; -}; +const isOffTopic = (tags) => !!tags.filter((t) => t.tag.name === 'OFF_TOPIC').length; export default (props) => ( diff --git a/test/server/graph/mutations/removeTag.js b/test/server/graph/mutations/removeTag.js index 95b3e8bd2..a1521c2ce 100644 --- a/test/server/graph/mutations/removeTag.js +++ b/test/server/graph/mutations/removeTag.js @@ -23,7 +23,7 @@ describe('graph.mutations.removeTag', () => { }); const query = ` - mutation RemoveCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) { removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { errors { translation_key From f6798af74d38f0b115e98dde1f3fcd59a0ac7577 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 12:37:37 -0300 Subject: [PATCH 45/56] Lint --- client/coral-framework/graphql/mutations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 6660af999..20ed175b3 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -252,7 +252,7 @@ export const withRemoveTag = withMutation( // Read the data from our cache for this query. const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - const idx = data.tags.findIndex(i => i.tag.name === 'BEST'); + const idx = data.tags.findIndex((i) => i.tag.name === 'BEST'); data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)]; From 283c13fae8551cf1906bd163d5bdfb2813155ad8 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 12:43:02 -0300 Subject: [PATCH 46/56] =?UTF-8?q?=C3=81dding=20typenames=20to=20update=20t?= =?UTF-8?q?he=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-embed-stream/src/graphql/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index dab648c8b..7ca22a5f4 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -154,11 +154,13 @@ const extension = { tag: { name: tag, created_at: new Date().toISOString(), + __typename: 'Tag' }, assigned_by: { id: auth.toJS().user.id, + __typename: 'User' }, - __typename: 'Tag' + __typename: 'TagLink' })), status: null, replyCount: 0, From 8eb8010a3346d19aedd19bd04031c5b208202d69 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 8 Jun 2017 23:54:08 +0700 Subject: [PATCH 47/56] Implement Response interface --- plugins/coral-plugin-like/server/typeDefs.graphql | 2 +- plugins/coral-plugin-love/server/typeDefs.graphql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/coral-plugin-like/server/typeDefs.graphql b/plugins/coral-plugin-like/server/typeDefs.graphql index 1a1100754..40c600f2f 100644 --- a/plugins/coral-plugin-like/server/typeDefs.graphql +++ b/plugins/coral-plugin-like/server/typeDefs.graphql @@ -60,7 +60,7 @@ type CreateLikeResponse implements Response { like: LikeAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } type RootMutation { diff --git a/plugins/coral-plugin-love/server/typeDefs.graphql b/plugins/coral-plugin-love/server/typeDefs.graphql index e22b91a2d..edc45e20b 100644 --- a/plugins/coral-plugin-love/server/typeDefs.graphql +++ b/plugins/coral-plugin-love/server/typeDefs.graphql @@ -60,7 +60,7 @@ type CreateLoveResponse implements Response { love: LoveAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } type RootMutation { From 9b839b76f508390629a12a814b3c5ad87844d67f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 8 Jun 2017 23:55:37 +0700 Subject: [PATCH 48/56] Don't assume response name pattern --- client/coral-framework/graphql/fragments.js | 34 +++++++++------------ client/coral-framework/utils/index.js | 15 +++++++++ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js index 9c396cbaa..3c64090bd 100644 --- a/client/coral-framework/graphql/fragments.js +++ b/client/coral-framework/graphql/fragments.js @@ -1,23 +1,19 @@ -import {gql} from 'react-apollo'; -import * as mutations from './mutations'; - -function createDefaultResponseFragments() { - const names = Object.keys(mutations).map((key) => key.replace('with', '')); - const result = {}; - names.forEach((name) => { - const response = `${name}Response`; - result[response] = gql` - fragment Coral_${response} on ${response} { - errors { - translation_key - } - } - `; - }); - return result; -} +import {createDefaultResponseFragments} from '../utils'; // fragments defined here are automatically registered. export default { - ...createDefaultResponseFragments() + ...createDefaultResponseFragments( + 'SetCommentStatusResponse', + 'SuspendUserResponse', + 'RejectUsernameResponse', + 'SetUserStatusResponse', + 'PostCommentResponse', + 'EditCommentResponse', + 'PostFlagResponse', + 'CreateDontAgreeResponse', + 'DeleteActionResponse', + 'ModifyTagResponse', + 'IgnoreUserResponse', + 'StopIgnoringUserResponse', + ) }; diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index ca2dd5ab2..1d430f1fc 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -112,3 +112,18 @@ export function getResponseErrors(mutationResult) { }); return result.length ? result : false; } + +export function createDefaultResponseFragments(...names) { + const result = {}; + names.forEach((name) => { + const response = `${name}Response`; + result[response] = gql` + fragment Coral_${response} on ${response} { + errors { + translation_key + } + } + `; + }); + return result; +} From 80900a98fdae57bb7d26f7eed1294b973dac1d4e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 11:19:43 -0600 Subject: [PATCH 49/56] perform migrations during web install --- bin/cli-serve | 42 +++++++++++++++++++++--- services/setup.js | 84 +++++++++++++++++++++++------------------------ 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/bin/cli-serve b/bin/cli-serve index 63eda3af2..f2a6d3592 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -2,10 +2,13 @@ const program = require('./commander'); const app = require('../app'); +const debug = require('debug')('talk:cli:serve'); +const errors = require('../errors'); const {createServer} = require('http'); const scraper = require('../services/scraper'); const mailer = require('../services/mailer'); const MigrationService = require('../services/migration'); +const SetupService = require('../services/setup'); const kue = require('../services/kue'); const mongoose = require('../services/mongoose'); const util = require('./util'); @@ -92,12 +95,41 @@ async function startApp(program) { try { - // Verify that the minimum migration version is met. - await MigrationService.verify(); + // Check to see if the application is installed. If the application + // has been installed, then it will throw errors.ErrSettingsNotInit, this + // just means we don't have to check that the migrations have run. + await SetupService.isAvailable(); - } catch(e) { - console.error(e); - process.exit(1); + debug('setup is currently available, migrations not being checked'); + + } catch (e) { + + // Check the error. + switch (e) { + case errors.ErrInstallLock, errors.ErrSettingsInit: + + debug('setup is not currently available, migrations now being checked'); + + // The error was expected, just continue. + break; + default: + + // The error was not expected, throw the error! + throw e; + } + + // Now try and check the migration status. + try { + + // Verify that the minimum migration version is met. + await MigrationService.verify(); + + } catch (e) { + console.error(e); + process.exit(1); + } + + debug('migrations do not have to be run'); } /** diff --git a/services/setup.js b/services/setup.js index 233021c17..053a85789 100644 --- a/services/setup.js +++ b/services/setup.js @@ -1,5 +1,6 @@ const UsersService = require('./users'); const SettingsService = require('./settings'); +const MigrationService = require('./migration'); const SettingsModel = require('../models/setting'); const errors = require('../errors'); const { @@ -15,34 +16,32 @@ module.exports = class SetupService { /** * This returns a promise which resolves if the setup is available. */ - static isAvailable() { + static async isAvailable() { // Check if we have an install lock present. if (INSTALL_LOCK) { - return Promise.reject(errors.ErrInstallLock); + throw errors.ErrInstallLock; } - // Get the current settings, we are expecing an error here. - return SettingsService - .retrieve() - .then(() => { + try { - // We should NOT have gotten a settings object, this means that the - // application is already setup. Error out here. - return Promise.reject(errors.ErrSettingsInit); + // Get the current settings, we are expecing an error here. + await SettingsService.retrieve(); + + // We should NOT have gotten a settings object, this means that the + // application is already setup. Error out here. + throw errors.ErrSettingsInit; + } catch (e) { - }) - .catch((err) => { + // If the error is `not init`, then we're good, otherwise, it's something + // else. + if (e !== errors.ErrSettingsNotInit) { + throw e; + } - // If the error is `not init`, then we're good, otherwise, it's something - // else. - if (err !== errors.ErrSettingsNotInit) { - return Promise.reject(err); - } - - // Allow the request to keep going here. - return; - }); + // Allow the request to keep going here. + return; + } } /** @@ -69,35 +68,34 @@ module.exports = class SetupService { /** * This will perform the setup. */ - static setup({settings, user: {email, password, username}}) { + static async setup({settings, user: {email, password, username}}) { // Validate the settings first. - return SetupService - .validate({settings, user: {email, password, username}}) - .then(() => { - return SettingsService.update(settings); - }) - .then((settings) => { + await SetupService.validate({settings, user: {email, password, username}}); - // Settings are created! Create the user. + // Get the migrations to run. + let migrations = await MigrationService.listPending(); - // Create the user. - return UsersService - .createLocalUser(email, password, username) + // Perform all migrations. + await MigrationService.run(migrations); - // Grant them administrative privileges and confirm the email account. - .then((user) => { + settings = await SettingsService.update(settings); - return Promise.all([ - UsersService.addRoleToUser(user.id, 'ADMIN'), - UsersService.confirmEmail(user.id, email) - ]) - .then(() => ({ - settings, - user - })); - }); - }); + // Settings are created! Create the user. + + // Create the user. + let user = await UsersService.createLocalUser(email, password, username); + + // Grant them administrative privileges and confirm the email account. + await Promise.all([ + UsersService.addRoleToUser(user.id, 'ADMIN'), + UsersService.confirmEmail(user.id, email) + ]); + + return { + settings, + user + }; } }; From b95e20e493f7356c0f1fdccbdad554fdfe5a5160 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 11:21:09 -0600 Subject: [PATCH 50/56] removed explicit migration step --- bin/cli-setup | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/bin/cli-setup b/bin/cli-setup index add134209..680fba91a 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -12,7 +12,6 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options'); const SettingsService = require('../services/settings'); const SetupService = require('../services/setup'); const UsersService = require('../services/users'); -const MigrationService = require('../services/migration'); const util = require('./util'); const errors = require('../errors'); @@ -64,37 +63,6 @@ const performSetup = async () => { } } - // Get the migrations to run. - let migrations = await MigrationService.listPending(); - if (migrations.length > 0) { - - console.log('Now going to run the following migrations:\n'); - - for (let {filename} of migrations) { - console.log(`\tmigrations/${filename}`); - } - - console.log(''); - - let {confirm} = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: 'Proceed with migrations', - default: false - } - ]); - - if (!confirm) { - throw new Error('migrations are needed to complete setup'); - } - - // Perform all migrations. - await MigrationService.run(migrations); - } else { - console.log('No migrations have to be run.'); - } - // Create the base settings model. let settings = new SettingModel(); From b06a58ebe1b96f9753fccc56b8b560914472e88a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 11:21:44 -0600 Subject: [PATCH 51/56] adjusted flow --- bin/cli-setup | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/cli-setup b/bin/cli-setup index 680fba91a..9ecdecc95 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -35,15 +35,6 @@ program const performSetup = async () => { - if (program.defaults) { - await SettingsService.init(); - - console.log('Settings created.'); - console.log('\nTalk is now installed!'); - - return; - } - // Get the current settings, we are expecing an error here. try { @@ -63,6 +54,15 @@ const performSetup = async () => { } } + if (program.defaults) { + await SettingsService.init(); + + console.log('Settings created.'); + console.log('\nTalk is now installed!'); + + return; + } + // Create the base settings model. let settings = new SettingModel(); From 57747e8f13486d1e9ed32a4cb8353a4e3ed25757 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 00:22:58 +0700 Subject: [PATCH 52/56] Make mutations extendable --- client/coral-framework/graphql/mutations.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 20ed175b3..dcba97891 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -187,9 +187,7 @@ export const withAddTag = withMutation( gql` mutation AddTag($id: ID!, $asset_id: ID!, $name: String!) { addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { - errors { - translation_key - } + ...ModifyTagResponse } } `, { @@ -232,9 +230,7 @@ export const withRemoveTag = withMutation( gql` mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) { removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { - errors { - translation_key - } + ...ModifyTagResponse } } `, { From 995dd9fd6065b15717a481ee5156ea0053ecff5c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 00:31:19 +0700 Subject: [PATCH 53/56] Correct fragment names --- client/coral-framework/utils/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 1d430f1fc..0b79b7b02 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -115,8 +115,7 @@ export function getResponseErrors(mutationResult) { export function createDefaultResponseFragments(...names) { const result = {}; - names.forEach((name) => { - const response = `${name}Response`; + names.forEach((response) => { result[response] = gql` fragment Coral_${response} on ${response} { errors { From 1153c52c2c702529df6d9d7209986a3b8f849eb8 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 00:31:29 +0700 Subject: [PATCH 54/56] Correct optimistic response --- client/coral-framework/graphql/mutations.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index dcba97891..3b9ef01f4 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -200,8 +200,8 @@ export const withAddTag = withMutation( asset_id: assetId }, optimisticResponse: { - deleteAction: { - __typename: 'DeleteActionResponse', + addTag: { + __typename: 'ModifyTagResponse', errors: null, } }, @@ -242,6 +242,12 @@ export const withRemoveTag = withMutation( name, asset_id: assetId }, + optimisticResponse: { + removeTag: { + __typename: 'ModifyTagResponse', + errors: null, + } + }, update: (proxy) => { const fragmentId = `Comment_${id}`; From 428246cbe2f199246e3e2bbe9d1b39afef95e68e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 01:16:39 +0700 Subject: [PATCH 55/56] Make it work without iframe --- client/coral-embed-stream/src/index.js | 27 ++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 821399488..16c510c29 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -19,14 +19,29 @@ loadPluginsTranslations(); injectPluginsReducers(); injectReducers(reducers); +function inIframe() { + try { + return window.self !== window.top; + } catch (e) { + return true; + } +} + +function init(config = {}) { + store.dispatch(addExternalConfig(config)); + store.dispatch(checkLogin()); +} + // Don't run this in the popup. if (!window.opener) { - pym.sendMessage('getConfig'); - - pym.onMessage('config', (config) => { - store.dispatch(addExternalConfig(JSON.parse(config))); - store.dispatch(checkLogin()); - }); + if (inIframe()) { + pym.sendMessage('getConfig'); + pym.onMessage('config', (config) => { + init(JSON.parse(config)); + }); + } else { + init(); + } } render( From 4e5680577e1f086327b0087f86d0621e9688c82b Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 01:17:27 +0700 Subject: [PATCH 56/56] Include public comments --- graph/resolvers/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js index 9dd67bb0e..ef3c70865 100644 --- a/graph/resolvers/util.js +++ b/graph/resolvers/util.js @@ -9,7 +9,7 @@ const decorateWithTags = (typeResolver) => { return tags; } - return tags.filter((tag) => tag.public); + return tags.filter((t) => t.tag.permissions.public); }; };