Custom plugin definitions

Creating custom plugin definitions

Valtimo offers the functionality needed to create and add plugins to Valtimo implementations.

Prerequisites

Plugins are added to a Valtimo implementation. See https://docs.valtimo.nl/getting-started/first-dive/creating-your-own-valtimo-implementation to get started with your own Valtimo implementation.

Back-end

Creating a plugin definition

Dependencies

To create a custom plugin in your project, the following dependency is needed:

  • valtimo:plugin

for example:

    implementation("com.ritense.valtimo:plugin:$valtimoVersion") 

Creating a plugin class

A plugin can be created with the @Plugin annotation on the class. All classes with the plugin annotation are detected when the application is started. The plugin class should not be a Spring bean.

For example:

@Plugin(
  key = "sample",
  title = "Sample Plugin",
  description = "Sample plugin description",
)
class SamplePlugin(
...
}

Plugin properties

A plugin can be configured with properties. Properties can be added to the plugin class with the @PluginProperty annotation.

For example:

By setting the secret attribute in this annotation to true the plugin property will be marked as a secret. This is meant to be used for sensitive information (e.g. passwords or API keys). Secrets are encrypted before being stored in the database and will be automatically decrypted before use in the plugin.

These things should be kept in mind when creating the frontend components for secrets:

  • When editing an existing plugin configuration, the value will not be sent back to the frontend to avoid exposing sensitive data.

  • Only when submitting a value that is not null or an empty string will the property be updated.

  • This functionality requires an application property valtimo.plugin.encryption-secret. The value of this property determines the encryption key. The encryption secret has to be EXACTLY 16, 24 or 32 bytes.

Plugin action

A plugin class can have methods that are marked as actions through the @PluginAction annotation. These methods can then be used in a process definition through the use of process links. A single action can be linked to a task, and will run when that task is reached.

For example:

The complete example

In order to have an example with constructor parameters, let's also assume there is a SampleClient that is used by the plugin. The SampleClient is a regular Spring Bean. Based on the previous steps, the complete example now looks like this:

Creating a plugin factory

The newly created plugin class can not be used yet because Valtimo does not know how to create the new plugin. This problem is solved by creating a factory that extends the PluginFactory and registering it as a bean. The create() method has to be implemented to inject Spring beans or other objects that are necessary. This does not include plugin properties, as those are set automatically.

For example:

Optional additional steps

The backend part of the Sample plugin is now ready. Some topics have not yet been mentioned. These are optional and can be found at the bottom of this page. @PluginEvent @PluginCategory

Frontend

To develop a frontend plugin, the library @valtimo/plugin provides several interfaces which a frontend plugin must conform to, in order to be used in an implementation.

Plugin specification

First, a plugin specification conforming to the PluginSpecification interface needs to be created. Below is an example specification with explanations for each property:

sample.plugin.specification.ts

Plugin logos are provided as a Base64 encoded string. This string is then imported in the plugin specification as shown above. The size has to be kept down to a minimum, so logo images must be resized before encoding them to Base64. A maximum height of 60 pixels is advised.

This website is good for encoding images to Base64. After uploading, click show code, and then copy the string under For use in <img> elements:.

Here is a sample file of a Base64 encoded logo:

sample-plugin-logo.ts

Typing the plugin configuration data

The plugin configuration component listens to prefill data of the interface PluginConfigurationData, and outputs configuration data of this same interface. It is advised to extend this interface and further specify what data the plugin requires. For the sample plugin, this would be:

sample-plugin-config.ts

Implementing the plugin configuration component

How a configuration component for the sample plugin can be implemented is shown below. The way this is implemented can differ, as long as the interfaces are conformed to. Below is sample code of implementing the component using components from the library @valtimo/user-interface.

sample-plugin-configuration.component.ts

The corresponding template file looks like this:

sample-plugin-configuration.component.html

Function configuration components

Each plugin action received from the backend must have a corresponding function configuration component in the frontend. Implementing these components works in much the same way as implementing the plugin configuration component, The only difference is that function configuration components do not have to provide a configuration title. Below is sample code for the sample plugin action with the id sample-action:

sample-action-config.ts

sample-action-configuration.component.ts

sample-action-configuration.component.html

Adding the plugin module to the @NgModule

The main app.module.ts needs to be updated as well. The @NgModule needs to have added imports:

app.module.ts

Plugin translations

Plugin translate pipe

The template code shown above uses the pluginTranslate pipe to show translations from the plugin specification. To use this, first import PluginTranslatePipeModule from @valtimo/plugin and add it to the imports array of the plugin module. Now, the pluginTranslate pipe can be used in templates. It uses the following syntax:

sample-translation-pipe.component.html

The pipe returns an observable, so do not forget to add | async at the end.

translationKey refers to one of the translation keys specified in the plugin specification.

pluginId refers to the plugin's definition key. For the sample plugin, this would be sampleplugin. It is provided by the pluginId input on the configuration component by default.

Plugin translation service

If translation is to take place inside the component (as opposed to inside the template using the pipe), the PluginTranslationService may be used, which is exported by @valtimo/plugin. It supports a translate method, which returns an observable containing the translation, and an instant method, which returns a string containing the translation.

Plugin module

Finally, after implementing the components and specification, a module has to be defined for the plugin. This module, together with the specification, is then imported in the app module.

The sample plugin module would look like this:

sample-plugin.module.ts

Optional backend annotations

For the plugin backend example above, not all annotations have been used. In this section you find information for the annotations @PluginEvent and @PluginCategory.

Plugin events

A plugin class can have methods that need to be run on plugin creation, update or deletion. This can be achieved with the @PluginEvent annotation. These methods will then be invoked during the corresponding event of a Plugin Configuration lifecycle.

For example, startListener() will be executed when a Plugin Configuration is being created:

Available event types: CREATE, UPDATE and DELETE. It is possible to run a method on all three of these events by specifying so in the invokedOn argument of the annotation e.g. @PluginEvent(invokedOn = [EventType.CREATE, EventType.DELETE]). A PluginEventInvocationException with references to the plugin and any underlying exceptions will be thrown should a method ran as part of an event fail.

NB! Limitations

  • If the annotation contains duplicate event types, then the method will still only be invoked once per event type per annotation.

  • Only methods without arguments are supported.

  • Annotated methods are resolved in alphabetical order.

Plugin categories

Plugin categories denote a commonality between plugins. They can be applied to interfaces, so any plugin implementing this will belong to that category. When a plugin implements more than one interface with a category, it belongs to multiple categories. This can be used by the backend to create plugins that rely on other plugins to be configured, and can be used by the frontend to search for plugins in a specific category.

A plugin implementing an interface annotated with @PluginCategory can be autowired into a @PluginProperty of the same type on a different plugin.

The example below explains the implementation of a sample supplier for the Sample plugin.

First, the interface is defined that includes the required functionality. @PluginCategory with a key is added so that can be used to find configurations of that type in the frontend:

At least one implementation of the plugin is required. In this case, the PropertySampleSupplier implements the interface SampleSupplier and supports all required functionality. When searching for configurations for category sample-supplier, all stored PropertySampleSupplier configurations are found.

When creating a configuration of the SamplePlugin, the frontend should get and show a list of all available configuration of type sample-supplier. The ID of the chosen configuration will be part of the properties submitted for the creation of the SamplePlugin configuration. The @PluginProperty can reference the interface type corresponding to the category. The plugin will then be automatically injected with the corresponding configuration when using the SamplePlugin.

Last updated