auto-block
Simplified controller creation built around async.auto
Example usage
var controller = data: context: context event: event optionsMapping: 'slug': 'event.slug' 'feed': 'event.feed' 'dryrun': 'event.dryrun' responseMapping: 'results.query' controllerblock = 'feedConfig': func: helpersclientsgetClientConfig after: 'options' with: 'options.slug' 'options.feed' 'redshiftPassword': func: helperssecretskmsDecrypt after: 'feedConfig' with: 'payload': 'feedConfig.import.redshiftPassword' autoBlock
auto-block versus async.auto
async.auto is extremely useful for determining running order of interdependent async functions:
async
When dealing with complex controllers, you often find it easier to move the step implementations into their own functions:
{ // step one } { // step two } async
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:
{ } { var params = 'alpha': resultsonealpha 'beta': resultsonebeta // 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-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' { if err && errstatus resstatuserrstatus }
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.
Controller configuration
.done
var controller = { console; }
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
This second parameter will not override the .done
field.
.data
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.
.block
controller = block: 'alpha': // ... 'beta': // ...
block
holds the actual steps used during autoBlock.run
. See below for details on how to configure steps properly.
Option mapping
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: utilitydoAlpha 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: utilitydoAlpha with: 'fizz': 'options.fizz' // resolves to 'buzz'
Error mapping
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: utilitydoAlpha // result is 'alpha' 'beta': func: utilitydoBeta // generates new Error('bad news') with: 'alpha' { // 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: utilitydoAlpha // result is 'alpha' 'beta': func: utilitydoBeta // generates new Error('bad news') with: 'alpha' errorDefaults: 'fizz': 'buzz' 'bar': 'not zaz' errorMapping: 'bar': 'foo.bar' 'alpha': 'results.alpha' { // error will be similar to: // { // message: 'bad news', // data: { // fizz: 'buzz', // bar: 'zaz', // alpha: 'alpha' // } // } }
.errorSuppress
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: utilitydoAlpha // generates new Error('bad news') { // error will be undefined }
Response mapping
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: utilitydoAlpha // result is 'alpha' { // response will be similar to: // { // fizz: 'buzz', // bar: 'zaz', // alpha: 'alpha' // } }
Hooks
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.
Custom hook mapping
You can alter the payloads for hooks by using .func
and .with
:
controller = onStart: func: consolelog with: 'foo': 'bar'
Block configuration
Each key in .block
represents one step that should be run. The definition for each step can include any number of settings:
.func
.func
is the asynchronous function that will be run during .run
:
block: 'alpha': func: utilitydoAlpha
The last parameter of the function must be a callback. The number of other parameters is flexible (see .when
below).
.sync
.sync
is the synchronous function that will be run during .run
:
block: 'alpha': sync: utilitydoAlpha
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
.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: utilitydoAlpha with: 'foo': 'options.foo'
.with
.with
defines the parameter mapping to be used with .func
. You can either define an object:
block: 'alpha': func: utilitydoAlpha with: 'foo': 'options.foo' 'fizz': 'fizz.buzz' // calls utility.doAlpha({ 'foo': '...', 'fizz': '...' }, cb)
Or an array:
block: 'alpha': func: utilitydoAlpha 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: utilitydoBeta // runs after doAlpha completes with: 'foo': 'alpha.foo' 'alpha': func: utilitydoAlpha // runs immediately
.after
You can add explicit dependencies using .after
:
block: 'delta': func: utilitydoDelta // runs after doAlpha and doBeta complete after: 'beta' with: 'foo': 'alpha.foo' 'alpha': func: utilitydoAlpha // runs immediately 'beta': func: utilitydoBeta // runs immediately
.when
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: utilitydoAlpha 'beta': func: utilitydoBeta 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: utilitydoAlpha 'beta': func: utilitydoBeta 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: utilitydoAlpha 'beta': func: utilitydoBeta when: '!alpha.flag'
Multiple checks are allowed (all checks must succeed):
block: 'alpha': func: utilitydoAlpha 'beta': func: utilitydoBeta when: '!alpha.flag' 'alpha.foo': 'bar'