UnityBase Messaging

Overview

UnityBase messaging subsystem consists of the following components:

  • Notifications: send messages from server to currently active users
  • Exchange: server to server messages exchange

Both components uses RabbitMQ as a transport, and there is set of services developed (out of scope of this package) to facilitate this. This particular package shall be part of solution as a UnityBase model. It contain both client and server APIs.

Notification

Notification of UnityBase users:

  • Uses JWT technology for managing permissions of UnityBase clients to AMQP resources and topics
  • Allows to send messages to:
    • administrative subjects: users / roles / groups
    • organizational units: staff unit, organization, etc, when use @unitybase/org model
    • customize what group of users to send to
    • broadcast messages, send to all users
  • Receive messages from users

Object- vs Subject- centric messages

There different cases, when sever needs notify client about something:

  • Notify all clients - broadcast
  • Notify some clients - subject-centric messages
  • Notify clients, who cares about some events - object-centric messages

These different types of notifications are organized into a topic system, where top-level topic determine notification type:

  • inbox.300010203
  • broadcast
  • entity.pln

Entity mixin

In order to add notification server method to an entity, add a mixin to the entity:

{
  "mixins": {
    "notification": null
  }
}

Make sure to configure ELS accordingly, to allow users to send messages using this method. As for receiving messages, any user who has ELS to select method, will be able to receive messages.

SECURITY NOTE!!! RLS is not applied to messages, so users will be able to receive messages even for instances, then do not have RLS permissions to. So, unlike inbox notifications, these notification messages are not as secure, do NOT send in those messages any sensitive information, use it purely for signaling purposes.

Messages will be sent to topic equal to entity model name. But this is customizable:

{
  "mixins": {
    "notification": {
      "topic": "custom_topic"
    }
  }
}

Message deduplication

There might be scenarios, when server sends messages for multiple roles, and if the current user is a member of more than one of them, it will receive the same message more than once. In order to prevent that, this class has message deduplication code based on unique message ID assigned by server.

Message exchange

Interaction with external services:

  • Allows to send messages to external services
  • Allows to receive messages from external services
    • API for registering onMessage handler for message type

Getting Started

Install package and add model to ubConfig.json

Install npm packages:

npm install @unitybase/messaging

Add @unitybase/messaging as a UnityBase model into ubConfig.json:

{
  "application": {
    "domain": {
      "models": [
        {"path": "./node_modules/@unitybase/messaging"}
      ]
    }
  }
}

After that, make sure to execute ub-migrate at least once, so that roles would be created and give access to appropriate users on UnityBase HTTP endpoint provided by this model.

Add some server-to-client notification code

Add the following code somewhere to your application:

const {notifyUser} = require('@unitybase/messaging/notification')

// Send a message to a user, who's profile was updated
uba_user.on('update:before', ctx => {
  notifyUser(
    ctx.mParams.execParams.ID,
     {
       userID: ctx.mParams.execParams.ID,
       operation: 'update'
     },
     'profile.updated'
    )
})

This shall send a notification message from server to user, every time, user's profile updated. Message will be actually delivered only if the user currently logged in. It won't be stored. So, server to client notifications are not to reliably deliver messages to user, but to make interaction between server and currently logged in user.

Make sure the code above is located in a javascript file, which is loaded on server start, require this file in index.js, if needed.

Add some reaction on message on a client

Add the following code somewhere in your client-side code, make sure the file is actually run. For example, you may put this code in initModel.js to guarantee that.

UB.messaging.ClientNotifier.on('message', m => alert(JSON.stringify(m)))

This will alert with any incoming message from server.

Configure interaction with rabbitMQ in ubConfig.json

Before the whole example work, we need to set up additional services. Apart from RabbitMQ itself, the following services needed:

  • JWT issuer, to make request for JWT issuing
  • AMQP to UB Gateway, to make requests for sending messages:
    • @unitybase/messaging does not rely on AMQP client compiled into UnityBase binary code, but it uses an external service to send messages.

URLs to both services MUST be set up in customSettings section of the configuration file, for this "Getting Started" guide, services are configured to localhost:

