diff --git a/.eslintrc.json b/.eslintrc.json index 8b737cbd2..34b46b83b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,7 +17,7 @@ "no-template-curly-in-string": [1], "no-unsafe-negation": [1], "array-callback-return": [1], - "eqeqeq": [2], + "eqeqeq": [2, "smart"], "no-eval": [2], "no-global-assign": [2], "no-implied-eval": [2], diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 0e5806e6b..4ed0296a2 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -22,7 +22,7 @@ const Comment = ({comment, currentUser, asset, depth}) => { id={`c_${comment.id}`} style={{marginLeft: depth * 30}}>
- {/* { - console.log(props) + console.log(props); if (props.notification.text) { setTimeout(() => { props.clearNotification(); diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 52a6377fe..ebafab918 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -59,7 +59,7 @@ class CommentBox extends Component { } postItem(comment, 'comments') .then(({data}) => { - const postedComment = data.createComment + const postedComment = data.createComment; const commentId = postedComment.id; if (postedComment.status === 'rejected') { addNotification('error', lang.t('comment-post-banned-word')); diff --git a/graph/context.js b/graph/context.js new file mode 100644 index 000000000..cdc05f6cf --- /dev/null +++ b/graph/context.js @@ -0,0 +1,21 @@ +const loaders = require('./loaders'); +const mutators = require('./mutators'); + +/** + * Stores the request context. + */ +class Context { + constructor({user = null}) { + + // Load the current logged in user to `user`, otherwise this'll be null. + this.user = user; + + // Create the loaders. + this.loaders = loaders(this); + + // Create the mutators. + this.mutators = mutators(this); + } +} + +module.exports = Context; diff --git a/graph/index.js b/graph/index.js index 514df2d0a..7fddce2eb 100644 --- a/graph/index.js +++ b/graph/index.js @@ -1,24 +1,14 @@ -const loaders = require('./loaders'); -const mutators = require('./mutators'); const schema = require('./schema'); +const Context = require('./context'); module.exports = { - createGraphOptions: (req) => { + createGraphOptions: (req) => ({ - let context = {}; + // Schema is created already, so just include it. + schema, - // Load the current logged in user to `user`, otherwise this'll be null. - context.user = req.user; - - // Create the loaders. - context.loaders = loaders(context); - - // Create the mutators. - context.mutators = mutators(context); - - return { - schema, - context - }; - } + // Load in the new context here, this'll create the loaders + mutators for + // the lifespan of this request. + context: new Context(req) + }) }; diff --git a/graph/loaders.js b/graph/loaders.js deleted file mode 100644 index 4388af4a8..000000000 --- a/graph/loaders.js +++ /dev/null @@ -1,171 +0,0 @@ -const DataLoader = require('dataloader'); -const _ = require('lodash'); -const url = require('url'); -const errors = require('../errors'); -const scraper = require('../services/scraper'); - -const Comment = require('../models/comment'); -const User = require('../models/user'); -const Action = require('../models/action'); -const Asset = require('../models/asset'); -const Settings = require('../models/setting'); - -/** - * SingletonResolver is a cached loader for a single result. - */ -class SingletonResolver { - constructor(resolver) { - this._cache = null; - this._resolver = resolver; - } - - load() { - if (this._cache) { - return this._cache; - } - - let promise = this._resolver(arguments).then((result) => { - return result; - }); - - // Set the promise on the cache. - this._cache = promise; - - return promise; - } -} - -/** - * This joins a set of results with a specific keys and sets an empty array in - * place if it was not found. - * @param {Array} ids ids to locate - * @param {String} key key to group by - * @return {Array} array of results - */ -const arrayJoinBy = (ids, key) => (items) => { - const itemsByKey = _.groupBy(items, key); - return ids.map((id) => { - if (id in itemsByKey) { - return itemsByKey[id]; - } - - return []; - }); -}; - -/** - * This joins a set of results with a specific keys and sets null in place if it - * was not found. - * @param {Array} ids ids to locate - * @param {String} key key to group by - * @return {Array} array of results - */ -const singleJoinBy = (ids, key) => (items) => { - const itemsByKey = _.groupBy(items, key); - return ids.map((id) => { - if (id in itemsByKey) { - return itemsByKey[id][0]; - } - - return null; - }); -}; - -/** - * Retrieves assets by an array of ids. - * @param {Array} ids array of ids to lookup - */ -const genAssetsByID = (ids) => Asset.find({ - id: { - $in: ids - } -}).then(singleJoinBy(ids, 'id')); - -/** - * Retrieves actions by an array of ids. - * @param {Array} ids array of ids to lookup - */ -const genActionsByID = (ids, user = {}) => Action.getActionSummaries(ids, user.id).then(arrayJoinBy(ids, 'item_id')); - -/** - * Retrieves comments by an array of asset id's. - * @param {Array} ids array of ids to lookup - */ -const genCommentsByAssetID = (ids) => Comment.find({ - asset_id: { - $in: ids - }, - parent_id: null, - status: { - $in: [null, 'accepted'] - } -}).then(arrayJoinBy(ids, 'asset_id')); - -/** - * Retrieves comments by an array of parent ids. - * @param {Array} ids array of ids to lookup - */ -const genCommentsByParentID = (ids) => Comment.find({ - parent_id: { - $in: ids - }, - status: { - $in: [null, 'accepted'] - } -}).then(arrayJoinBy(ids, 'parent_id')); - -/** - * This endpoint find or creates an asset at the given url when it is loaded. - * @param {String} asset_url the url passed in from the query - * @returns {Promise} resolves to the asset - */ -const findOrCreateAssetByURL = (asset_url) => { - - // Verify that the asset_url is parsable. - let parsed_asset_url = url.parse(asset_url); - if (!parsed_asset_url.protocol) { - return Promise.reject(errors.ErrInvalidAssetURL); - } - - return Asset.findOrCreateByUrl(asset_url) - .then((asset) => { - - // If the asset wasn't scraped before, scrape it! Otherwise just return - // the asset. - if (!asset.scraped) { - return scraper.create(asset).then(() => asset); - } - - return asset; - }); -}; - -/** - * Creates a set of loaders based on a GraphQL context. - * @param {Object} context the context of the GraphQL request - * @return {Object} object of loaders - */ -const createLoaders = (context) => ({ - Comments: { - getByParentID: new DataLoader((ids) => genCommentsByParentID(ids)), - getByAssetID: new DataLoader((ids) => genCommentsByAssetID(ids)), - }, - Actions: { - getByID: new DataLoader((ids) => genActionsByID(ids, context.user)), - }, - Users: { - getByID: new DataLoader((ids) => User.findByIdArray(ids)) - }, - Assets: { - - // TODO: decide whether we want to move these to mutators or not, as in fact - // this operation create a new asset if one isn't found. - getByURL: (url) => findOrCreateAssetByURL(url), - - getByID: new DataLoader((ids) => genAssetsByID(ids)), - getAll: new SingletonResolver(() => Asset.find({})) - }, - Settings: new SingletonResolver(() => Settings.retrieve()) -}); - -module.exports = createLoaders; diff --git a/graph/loaders/actions.js b/graph/loaders/actions.js new file mode 100644 index 000000000..c4664631a --- /dev/null +++ b/graph/loaders/actions.js @@ -0,0 +1,27 @@ +const DataLoader = require('dataloader'); + +const util = require('./util'); + +const Action = require('../../models/action'); + +/** + * Looks up actions based on the requested id's all bounded by the user. + * @param {Object} context the context of the request + * @param {Array} ids array of id's to get + * @return {Promise} resolves to the promises of the requested actions + */ +const genActionSummariessByItemID = ({user = {}}, item_ids) => { + return Action.getActionSummaries(item_ids, user.id) + .then(util.arrayJoinBy(item_ids, 'item_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) => ({ + Actions: { + getByItemID: new DataLoader((ids) => genActionSummariessByItemID(context, ids)), + } +}); diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js new file mode 100644 index 000000000..cd2ef881d --- /dev/null +++ b/graph/loaders/assets.js @@ -0,0 +1,63 @@ +const DataLoader = require('dataloader'); +const url = require('url'); + +const errors = require('../../errors'); +const scraper = require('../../services/scraper'); +const util = require('./util'); + +const Asset = require('../../models/asset'); + +/** + * Retrieves assets by an array of ids. + * @param {Object} context the context of the request + * @param {Array} ids array of ids to lookup + */ +const genAssetsByID = (context, ids) => Asset.find({ + id: { + $in: ids + } +}).then(util.singleJoinBy(ids, 'id')); + +/** + * This endpoint find or creates an asset at the given url when it is loaded. + * @param {Object} context the context of the request + * @param {String} asset_url the url passed in from the query + * @returns {Promise} resolves to the asset + */ +const findOrCreateAssetByURL = (context, asset_url) => { + + // Verify that the asset_url is parsable. + let parsed_asset_url = url.parse(asset_url); + if (!parsed_asset_url.protocol) { + return Promise.reject(errors.ErrInvalidAssetURL); + } + + return Asset.findOrCreateByUrl(asset_url) + .then((asset) => { + + // If the asset wasn't scraped before, scrape it! Otherwise just return + // the asset. + if (!asset.scraped) { + return scraper.create(asset).then(() => asset); + } + + return asset; + }); +}; + +/** + * 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) => ({ + Assets: { + + // TODO: decide whether we want to move these to mutators or not, as in fact + // this operation create a new asset if one isn't found. + getByURL: (url) => findOrCreateAssetByURL(context, url), + + getByID: new DataLoader((ids) => genAssetsByID(context, ids)), + getAll: new util.SingletonResolver(() => Asset.find({})) + } +}); diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js new file mode 100644 index 000000000..24db48079 --- /dev/null +++ b/graph/loaders/comments.js @@ -0,0 +1,91 @@ +const DataLoader = require('dataloader'); + +const util = require('./util'); + +const Action = require('../../models/action'); +const Comment = require('../../models/comment'); + +/** + * Retrieves comments by an array of asset id's. + * @param {Array} ids array of ids to lookup + */ +const genCommentsByAssetID = (context, ids) => Comment.find({ + asset_id: { + $in: ids + }, + parent_id: null, + status: { + $in: [null, 'accepted'] + } +}).then(util.arrayJoinBy(ids, 'asset_id')); + +/** + * Retrieves comments by an array of parent ids. + * @param {Array} ids array of ids to lookup + */ +const genCommentsByParentID = (context, ids) => Comment.find({ + parent_id: { + $in: ids + }, + status: { + $in: [null, 'accepted'] + } +}).then(util.arrayJoinBy(ids, 'parent_id')); + +const getCommentsByStatusAndAssetID = (context, {status = null, asset_id = null}) => { + + // TODO: remove when we move the enum over to the uppercase. + if (status) { + status = status.toLowerCase(); + } + + return Comment.moderationQueue(status, asset_id); +}; + +const getCommentsByActionTypeAndAssetID = (context, {action_type, asset_id = null}) => { + + // TODO: remove when we move the enum over to the uppercase. + if (action_type) { + action_type = action_type.toLowerCase(); + } + + return Action.find({ + action_type, + + // TODO: remove when we move the enum over to the uppercase. + item_type: 'comments' + }).then((actions) => { + let comments = Comment.find({ + id: { + $in: actions.map((action) => action.item_id) + } + }); + + if (asset_id) { + comments = comments.where({asset_id}); + } + + return comments; + }); +}; + +const genCommentsByAuthorID = (context, authorIDs) => Comment.find({ + author_id: { + $in: authorIDs + } +}).then(util.arrayJoinBy(authorIDs, 'author_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) => ({ + Comments: { + getByParentID: new DataLoader((ids) => genCommentsByParentID(context, ids)), + getByAssetID: new DataLoader((ids) => genCommentsByAssetID(context, ids)), + getByStatusAndAssetID: (query) => getCommentsByStatusAndAssetID(context, query), + getByActionTypeAndAssetID: (query) => getCommentsByActionTypeAndAssetID(context, query), + getByAuthorID: new DataLoader((authorIDs) => genCommentsByAuthorID(context, authorIDs)) + } +}); diff --git a/graph/loaders/index.js b/graph/loaders/index.js new file mode 100644 index 000000000..536e40fa9 --- /dev/null +++ b/graph/loaders/index.js @@ -0,0 +1,28 @@ +const _ = require('lodash'); + +const Actions = require('./actions'); +const Assets = require('./assets'); +const Comments = require('./comments'); +const Settings = require('./settings'); +const Users = require('./users'); + +/** + * 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) => { + + // We need to return an object to be accessed. + return _.merge(...[ + Actions, + Assets, + Comments, + Settings, + Users + ].map((loaders) => { + + // Each loader is a function which takes the context. + return loaders(context); + })); +}; diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js new file mode 100644 index 000000000..c7a8810dd --- /dev/null +++ b/graph/loaders/settings.js @@ -0,0 +1,12 @@ +const Settings = require('../../models/setting'); + +const util = require('./util'); + +/** + * 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(() => Settings.retrieve()) +}); diff --git a/graph/loaders/users.js b/graph/loaders/users.js new file mode 100644 index 000000000..d59e524b6 --- /dev/null +++ b/graph/loaders/users.js @@ -0,0 +1,16 @@ +const DataLoader = require('dataloader'); + +const User = require('../../models/user'); + +const genUserByIDs = (context, ids) => User.findByIdArray(ids); + +/** + * 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) => ({ + Users: { + getByID: new DataLoader((ids) => genUserByIDs(context, ids)) + } +}); diff --git a/graph/loaders/util.js b/graph/loaders/util.js new file mode 100644 index 000000000..e947f2fa6 --- /dev/null +++ b/graph/loaders/util.js @@ -0,0 +1,68 @@ +const _ = require('lodash'); + +/** + * SingletonResolver is a cached loader for a single result. + */ +class SingletonResolver { + constructor(resolver) { + this._cache = null; + this._resolver = resolver; + } + + load() { + if (this._cache) { + return this._cache; + } + + let promise = this._resolver(arguments).then((result) => { + return result; + }); + + // Set the promise on the cache. + this._cache = promise; + + return promise; + } +} + +/** + * This joins a set of results with a specific keys and sets an empty array in + * place if it was not found. + * @param {Array} ids ids to locate + * @param {String} key key to group by + * @return {Array} array of results + */ +const arrayJoinBy = (ids, key) => (items) => { + const itemsByKey = _.groupBy(items, key); + return ids.map((id) => { + if (id in itemsByKey) { + return itemsByKey[id]; + } + + return []; + }); +}; + +/** + * This joins a set of results with a specific keys and sets null in place if it + * was not found. + * @param {Array} ids ids to locate + * @param {String} key key to group by + * @return {Array} array of results + */ +const singleJoinBy = (ids, key) => (items) => { + const itemsByKey = _.groupBy(items, key); + return ids.map((id) => { + if (id in itemsByKey) { + return itemsByKey[id][0]; + } + + return null; + }); +}; + +module.exports = { + singleJoinBy, + arrayJoinBy, + SingletonResolver +}; diff --git a/graph/mutators/action.js b/graph/mutators/action.js new file mode 100644 index 000000000..7c37ddd86 --- /dev/null +++ b/graph/mutators/action.js @@ -0,0 +1,54 @@ +const Action = require('../../models/action'); + +/** + * Creates an action on a item. + * @param {Object} user the user performing the request + * @param {String} item_id id of the item to add the action to + * @param {String} item_type type of the item + * @param {String} action_type type of the action + * @return {Promise} resolves to the action created + */ +const createAction = ({user = {}}, {item_id, item_type, action_type, metadata = {}}) => { + return Action.insertUserAction({ + item_id, + item_type, + user_id: user.id, + action_type, + metadata + }); +}; + +/** + * Deletes an action based on the user id if the user owns that action. + * @param {Object} user the user performing the request + * @param {String} id the id of the action to delete + * @return {Promise} resolves when the action is deleted + */ +const deleteAction = ({user}, {id}) => { + return Action.remove({ + id, + user_id: user.id + }); +}; + +module.exports = (context) => { + + // TODO: refactor to something that'll return an error in the event an attempt + // is made to mutate state while not logged in. There's got to be a better way + // to do this. + if (context.user) { + return { + Action: { + create: (action) => createAction(context, action), + delete: (action) => deleteAction(context, action) + } + }; + } + + return { + Action: { + create: () => {}, + delete: () => {} + } + }; +}; diff --git a/graph/mutators.js b/graph/mutators/comment.js similarity index 73% rename from graph/mutators.js rename to graph/mutators/comment.js index 69fdcd92e..26ee691b4 100644 --- a/graph/mutators.js +++ b/graph/mutators/comment.js @@ -1,12 +1,8 @@ -/* eslint eqeqeq: ["error", "smart"]*/ +const errors = require('../../errors'); +const Asset = require('../../models/asset'); +const Comment = require('../../models/comment'); -const errors = require('../errors'); -const Action = require('../models/action'); -const Asset = require('../models/asset'); -const Comment = require('../models/comment'); -const User = require('../models/user'); - -const Wordlist = require('../services/wordlist'); +const Wordlist = require('../../services/wordlist'); /** * Creates a new comment. @@ -126,7 +122,7 @@ const createPublicComment = (context, commentInput) => { // TODO: this is kind of fragile, we should refactor this to resolve // all these const's that we're using like 'comments', 'flag' to be // defined in a checkable schema. - return createAction(null, { + return context.mutators.Action.createAction(null, { item_id: comment.id, item_type: 'comments', action_type: 'flag', @@ -142,47 +138,6 @@ const createPublicComment = (context, commentInput) => { })); }; -/** - * Creates an action on a item. - * @param {Object} user the user performing the request - * @param {String} item_id id of the item to add the action to - * @param {String} item_type type of the item - * @param {String} action_type type of the action - * @return {Promise} resolves to the action created - */ -const createAction = ({user = {}}, {item_id, item_type, action_type, metadata = {}}) => { - return Action.insertUserAction({ - item_id, - item_type, - user_id: user.id, - action_type, - metadata - }); -}; - -/** - * Deletes an action based on the user id if the user owns that action. - * @param {Object} user the user performing the request - * @param {[type]} id [description] - * @return {[type]} [description] - */ -const deleteAction = ({user}, {id}) => { - return Action.remove({ - id, - user_id: user.id - }); -}; - -/** - * Updates a users settings. - * @param {Object} user the user performing the request - * @param {String} bio the new user bio - * @return {Promise} - */ -const updateUserSettings = ({user}, {bio}) => { - return User.updateSettings(user.id, {bio}); -}; - module.exports = (context) => { // TODO: refactor to something that'll return an error in the event an attempt @@ -192,13 +147,6 @@ module.exports = (context) => { return { Comment: { create: (comment) => createPublicComment(context, comment) - }, - Action: { - create: (action) => createAction(context, action), - delete: (action) => deleteAction(context, action) - }, - User: { - updateSettings: (settings) => updateUserSettings(context, settings) } }; } @@ -206,13 +154,6 @@ module.exports = (context) => { return { Comment: { create: () => {} - }, - Action: { - create: () => {}, - delete: () => {} - }, - User: { - updateSettings: () => {} } }; }; diff --git a/graph/mutators/index.js b/graph/mutators/index.js new file mode 100644 index 000000000..b799cf83d --- /dev/null +++ b/graph/mutators/index.js @@ -0,0 +1,19 @@ +const _ = require('lodash'); + +const Comment = require('./comment'); +const Action = require('./action'); +const User = require('./user'); + +module.exports = (context) => { + + // We need to return an object to be accessed. + return _.merge(...[ + Comment, + Action, + User, + ].map((mutators) => { + + // Each set of mutators is a function which takes the context. + return mutators(context); + })); +}; diff --git a/graph/mutators/user.js b/graph/mutators/user.js new file mode 100644 index 000000000..b386a5535 --- /dev/null +++ b/graph/mutators/user.js @@ -0,0 +1,31 @@ +const User = require('../../models/user'); + +/** + * Updates a users settings. + * @param {Object} user the user performing the request + * @param {String} bio the new user bio + * @return {Promise} + */ +const updateUserSettings = ({user}, {bio}) => { + return User.updateSettings(user.id, {bio}); +}; + +module.exports = (context) => { + + // TODO: refactor to something that'll return an error in the event an attempt + // is made to mutate state while not logged in. There's got to be a better way + // to do this. + if (context.user) { + return { + User: { + updateSettings: (settings) => updateUserSettings(context, settings) + } + }; + } + + return { + User: { + updateSettings: () => {} + } + }; +}; diff --git a/graph/resolvers/action.js b/graph/resolvers/action.js index 6b42b84dd..f82cefdb1 100644 --- a/graph/resolvers/action.js +++ b/graph/resolvers/action.js @@ -1,18 +1,23 @@ const Action = { action_type({action_type}) { - // TODO: remove once we cast the data model to have uppercase action + // FIXME: remove once we cast the data model to have uppercase action // types. return action_type.toUpperCase(); }, item_type({item_type}) { - // TODO: remove once we cast the data model to have uppercase item + // FIXME: remove once we cast the data model to have uppercase item // types. return item_type.toUpperCase(); }, - user({user_id}, _, {loaders}) { - return loaders.Users.getByID.load(user_id); + + // This will load the user for the specific action. We'll limit this to the + // admin users only. + user({user_id}, _, {loaders, user}) { + if (user.hasRole('admin')) { + return loaders.Users.getByID.load(user_id); + } } }; diff --git a/graph/resolvers/action_summary.js b/graph/resolvers/action_summary.js index 662078500..5a2ef0994 100644 --- a/graph/resolvers/action_summary.js +++ b/graph/resolvers/action_summary.js @@ -1,13 +1,13 @@ const ActionSummary = { action_type({action_type}) { - // TODO: remove once we cast the data model to have uppercase action + // FIXME: remove once we cast the data model to have uppercase action // types. return action_type.toUpperCase(); }, item_type({item_type}) { - // TODO: remove once we cast the data model to have uppercase item + // FIXME: remove once we cast the data model to have uppercase item // types. return item_type.toUpperCase(); } diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 1b1c10046..d7d90918b 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -6,7 +6,7 @@ const Comment = { return loaders.Comments.getByParentID.load(id); }, actions({id}, _, {loaders}) { - return loaders.Actions.getByID.load(id); + return loaders.Actions.getByItemID.load(id); }, status({status}) { @@ -14,6 +14,9 @@ const Comment = { if (status) { return status.toUpperCase(); } + }, + asset({asset_id}, _, {loaders}) { + return loaders.Assets.getByID.load(asset_id); } }; diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 4042ddb21..dd5033d85 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -1,23 +1,46 @@ const RootQuery = { - assets(_, args, {loaders}) { - return loaders.Assets.getAll.load(); + assets(_, args, {loaders, user}) { + if (user.hasRole('admin')) { + return loaders.Assets.getAll.load(); + } }, - asset(_, {id = null, url}, {loaders}) { - if (id) { + asset(_, query, {loaders}) { + if (query.id) { // TODO: we may not always have a comment stream here, therefore, when we // load it, we may also need to create with the url. This may also have to // move the logic over to the mutators function as an upsert operation // possibly. - return loaders.Assets.getByID.load(id); - } else { - return loaders.Assets.getByURL(url); + return loaders.Assets.getByID.load(query.id); } + + return loaders.Assets.getByURL(query.url); }, settings(_, args, {loaders}) { return loaders.Settings.load(); }, + + // This endpoint is used for loading moderation queues, so hide it in the + // event that we aren't an admin. + comments(_, {query}, {loaders, user}) { + if (user == null || !user.hasRole('admin')) { + return null; + } + + if (query.action_type) { + return loaders.Comments.getByActionTypeAndAssetID(query); + } else { + return loaders.Comments.getByStatusAndAssetID(query); + } + }, + + // This returns the current user, ensure that if we aren't logged in, we + // return null. me(_, args, {user}) { + if (user == null) { + return null; + } + return user; } }; diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 98d25f524..3d65416fa 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -1,6 +1,16 @@ const User = { actions({id}, _, {loaders}) { return loaders.Actions.getByID.load(id); + }, + comments({id}, _, {loaders, user}) { + + // If the user is not an admin, only return comment list for the owner of + // the comments. + if (!user.hasRoles('admin') || user.id !== id) { + return null; + } + + return loaders.Comments.getByAuthorID.load(id); } }; diff --git a/graph/typeDefs.js b/graph/typeDefs.js index 9889815d5..75deee196 100644 --- a/graph/typeDefs.js +++ b/graph/typeDefs.js @@ -1,26 +1,66 @@ -// TODO: Adjust `RootQuery.asset(id: ID, url: URL)` to instead be -// `RootQuery.asset(id: ID, url: URL!)` because we'll always need the url, if +// TODO: Adjust `RootQuery.asset(id: ID, url: String)` to instead be +// `RootQuery.asset(id: ID, url: String!)` because we'll always need the url, if // this change is done now everything will likely break on the front end. const typeDefs = [` +interface ActionableItem { + id: ID! +} + type UserSettings { + # bio of the user. bio: String } +input CommentsInput { + # current status of a comment. + status: COMMENT_STATUS + + # asset that a comment is on. + asset_id: ID + + # action type to find comments that have an action with. + action_type: ACTION_TYPE +} + +# Any person who can author comments, create actions, and view comments on a +# stream. type User { id: ID! + + # display name of a user. displayName: String! + + # actions against a specific user. actions: [ActionSummary] + + # settings for a user. settings: UserSettings + + # returns all comments based on a query. + comments(query: CommentsInput): [Comment] } type Comment { id: ID! + + # the actual comment data. body: String! + + # the user who authored the comment. user: User + + # the replies that were made to the comment. replies(limit: Int = 3): [Comment] + + # the actions made against a comment. actions: [ActionSummary] - status: String + + # the asset that a comment was made on. + asset: Asset + + # the current status of a comment. + status: COMMENT_STATUS } enum ITEM_TYPE { @@ -34,22 +74,20 @@ enum ACTION_TYPE { FLAG } -interface ActionInterface { - action_type: ACTION_TYPE! - item_type: ITEM_TYPE! -} - -type Action implements ActionInterface { +type Action { id: ID! - item_id: ID! action_type: ACTION_TYPE! + + item_id: ID! item_type: ITEM_TYPE! + item: ActionableItem + user: User! updated_at: String created_at: String } -type ActionSummary implements ActionInterface { +type ActionSummary { action_type: ACTION_TYPE! item_type: ITEM_TYPE! count: Int @@ -76,10 +114,26 @@ type Asset { closedAt: String } +enum COMMENT_STATUS { + ACCEPTED + REJECTED + PREMOD +} + type RootQuery { + # retrieves site wide settings and defaults. settings: Settings + + # retrieves all assets. assets: [Asset] + + # retrieves a specific asset. asset(id: ID, url: String): Asset + + # retrieves comments based on the input query. + comments(query: CommentsInput): [Comment] + + # retrieves the current logged in user. me: User } diff --git a/models/action.js b/models/action.js index 8e6634072..b73ea77c5 100644 --- a/models/action.js +++ b/models/action.js @@ -166,8 +166,7 @@ ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = ' current_user: '$current_user' } } - ]) - .exec(); + ]); }; /* diff --git a/models/user.js b/models/user.js index a0ca34d7a..5fe84c9eb 100644 --- a/models/user.js +++ b/models/user.js @@ -169,6 +169,13 @@ UserSchema.method('filterForUser', function(user = false) { return this.toJSON(); }); +/** + * Returns true if the user has all the roles specified. + */ +UserSchema.method('hasRoles', function(...roles) { + return roles.every((role) => this.roles.indexOf(role) >= 0); +}); + // Create the User model. const UserModel = mongoose.model('User', UserSchema);