The thorin action
In Thorin, actions can be regarded as routes that have a call stack containing input validators, middleware functions,
authorization functions and aliases. When defining an action, you are preparing your application to react to an
external stimuli (initiated by the transport layer). To keep things predictable, your action will define the structure
of the input data, acting like a contract between your application and the outside world.
When the transport layer creates a new Intent, that instance will be matched to one of your defined actions and
sent out to be processed. An intent can be viewed as both the req
and the res
objects of
a regular express app.
The core action class is under thorin.Action
and can be extended by other thorin components, using the
pattern below:
'use strict';
const CoreAction = thorin.Action;
thorin.Action = class ThorinAction extends CoreAction {
constructor(name) {
super(name);
// your custom functionality
}
}
Functionality
The core thorin intent implements the functions below. However, keep in mind that some plugins can extend
it by adding additional functions or injecting themselves in specific points in its lifecycle.
Properties
-
hasDebug boolean, true Does the action enable debugging?
-
isTemplateboolean, false Is this action a template one?
-
rootstring When the action is a template, the root path to apply to sub aliases
-
stackarray The internal call stack to use when an intent is triggered. Contains middlewares, authorizations, validations, etc.
-
namestring The action name (identifier)
-
aliasesarray of object Array of {verb, name} for HTTP aliases
-
templatesarray of string Array of template names to extend from
-
endsarray of function Array of functions to call when the intent is completed
-
eventsobject Map of internal before nad after event handlers.
actionObj.debug(bool)
Enables or disable debugging for the given action.
actionObj.alias(verb, path)
Sets an alias to this action. An alias will be typically used by the HTTP transport to
map URL requests to this action, essentially enabling you to define a POST /todo handler
that will convert to this action. When an action extends a template action, the alias
path will have the template's root
property prepended.
-
verbstring the HTTP Verb to use (GET,POST,PUT,DELETE,PATCH)
-
pathstring the HTTP path to use
actionObj.input(obj)
Specifies the required input data for the action to work properly. The dispatcher will
apply the specified sanitization on the payload and continue or stop the call stack.
-
objobject An object that uses its keys to define the structure of the
input payload, and the associated validator function (see below example)
actionObj.authorize(name, opt)
Adds a previously registered authorization to the internal call stack.
-
namestring* the name of the previously registered thorin.Authorization instance
-
optobject additional options to pass to the authorization handler, when called
actionObj.use(middleware, opt)
Adds the given middleware(s) to the action's call stack.
-
middlewarestring* | function* | array of strings* Specifies which middleware to register.
-
optobject Additional options to be passed to the middleware's handler when called.
You can register a middleware as such:
actionObj.use('middleware.name');
named middleware
actionObj.use('middleware.name', {my: 'options'})
named middleware with options
actionObj.use((intentObj, next) => { next()})
inline middleware
actionObj.use(['middleware.one', 'middleware.two'])
multiple named middlewares
actionObj.before(type, name, fn)
Registers a callback handler that will be called before an item from the call stack
is executed. The handler can be either for specific handler names, or after all handlers of a specific type were called.
-
typestring the event hook type. Values are: authorize, validate, middleware, use
-
namestring | func if string, the handler will be called only for the specified hook name.
-
fnfunc the callback function to call when the hook is triggered.
actionObj.after(type, name, fn)
Registers a callback handler that will be called after an item from the call stack
is executed. The handler can be either for specific handler names, or after all handlers of a specific type were called.
-
typestring the event hook type. Values are: authorize, validate, middleware, use
-
namestring | func if string, the handler will be called only for the specified hook name.
-
fnfunc the callback function to call when the hook is triggered.
actionObj.end(fn)
Registers an end callback, similar to the before and after ones, it will be called
whenever the intent will complete with either a success or an error. When called, it will receive
the intentObj as a parameter.
- fnfunction the callback function to use
actionObj.onRegister(fn)
The function will be called when the dispatcher has registered the action, after it inherited
any functionality from all its parent templates (if any)
-
fnfunction the callback function to use.
actionObj._runCustomType(intentObj, handler, done)
Plugins or other components can inject functionality into an action by
overriding this function. Whenever a custom action handler that is not the
default handler will be registered, it will call this function (see below)
-
intentObjinstance of thorin.Intent the incoming intent object
-
handlerobject an object containing the handler description (with a type key)
-
donefunction the callback function to call when ready.
// The render plugin extending the custom type.
_runCustomType(intentObj, handler, done) {
if(handler.type !== Action.HANDLER_TYPE.RENDER) {
return super._runCustomType.apply(this, arguments);
}
intentObj._setTemplateSource(getTemplateSource.bind(this));
if(this.events.before.render || this.events.after.render) {
intentObj._setTemplateSuccess(this._runHandler.bind(this, 'after', 'render', intentObj));
}
done();
}
actionObj._runStack(intentObj, done)
When an intent was dispatcher by the transport layer, the dispatcher will call this
function of the matched action. Internally, it will execute all the handlers that
were registered in its call stack, chronologically and trigger before and after events (if any).
-
intentObjinstance of thorin.Intent the incoming intent
-
donefunction the function to call when the action has finished processing the intent.
actionObj.template(name)
Specifies that the current action will use the given template as a parent and
essentially have the template's stack be executed before any of the internal stack.
-
namename a previously registered template name.
actionObj._extendFromParent(actionObj)
The function is called by the dispatcher once all the action's parents were registered
and essentially copies the structure of the parent action. This should more likely be used only by other components
-
actionObjinstance of thorin.Action the parent action to extend.
actionObj._runHandler(handlerType, sourceEvent, intentObj, sourceName)
Whenever a custom event is triggered (eg, before validate), the triggering source will call
this function to run all the handlers that are listening to the given event). It can be seen as
a specialized trigger() function.
-
handlerTypestring the handler type, values are before or after
-
sourceEventstring the event that occurred, eg: validation or authorization
-
intentObjinstance of thorin.Intent the intent that was used
-
sourceNamestring if specified, the name of the source event
A small example of the render action can be viewed below
this._runHandler('before', 'render', intentObj, templateName);
Example
'use strict';
const dispatcher = thorin.dispatcher;
// A small authorization function
dispatcher
.addAuthorization('session.check')
.use((intentObj, next) => {
if(!intentObj.session.account) return next(thorin.error('AUTH', 'Please login', 403));
next();
});
// Create a small template action
dispatcher
.addTemplate('session.account')
.alias('/todo/:id')
.authorize('session.check')
.input({// if no id in payload, stop request.
id: dispatcher.validate('NUMBER').error('INVALID_TODO', 'Missing todo', 400)
});
// Define an action that uses the above template.
dispatcher
.addAction('todo.view')
.template('session.account')
.alias('GET', '/')
.input({ // if no time in payload, use "now"
time: dispatcher.validate('DATE').default("now")
})
.use((intentObj, next) => {
// Your code here
intentObj.result({
"some": "result"
});
next();
})
.before('authorize', 'session.account', (intentObj) => {
log.info(`Checking session`);
})
.end((intentObj) => {
log.info(`Intent completed for ${intentObj.action}`);
});