mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 04:21:05 +08:00
Added docs and added new context based plugins.
This commit is contained in:
+261
@@ -0,0 +1,261 @@
|
||||
# Talk Plugins
|
||||
|
||||
Plugins for Talk can take various forms, currently we are only supporting server
|
||||
side plugins.
|
||||
|
||||
## Plugin Registration: `plugins.json`
|
||||
|
||||
All plugins must be registered in the root file `plugins.json`.
|
||||
|
||||
The format for this file is thus:
|
||||
|
||||
```js
|
||||
{
|
||||
"server": [
|
||||
"people"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Where we have a `server` key with an array of plugins that match the folder
|
||||
name in the `plugins/` folder. For example, the above `plugins.json` would
|
||||
require a plugin from `plugins/people`, which must provide a `index.js` file
|
||||
that returns an object that matches the Plugin Specification.
|
||||
|
||||
## Server Plugins
|
||||
|
||||
### Specification
|
||||
|
||||
Each plugin should export a single object with all hooks available on it.
|
||||
|
||||
_**Note: You will have access to the whole core and other plugin's typeDefs,
|
||||
context, loaders, mutators, resolvers, hooks. This is intentional, as it
|
||||
encourages composing plugins to merge functionality, like a Slack plugin which
|
||||
provides a Slack notify context function as well as having the loader for
|
||||
comments.**_
|
||||
|
||||
The following are the hooks available:
|
||||
|
||||
#### Field: `typeDefs`
|
||||
|
||||
```graphql
|
||||
enum COLOUR {
|
||||
RED
|
||||
BLUE
|
||||
}
|
||||
|
||||
type Person {
|
||||
name: String!
|
||||
colour: COLOUR!
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
createPerson(name: String!): Person
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
people: [Person!]
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
|
||||
`typeDefs` should be a string that will be _merged_ with the existing type
|
||||
definitions. `enum`'s will be appended to, types will be appended, and new types
|
||||
will be added.
|
||||
|
||||
#### Field: `context`
|
||||
|
||||
```js
|
||||
{
|
||||
Slack: (context) => ({
|
||||
notify: (message) => {
|
||||
// return a promise after we're done sending notifications.
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Any property provided here will be added to the context parameter available
|
||||
inside all resolvers, loaders, mutators, and of course, other context based
|
||||
plugins.
|
||||
|
||||
The top level item must accept a context for the request which it should use to
|
||||
configure the context plugin before it would be mounted at `context.plugins`.
|
||||
This plugin above would mount at: `context.plugins.Slack`, or, if you're using
|
||||
[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
|
||||
|
||||
#### Field: `loaders`
|
||||
|
||||
```js
|
||||
(context) => ({
|
||||
People: {
|
||||
load: () => db.people.find({user: context.user})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Loaders should be provided as a function which returns a map which is used in
|
||||
the resolvers function. These must return a promise or a value.
|
||||
|
||||
#### Field: `mutators`
|
||||
|
||||
```js
|
||||
(context) => ({
|
||||
People: {
|
||||
create: (name) => {
|
||||
return db.people.insert({user: context.user, name});
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Mutators should be provided as a function which returns a map which is used in
|
||||
the resolvers function. These must return a promise or a value.
|
||||
|
||||
#### Field: `resolvers`
|
||||
|
||||
```js
|
||||
{
|
||||
Person: {
|
||||
name(obj, args, context) {
|
||||
return obj.name;
|
||||
},
|
||||
colour(obj, args, context) {
|
||||
// Bill likes the colour red, everyone else likes blue.
|
||||
return obj.name === 'bill' ? 'RED' : 'BLUE';
|
||||
}
|
||||
},
|
||||
RootQuery: {
|
||||
people(obj, args, {loaders: {People}}) {
|
||||
return People.load();
|
||||
}
|
||||
},
|
||||
RootMutation: {
|
||||
createPerson(obj, {name}, {mutators: {People}}) {
|
||||
return People.create(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Should return a resolver map as described in the
|
||||
[Apollo Docs](http://dev.apollodata.com/tools/graphql-tools/resolvers.html#Resolver-map).
|
||||
|
||||
This will merge with the existing resolvers in core and from previous plugins.
|
||||
|
||||
#### Field: `hooks`
|
||||
|
||||
```js
|
||||
{
|
||||
RootMutation: {
|
||||
createPerson: {
|
||||
post(obj, args, {plugins: {Slack}}, person) {
|
||||
if (!person) {
|
||||
return person;
|
||||
}
|
||||
|
||||
await Slack.notify(`A new person just was created with name ${person.name}`);
|
||||
|
||||
return person;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hooks here are pretty special, for each resolver field, you can specify a
|
||||
pre/post hook that will execute pre and post field resolution.
|
||||
|
||||
If your post function accepts four parameters, then it can modify the field
|
||||
result. It is *required* that the function resolves a promise (or returns) with
|
||||
the modified value or simply the original if you didn't modify it.
|
||||
|
||||
### Full Example
|
||||
|
||||
Contents of `plugins.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": [
|
||||
"people"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Located in `plugins/people/index.js`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
typeDefs: `
|
||||
enum COLOUR {
|
||||
RED
|
||||
BLUE
|
||||
}
|
||||
|
||||
type Person {
|
||||
name: String!
|
||||
colour: COLOUR!
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
people: [Person!]
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
Slack: () => ({
|
||||
notify: (message) => {
|
||||
// return a promise after we're done sending notifications.
|
||||
}
|
||||
})
|
||||
},
|
||||
loaders: ({user}) => ({
|
||||
People: {
|
||||
load: () => db.people.find({user})
|
||||
}
|
||||
}),
|
||||
mutators: ({user}) => ({
|
||||
People: {
|
||||
create: (name) => {
|
||||
return db.people.insert({user, name});
|
||||
}
|
||||
}
|
||||
}),
|
||||
resolvers: {
|
||||
Person: {
|
||||
name(obj, args, context) {
|
||||
return obj.name;
|
||||
},
|
||||
colour(obj, args, context) {
|
||||
// Bill likes the colour red, everyone else likes blue.
|
||||
return obj.name === 'bill' ? 'RED' : 'BLUE';
|
||||
}
|
||||
},
|
||||
RootQuery: {
|
||||
people(obj, args, {loaders: {People}}) {
|
||||
return People.load();
|
||||
}
|
||||
},
|
||||
RootMutation: {
|
||||
createPerson(obj, {name}, {mutators: {People}}) {
|
||||
return People.create(name);
|
||||
}
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
RootMutation: {
|
||||
createPerson: {
|
||||
post(obj, args, {plugins: {Slack}}, person) {
|
||||
if (!person) {
|
||||
return person;
|
||||
}
|
||||
|
||||
await Slack.notify(`A new person just was created with name ${person.name}`);
|
||||
|
||||
return person;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
@@ -1,6 +1,32 @@
|
||||
const loaders = require('./loaders');
|
||||
const mutators = require('./mutators');
|
||||
|
||||
const plugins = require('../plugins');
|
||||
const debug = require('debug')('talk:graph:context');
|
||||
|
||||
/**
|
||||
* Contains the array of plugins that provide context to the server, these top
|
||||
* 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};
|
||||
});
|
||||
|
||||
/**
|
||||
* This should itterate over the passed in plugins and load them all with the
|
||||
* current graph context.
|
||||
* @return {Object} the saturated plugins object
|
||||
*/
|
||||
const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduce((acc, plugin) => {
|
||||
Object.keys(plugin.context).forEach((service) => {
|
||||
acc[service] = plugin.context[service](context);
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Stores the request context.
|
||||
*/
|
||||
@@ -17,6 +43,9 @@ class Context {
|
||||
|
||||
// Create the mutators.
|
||||
this.mutators = mutators(this);
|
||||
|
||||
// Decorate the plugin context.
|
||||
this.plugins = decorateContextPlugins(this, contextPlugins);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-3
@@ -99,14 +99,31 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
|
||||
field.resolve = async (obj, args, context, info) => {
|
||||
|
||||
// Issue all pre hooks before we resolve the field.
|
||||
await Promise.all(pre.map(async (pre) => await pre(obj, args, context, info)));
|
||||
await Promise.all(pre.map((pre) => pre(obj, args, context, info)));
|
||||
|
||||
// Resolve the field.
|
||||
let result = await resolve(obj, args, context, info);
|
||||
let result = 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) => await post(obj, args, context, info, result), result);
|
||||
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);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user