Another interesting feature of enigma.js are interceptors. Interceptors are similar to the mixins functions but they are executed on lower level - request/response level.

Request interceptors are invoked right before the request is send to the Engine and response interceptors are invoked right after the response from the Engine is received and before the data is send to the calling function.

You can think of the interceptors as "enigma.js middleware".

There are couple of functions available for request and response:

  • Request
    • onFulfiled - invoked just before the request is going to be send to the Engine so you can alter the request is needed
  • Response
    • onFulfiled - invoked when the response from the engine is ok (no errors)
    • onRejected - invoked when the Engine returns an error so you can handle the errors on enigma.js level

The code below shows how to construct request interceptor:

const enigma = require('enigma.js');
const schema = require('enigma.js/schemas/12.20.0.json');
const WebSocket = require('ws');

const session = enigma.create({
  schema,
  url: 'ws://localhost:9076/app/engineData',
  createSocket: (url) => new WebSocket(url),
  requestInterceptors: [{
    onFulfilled: function myHandler(sessionReference, request) {
      // do somthing with the request data here
      // OR return the original request 
      return request
    },
  }],
});

Example

A simple example of response interceptor can be - change the return data format of a getLayout() method which gets the current selections data.

This interceptor will "listen" for getLayout requests and if the response is for a specific object type it will "flatten" the returned data and return it in the following format:

[
    ...
    {"field": "name-of-the-field", "value": "selected-value-1"},
    {"field": "name-of-the-field", "value": "selected-value-2"},
    {"field": "some-other-field", "value": "selected-value-1"}
    ...
]
  • First lets prepare the session object and define our interceptor there:
const session = enigma.create({
    schema,
    url: 'ws://localhost:9076/app/engineData',
    createSocket: (url) => new WebSocket(url),
    responseInterceptors: [{
        onFulfilled: function toggleDelta(session, request, response) {
          // check if the request method is getLayout and 
          // the response is for object with type current-selections-modified
            if (
                request.method === 'GetLayout' && 
                response.qInfo.qType == 'current-selections-modified'
            ) {
                // if the above is valid then "flatten" the response
                return response.qSelectionObject.qSelections.map((s) => {
                    return s.qSelectedFieldSelectionInfo.map((f) => {
                        return {
                            field: s.qField,
                            value: f.qName
                        }
                    })
                }).flat()
            }

            // for every other response return the original response 
            return response;
        },
    }],
});
  • Let's select some values first:
    let field = await qDoc.getField('Product Sub Group Desc')
    let selectValues = await field.selectValues([
        { qText: 'Ice Cream' },
        { qText: 'Juice' },
        { qText: 'Chips' }
    ])
  • Once we have the interceptor code ready and some selections are made then we can prepare the properties of the object that will hold the selections:
    let selectionObjectProps = {
        "qInfo": {
            "qId": "",
            "qType": "current-selections-modified"
        },
        "qSelectionObjectDef": {}
    }

Check out the type of our object - current-selections-modified. Our interceptor will change the data output only if the response is for this object type. This way we will not "disrupt" the data for the other object types.

  • once we have the properties set, we continue our enigma calls as usual: create session object and get its layout:
let sessionObj = await qDoc.createSessionObject(selectionObjectProps);
let sessionObjLayout = await sessionObj.getLayout(); // out interceptor will be invoked here
console.log(sessionObjLayout)

The result of the last row will print the following "flatten" array:

[
  { field: 'Product Sub Group Desc', value: 'Juice' },
  { field: 'Product Sub Group Desc', value: 'Ice Cream' },
  { field: 'Product Sub Group Desc', value: 'Chips' }
]

It's probably not the most practical example but it will give you an idea what enigma.js interceptors are. There are couple of more examples in enigma.js repository itself:

qlik-oss/enigma.js
JavaScript library for consuming Qlik’s Associative Engine. - qlik-oss/enigma.js

Conclusion

The same interceptor can be written as mixin as well and instead intercepting the response just call the mixin which will return the same result. But if you want low level / fine grain control over the request/response traffic then interceptors are the way.

Code

Check out the repository below for the full example code

countnazgul/enigma-js-interceptors
Small script to demonstrate the usage of enigma.js interceptors - countnazgul/enigma-js-interceptors

Hope you liked it!

Stefan