mirror of
https://github.com/wassname/talk.git
synced 2026-08-13 12:40:11 +08:00
replaced eslint:recommended with prettier
This commit is contained in:
+8
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
const {decorateUserField} = require('./util');
|
||||
const { decorateUserField } = require('./util');
|
||||
|
||||
const BannedStatusHistory = {};
|
||||
|
||||
|
||||
+15
-15
@@ -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,4 +1,4 @@
|
||||
const {decorateUserField} = require('./util');
|
||||
const { decorateUserField } = require('./util');
|
||||
|
||||
const CommentStatusHistory = {};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const FlagActionSummary = {
|
||||
reason({group_id}) {
|
||||
reason({ group_id }) {
|
||||
return group_id;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = FlagActionSummary;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
const {decorateUserField} = require('./util');
|
||||
const { decorateUserField } = require('./util');
|
||||
|
||||
const SuspensionStatusHistory = {};
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const Tag = {
|
||||
|
||||
};
|
||||
const Tag = {};
|
||||
|
||||
module.exports = Tag;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const UserError = {
|
||||
__resolveType({field_name}) {
|
||||
__resolveType({ field_name }) {
|
||||
if (field_name) {
|
||||
return 'ValidationUserError';
|
||||
}
|
||||
|
||||
return 'GenericUserError';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = UserError;
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
const {decorateUserField} = require('./util');
|
||||
const { decorateUserField } = require('./util');
|
||||
|
||||
const UsernameStatusHistory = {};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user