Categories:
Aura bridge plugins
Aura bridge plugins are components that provide different functionalities to the bridge
Introduction
aura-bridge is composed of plugins, which provide functionality to the bridge. Plugins work independently, the same way as a service in a microservices oriented architecture: isolated, self-contained and without affecting other existing functionalities in the system.
The aura-bridge plugins system also allows an OB to generate new plugins and create an “ad hoc” aura-bridge, with the functionalities required and removing those which are not needed for the OB.
ℹ️ The practical process for the generation of plugins is included in develop a plugin and activate in aura-bridge.
Discover in the current documents detailed information regarding aura-bridge plugins:
- Types of plugins
- Plugins management
- Plugin structure and modules
- Global plugins catalog
- Plugins in specific channels and how to disable them
Types of plugins
There are different types of plugins:
-
Api. REST services that perform specific tasks not associated with the communication with a channel.
-
Client. These plugins define clients that can be used to communicate with a channel. These clients are normally used by processor plugins.
-
Processor. Plugins in charge of communicating with a channel, transforming the message received from a source channel to a destination channel.
-
Service. Utility plugins to be used by the rest of plugins.
Plugins management
As indicated in the previous section, aura-bridge uses the library@architect/architect for the management of plugins, so it is the architect library that is responsible for managing the dependencies injection in each module.
To create the architect application, aura-bridge uses the PluginManager module (located in the modules/plugin-manager folder). This module starts as the rest of modules at the aura-bridge start-up.
The PluginManager performs the following tasks:
- It starts the architect application with the plugins defined in
plugin-config.jsonfile, located at the root of the aura-bridge component. - It adds the core modules to the IOC context. See the section modules added by aura-bridge.
- It stores the information of each module defined in the plugins.
An example to define aura-bridge-example-service plugin of the previous section is shown below:
/* file: plugin-config.json */
[
"./lib/plugins/aura-bridge-example-service",
]
Currently, the plugins are in the src/plugins folder of aura-bridge, but in the future these plugins should be independent libraries and could be charged by library name (for example: @telefonica/aura-bridge-example-service).
Apart from the aura-bridge core environment variables, each plugin can define its own specific variables. Access the document Aura bridge environment variables and find them in the section corresponding to your plugin.
Plugin basic structure
Currently, aura-bridge uses @architect/architect library for plugins management.
A basic plugin must define at least:
- A
package.jsonfile defining the library, like any other JavaScript library, with apluginsection defining which modules it consumes and supplies. - A source code file that defines the modules that it supplies (
index.tsfor example).
The structure of this basic plugin is as follows:
aura-bridge-example-service
├── index.ts
└── package.json
A couple of examples with the content of each file are included below:
/* file: index.ts */
import { PluginType, registerPlugin } from '@telefonica/aura-bridge-common';
import { v4 as uuidv4 } from 'uuid';
import { Services } from './example-consume-services';
export = registerPlugin([
{
type: PluginType.Service, // Plugin service type
name: 'exampleService', // Name of the plugin service
instance: { // [provides] Instance that provides the module
getUniqueId() {
return uuidv4();
}
},
services: Services // [consumes] Needed modules are added here
}
]);
/* file: package.json */
{
"name": "@telefonica/aura-bridge-example-service",
"version": "1.0.0",
"main": "index.js",
"private": true,
"plugin": {
"consumes": [
"configurationManager"
],
"provides": [
"exampleService"
]
}
}
The modules specified in the plugin.consumes field define the services that are needed by this plugin. The modules specified in the plugin.provides field define the modules that this plugin offered.
Plugins modules
aura-bridge currently adds three modules that can be used by the different plugins. To use them, it is only necessary to add the package.json dependencies on plugin.consumes (like any other module/component).
- configurationManager: Module with the aura-bridge configuration information.
- auraBridgeCache: Module to manage the aura-bridge cache.
- prometheus: Service for metrics management.
A plugin can provide one or more plugin modules and each plugin module can be of a different type. Each type of module is intended to add a specific functionality to aura-bridge.
The existing types are defined in PluginType, which are described in the following sections.
export enum PluginType {
Api = 'Api',
Client = 'Client',
Processor = 'Processor',
Service = 'Service'
}
All plugins modules must follow the base Plugin interface:
export interface Plugin {
/**
* Plugin module name.
*/
name: string;
/**
* Object where the services that plugin module consume will be injected (IOC).
*/
services?: unknown;
/**
* Plugin module type.
*/
type?: PluginType;
/**
* @hapi/joi schema definition with the variables used by the plugin module.
*/
configuration?: { [key: string]: any; };
}
API plugin module
Use the aucli tool to generate the scaffolding of an API plugin: aucli bridge generate api
An API plugin module type mainly contains:
- At least one source code file that defines the API plugin module and controllers for each of the operations specified in the swagger definition.
- A
package.jsonfile defining the library, with apluginsection defining which modules it consumes and supplies. - A
swagger.yamlfile that contains a detailed description of the entire API defined by the plugin.
The basic structure for a API plugin module type is as follows:
aura-bridge-example-api
├── index.ts
├── package.json
└── swagger.yaml
A type API module must implement the PluginApi interface:
export interface PluginApi extends Plugin {
/**
* List of controllers (Express) defined by the plugin module.
*/
controller: { [operationId: string]: PluginApiController | ((req: Request, res: Response) => Promise<void>) };
}
export interface PluginApiController {
/**
* Is directline processor?
*/
isDirectlineProcessor?: boolean;
/**
* Channel type.
*/
channelType?: BridgeType;
/**
* Express controller function.
*/
controller: (req: Request, res: Response) => Promise<void>;
/**
* Custom error handler.
*/
errorHandler?: (err: any, request: express.Request, response: express.Response, next: express.NextFunction) => void;
}
A couple of examples with the content of each file are included below:
/* file: index.ts */
import { PluginType, registerPlugin } from '@telefonica/aura-bridge-common';
import { Services } from './example-api-consume-services';
const controller = {
hello: (req: Request, res: Response) => {
res.send({ message: 'Hello from example api!' });
}
}
export = registerPlugin([
{
type: PluginType.Api, // Plugin module type
name: 'exampleApi', // Plugin module name
controller, // Controllers definitions (linked to swagger operationId),
// or it is possible to also use the PluginApiController interface
services: Services, // [consumes] Needed modules are added here
}
]);
/* file: package.json */
{
"name": "@telefonica/aura-bridge-example-api",
"version": "1.0.0",
"main": "index.js",
"private": true,
"plugin": {
"consumes": [ ],
"provides": [
"exampleApi"
]
}
}
# file: swagger.yaml
openapi: 3.0.0
info:
title: aura bridge example api
...
paths:
/example/hello:
get:
operationId: hello # Function name defined in "controller".
x-router-controller: plugins # It should always be "plugins".
responses:
'200':
description: OK
headers: { }
An API module can also define its own error handling and not use the default error handling defined in the bridge core.
Example:
public errorHandler(err: any, request: express.Request, response: express.Response, next: express.NextFunction) {
const corr = (request as any).correlator;
let status: number = 200;
let message: string = 'Accepted request';
if (err.statusCode === 401) {
status = err.statusCode;
message = 'Unauthorized request';
}
// An error in swagger validation is simply logged as a warning
if (err.failedValidation) {
logger.warning({
msg: `Bad request on swagger validation. The first error message was: ${err?.validationResult[0]?.message}`,
stck: err.validationResult, corr,
step: AuraBridgeStep.Controller
});
}
response.status(status).send({ code: status, message });
}
Client module
Use the aucli tool to generate the scaffolding of a client module: aucli bridge generate client
A client module type mainly contains:
- At least one source code file that defines the client module and the client itself.
- A
package.jsonfile defining the library, with apluginsection defining which modules it consumes and supplies.
The basic structure for a client module type is as follows:
aura-bridge-example-client
├── index.ts
├── package.json
A type client module must implement the PluginClient interface:
export interface PluginClient extends Plugin {
/**
* Client instance.
*/
instance?: any;
/**
* Returns a client instance if it must be initiated asynchronously.
*/
getInstance?: () => any;
}
The instance field or the getInstance method should be used to obtain the client, but not both. If the client does not need to be initiated asynchronously, the client definition in the instance field is the preferred option. If it is necessary to asynchronously start the client, the getInstance method must be used.
A couple of examples with the content of each file are included below:
/* file: index.ts */
import { PluginType, registerPlugin } from '@telefonica/aura-bridge-common';
import { Services } from './example-consume-services';
import { ExampleClient } from './example-client';
export = registerPlugin([
{
type: PluginType.Client,
name: 'ExampleClient',
instance: new ExampleClient(),
services: Services
}
]);
/* file: package.json */
{
"name": "@telefonica/aura-bridge-example-client",
"version": "1.0.0",
"main": "index.js",
"private": true,
"plugin": {
"consumes": [
"configurationManager"
],
"provides": [
"ExampleClient"
]
}
}
/* file: example-client.ts */
import { AuraBridgeClient, AuraBridgeRequestInfo, SendMessageOptions } from '@telefonica/aura-bridge-common';
import { AuraLogger } from '@telefonica/aura-logging';
import { Services } from './example-consume-services';
const logger: AuraLogger.AuraBusEmitter = new AuraLogger.AuraBusEmitter('ExampleClient');
export class ExampleClient extends AuraBridgeClient {
public constructor() {
super('ExampleClient', Services.configurationManager.environmentConfiguration);
}
public async sendMessage(message: any, options: SendMessageOptions): Promise<void> {
const { corr }: Partial<AuraBridgeRequestInfo> = options.requestInfo;
// TODO: Implement the message sending logic here.
}
}
A basic client should only extend the AuraBridgeClient class and implement the sendMessage method. The client can define any other method, but the sendMessage method will be used by the processors to send a message to a destination.
If the client uses the OAuth protocol, it is possible to add the OpenID authorization information to the client and automatically update the access token for the communication. To make use of this functionality, it is necessary that the new client extends from AuraBridgeClientOAuthTokens class instead of AuraBridgeClient class:
/* file: example-client.ts */
import { AuraBridgeClientOAuthTokens, AuraBridgeRequestInfo, SendMessageOptions } from '@telefonica/aura-bridge-common';
import { AuraLogger } from '@telefonica/aura-logging';
import { Services } from './example-consume-services';
const logger: AuraLogger.AuraBusEmitter = new AuraLogger.AuraBusEmitter('ExampleClient');
export class ExampleClient extends AuraBridgeClientOAuthTokens {
public constructor() {
super(
'ExampleClient',
Services.configurationManager.environmentConfiguration,
[] // TODO: Add list ClientOAuthInformation here
);
}
public async init(): Promise<void> {
await this.refreshTokens(); // <-- It is responsible for updating the tokens before it expire.
}
public async sendMessage(message: any, options: SendMessageOptions): Promise<void> {
const { corr }: Partial<AuraBridgeRequestInfo> = options.requestInfo;
// TODO: Implement the message sending logic here.
}
}
In addition to the refreshTokens method, the AuraBridgeClientOAuthTokens class defines the following methods:
addOpenIdClients: Add new clients to the list to update your tokens.getTokenByClientId: Get token by client id.stopRefreshTokens: Stop refresh client tokens.
Processor module
Use the aucli tool to generate the scaffolding of a processor module: aucli bridge generate processor.
As a general rule, a processor is responsible for converting an input message (from a “source” channel) to an output message (that will be sent to a “destination” channel).
Processor flow
To help with the task of generating a processor (in addition to the aucli tool) , the aura-bridge-common library provides the AuraBridgeFlow class that allows to define the converter and the client that will be used to convert and send the incoming message to the destination in a simple and declarative way.
const flow: BridgeFlow = {
source: { // Source from where the request is initiated
type: BridgeNodeType.Directline
},
destination: { // Destination where the request will be redirected
type: BridgeNodeType.TestModel,
converter: DirectlineToTestModelConverter,
client: services.testModelClient,
clientOptions: { queue: { bridgeFlowName: directlineToTestModelFlow.name } }
},
onError: {
// ...
}
};
In the same way, in case an exception occurs, it is possible to define the list of channels that should be informed.
const flow: BridgeFlow = {
source: {
// ...
},
destination: {
// ...
},
onError: { // How and which channels should be informed on exception
destinations: [
{
type: BridgeNodeType.TestModel,
converter: ErrorToTestModelApiConverter,
client: services.testModelClient,
decode: TestModelDestinationResponseError
},
{
type: BridgeNodeType.Directline,
converter: ErrorToDirectlineEventConverter,
client: services.directlineClient,
decode: directlineDestinationResponseError
}
]
}
};
Declaring a destination
The destination field must follow the interface BridgeDestination.
export interface BridgeDestination extends BridgeNode {
/**
* Client instance that will be used to send the message to the destination
*/
client: AuraBridgeClient;
/**
* Client options. System to send the message used: queue, retries, etc
*/
clientOptions?: SendMessageClientOptions;
/**
* Converter to use
*/
converter?: AuraBridgeMessageConverter;
/**
* Relationship between exception and response error
*/
decode?: ExceptionResponseError[];
/**
* Optional condition for a destination to be executed
*/
condition?: (error?: Error, lastError?: Error, requestInfo?: AuraBridgeRequestInfo, message?: any) => boolean;
}
The process to send a message using the previous definition is shown below:
If the indicated condition is true, the message will be converted (using converter) and sent (using client with options defined in clientOptions field). If during the process there is an error that throws an exception, all the destinations defined in onError.destinations field will be executed in order in the same way.
The following diagram shows the execution sequence:
flowchart LR
subgraph source
A[Message]
end
subgraph destination
direction LR
A[Message] --> Check{check condition}
Check -->|True| Converter[Converter]
Check -->|False| End[end]
Converter --> |Converted message| Client[Client]
help[Send message using clientOptions]
end
When an exception is thrown, all the destinations defined in onError.Destinations are processed, passing both the original message and the exception occurred:
flowchart LR
subgraph onError information
Message[Message]
Exception[Exception message]
end
subgraph onError destinations
direction LR
Message[Message] --> CheckOnError{check condition}
Exception --> CheckOnError{check condition}
CheckOnError -->|True| ConverterOnError[Converter]
CheckOnError -->|False| EndOnError[Next destination]
ConverterOnError --> |Converted message| ClientOnError[Client]
helpOnError[Send message using clientOptions]
end
Sending a specific message on exception
When an exception occurred during the execution of a flow (BridgeFlow), it is possible to decode the exception in a specific error message for each destination defined in onError.destinations field.
The list that relates the exception with each message is added in the field decode of BridgeDestination. Each of these elements must implement the following interface:
/**
* @interface ExceptionResponseError
*/
export interface ExceptionResponseError {
/**
* Name error from exception
*/
name?: string;
/**
* Message to user key
*/
messageToUserKey?: string;
/**
* Error status from exception
*/
status?: number;
/**
* Error response code
*/
responseCode?: string;
/**
* Response error information
*/
response: ResponseError;
}
In the following code, the example shows how to send a message with status 500 and message key bridge:error.transform when an error in the converter has occurred:
const exampleResponseError: ExceptionResponseError[] = [
{
name: AuraBridgeConverterError.name,
response: {
status: StatusCodes.INTERNAL_SERVER_ERROR,
description: 'Internal bridge error converting input message to output format',
messageToUserKey: 'bridge:error.transform'
}
}
];
Basic structure of the processor module
The processor module type mainly contains:
- At least one source code file that defines the processor plugin module and controllers for each of the operations specified in the swagger definition.
- A
package.jsonfile defining the library, with apluginsection defining which modules it consumes and supplies. - A
swagger.yamlfile that contains a detailed description of the entire API defined by the plugin. - A source code file that defines the
BridgeFlow. - A source code file that defines the
AuraBridgeMessageConverter.
The basic structure for a processor plugin module type is as follows:
aura-bridge-example-processor
├── index.ts # Processor plugin module and controller
├── converter.ts # Converter (AuraBridgeMessageConverter)
├── flow.ts # Flow (BridgeFlow)
├── package.json
└── swagger.yaml # Api definition
A type processor module must implement the PluginProcessor interface:
export interface PluginProcessor extends PluginApi {
/**
* List of defined flows.
*/
flows: BridgeFlow[];
}
In addition to the definition of flows, the same requirements as for an API plugin type are applied.
If the processor plugin must manage the aura-groot response (using DirectLine) for a channel type, it is possible to use the PluginApiController interface to define the controller.
For example:
/* Manage aura-groot response messages for whatsapp channels */
postDirectlineConversationsActivities: {
isDirectlineProcessor: true, // Indicates if it is a directline processor
channelType: BridgeType.Whatsapp, // Channel type
controller: directlineWhatsappController.controller // Controller definition
}
In this way, all the messages sent for the WhatsApp type channel to /aura-services/{channelName}/v3/conversations/{conversationId}/activities and
/aura-services/{channelName}/v3/conversations/{conversationId}/activities/{messageId} endpoints will be managed by the defined controller.
⚠️ These endpoints do not need to be defined in the plugin swagger, since they are supplied by the aura-bridge core.
A couple of examples with the content of each file are shown below:
/* file: index.ts */
/* description: Using controller function */
import ...
const controller = {
exampleNotification: async (req: Request, res: Response) => {
const env = Services.configurationManager.environmentConfiguration;
res.status(200).send();
await AuraBridgeFlow.process(exampleToDirectlineFlow(Services), getRequestInformation(req, env), req.body);
}
};
export = registerPlugin([
{
type: PluginType.Processor, // Plugin module type
name: 'exampleProcessor', // Plugin module name
controller, // Controllers definitions (linked to swagger operationId)
flows: [exampleToDirectlineFlow], // Flows
services: Services // [consumes] Needed modules are added here
}
]);
/* file: index.ts */
/* description: Using PluginApiController interface */
import ...
const controller = {
// This plugin manages all the requests from aura-groot for the testModel channel type destination
postDirectlineConversationsActivities: {
isDirectlineProcessor: true,
channelType: BridgeType.TestModel,
controller: async (req: Request, res: Response) => {
...
}
}
};
export = registerPlugin([
{
type: PluginType.Processor, // Plugin module type
name: 'directlineTestModelProcessor', // Plugin module name
controller, // Controllers definitions (linked to swagger operationId)
flows: [exampleToDirectlineFlow], // Flows
services: Services // [consumes] Needed modules are added here
}
]);
/* file: converter.ts */
import ...
export class ExampleConverter extends AuraBridgeMessageConverter {
public static async message(incomingMessage: ExampleMessage) {
// Logic of the converter here
}
}
/* file: flow.ts */
/**
* Flow: example to directline.
*
*
* [Example] ─────> (bridge) ───(ok)──────> [Directline]
*
* @returns {BridgeFlow} flow
*/
export function asyncCallbackToDirectlineFlow(services: any): BridgeFlow {
const flow: BridgeFlow = {
source: {
type: BridgeNodeType.Example
},
destination: {
type: BridgeNodeType.Directline,
converter: ExampleConverter,
client: services.directlineClient,
clientOptions: { retries: {} }
},
onError: {
destinations: [
...
]
}
};
return flow;
}
/* file: package.json */
{
"name": "@telefonica/aura-bridge-example-processor",
"version": "1.0.0",
"main": "index.js",
"private": true,
"plugin": {
"consumes": [ ... ],
"provides": [
"exampleProcessor"
]
}
}
# file: swagger.yaml
openapi: 3.0.0
info:
title: aura bridge example processor
...
paths:
/example/notification:
get:
operationId: exampleNotification # Function name defined in "controller".
x-router-controller: plugins # It should always be "plugins".
responses:
'200':
description: OK
headers: { }
Service module
Use the aucli tool to generate the scaffolding of a service plugin: aucli bridge generate service
A service plugin module is a set of utilities that can be reused by the rest of plugins and mainly contains:
- At least one source code file that defines the service plugin module and the service itself.
- A
package.jsonfile defining the library, with apluginsection defining that modules consume and supplies.
The basic structure for a service plugin module type is as follows:
aura-bridge-example-service
├── index.ts
└── package.json
A type service module must implement the PluginService interface:
export interface PluginService extends Plugin {
/**
* Service instance.
*/
instance?: any;
/**
* Returns a service instance if it must be initiated asynchronously.
*/
getInstance?: () => any;
}
The instance field or the getInstance method should be used to obtain the service, but not both. If the service does not need to be initiated asynchronously, the service definition in the instance field is the preferred option. If it is necessary to asynchronously start the service, the getInstance method must be used.
A couple of examples with the content of each file are shown below:
/* file: index.ts */
import { PluginType, registerPlugin } from '@telefonica/aura-bridge-common';
...
const exampleService = {
isActionMessage: (message: ExampleMesageModel) => {
return !!message?.channelData?.customData?.action;
}
}
export = registerPlugin([
{
type: PluginType.Service, // Plugin module type
name: 'exampleService', // Plugin module name
instance: exampleService, // Service instance
services: Services // [consumes] Needed modules are added here
}
]);
/* file: package.json */
{
"name": "@telefonica/aura-bridge-example-service",
"version": "1.0.0",
"main": "index.js",
"private": true,
"plugin": {
"consumes": [ ],
"provides": [
"exampleService"
]
}
}
Plugins in specific channels
There is a relationship between a channel and the different plugins that give full support to the channel in aura-bridge. This is because plugins can supply: services, incoming messages management, output messages management, etc.
Therefore, to support a channel like WhatsApp, several plugins may be necessary and knowing that relationship allows to enable/disable the support to the complete channel in aura-bridge correctly.
Plugins for WhatsApp channel
| Plugin | Dependencies | Core dependencies |
|---|---|---|
| whatsapp-incoming-processor | whatsapp-service, directline-service | configurationManager, prometheus |
| directline-whatsapp-processor | whatsapp-service, directline-service | configurationManager |
| directline-whatsapp-service | whatsapp-service, directline-service | configurationManager |
| whatsapp-service | configurationManager, prometheus | |
| directline-service | configurationManager, auraBridgeCache |
How to disable the WhatsApp channel
To disable the WhatsApp channel, it is necessary to take into account the following:
-
The
whatsapp-incoming-processoranddirectline-whatsapp-processorplugins are not dependent on any other plugin, so they can be removed from theplugin-config.jsonfile. -
The
directline-whatsapp-service,whatsapp-serviceanddirectline-serviceplugins can be used by other plugins and can only be removed if they are not needed.
Plugins for async-callback
| Plugin | Dependencies | Core dependencies |
|---|---|---|
| async-callback-directline-processor | directline-service | configurationManager, prometheus |
How to disable the async-callback functionality
The async-callback-directline-processor plugin is not dependent on any other plugin, so it can be removed from the plugin-config.json file to completely disable the async-callback functionality.
Plugins for Genesys
| Plugin | Dependencies | Core dependencies |
|---|---|---|
| genesys-directline-processor | directline-service | configurationManager |
How to disable the Genesys functionality
The genesys-directline-processor plugin is not dependent on any other plugin, so it can be removed from the plugin-config.json file to completely disable the Genesys functionality.