mirror of
https://github.com/wassname/talk.git
synced 2026-08-16 11:29:31 +08:00
Initial implementation at plugins
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
const {forEachField} = require('graphql-tools');
|
||||
const debug = require('debug')('talk:graph:schema');
|
||||
|
||||
/**
|
||||
* XXX taken from graphql-js: src/execution/execute.js, because that function
|
||||
* is not exported
|
||||
*
|
||||
* If a resolve function is not given, then a default resolve behavior is used
|
||||
* which takes the property of the source object of the same name as the field
|
||||
* 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}) => {
|
||||
|
||||
// ensure source is a value for which property access is acceptable.
|
||||
if (typeof source === 'object' || typeof source === 'function') {
|
||||
const property = source[fieldName];
|
||||
if (typeof property === 'function') {
|
||||
return source[fieldName](args, context);
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorates the schema with before and after hooks as provided by the Plugin
|
||||
* Manager.
|
||||
* @param {GraphQLSchema} schema the schema to decorate
|
||||
* @param {Array} hooks hooks to apply to the schema
|
||||
* @return {void}
|
||||
*/
|
||||
const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName) => {
|
||||
|
||||
// Pull out the before/after hooks from the available hooks.
|
||||
const {
|
||||
before,
|
||||
after
|
||||
} = hooks
|
||||
|
||||
// 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]}))
|
||||
|
||||
// Combine the before/after 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 'before':
|
||||
debug(`adding before hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
|
||||
|
||||
if (typeof hooks.before !== '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.before.push(hooks.before);
|
||||
break;
|
||||
case 'after':
|
||||
debug(`adding after hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
|
||||
|
||||
if (typeof hooks.after !== '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.after.unshift(hooks.after);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`invalid hook '${hook}' on resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {
|
||||
before: [],
|
||||
after: []
|
||||
});
|
||||
|
||||
// If we have no hooks to add here, don't try to modify anything.
|
||||
if (before.length === 0 && after.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 before functions (and
|
||||
// wait until they resolve) followed by waiting for the response and then
|
||||
// firing their after hooks. Lastly, we respond with the result of the
|
||||
// original resolver.
|
||||
field.resolve = async (obj, args, context, info) => {
|
||||
|
||||
// Issue all before hooks before we resolve the field.
|
||||
await Promise.all(before.map(async (before) => await before(obj, args, context, info)));
|
||||
|
||||
// Resolve the field.
|
||||
let result = await resolve(obj, args, context, info);
|
||||
|
||||
// Insure all after hooks after we've resolved the field with the result
|
||||
// passed in as the fifth argument.
|
||||
return await after.reduce(async (result, after) => await after(obj, args, context, info, result), result);
|
||||
};
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
decorateWithHooks
|
||||
};
|
||||
+23
-8
@@ -1,4 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const debug = require('debug')('talk:graph:loaders');
|
||||
|
||||
const Actions = require('./actions');
|
||||
const Assets = require('./assets');
|
||||
@@ -7,6 +8,27 @@ const Metrics = require('./metrics');
|
||||
const Settings = require('./settings');
|
||||
const Users = require('./users');
|
||||
|
||||
const plugins = require('../../plugins');
|
||||
|
||||
let loaders = [
|
||||
|
||||
// Load the core loaders.
|
||||
Actions,
|
||||
Assets,
|
||||
Comments,
|
||||
Metrics,
|
||||
Settings,
|
||||
Users,
|
||||
|
||||
// Load the plugin loaders from the manager.
|
||||
...plugins
|
||||
.get('server', 'loaders').map(({plugin, loaders}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
return loaders;
|
||||
})
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
@@ -15,14 +37,7 @@ const Users = require('./users');
|
||||
module.exports = (context) => {
|
||||
|
||||
// We need to return an object to be accessed.
|
||||
return _.merge(...[
|
||||
Actions,
|
||||
Assets,
|
||||
Comments,
|
||||
Metrics,
|
||||
Settings,
|
||||
Users
|
||||
].map((loaders) => {
|
||||
return _.merge(...loaders.map((loaders) => {
|
||||
|
||||
// Each loader is a function which takes the context.
|
||||
return loaders(context);
|
||||
|
||||
+25
-5
@@ -1,17 +1,37 @@
|
||||
const _ = require('lodash');
|
||||
const debug = require('debug')('talk:graph:mutators');
|
||||
|
||||
const Comment = require('./comment');
|
||||
const Action = require('./action');
|
||||
const User = require('./user');
|
||||
|
||||
const plugins = require('../../plugins');
|
||||
|
||||
let mutators = [
|
||||
|
||||
// Load in the core mutators.
|
||||
Comment,
|
||||
Action,
|
||||
User,
|
||||
|
||||
// Load the plugin mutators from the manager.
|
||||
...plugins
|
||||
.get('server', 'mutators').map(({plugin, mutators}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
return mutators;
|
||||
})
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates a set of mutators based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
* @return {Object} object of mutators
|
||||
*/
|
||||
module.exports = (context) => {
|
||||
|
||||
// We need to return an object to be accessed.
|
||||
return _.merge(...[
|
||||
Comment,
|
||||
Action,
|
||||
User,
|
||||
].map((mutators) => {
|
||||
return _.merge(...mutators.map((mutators) => {
|
||||
|
||||
// Each set of mutators is a function which takes the context.
|
||||
return mutators(context);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const debug = require('debug')('talk:graph:resolvers');
|
||||
|
||||
const ActionSummary = require('./action_summary');
|
||||
const Action = require('./action');
|
||||
const AssetActionSummary = require('./asset_action_summary');
|
||||
@@ -17,7 +20,10 @@ const UserError = require('./user_error');
|
||||
const User = require('./user');
|
||||
const ValidationUserError = require('./validation_user_error');
|
||||
|
||||
module.exports = {
|
||||
const plugins = require('../../plugins');
|
||||
|
||||
// Provide the core resolvers.
|
||||
let resolvers = {
|
||||
ActionSummary,
|
||||
Action,
|
||||
AssetActionSummary,
|
||||
@@ -37,3 +43,16 @@ module.exports = {
|
||||
User,
|
||||
ValidationUserError,
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin support requires that we merge in existing resolvers with our new
|
||||
* plugin based ones. This allows plugins to extend existing resolvers as well
|
||||
* as provide new ones.
|
||||
*/
|
||||
resolvers = plugins.get('server', 'resolvers').reduce((resolvers, {plugin}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
return _.merge(resolvers, plugin.resolvers);
|
||||
}, resolvers);
|
||||
|
||||
module.exports = resolvers;
|
||||
|
||||
+9
-3
@@ -1,11 +1,17 @@
|
||||
const tools = require('graphql-tools');
|
||||
const maskErrors = require('graphql-errors').maskErrors;
|
||||
const {makeExecutableSchema} = require('graphql-tools');
|
||||
const {maskErrors} = require('graphql-errors');
|
||||
const {decorateWithHooks} = require('./hooks');
|
||||
|
||||
const plugins = require('../plugins');
|
||||
const resolvers = require('./resolvers');
|
||||
const typeDefs = require('./typeDefs');
|
||||
|
||||
const schema = tools.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'));
|
||||
|
||||
// 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.
|
||||
|
||||
+20
-2
@@ -4,8 +4,26 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {mergeStrings} = require('gql-merge');
|
||||
const debug = require('debug')('talk:graph:typeDefs');
|
||||
const plugins = require('../plugins');
|
||||
|
||||
// Load the typeDefs from the graphql file.
|
||||
const typeDefs = fs.readFileSync(path.join(__dirname, 'typeDefs.graphql'), 'utf8');
|
||||
/**
|
||||
* Plugin support requires us to merge the type definitions from the loaded
|
||||
* graphql tags, this gives us the ability to extend any portion of the
|
||||
* 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}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
return typeDefs;
|
||||
})
|
||||
]);
|
||||
|
||||
module.exports = typeDefs;
|
||||
|
||||
Reference in New Issue
Block a user