Simplified controller creation built around async.auto
var controller = {
data: {
context: context,
event: event,
},
optionsMapping: {
'slug': 'event.slug',
'feed': 'event.feed',
'dryrun': 'event.dryrun'
},
responseMapping: 'results.query'
}
controller.block = {
'feedConfig': {
func: helpers.clients.getClientConfig,
after: ['options'],
with: [
'options.slug',
'options.feed'
]
},
'redshiftPassword': {
func: helpers.secrets.kmsDecrypt,
after: ['feedConfig'],
with: {
'payload': 'feedConfig.import.redshiftPassword'
}
}
}
autoBlock.run(controller, context.done)
async.auto is extremely useful for determining running order of interdependent async functions:
async.auto({
'one': function (results, cb) {
// step one
},
'two': ['one', function (results, cb) {
// step two
}]
}, function (err, results) {
// all done
})
When dealing with complex controllers, you often find it easier to move the step implementations into their own functions:
function one(results, cb) {
// step one
}
function two(results, cb) {
// step two
}
async.auto({
'one': one,
'two': ['one', two]
}, function (err, results) {
// all done
})
This allows for easier unit testing and debugging and helps keep your code clean. But you may often find yourself adding lots of small wrappers to convert values between the various steps:
function one(results, cb) {
cb(null, {
'alpha': 'foo',
'beta': 'bar'
})
}
function two(results, cb) {
var params = {
'alpha': results.one.alpha,
'beta': results.one.beta
}
// step two
}
auto-block allows you to declare all of the mappings in the same place you declare the dependencies:
block = {
'one': {
func: one
},
'two': {
func: two,
with: {
'alpha': 'one.alpha',
'beta': 'one.beta'
}
}
}
Similarly, controllers often need to detect errors or attaching extra data if a step fails:
async.auto({
'one': utility.doStuff,
'two': function (results, cb) {
var options = {
'alpha': results.one.alpha,
'beta': results.one.beta
}
utility.findUser(options, function (err, result) {
if (err) {
err.status = 404
err.alpha = options.alpha
}
cb(err, result);
})
}
}, function (err, results) {
if (err && err.status) {
res.status(err.status).send({
message: err.message
})
}
})
auto-block lets you declare these error mappings using errorDefaults
and
errorMappings
:
var controller: {
block: {
'one': {
func: one
},
'two': {
func: two,
with: {
'alpha': 'one.alpha',
'beta': 'one.beta'
},
errorDefaults: {
'status': 404
},
errorMappings: {
'alpha': 'one.alpha'
}
}
},
done: function (err, results) {
if (err && err.status) {
res.status(err.status).send({
message: err.message
})
}
}
}
These, along with other features, allow you to build complex controllers with needing to explicitly write function wrappers. auto-block does all of that for you.
Without the need for extra wrappers, the temptation to inline business logic is removed. You can comfortably move the business logic out of the controller without needing to know the details of which controller module you used.
var controller = {
done: function (error, response) {
console.log('do stuff');
}
}
Called after the entire block has been completed. The values of error
and
response
are generated by their respective mapping configurations (see
below).
The done
function can also be provided as the second parameter to autoBlock.run
:
autoBlock.run(controller, function (error, response) {
console.log('do stuff');
})
This second parameter will not override the .done
field.
var controller = {
data: {
'foo': 'bar',
'fizz': 'buzz'
}
}
Values provided in the data
fields are available during options
, error
and
response
mappings but not during results
mapping.
controller = {
block: {
'alpha': {
// ...
},
'beta': {
// ...
}
}
}
block
holds the actual steps used during autoBlock.run
. See below for details on how to configure steps properly.
The values in .data
are not exposed to the individual steps. You can
explicitly expose them, however, using optionsMapping
:
controller = {
data: {
'foo': {
'bar': 'zaz'
}
},
optionsDefaults: {
'fizz': 'buzz'
'bar': 'not zaz'
},
optionsMapping: {
'bar': 'foo.bar'
},
block: {
'alpha': {
func: utility.doAlpha,
with: {
'bar': 'options.bar', // resolves to 'zaz'
'fizz': 'options.fizz' // resolves to 'buzz'
}
}
}
}
Behind the scenes, auto-block builds a special options
step that runs before
any other step you've declared on .block
. If you don't need optionsMapping
or optionsDefaults
, you can set your own options
step:
controller = {
block: {
'options': {
value: {
'fizz': 'buzz'
}
},
'alpha': {
func: utility.doAlpha,
with: {
'fizz': 'options.fizz' // resolves to 'buzz'
}
}
}
}
If a step produces an error, you can map additional fields onto the error before it is sent to .done
:
controller = {
data: {
'foo': {
'bar': 'zaz'
}
},
errorDefaults: {
'fizz': 'buzz'
'bar': 'not zaz'
},
errorMapping: {
'bar': 'foo.bar',
'alpha': 'results.alpha'
},
block: {
'alpha': {
func: utility.doAlpha // result is 'alpha'
},
'beta': {
func: utility.doBeta, // generates new Error('bad news')
with: ['alpha']
}
},
done: function (error, response) {
// error will be similar to:
// {
// message: 'bad news',
// data: {
// fizz: 'buzz',
// bar: 'zaz',
// alpha: 'alpha'
// }
// }
}
}
If necessary, you can also add error mapping for particular steps:
controller = {
data: {
'foo': {
'bar': 'zaz'
}
},
block: {
'alpha': {
func: utility.doAlpha // result is 'alpha'
},
'beta': {
func: utility.doBeta, // generates new Error('bad news')
with: ['alpha']
errorDefaults: {
'fizz': 'buzz'
'bar': 'not zaz'
},
errorMapping: {
'bar': 'foo.bar',
'alpha': 'results.alpha'
},
}
},
done: function (error, response) {
// error will be similar to:
// {
// message: 'bad news',
// data: {
// fizz: 'buzz',
// bar: 'zaz',
// alpha: 'alpha'
// }
// }
}
}
Errors that break out of the controller will be sent through to the .done
handler. If you need to suppress these, use .errorSuppress
:
controller = {
errorDefaults: {
'retry': false
},
errorSuppress: {
'data.retry': false
},
block: {
'alpha': {
func: utility.doAlpha // generates new Error('bad news')
}
},
done: function (error, response) {
// error will be undefined
}
}
After all steps are completed, you can map values into the response parameter of .done
:
controller = {
data: {
'foo': {
'bar': 'zaz'
}
},
responseDefaults: {
'fizz': 'buzz'
'bar': 'not zaz'
},
responseMapping: {
'bar': 'foo.bar',
'alpha': 'results.alpha'
},
block: {
'alpha': {
func: utility.doAlpha // result is 'alpha'
}
},
done: function (error, response) {
// response will be similar to:
// {
// fizz: 'buzz',
// bar: 'zaz',
// alpha: 'alpha'
// }
}
}
auto-block provides five hooks that can be used for things like logging or debugging:
onStart(data)
-- called exactly once before any step is runonStartStep(name, data)
-- called immediately after a step startsonFinishStep(name, data, stepData)
-- called just before the step callback is runonSuccess(response, data)
-- called after the response has been mapped if no error existsonFailure(error, data)
-- called after the response has been mapped if an error exists
The data
parameter noted above is the same as the .data
configuration with a few extra fields added.
stepData
is a string with some debugging information in it but is not well defined.
You can alter the payloads for hooks by using .func
and .with
:
controller = {
onStart: {
func: console.log,
with: {
'foo': 'bar'
}
}
}
Each key in .block
represents one step that should be run. The definition for each step can include any number of settings:
.func
is the asynchronous function that will be run during .run
:
block: {
'alpha': {
func: utility.doAlpha
}
}
The last parameter of the function must be a callback. The number of other parameters is flexible (see .when
below).
.sync
is the synchronous function that will be run during .run
:
block: {
'alpha': {
sync: utility.doAlpha
}
}
The parameters work exactly like .func
except no callback function is required. If the function returns a Promise, it will handle .then
asynchronously as expected.
.value
will merely add an object to the internal results payload. This can be useful for adding extra fields for mapping:
block: {
'options': {
value: {
'foo': 'bar'
}
},
'alpha': {
func: utility.doAlpha,
with: {
'foo': 'options.foo'
}
}
}
.with
defines the parameter mapping to be used with .func
. You can either define an object:
block: {
'alpha': {
func: utility.doAlpha,
with: {
'foo': 'options.foo',
'fizz': 'fizz.buzz'
}
}
}
// calls utility.doAlpha({ 'foo': '...', 'fizz': '...' }, cb)
Or an array:
block: {
'alpha': {
func: utility.doAlpha,
with: [
'options.foo',
'fizz': 'fizz.buzz'
]
}
}
// calls utility.doAlpha('...', '...', cb)
The dot syntax starts with the results from all previous steps and will automatically wait for those steps to complete:
block: {
'beta': {
func: utility.doBeta, // runs after doAlpha completes
with: {
'foo': 'alpha.foo'
}
},
'alpha': {
func: utility.doAlpha, // runs immediately
}
}
You can add explicit dependencies using .after
:
block: {
'delta': {
func: utility.doDelta, // runs after doAlpha and doBeta complete
after: ['beta'],
with: {
'foo': 'alpha.foo'
}
},
'alpha': {
func: utility.doAlpha, // runs immediately
},
'beta': {
func: utility.doBeta, // runs immediately
}
}
Some steps are contingent on specific values or results from previous steps.
You can add these sorts of value dependencies using .when
:
block: {
'alpha': {
func: utility.doAlpha,
},
'beta': {
func: utility.doBeta,
when: 'alpha.flag'
}
}
// doBeta will only run if doAlpha results in a value similar to:
// {
// "flag": true
// }
.when
settings will automatically add dependencies. In the above example, the
"beta" step will still occur after the "alpha" step.
Value comparison is supported using objects:
block: {
'alpha': {
func: utility.doAlpha,
},
'beta': {
func: utility.doBeta,
when: {
'alpha.foo': 'bar'
}
}
}
// doBeta will only run if doAlpha result includes the follow key/value pair:
// {
// "foo": "bar"
// }
Negative checks can be made by using a !
prefix:
block: {
'alpha': {
func: utility.doAlpha,
},
'beta': {
func: utility.doBeta,
when: '!alpha.flag'
}
}
Multiple checks are allowed (all checks must succeed):
block: {
'alpha': {
func: utility.doAlpha,
},
'beta': {
func: utility.doBeta,
when: [
'!alpha.flag',
{
'alpha.foo': 'bar'
}
]
}
}