Plugins
Learn how you can build your own plugins to customize and extend the Jovo Framework. For lightweight plugins, take a look at hooks.
Introduction
Jovo plugins allow you to hook into the Jovo middleware architecture to extend or modify the framework without having to change its core code. Usually, this is used to hook into the RIDR Lifecycle, but it's also possible to hook into events.
Here are a few use cases where plugins can be helpful:
- Modify Jovo properties: For example, you could hook into
before.platform.output
and make changes to the output before it gets turned into a native platform response. - Logging: You could hook into specific middlewares and log things. For example, you could could send requests and responses to a monitoring service.
- Retrieve data: You could call an API, for example a content management system (CMS), and store the data in a property to use in handlers or output classes.
- Our database, NLU, and platform integrations are also plugins.
We recommend first taking a look at the get started with Jovo plugins section before diving deeper into advanced Jovo plugins.
Get Started with Jovo Plugins
This section provides a first overview of Jovo plugins. First we're going to take a look at the basic plugin structure, then at potential plugin configurations. After that, we're going to add the plugin to our Jovo app.
Basic Plugin Structure
Here is an example of a basic plugin called SomePlugin
:
// src/plugins/SomePlugin.ts import { Jovo, HandleRequest, Plugin, PluginConfig, Extensible, InvalidParentError, } from '@jovotech/framework'; export class SomePlugin extends Plugin { mount(extensible: Extensible) { if (!(extensible instanceof HandleRequest)) { throw new InvalidParentError(this.constructor.name, HandleRequest); } extensible.middlewareCollection.use('<middleware>', (jovo) => { return this.someMethod(jovo); }); } someMethod(jovo: Jovo) { // ... } getDefaultConfig(): PluginConfig { return {}; } }
The plugin above includes the following methods:
mount
: Use themiddlewareCollection.use
method to hook into middlewares. You can addbefore
andafter
, e.g.before.platform.output
. Find all middlewares in the RIDR docs. Depending on the type of the plugin, it's also possible to use different (or additional) methods likeinstall
. Learn more about this and the plugin lifecycle below.- Some method (in this example
someMethod
, but you can choose any name) that gets called when the middleware referenced ininstall
gets executed. This is where your plugin gets to work. Through thejovo
parameter, you have access to all Jovo properties, e.g.jovo.$output
. getDefaultConfig
: If your plugin uses configuration, you can return the default config here. This method has to be implemented by every plugin, even if it just returns an empty object as shown in the example above. Learn more in the plugin configuration section below.
We recommend putting each plugin into a separate file in a plugins
folder. In this case, the SomePlugin
above would be located at plugins/SomePlugin.ts
.
Plugin Configuration
If your plugin needs configuration (for example API keys), you can pass a generic type parameter that extends PluginConfig
to Plugin
(see Plugin<SomePluginConfig>
below).
For SomePlugin
it could look like this:
import { PluginConfig /* ... */ } from '@jovotech/framework'; // ... export interface SomePluginConfig extends PluginConfig { someConfig: string; // ... } export class SomePlugin extends Plugin<SomePluginConfig> { // ... someMethod(jovo: Jovo) { console.log(this.config.someConfig); // ... } getDefaultConfig(): SomePluginConfig { return { someConfig: 'someString', }; } }
The following properties and methods are related to the configuration:
config
: The actual configuration of the plugin. Can be accessed from plugin methods usingthis.config
.initConfig
: The initial configuration that was passed in the constructor if there was any. Can be accessed from plugin methods usingthis.initConfig
.getDefaultConfig()
: Returns the default configuration of the plugin. Has to be implemented by every plugin.
Configuration can be passed to the constructor of the plugin (see add a plugin to the Jovo app below), which will be merged with the default configuration from getDefaultConfig()
. If no configuration is passed, the default configuration will be used.
Add a Plugin to the Jovo App
Import the plugin and add it to the app configuration like this:
import { SomePlugin } from './plugins/SomePlugin'; // ... const app = new App({ plugins: [ new SomePlugin(), // ... ], // ... });
You can also add it by calling use
:
app.use(new SomePlugin());
If your plugin uses configuration, you can add it to the constructor like this:
new SomePlugin({ someConfig: 'someValue', });
Advanced Jovo Plugins
After getting an initial understanding of how to create and add a plugin from the getting started section, let's dive a bit deeper and take a look under the hood and at some advanced plugin structures.
First, we're going to take a look at the plugin lifecycle and how plugin mounting works. We'll also learn more about parent and child plugins using the Extensible structure.
If you want to dive even deeper, take a look at the Plugin
class here.
Plugin Lifecycle
In the basic plugin structure section, we used the mount
method to define which middlewares should be used for this plugin:
import { Jovo, HandleRequest, Plugin, Extensible, InvalidParentError } from '@jovotech/framework'; export class SomePlugin extends Plugin { mount(extensible: Extensible) { if (!(extensible instanceof HandleRequest)) { throw new InvalidParentError(this.constructor.name, HandleRequest); } extensible.middlewareCollection.use('<middleware>', (jovo) => { return this.someMethod(jovo); }); } // ... }
It's also possible to use other methods for this, which we call plugin lifecycle hooks. Below is a table of all available methods:
Name | Trigger | Use Case | Notes |
---|---|---|---|
install | When the plugin is installed via use (once) | Installing other plugins as well as modifying App | Can only be synchronous. |
initialize | When App.initialize is called (once) | Time-consuming actions like API-calls that only need to be done once | Can be asynchronous. |
mount | When plugins are mounted onto HandleRequest (every request) | Registering middleware-functions | Can be asynchronous. |
dismount | After the RIDR Lifecycle (every request) | Cleanup | Can be asynchronous. |
It's important to note that the install
and initialize
plugin lifecycle hooks don't have access to HandleRequest
, since they happen when the app gets started, before the request gets handled. Learn more in the plugin mounting section below.
Here is how install
looks like with App
instead of HandleRequest
:
import { Jovo, App, Plugin, Extensible, InvalidParentError } from '@jovotech/framework'; export class SomePlugin extends Plugin { install(extensible: Extensible) { if (!(extensible instanceof App)) { throw new InvalidParentError(this.constructor.name, App); } extensible.middlewareCollection.use('<middleware>', (jovo) => { return this.someMethod(jovo); }); } // ... }
For more details about signatures, take a look at the Plugin
class here.
Plugin Mounting
On every request, the mounting takes place, which consists of the following steps:
- Every plugin and nested child plugin in
App
is cloned. - The cloned plugins get referenced in the
config
andplugins
ofHandleRequest
under same path as they were forApp
.
The config
of the plugins is now the request config. Changes to the request config are just applied during this request and do not mutate the original config.
Due to the request config getting set during mounting, the mount
-lifecycle-hook should be used for registering middlewares.
Jovo Properties
You can also add your own Jovo properties using a plugin:
// MyPropertyPlugin.ts import { Jovo, HandleRequest, Plugin, PluginConfig, Extensible, InvalidParentError, } from '@jovotech/framework'; // Add the $myProperty type to the Jovo class declare module '@jovotech/framework/dist/types/Jovo' { interface Jovo { $myProperty: MyPropertyPlugin; } } export class MyPropertyPlugin extends Plugin { mount(extensible: Extensible) { if (!(extensible instanceof HandleRequest)) { throw new InvalidParentError(this.constructor.name, HandleRequest); } // Add the $myProperty property to the Jovo object extensible.middlewareCollection.use('before.request.start', (jovo) => { jovo.$myProperty = new MyPropertyPlugin(this); }); } // Sample method that can be called using $myProperty myFunction(jovo: Jovo) { // ... } getDefaultConfig(): PluginConfig { return {}; } }
As a result, you can use this in your handler:
this.$myProperty.myFunction();
Jovo Extensible Structure
Besides normal plugins, there are also plugins that extend Extensible
which itself extends Plugin
.
The main difference to normal plugins is that these plugins have a MiddlewareCollection
and can have child plugins.
Extensible
has two optional generic type parameters:
- The type of the plugin's configuration that has to extend
ExtensibleConfig
- The names of the middlewares in case type-hinting for the
MiddlewareCollection
should work
Every class that extends Platform
as well as App
extend Extensible
.
Add a Plugin as a Child
Similar to the add plugin to your Jovo app section, you can add the child plugin to the extensible plugin either by using the plugins
array of the constructor or the use
method.
Here's how you can add it using the constructor:
import { SomeExtensiblePlugin } from './plugins/SomeExtensiblePlugin'; import { SomePlugin } from './plugins/SomePlugin'; // ... const app = new App({ plugins: [ new SomeExtensiblePlugin({ plugins: [new SomePlugin()], }), // ... ], // ... });
And here's a version with use
:
const extensiblePlugin = new SomeExtensiblePlugin(); extensiblePlugin.use(new SomePlugin());