Components
Components are modules or plugins that modify or add functionality to the pipeline when they are added to it. Components can be written by consumers of twine to augment pipelines in ways not already included in the library.
A component added to a pipeline via the addComponent(callback)
function has 2 phases: Before-Request and After-Request
Before-Request occurs when the pipeline is executed. When .execute()
is called, it iterates through the added components in reverse, executing each component's callback function. When it does this, it provides 2 arguments: The Twine Context, and a function to call to invoke the next component in the list.
addComponent((context, next) => {
...
return next()
})
During this phase, the component has the ability to setup aspects of the pipeline's context, storing variables and other things that will influence the request that is made by the the Resource Service.
After the component has completed its setup actions, it should call and return the provided next()
function. This function continues the rest of the pipeline. This function provides a Promise
which is how a component can access the After-Request life cycle phase.
After all components have executed (and executed their provided next()
callbacks to continue), the ResourceService will execute and return a Promise. Once this Promise is fulfilled, components will shift into their After-Request phase. A component will have access to this phase by using the .then
and .catch
methods on the promise returned by next()
callback. It is in this phase that a component will have access to the result of the request.
LifeCycle Example:
const twine = require('@inmar/twine-node')
const jsonPlaceholderService = twine.createResourceService('jsonplaceholder.typicode.com')
.usingHTTPS()
const getTodoTemplate = jsonPlaceholderService.createRequestTemplate('get-todo')
.withoutInstrumentation() //This can be ignored for now.
.withMethod('GET')
.withUriTemplate('/todos/{id}')
.addComponent((context, next) => {
// This is the Before-Request Phase
context['timing.start-time'] = Date.now()
//If you don't execute next(), the pipeline will not continue.
//If you don't return next(), the component that proceeded this will not get to
// enter its After-Request phase
return next()
.then(() => {
// This is the After-Request phase.
// You still have access to 'context' here which will be updated.
console.log(`Request Took ${Date.now() - context['timing.start-time']}ms`)
})
})
module.exports = {
getTodo(todoId) {
return getTodoTemplate.createRequest()
.withParameters({ id: todoId })
.createRequest()
}
}
In the above example, we built a simple, in-line, timing component. However, we also saw a few library-provided methods which register components for us:
RequestTemplate.prototype.withMethod(string)
RequestTemplate.prototype.withUriTemplate(string)
Request.prototype.withParameters(object)
Each of these methods is modifying the Twine Context in a way that the usingHTTPS
Resource Service component understands.
This is the source of the withMethod
component.
RequestTemplate.prototype.withMethod = function(method) {
return this.addComponent((context, next) => {
context['http.RequestMethod'] = method
return next()
})
}
As you can see, it internally is calling addComponent
. With that in mind, we could create the following to extend Twine and provide functionality that other templates could easily reuse:
const twine = require('@inmar/twine-node')
const { RequestTemplate } = twine
RequestTemplate.prototype.logRequestDuration = function() {
return this.addComponent((context, next) => {
context['timing.start-time'] = Date.now()
return next()
.then(() => {
console.log(`Request Took ${Date.now() - context['timing.start-time']}ms`)
})
})
}
Then our Request Template could be simplified to:
const getTodoTemplate = jsonPlaceholderService.createRequestTemplate('get-todo')
.withoutInstrumentation() //This can be ignored for now.
.withMethod('GET')
.withUriTemplate('/todos/{id}')
.logRequestDuration()
For our above example, if we looked internally at the components of the pipeline, we'd find the following:
[
() => //root of twine.
(context, next) => // usingHTTPS
(context, next) => // withMethod
(context, next) => // withUriTemplate
(context, next) => // logRequestDuration
(context, next) => // withParameters
]
When execute()
is called on the Request created from .createRequest()
, the pipeline's array of components begins to execute in reverse. So: withParameters
, logRequestDuration
, withUriTemplate
, withMethod
, and finishing with usingHTTPS
.
Understanding this ordering is quite important when you need to write components that rely on eachother. In the example, withUriTemplate
and withParameters
are components which rely on eachother. withParameters
accepts an object whose keys and values are used to replace the tokens ({keyName}
) that are present in the string provided to withUriTemplate
.
// RequestTemplate component
.withUriTemplate('/todos/{id}`)
// Request component
.withParameters({ id: todoId })
The withParameters
function is quite simple. All it does is store the object it was provided into the Twine Context so that the withUriTemplate
can retrieve it and use it to replace the tokens.
Request.prototype.withParameters = function(parametersHash) {
return this.addComponent((context, next) => {
context.environment['http.RequestParametersHash'] = parametersHash
return next()
})
}
And, for added clarity, a greatly simplified implementation of withUriTemplate
:
RequestTemplate.prototype.withURITemplate = function(templateUri) {
return this.addComponent((context, next) => {
const parametersHash = Object.assign({}, context.environment['http.RequestParametersHash'])
let path = templateUri
for (const key in parametersHash) {
const newPath = path.replace(new RegExp(`\\{${key}\\}`, 'ig'), parametersHash[key])
if (newPath !== path) {
delete parametersHash[key]
path = newPath
}
}
context.environment['http.RequestPath'] = path
return next()
})
}