Implemented sortBy

This commit is contained in:
Wyatt Johnson
2017-08-22 10:11:47 -06:00
parent e0f9bb4165
commit b719168b18
10 changed files with 227 additions and 52 deletions
@@ -56,7 +56,7 @@ const extension = {
fragment CoralEmbedStream_CreateCommentResponse on CreateCommentResponse {
comment {
...CoralEmbedStream_CreateCommentResponse_Comment
replies {
replies(query: {}) {
nodes {
...CoralEmbedStream_CreateCommentResponse_Comment
}
+15 -7
View File
@@ -1,6 +1,7 @@
const loaders = require('./loaders');
const mutators = require('./mutators');
const uuid = require('uuid');
const merge = require('lodash/merge');
const plugins = require('../services/plugins');
const pubsub = require('../services/pubsub');
@@ -17,17 +18,24 @@ const contextPlugins = plugins.get('server', 'context').map(({plugin, context})
});
/**
* This should itterate over the passed in plugins and load them all with the
* This should iterate over the passed in plugins and load them all with the
* current graph context.
* @return {Object} the saturated plugins object
*/
const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduce((acc, plugin) => {
Object.keys(plugin.context).forEach((service) => {
acc[service] = plugin.context[service](context);
});
const decorateContextPlugins = (context, contextPlugins) => {
return acc;
}, {});
// For each of the plugins, we execute with the context to get the context
// based plugin. We then merge that into an object for the plugin. Once the
// plugin is assembled, we merge that object with all the other objects
// provided from the other plugins.
return merge(...contextPlugins.map((plugin) => {
return Object.keys(plugin.context).reduce((services, serviceName) => {
services[serviceName] = plugin.context[serviceName](context);
return services;
}, {});
}));
};
/**
* Stores the request context.
+149 -41
View File
@@ -133,18 +133,156 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
.count();
};
/**
* getStartCursor will retrieve the start cursor based on the sortBy field.
*
* @param {Object} ctx the graph context
* @param {Object} nodes the result set of retrieved comments
* @param {Object} params the params from the client describing the query
*/
const getStartCursor = (ctx, nodes, {cursor, sortBy}) => {
switch (sortBy) {
case 'CREATED_AT':
return nodes.length ? nodes[0].created_at : null;
case 'REPLIES':
// The cursor is the start! This is using numeric pagination.
return cursor != null ? cursor : 0;
}
const SORT_KEY = sortBy.toLowerCase();
if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].startCursor) {
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
}
return ctx.plugins.CommentSort[SORT_KEY].startCursor(ctx, nodes, {cursor});
};
/**
* getEndCursor will fetch the end cursor based on the desired sortBy parameter.
*
* @param {Object} ctx the graph context
* @param {Object} nodes the result set of retrieved comments
* @param {Object} params the params from the client describing the query
*/
const getEndCursor = (ctx, nodes, {cursor, sortBy}) => {
switch (sortBy) {
case 'CREATED_AT':
return nodes.length ? nodes[nodes.length - 1].created_at : null;
case 'REPLIES':
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
}
const SORT_KEY = sortBy.toLowerCase();
if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].endCursor) {
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
}
return ctx.plugins.CommentSort[SORT_KEY].endCursor(ctx, nodes, {cursor});
};
/**
* applySort will add the actual `.sort` and `.skip/.where` clauses to the query
* to apply the desired sort.
*
* @param {Object} ctx the graph context
* @param {Object} query the current mongoose query object
* @param {Object} params the params from the client describing the query
*/
const applySort = (ctx, query, {cursor, sort, sortBy}) => {
switch (sortBy) {
case 'CREATED_AT': {
if (cursor) {
if (sort === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
}
}
return query.sort({created_at: sort === 'DESC' ? -1 : 1});
}
case 'REPLIES': {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({reply_count: sort === 'DESC' ? -1 : 1, created_at: sort === 'DESC' ? -1 : 1});
}
}
const SORT_KEY = sortBy.toLowerCase();
if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].sort) {
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
}
return ctx.plugins.CommentSort[SORT_KEY].sort(ctx, query, {cursor, sort});
};
/**
* executeWithSort will actually retrieve the comments based on the pre-assembled
* query and will compose on top the sort operators necessary to get the desired
* result.
*
* @param {Object} ctx the graph context
* @param {Object} query the current mongoose query object
* @param {Object} params the params from the client describing the query
*/
const executeWithSort = async (ctx, query, {cursor, sort, sortBy, limit}) => {
// Apply the sort to the query.
query = applySort(ctx, query, {cursor, sort, sortBy});
// Apply the limit (if it exists, as it's applied universally).
if (limit) {
query = query.limit(limit + 1);
}
// Fetch the nodes based on the source query.
const nodes = await query.exec();
// The hasNextPage is always handled the same (ask for one more than we need,
// if there is one more, than there is more).
let hasNextPage = false;
if (limit && nodes.length > limit) {
// There was one more than we expected! Set hasNextPage = true and remove
// the last item from the array that we requested.
hasNextPage = true;
nodes.splice(limit, 1);
}
// Use the generator functions below to extract the cursor details based on
// the current sortBy parameter.
return {
startCursor: getStartCursor(ctx, nodes, {cursor, sort, sortBy, limit}),
endCursor: getEndCursor(ctx, nodes, {cursor, sort, sortBy, limit}),
hasNextPage,
nodes,
};
};
/**
* Retrieves comments based on the passed in query that is filtered by the
* current used passed in via the context.
*
* @param {Object} context graph context
* @param {Object} query query terms to apply to the comments query
*/
const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, excludeIgnored, tags, action_type}) => {
const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, sortBy, excludeIgnored, tags, action_type}) => {
let comments = CommentModel.find();
// Only administrators can search for comments with statuses that are not
// `null`, or `'ACCEPTED'`.
if (user != null && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses && statuses.length > 0) {
if (ctx.user != null && ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses && statuses.length > 0) {
comments = comments.where({
status: {
$in: statuses
@@ -158,7 +296,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
});
}
if (user != null && user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
comments = comments.where({
action_counts: {
[action_type.toLowerCase()]: {
@@ -185,7 +323,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
}
// Only let an admin request any user or the current user request themself.
if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === author_id) && author_id != null) {
if (ctx.user && (ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) && author_id != null) {
comments = comments.where({author_id});
}
@@ -199,50 +337,19 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
comments = comments.where({parent_id});
}
if (excludeIgnored && user && user.ignoresUsers && user.ignoresUsers.length > 0) {
if (excludeIgnored && ctx.user && ctx.user.ignoresUsers && ctx.user.ignoresUsers.length > 0) {
comments = comments.where({
author_id: {$nin: user.ignoresUsers}
author_id: {$nin: ctx.user.ignoresUsers}
});
}
if (cursor) {
if (sort === 'DESC') {
comments = comments.where({
created_at: {
$lt: cursor
}
});
} else {
comments = comments.where({
created_at: {
$gt: cursor
}
});
}
}
let query = comments
.sort({created_at: sort === 'DESC' ? -1 : 1});
if (limit) {
query = query.limit(limit + 1);
}
return query.then((nodes) => {
let hasNextPage = false;
if (limit && nodes.length > limit) {
hasNextPage = true;
nodes.splice(limit, 1);
}
return Promise.resolve({
startCursor: nodes.length ? nodes[0].created_at : null,
endCursor: nodes.length ? nodes[nodes.length - 1].created_at : null,
hasNextPage,
nodes,
});
});
return executeWithSort(ctx, comments, {cursor, sort, sortBy, limit});
};
/**
* getComments returns the comments by the id's. Only admins can see non-public comments.
* getComments 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
@@ -270,6 +377,7 @@ const getComments = ({user}, 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
*/
+2 -1
View File
@@ -17,10 +17,11 @@ const Asset = {
return comments.nodes[0];
},
comments({id}, {query: {sort, limit, excludeIgnored, tags}, deep}, {loaders: {Comments}}) {
comments({id}, {query: {sort, sortBy, limit, excludeIgnored, tags}, deep}, {loaders: {Comments}}) {
return Comments.getByQuery({
asset_id: id,
sort,
sortBy,
limit,
parent_id: deep ? undefined : null,
tags,
+2 -1
View File
@@ -11,7 +11,7 @@ const Comment = {
user({author_id}, _, {loaders: {Users}}) {
return Users.getByID.load(author_id);
},
replies({id, asset_id, reply_count}, {query: {sort, limit, excludeIgnored}}, {loaders: {Comments}}) {
replies({id, asset_id, reply_count}, {query: {sort, sortBy, limit, excludeIgnored}}, {loaders: {Comments}}) {
// Don't bother looking up replies if there aren't any there!
if (reply_count === 0) {
@@ -25,6 +25,7 @@ const Comment = {
asset_id,
parent_id: id,
sort,
sortBy,
limit,
excludeIgnored,
});
+20
View File
@@ -249,6 +249,10 @@ input CommentsQuery {
# Sort the results by from largest first.
sort: SORT_ORDER = DESC
# The order to sort the comments by, sorting by default the created at
# timestamp.
sortBy: SORT_COMMENTS_BY = CREATED_AT
# Filter by a specific tag name.
tags: [String!]
@@ -261,6 +265,10 @@ input RepliesQuery {
# Sort the results by from smallest first.
sort: SORT_ORDER = ASC
# The order to sort the comments by, sorting by default the created at
# timestamp.
sortBy: SORT_COMMENTS_BY = CREATED_AT
# Limit the number of results to be returned.
limit: Int = 3
@@ -660,6 +668,18 @@ enum SORT_ORDER {
ASC
}
# SORT_COMMENTS_BY selects the means for which comments are ordered when
# sorting.
enum SORT_COMMENTS_BY {
# Comments will be sorted by their created at date.
CREATED_AT
# Comments will be sorted by their immediate reply count (replies to the comment
# in question only, not including descendants).
REPLIES
}
# All queries that can be executed.
enum USER_STATUS {
ACTIVE
+1
View File
@@ -138,6 +138,7 @@
"passport": "^0.4.0",
"passport-jwt": "^3.0.0",
"passport-local": "^1.0.0",
"pluralize": "^7.0.0",
"postcss-loader": "^1.3.3",
"postcss-modules": "^0.5.2",
"postcss-smart-import": "^0.5.1",
@@ -1,12 +1,15 @@
const wrapResponse = require('../../../graph/helpers/response');
const {SEARCH_OTHER_USERS} = require('../../../perms/constants');
const errors = require('../../../errors');
const pluralize = require('pluralize');
function getReactionConfig(reaction) {
reaction = reaction.toLowerCase();
const reactionPlural = pluralize(reaction);
const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1);
const REACTION = reaction.toUpperCase();
const REACTION_PLURAL = reactionPlural.toUpperCase();
const typeDefs = `
enum ACTION_TYPE {
@@ -26,6 +29,13 @@ function getReactionConfig(reaction) {
item_id: ID!
}
enum SORT_COMMENTS_BY {
# Comments will be sorted by their count of ${reactionPlural}
# on the comment.
${REACTION_PLURAL}
}
input Delete${Reaction}ActionInput {
# The item's id for which we are deleting a ${reaction}.
@@ -107,6 +117,27 @@ function getReactionConfig(reaction) {
return {
typeDefs,
context: {
CommentSort: () => ({
[reactionPlural]: {
startCursor(ctx, nodes, {cursor}) {
// The cursor is the start! This is using numeric pagination.
return cursor != null ? cursor : 0;
},
endCursor(ctx, nodes, {cursor}) {
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
},
sort(ctx, query, {cursor, sort}) {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({[`action_counts.${reaction}`]: sort === 'DESC' ? -1 : 1, created_at: sort === 'DESC' ? -1 : 1});
},
},
}),
},
resolvers: {
Subscription: {
[`${reaction}ActionCreated`]: ({action}) => {
+2 -1
View File
@@ -61,7 +61,8 @@ if (WEBPACK) {
debug('connection established');
})
.catch((err) => {
throw err;
console.error(err);
process.exit(1);
});
}
+4
View File
@@ -5648,6 +5648,10 @@ pluralize@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
pluralize@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
pop-iterate@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pop-iterate/-/pop-iterate-1.0.1.tgz#ceacfdab4abf353d7a0f2aaa2c1fc7b3f9413ba3"