Request Transformation Function

A request transformation (or transform) function is generally specified on the service endpoint. It can also be specified on the ServiceDataProvider variable, which overrides the endpoint one.

A request transform function is called right before a request is made to the server/endpoint. It provides a chance for page authors to transform the options (paginate, filter, sort, and so on) and build the final (request) configuration. The ServiceDataProvider supports a predefined list of request transform function types, described in this section. Note that there are no guarantees of the order in which transform functions are called.

A request transformation function has the following signature: function (configuration, options) { return configuration }. The parameters to the function are:
  • configuration: An object that has the following properties:

    • url: Full URL of the request.

    • parameters: Path and query parameters. These are not writable.

    • initConfig: Map of another configuration passed into the request. The 'initConfig' exactly matches the 'init' parameter of the request.

  • options: An object that is relevant to the type of transformation function. For a filter function, for example, this would be the filterCriterion.

  • context: A context object that is passed to every transform function to store or retrieve any contextual information for the current request lifecycle.

If transformations are needed for a specific data provider instance, these functions can be defined on the ServiceDataProvider variable under the 'transforms' property. For externalized fetch cases, the RestAction properties can be used for configuring transformations. 

Types of Request Transform Functions

paginate

The  'pagingCriteria' is passed in as the 'options' parameter to the paginate function. The pagingCriteria is often based on the current paging/scrolled state of the component. 

  • For implicit fetches, the pagingCriteria provided to the 'paginate' transform function can be used to construct a URL with the right paging query.

  • For externalized fetches, the pagingCriteria is always set on the REST instance through the hook handler. This means that if the RestAction has a responseTransformFunctions.paginate transform function property configured, then it can expect the pagingCriteria to be provided to it.

