mirror of
https://github.com/wassname/talk.git
synced 2026-08-07 11:29:44 +08:00
Merge branch 'master' into story-138187767-mod-flag-names
This commit is contained in:
@@ -39,7 +39,7 @@ const getCountsByAssetID = (context, asset_ids) => {
|
||||
/**
|
||||
* Returns the comment count for all comments that are public based on their
|
||||
* parent ids.
|
||||
* @param {Object} context graph context
|
||||
*
|
||||
* @param {Array<String>} parent_ids the ids of parents for which there are
|
||||
* comments that we want to get
|
||||
*/
|
||||
@@ -270,6 +270,33 @@ const genRecentComments = (_, ids) => {
|
||||
.then(util.arrayJoinBy(ids, 'asset_id'));
|
||||
};
|
||||
|
||||
/**
|
||||
* genComments returns the comments by the id's. Only admins can see non-public comments.
|
||||
* @param {Object} context graph context
|
||||
* @param {Array<String>} ids the comment id's to fetch
|
||||
* @return {Promise} resolves to the comments
|
||||
*/
|
||||
const genComments = ({user}, ids) => {
|
||||
let comments;
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
comments = CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
});
|
||||
} else {
|
||||
comments = CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
},
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
}
|
||||
});
|
||||
}
|
||||
return comments.then(util.singleJoinBy(ids, 'id'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
@@ -277,6 +304,7 @@ const genRecentComments = (_, ids) => {
|
||||
*/
|
||||
module.exports = (context) => ({
|
||||
Comments: {
|
||||
get: new DataLoader((ids) => genComments(context, ids)),
|
||||
getByQuery: (query) => getCommentsByQuery(context, query),
|
||||
getCountByQuery: (query) => getCommentCountByQuery(context, query),
|
||||
countByAssetID: new util.SharedCacheDataLoader('Comments.countByAssetID', 3600, (ids) => getCountsByAssetID(context, ids)),
|
||||
|
||||
+96
-34
@@ -2,10 +2,12 @@ const _ = require('lodash');
|
||||
const DataLoader = require('dataloader');
|
||||
const {objectCacheKeyFn} = require('./util');
|
||||
|
||||
const CommentModel = require('../../models/comment');
|
||||
const ActionModel = require('../../models/action');
|
||||
|
||||
const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
|
||||
/**
|
||||
* Returns a list of assets with action metadata included on the models.
|
||||
*/
|
||||
const getAssetMetrics = ({loaders: {Metrics, Assets, Comments}}, {from, to, sort, limit}) => {
|
||||
|
||||
let commentMetrics = {};
|
||||
let assetMetrics = [];
|
||||
@@ -14,6 +16,10 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
|
||||
.then((actionSummaries) => {
|
||||
|
||||
commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
|
||||
if (action_type !== sort) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!(item_id in acc)) {
|
||||
acc[item_id] = [];
|
||||
}
|
||||
@@ -24,10 +30,10 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
|
||||
}, {});
|
||||
|
||||
// Collect just the comment id's.
|
||||
let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id));
|
||||
let commentIDs = Object.keys(commentMetrics);
|
||||
|
||||
// Find those comments.
|
||||
return Metrics.getSpecificComments.loadMany(commentIDs);
|
||||
return Comments.get.loadMany(commentIDs);
|
||||
})
|
||||
.then((comments) => {
|
||||
|
||||
@@ -44,29 +50,24 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
|
||||
}));
|
||||
|
||||
return {action_summaries, id: asset_id};
|
||||
});
|
||||
})
|
||||
|
||||
.filter((asset) => {
|
||||
let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sort));
|
||||
if (contextActionSummary === null || contextActionSummary.actionCount === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
|
||||
// Sort these metrics by the predefined sort order. This will ensure that
|
||||
// if the action summary does not exist on the object, that it is less
|
||||
// prefered over the one that does have it.
|
||||
assetMetrics.sort((a, b) => {
|
||||
.sort((a, b) => {
|
||||
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort));
|
||||
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort));
|
||||
|
||||
// If either a or b don't have this action type, then one of them will
|
||||
// automatically win.
|
||||
if (aActionSummary == null || bActionSummary == null) {
|
||||
if (bActionSummary != null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aActionSummary != null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Both of them had an actionCount, hence we can determine that we could
|
||||
// compare the actual values directly.
|
||||
return bActionSummary.actionCount - aActionSummary.actionCount;
|
||||
@@ -99,6 +100,75 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of comments that are retrieved based on most activity within
|
||||
* the indicated time range.
|
||||
*/
|
||||
const getCommentMetrics = ({loaders: {Metrics, Comments}}, {from, to, sort, limit}) => {
|
||||
|
||||
let commentActionSummaries = {};
|
||||
|
||||
return Metrics.getRecentActions.load({from, to})
|
||||
.then((actionSummaries) => {
|
||||
|
||||
actionSummaries.sort((a, b) => {
|
||||
let aActionSummary = a.action_type === sort ? a : null;
|
||||
let bActionSummary = b.action_type === sort ? b : null;
|
||||
|
||||
// If either a or b don't have this action type, then one of them will
|
||||
// automatically win.
|
||||
if (aActionSummary == null || bActionSummary == null) {
|
||||
if (bActionSummary != null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aActionSummary != null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Both of them had an actionCount, hence we can determine that we could
|
||||
// compare the actual values directly.
|
||||
return bActionSummary.count - aActionSummary.count;
|
||||
});
|
||||
|
||||
commentActionSummaries = _.groupBy(actionSummaries, 'item_id');
|
||||
|
||||
// Grab the comment id's for comment where they have at least one of the
|
||||
// actions being sorted by.
|
||||
let commentIDs = Object.keys(commentActionSummaries).filter((item_id) => {
|
||||
let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sort);
|
||||
if (contextActionSummary == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Only keep the top `limit`.
|
||||
commentIDs = commentIDs.slice(0, limit);
|
||||
|
||||
// If there are no comment's to get, then just continue with an empty
|
||||
// array.
|
||||
if (commentIDs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Find those comments, this is the final stage, so let's get all the
|
||||
// fields.
|
||||
return Comments.get.loadMany(commentIDs);
|
||||
})
|
||||
.then((comments) => comments.map((comment) => {
|
||||
|
||||
// Add in the action summaries genrerated.
|
||||
comment.action_summaries = commentActionSummaries[comment.id];
|
||||
|
||||
return comment;
|
||||
}));
|
||||
};
|
||||
|
||||
const getRecentActions = (context, {from, to}) => {
|
||||
return ActionModel.aggregate([
|
||||
|
||||
@@ -131,25 +201,17 @@ const getRecentActions = (context, {from, to}) => {
|
||||
]);
|
||||
};
|
||||
|
||||
const getSpecificComments = (context, ids) => {
|
||||
return CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
})
|
||||
.select({
|
||||
id: 1,
|
||||
asset_id: 1
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (context) => ({
|
||||
Metrics: {
|
||||
getSpecificComments: new DataLoader((ids) => getSpecificComments(context, ids)),
|
||||
getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), {
|
||||
batch: false,
|
||||
cacheKeyFn: objectCacheKeyFn('from', 'to')
|
||||
}),
|
||||
get: ({from, to, sort, limit}) => getMetrics(context, {from, to, sort, limit})
|
||||
Assets: {
|
||||
get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit})
|
||||
},
|
||||
Comments: {
|
||||
get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const errors = require('../../errors');
|
||||
|
||||
const AssetsService = require('../../services/assets');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const CommentsService = require('../../services/comments');
|
||||
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
@@ -146,10 +147,11 @@ 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 context.mutators.Action.create({
|
||||
return ActionsService.insertUserAction({
|
||||
item_id: comment.id,
|
||||
item_type: 'COMMENTS',
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'Matched suspect word filter',
|
||||
metadata: {}
|
||||
})
|
||||
@@ -187,11 +189,31 @@ const setCommentStatus = ({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
|
||||
*/
|
||||
const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => {
|
||||
return CommentsService.addTag(id, tag, user.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a tag from a Comment
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} tag name of the tag
|
||||
*/
|
||||
const removeCommentTag = ({user, loaders: {Comments}}, {id, tag}) => {
|
||||
return CommentsService.removeTag(id, tag);
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
let mutators = {
|
||||
Comment: {
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
addCommentTag: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
removeCommentTag: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,5 +225,13 @@ 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;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
const Comment = {
|
||||
parent({parent_id}, _, {loaders: {Comments}}) {
|
||||
if (parent_id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Comments.get.load(parent_id);
|
||||
},
|
||||
user({author_id}, _, {loaders: {Users}}) {
|
||||
return Users.getByID.load(author_id);
|
||||
},
|
||||
@@ -25,7 +32,11 @@ const Comment = {
|
||||
|
||||
return null;
|
||||
},
|
||||
action_summaries({id}, _, {loaders: {Actions}}) {
|
||||
action_summaries({id, action_summaries}, _, {loaders: {Actions}}) {
|
||||
if (action_summaries) {
|
||||
return action_summaries;
|
||||
}
|
||||
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
},
|
||||
asset({asset_id}, _, {loaders: {Assets}}) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const {Error: {ValidationError}} = require('mongoose');
|
||||
const errors = require('../../errors');
|
||||
const CommentsService = require('../../services/comments');
|
||||
|
||||
/**
|
||||
* Wraps up a promise to return an object with the resolution of the promise
|
||||
@@ -51,7 +52,13 @@ 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)));
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = RootMutation;
|
||||
|
||||
@@ -38,7 +38,9 @@ const RootQuery = {
|
||||
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
|
||||
comment(_, {id}, {loaders: {Comments}}) {
|
||||
return Comments.get.load(id);
|
||||
},
|
||||
commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
return null;
|
||||
@@ -56,12 +58,20 @@ const RootQuery = {
|
||||
return Comments.getCountByQuery({statuses, asset_id, parent_id});
|
||||
},
|
||||
|
||||
metrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics}}) {
|
||||
assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Metrics.get({from, to, sort, limit});
|
||||
return Assets.get({from, to, sort, limit});
|
||||
},
|
||||
|
||||
commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Comments.get({from, to, sort, limit});
|
||||
},
|
||||
|
||||
// This returns the current user, ensure that if we aren't logged in, we
|
||||
|
||||
@@ -15,7 +15,7 @@ const User = {
|
||||
// If the user is not an admin, only return comment list for the owner of
|
||||
// the comments.
|
||||
if (user && (user.hasRoles('ADMIN') || user.id === id)) {
|
||||
return Comments.getByQuery({author_id: id});
|
||||
return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'});
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
+35
-5
@@ -47,7 +47,7 @@ type User {
|
||||
|
||||
# returns all users based on a query.
|
||||
users(query: UsersQuery): [User]
|
||||
|
||||
|
||||
# returns user status
|
||||
status: USER_STATUS
|
||||
}
|
||||
@@ -166,6 +166,9 @@ input CommentCountQuery {
|
||||
# Comment is the base representation of user interaction in Talk.
|
||||
type Comment {
|
||||
|
||||
# The parent of the comment (if there is one).
|
||||
parent: Comment
|
||||
|
||||
# The ID of the comment.
|
||||
id: ID!
|
||||
|
||||
@@ -494,6 +497,9 @@ type RootQuery {
|
||||
# Site wide settings and defaults.
|
||||
settings: Settings
|
||||
|
||||
# Finds a specific comment based on it's id.
|
||||
comment(id: ID!): Comment
|
||||
|
||||
# All assets. Requires the `ADMIN` role.
|
||||
assets: [Asset]
|
||||
|
||||
@@ -511,12 +517,16 @@ type RootQuery {
|
||||
# role.
|
||||
me: User
|
||||
|
||||
# Metrics related to user actions are saturated into the assets returned. The
|
||||
# sort will affect if it will allow
|
||||
metrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset]
|
||||
|
||||
# Users returned based on a query.
|
||||
users(query: UsersQuery): [User]
|
||||
|
||||
# Asset metrics related to user actions are saturated into the assets
|
||||
# returned.
|
||||
assetMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset!]
|
||||
|
||||
# Comment metrics related to user actions are saturated into the comments
|
||||
# returned.
|
||||
commentMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Comment!]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -659,6 +669,20 @@ type SetCommentStatusResponse implements Response {
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# 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]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
type RootMutation {
|
||||
|
||||
@@ -685,6 +709,12 @@ type RootMutation {
|
||||
|
||||
# Sets Comment status. Requires the `ADMIN` role.
|
||||
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
|
||||
|
||||
# Add tag to comment.
|
||||
addCommentTag(id: ID!, tag: String!): AddCommentTagResponse
|
||||
|
||||
# Remove tag from comment.
|
||||
removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
Reference in New Issue
Block a user