replaced eslint:recommended with prettier

This commit is contained in:
Wyatt Johnson
2018-01-11 20:00:34 -07:00
parent d56c19016a
commit 0abc2ca243
649 changed files with 16235 additions and 13008 deletions
+8 -5
View File
@@ -79,9 +79,12 @@ const connectors = {
},
};
module.exports = Plugins.get('server', 'connectors').reduce((defaultConnectors, {plugin, connectors: pluginConnectors}) => {
debug(`adding plugin '${plugin.name}'`);
module.exports = Plugins.get('server', 'connectors').reduce(
(defaultConnectors, { plugin, connectors: pluginConnectors }) => {
debug(`adding plugin '${plugin.name}'`);
// Merge in the plugin connectors.
return merge(defaultConnectors, pluginConnectors);
}, connectors);
// Merge in the plugin connectors.
return merge(defaultConnectors, pluginConnectors);
},
connectors
);
+17 -15
View File
@@ -13,10 +13,12 @@ const debug = require('debug')('talk:graph:context');
* level functions all need the context reference.
* @type {Array}
*/
const contextPlugins = plugins.get('server', 'context').map(({plugin, context}) => {
debug(`added plugin '${plugin.name}'`);
return {context};
});
const contextPlugins = plugins
.get('server', 'context')
.map(({ plugin, context }) => {
debug(`added plugin '${plugin.name}'`);
return { context };
});
/**
* This should iterate over the passed in plugins and load them all with the
@@ -24,18 +26,19 @@ const contextPlugins = plugins.get('server', 'context').map(({plugin, context})
* @return {Object} the saturated plugins object
*/
const decorateContextPlugins = (context, contextPlugins) => {
// 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 merge(
...contextPlugins.map(plugin => {
return Object.keys(plugin.context).reduce((services, serviceName) => {
services[serviceName] = plugin.context[serviceName](context);
return services;
}, {});
}));
return services;
}, {});
})
);
};
/**
@@ -43,7 +46,6 @@ const decorateContextPlugins = (context, contextPlugins) => {
*/
class Context {
constructor(parent) {
// Generate a new context id for the request if the parent doesn't provide
// one.
this.id = parent.id || uuid.v4();
@@ -76,12 +78,12 @@ class Context {
*
*/
static forSystem() {
const {models: {User}} = connectors;
const { models: { User } } = connectors;
// Create the system user.
const user = new User({system: true});
const user = new User({ system: true });
return new Context({user});
return new Context({ user });
}
}
+7 -11
View File
@@ -1,23 +1,21 @@
const {forEachField} = require('./utils');
const {maskErrors} = require('graphql-errors');
const { forEachField } = require('./utils');
const { maskErrors } = require('graphql-errors');
const errors = require('../errors');
const {Error: {ValidationError}} = require('mongoose');
const { Error: { ValidationError } } = require('mongoose');
// If an APIError happens in a mutation, then respond with `{errors: Array}`
// according to the schema.
const decorateWithMutationErrorHandler = (field) => {
const decorateWithMutationErrorHandler = field => {
const fieldResolver = field.resolve;
field.resolve = async (obj, args, ctx, info) => {
try {
return await fieldResolver(obj, args, ctx, info);
}
catch(err) {
} catch (err) {
if (err instanceof errors.APIError) {
return {
errors: [err]
errors: [err],
};
} else if (err instanceof ValidationError) {
// TODO: wrap this with one of our internal errors.
throw err;
}
@@ -32,9 +30,8 @@ const decorateWithMutationErrorHandler = (field) => {
* @param {GraphQLSchema} schema the schema to decorate
* @return {void}
*/
const decorateWithErrorHandler = (schema) => {
const decorateWithErrorHandler = schema => {
forEachField(schema, (field, typeName) => {
// Handle mutation errors.
if (typeName === 'RootMutation') {
decorateWithMutationErrorHandler(field);
@@ -42,7 +39,6 @@ const decorateWithErrorHandler = (schema) => {
// If we are in production mode, don't show server errors to the front end.
if (process.env.NODE_ENV === 'production') {
// Mask errors that are thrown if we are in a production environment.
maskErrors(field);
}
+143 -111
View File
@@ -1,4 +1,4 @@
const {forEachField} = require('./utils');
const { forEachField } = require('./utils');
const debug = require('debug')('talk:graph:schema');
const Joi = require('joi');
@@ -11,8 +11,7 @@ const Joi = require('joi');
* and returns it as the result, or if it's a function, returns the result
* of calling that function.
*/
const defaultResolveFn = (source, args, context, {fieldName}) => {
const defaultResolveFn = (source, args, context, { fieldName }) => {
// ensure source is a value for which property access is acceptable.
if (typeof source === 'object' || typeof source === 'function') {
const property = source[fieldName];
@@ -28,7 +27,6 @@ const defaultResolveFn = (source, args, context, {fieldName}) => {
* default type in the form of `Default${typeName}`.
*/
const decorateResolveFunction = (field, typeName, fieldName, post) => {
// Cache the original resolverType function.
let resolveType = field.resolveType;
@@ -53,7 +51,6 @@ const decorateResolveFunction = (field, typeName, fieldName, post) => {
// This only needs to do something if post hooks are defined.
if (post.length === 0) {
// Set the default on the resolveType function.
field.resolveType = defaultResolveFn;
@@ -61,7 +58,11 @@ const decorateResolveFunction = (field, typeName, fieldName, post) => {
}
// Ensure it matches the format we expect.
Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`);
Joi.assert(
post,
Joi.array().items(Joi.func().maxArity(3)),
`invalid post hooks were found for ${typeName}.${fieldName}`
);
// Return the function to handle the resolveType hooks.
field.resolveType = (obj, context, info) => {
@@ -70,7 +71,11 @@ const decorateResolveFunction = (field, typeName, fieldName, post) => {
// Only if a previous resolver was unable to resolve the field type do we
// progress to the hooks (in order!) to resolve the field name until we
// have resolved it.
if (typeof type !== 'undefined' && type != null && type !== defaultResolveType) {
if (
typeof type !== 'undefined' &&
type != null &&
type !== defaultResolveType
) {
return type;
}
@@ -95,124 +100,151 @@ const decorateResolveFunction = (field, typeName, fieldName, post) => {
* @param {Array} hooks hooks to apply to the schema
* @return {void}
*/
const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName, isResolveType = false) => {
const decorateWithHooks = (schema, hooks) =>
forEachField(
schema,
(field, typeName, fieldName, isResolveType = false) => {
// Pull out the pre/post hooks from the available hooks.
const { pre, post } = hooks
// Pull out the pre/post hooks from the available hooks.
const {
pre,
post
} = hooks
// Only grab hooks that are associated with thie field and typeName.
.filter(
({ hooks }) => typeName in hooks && fieldName in hooks[typeName]
)
// Only grab hooks that are associated with thie field and typeName.
.filter(({hooks}) => (typeName in hooks) && (fieldName in hooks[typeName]))
// Grab the hooks we need.
.map(({ plugin, hooks }) => ({
plugin,
hooks: hooks[typeName][fieldName],
}))
// Grab the hooks we need.
.map(({plugin, hooks}) => ({plugin, hooks: hooks[typeName][fieldName]}))
// Combine the pre/post hooks from each plugin into an array we can
// execute.
.reduce(
(acc, { plugin, hooks }) => {
// Itterate over the hooks on the fields and look at it with a switch
// block to check for misconfigured plugins.
Object.keys(hooks).forEach(hook => {
switch (hook) {
case 'pre':
Joi.assert(hooks.pre, Joi.func().maxArity(4));
// Combine the pre/post hooks from each plugin into an array we can
// execute.
.reduce((acc, {plugin, hooks}) => {
debug(
`adding pre hook to resolver ${typeName}.${fieldName} from plugin '${
plugin.name
}'`
);
// Itterate over the hooks on the fields and look at it with a switch
// block to check for misconfigured plugins.
Object.keys(hooks).forEach((hook) => {
switch (hook) {
case 'pre':
Joi.assert(hooks.pre, Joi.func().maxArity(4));
if (typeof hooks.pre !== 'function') {
throw new Error(
`expected ${hook} hook on resolver ${typeName}.${fieldName} from plugin '${
plugin.name
}' to be a function, it was a '${typeof hooks[hook]}'`
);
}
debug(`adding pre hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
acc.pre.push(hooks.pre);
break;
case 'post':
Joi.assert(hooks.pre, Joi.func().maxArity(5));
if (typeof hooks.pre !== 'function') {
throw new Error(`expected ${hook} hook on resolver ${typeName}.${fieldName} from plugin '${plugin.name}' to be a function, it was a '${typeof hooks[hook]}'`);
debug(
`adding post hook to resolver ${typeName}.${fieldName} from plugin '${
plugin.name
}'`
);
if (typeof hooks.post !== 'function') {
throw new Error(
`expected ${hook} hook on resolver ${typeName}.${fieldName} from plugin '${
plugin.name
}' to be a function, it was a '${typeof hooks[hook]}'`
);
}
acc.post.unshift(hooks.post);
break;
default:
throw new Error(
`invalid hook '${hook}' on resolver ${typeName}.${fieldName} from plugin '${
plugin.name
}'`
);
}
});
return acc;
},
{
pre: [],
post: [],
}
);
acc.pre.push(hooks.pre);
break;
case 'post':
Joi.assert(hooks.pre, Joi.func().maxArity(5));
debug(`adding post hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.post !== 'function') {
throw new Error(`expected ${hook} hook on resolver ${typeName}.${fieldName} from plugin '${plugin.name}' to be a function, it was a '${typeof hooks[hook]}'`);
}
acc.post.unshift(hooks.post);
break;
default:
throw new Error(`invalid hook '${hook}' on resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
// If this is a resolve type, we need to do some specific things to handle
// this type of field.
if (isResolveType) {
// Warn if we have any pre hooks.
if (pre.length !== 0) {
throw new Error(
`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`
);
}
});
return acc;
}, {
pre: [],
post: []
});
// If this is a resolve type, we need to do some specific things to handle
// this type of field.
if (isResolveType) {
// Warn if we have any pre hooks.
if (pre.length !== 0) {
throw new Error(`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`);
}
// Decorate the resolve function on the field with the new resolveType func.
decorateResolveFunction(field, typeName, fieldName, post);
return;
}
// If we have no hooks to add here, don't try to modify anything.
if (pre.length === 0 && post.length === 0) {
return;
}
// Cache the original resolve function, this emulates the beheviour found in
// graphql-tools: https://github.com/apollographql/graphql-tools/blob/6e9cc124b10d673448386041e6c3d058bc205a02/src/schemaGenerator.ts#L423-L425
let resolve = field.resolve;
if (typeof resolve === 'undefined') {
resolve = defaultResolveFn;
}
// Apply our async resolve function which will fire all pre functions (and
// wait until they resolve) followed by waiting for the response and then
// firing their post hooks. Lastly, we respond with the result of the
// original resolver.
field.resolve = async (obj, args, context, info) => {
// Issue all pre hooks before we resolve the field.
await Promise.all(pre.map((pre) => pre(obj, args, context, info)));
// Resolve the field.
let result = await resolve(obj, args, context, info);
// Insure all post hooks after we've resolved the field with the result
// passed in as the fifth argument.
return await post.reduce(async (result, post) => {
// Wait for the accumulator to resolve before we continue.
result = await result;
// Check to see if this post function accepts a result, if it does, we
// expect that it modifies the result, otherwise, just fire the post hook,
// wait till it's done, then move onto the next hook.
if (post.length === 5) {
return await post(obj, args, context, info, result);
// Decorate the resolve function on the field with the new resolveType func.
decorateResolveFunction(field, typeName, fieldName, post);
return;
}
// Wait for the post hook to finish.
await post(obj, args, context, info);
// If we have no hooks to add here, don't try to modify anything.
if (pre.length === 0 && post.length === 0) {
return;
}
// Return the result, which we already awaited for before.
return result;
}, result);
};
}, {
includeResolveType: true,
});
// Cache the original resolve function, this emulates the beheviour found in
// graphql-tools: https://github.com/apollographql/graphql-tools/blob/6e9cc124b10d673448386041e6c3d058bc205a02/src/schemaGenerator.ts#L423-L425
let resolve = field.resolve;
if (typeof resolve === 'undefined') {
resolve = defaultResolveFn;
}
// Apply our async resolve function which will fire all pre functions (and
// wait until they resolve) followed by waiting for the response and then
// firing their post hooks. Lastly, we respond with the result of the
// original resolver.
field.resolve = async (obj, args, context, info) => {
// Issue all pre hooks before we resolve the field.
await Promise.all(pre.map(pre => pre(obj, args, context, info)));
// Resolve the field.
let result = await resolve(obj, args, context, info);
// Insure all post hooks after we've resolved the field with the result
// passed in as the fifth argument.
return await post.reduce(async (result, post) => {
// Wait for the accumulator to resolve before we continue.
result = await result;
// Check to see if this post function accepts a result, if it does, we
// expect that it modifies the result, otherwise, just fire the post hook,
// wait till it's done, then move onto the next hook.
if (post.length === 5) {
return await post(obj, args, context, info, result);
}
// Wait for the post hook to finish.
await post(obj, args, context, info);
// Return the result, which we already awaited for before.
return result;
}, result);
};
},
{
includeResolveType: true,
}
);
module.exports = {
decorateWithHooks
decorateWithHooks,
};
+5 -6
View File
@@ -1,11 +1,10 @@
const schema = require('./schema');
const Context = require('./context');
const {createSubscriptionManager} = require('./subscriptions');
const {ENABLE_TRACING} = require('../config');
const { createSubscriptionManager } = require('./subscriptions');
const { ENABLE_TRACING } = require('../config');
module.exports = {
createGraphOptions: (req) => ({
createGraphOptions: req => ({
// Schema is created already, so just include it.
schema,
@@ -15,7 +14,7 @@ module.exports = {
// Tracing request options, needed for Apollo Engine.
tracing: ENABLE_TRACING,
cacheControl: ENABLE_TRACING
cacheControl: ENABLE_TRACING,
}),
createSubscriptionManager
createSubscriptionManager,
};
+16 -13
View File
@@ -9,9 +9,9 @@ const ActionModel = require('../../models/action');
* Gets actions based on their item id's.
*/
const genActionsByItemID = (_, item_ids) => {
return ActionsService
.findByItemIdArray(item_ids)
.then(util.arrayJoinBy(item_ids, 'item_id'));
return ActionsService.findByItemIdArray(item_ids).then(
util.arrayJoinBy(item_ids, 'item_id')
);
};
/**
@@ -20,10 +20,10 @@ const genActionsByItemID = (_, item_ids) => {
* @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 ActionsService
.getActionSummaries(item_ids, user.id)
.then(util.arrayJoinBy(item_ids, 'item_id'));
const genActionSummariessByItemID = ({ user = {} }, item_ids) => {
return ActionsService.getActionSummaries(item_ids, user.id).then(
util.arrayJoinBy(item_ids, 'item_id')
);
};
/**
@@ -34,7 +34,7 @@ const genActionSummariessByItemID = ({user = {}}, item_ids) => {
* @return {Promise} resolves to distinct items actions
*/
const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
return ActionModel.distinct('item_id', {action_type, item_type});
return ActionModel.distinct('item_id', { action_type, item_type });
};
/**
@@ -42,10 +42,13 @@ const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
module.exports = context => ({
Actions: {
getByID: new DataLoader((ids) => genActionsByItemID(context, ids)),
getSummariesByItemID: new DataLoader((ids) => genActionSummariessByItemID(context, ids)),
getByTypes: ({action_type, item_type}) => getItemIdsByActionTypeAndItemType(context, action_type, item_type)
}
getByID: new DataLoader(ids => genActionsByItemID(context, ids)),
getSummariesByItemID: new DataLoader(ids =>
genActionSummariessByItemID(context, ids)
),
getByTypes: ({ action_type, item_type }) =>
getItemIdsByActionTypeAndItemType(context, action_type, item_type),
},
});
+49 -54
View File
@@ -1,15 +1,18 @@
const DataLoader = require('dataloader');
const {URL} = require('url');
const {singleJoinBy, SingletonResolver} = require('./util');
const { URL } = require('url');
const { singleJoinBy, SingletonResolver } = require('./util');
const genAssetsByID = ({connectors: {models: {Asset}}}, ids) => Asset.find({
id: {
$in: ids
}
}).then(singleJoinBy(ids, 'id'));
const getAssetsByQuery = async ({connectors: {services: {Assets}}}, query) => {
const genAssetsByID = ({ connectors: { models: { Asset } } }, ids) =>
Asset.find({
id: {
$in: ids,
},
}).then(singleJoinBy(ids, 'id'));
const getAssetsByQuery = async (
{ connectors: { services: { Assets } } },
query
) => {
// If we are requesting based on a limit, ask for one more than we want.
const limit = query.limit;
if (limit) {
@@ -22,7 +25,6 @@ const getAssetsByQuery = async ({connectors: {services: {Assets}}}, query) => {
// 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;
@@ -31,31 +33,21 @@ const getAssetsByQuery = async ({connectors: {services: {Assets}}}, query) => {
return {
startCursor: nodes && nodes.length > 0 ? nodes[0].created_at : null,
endCursor: nodes && nodes.length > 0 ? nodes[nodes.length - 1].created_at : null,
endCursor:
nodes && nodes.length > 0 ? nodes[nodes.length - 1].created_at : null,
hasNextPage,
nodes,
};
};
const findOrCreateAssetByURL = async (ctx, url) => {
// Pull our connectors out of the context.
const {
loaders: {
Assets,
Settings,
},
loaders: { Assets, Settings },
connectors: {
models: {
Asset,
},
services: {
DomainList,
Scraper,
},
errors: {
ErrInvalidAssetURL,
},
models: { Asset },
services: { DomainList, Scraper },
errors: { ErrInvalidAssetURL },
},
} = ctx;
@@ -77,10 +69,7 @@ const findOrCreateAssetByURL = async (ctx, url) => {
// Seems the asset wasn't here yet.. We should do some validation.
// Check for whitelisting + get the settings at the same time.
const [
whitelisted,
settings,
] = await Promise.all([
const [whitelisted, settings] = await Promise.all([
DomainList.urlCheck(url),
Settings.load('autoCloseStream closedTimeout'),
]);
@@ -100,31 +89,35 @@ const findOrCreateAssetByURL = async (ctx, url) => {
// If the auto-close stream is enabled, close the stream after the designated
// timeout.
if (settings.autoCloseStream) {
update.$setOnInsert.closedAt = new Date(Date.now() + settings.closedTimeout * 1000);
update.$setOnInsert.closedAt = new Date(
Date.now() + settings.closedTimeout * 1000
);
}
// We're using the findOneAndUpdate here instead of a insert to protect
// against race conditions.
asset = await Asset.findOneAndUpdate({
url,
}, update, {
asset = await Asset.findOneAndUpdate(
{
url,
},
update,
{
// Ensure that if it's new, we return the new object created.
new: true,
// 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,
// 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,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true,
// Ensure that we validate the input that we do have.
runValidators: true,
});
// Ensure that we validate the input that we do have.
runValidators: true,
}
);
// If this is a new asset, then we need to scrape it!
if (!asset.scraped) {
// Create the Scraper job.
await Scraper.create(asset);
}
@@ -132,8 +125,10 @@ const findOrCreateAssetByURL = async (ctx, url) => {
return asset;
};
const findByUrl = async ({connectors: {errors, services: {Assets}}}, asset_url) => {
const findByUrl = async (
{ connectors: { errors, services: { Assets } } },
asset_url
) => {
// Try to validate that the url is valid. If the URL constructor throws an
// error, throw our internal ErrInvalidAssetURL instead. This will validate
// that the url contains a valid scheme.
@@ -146,12 +141,12 @@ const findByUrl = async ({connectors: {errors, services: {Assets}}}, asset_url)
return Assets.findByUrl(asset_url);
};
module.exports = (ctx) => ({
module.exports = ctx => ({
Assets: {
getByURL: (url) => findOrCreateAssetByURL(ctx, url),
findByUrl: (url) => findByUrl(ctx, url),
getByQuery: (query) => getAssetsByQuery(ctx, query),
getByID: new DataLoader((ids) => genAssetsByID(ctx, ids)),
getAll: new SingletonResolver(() => ctx.connectors.models.Asset.find({}))
}
getByURL: url => findOrCreateAssetByURL(ctx, url),
findByUrl: url => findByUrl(ctx, url),
getByQuery: query => getAssetsByQuery(ctx, query),
getByID: new DataLoader(ids => genAssetsByID(ctx, ids)),
getAll: new SingletonResolver(() => ctx.connectors.models.Asset.find({})),
},
});
+180 -110
View File
@@ -1,15 +1,10 @@
const {
SharedCounterDataLoader,
singleJoinBy,
} = require('./util');
const { SharedCounterDataLoader, singleJoinBy } = require('./util');
const DataLoader = require('dataloader');
const {
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
SEARCH_OTHERS_COMMENTS
SEARCH_OTHERS_COMMENTS,
} = require('../../perms/constants');
const {
CACHE_EXPIRY_COMMENT_COUNT
} = require('../../config');
const { CACHE_EXPIRY_COMMENT_COUNT } = require('../../config');
const ms = require('ms');
const sc = require('snake-case');
@@ -27,24 +22,24 @@ const getCountsByAssetID = (context, asset_ids) => {
{
$match: {
asset_id: {
$in: asset_ids
$in: asset_ids,
},
status: {
$in: ['NONE', 'ACCEPTED']
}
}
$in: ['NONE', 'ACCEPTED'],
},
},
},
{
$group: {
_id: '$asset_id',
count: {
$sum: 1
}
}
}
$sum: 1,
},
},
},
])
.then(singleJoinBy(asset_ids, '_id'))
.then((results) => results.map((result) => result ? result.count : 0));
.then(results => results.map(result => (result ? result.count : 0)));
};
/**
@@ -59,25 +54,25 @@ const getParentCountsByAssetID = (context, asset_ids) => {
{
$match: {
asset_id: {
$in: asset_ids
$in: asset_ids,
},
status: {
$in: ['NONE', 'ACCEPTED']
$in: ['NONE', 'ACCEPTED'],
},
parent_id: null
}
parent_id: null,
},
},
{
$group: {
_id: '$asset_id',
count: {
$sum: 1
}
}
}
$sum: 1,
},
},
},
])
.then(singleJoinBy(asset_ids, '_id'))
.then((results) => results.map((result) => result ? result.count : 0));
.then(results => results.map(result => (result ? result.count : 0)));
};
/**
@@ -89,12 +84,20 @@ const getParentCountsByAssetID = (context, asset_ids) => {
* query
*/
const getCommentCountByQuery = (ctx, options) => {
const {statuses, asset_id, parent_id, author_id, tags, action_type} = options;
const {
statuses,
asset_id,
parent_id,
author_id,
tags,
action_type,
} = options;
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
// special privileges.
if (
(!statuses || statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
(!statuses ||
statuses.some(status => !['NONE', 'ACCEPTED'].includes(status))) &&
(ctx.user == null || !ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS))
) {
return null;
@@ -103,15 +106,15 @@ const getCommentCountByQuery = (ctx, options) => {
const query = CommentModel.find();
if (asset_id != null) {
query.merge({asset_id});
query.merge({ asset_id });
}
if (parent_id !== undefined) {
query.merge({parent_id});
query.merge({ parent_id });
}
if (author_id) {
query.merge({author_id});
query.merge({ author_id });
}
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
@@ -123,7 +126,7 @@ const getCommentCountByQuery = (ctx, options) => {
}
if (statuses && statuses.length > 0) {
query.merge({status: {$in: statuses}});
query.merge({ status: { $in: statuses } });
}
if (tags && tags.length > 0) {
@@ -134,9 +137,7 @@ const getCommentCountByQuery = (ctx, options) => {
});
}
return CommentModel
.find(query)
.count();
return CommentModel.find(query).count();
};
/**
@@ -146,22 +147,30 @@ const getCommentCountByQuery = (ctx, options) => {
* @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}) => {
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;
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.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].startCursor) {
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
if (
!ctx.plugins ||
!ctx.plugins.Sort.Comments ||
!ctx.plugins.Sort.Comments[SORT_KEY] ||
!ctx.plugins.Sort.Comments[SORT_KEY].startCursor
) {
throw new Error(
`unable to sort by ${sortBy}, no plugin was provided to handle this type`
);
}
return ctx.plugins.Sort.Comments[SORT_KEY].startCursor(ctx, nodes, {cursor});
return ctx.plugins.Sort.Comments[SORT_KEY].startCursor(ctx, nodes, {
cursor,
});
};
/**
@@ -171,20 +180,27 @@ const getStartCursor = (ctx, nodes, {cursor, sortBy}) => {
* @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}) => {
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;
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.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].endCursor) {
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
if (
!ctx.plugins ||
!ctx.plugins.Sort.Comments ||
!ctx.plugins.Sort.Comments[SORT_KEY] ||
!ctx.plugins.Sort.Comments[SORT_KEY].endCursor
) {
throw new Error(
`unable to sort by ${sortBy}, no plugin was provided to handle this type`
);
}
return ctx.plugins.Sort.Comments[SORT_KEY].endCursor(ctx, nodes, {cursor});
return ctx.plugins.Sort.Comments[SORT_KEY].endCursor(ctx, nodes, { cursor });
};
/**
@@ -195,42 +211,55 @@ const getEndCursor = (ctx, nodes, {cursor, sortBy}) => {
* @param {Object} query the current mongoose query object
* @param {Object} params the params from the client describing the query
*/
const applySort = (ctx, query, {cursor, sortOrder, sortBy}) => {
const applySort = (ctx, query, { cursor, sortOrder, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT': {
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
case 'CREATED_AT': {
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
}
}
}
return query.sort({created_at: sortOrder === 'DESC' ? -1 : 1});
}
case 'REPLIES': {
if (cursor) {
query = query.skip(cursor);
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
}
case 'REPLIES': {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({reply_count: sortOrder === 'DESC' ? -1 : 1, created_at: sortOrder === 'DESC' ? -1 : 1});
}
return query.sort({
reply_count: sortOrder === 'DESC' ? -1 : 1,
created_at: sortOrder === 'DESC' ? -1 : 1,
});
}
}
const SORT_KEY = sortBy.toLowerCase();
if (!ctx.plugins || !ctx.plugins.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].sort) {
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
if (
!ctx.plugins ||
!ctx.plugins.Sort.Comments ||
!ctx.plugins.Sort.Comments[SORT_KEY] ||
!ctx.plugins.Sort.Comments[SORT_KEY].sort
) {
throw new Error(
`unable to sort by ${sortBy}, no plugin was provided to handle this type`
);
}
return ctx.plugins.Sort.Comments[SORT_KEY].sort(ctx, query, {cursor, sortOrder});
return ctx.plugins.Sort.Comments[SORT_KEY].sort(ctx, query, {
cursor,
sortOrder,
});
};
/**
@@ -242,10 +271,13 @@ const applySort = (ctx, query, {cursor, sortOrder, sortBy}) => {
* @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, sortOrder, sortBy, limit}) => {
const executeWithSort = async (
ctx,
query,
{ cursor, sortOrder, sortBy, limit }
) => {
// Apply the sort to the query.
query = applySort(ctx, query, {cursor, sortOrder, sortBy});
query = applySort(ctx, query, { cursor, sortOrder, sortBy });
// Apply the limit (if it exists, as it's applied universally).
if (limit) {
@@ -259,7 +291,6 @@ const executeWithSort = async (ctx, query, {cursor, sortOrder, sortBy, limit}) =
// 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;
@@ -269,8 +300,13 @@ const executeWithSort = async (ctx, query, {cursor, sortOrder, sortBy, limit}) =
// Use the generator functions below to extract the cursor details based on
// the current sortBy parameter.
return {
startCursor: getStartCursor(ctx, nodes, {cursor, sortOrder, sortBy, limit}),
endCursor: getEndCursor(ctx, nodes, {cursor, sortOrder, sortBy, limit}),
startCursor: getStartCursor(ctx, nodes, {
cursor,
sortOrder,
sortBy,
limit,
}),
endCursor: getEndCursor(ctx, nodes, { cursor, sortOrder, sortBy, limit }),
hasNextPage,
nodes,
};
@@ -283,20 +319,37 @@ const executeWithSort = async (ctx, query, {cursor, sortOrder, sortBy, limit}) =
* @param {Object} context graph context
* @param {Object} query query terms to apply to the comments query
*/
const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sortOrder, sortBy, excludeIgnored, tags, action_type}) => {
const getCommentsByQuery = async (
ctx,
{
ids,
statuses,
asset_id,
parent_id,
author_id,
limit,
cursor,
sortOrder,
sortBy,
excludeIgnored,
tags,
action_type,
}
) => {
let comments = CommentModel.find();
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
// special privileges.
if (
(!statuses || statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
(!statuses ||
statuses.some(status => !['NONE', 'ACCEPTED'].includes(status))) &&
(ctx.user == null || !ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS))
) {
return null;
}
if (statuses) {
comments = comments.where({status: {$in: statuses}});
comments = comments.where({ status: { $in: statuses } });
}
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
@@ -310,8 +363,8 @@ const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, auth
if (ids) {
comments = comments.find({
id: {
$in: ids
}
$in: ids,
},
});
}
@@ -324,27 +377,36 @@ const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, auth
}
// Only let an admin request any user or the current user request themself.
if (ctx.user && (ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) && author_id != null) {
comments = comments.where({author_id});
if (
ctx.user &&
(ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) &&
author_id != null
) {
comments = comments.where({ author_id });
}
if (asset_id) {
comments = comments.where({asset_id});
comments = comments.where({ asset_id });
}
// We perform the undefined check because, null, is a valid state for the
// search to be with, which indicates that it is at depth 0.
if (parent_id !== undefined) {
comments = comments.where({parent_id});
comments = comments.where({ parent_id });
}
if (excludeIgnored && ctx.user && ctx.user.ignoresUsers && ctx.user.ignoresUsers.length > 0) {
if (
excludeIgnored &&
ctx.user &&
ctx.user.ignoresUsers &&
ctx.user.ignoresUsers.length > 0
) {
comments = comments.where({
author_id: {$nin: ctx.user.ignoresUsers}
author_id: { $nin: ctx.user.ignoresUsers },
});
}
return executeWithSort(ctx, comments, {cursor, sortOrder, sortBy, limit});
return executeWithSort(ctx, comments, { cursor, sortOrder, sortBy, limit });
};
/**
@@ -355,22 +417,22 @@ const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, auth
* @param {Array<String>} ids the comment id's to fetch
* @return {Promise} resolves to the comments
*/
const getComments = ({user}, ids) => {
const getComments = ({ user }, ids) => {
let comments;
if (user && user.can(SEARCH_OTHERS_COMMENTS)) {
comments = CommentModel.find({
id: {
$in: ids
}
$in: ids,
},
});
} else {
comments = CommentModel.find({
id: {
$in: ids
$in: ids,
},
status: {
$in: ['NONE', 'ACCEPTED']
}
$in: ['NONE', 'ACCEPTED'],
},
});
}
return comments.then(singleJoinBy(ids, 'id'));
@@ -382,12 +444,20 @@ const getComments = ({user}, ids) => {
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
module.exports = context => ({
Comments: {
get: new DataLoader((ids) => getComments(context, ids)),
getByQuery: (query) => getCommentsByQuery(context, query),
getCountByQuery: (query) => getCommentCountByQuery(context, query),
countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByAssetID(context, ids)),
parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getParentCountsByAssetID(context, ids))
}
get: new DataLoader(ids => getComments(context, ids)),
getByQuery: query => getCommentsByQuery(context, query),
getCountByQuery: query => getCommentCountByQuery(context, query),
countByAssetID: new SharedCounterDataLoader(
'Comments.totalCommentCount',
ms(CACHE_EXPIRY_COMMENT_COUNT),
ids => getCountsByAssetID(context, ids)
),
parentCountByAssetID: new SharedCounterDataLoader(
'Comments.countByAssetID',
ms(CACHE_EXPIRY_COMMENT_COUNT),
ids => getParentCountsByAssetID(context, ids)
),
},
});
+11 -13
View File
@@ -11,7 +11,6 @@ const Users = require('./users');
const plugins = require('../../services/plugins');
let loaders = [
// Load the core loaders.
Actions,
Assets,
@@ -21,12 +20,11 @@ let loaders = [
Users,
// Load the plugin loaders from the manager.
...plugins
.get('server', 'loaders').map(({plugin, loaders}) => {
debug(`added plugin '${plugin.name}'`);
...plugins.get('server', 'loaders').map(({ plugin, loaders }) => {
debug(`added plugin '${plugin.name}'`);
return loaders;
})
return loaders;
}),
];
/**
@@ -34,12 +32,12 @@ let loaders = [
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => {
module.exports = context => {
// We need to return an object to be accessed.
return _.merge(...loaders.map((loaders) => {
// Each loader is a function which takes the context.
return loaders(context);
}));
return _.merge(
...loaders.map(loaders => {
// Each loader is a function which takes the context.
return loaders(context);
})
);
};
+8 -4
View File
@@ -7,13 +7,17 @@ const DataLoader = require('dataloader');
* @return {Object} object of loaders
*/
module.exports = () => {
const loader = new DataLoader((selections) => Promise.all(selections.map((fields) => {
return SettingsService.retrieve(fields);
})));
const loader = new DataLoader(selections =>
Promise.all(
selections.map(fields => {
return SettingsService.retrieve(fields);
})
)
);
return {
Settings: {
load: (fields = false) => loader.load(fields),
}
},
};
};
+18 -14
View File
@@ -2,26 +2,30 @@ const DataLoader = require('dataloader');
const TagsService = require('../../services/tags');
const plugins = require('../../services/plugins');
const debug = require('debug')('talk:graph:loaders:tags');
const PLUGIN_TAGS = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => {
debug(`added plugin '${plugin.name}'`);
const PLUGIN_TAGS = plugins
.get('server', 'tags')
.reduce((acc, { plugin, tags }) => {
debug(`added plugin '${plugin.name}'`);
acc = acc.concat(tags);
acc = acc.concat(tags);
return acc;
}, []);
return acc;
}, []);
/**
* Get all the tags for the context for the dataloader.
*/
const genAll = (context, queries) => {
return Promise.all(queries.map(async ({id, item_type, asset_id}) => {
let tags = await TagsService.getAll({id, item_type, asset_id});
return Promise.all(
queries.map(async ({ id, item_type, asset_id }) => {
let tags = await TagsService.getAll({ id, item_type, asset_id });
// Merge in the global plugin tags as well.
tags = tags.concat(PLUGIN_TAGS);
// Merge in the global plugin tags as well.
tags = tags.concat(PLUGIN_TAGS);
return tags;
}));
return tags;
})
);
};
/**
@@ -29,8 +33,8 @@ const genAll = (context, queries) => {
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
module.exports = context => ({
Tags: {
getAll: new DataLoader((queries) => genAll(context, queries))
}
getAll: new DataLoader(queries => genAll(context, queries)),
},
});
+37 -41
View File
@@ -2,31 +2,29 @@ const DataLoader = require('dataloader');
const util = require('./util');
const {
SEARCH_OTHER_USERS,
} = require('../../perms/constants');
const { SEARCH_OTHER_USERS } = require('../../perms/constants');
const UsersService = require('../../services/users');
const {escapeRegExp} = require('../../services/regex');
const { escapeRegExp } = require('../../services/regex');
const UserModel = require('../../models/user');
const mergeState = (query, state) => {
const {status} = state;
const { status } = state;
if (status) {
const {username, banned, suspended} = status;
const { username, banned, suspended } = status;
if (typeof username !== 'undefined' && username && username.length > 0) {
query.merge({
'status.username.status': {
$in: username
}
$in: username,
},
});
}
if (typeof banned !== 'undefined' && banned !== null) {
query.merge({
'status.banned.status': banned
'status.banned.status': banned,
});
}
@@ -34,17 +32,19 @@ const mergeState = (query, state) => {
if (suspended) {
query.merge({
'status.suspension.until': {
$gte: Date.now()
}
$gte: Date.now(),
},
});
} else {
query.merge({
$or: [
{'status.suspension.until': null},
{'status.suspension.until': {
$lt: Date.now()
}}
]
{ 'status.suspension.until': null },
{
'status.suspension.until': {
$lt: Date.now(),
},
},
],
});
}
}
@@ -61,9 +61,7 @@ const genUserByIDs = async (context, ids) => {
return [user];
}
return UsersService
.findByIdArray(ids)
.then(util.singleJoinBy(ids, 'id'));
return UsersService.findByIdArray(ids).then(util.singleJoinBy(ids, 'id'));
};
/**
@@ -72,7 +70,10 @@ const genUserByIDs = async (context, ids) => {
* @param {Object} context graph context
* @param {Object} query query terms to apply to the users query
*/
const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action_type, sortOrder}) => {
const getUsersByQuery = async (
{ user },
{ limit, cursor, value = '', state, action_type, sortOrder }
) => {
let query = UserModel.find();
if (action_type || state || value.length > 0) {
@@ -81,7 +82,6 @@ const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action
}
if (value.length > 0) {
// Lowercase the search term and escape any regex characters.
value = escapeRegExp(value).toLowerCase();
@@ -91,7 +91,6 @@ const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action
// Merge in the regex params.
query.merge({
$or: [
// Search by a prefix match on the username.
{
lowercaseUsername: {
@@ -121,8 +120,8 @@ const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action
if (action_type) {
query.merge({
[`action_counts.${action_type.toLowerCase()}`]: {
$gt: 0
}
$gt: 0,
},
});
}
}
@@ -131,14 +130,14 @@ const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor
}
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor
}
$gt: cursor,
},
});
}
}
@@ -149,7 +148,7 @@ const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action
}
// Sort by created_at.
query.sort({created_at: sortOrder === 'DESC' ? -1 : 1});
query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
// Execute the query.
const nodes = await query.exec();
@@ -158,7 +157,6 @@ const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action
// 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;
@@ -184,7 +182,7 @@ const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action
* @return {Promise} resolves to the counts of the users from the
* query
*/
const getCountByQuery = async ({user}, {action_type, state}) => {
const getCountByQuery = async ({ user }, { action_type, state }) => {
let query = UserModel.find();
if (action_type || state) {
@@ -199,15 +197,13 @@ const getCountByQuery = async ({user}, {action_type, state}) => {
if (action_type) {
query.merge({
[`action_counts.${action_type.toLowerCase()}`]: {
$gt: 0
}
$gt: 0,
},
});
}
}
return UserModel
.find(query)
.count();
return UserModel.find(query).count();
};
/**
@@ -215,10 +211,10 @@ const getCountByQuery = async ({user}, {action_type, state}) => {
* @param {Object} context the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = (context) => ({
module.exports = context => ({
Users: {
getByQuery: (query) => getUsersByQuery(context, query),
getByID: new DataLoader((ids) => genUserByIDs(context, ids)),
getCountByQuery: (query) => getCountByQuery(context, query)
}
getByQuery: query => getUsersByQuery(context, query),
getByID: new DataLoader(ids => genUserByIDs(context, ids)),
getCountByQuery: query => getCountByQuery(context, query),
},
});
+24 -18
View File
@@ -16,7 +16,7 @@ class SingletonResolver {
return this._cache;
}
let promise = this._resolver(arguments).then((result) => {
let promise = this._resolver(arguments).then(result => {
return result;
});
@@ -34,10 +34,10 @@ class SingletonResolver {
* @param {String} key key to group by
* @return {Array} array of results
*/
const arrayJoinBy = (ids, key) => (items) => {
const arrayJoinBy = (ids, key) => items => {
const itemsByKey = _.groupBy(items, key);
return ids.map((id) => {
return ids.map(id => {
if (id in itemsByKey) {
return itemsByKey[id];
}
@@ -53,9 +53,9 @@ const arrayJoinBy = (ids, key) => (items) => {
* @param {String} key key to group by
* @return {Array} array of results
*/
const singleJoinBy = (ids, key) => (items) => {
const singleJoinBy = (ids, key) => items => {
const itemsByKey = _.groupBy(items, key);
return ids.map((id) => {
return ids.map(id => {
if (id in itemsByKey) {
return itemsByKey[id][0];
}
@@ -70,7 +70,10 @@ const singleJoinBy = (ids, key) => (items) => {
*/
class SharedCacheDataLoader extends DataLoader {
constructor(prefix, expiry, batchLoadFn, options) {
super(SharedCacheDataLoader.batchLoadFn(prefix, expiry, batchLoadFn), options);
super(
SharedCacheDataLoader.batchLoadFn(prefix, expiry, batchLoadFn),
options
);
// Expiry is provided as a number in ms, we're using commands optimized for
// seconds, so convert this to seconds.
@@ -83,9 +86,7 @@ class SharedCacheDataLoader extends DataLoader {
* clear the key from the shared cache and the request cache
*/
clear(key) {
return cache
.invalidate(key, this._keyFunc)
.then(() => super.clear(key));
return cache.invalidate(key, this._keyFunc).then(() => super.clear(key));
}
/**
@@ -110,16 +111,22 @@ class SharedCacheDataLoader extends DataLoader {
* wraps up the prefix needed for the redis backed shared cache driver
*/
static keyFunc(prefix) {
return (key) => `cache.sbl[${prefix}][${key}]`;
return key => `cache.sbl[${prefix}][${key}]`;
}
/**
* wraps the dataloader batchLoadFn with the shared cache's wrapper
*/
static batchLoadFn(prefix, expiry, batchLoadFn) {
return (ids) => cache.wrapMany(ids, expiry, (workKeys) => {
return batchLoadFn(workKeys);
}, SharedCacheDataLoader.keyFunc(prefix));
return ids =>
cache.wrapMany(
ids,
expiry,
workKeys => {
return batchLoadFn(workKeys);
},
SharedCacheDataLoader.keyFunc(prefix)
);
}
}
@@ -128,7 +135,6 @@ class SharedCacheDataLoader extends DataLoader {
* exception in that it is designed to work with numerical cached data.
*/
class SharedCounterDataLoader extends SharedCacheDataLoader {
/**
* Increments the key in the cache if it already exists in the cache, if not
* it does nothing.
@@ -151,8 +157,8 @@ class SharedCounterDataLoader extends SharedCacheDataLoader {
* @param {Array} paths paths on the object to be used to generate the cache
* key
*/
const objectCacheKeyFn = (...paths) => (obj) => {
return paths.map((path) => obj[path]).join(':');
const objectCacheKeyFn = (...paths) => obj => {
return paths.map(path => obj[path]).join(':');
};
/**
@@ -160,7 +166,7 @@ const objectCacheKeyFn = (...paths) => (obj) => {
* @param {Array} paths paths on the object to be used to generate the cache
* key
*/
const arrayCacheKeyFn = (arr) => {
const arrayCacheKeyFn = arr => {
return arr.sort().join(':');
};
@@ -171,5 +177,5 @@ module.exports = {
arrayCacheKeyFn,
SingletonResolver,
SharedCacheDataLoader,
SharedCounterDataLoader
SharedCounterDataLoader,
};
+25 -47
View File
@@ -1,8 +1,6 @@
const errors = require('../../errors');
const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
const {
IGNORE_FLAGS_AGAINST_STAFF,
} = require('../../config');
const { CREATE_ACTION, DELETE_ACTION } = require('../../perms/constants');
const { IGNORE_FLAGS_AGAINST_STAFF } = require('../../config');
/**
* getActionItem will return the item that is associated with the given action.
@@ -12,21 +10,16 @@ const {
* @param {Object} action the action being performed
* @return {Promise} resolves to the referenced item
*/
const getActionItem = async (ctx, {item_id, item_type}) => {
const {
loaders: {
Comments,
Users,
},
} = ctx;
const getActionItem = async (ctx, { item_id, item_type }) => {
const { loaders: { Comments, Users } } = ctx;
switch (item_type) {
case 'COMMENTS':
return Comments.get.load(item_id);
case 'USERS':
return Users.getByID.load(item_id);
default:
return null;
case 'COMMENTS':
return Comments.get.load(item_id);
case 'USERS':
return Users.getByID.load(item_id);
default:
return null;
}
};
@@ -38,19 +31,14 @@ const getActionItem = async (ctx, {item_id, item_type}) => {
* @param {Object} action the action being created
* @return {Promise} resolves to the action created
*/
const createAction = async (ctx, {item_id, item_type, action_type, group_id, metadata = {}}) => {
const {
user = {},
pubsub,
connectors: {
services: {
Actions,
},
},
} = ctx;
const createAction = async (
ctx,
{ item_id, item_type, action_type, group_id, metadata = {} }
) => {
const { user = {}, pubsub, connectors: { services: { Actions } } } = ctx;
// Gets the item referenced by the action.
const item = await getActionItem(ctx, {item_id, item_type});
const item = await getActionItem(ctx, { item_id, item_type });
if (!item || item === null) {
throw errors.ErrNotFound;
}
@@ -59,7 +47,6 @@ const createAction = async (ctx, {item_id, item_type, action_type, group_id, met
// staff member.
if (IGNORE_FLAGS_AGAINST_STAFF) {
if (action_type === 'FLAG') {
// If the item is a user, and this is a flag. Check to see if they are
// staff, if they are, don't permit the flag.
if (item_type === 'USERS' && item.isStaff()) {
@@ -69,7 +56,6 @@ const createAction = async (ctx, {item_id, item_type, action_type, group_id, met
}
if (action_type === 'FLAG' && item_type === 'USERS') {
// The item is a user, and this is a flag. Check to see if they are staff,
// if they are, don't permit the flag.
if (item.isStaff()) {
@@ -84,11 +70,10 @@ const createAction = async (ctx, {item_id, item_type, action_type, group_id, met
user_id: user.id,
group_id,
action_type,
metadata
metadata,
});
if (action_type === 'FLAG' && item_type === 'COMMENTS') {
// The item is a comment, and this is a flag. Push that the comment was
// flagged, don't wait for it to finish.
pubsub.publish('commentFlagged', item);
@@ -104,33 +89,26 @@ const createAction = async (ctx, {item_id, item_type, action_type, group_id, met
* @param {String} id the id of the action to delete
* @return {Promise} resolves to the deleted action, or null if not found.
*/
const deleteAction = (ctx, {id}) => {
const {
user,
connectors: {
services: {
Actions,
},
},
} = ctx;
const deleteAction = (ctx, { id }) => {
const { user, connectors: { services: { Actions } } } = ctx;
return Actions.delete({id, user_id: user.id});
return Actions.delete({ id, user_id: user.id });
};
module.exports = (ctx) => {
module.exports = ctx => {
let mutators = {
Action: {
create: () => Promise.reject(errors.ErrNotAuthorized),
delete: () => Promise.reject(errors.ErrNotAuthorized)
}
delete: () => Promise.reject(errors.ErrNotAuthorized),
},
};
if (ctx.user && ctx.user.can(CREATE_ACTION)) {
mutators.Action.create = (action) => createAction(ctx, action);
mutators.Action.create = action => createAction(ctx, action);
}
if (ctx.user && ctx.user.can(DELETE_ACTION)) {
mutators.Action.delete = (action) => deleteAction(ctx, action);
mutators.Action.delete = action => deleteAction(ctx, action);
}
return mutators;
+21 -14
View File
@@ -14,7 +14,8 @@ const AssetModel = require('../../models/asset');
* @param {String} id the asset's id to update
* @param {Object} settings the settings to update on the asset.
*/
const updateSettings = async (ctx, id, settings) => AssetsService.overrideSettings(id, settings);
const updateSettings = async (ctx, id, settings) =>
AssetsService.overrideSettings(id, settings);
/**
* updateStatus will update the status of an asset.
@@ -24,30 +25,36 @@ const updateSettings = async (ctx, id, settings) => AssetsService.overrideSettin
* @param {Object} status the status to change on the asset relating to it's
* current state.
*/
const updateStatus = async (ctx, id, {closedAt, closedMessage}) => AssetModel.update({
id,
}, {
$set: {
closedAt,
closedMessage
}
});
const updateStatus = async (ctx, id, { closedAt, closedMessage }) =>
AssetModel.update(
{
id,
},
{
$set: {
closedAt,
closedMessage,
},
}
);
module.exports = (ctx) => {
module.exports = ctx => {
let mutators = {
Asset: {
updateSettings: () => Promise.reject(errors.ErrNotAuthorized),
updateStatus: () => Promise.reject(errors.ErrNotAuthorized)
}
updateStatus: () => Promise.reject(errors.ErrNotAuthorized),
},
};
if (ctx.user) {
if (ctx.user.can(UPDATE_ASSET_SETTINGS)) {
mutators.Asset.updateSettings = (id, settings) => updateSettings(ctx, id, settings);
mutators.Asset.updateSettings = (id, settings) =>
updateSettings(ctx, id, settings);
}
if (ctx.user.can(UPDATE_ASSET_STATUS)) {
mutators.Asset.updateStatus = (id, status) => updateStatus(ctx, id, status);
mutators.Asset.updateStatus = (id, status) =>
updateStatus(ctx, id, status);
}
}
+158 -139
View File
@@ -12,7 +12,7 @@ const {
CREATE_COMMENT,
SET_COMMENT_STATUS,
ADD_COMMENT_TAG,
EDIT_COMMENT
EDIT_COMMENT,
} = require('../../perms/constants');
const debug = require('debug')('talk:graph:mutators:comment');
const {
@@ -20,26 +20,30 @@ const {
IGNORE_FLAGS_AGAINST_STAFF,
} = require('../../config');
const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => {
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
asset_id,
});
if (!Array.isArray(globalTags)) {
globalTags = [];
}
// Merge in the tags for the given comment.
tags = tags.map((name) => {
tags = tags.map(name => {
// Resolve the TagLink that we can use for the comment.
let {tagLink} = TagsService.resolveLink(user, globalTags, {name, item_type});
let { tagLink } = TagsService.resolveLink(user, globalTags, {
name,
item_type,
});
// Return the tagLink for tag insertion.
return tagLink;
@@ -48,10 +52,12 @@ const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags =
// Add the staff tag for comments created as a staff member.
if (user.can(ADD_COMMENT_TAG)) {
tags.push(TagsService.newTagLink(user, {
name: 'STAFF',
item_type
}));
tags.push(
TagsService.newTagLink(user, {
name: 'STAFF',
item_type,
})
);
}
return tags;
@@ -63,14 +69,9 @@ const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags =
*/
const adjustKarma = (Comments, id, status) => async () => {
try {
// Use the dataloader to get the comment that was just moderated and
// get the flag user's id's so we can adjust their karma too.
let [
comment,
flagUserIDs
] = await Promise.all([
let [comment, flagUserIDs] = await Promise.all([
// Load the comment that was just made/updated by the setCommentStatus
// operation.
Comments.get.load(id),
@@ -80,51 +81,53 @@ const adjustKarma = (Comments, id, status) => async () => {
ActionModel.find({
item_id: id,
item_type: 'COMMENTS',
action_type: 'FLAG'
}).then((actions) => {
action_type: 'FLAG',
}).then(actions => {
// This is to ensure that this is always an array.
if (!actions) {
return [];
}
return actions.map(({user_id}) => user_id);
})
return actions.map(({ user_id }) => user_id);
}),
]);
debug(`Comment[${id}] by User[${comment.author_id}] was Status[${status}]`);
switch (status) {
case 'REJECTED':
case 'REJECTED':
// Reduce the user's karma.
debug(`CommentUser[${comment.author_id}] had their karma reduced`);
// Reduce the user's karma.
debug(`CommentUser[${comment.author_id}] had their karma reduced`);
// Decrease the flag user's karma, the moderator disagreed with this
// action.
debug(
`FlaggingUser[${flagUserIDs.join(', ')}] had their karma increased`
);
await Promise.all([
KarmaService.modifyUser(comment.author_id, -1, 'comment'),
KarmaService.modifyUser(flagUserIDs, 1, 'flag', true),
]);
// Decrease the flag user's karma, the moderator disagreed with this
// action.
debug(`FlaggingUser[${flagUserIDs.join(', ')}] had their karma increased`);
await Promise.all([
KarmaService.modifyUser(comment.author_id, -1, 'comment'),
KarmaService.modifyUser(flagUserIDs, 1, 'flag', true)
]);
break;
break;
case 'ACCEPTED':
// Increase the user's karma.
debug(`CommentUser[${comment.author_id}] had their karma increased`);
case 'ACCEPTED':
// Increase the user's karma.
debug(`CommentUser[${comment.author_id}] had their karma increased`);
// Increase the flag user's karma, the moderator agreed with this
// action.
debug(`FlaggingUser[${flagUserIDs.join(', ')}] had their karma reduced`);
await Promise.all([
KarmaService.modifyUser(comment.author_id, 1, 'comment'),
KarmaService.modifyUser(flagUserIDs, -1, 'flag', true)
]);
break;
// Increase the flag user's karma, the moderator agreed with this
// action.
debug(
`FlaggingUser[${flagUserIDs.join(', ')}] had their karma reduced`
);
await Promise.all([
KarmaService.modifyUser(comment.author_id, 1, 'comment'),
KarmaService.modifyUser(flagUserIDs, -1, 'flag', true),
]);
break;
default:
return;
}
return;
@@ -142,11 +145,21 @@ const adjustKarma = (Comments, id, status) => async () => {
* @param {String} [status='NONE'] the status of the new comment
* @return {Promise} resolves to the created comment
*/
const createComment = async (context, {tags = [], body, asset_id, parent_id = null, status = 'NONE', metadata = {}}) => {
const {user, loaders: {Comments}, pubsub} = context;
const createComment = async (
context,
{
tags = [],
body,
asset_id,
parent_id = null,
status = 'NONE',
metadata = {},
}
) => {
const { user, loaders: { Comments }, pubsub } = context;
// Resolve the tags for the comment.
tags = await resolveTagsForComment(context, {asset_id, tags});
tags = await resolveTagsForComment(context, { asset_id, tags });
let comment = await CommentsService.publicCreate({
body,
@@ -182,13 +195,9 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu
* @param {String} [asset_id] id of asset comment is posted on
* @return {Object} resolves to the wordlist results
*/
const filterNewComment = async (context, {body, asset_id}) => {
const filterNewComment = async (context, { body, asset_id }) => {
// Load the settings.
const [
settings,
asset,
] = await Promise.all([
const [settings, asset] = await Promise.all([
context.loaders.Settings.load(),
context.loaders.Assets.getByID.load(asset_id),
]);
@@ -201,12 +210,11 @@ const filterNewComment = async (context, {body, asset_id}) => {
// Load the wordlist and filter the comment content.
return [
// Scan the word.
wl.scan('body', body),
// Return the asset's settings.
await AssetsService.rectifySettings(asset, settings)
await AssetsService.rectifySettings(asset, settings),
];
};
@@ -215,10 +223,8 @@ const filterNewComment = async (context, {body, asset_id}) => {
* returned.
*/
const moderationPhases = [
// This phase checks to see if the comment is long enough.
(context, comment) => {
// Check to see if the body is too short, if it is, then complain about it!
if (comment.body.length < 2) {
throw errors.ErrCommentTooShort;
@@ -226,8 +232,7 @@ const moderationPhases = [
},
// This phase checks to see if the asset being processed is closed or not.
(context, comment, {asset}) => {
(context, comment, { asset }) => {
// Check to see if the asset has closed commenting...
if (asset.isClosed) {
throw new errors.ErrAssetCommentingClosed(asset.closedMessage);
@@ -235,23 +240,23 @@ const moderationPhases = [
},
// This phase checks the comment against the wordlist.
(context, comment, {wordlist}) => {
(context, comment, { 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
// wordlist, then reject it, otherwise if the moderation setting is
// premod, set it to `premod`.
if (wordlist.banned) {
// Add the flag related to Trust to the comment.
return {
status: 'REJECTED',
actions: [{
action_type: 'FLAG',
user_id: null,
group_id: 'BANNED_WORD',
metadata: {}
}]
actions: [
{
action_type: 'FLAG',
user_id: null,
group_id: 'BANNED_WORD',
metadata: {},
},
],
};
}
@@ -262,44 +267,45 @@ const moderationPhases = [
// If the wordlist has matched the suspect word filter and we haven't disabled
// auto-flagging suspect words, then we should flag the comment!
if (wordlist.suspect && !DISABLE_AUTOFLAG_SUSPECT_WORDS) {
// 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 {
actions: [{
action_type: 'FLAG',
user_id: null,
group_id: 'SUSPECT_WORD',
metadata: {}
}],
actions: [
{
action_type: 'FLAG',
user_id: null,
group_id: 'SUSPECT_WORD',
metadata: {},
},
],
};
}
},
// This phase checks to see if the comment's length exceeds maximum.
(context, comment, {assetSettings: {charCountEnable, charCount}}) => {
(context, comment, { assetSettings: { charCountEnable, charCount } }) => {
// Reject if the comment is too long
if (charCountEnable && comment.body.length > charCount) {
// Add the flag related to Trust to the comment.
return {
status: 'REJECTED',
actions: [{
action_type: 'FLAG',
user_id: null,
group_id: 'BODY_COUNT',
metadata: {
count: comment.body.length,
}
}]
actions: [
{
action_type: 'FLAG',
user_id: null,
group_id: 'BODY_COUNT',
metadata: {
count: comment.body.length,
},
},
],
};
}
},
// If a given user is a staff member, always approve their comment.
(context) => {
context => {
if (IGNORE_FLAGS_AGAINST_STAFF && context.user && context.user.isStaff()) {
return {
status: 'ACCEPTED',
@@ -309,48 +315,52 @@ const moderationPhases = [
// This phase checks the comment if it has any links in it if the check is
// enabled.
(context, comment, {assetSettings: {premodLinksEnable}}) => {
(context, comment, { assetSettings: { premodLinksEnable } }) => {
if (premodLinksEnable && linkify.test(comment.body)) {
// Add the flag related to Trust to the comment.
return {
status: 'SYSTEM_WITHHELD',
actions: [{
action_type: 'FLAG',
user_id: null,
group_id: 'LINKS',
metadata: {
links: comment.body,
}
}],
actions: [
{
action_type: 'FLAG',
user_id: null,
group_id: 'LINKS',
metadata: {
links: comment.body,
},
},
],
};
}
},
// This phase checks to see if the user making the comment is allowed to do so
// considering their reliability (Trust) status.
(context) => {
context => {
if (context.user && context.user.metadata) {
// If the user is not a reliable commenter (passed the unreliability
// threshold by having too many rejected comments) then we can change the
// status of the comment to `SYSTEM_WITHHELD`, therefore pushing the user's
// comments away from the public eye until a moderator can manage them. This of
// course can only be applied if the comment's current status is `NONE`,
// we don't want to interfere if the comment was rejected.
if (KarmaService.isReliable('comment', context.user.metadata.trust) === false) {
if (
KarmaService.isReliable('comment', context.user.metadata.trust) ===
false
) {
// Add the flag related to Trust to the comment.
return {
status: 'SYSTEM_WITHHELD',
actions: [{
action_type: 'FLAG',
user_id: null,
group_id: 'TRUST',
metadata: {
trust: context.user.metadata.trust,
}
}],
actions: [
{
action_type: 'FLAG',
user_id: null,
group_id: 'TRUST',
metadata: {
trust: context.user.metadata.trust,
},
},
],
};
}
}
@@ -358,7 +368,6 @@ const moderationPhases = [
// This phase checks to see if the comment was already prescribed a status.
(context, comment) => {
// If the status was already defined, don't redefine it. It's only defined
// when specific external conditions exist, we don't want to override that.
if (comment.status && comment.status.length > 0) {
@@ -370,8 +379,7 @@ const moderationPhases = [
// This phase checks to see if the settings have premod enabled, if they do,
// the comment is premod, otherwise, it's just none.
(context, comment, {assetSettings: {moderation}}) => {
(context, comment, { assetSettings: { moderation } }) => {
// If the settings say that we're in premod mode, then the comment is in
// premod status.
if (moderation === 'PRE') {
@@ -383,7 +391,7 @@ const moderationPhases = [
return {
status: 'NONE',
};
}
},
];
/**
@@ -395,7 +403,6 @@ const moderationPhases = [
* @return {Promise} resolves to the comment's status and actions
*/
const resolveCommentModeration = async (context, comment) => {
// First we filter the comment contents to ensure that we note any validation
// issues.
let [wordlist, settings] = await filterNewComment(context, comment);
@@ -403,7 +410,6 @@ const resolveCommentModeration = async (context, comment) => {
// Get the asset from the loader.
const asset = await context.loaders.Assets.getByID.load(comment.asset_id);
if (!asset) {
// And leave now if this asset wasn't found.
throw errors.ErrNotFound;
}
@@ -423,7 +429,6 @@ const resolveCommentModeration = async (context, comment) => {
});
if (result) {
if (result.actions) {
actions.push(...result.actions);
}
@@ -431,7 +436,7 @@ const resolveCommentModeration = async (context, comment) => {
// If this result contained a status, then we've finished resolving
// phases!
if (result.status) {
return {status: result.status, actions};
return { status: result.status, actions };
}
}
}
@@ -446,11 +451,10 @@ const resolveCommentModeration = async (context, comment) => {
* @return {Promise} resolves to a new comment
*/
const createPublicComment = async (context, comment) => {
// 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.
let {actions, status} = await resolveCommentModeration(context, comment);
let { actions, status } = await resolveCommentModeration(context, comment);
// Assign status to comment.
comment.status = status;
@@ -468,10 +472,17 @@ const createPublicComment = async (context, comment) => {
// createActions will for each of the provided actions, create the given action
// on the comment at the same time using Promise.all.
const createActions = async (item_id, actions = []) => Promise.all(actions.map((action) => merge(action, {
item_id,
item_type: 'COMMENTS',
})).map((action) => ActionsService.create(action)));
const createActions = async (item_id, actions = []) =>
Promise.all(
actions
.map(action =>
merge(action, {
item_id,
item_type: 'COMMENTS',
})
)
.map(action => ActionsService.create(action))
);
/**
* Sets the status of a comment
@@ -480,8 +491,12 @@ const createActions = async (item_id, actions = []) => Promise.all(actions.map((
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
*/
const setStatus = async ({user, loaders: {Comments}}, {id, status}) => {
let comment = await CommentsService.pushStatus(id, status, user ? user.id : null);
const setStatus = async ({ user, loaders: { Comments } }, { id, status }) => {
let comment = await CommentsService.pushStatus(
id,
status,
user ? user.id : null
);
// If the loaders are present, clear the caches for these values because we
// just added a new comment, hence the counts should be updated. It would
@@ -507,17 +522,21 @@ const setStatus = async ({user, loaders: {Comments}}, {id, status}) => {
* @param {Object} edit describes how to edit the comment
* @param {String} edit.body the new Comment body
*/
const edit = async (context, {id, asset_id, edit: {body}}) => {
const edit = async (context, { id, asset_id, edit: { body } }) => {
// Build up the new comment we're setting. We need to check this with
// moderation now.
let comment = {id, asset_id, body};
let comment = { id, asset_id, body };
// Determine the new status of the comment.
const {actions, status} = await resolveCommentModeration(context, comment);
const { actions, status } = await resolveCommentModeration(context, comment);
// Execute the edit.
comment = await CommentsService.edit({id, author_id: context.user.id, body, status});
comment = await CommentsService.edit({
id,
author_id: context.user.id,
body,
status,
});
// Create all the actions that were determined during the moderation check
// phase.
@@ -529,25 +548,25 @@ const edit = async (context, {id, asset_id, edit: {body}}) => {
return comment;
};
module.exports = (context) => {
module.exports = context => {
let mutators = {
Comment: {
create: () => Promise.reject(errors.ErrNotAuthorized),
setStatus: () => Promise.reject(errors.ErrNotAuthorized),
edit: () => Promise.reject(errors.ErrNotAuthorized)
}
edit: () => Promise.reject(errors.ErrNotAuthorized),
},
};
if (context.user && context.user.can(CREATE_COMMENT)) {
mutators.Comment.create = (comment) => createPublicComment(context, comment);
mutators.Comment.create = comment => createPublicComment(context, comment);
}
if (context.user && context.user.can(SET_COMMENT_STATUS)) {
mutators.Comment.setStatus = (action) => setStatus(context, action);
mutators.Comment.setStatus = action => setStatus(context, action);
}
if (context.user && context.user.can(EDIT_COMMENT)) {
mutators.Comment.edit = (action) => edit(context, action);
mutators.Comment.edit = action => edit(context, action);
}
return mutators;
+11 -13
View File
@@ -12,7 +12,6 @@ const User = require('./user');
const plugins = require('../../services/plugins');
let mutators = [
// Load in the core mutators.
Comment,
Action,
@@ -23,12 +22,11 @@ let mutators = [
User,
// Load the plugin mutators from the manager.
...plugins
.get('server', 'mutators').map(({plugin, mutators}) => {
debug(`added plugin '${plugin.name}'`);
...plugins.get('server', 'mutators').map(({ plugin, mutators }) => {
debug(`added plugin '${plugin.name}'`);
return mutators;
})
return mutators;
}),
];
/**
@@ -36,12 +34,12 @@ let mutators = [
* @param {Object} context the context of the GraphQL request
* @return {Object} object of mutators
*/
module.exports = (context) => {
module.exports = context => {
// We need to return an object to be accessed.
return _.merge(...mutators.map((mutators) => {
// Each set of mutators is a function which takes the context.
return mutators(context);
}));
return _.merge(
...mutators.map(mutators => {
// Each set of mutators is a function which takes the context.
return mutators(context);
})
);
};
+3 -5
View File
@@ -1,18 +1,16 @@
const errors = require('../../errors');
const {
UPDATE_SETTINGS,
} = require('../../perms/constants');
const { UPDATE_SETTINGS } = require('../../perms/constants');
const SettingsService = require('../../services/settings');
const update = async (ctx, settings) => SettingsService.update(settings);
module.exports = (ctx) => {
module.exports = ctx => {
let mutators = {
Settings: {
update: () => Promise.reject(errors.ErrNotAuthorized),
}
},
};
if (ctx.user) {
+19 -10
View File
@@ -1,38 +1,47 @@
const TagsService = require('../../services/tags');
const errors = require('../../errors');
const {ADD_COMMENT_TAG, REMOVE_COMMENT_TAG} = require('../../perms/constants');
const {
ADD_COMMENT_TAG,
REMOVE_COMMENT_TAG,
} = require('../../perms/constants');
/**
* Modifies the targeted model with the specified operation to add/remove a tag.
*/
const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type, asset_id}) => {
const modify = async (
{ user, loaders: { Tags } },
operation,
{ 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});
const tags = await Tags.getAll.load({ id, item_type, asset_id });
// Resolve the TagLink that should be used to insert to the user. This will
// additionally 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});
let { tagLink, ownership } = TagsService.resolveLink(user, tags, {
name,
item_type,
});
// Actually modify the tag on the model.
return operation(id, item_type, tagLink, ownership);
};
module.exports = (context) => {
module.exports = context => {
let mutators = {
Tag: {
add: () => Promise.reject(errors.ErrNotAuthorized),
remove: () => Promise.reject(errors.ErrNotAuthorized)
}
remove: () => Promise.reject(errors.ErrNotAuthorized),
},
};
if (context.user && context.user.can(ADD_COMMENT_TAG)) {
mutators.Tag.add = (tag) => modify(context, TagsService.add, tag);
mutators.Tag.add = tag => modify(context, TagsService.add, tag);
}
if (context.user && context.user.can(REMOVE_COMMENT_TAG)) {
mutators.Tag.remove = (tag) => modify(context, TagsService.remove, tag);
mutators.Tag.remove = tag => modify(context, TagsService.remove, tag);
}
return mutators;
+9 -12
View File
@@ -1,13 +1,10 @@
const errors = require('../../errors');
const TokensService = require('../../services/tokens');
const {
CREATE_TOKEN,
REVOKE_TOKEN
} = require('../../perms/constants');
const { CREATE_TOKEN, REVOKE_TOKEN } = require('../../perms/constants');
// Creates a new token for a user.
const createToken = async ({user}, {name}) => {
let {pat, jwt} = await TokensService.create(user.id, name);
const createToken = async ({ user }, { name }) => {
let { pat, jwt } = await TokensService.create(user.id, name);
// Attach the token to the PAT.
pat.jwt = jwt;
@@ -17,24 +14,24 @@ const createToken = async ({user}, {name}) => {
};
// Revokes the token from the user.
const revokeToken = async ({user}, {id}) => {
const revokeToken = async ({ user }, { id }) => {
return TokensService.revoke(user.id, id);
};
module.exports = (context) => {
module.exports = context => {
let mutators = {
Token: {
create: () => Promise.reject(errors.ErrNotAuthorized),
revoke: () => Promise.reject(errors.ErrNotAuthorized)
}
revoke: () => Promise.reject(errors.ErrNotAuthorized),
},
};
if (context.user && context.user.can(CREATE_TOKEN)) {
mutators.Token.create = (input) => createToken(context, input);
mutators.Token.create = input => createToken(context, input);
}
if (context.user && context.user.can(REVOKE_TOKEN)) {
mutators.Token.revoke = (input) => revokeToken(context, input);
mutators.Token.revoke = input => revokeToken(context, input);
}
return mutators;
+34 -14
View File
@@ -19,24 +19,39 @@ const setUserUsernameStatus = async (ctx, id, status) => {
};
const setUserBanStatus = async (ctx, id, status = false, message = null) => {
const user = await UsersService.setBanStatus(id, status, ctx.user.id, message);
const user = await UsersService.setBanStatus(
id,
status,
ctx.user.id,
message
);
if (user.banned) {
ctx.pubsub.publish('userBanned', user);
}
};
const setUserSuspensionStatus = async (ctx, id, until = null, message = null) => {
const user = await UsersService.setSuspensionStatus(id, until, ctx.user.id, message);
const setUserSuspensionStatus = async (
ctx,
id,
until = null,
message = null
) => {
const user = await UsersService.setSuspensionStatus(
id,
until,
ctx.user.id,
message
);
if (user.suspended) {
ctx.pubsub.publish('userSuspended', user);
}
};
const ignoreUser = ({user}, userToIgnore) => {
const ignoreUser = ({ user }, userToIgnore) => {
return UsersService.ignoreUsers(user.id, [userToIgnore.id]);
};
const stopIgnoringUser = ({user}, userToStopIgnoring) => {
const stopIgnoringUser = ({ user }, userToStopIgnoring) => {
return UsersService.stopIgnoringUsers(user.id, [userToStopIgnoring.id]);
};
@@ -52,7 +67,7 @@ const setRole = (ctx, id, role) => {
return UsersService.setRole(id, role);
};
module.exports = (ctx) => {
module.exports = ctx => {
let mutators = {
User: {
changeUsername: () => Promise.reject(errors.ErrNotAuthorized),
@@ -63,35 +78,40 @@ module.exports = (ctx) => {
setUserUsernameStatus: () => Promise.reject(errors.ErrNotAuthorized),
setUsername: () => Promise.reject(errors.ErrNotAuthorized),
stopIgnoringUser: () => Promise.reject(errors.ErrNotAuthorized),
}
},
};
if (ctx.user) {
mutators.User.ignoreUser = (action) => ignoreUser(ctx, action);
mutators.User.stopIgnoringUser = (action) => stopIgnoringUser(ctx, action);
mutators.User.ignoreUser = action => ignoreUser(ctx, action);
mutators.User.stopIgnoringUser = action => stopIgnoringUser(ctx, action);
if (ctx.user.can(UPDATE_USER_ROLES)) {
mutators.User.setRole = (id, role) => setRole(ctx, id, role);
}
if (ctx.user.can(CHANGE_USERNAME)) {
mutators.User.changeUsername = (id, username) => changeUsername(ctx, id, username);
mutators.User.changeUsername = (id, username) =>
changeUsername(ctx, id, username);
}
if (ctx.user.can(SET_USERNAME)) {
mutators.User.setUsername = (id, username) => setUsername(ctx, id, username);
mutators.User.setUsername = (id, username) =>
setUsername(ctx, id, username);
}
if (ctx.user.can(SET_USER_USERNAME_STATUS)) {
mutators.User.setUserUsernameStatus = (id, status) => setUserUsernameStatus(ctx, id, status);
mutators.User.setUserUsernameStatus = (id, status) =>
setUserUsernameStatus(ctx, id, status);
}
if (ctx.user.can(SET_USER_BAN_STATUS)) {
mutators.User.setUserBanStatus = (id, status, message) => setUserBanStatus(ctx, id, status, message);
mutators.User.setUserBanStatus = (id, status, message) =>
setUserBanStatus(ctx, id, status, message);
}
if (ctx.user.can(SET_USER_SUSPENSION_STATUS)) {
mutators.User.setUserSuspensionStatus = (id, until, message) => setUserSuspensionStatus(ctx, id, until, message);
mutators.User.setUserSuspensionStatus = (id, until, message) =>
setUserSuspensionStatus(ctx, id, until, message);
}
}
+10 -8
View File
@@ -1,22 +1,24 @@
const {SEARCH_OTHER_USERS} = require('../../perms/constants');
const { SEARCH_OTHER_USERS } = require('../../perms/constants');
const Action = {
__resolveType({action_type}) {
__resolveType({ action_type }) {
switch (action_type) {
case 'DONTAGREE':
return 'DontAgreeAction';
case 'FLAG':
return 'FlagAction';
case 'DONTAGREE':
return 'DontAgreeAction';
case 'FLAG':
return 'FlagAction';
default:
return undefined;
}
},
// This will load the user for the specific action. We'll limit this to the
// admin users only or the current logged in user.
user({user_id}, _, {loaders: {Users}, user}) {
user({ user_id }, _, { loaders: { Users }, user }) {
if (user && (user.can(SEARCH_OTHER_USERS) || user_id === user.id)) {
return Users.getByID.load(user_id);
}
}
},
};
module.exports = Action;
+8 -6
View File
@@ -1,12 +1,14 @@
const ActionSummary = {
__resolveType({action_type}) {
__resolveType({ action_type }) {
switch (action_type) {
case 'FLAG':
return 'FlagActionSummary';
case 'DONTAGREE':
return 'DontAgreeActionSummary';
case 'FLAG':
return 'FlagActionSummary';
case 'DONTAGREE':
return 'DontAgreeActionSummary';
default:
return undefined;
}
}
},
};
module.exports = ActionSummary;
+11 -11
View File
@@ -1,8 +1,7 @@
const {decorateWithTags} = require('./util');
const { decorateWithTags } = require('./util');
const Asset = {
async comment({id}, {id: commentId}, {loaders: {Comments}}) {
async comment({ id }, { id: commentId }, { loaders: { Comments } }) {
// Load the comment from the database.
const comment = await Comments.get.load(commentId);
if (!comment) {
@@ -16,7 +15,7 @@ const Asset = {
return comment;
},
comments({id}, {query, deep}, {loaders: {Comments}}) {
comments({ id }, { query, deep }, { loaders: { Comments } }) {
if (!deep) {
query.parent_id = null;
}
@@ -26,14 +25,13 @@ const Asset = {
return Comments.getByQuery(query);
},
commentCount({id, commentCount}, {tags}, {loaders: {Comments}}) {
commentCount({ id, commentCount }, { tags }, { loaders: { Comments } }) {
if (commentCount != null) {
return commentCount;
}
// If we are filtering by a tag.
if (tags && tags.length > 0) {
// Then count the comments with those tags.
return Comments.getCountByQuery({
tags,
@@ -45,14 +43,17 @@ const Asset = {
return Comments.parentCountByAssetID.load(id);
},
totalCommentCount({id, totalCommentCount}, {tags}, {loaders: {Comments}}) {
totalCommentCount(
{ id, totalCommentCount },
{ tags },
{ loaders: { Comments } }
) {
if (totalCommentCount != null) {
return totalCommentCount;
}
// If we are filtering by a tag.
if (tags && tags.length > 0) {
// Then count the comments with those tags.
return Comments.getCountByQuery({
tags,
@@ -63,8 +64,7 @@ const Asset = {
return Comments.countByAssetID.load(id);
},
async settings({settings = null}, _, {loaders: {Settings}}) {
async settings({ settings = null }, _, { loaders: { Settings } }) {
// Load the global settings, and merge them into the asset specific settings
// if we have some.
let globalSettings = await Settings.load();
@@ -75,7 +75,7 @@ const Asset = {
}
return settings;
}
},
};
// Decorate the Asset type resolver with a tags field.
+8 -6
View File
@@ -1,12 +1,14 @@
const AssetActionSummary = {
__resolveType({action_type}) {
__resolveType({ action_type }) {
switch (action_type) {
case 'FLAG':
return 'FlagAssetActionSummary';
case 'LIKE':
return 'LikeAssetActionSummary';
case 'FLAG':
return 'FlagAssetActionSummary';
case 'LIKE':
return 'LikeAssetActionSummary';
default:
return undefined;
}
}
},
};
module.exports = AssetActionSummary;
+1 -1
View File
@@ -1,4 +1,4 @@
const {decorateUserField} = require('./util');
const { decorateUserField } = require('./util');
const BannedStatusHistory = {};
+15 -15
View File
@@ -1,21 +1,20 @@
const {decorateWithTags} = require('./util');
const { decorateWithTags } = require('./util');
const Comment = {
hasParent({parent_id}) {
hasParent({ parent_id }) {
return !!parent_id;
},
parent({parent_id}, _, {loaders: {Comments}}) {
parent({ parent_id }, _, { loaders: { Comments } }) {
if (parent_id == null) {
return null;
}
return Comments.get.load(parent_id);
},
user({author_id}, _, {loaders: {Users}}) {
user({ author_id }, _, { loaders: { Users } }) {
return Users.getByID.load(author_id);
},
replies({id, asset_id, reply_count}, {query}, {loaders: {Comments}}) {
replies({ id, asset_id, reply_count }, { query }, { loaders: { Comments } }) {
// Don't bother looking up replies if there aren't any there!
if (reply_count === 0) {
return {
@@ -30,36 +29,37 @@ const Comment = {
return Comments.getByQuery(query);
},
replyCount({reply_count}) {
replyCount({ reply_count }) {
// A simple remap from the underlying database model to the graph model.
return reply_count;
},
actions({id}, _, {user, loaders: {Actions}}) {
actions({ id }, _, { user, loaders: { Actions } }) {
if (!user || !user.can('SEARCH_ACTIONS')) {
return null;
}
return Actions.getByID.load(id);
},
action_summaries({id, action_summaries}, _, {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}}) {
asset({ asset_id }, _, { loaders: { Assets } }) {
return Assets.getByID.load(asset_id);
},
async editing(comment, _, {loaders: {Settings}}) {
async editing(comment, _, { loaders: { Settings } }) {
const settings = await Settings.load();
const editableUntil = new Date(Number(new Date(comment.created_at)) + settings.editCommentWindowLength);
const editableUntil = new Date(
Number(new Date(comment.created_at)) + settings.editCommentWindowLength
);
return {
edited: comment.edited,
editableUntil: editableUntil
editableUntil: editableUntil,
};
}
},
};
// Decorate the Comment type resolver with a tags field.
+1 -1
View File
@@ -1,4 +1,4 @@
const {decorateUserField} = require('./util');
const { decorateUserField } = require('./util');
const CommentStatusHistory = {};
+9 -10
View File
@@ -20,16 +20,15 @@ module.exports = new GraphQLScalarType({
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.STRING:
// This handles an empty string.
if (ast.value && ast.value.length === 0) {
return null;
}
// This handles an empty string.
if (ast.value && ast.value.length === 0) {
return null;
}
return new Date(ast.value);
default:
return ast.value;
return new Date(ast.value);
default:
return ast.value;
}
}
},
});
+8 -9
View File
@@ -16,16 +16,15 @@ module.exports = new GraphQLScalarType({
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.STRING:
// This handles an empty string.
if (ast.value && ast.value.length === 0) {
return null;
}
// This handles an empty string.
if (ast.value && ast.value.length === 0) {
return new Date(ast.value);
default:
return null;
}
return new Date(ast.value);
default:
return null;
}
}
},
});
+3 -4
View File
@@ -1,13 +1,12 @@
const FlagAction = {
// Stored in the metadata, extract and return.
message({metadata: {message}}) {
message({ metadata: { message } }) {
return message;
},
reason({group_id}) {
reason({ group_id }) {
return group_id;
},
user({user_id}, _, {loaders: {Users}}) {
user({ user_id }, _, { loaders: { Users } }) {
if (!user_id) {
return null;
}
+2 -2
View File
@@ -1,7 +1,7 @@
const FlagActionSummary = {
reason({group_id}) {
reason({ group_id }) {
return group_id;
}
},
};
module.exports = FlagActionSummary;
+6 -4
View File
@@ -65,10 +65,12 @@ let resolvers = {
* plugin based ones. This allows plugins to extend existing resolvers as well
* as provide new ones.
*/
resolvers = plugins.get('server', 'resolvers').reduce((acc, {plugin, resolvers}) => {
debug(`added plugin '${plugin.name}'`);
resolvers = plugins
.get('server', 'resolvers')
.reduce((acc, { plugin, resolvers }) => {
debug(`added plugin '${plugin.name}'`);
return _.merge(acc, resolvers);
}, resolvers);
return _.merge(acc, resolvers);
}, resolvers);
module.exports = resolvers;
+82 -35
View File
@@ -1,65 +1,108 @@
const RootMutation = {
createComment: async (_, {input}, {mutators: {Comment}, loaders: {Actions}}) => {
createComment: async (
_,
{ input },
{ mutators: { Comment }, loaders: { Actions } }
) => {
const comment = await Comment.create(input);
// Retrieve actions that was assigned to comment.
const actions = await Actions.getByID.load(comment.id);
return {comment, actions};
return { comment, actions };
},
editComment: async (_, {id, asset_id, edit: {body}}, {mutators: {Comment}}) => ({
comment: await Comment.edit({id, asset_id, edit: {body}}),
editComment: async (
_,
{ id, asset_id, edit: { body } },
{ mutators: { Comment } }
) => ({
comment: await Comment.edit({ id, asset_id, edit: { body } }),
}),
createFlag: async (_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) => ({
flag: Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}),
createFlag: async (
_,
{ flag: { item_id, item_type, reason, message } },
{ mutators: { Action } }
) => ({
flag: Action.create({
item_id,
item_type,
action_type: 'FLAG',
group_id: reason,
metadata: { message },
}),
}),
createDontAgree: async (_, {dontagree: {item_id, item_type, message}}, {mutators: {Action}}) => ({
dontagree: await Action.create({item_id, item_type, action_type: 'DONTAGREE', metadata: {message}}),
createDontAgree: async (
_,
{ dontagree: { item_id, item_type, message } },
{ mutators: { Action } }
) => ({
dontagree: await Action.create({
item_id,
item_type,
action_type: 'DONTAGREE',
metadata: { message },
}),
}),
deleteAction: async (_, {id}, {mutators: {Action}}) => {
await Action.delete({id});
deleteAction: async (_, { id }, { mutators: { Action } }) => {
await Action.delete({ id });
},
approveUsername: async (_, {id}, {mutators: {User}}) => {
approveUsername: async (_, { id }, { mutators: { User } }) => {
await User.setUserUsernameStatus(id, 'APPROVED');
},
rejectUsername: async (_, {id}, {mutators: {User}}) => {
rejectUsername: async (_, { id }, { mutators: { User } }) => {
await User.setUserUsernameStatus(id, 'REJECTED');
},
changeUsername: async (_, {id, username}, {mutators: {User}}) => {
changeUsername: async (_, { id, username }, { mutators: { User } }) => {
await User.changeUsername(id, username);
},
setUsername: async (_, {id, username}, {mutators: {User}}) => {
setUsername: async (_, { id, username }, { mutators: { User } }) => {
await User.setUsername(id, username);
},
suspendUser: async (obj, {input: {id, until, message}}, {mutators: {User}}) => {
suspendUser: async (
obj,
{ input: { id, until, message } },
{ mutators: { User } }
) => {
await User.setUserSuspensionStatus(id, until, message);
},
unsuspendUser: async (obj, {input: {id}}, {mutators: {User}}) => {
await User.setUserSuspensionStatus(id);
unsuspendUser: async (obj, { input: { id } }, { mutators: { User } }) => {
await User.setUserSuspensionStatus(id);
},
banUser: async (obj, {input: {id, message}}, {mutators: {User}}) => {
banUser: async (obj, { input: { id, message } }, { mutators: { User } }) => {
await User.setUserBanStatus(id, true, message);
},
unbanUser: async (obj, {input: {id}}, {mutators: {User}}) => {
await User.setUserBanStatus(id, false);
unbanUser: async (obj, { input: { id } }, { mutators: { User } }) => {
await User.setUserBanStatus(id, false);
},
ignoreUser: async (_, {id}, {mutators: {User}}) => {
await User.ignoreUser({id});
ignoreUser: async (_, { id }, { mutators: { User } }) => {
await User.ignoreUser({ id });
},
stopIgnoringUser: async (_, {id}, {mutators: {User}}) => {
await User.stopIgnoringUser({id});
stopIgnoringUser: async (_, { id }, { mutators: { User } }) => {
await User.stopIgnoringUser({ id });
},
updateAssetSettings: async (_, {id, input: settings}, {mutators: {Asset}}) => {
updateAssetSettings: async (
_,
{ id, input: settings },
{ mutators: { Asset } }
) => {
await Asset.updateSettings(id, settings);
},
updateAssetStatus: async (_, {id, input: status}, {mutators: {Asset}}) => {
updateAssetStatus: async (
_,
{ id, input: status },
{ mutators: { Asset } }
) => {
await Asset.updateStatus(id, status);
},
setUserRole: async (_, {id, role}, {mutators: {User}}) => {
setUserRole: async (_, { id, role }, { mutators: { User } }) => {
await User.setRole(id, role);
},
setCommentStatus: async (_, {id, status}, {mutators: {Comment}, pubsub}) => {
const comment = await Comment.setStatus({id, status});
setCommentStatus: async (
_,
{ id, status },
{ mutators: { Comment }, pubsub }
) => {
const comment = await Comment.setStatus({ id, status });
if (status === 'ACCEPTED') {
pubsub.publish('commentAccepted', comment);
} else if (status === 'REJECTED') {
@@ -68,21 +111,25 @@ const RootMutation = {
pubsub.publish('commentReset', comment);
}
},
addTag: async (_, {tag}, {mutators: {Tag}}) => {
addTag: async (_, { tag }, { mutators: { Tag } }) => {
await Tag.add(tag);
},
removeTag: async (_, {tag}, {mutators: {Tag}}) => {
removeTag: async (_, { tag }, { mutators: { Tag } }) => {
await Tag.remove(tag);
},
updateSettings: async (_, {input: settings}, {mutators: {Settings}}) => {
updateSettings: async (
_,
{ input: settings },
{ mutators: { Settings } }
) => {
await Settings.update(settings);
},
createToken: async (_, {input}, {mutators: {Token}}) => ({
createToken: async (_, { input }, { mutators: { Token } }) => ({
token: await Token.create(input),
}),
revokeToken: async (_, {input}, {mutators: {Token}}) => {
revokeToken: async (_, { input }, { mutators: { Token } }) => {
await Token.revoke(input);
}
},
};
module.exports = RootMutation;
+18 -14
View File
@@ -1,45 +1,49 @@
const {
SEARCH_ASSETS,
SEARCH_OTHERS_COMMENTS,
SEARCH_OTHER_USERS
SEARCH_OTHER_USERS,
} = require('../../perms/constants');
const RootQuery = {
assets(_, {query}, {loaders: {Assets}, user}) {
assets(_, { query }, { loaders: { Assets }, user }) {
if (user == null || !user.can(SEARCH_ASSETS)) {
return null;
}
return Assets.getByQuery(query);
},
asset(_, query, {loaders: {Assets}}) {
asset(_, query, { loaders: { Assets } }) {
if (query.id) {
return Assets.getByID.load(query.id);
}
return Assets.getByURL(query.url);
},
settings(_, args, {loaders: {Settings}}) {
settings(_, args, { loaders: { Settings } }) {
return Settings.load();
},
// This endpoint is used for loading moderation queues, so hide it in the
// event that we aren't an admin.
async comments(_, {query}, {loaders: {Comments}}) {
async comments(_, { query }, { loaders: { Comments } }) {
return Comments.getByQuery(query);
},
comment(_, {id}, {loaders: {Comments}}) {
comment(_, { id }, { loaders: { Comments } }) {
return Comments.get.load(id);
},
async commentCount(_, {query}, {user, loaders: {Comments, Assets}}) {
async commentCount(_, { query }, { user, loaders: { Comments, Assets } }) {
if (user == null || !user.can(SEARCH_OTHERS_COMMENTS)) {
return null;
}
const {asset_url, asset_id} = query;
if ((!asset_id || asset_id.length === 0) && asset_url && asset_url.length > 0) {
const { asset_url, asset_id } = query;
if (
(!asset_id || asset_id.length === 0) &&
asset_url &&
asset_url.length > 0
) {
let asset = await Assets.findByUrl(asset_url);
if (asset) {
query.asset_id = asset.id;
@@ -49,7 +53,7 @@ const RootQuery = {
return Comments.getCountByQuery(query);
},
async userCount(_, {query}, {user, loaders: {Users}}) {
async userCount(_, { query }, { user, loaders: { Users } }) {
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
return null;
}
@@ -59,7 +63,7 @@ const RootQuery = {
// This returns the current user, ensure that if we aren't logged in, we
// return null.
me(_, args, {user}) {
me(_, args, { user }) {
if (user == null) {
return null;
}
@@ -68,7 +72,7 @@ const RootQuery = {
},
// this returns an arbitrary user
user(_, {id}, {user, loaders: {Users}}) {
user(_, { id }, { user, loaders: { Users } }) {
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
return null;
}
@@ -78,13 +82,13 @@ const RootQuery = {
// This endpoint is used for loading the user moderation queues (users whose username has been flagged),
// so hide it in the event that we aren't an admin.
users(_, {query}, {user, loaders: {Users}}) {
users(_, { query }, { user, loaders: { Users } }) {
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
return null;
}
return Users.getByQuery(query);
}
},
};
module.exports = RootQuery;
+6 -8
View File
@@ -1,18 +1,16 @@
const {
VIEW_PROTECTED_SETTINGS,
} = require('../../perms/constants');
const { VIEW_PROTECTED_SETTINGS } = require('../../perms/constants');
const {decorateWithPermissionCheck} = require('./util');
const { decorateWithPermissionCheck } = require('./util');
const Settings = {};
// PROTECTED_SETTINGS are the settings keys that must be protected for only some
// eyes.
const PROTECTED_SETTINGS = {
'premodLinksEnable': [VIEW_PROTECTED_SETTINGS],
'autoCloseStream': [VIEW_PROTECTED_SETTINGS],
'wordlist': [VIEW_PROTECTED_SETTINGS],
'domains': [VIEW_PROTECTED_SETTINGS],
premodLinksEnable: [VIEW_PROTECTED_SETTINGS],
autoCloseStream: [VIEW_PROTECTED_SETTINGS],
wordlist: [VIEW_PROTECTED_SETTINGS],
domains: [VIEW_PROTECTED_SETTINGS],
};
// decorate the fields on the settings resolver with a permission check.
+1 -1
View File
@@ -1,4 +1,4 @@
const {decorateUserField} = require('./util');
const { decorateUserField } = require('./util');
const SuspensionStatusHistory = {};
+1 -3
View File
@@ -1,5 +1,3 @@
const Tag = {
};
const Tag = {};
module.exports = Tag;
+3 -3
View File
@@ -1,11 +1,11 @@
const {SEARCH_OTHER_USERS} = require('../../perms/constants');
const { SEARCH_OTHER_USERS } = require('../../perms/constants');
const TagLink = {
assigned_by({assigned_by}, _, {user, loaders: {Users}}) {
assigned_by({ assigned_by }, _, { user, loaders: { Users } }) {
if (user && user.can(SEARCH_OTHER_USERS) && assigned_by != null) {
return Users.getByID.load(assigned_by);
}
}
},
};
module.exports = TagLink;
+16 -19
View File
@@ -1,4 +1,4 @@
const {decorateWithTags} = require('./util');
const { decorateWithTags } = require('./util');
const KarmaService = require('../../services/karma');
const {
SEARCH_ACTIONS,
@@ -10,19 +10,16 @@ const {
} = require('../../perms/constants');
const User = {
action_summaries({id}, _, {loaders: {Actions}}) {
action_summaries({ id }, _, { loaders: { Actions } }) {
return Actions.getSummariesByItemID.load(id);
},
actions({id}, _, {user, loaders: {Actions}}) {
actions({ id }, _, { user, loaders: { Actions } }) {
// Only return the actions if the user is not an admin.
if (user && user.can(SEARCH_ACTIONS)) {
return Actions.getByID.load(id);
}
},
comments({id}, {query}, {loaders: {Comments}, user}) {
comments({ id }, { query }, { loaders: { Comments }, user }) {
// If there is no user, or there is a user, but they are requesting someone
// else's comments, and they aren't allowed, don't return then anything!
if (!user || (user.id !== id && !user.can(SEARCH_OTHERS_COMMENTS))) {
@@ -34,8 +31,7 @@ const User = {
return Comments.getByQuery(query);
},
profiles({profiles}, _, {user}) {
profiles({ profiles }, _, { user }) {
// if the user is not an admin, do not return the profiles
if (user && user.can(SEARCH_OTHER_USERS)) {
return profiles;
@@ -43,18 +39,17 @@ const User = {
return null;
},
tokens({id, tokens}, args, {user}) {
if (!user || ((user.id !== id) && !user.can(LIST_OWN_TOKENS))) {
tokens({ id, tokens }, args, { user }) {
if (!user || (user.id !== id && !user.can(LIST_OWN_TOKENS))) {
return null;
}
return tokens;
},
ignoredUsers({id}, args, {user, loaders: {Users}}) {
ignoredUsers({ id }, args, { user, loaders: { Users } }) {
// Only allow a logged in user that is either the current user or is a staff
// member to access the ignoredUsers of a given user.
if (!user || ((user.id !== id) && !user.can(SEARCH_OTHER_USERS))) {
if (!user || (user.id !== id && !user.can(SEARCH_OTHER_USERS))) {
return null;
}
@@ -65,8 +60,7 @@ const User = {
return Users.getByID.loadMany(user.ignoresUsers);
},
role({id, role}, _, {user}) {
role({ id, role }, _, { user }) {
// If the user is not an admin, only return the current user's roles.
if (user && (user.can(VIEW_USER_ROLE) || user.id === id)) {
return role;
@@ -76,17 +70,20 @@ const User = {
},
// Extract the reliability from the user metadata if they have permission.
reliable(user, _, {user: requestingUser}) {
reliable(user, _, { user: requestingUser }) {
if (requestingUser && requestingUser.can(SEARCH_ACTIONS)) {
return KarmaService.model(user);
}
},
state(user, args, ctx) {
if (ctx.user && (ctx.user.id === user.id || ctx.user.can(VIEW_USER_STATUS))) {
if (
ctx.user &&
(ctx.user.id === user.id || ctx.user.can(VIEW_USER_STATUS))
) {
return user;
}
}
},
};
// Decorate the User type resolver with a tags field.
+2 -2
View File
@@ -1,11 +1,11 @@
const UserError = {
__resolveType({field_name}) {
__resolveType({ field_name }) {
if (field_name) {
return 'ValidationUserError';
}
return 'GenericUserError';
}
},
};
module.exports = UserError;
+6 -5
View File
@@ -1,13 +1,14 @@
const {
VIEW_USER_STATUS
} = require('../../perms/constants');
const { VIEW_USER_STATUS } = require('../../perms/constants');
const UserState = {
status: (user, args, ctx) => {
if (ctx.user && (ctx.user.id === user.id || ctx.user.can(VIEW_USER_STATUS))) {
if (
ctx.user &&
(ctx.user.id === user.id || ctx.user.can(VIEW_USER_STATUS))
) {
return user.status;
}
}
},
};
module.exports = UserState;
+1 -1
View File
@@ -1,4 +1,4 @@
const {decorateUserField} = require('./util');
const { decorateUserField } = require('./util');
const UsernameStatusHistory = {};
+3 -4
View File
@@ -7,13 +7,13 @@ const property = require('lodash/property');
/**
* Decorates the typeResolver with the tags field.
*/
const decorateWithTags = (typeResolver) => {
typeResolver.tags = ({tags = []}, _, {user}) => {
const decorateWithTags = typeResolver => {
typeResolver.tags = ({ tags = [] }, _, { user }) => {
if (user && user.can(ADD_COMMENT_TAG)) {
return tags;
}
return tags.filter((t) => t.tag.permissions.public);
return tags.filter(t => t.tag.permissions.public);
};
};
@@ -49,7 +49,6 @@ const decorateWithPermissionCheck = (typeResolver, protect) => {
* @param {String} field the field to decorate
*/
const decorateUserField = (typeResolver, field) => {
// The default resolver for the user decorator is loading the user by id.
let fieldResolver = (obj, args, ctx) =>
ctx.loaders.Users.getByID.load(obj[field]);
+9 -7
View File
@@ -3,14 +3,14 @@ const {
addSchemaLevelResolveFunction,
} = require('graphql-tools');
const debug = require('debug')('talk:graph:schema');
const {decorateWithHooks} = require('./hooks');
const {decorateWithErrorHandler} = require('./errorHandler');
const { decorateWithHooks } = require('./hooks');
const { decorateWithErrorHandler } = require('./errorHandler');
const plugins = require('../services/plugins');
const resolvers = require('./resolvers');
const typeDefs = require('./typeDefs');
const schema = makeExecutableSchema({typeDefs, resolvers});
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Plugin to the schema level resolvers to provide an before/after hook.
decorateWithHooks(schema, plugins.get('server', 'hooks'));
@@ -19,10 +19,12 @@ decorateWithHooks(schema, plugins.get('server', 'hooks'));
decorateWithErrorHandler(schema);
// For each schemaLevelResolveFunction, add it to the schema.
plugins.get('server', 'schemaLevelResolveFunction').forEach(({plugin, schemaLevelResolveFunction}) => {
debug(`added schemaLevelResolveFunction from plugin '${plugin.name}'`);
plugins
.get('server', 'schemaLevelResolveFunction')
.forEach(({ plugin, schemaLevelResolveFunction }) => {
debug(`added schemaLevelResolveFunction from plugin '${plugin.name}'`);
addSchemaLevelResolveFunction(schema, schemaLevelResolveFunction);
});
addSchemaLevelResolveFunction(schema, schemaLevelResolveFunction);
});
module.exports = schema;
+41 -28
View File
@@ -17,17 +17,20 @@ const plugins = require('../services/plugins');
const setupFunctions = {
commentAdded: (options, args, comment, context) => {
// Only privileged users can subscribe to all assets.
if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))) {
if (
!args.asset_id &&
(!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))
) {
return false;
}
// If user subscribes for statuses other than NONE and/or ACCEPTED statuses, it needs
// special privileges.
if (
(!args.statuses || args.statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
(!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))
(!args.statuses ||
args.statuses.some(status => !['NONE', 'ACCEPTED'].includes(status))) &&
(!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))
) {
return false;
}
@@ -43,7 +46,10 @@ const setupFunctions = {
return true;
},
commentEdited: (options, args, comment, context) => {
if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_EDITED))) {
if (
!args.asset_id &&
(!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_EDITED))
) {
return false;
}
return !args.asset_id || comment.asset_id === args.asset_id;
@@ -74,8 +80,9 @@ const setupFunctions = {
},
userSuspended: (options, args, user, context) => {
if (
!context.user
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED)
!context.user ||
(args.user_id !== user.id &&
!context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED))
) {
return false;
}
@@ -83,8 +90,8 @@ const setupFunctions = {
},
userBanned: (options, args, user, context) => {
if (
!context.user
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED)
!context.user ||
(args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED))
) {
return false;
}
@@ -92,8 +99,9 @@ const setupFunctions = {
},
usernameRejected: (options, args, user, context) => {
if (
!context.user
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED)
!context.user ||
(args.user_id !== user.id &&
!context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED))
) {
return false;
}
@@ -101,8 +109,9 @@ const setupFunctions = {
},
usernameApproved: (options, args, user, context) => {
if (
!context.user
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_APPROVED)
!context.user ||
(args.user_id !== user.id &&
!context.user.can(SUBSCRIBE_ALL_USERNAME_APPROVED))
) {
return false;
}
@@ -116,21 +125,25 @@ const setupFunctions = {
* well as provide new ones. We'll remap our internal representation of the
* setupFunctions into the format needed by Apollo.
*/
module.exports = plugins.get('server', 'setupFunctions').reduce((acc, {plugin, setupFunctions}) => {
debug(`added plugin '${plugin.name}'`);
module.exports = plugins.get('server', 'setupFunctions').reduce(
(acc, { plugin, setupFunctions }) => {
debug(`added plugin '${plugin.name}'`);
return merge(acc, setupFunctions);
}, Object.keys(setupFunctions).map((key) => {
const filter = setupFunctions[key];
return merge(acc, setupFunctions);
},
Object.keys(setupFunctions)
.map(key => {
const filter = setupFunctions[key];
return {
[key]: (options, args) => ({
[key]: {
filter: (user, ctx) => filter(options, args, user, ctx)
}
return {
[key]: (options, args) => ({
[key]: {
filter: (user, ctx) => filter(options, args, user, ctx),
},
}),
};
})
};
})
.reduce((setupFunction, setupFunctions) => {
return merge(setupFunctions, setupFunction);
}, {}));
.reduce((setupFunction, setupFunctions) => {
return merge(setupFunctions, setupFunction);
}, {})
);
+58 -45
View File
@@ -1,5 +1,5 @@
const {SubscriptionManager} = require('graphql-subscriptions');
const {SubscriptionServer} = require('subscriptions-transport-ws');
const { SubscriptionManager } = require('graphql-subscriptions');
const { SubscriptionServer } = require('subscriptions-transport-ws');
const debug = require('debug')('talk:graph:subscriptions');
const pubsub = require('../services/pubsub');
@@ -7,56 +7,65 @@ const schema = require('./schema');
const Context = require('./context');
const plugins = require('../services/plugins');
const {deserializeUser} = require('../services/subscriptions');
const { deserializeUser } = require('../services/subscriptions');
const setupFunctions = require('./setupFunctions');
const ms = require('ms');
const {
KEEP_ALIVE
} = require('../config');
const { KEEP_ALIVE } = require('../config');
const {BASE_PATH} = require('../url');
const { BASE_PATH } = require('../url');
// Collect all the plugin hooks that should be executed onConnect and
// onDisconnect.
const hooks = plugins.get('server', 'websockets')
.map(({plugin, websockets}) => {
debug(`added websocket hooks ${Object.keys(websockets)} from plugin '${plugin.name}'`);
const hooks = plugins
.get('server', 'websockets')
.map(({ plugin, websockets }) => {
debug(
`added websocket hooks ${Object.keys(websockets)} from plugin '${
plugin.name
}'`
);
return websockets;
})
.reduce((hooks, {onConnect = null, onDisconnect = null}) => {
if (onConnect) {
hooks.onConnect.push(onConnect);
}
.reduce(
(hooks, { onConnect = null, onDisconnect = null }) => {
if (onConnect) {
hooks.onConnect.push(onConnect);
}
if (onDisconnect) {
hooks.onDisconnect.push(onDisconnect);
}
if (onDisconnect) {
hooks.onDisconnect.push(onDisconnect);
}
return hooks;
}, {
onConnect: [],
onDisconnect: [],
});
return hooks;
},
{
onConnect: [],
onDisconnect: [],
}
);
const onConnect = async (connectionParams, connection) => {
// Attach the token from the connection options if it was provided.
if (connectionParams.token) {
debug('token sent via onConnect, attaching to the headers of the upgrade request');
debug(
'token sent via onConnect, attaching to the headers of the upgrade request'
);
// Attach it to the upgrade request.
connection.upgradeReq.headers['authorization'] = `Bearer ${connectionParams.token}`;
connection.upgradeReq.headers['authorization'] = `Bearer ${
connectionParams.token
}`;
}
// Call all the hooks.
await Promise.all(hooks.onConnect.map((hook) => hook(connectionParams, connection)));
await Promise.all(
hooks.onConnect.map(hook => hook(connectionParams, connection))
);
};
const onOperation = (parsedMessage, baseParams, connection) => {
// Cache the upgrade request.
let upgradeReq = connection.upgradeReq;
@@ -79,27 +88,31 @@ const onOperation = (parsedMessage, baseParams, connection) => {
return baseParams;
};
const onDisconnect = (connection) =>
Promise.all(hooks.onDisconnect.map((hook) => hook(connection)));
const onDisconnect = connection =>
Promise.all(hooks.onDisconnect.map(hook => hook(connection)));
/**
* This creates a new subscription manager.
*/
const createSubscriptionManager = (server) => new SubscriptionServer({
subscriptionManager: new SubscriptionManager({
schema,
pubsub: pubsub.getClient(),
setupFunctions,
}),
onConnect,
onDisconnect,
onOperation,
keepAlive: ms(KEEP_ALIVE)
}, {
server,
path: `${BASE_PATH}api/v1/live`
});
const createSubscriptionManager = server =>
new SubscriptionServer(
{
subscriptionManager: new SubscriptionManager({
schema,
pubsub: pubsub.getClient(),
setupFunctions,
}),
onConnect,
onDisconnect,
onOperation,
keepAlive: ms(KEEP_ALIVE),
},
{
server,
path: `${BASE_PATH}api/v1/live`,
}
);
module.exports = {
createSubscriptionManager
createSubscriptionManager,
};
+3 -4
View File
@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const {mergeStrings} = require('gql-merge');
const { mergeStrings } = require('gql-merge');
const debug = require('debug')('talk:graph:typeDefs');
const plugins = require('../services/plugins');
@@ -14,16 +14,15 @@ const plugins = require('../services/plugins');
* available graph.
*/
const typeDefs = mergeStrings([
// Load the core graph definitions from the filesystem.
fs.readFileSync(path.join(__dirname, 'typeDefs.graphql'), 'utf8'),
// Load the plugin definitions from the manager.
...plugins.get('server', 'typeDefs').map(({plugin, typeDefs}) => {
...plugins.get('server', 'typeDefs').map(({ plugin, typeDefs }) => {
debug(`added plugin '${plugin.name}'`);
return typeDefs;
})
}),
]);
module.exports = typeDefs;
+8 -9
View File
@@ -1,7 +1,4 @@
const {
GraphQLObjectType,
GraphQLInterfaceType
} = require('graphql');
const { GraphQLObjectType, GraphQLInterfaceType } = require('graphql');
/**
* Iterates over each field in a schema.
@@ -18,14 +15,16 @@ const {
* @return {void}
*/
const forEachField = (schema, fn, options = {}) => {
const {includeResolveType = false} = options;
const { includeResolveType = false } = options;
const typeMap = schema.getTypeMap();
Object.keys(typeMap).forEach((typeName) => {
Object.keys(typeMap).forEach(typeName => {
const type = typeMap[typeName];
if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) {
if (
type instanceof GraphQLObjectType ||
type instanceof GraphQLInterfaceType
) {
// Here we capture the change to extract the resolve type. We pass this
// with the `isResolveType = true` to introduce the specific beheviour.
if (includeResolveType && 'resolveType' in type) {
@@ -33,7 +32,7 @@ const forEachField = (schema, fn, options = {}) => {
}
const fields = type.getFields();
Object.keys(fields).forEach((fieldName) => {
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
fn(field, typeName, fieldName);
});