{
  "uiSettings": {
    "adminUI": {
      "amqpNotificationUrl": "ws://localhost:15674/ws"
    }
  },

  "application": {
    "customSettings": {
      "messaging": {
        "transport": "rabbitMQ:router-service"
      },

      "rabbitMQ": {
        "vhost": "/",
        "routerUrl": "http://localhost:8081",
        "jwtIssuerUrl": "http://localhost:8082",
        "notifyExchange": "user-notify"
      }
    }
  }
}

uiSettings.adminUI.amqpNotificationUrl is needed so that client knows where to connect to.

Now, UnityBase application is ready to run, start it.

Run a docker composition

This package contains an example of how to configure services for a developer environment using Docker Composition. Make sure you have installed Docker for your operating system.

Copy content of https://gitlab.intecracy.com/unitybase/bpm/tree/master/packages/messaging/doc/examples/messaging_dev_services into your project, and in context of that directory run the following command:

docker-compose up --build

It assumes your already have up and running UnityBase on local port 8881, so if you have not started it yet, start it.

Testing it

Log into the test application.

Try to update the current user profile, for example, the admin user. If everything was done right, you will see a notification alert message.

If you experience any problems with it, the package contains a working example with this example: https://gitlab.intecracy.com/unitybase/bpm/tree/master/packages/messaging/doc/examples/ub-app

Using UnityBase Messaging

RabbitMQ configuration

RabbitMQ MUST be configured to allow UnityBase issued JWT as a user password. See documentation for rabbitmq-jwt-auth project on how to do that. Make sure that shared secret for rabbitmq-jwt-auth service matches the secret used for jwt-issuer service, as well as other parameters: issuer, audience, subject.

Create an exchange for notifications:

  • Exchange name: default is user-notify
  • Exchange type: topic

Notify users

Notify users: server

Send a message to a particular user or group or role or staff unit:

const {notifyUser} = require('@unitybase/messaging/notification')

// Some userID...
let userID = 10

// Send a message to a user about new task assigned
notifyUser(userID, 'bpm.task.assigned', {
  taskID,
  dueDate: new Date()  
})

// Some orgID...
let orgID = 123456

// Send a message to all organization users about new CEO
notifyUser(orgID, 'message', {
  text: 'Our organization has a new CEO!'
})

Send message to every user:

const {broadcast} = require('@unitybase/messaging/notification')

// ...

// Broadcast message to all users about maintenance
broadcast('maintenance', {
  severity: 'high',
  message: 'Application will be updated to a new version next weekend'
})

Notify users: client

To listen to client-side events, there is message event.

UB.messaging.ClientNotifier.on(
  'message',
  function(payload) {
    // ...
  }
)

To unsubscribe, there is corresponding method un.

To listen to object-centric events, use pair of methods onEntityEvent and unEntityEvent to subscribe and unsubscribe events, for example, when using with Vue components:

function onOpen(message) {
  const senderUser = Repository('uba_user')
    .attrs('fullName', 'name')
    .selectById(message.senderID)
    .then(
      function (senderUser) {
        uiServices.$notify({
          message: `User ${senderUser.fullName || senderUser.name} opened Document ${store.state.docNumber} for editing`
        })
      }
    )
}

new Vue({
  el,
  render: h => h(Root),
  store,
  created() {
    UB.messaging.ClientNotifier.onEntityEvent('doc_Document', instanceID, 'clientAction', 'open', onOpen)
  },
  destroyed() {
    UB.messaging.ClientNotifier.unEntityEvent('doc_Document', instanceID, 'clientAction', 'open', onOpen)
  }
})

It is crucial to always unsubscribe from event, when listening is is not needed anymore, to avoid memory leaks and misbehavior of application, like triggering event handlers when they are not supposed to, or to avoid double triggering the same handler, when re-subscribe to the same event.

Publish event from client

Entity-centric events could be published from clients using UB.messaging.ClientNotifier.publishEntityClientAction function. There shall be ELS configured for notification method and there MUST be notification plugin set for the entity.

UB.messaging.ClientNotifier.publishEntityClientAction('doc_Document', instanceID, 'open')

Exchange messages

Exchange messages: receive

