Introduced some changes to the resolvers + mutators

This commit is contained in:
Wyatt Johnson
2017-01-19 18:06:06 -07:00
parent 8bad6f7a73
commit 1db1908778
7 changed files with 160 additions and 65 deletions
+36 -2
View File
@@ -1,5 +1,8 @@
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');
@@ -72,7 +75,7 @@ const singleJoinBy = (ids, key) => (items) => {
* Retrieves assets by an array of ids.
* @param {Array} ids array of ids to lookup
*/
const genAssetByID = (ids) => Asset.find({
const genAssetsByID = (ids) => Asset.find({
id: {
$in: ids
}
@@ -111,6 +114,32 @@ const genCommentsByParentID = (ids) => Comment.find({
}
}).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
@@ -128,7 +157,12 @@ const createLoaders = (context) => ({
getByID: new DataLoader((ids) => User.findByIdArray(ids))
},
Assets: {
getByID: new DataLoader((ids) => genAssetByID(ids)),
// 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())
+86 -36
View File
@@ -13,10 +13,44 @@ const Wordlist = require('../../../services/wordlist');
* @param {String} body body of the comment
* @param {String} asset_id asset for the comment
* @param {String} parent_id optional parent of the comment
* @param {Object} [wordlist={}] results for the wordlist analysis
* @param {String} [status=null] the status of the new comment
* @return {Promise} resolves to the created comment
*/
const createComment = ({user}, {body, asset_id, parent_id = null}, wordlist = {}) => {
const createComment = ({user}, {body, asset_id, parent_id = null}, status = null) => {
return Comment.publicCreate({
body,
asset_id,
parent_id,
status,
author_id: user.id
});
};
/**
* Filters the comment object and outputs wordlist results.
* @param {Object} context graphql context
* @param {String} body body of a comment
* @return {Object} resolves to the wordlist results
*/
const filterNewComment = (context, {body}) => {
// Create a new instance of the Wordlist.
const wl = new Wordlist();
// Load the wordlist and filter the comment content.
return wl.load().then(() => wl.scan('body', body));
};
/**
* This resolves a given comment's status to take into account moderator actions
* are applied.
* @param {Object} context graphql context
* @param {String} body body of the comment
* @param {String} asset_id asset for the comment
* @param {Object} [wordlist={}] the results of the wordlist scan
* @return {Promise} resolves to the comment's status
*/
const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => {
// Decide the status based on whether or not the current asset/settings
// has pre-mod enabled or not. If the comment was rejected based on the
@@ -55,53 +89,71 @@ const createComment = ({user}, {body, asset_id, parent_id = null}, wordlist = {}
});
}
return status.then((status) => Comment.publicCreate({
body,
asset_id,
parent_id,
status,
author_id: user.id
}))
.then((comment) => {
if (wordlist.suspect) {
return Comment
.addAction(comment.id, null, 'flag', {field: 'body', details: 'Matched suspect word filters.'})
.then(() => comment);
}
return comment;
});
return status;
};
/**
* Filters the comment object and outputs wordlist results.
* @param {Object} context graphql context
* @param {String} body body of a comment
* @return {Object} resolves to the wordlist results
* createPublicComment is designed to create a comment from a public source. It
* validates the comment, and performs some automated moderator actions based on
* the settings.
* @param {Object} context the graphql context
* @param {Object} commentInput the new comment to be created
* @return {Promise} resolves to a new comment
*/
const filterNewComment = (context, {body}) => {
const createPublicComment = (context, commentInput) => {
// Create a new instance of the Wordlist.
const wl = new Wordlist();
// First we filter the comment contents to ensure that we note any validation
// issues.
return filterNewComment(context, commentInput)
// Load the wordlist and filter the comment content.
return wl.load().then(() => wl.scan('body', body));
// We then take the wordlist and the comment into consideration when
// considering what status to assign the new comment, and resolve the new
// status to set the comment to.
.then((wordlist) => resolveNewCommentStatus(context, commentInput, wordlist)
// Then we actually create the comment with the new status.
.then((status) => createComment(context, commentInput, status))
.then((comment) => {
// If the comment was flagged as being suspect, we need to add a
// flag to it to indicate that it needs to be looked at.
// Otherwise just return the new comment.
if (wordlist.suspect) {
// 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, {
item_id: comment.id,
item_type: 'comments',
action_type: 'flag',
metadata: {
field: 'body',
details: 'Matched suspect word filters.'
}
}).then(() => comment);
}
// Finally, we return the comment.
return comment;
}));
};
/**
* Creates an action on a item.
* @param {Object} user the user performing the request
* @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}) => {
const createAction = ({user = {}}, {item_id, item_type, action_type, metadata = {}}) => {
return Action.insertUserAction({
item_id,
item_type,
user_id: user.id,
action_type
action_type,
metadata
});
};
@@ -120,9 +172,9 @@ const deleteAction = ({user}, {id}) => {
/**
* Updates a users settings.
* @param {[type]} user [description]
* @param {[type]} bio [description]
* @return {[type]} [description]
* @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});
@@ -136,9 +188,7 @@ module.exports = (context) => {
if (context.user) {
return {
Comment: {
create: (comment) => filterNewComment(context, comment).then((wordlist) => {
return createComment(context, comment, wordlist);
})
create: (comment) => createPublicComment(context, comment)
},
Action: {
create: (action) => createAction(context, action),
+4 -4
View File
@@ -2,8 +2,8 @@ const Action = require('./action');
const ActionSummary = require('./action_summary');
const Asset = require('./asset');
const Comment = require('./comment');
const Mutation = require('./mutation');
const Query = require('./Query');
const RootMutation = require('./root_mutation');
const RootQuery = require('./root_query');
const User = require('./user');
module.exports = {
@@ -11,7 +11,7 @@ module.exports = {
ActionSummary,
Asset,
Comment,
Mutation,
Query,
RootMutation,
RootQuery,
User
};
-16
View File
@@ -1,16 +0,0 @@
const Query = {
assets(_, args, {loaders}) {
return loaders.Assets.getAll.load();
},
asset(_, {id}, {loaders}) {
return loaders.Assets.getByID.load(id);
},
settings(_, args, {loaders}) {
return loaders.Settings.load();
},
me(_, args, {user}) {
return user;
}
};
module.exports = Query;
@@ -1,4 +1,4 @@
const Mutation = {
const RootMutation = {
createComment(_, {asset_id, parent_id, body}, {mutators}) {
return mutators.Comment.create({asset_id, parent_id, body});
},
@@ -13,4 +13,4 @@ const Mutation = {
}
};
module.exports = Mutation;
module.exports = RootMutation;
+25
View File
@@ -0,0 +1,25 @@
const RootQuery = {
assets(_, args, {loaders}) {
return loaders.Assets.getAll.load();
},
asset(_, {id = null, url}, {loaders}) {
if (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);
}
},
settings(_, args, {loaders}) {
return loaders.Settings.load();
},
me(_, args, {user}) {
return user;
}
};
module.exports = RootQuery;
+7 -5
View File
@@ -71,10 +71,12 @@ type Asset {
currentUser: User
}
type Query {
scalar URL
type RootQuery {
settings: Settings
assets: [Asset]
asset(id: ID!): Asset
asset(id: ID, url: URL!): Asset
me: User
}
@@ -94,7 +96,7 @@ input UpdateUserSettingsInput {
bio: String!
}
type Mutation {
type RootMutation {
# creates a comment on the asset.
createComment(asset_id: ID!, parent_id: ID, body: String!): Comment
@@ -109,8 +111,8 @@ type Mutation {
}
schema {
query: Query
mutation: Mutation
query: RootQuery
mutation: RootMutation
}
`];