refactored resolvers, cleaned

This commit is contained in:
Wyatt Johnson
2018-02-09 18:00:12 -07:00
parent b758dc91cb
commit eab1e6ca59
19 changed files with 387 additions and 228 deletions
+2
View File
@@ -9,6 +9,7 @@ const subscriptions = require('./subscriptions');
const resolvers = require('./resolvers');
const mutators = require('./mutators');
const loaders = require('./loaders');
const schema = require('./schema');
// Models.
const Action = require('../models/action');
@@ -88,6 +89,7 @@ const defaultConnectors = {
resolvers,
mutators,
loaders,
schema,
},
};
+2
View File
@@ -2,6 +2,7 @@ const schema = require('./schema');
const Context = require('./context');
const { createSubscriptionManager } = require('./subscriptions');
const { ENABLE_TRACING } = require('../config');
const connectors = require('./connectors');
module.exports = {
createGraphOptions: req => ({
@@ -17,4 +18,5 @@ module.exports = {
cacheControl: ENABLE_TRACING,
}),
createSubscriptionManager,
connectors,
};
+3 -9
View File
@@ -1,4 +1,4 @@
const { SEARCH_OTHER_USERS } = require('../../perms/constants');
const { decorateUserField } = require('./util');
const Action = {
__resolveType({ action_type }) {
@@ -11,14 +11,8 @@ const Action = {
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 }) {
if (user && (user.can(SEARCH_OTHER_USERS) || user_id === user.id)) {
return Users.getByID.load(user_id);
}
},
};
decorateUserField(Action, 'user', 'user_id');
module.exports = Action;
+10 -10
View File
@@ -1,4 +1,6 @@
const { decorateWithTags } = require('./util');
const { property } = require('lodash');
const { SEARCH_ACTIONS } = require('../../perms/constants');
const { decorateWithTags, decorateWithPermissionCheck } = require('./util');
const Comment = {
hasParent({ parent_id }) {
@@ -29,15 +31,8 @@ const Comment = {
return Comments.getByQuery(query);
},
replyCount({ reply_count }) {
// A simple remap from the underlying database model to the graph model.
return reply_count;
},
actions({ id }, _, { user, loaders: { Actions } }) {
if (!user || !user.can('SEARCH_ACTIONS')) {
return null;
}
replyCount: property('reply_count'),
actions({ id }, _, { loaders: { Actions } }) {
return Actions.getByID.load(id);
},
action_summaries({ id, action_summaries }, _, { loaders: { Actions } }) {
@@ -65,4 +60,9 @@ const Comment = {
// Decorate the Comment type resolver with a tags field.
decorateWithTags(Comment);
// Protect direct action access.
decorateWithPermissionCheck(Comment, {
actions: [SEARCH_ACTIONS],
});
module.exports = Comment;
+7 -14
View File
@@ -1,18 +1,11 @@
const FlagAction = {
// Stored in the metadata, extract and return.
message({ metadata: { message } }) {
return message;
},
reason({ group_id }) {
return group_id;
},
user({ user_id }, _, { loaders: { Users } }) {
if (!user_id) {
return null;
}
const { decorateUserField } = require('./util');
const { property } = require('lodash');
return Users.getByID.load(user_id);
},
const FlagAction = {
message: property('metadata.message'),
reason: property('group_id'),
};
decorateUserField(FlagAction, 'user', 'user_id');
module.exports = FlagAction;
+3 -3
View File
@@ -1,7 +1,7 @@
const { property } = require('lodash');
const FlagActionSummary = {
reason({ group_id }) {
return group_id;
},
reason: property('group_id'),
};
module.exports = FlagActionSummary;
+2 -2
View File
@@ -1,4 +1,4 @@
const _ = require('lodash');
const { merge } = require('lodash');
const debug = require('debug')('talk:graph:resolvers');
const Action = require('./action');
@@ -70,7 +70,7 @@ resolvers = plugins
.reduce((acc, { plugin, resolvers }) => {
debug(`added plugin '${plugin.name}'`);
return _.merge(acc, resolvers);
return merge(acc, resolvers);
}, resolvers);
module.exports = resolvers;
+28 -25
View File
@@ -1,3 +1,4 @@
const { decorateWithPermissionCheck, checkSelfField } = require('./util');
const {
SEARCH_ASSETS,
SEARCH_OTHERS_COMMENTS,
@@ -5,11 +6,7 @@ const {
} = require('../../perms/constants');
const RootQuery = {
assets(_, { query }, { loaders: { Assets }, user }) {
if (user == null || !user.can(SEARCH_ASSETS)) {
return null;
}
assets(_, { query }, { loaders: { Assets } }) {
return Assets.getByQuery(query);
},
asset(_, query, { loaders: { Assets } }) {
@@ -33,11 +30,7 @@ const RootQuery = {
return Comments.get.load(id);
},
async commentCount(_, { query }, { user, loaders: { Comments, Assets } }) {
if (user == null || !user.can(SEARCH_OTHERS_COMMENTS)) {
return null;
}
async commentCount(_, { query }, { loaders: { Comments, Assets } }) {
const { asset_url, asset_id } = query;
if (
(!asset_id || asset_id.length === 0) &&
@@ -53,11 +46,7 @@ const RootQuery = {
return Comments.getCountByQuery(query);
},
async userCount(_, { query }, { user, loaders: { Users } }) {
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
return null;
}
async userCount(_, { query }, { loaders: { Users } }) {
return Users.getCountByQuery(query);
},
@@ -72,23 +61,37 @@ const RootQuery = {
},
// this returns an arbitrary user
user(_, { id }, { user, loaders: { Users } }) {
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
return null;
}
user(_, { id }, { loaders: { Users } }) {
return Users.getByID.load(id);
},
// 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 } }) {
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
return null;
}
users(_, { query }, { loaders: { Users } }) {
return Users.getByQuery(query);
},
};
// Protect some query fields that are privileged.
decorateWithPermissionCheck(RootQuery, {
assets: [SEARCH_ASSETS],
users: [SEARCH_OTHER_USERS],
userCount: [SEARCH_OTHER_USERS],
commentCount: [SEARCH_OTHERS_COMMENTS],
});
// Protect the user field so only users who have permission to look up another
// user may do so as well as a user looking up themselves.
decorateWithPermissionCheck(
RootQuery,
{
user: [SEARCH_OTHER_USERS],
},
(obj, { id }, { user }) => {
if (user && user.id === id) {
return true;
}
}
);
module.exports = RootQuery;
+21 -38
View File
@@ -1,40 +1,23 @@
const Subscription = {
commentAdded(comment) {
return comment;
},
commentEdited(comment) {
return comment;
},
commentAccepted(comment) {
return comment;
},
commentRejected(comment) {
return comment;
},
commentReset(comment) {
return comment;
},
commentFlagged(comment) {
return comment;
},
userBanned(user) {
return user;
},
userSuspended(user) {
return user;
},
usernameApproved(user) {
return user;
},
usernameRejected(user) {
return user;
},
usernameFlagged(user) {
return user;
},
usernameChanged(payload) {
return payload;
},
};
const Subscription = {};
// All of the subscription endpoints need to have an object to serialize when
// pushing out via PubSub, this simply ensures that all these entries will
// return the root object from the subscription to the PubSub framework.
[
'commentAdded',
'commentEdited',
'commentAccepted',
'commentRejected',
'commentReset',
'commentFlagged',
'userBanned',
'userSuspended',
'usernameApproved',
'usernameRejected',
'usernameFlagged',
'usernameChanged',
].forEach(field => {
Subscription[field] = obj => obj;
});
module.exports = Subscription;
+4 -8
View File
@@ -1,11 +1,7 @@
const { SEARCH_OTHER_USERS } = require('../../perms/constants');
const { decorateUserField } = require('./util');
const TagLink = {
assigned_by({ assigned_by }, _, { user, loaders: { Users } }) {
if (user && user.can(SEARCH_OTHER_USERS) && assigned_by != null) {
return Users.getByID.load(assigned_by);
}
},
};
const TagLink = {};
decorateUserField(TagLink, 'assigned_by');
module.exports = TagLink;
+38 -55
View File
@@ -1,4 +1,8 @@
const { decorateWithTags } = require('./util');
const {
decorateWithTags,
decorateWithPermissionCheck,
checkSelfField,
} = require('./util');
const KarmaService = require('../../services/karma');
const {
SEARCH_ACTIONS,
@@ -7,86 +11,65 @@ const {
VIEW_USER_ROLE,
LIST_OWN_TOKENS,
VIEW_USER_STATUS,
VIEW_USER_EMAIL,
} = require('../../perms/constants');
const { property } = require('lodash');
const User = {
action_summaries({ id }, _, { loaders: { Actions } }) {
return Actions.getSummariesByItemID.load(id);
},
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);
}
return Actions.getByID.load(id);
},
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))) {
return null;
}
comments({ id }, { query }, { loaders: { Comments } }) {
// Set the author id on the query.
query.author_id = id;
return Comments.getByQuery(query);
},
profiles({ profiles }, _, { user }) {
// if the user is not an admin, do not return the profiles
if (user && user.can(SEARCH_OTHER_USERS)) {
return profiles;
}
return null;
},
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 } }) {
// 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))) {
return null;
}
ignoredUsers({ ignoresUsers }, args, { user, loaders: { Users } }) {
// Return nothing if there is nothing to query for.
if (!user.ignoresUsers || user.ignoresUsers.length <= 0) {
return [];
}
return Users.getByID.loadMany(user.ignoresUsers);
},
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;
}
return null;
return Users.getByID.loadMany(ignoresUsers);
},
// Extract the reliability from the user metadata if they have permission.
reliable(user, _, { user: requestingUser }) {
if (requestingUser && requestingUser.can(SEARCH_ACTIONS)) {
return KarmaService.model(user);
}
},
reliable: user => KarmaService.model(user),
state(user, args, ctx) {
if (
ctx.user &&
(ctx.user.id === user.id || ctx.user.can(VIEW_USER_STATUS))
) {
return user;
}
},
// The state requires the whole user object to make decisions.
state: user => user,
// Get the first email on the user.
email: property('firstEmail'),
};
// Decorate the User type resolver with a tags field.
decorateWithTags(User);
// decorate the fields on the User resolver with a permission check where the
// current user can also get their own properties.
decorateWithPermissionCheck(
User,
{
actions: [SEARCH_ACTIONS],
email: [VIEW_USER_EMAIL],
state: [VIEW_USER_STATUS],
role: [VIEW_USER_ROLE],
ignoredUsers: [SEARCH_OTHER_USERS],
tokens: [LIST_OWN_TOKENS],
profiles: [SEARCH_OTHER_USERS],
comments: [SEARCH_OTHERS_COMMENTS],
},
checkSelfField('id')
);
// Decorate the fields on the User resolver where the current user has no impact
// on the resolvability of a field.
decorateWithPermissionCheck(User, { reliable: [SEARCH_ACTIONS] });
module.exports = User;
+8 -10
View File
@@ -1,14 +1,12 @@
const { decorateWithPermissionCheck, checkSelfField } = require('./util');
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))
) {
return user.status;
}
},
};
const UserState = {};
decorateWithPermissionCheck(
UserState,
{ status: [VIEW_USER_STATUS] },
checkSelfField('id')
);
module.exports = UserState;
+168 -32
View File
@@ -2,59 +2,171 @@ const {
ADD_COMMENT_TAG,
SEARCH_OTHER_USERS,
} = require('../../perms/constants');
const property = require('lodash/property');
const { property, isBoolean } = require('lodash');
/**
* Decorates the typeResolver with the tags field.
* getResolver will get the resolver from the typeResolver or apply the default
* resolver.
*
* @param {Object} typeResolver the type resolver
* @param {String} field the field name of the resolver we're getting
*/
const decorateWithTags = typeResolver => {
typeResolver.tags = ({ tags = [] }, _, { user }) => {
if (user && user.can(ADD_COMMENT_TAG)) {
return tags;
const getResolver = (typeResolver, field) => {
if (field in typeResolver) {
return typeResolver[field];
}
return property(field);
};
/**
*
* @param {Object} typeResolver the type resolver
* @param {String} field the name of the field being wrapped
* @param {Function} customCheck the function that can return a boolean
* indicating the resolution status
* @param {Function} skipFieldResolver the optional skip resolver that can be used
* skip out from the oldFieldResolver.
*/
const wrapCheck = (
typeResolver,
field,
customCheck,
skipFieldResolver = getResolver(typeResolver, field)
) => {
// Cache the old field resolver. In the event that the check does not return
// with a boolean, we'll use this.
const oldFieldResolver = getResolver(typeResolver, field);
// Override the field resolver on the type resolver with this wrapped
// function.
typeResolver[field] = (obj, args, ctx, info) => {
const decision = customCheck(obj, args, ctx, info);
if (isBoolean(decision)) {
if (decision) {
// The custom check returns a boolean true, so we should execute the
// underlying field resolver (which may just be the old resolver).
return skipFieldResolver(obj, args, ctx, info);
}
// The custom check returns a boolean false, then we should return null,
// because the check explicity said that we weren't allowed to access that
// field.
return null;
}
return tags.filter(t => t.tag.permissions.public);
// The custom check yielded no decision, so we should just fall back to the
// oldFieldResolver.
return oldFieldResolver(obj, args, ctx, info);
};
};
/**
* checkPermissions checks that the current user has all the required
* permissions.
*
* @param {Object} ctx graph context
* @param {Array<String>} permissions permissions that the user must have
*/
const checkPermissions = (ctx, permissions) =>
!ctx.user || !ctx.user.can(...permissions);
/**
* wrapCheckPermissions will wrap a specific field with a permission check.
*
* @param {Object} typeResolver the type resolver
* @param {String} field the field name of the resolver we're wrapping
* @param {Array<String>} permissions array of permissions to check against
* @param {Function} fieldResolver base resolver for the field
*/
const wrapCheckPermissions = (
typeResolver,
field,
permissions,
skipFieldResolver = getResolver(typeResolver, field)
) =>
wrapCheck(
typeResolver,
field,
(obj, args, ctx) => {
if (checkPermissions(ctx, permissions)) {
return false;
}
},
skipFieldResolver
);
/**
* decorateWithPermissionCheck will decorate the field resolver with
* permission checks.
*
* @param {Object} typeResolver the type resolver
* @param {Object} protect the object with field -> Array<String> of permissions
* @param {Function} customCheck a function that can return a boolean based on a
* custom check
*/
const decorateWithPermissionCheck = (typeResolver, protect) => {
const decorateWithPermissionCheck = (
typeResolver,
protect,
customCheck = null
) => {
for (const [field, permissions] of Object.entries(protect)) {
let fieldResolver = property(field);
if (field in typeResolver) {
fieldResolver = typeResolver[field];
const baseFieldResolver = getResolver(typeResolver, field);
wrapCheckPermissions(typeResolver, field, permissions, baseFieldResolver);
if (customCheck !== null) {
wrapCheck(typeResolver, field, customCheck, baseFieldResolver);
}
typeResolver[field] = (obj, args, ctx, info) => {
if (!ctx.user || !ctx.user.can(...permissions)) {
return null;
}
return fieldResolver(obj, args, ctx, info);
};
}
};
/**
* decorateUserField will decorate the user field accesses with correct
* permission checks.
* checkSelf will check if the current object is the same as the current user.
*
* @param {String} referenceField the field for the user id to check.
*/
const checkSelfField = referenceField => (obj, args, ctx) => {
if (
ctx.user &&
obj[referenceField] !== null &&
ctx.user.id === obj[referenceField]
) {
return true;
}
};
/**
* wrapCheckSelf wraps a typeResolver with a check for self (if the type is
* referencing the current user).
*
* @param {Object} typeResolver the type resolver
* @param {String} field the field to decorate
* @param {String} referenceField the field to pull the user id from.
* @param {Function} fieldResolver base resolver for the field
*/
const decorateUserField = (typeResolver, field) => {
const wrapCheckSelf = (
typeResolver,
field,
referenceField = field,
fieldResolver = getResolver(typeResolver, field)
) =>
wrapCheck(typeResolver, field, checkSelfField(referenceField), fieldResolver);
/**
* decorateUserField will decorate the user field accesses with correct
* permission checks and will load the user.
*
* @param {Object} typeResolver the type resolver
* @param {String} field the field to decorate
* @param {String} referenceField the field to pull the user id from.
*/
const decorateUserField = (typeResolver, field, referenceField = field) => {
// The default resolver for the user decorator is loading the user by id.
let fieldResolver = (obj, args, ctx) => {
if (!obj[field]) {
if (!obj[referenceField]) {
return null;
}
return ctx.loaders.Users.getByID.load(obj[field]);
return ctx.loaders.Users.getByID.load(obj[referenceField]);
};
// The resolver can be overridden however. This decorator will simply wrap the
@@ -63,16 +175,39 @@ const decorateUserField = (typeResolver, field) => {
fieldResolver = typeResolver[field];
}
typeResolver[field] = (obj, args, ctx, info) => {
if (
!ctx.user ||
obj[field] === null ||
(ctx.user.id !== obj[field] && !ctx.user.can(SEARCH_OTHER_USERS))
) {
return null;
// Wrap the current fieldResolver with the resolver which will return the
// user.
wrapCheckPermissions(
typeResolver,
field,
[SEARCH_OTHER_USERS],
fieldResolver
);
// Wrap the checked resolver with a check to see if the current user is the
// one being retrieved, in which case we should allow it.
wrapCheckSelf(typeResolver, field, referenceField, fieldResolver);
};
/**
* decorateWithTags decorates a typeResolver with a tags getter that will
* sanitize the tag output for users without permission to see non-public
* tags.
*
* @param {Object} typeResolver base type resolver
* @param {Function} fieldResolver the field resolver that gets the tags
*/
const decorateWithTags = (
typeResolver,
fieldResolver = getResolver(typeResolver, 'tags')
) => {
typeResolver.tags = async (obj, args, ctx, info) => {
const tags = await fieldResolver(obj, args, ctx, info);
if (checkPermissions(ctx, [ADD_COMMENT_TAG])) {
return tags;
}
return fieldResolver(obj, args, ctx, info);
return tags.filter(t => t.tag.permissions.public);
};
};
@@ -80,4 +215,5 @@ module.exports = {
decorateUserField,
decorateWithTags,
decorateWithPermissionCheck,
checkSelfField,
};
+17 -13
View File
@@ -63,7 +63,7 @@ type Token {
}
type UserProfile {
# the id is an identifier for the user profile (email, facebook id, etc)
# The id is an identifier for the user profile (email, facebook id, etc)
id: String!
# name of the provider attached to the authentication mode
@@ -184,13 +184,17 @@ type User {
# Actions completed on the parent.
actions: [Action!]
# the current roles of the user.
# The current roles of the user.
role: USER_ROLES
# the current profiles of the user.
# The current profiles of the user.
profiles: [UserProfile]
# the tags on the user
# The primary email address of the user. Only accessible to Administrators or
# the current user.
email: String
# The tags on the user.
tags: [TagLink!]
# ignored users.
@@ -421,7 +425,7 @@ input CommentCountQuery {
# The URL that the asset is located on.
asset_url: String
# the parent of the comment that we want to retrieve.
# The parent of the comment that we want to retrieve.
parent_id: ID
# comments returned will only be ones which have at least one action of this
@@ -479,13 +483,13 @@ type Comment {
# The body history of the comment.
body_history: [CommentBodyHistory!]!
# the tags on the comment
# The tags on the comment
tags: [TagLink!]
# the user who authored the comment.
# The user who authored the comment.
user: User
# the replies that were made to the comment.
# The replies that were made to the comment.
replies(query: RepliesQuery = {}): CommentConnection!
# replyCount is the number of replies with a depth of 1. Only direct replies
@@ -765,7 +769,7 @@ type Settings {
infoBoxContent: String
# questionBoxEnable will enable the Question Box's content to be visible above
# the comment box.
# The comment box.
questionBoxEnable: Boolean
# questionBoxContent is the content of the Question Box.
@@ -858,7 +862,7 @@ type Asset {
# The date that the asset was created.
created_at: Date
# the tags on the asset
# The tags on the asset
tags: [TagLink!]
# The author(s) of the asset.
@@ -1091,7 +1095,7 @@ input AssetSettingsInput {
moderation: MODERATION_MODE
# questionBoxEnable will enable the Question Boxs' content to be visible above
# the comment box.
# The comment box.
questionBoxEnable: Boolean
# questionBoxContent is the content of the Question Box.
@@ -1167,7 +1171,7 @@ input ModifyTagInput {
item_type: TAGGABLE_ITEM_TYPE!
# asset_id is used when the item_type is `COMMENTS`, the is needed to rectify
# the settings to get the asset specific tags/settings.
# The settings to get the asset specific tags/settings.
asset_id: ID
}
@@ -1225,7 +1229,7 @@ input UpdateSettingsInput {
infoBoxContent: String
# questionBoxEnable will enable the Question Box's content to be visible above
# the comment box.
# The comment box.
questionBoxEnable: Boolean
# questionBoxContent is the content of the Question Box.
+1
View File
@@ -9,4 +9,5 @@ module.exports = {
VIEW_PROTECTED_SETTINGS: 'VIEW_PROTECTED_SETTINGS',
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
VIEW_USER_ROLE: 'VIEW_USER_ROLE',
VIEW_USER_EMAIL: 'VIEW_USER_EMAIL',
};
+1 -1
View File
@@ -8,11 +8,11 @@ module.exports = (user, perm) => {
case types.SEARCH_ACTIONS:
case types.SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS:
case types.SEARCH_OTHERS_COMMENTS:
return check(user, ['ADMIN', 'MODERATOR']);
case types.SEARCH_COMMENT_STATUS_HISTORY:
case types.VIEW_USER_STATUS:
case types.VIEW_PROTECTED_SETTINGS:
case types.VIEW_USER_ROLE:
case types.VIEW_USER_EMAIL:
return check(user, ['ADMIN', 'MODERATOR']);
case types.LIST_OWN_TOKENS:
return check(user, ['ADMIN']);
+2 -8
View File
@@ -2,7 +2,6 @@ const SetupService = require('../services/setup');
const apollo = require('apollo-server-express');
const authentication = require('../middleware/authentication');
const cookieParser = require('cookie-parser');
const debug = require('debug')('talk:routes');
const enabled = require('debug').enabled;
const errors = require('../errors');
const express = require('express');
@@ -159,13 +158,8 @@ if (process.env.NODE_ENV !== 'production') {
});
}
// Inject server route plugins.
plugins.get('server', 'router').forEach(plugin => {
debug(`added plugin '${plugin.plugin.name}'`);
// Pass the root router to the plugin to mount it's routes.
plugin.router(router);
});
// Mount the plugin routes.
router.use(require('./plugins'));
//==============================================================================
// ERROR HANDLING
+23
View File
@@ -0,0 +1,23 @@
const express = require('express');
const debug = require('debug')('talk:routes:plugins');
const plugins = require('../services/plugins');
const { connectors } = require('../graph');
const { set } = require('lodash');
const router = express.Router();
// Mount the connectors on the router.
router.use((req, res, next) => {
set(req, 'talk.connectors', connectors);
next();
});
// Inject server route plugins.
plugins.get('server', 'router').forEach(plugin => {
debug(`added plugin '${plugin.plugin.name}'`);
// Pass the root router to the plugin to mount it's routes.
plugin.router(router);
});
module.exports = router;
+47
View File
@@ -22,6 +22,53 @@ describe('graph.queries.user', () => {
);
});
describe('email', () => {
const query = `
query User($user_id: ID!) {
user(id: $user_id) {
id
email
}
}
`;
it('can query for your own email', async () => {
const ctx = new Context({ user });
const { data, errors } = await graphql(schema, query, {}, ctx, {
user_id: user.id,
});
expect(errors).to.be.undefined;
expect(data.user).to.not.be.null;
expect(data.user.email).to.be.equal(user.firstEmail);
});
[
{ role: 'COMMENTER', can: false },
{ role: 'STAFF', can: false },
{ role: 'MODERATOR', can: true },
{ role: 'ADMIN', can: true },
].forEach(({ role, can }) => {
it(`${can ? 'can' : 'can not'} query with role = ${role}`, async () => {
const actor = new UserModel({ role });
const ctx = new Context({ user: actor });
const { data, errors } = await graphql(schema, query, {}, ctx, {
user_id: user.id,
});
expect(errors).to.be.undefined;
if (!can) {
expect(data.user).to.be.null;
} else {
expect(data.user).to.not.be.null;
expect(data.user.email).to.be.equal(user.firstEmail);
}
});
});
});
describe('state', () => {
const meQuery = `
query Me {