API
.createMiddleware()
Instantiates middleware. A middleware hook is created with either the before or after method. The before hooks are fired before a query/mutation is resolved. The after hooks are fired after a query/mutation is resolved. Each middleware hook takes in a query/mutation pattern as the first argument and a callback as the second callback.
Valid patterns are * for all queries and mutations, pattern* for queries and mutations whose names start with "pattern", and fullPattern for a single query or mutation with the name "fullPattern".
The middleware callback receives three arguments: args, ctx, and next.
argsis a cloned, mutable object passed into either the query or mutation from the clientctxis a cloned, mutable object that is defined by the servernextis a function that is used to propagate to the next middleware hook (or resolve hook if it happens to be located in front of the resolve function)
A common practice is to mutate args along the middleware chain with input information (i.e. overwriting args.input.password with a hashed string). ctx is typically mutated with information pertaining to the state of the request.
Example middleware chain for the createUser mutation:
// middleware.js
import { createMiddleware } from 'micrograph';
const middleware = createMiddleware();
// fires before all queries and mutations
middleware.before((args, ctx, next) => {...});
// same as above
middleware.before('*', (args, ctx, next) => {...});
// fires before any query or mutation that starts with "create"
middleware.before('create*', (args, ctx, next) => {...});
// fires before the "createUser" mutation
middleware.before('createUser', (args, ctx, next) => {...});
// fires after the "createUser" mutation
middleware.after('createUser', (args, ctx, next) => {...});
// fires after any query or mutation that starts with "create"
middleware.after('create*', (args, ctx, next) => {...});
// fires after all queries and mutations are done resolving
middleware.after((args, ctx, next) => {...});
// same as above
middleware.after('*', (args, ctx, next) => {...});
export default middleware;
Propagation
Middleware propagation happens by calling next or by returning a Promise. If, at any point, you throw an error or return a rejected Promise, the chain breaks and all subsequent middleware hooks (including the resolve hook) are skipped. The error is forwarded to the query/mutation error handler.
Don't call next inside a Promise
In order to avoid unpredictable behavior, avoid calling next inside a Promise. Before writing a middleware function, decide whether using next or using a Promise makes more sense.
middleware.before('createUser', (args, ctx, next) => {
return somePromise.then(() => {
doSomething();
next(); // BAD! don't need to call this
}).then(() => {
doSomething(); // this may or may not get called
});
});
If you decide to use a Promise, return a rejected Promise if you need to break the middleware chain:
middleware.before('createUser', (args, ctx, next) => {
return somePromise.then(() => {
doSomething();
return Promise.reject(new Error('Oh no!'));
}).then(() => {
// this and any subsequent middleware hooks
// will not be called (including the resolve hook)
});
});
Don't catch rejected promises
Micrograph automatically handles Promise rejections and forwards the error to the query/mutation error handler.
middleware.before('createUser', (args, ctx, next) => {
return somePromise.then(() => {
doSomething();
return Promise.reject(new Error('Oh no!'));
}).then(() => {
// this and any subsequent middleware hooks
// will not be called (including the resolve hook)
})
// BAD! don't catch!
.catch(err => {
// if you catch, middleware will propagate normally
// and the error handler will not fire
});
});
Error handling
You can break out of a middleware chain (including inside of a resolve method) by either throwing an error or returning a rejected promise.
middleware.before('createUser', (args, ctx, next) => {
// throwing an error will break out of the middleware chain
throw new Error('Oh no!');
});
or
middleware.before('createUser', (args, ctx) => {
return new Promise((resolve, reject) => {
reject(new Error('Oh no!'));
});
});
Query and mutation error handlers are defined in the error key next to the resolve and finalize hooks.
// mutations.js
...
[`create${titleize(type.name)}`]: {
description: ...,
args: ...,
actions: {
resolve: ...,
finalize: ...,
// add an optional error key to catch errors before
// its sent to the client
error(err) {
console.log(err.message); // Oh no!
return err;
},
},
},
...
Make sure to return err (or a transformed one). The returned err is sent to the client. This is a good place to log or transform errors.