For offset-based paging:
  •  size: Specifies how many items should be returned.

  • offset: Specifies which item the response should begin from.

  • The default value for the pagingCriteria can be set on the configuration, but generally a component that is bound to the ServiceDataProvider variable will provide the values, where offset and size will be based on the configuration set in the component.

    // Variable Configuration
    "incidentListTableSource": {
      "type": "vb/ServiceDataProvider",     // variable of type vb/ServiceDataProvider
      "input": "none",
      "defaultValue": {
        "pagingCriteria": {                 // default size
          "size": 10
        },
     
        "transforms": {                     // transform function for paginate
          "request": {
            "paginate": "{{ $page.functions.paginate }}"
          }
        }
    }
    // paginate Transform Function
    // Transform function appends limit and offset parameters to the URL
    PageModule.prototype.paginate = function (configuration, options, context) { 
      const c = configuration;
      let newUrl = c.url;
      newUrl = `${newUrl}&limit=${options.size}&offset=${options.offset}`;
      c.url = newUrl;
      return c;
    };

filter

For this transform function, the 'filterCriterion' property is passed in as the 'options' parameter. The filterCriterion JSON property is an object representing a attribute criterion or a compound criterion. This example defines a simple structure for filter criteria that is a single criterion: 
// Variable Configuration
"incidentListTableSource": {
  "type": "vb/ServiceDataProvider",
  "input": "none",
  "defaultValue": {
    "filterCriterion": {                     // filterCriterion property defaultValue
      "attribute": "",
      "op": "eq",
      "value": ""
    },
    "transforms": {
      "request": {
        "filter": "{{ $page.functions.filter }}"  // transform function for filter
      }
    }
  }
}
Here's a sample filter transform function that converts the filterCriterion property to a query parameter appropriate to the endpoint: 
/**
 * Filter Transform Function Implementation
 * @param configuration
 * @param options the JSON payload that defines the filterCriterion
 * @param context an object to store/retrieve any contextual information for the 
 *  current request lifecycle
 * @returns {object} configuration object. the url looks like ?filter=foo eq 'bar'
 */
 
PageModule.prototype.filter = function (configuration, options, context) {
  const c = configuration;
  const filterCriterion = options;
 
  function jetFilterOpToScim(fop) {
    switch (fop) {
      case '$eq':
        return 'eq';
      case '$ne':
        return 'ne';
      case '$co':
        return 'co';
      default:
        console.warn('unable to interpret the op ' + fop);
        return null;
    }
  }
 
  function isEmpty(val) {
    return (val === undefined || val === null || val === '');
  }
 
  if (typeof filterCriterion === 'object' && Object.keys(filterCriterion).length > 0) {
    if (filterCriterion.op && filterCriterion.attribute && 
          !isEmpty(filterCriterion.value)) {
      const atomicExpr = {};
      atomicExpr.op = jetFilterOpToScim(filterCriterion.op);
      atomicExpr.attribute = filterCriterion.attribute;
      atomicExpr.value = filterCriterion.value;
 
      if (atomicExpr.op && atomicExpr.attribute) {
        c.url = URI(c.url).addQuery({
          filter: `${atomicExpr.attribute} ${atomicExpr.op} ${atomicExpr.value}`,
        }).toString();
      }
    }
  }
 
  return c;
};

Write a Filter Transforms Function for Text Filtering

If your SDP binds to a business object REST API endpoint, you have the following options to get text filtering to work:

  • Option 1: Configure the SDP to include a vb-textFilterAttributes property where the attributes to apply the text filter is specified. The built-in business object REST API transforms look for this property and automatically build a filter criterion using the text and turns it into a 'q' param.
    "transformsContext": {
        "vb-textFilterAttributes": ["lastName"]
    }

    For the above configuration example, if a user enters text 'foo' in select-single, the SDP generates q=lastName LIKE 'foo%'

    By default, the operator used is 'startsWith' as this is considered to be more optimized for db queries than 'contains'.

  • Option 2: If Option 1 doesn't meet your needs, then you can write a custom filter transform that massages the text filter and turns it into a regular filterCriterion.

If you use option 2, you could do something similar to the following example. In this example, resourcesListSDP uses the getall_resources endpoint. The (request) filter transforms property is a callback that is defined in the PageModule.

"resourcesListSDP": {
  "type": "vb/ServiceDataProvider",
  "defaultValue": {
    "endpoint": "crmRestApi11_12_0_0/getall_resources",
    "keyAttributes": "PartyNumber",
    "itemsPath": "items",
    "responseType": "page:getallResourcesResponse",
    "transformsContext": {
      "vb-textFilterAttributes": ["PartyName"]
    },
    "transforms": {
      "request": {
        "filter": "{{ $functions.processFilter }}"
      }
    }
  }
}

It's important to note that the transformsContext object is an argument to every transforms function, so transforms authors can read the attributes and build the query that way.

The transforms function below takes the text value provided by the component and turns into an attribute filter criterion using the attributes passed in:
define(['vb/BusinessObjectsTransforms'], function(BOTransforms) {
  'use strict';

  var PageModule = function PageModule() {};

   /**
   * The filter transform parses the text filter that may be part of the options and replaces
   * it with an appropriate attribute filter criterion using the textFilterAttrs.
   *
   * Note: select-single provides a text filter in the form { text: 'someTextToSearch' }.
   *
   * The processing of the resulting filterCriterion is delegated to the Business Object REST API
   * transforms module, which takes the filterCriterion and turns it into the 'q' param.
   * @param textFilterAttrs
   * @return a transforms func that is called later with the options
   */
  PageModule.prototype.processFilter = function(config, options, transformsContext) {
    const c = configuration;
    let o = options;
    let textValue;
    let isCompound;
    const tc = transformsContext;
    const textFilterAttributes = tc && tc['vb-textFilterAttributes];
 
    textValue = o && o.text;
 
    // build your regular filtercriterion and delegate to VB BO REST API filter transforms
 
    return BOTransforms.request.filter(configuration, o);
  }
  return PageModule;
});

Note:

Page authors are discouraged from configuring the SDP with the 'q' parameter directly, for example by setting a 'q' parameter in the uriParameters property. It is recommended that authors always use filterCriterion property to define 'q' criteria. This is especially important when using text filtering because the components always provide a filterCriterion which is appended to any configured filterCriterion on the SDP It becomes especially difficult for VB to reconcile the 'q' defined in uriParameters with the filterCriterion and authors are on their own to merge the two.

It's also important to note that select-single calls fetchByKeys very often to locate the record(s) pertaining to the select keys. For this reason, a new fetchByKeys transforms function has been added. Refer to the fetchByKeys transforms function for details.

sort

For this transform function, the 'sortCriteria' is passed in as the 'options' parameter. If page authors have complex sort expressions that cannot be expressed as a simple array, they can externalize the fetch to configure their own sort criteria and build a request using that.

// Variable Configuration
"incidentListTableSource": {
  "type": "vb/ServiceDataProvider",
  "input": "none",
  "defaultValue": {
    "sortCriteria": [                           // sortCriteria property default value
      {
        "direction": "ascending"
      }
    ],
     
    "transforms": {
      "request": {
        "sort": "{{ $page.functions.sort }}"    // transform function for sort
      }
    }
  }
}
/**
 * Sort Transform Function Implementation
 * @param configuration
 * @param options the JSON payload that defines the sortCriteria
 * @param context an object to store/retrieve any contextual information for the
 *  current request lifecycle.
 * @returns {object} configuration object. the url looks like ?orderBy=foo:asc
 */
PageModule.prototype.sort = function (configuration, options, context) {
  const c = configuration;
 
  if (options && Array.isArray(options) && options.length > 0) { 
    const firstItem = options[0]; 
    if (firstItem.name) { 
      const dir = firstItem.direction === 'descending' ? 'desc' : 'asc' 
      let newUrl = c.url; 
      newUrl = `${newUrl}&orderBy=${firstItem.attribute}:${dir}`; 
      c.url = newUrl; 
    } 
  } 
  return c; 
};

query

For this transform function, the 'uriParameters' property is passed in as options. Normally uriParameters are appended to the URL automatically, but there may be cases where the user would want to adjust the query parameters. For example, suppose the endpoint GET /incidents supports a query parameter called "search", which does a semantic search. If a specific transform needs to happen before the endpoint us cakked, then the transform function could be used for that. 

// Variable Configuration
"incidentListTableSource": {
  "type": "vb/ServiceDataProvider",
  "input": "none",
  "defaultValue": {
    "uriParameters":
      {
        "technician": "hcr",
        "search": "{{ $page.variables.searchBoxValue }}"// search query parameter 
                                                        // bound to some UI field
      }
    ],
     
    "transforms": {
      "request": {
        "query": "{{ $page.functions.query }}"       // transform function for query
      }
    }
  }
}
/**
 * query Transform Function Implementation
 */
PageModule.prototype.query = function (configuration, options, context) {
  const c = configuration;
  if (options && options.search) {
    let newUrl = c.url;
      newUrl = `${newUrl}&search=${options.search} faq`; // appends 'faq' to the 
                                                         // search term 
      c.url = newUrl; 
	}
	return c;
  // configuration, options}; 
};

select

This transform typically uses the 'responseType' to construct a query parameter to select and expand the fields returned from the service. The built-in vb/BusinessObjectsTransforms creates a 'fields' query parameter, such that the response will include all fields in the responseType structure, including expanded fields. For example:

/**
 * select transform function.
 * Example:
 *
 * Employee
 * - firstName
 * - lastName
 * - department
 *   - items[]
 *     - departmentName
 *     - location
 *        - items[]
 *          - locationName
 *
 * would result in this 'fields' query parameter:
 *
 * fields=firstName,lastName;department:departmentName;department.location:locationName
 *
 * @param configuration
 * @param options
 * @param context a transforms context object that can be used by authors of transform
 *  functions to store contextual information for the duration of the request.
 */
PageModule.prototype.select = function(configuration, options, context) {
  // the options should contain a 'type' object, to override
  var c = configuration;

  // do nothing if it's not a GET
  if (c.endpointDefinition && c.endpointDefinition.method !== 'GET') {
    return c;
  }

  // do nothing if there's already a '?fields='
  if(queryParamExists(c.url, 'fields')) {
    return c;
  }

  // if there's an 'items', use its type; otherwise, use the whole type
  vartypeToInspect = (options && options.type && (options.type.items || options.type));
  if(typeToInspect && typeoftypeToInspect === 'object') {
    var fields; // just an example; query parameter construction is left to the 
                // developer
 
    if(fields) {
      c.url = appendToUrl(c.url, 'fields', fields);
    }
  }
  return c;
}

function appendToUrl(url, name, value) {
  // skip undefined and null
  if (value !== undefined && value !== null) {
    var sep = url.indexOf('?') >= 0 ? '&' : '?';
    return url + sep + name + '=' + value;
  }
  return url;
}
 
function queryParamExists(url, name) {
  const q = url.indexOf('?');
  if (q >= 0) {
    return (url.indexOf(`?${name}`) === q) || (url.indexOf(`&${name}`) > q);
  }
  return false;
}

body

This transform is used to build or tweak the body for the fetch request. With some endpoints, especially those involving ElasticSearch, the search is made with a complex search criteria set on the body that can be tweaked here.

This transform function is the only function that is guaranteed to be called after all other request transform functions, (filter, sort, paginate, and so on). The reason is that any of the other transform functions can set info into the 'transformsContext' parameter, as a way to update the body. It's entirely left to the discretion of the transforms author how to use the 'transformsContext' property and the 'fetchMetadata' parameter provided via the fetch call. Currently, the built-in business object REST API transforms implementation does not use either.

/**
 * If a body is specified then we look for 'search' and 'technician' in the post body.
 * All other keys are ignored.
 * @param configuration
 * @param options
 * @param context transforms context
 */
function bodyRequest(configuration, options, transformsContext) {
  const c = configuration;
  if (options && typeof options === 'object' && Object.keys(options).length > 0) {
    c.initConfig.body = c.initConfig.body || {};
    // update body
  }
  return c;
}

fetchByKeys

A fetchByKeys transforms function allows the page author to take a key or Set of keys passed in via the options and tweak the URL, to fetch the data for the requested keys.

When the consumer of the SDP calls the fetchByKeys() method, if the transforms author has provided a 'fetchByKeys' transforms implementation then it gets called over the other transforms. If no fetchByKeys transforms function is provided then the default transforms will get called.

The built-in business object REST API transforms already provides a fetchByKeys transforms function implementation that appends the keys to the URL. This should suffice for most common cases and should result in at most one fetch request to the server. For third-party REST endpoints, the author can provide a custom fetchByKeys transforms implementation.

Example: For a sample third-party endpoint, the key is appended to the URL as a query param 'id=<key>'

define(['ojs/ojcore', 'urijs/URI'], function (oj, URI) {

  PageModule.prototype.fetchByKeysTransformsFunc = function (configuration, transformOptions){
    var c = configuration;
    var to = transformOptions || {};
    var fetchByKeys = !!(c && c.capability === 'fetchByKeys'); // this tells us that the current fetch call is a fetchByKeys

    if (fetchByKeys) {
      var keysArr = Array.from(c.fetchParameters.keys);
      var key = keysArr[0]; // grab the key provided by caller
      if (key) {
        c.url = URI(c.url).addQuery({
          id: key,
        }).toString();
      }
    }
    return c;
  };
});