Initial implementation at plugins

This commit is contained in:
gaba
2017-03-20 07:33:34 -07:00
parent 2ee6335328
commit 258fd8b25a
11 changed files with 334 additions and 23 deletions
+4
View File
@@ -14,3 +14,7 @@ dump.rdb
coverage/
.tags
.tags1
# remove plugin folders
plugins
plugins.json
+5 -2
View File
@@ -12,9 +12,12 @@ EXPOSE 5000
# Install app dependencies
COPY package.json yarn.lock /usr/src/app/
RUN yarn install --production
RUN yarn install
# Bundle app source
COPY . /usr/src/app
CMD [ "yarn", "start" ]
# Build static assets
RUN yarn build
CMD ["yarn", "start"]
+115
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+20 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -62,6 +62,7 @@
"env-rewrite": "^1.0.2",
"express": "^4.14.0",
"express-session": "^1.14.2",
"gql-merge": "^0.0.4",
"graphql": "^0.8.2",
"graphql-errors": "^2.1.0",
"graphql-server-express": "^0.5.0",
+81
View File
@@ -0,0 +1,81 @@
const fs = require('fs');
const path = require('path');
let plugins = {};
// Try to parse the plugins.json file, logging out an error if the plugins.json
// file isn't loaded, but continuing. Else, like a parsing error, throw it and
// crash the program.
try {
plugins = JSON.parse(fs.readFileSync(path.join(__dirname, 'plugins.json'), 'utf8'));
} catch (err) {
if (err.code === 'ENOENT') {
console.error('plugins.json not found, plugins will not be active');
} else {
throw err;
}
}
/**
* Stores a reference to a section for a section of Plugins.
*/
class PluginSection {
constructor(plugin_names) {
this.plugins = plugin_names.map((plugin_name) => {
let plugin = require(`./plugins/${plugin_name}`);
// Ensure we have a default plugin name, but allow the name to be
// overrided by the plugin.
plugin.name = plugin.name || plugin_name;
return plugin;
});
}
/**
* This itterates over the section to provide all plugin hooks that are
* available.
*/
hook(hook) {
return this.plugins
.filter((plugin) => hook in plugin)
.map((plugin) => ({plugin, [hook]: plugin[hook]}));
}
}
const NullPluginSection = new PluginSection([]);
/**
* Stores references to all the plugins available on the application.
*/
class PluginManager {
constructor(plugins) {
this.sections = {};
for (let section in plugins) {
this.sections[section] = new PluginSection(plugins[section]);
}
}
/**
* Utility function which combines the Plugins.section and PluginSection.hook
* calls.
*/
get(section, hook) {
return this.section(section).hook(hook);
}
/**
* Returns the named section if it exists, otherwise it returns an empty
* plugin section.
*/
section(section) {
if (section in this.sections) {
return this.sections[section];
}
return NullPluginSection;
}
}
module.exports = new PluginManager(plugins);
+31 -2
View File
@@ -1133,7 +1133,7 @@ bluebird@3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.3.4.tgz#f780fe43e1a7a6510f67abd7d0d79533a40ddde6"
bluebird@3.4.6:
bluebird@3.4.6, bluebird@^3.4.6:
version "3.4.6"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f"
@@ -3230,7 +3230,7 @@ glob@7.0.5, glob@7.0.x:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5:
glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
@@ -3302,6 +3302,29 @@ got@^3.2.0:
read-all-stream "^3.0.0"
timed-out "^2.0.0"
gql-format@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/gql-format/-/gql-format-0.0.4.tgz#8237de7647de37f00aba2d0073abf6087e2da119"
dependencies:
commander "^2.9.0"
graphql "^0.7.2"
gql-merge@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/gql-merge/-/gql-merge-0.0.4.tgz#1cb1d4cc8bb8768172cf08a45c5a4fbd0ecedc9f"
dependencies:
commander "^2.9.0"
gql-format "^0.0.4"
gql-utils "^0.0.2"
graphql "^0.7.2"
gql-utils@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/gql-utils/-/gql-utils-0.0.2.tgz#962b3c1b34bf965a45d2564a93d3072921f61e86"
dependencies:
bluebird "^3.4.6"
glob "^7.1.1"
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
@@ -3369,6 +3392,12 @@ graphql-tools@^0.9.0:
optionalDependencies:
"@types/graphql" "^0.8.5"
graphql@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.7.2.tgz#cc894a32823399b8a0cb012b9e9ecad35cd00f72"
dependencies:
iterall "1.0.2"
graphql@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.8.2.tgz#eb1bb524b38104bbf2c9157f9abc67db2feba7d2"