To receive messages, call subscribeToMessageType function on server start. It receives a callback called on message receiving:

const {subscribeToMessageType} = require('@unitybase/messaging/exchange')

subscribeToMessageType('doc_Document.published', documentPublished)
subscribeToMessageType('doc_Document.unpublished', documentUnpublished)

function documentPublished(payload) {
  // ...
}

function documentUnpublished(payload) {
  // ...
}

Router service will create a permanent queue per subscription, messages won't be lost, even when UnityBase server is not available.

Exchange messages: send

const {publishMessage} = require('@unitybase/messaging/exchange')

function sendToExternalSystem(messageType, messageBody) {
  console.log('Network: sending message: "%s"', messageType)

  publishMessage(
    EXCHANGE_NAME,
    `main_topic.${messageType}`,
    messageBody,
    true
  )
}

Advanced server-side scenarios

Support additional subject IDs in token

Subscribe to messaging:notifications:jwt_subject_ids event to push additional IDs to subject IDs of the user, so that when notification is sent using that ID, the current user will be notified.

const {Session} = require('@unitybase/ub')

Session.on('messaging:notifications:jwt_subject_ids', (/** @type {number[]} */ admSubjIDs) => {
  /** @type {number} */
  let myID = 1234

  admSubjIDs.push(myID)
})

Adjust rules in JWT payload

Subscribe to customize rules, which will be put into JWT used to RabbitMQ user authorization:

const {Session} = require('@unitybase/ub')

Session.on('messaging:notifications:jwt_payload', /** @type {JwtPayload} */ data => {
  data.rules.push('read:vhost2/queue1234')
})

See documentation for rabbitmq-jwt-auth project on format of data object.

Customize client-side configuration

Client-side utility gets information about what to listen from server. It is possible to customize that info from server side:

Session.on('messaging:notifications:client_config', clientConfig => {
  /** @type {string} */
  let myTopic = '/exchange1/topic1'

  clientConfig.topics.push(myTopic)
})

Q&A

What is the difference between @unitybase/messaging and @unitybase/amqp models

@unitybase/amqp* packages rely on built-in AMQP UnityBase client. But its implementation is not complete and its completion is complication, because of conflict of UnityBase synchronous model and AMQP asynchronous nature. It is very hard to implement receiving messages, and therefore, even such things as getting error messages from server.

Used AMQP client to send messages

  • @unitybase/messaging: External service, usually deployed as a docker container.
  • @unitybase/amqp*: Built-in AMQP client, the @unitybase/amqp packet is the binding for it.

Ability to receive messages:

  • @unitybase/messaging: Yes, using external service.
  • @unitybase/messaging: No, need to do custom development.

RabbitMQ authentication and authorization:

  • @unitybase/messaging: Yes, using JWT, secure.
  • @unitybase/amqp*: Partially, hardcoded logic, non-secure login/password.

RabbitMQ authentication and authorization:

  • @unitybase/messaging: Yes, using JWT, secure.
  • @unitybase/amqp*: Partially, hardcoded logic, non-secure login/password.

RabbitMQ authentication and authorization:

  • @unitybase/messaging: Yes, using JWT, secure.
  • @unitybase/amqp*: Partially, hardcoded logic, non-secure login/password.

Ability to send message to different subjects:

  • @unitybase/messaging: Yes, send to role, group, department, etc.
  • @unitybase/amqp*: No, send to a user or broadcast.

Exchange name:

  • @unitybase/messaging: user-notify
  • @unitybase/amqp*: ub-amqp-notify

Broadcast topic name:

  • @unitybase/messaging: broadcast.<event>
  • @unitybase/amqp*: <name>.bcst.bcst

User/subject topic name:

  • @unitybase/messaging: <subjectID>.<event>
  • @unitybase/amqp*: <name>.U<user>.0

Migration from @unitybase/amqp:

  • Change used npm modules:
    npm uninstall @unitybase/amqp @unitybase/amqp-notify @unitybase/amqp-notify-pub
    npm install @unitybase/messaging
    
  • Reconfigure exchange topic name from ub-amqp-notify to user-notify.

Submodules

Members

exchange: exchangestatic #

Description

notification: notificationstatic #

Description