Multi-Service Data Provider

The vb/MultiServiceDataProvider built-in type is a data provider implementation that combines multiple vb/ServiceDataProvider variables, each providing a unique fetch capability.

Often components that bind to data providers, like oj-combobox-one and oj-select-single (or the -many variants), require or use different 'fetch' capabilities on the data provider implementation.

For example, an oj-select-single component might call fetchFirst() (on the DataProvider implementation) to populate its options, and then call fetchByKeys() to fetch data for selected value, and fetchByOffset() to fetch items from an offset. Often the endpoint configured on a ServiceDataProvider may provide multiple capabilities - for example, most GETAll endpoints for business object REST API services also allow fetching data for specific keys, and from an offset, on the same endpoint. However, on rare occasions authors might require different endpoints to support different fetch capabilities. A MultiServiceDataProvider can be used for this purpose.

Design Time Assumptions

At design time, a service author can identify different endpoints that provide the fetchByKeys and fetchByOffset capabilities, in addition to the current fetch all (fetchFirst capability). When there are different endpoints a page author must pick different endpoints for each (fetch) capability when configuring a variable of a type vb/MultiServiceDataProvider. It is common for the same REST endpoint to support multiple capabilities.

  • for fetchByKeys
    • For example, the same endpoint can fetch all territories and a set of territories that match a set of territory codes (fetchByKeys): GET /fndTerritories? and /fndTerritories?q=TerritoryCode in ('US', 'AE')
    • the same endpoint can be used to fetch all customers, or to fetch customers by specific keys using the same endpoint but different query parameters: GET /customers and GET /customers?ids=cus-101,cus-103.
  • for fetchByOffest
    • an Oracle Cloud application endpoint can fetch all territories, and territories at a given offset - GET /fndTerritories and /fndTerritories?offset=50&size=10

Properties

A variable of the built-in type vb/MultiServiceDataProvider can be configured with the dataProviders property using the following sub-properties.

dataProviders Sub-property Type Example Description
fetchFirst "vb/ServiceDataProvider"
{
  "variables": {
    "activitiesMultiSDP": {
      "type": "vb/MultiServiceDataProvider",
      "defaultValue": {
        "dataProviders": {
          "fetchFirst": "{{ $variables.listSDP }}"
        }
      }
    }
  }
}
A MultiServiceDataProvider is needed only when more than one fetch capability needs to be configured.
fetchByKeys "vb/ServiceDataProvider"
{
  "variables": {
    "activitiesMultiSDP": {
      "type": "vb/MultiServiceDataProvider",
      "defaultValue": {
        "dataProviders": {
          "fetchFirst": "{{ $variables.listSDP }}"
          "fetchByKeys": "{{ $variables.detailSDP }}"       
        }
      }
    }
  }
}
A reference to the vb/ServiceDataProvider variable.
fetchByOffset "vb/ServiceDataProvider"
{
  "variables": {
    "activitiesMultiSDP": {
      "type": "vb/MultiServiceDataProvider",
      "defaultValue": {
        "dataProviders": {
          "fetchFirst": "{{ $variables.listSDP }}"
          "fetchByOffset": "{{ $variables.listSDP }}"       
        }
      }
    }
  }
}
A reference to the vb/ServiceDataProvider variable.

Behavior

  • A variable of type vb/MultiServiceDataProvider must have at least one fetch capability defined. Otherwise an error is flagged.
  • When a fetchFirst capability is not defined, a no-op fetchFirst capability is used. The JET DataProvider contract requires a fetchFirst implementation to be provided.
  • All fetch capabilities must point to a variable of type vb/ServiceDataProvider. 
  • A MultiServiceDataProvider cannot reference another MultiServiceDataProvider variable.

Usage

Here are some of the common ways service endpoints might provide their fetch capabilities.

Usage: When a service provides unique endpoints for different fetch capabilities

When a service has unique endpoints for each fetch capability, we will require one variable of type 'vb/ServiceDataProvider' per fetch API, and a variable of type 'vb/MultiServiceDataProvider' variable that combines the individual ServiceDataProvider variables together. The list-of-values component will then bind to a variable of type vb/MultiServiceDataProvider.

Let's consider this third-party REST API that is used to get information about countries.

  • fetchFirst capability: to get a list of all countries and their info, where the alpha3Code is the primary key

    • service/endpoint: rest-service/getAllCountries

    • GET https://restcountries.eu/rest/v2/all

  • fetchByKeys capability (with multi key lookup): to get a list of countries by their three-letter alpha code
    • service/endpoint: rest-service/getCountriesByCodes

    • GET https://restcountries.eu/rest/v2/alpha?codes=usa;mex

In order for the list-of-values component to use the above endpoints, the design time will need to create three variables:

  • One vb/MultiServiceDataProvider variable that references two ServiceDataProvider variables, one for each fetch capability

  • Two vb/ServiceDataProvider variables

vb/MultiServiceDataProvider Configuration

At design time, a variable using this type will be created that looks like this:

1  {
2    "variables": {
3      "countriesMultiSDP": {
4        "type": "vb/MultiServiceDataProvider",
5        "defaultValue": {
6          "dataProviders": {
7            "fetchFirst": "{{ $page.variables.allCountriesSDP }}
8            "fetchByKeys": "{{ $page.variables.countriesByCodesSDP }}"
9          }
10       }
11     }
12   }
13 }
  • Line 3: countriesMultiSDP is a variable of type vb/MultiServiceDataProvider. This defines two properties: 'fetchFirst' and 'fetchByKeys'.

  • Line 7: The fetchFirst property allows the MultiServiceDataProvider to call fetchFirst() on the referenced ServiceDataProvider variable.

  • Line 8: The fetchByKeys property allows the MultiServiceDataProvider to call fetchByKeys() on the referenced ServiceDataProvider variable.

vb/ServiceDataProvider Variables Configuration

For the above use case, the referenced ServiceDataProvider variables will be configured as follows:

Configuration Description
1  { 
2    "variables": { 
3      "allCountriesSDP": { 
4        "type": "vb/ServiceDataProvider",
5        "defaultValue": {
6          "endpoint": "rest-service/getAllCountries",
7          "keyAttributes": "alpha3Code"
8        }
9      },
10     "countriesByCodesSDP": {...}
11   }
12 }

Line 3: defines the ServiceDataProvider variable with a fetchFirst capability.

  • When a capabilities property is not specified, it's assumed that the ServiceDataProvider supports a fetchFirst capability.

  • When a capabilities property is present but no fetch capability is defined (that is, only the filter and sort capabilities are defined), fetchFirst is assumed.

Line 6: defines the endpoint to use the getAllCountries operation to fetch all countries.

1  { 
2    "variables": { 
3      "allCountriesSDP": { 
4     "countriesByCodesSDP": { 
5        "type": "vb/ServiceDataProvider",
6        "defaultValue": {
7          "endpoint": "rest-service/getCountriesByCodes", 
8          "keyAttributes": "alpha3Code",
9          "capabilities": {
10            "fetchByKeys": {
11             "implementation": "lookup",
12             "multiKeyLookup" : 'no'
13           }  
14         },
15         "mergeTransformOptions": "{{ $functions.fixupTransformOptions }}"
16       }
17     }
18   }

Line 4: defines the ServiceDataProvider variable that supports a fetchByKeys capability.

Line 7: uses the getCountriesByCodes operation to fetch a list of countries by their codes.

Line 9: a 'capabilities' property is added to ServiceDataProvider that has a 'fetchByKeys' property object. See next section for details.

  • 'implementation' property is set to "lookup"

  • 'multiKeyLookup' property set to "no"

Line 15: the 'mergeTransformOptions' property is set to a page function.

  • this is needed so page author can map the keys set programmatically to be turned into the query parameters '?codes='

    Note:

     Normally fetchByKeys() is called by a JET component programmatically with one or more keys.
  • When keys are provided programmatically, ServiceDataProvider will use a best-guess heuristic to map keys to the appropriate transform options. But when this is not easily decipherable by ServiceDataProvider, page authors can use a 'mergeTransformOptions' property that maps to a function, to fix up the list of the 'query' options. This function will be passed in all the info it needs to merge the final transform options.

    Note:

     In this example the keys need to map to the codes uriParameters, and such a mapping cannot be represented in the page model using an expression.
  • When no keys are provided, ServiceDataProvider will throw an error.

1  /**
2   * fix up the query transform options.
3   * When the fetchByKeys capability is set, the 'keys' provided via the fetch call
4   * can be be looked up via configuration.fetchParameters.
5   * This can be used to set a 'codes' property on the 'query' transform options
6   * whose value is the keys provided via a fetch call.
7   *
8   * @param configuration a map of 3 key values, The keys are	
9   *   - fetchParameters: parameters passed to a fetch call
10  *   - capability: 'fetchByKeys' | 'fetchFirst' | 'fetchByOffset'
11  *   - context: the context of the SDP when the fetch was initiated.
12  *
13  * @param transformOptions a map of key values, where the keys are the
14  *        names of the transform functions.
15  * @returns {*}
16  */
17 PageModule.prototype.fixupTransformOptions =
18     function (configuration, transformOptions) {
19   var c = configuration;
20   var to = transformOptions;
21   var fbkCap = !!(c && c.capability === 'fetchByKeys');
22   var keysToFetch = fbkCap ?
23                      (c && c.fetchParameters && c.fetchParameters.keys) : null
24 
25   if (fbkCap && keysToFetch && keysToFetch.length > 0) {
26     // join keys
27     var keysToFetchStr = keysToFetch.join(';');
28     to = to || {};
29     to.query = to.query || {};
30 
31     // ignore codes set on the query options and instead use ones passed in
32     // by fetchByKeys call
33     to.query.codes = keysToFetchStr;
34   }
35 
36   return to;
37 };

Line 17: function that fixes up the transform options that will be sent to the transform functions.

Line 33: set a new 'codes' query parameter, whose value is a ';' separated list of country alpha codes.

Configuring a JET Combo/Select at Design Time

To configure a list-of-values field that uses the above, the design time needs to create three variables:
  • One vb/MultiServiceDataProvider variable

  • Two vb/ServiceDataProvider variables

The MultiServiceDataProvider variables are bound to the combo/select components as follows.
  • Line 2 points to a variable of type vb/MultiServiceDataProvider.

1  <oj-combobox-one id="so11" value="{{ $variables.selectedActivities }}"
2                         options="[[ $variables.countriesMultiSDP ]]"
3                          options-keys.label='[[ "name" ]]'
4                         options-keys.value='[[ "alpha3Code" ]]'
5  </oj-combobox-one>

A distinct vb/ServiceDataProvider variable is needed for each unique service/endpoint. Often authors want to provide different default filterCriterion, sortCriteria or uriParams, or even write different transforms for each capability. Isolating each capability to a unique ServiceDataProvider variable allows for this separation.

Any individual vb/ServiceDataProvider variables might externalize its fetch, or allow an actionChain to assign values to its properties directly via expressions. They can also allow a fireDataProviderEventAction to reference the Service Data Provider variable directly. First class variables are the easiest way to give page authors access.

Usage: When a service provides unique endpoints for different fetch capabilities, but the fetchByKeys endpoint only supports a single-key-based lookup

In this use case, the service supports a fetchFirst capability that fetches all rows, and a fetchByKeys capability that returns a single row by its key. There is no endpoint that can return rows by multiple keys.

To understand this usecase further let's take the example of the sample ifixitfast service - and the incidents endpoints that is used to get information about incidents.

  • fetchFirst capability: to get a list of all incidents for the selected technician,
    • service/endpoint: fixitfast-service/getIncidents
    • GET https://.../ifixitfaster/api/incidents?technician=hcr
  • fetchByKeys capability (with single key lookup): to get a single incident it its 'id'
    • service/endpoint: fixitfast-service/getIncident
    • GET https://.../ifixitfaster/api/incidents/inc-101

In order for the list-of-values component to use the above endpoints, the design time will need to create three variables:

  • One vb/MultiServiceDataProvider variable that references two ServiceDataProvider variables, one for each fetch capability
  • Two vb/ServiceDataProvider variables

vb/MultiServiceDataProvider Variable Configuration

The configuration for the vb/MultiServiceDataProvider variable is similar to the previous examples.

1  {
2    "variables": {
3       "countriesMultiSDP": {
4         "type": "vb/MultiServiceDataProvider",
5         "defaultValue": {
6          "dataProviders": {
7             "fetchFirst": "{{ $page.variables.allIncidentsSDP }}"
8              "fetchByKeys": "{{ $page.variables.incidentBySingleKeySDP }}"
9          }
10       }
11      }
12    }
13  }

vb/ServiceDataProvider Variables Configuration

For the previous use case, the referenced ServiceDataProvider variables will be configured as follows.

Configuration Description

some-page.json

1   {
2       "variables": {
3        "allIncidentsSDP": {
4           "type": "vb/ServiceDataProvider",
5           "defaultValue": {
6              "endpoint": "fixitfast-service/getAllIncidents",
7              "keyAttributes": "id",
8              "itemsPath": "result",
9              "uriParameters": {
10                "technician": "{{ $application.user.userId }}"
11             }
12          }
13       },
14       "incidentBySingleKeySDP": {...}
15    }
16  }
  • Line 3: defines the ServiceDataProvider variable with the fetchFirst capability.

  • Line 6: defines the endpoint that uses the getAllIncidents operation to fetch all incidents.

1  {
2    "variables": {
3      "allIncidentsSDP": {...},
4      "incidentBySingleKeySDP": {
5        "type": "vb/ServiceDataProvider",
6        "defaultValue": {
7          "endpoint": "fixitfast-service/getIncident",
8          "keyAttributes": "id",
9          "uriParameters": {
10           "id": "{{ $variables.incidentId }}"
11         }
12         "capabilities": {
13           "fetchByKeys": {
14             "implementation": "lookup",
15             "multiKeyLookup" : 'no'
16           }         
17         }
18       }
19     }
20   }

Line 4: defines the ServiceDataProvider variable with the fetchByKeys capability. The ServiceDataProvider variable is configured for an implicit fetch.

Line 7: uses the getIncident operation to fetch a single incident by its id.

Line 9: maps the 'id' key in the 'uriParameters'.

  • At runtime the 'id' key value is substituted in the path parameter of the URL.

  • For example, if the 'id' value is "inc-101", the request URL goes from https://.../incidents/{id} → http://.../incidents/inc-101

Line 12: a new 'capabilities' property is added to ServiceDataProvider that has a 'fetchByKeys' key object.

  • The 'implementation' property is set to "lookup".

  • The 'multiKeyLookup' property is set to "no", as the endpoint only supports lookup using a single key at a time.

Notice that a 'mergeTransformOptions' property is not set.

  • This is because Service Data Provider uses a simple heuristic to map the 'keys' provided programmatically to the 'id' sub-property of the 'uriParameters'.

    • It can do this because ServiceDataProvider sees that the keyAttributes value "id" is the same attribute key set on 'uriParameters'.

    • Also, this is only possible when ServiceDataProvider is configured to use implicit fetch (that is, it does not use an external action chain to do a fetch).

  • In some cases the ServiceDataProvider cannot easily decipher the mapping (as seen in the previous example), and this is when page authors can use a 'mergeTransformOptions' property to map the keys to the right transform options.

  • When multiple keys are provided by the caller, ServiceDataProvider as an optimization calls the single endpoint a single key at a time, assembles the result, and returns this to caller.

1  {
2    "variables": {
3      "allIncidentsSDP": {...},
4      "incidentBySingleKeySDP_External": {
5        "type": "vb/ServiceDataProvider",
6        "defaultValue": {
7          "fetchChainId": "fetchSingleIncidentChain",
8          "keyAttributes": "id",
9          "mergeTransformOptions": "{{ $page.functions.fixupTransformOptions }}",
10         "capabilities": {
11           "fetchByKeys": {
12           "implementation": "lookup",
13           "multiKeyLookup": "no"
14         }
15       }
16     }
17   },
18   "chains": {}
19 }

Line 4: defines the ServiceDataProvider variable with a fetchByKeys capability.

  • The Service Data Provider variable uses an action chain to fetch data. See the next section for the action chain configuration.

Line 9: sets a mergeTransformOptions function.

  • This function is used by the page author to fix up the 'query' transform options to use the key passed in via the fetch call.

/**
 * Process the transform options.
 * When ServiceDataProvider uses external fetch chain, it doesn't 
 * have all the information to build the final transform options
 * to use with the transform functions. In such cases the page 
 * author can use this method to build the final list of options. 
 * Replaces id set via configuration with the value passed in by caller.
 *
 * @param configuration an Object with the following properties
 *   - capability: 'fetchByKeys' | 'fetchFirst' | 'fetchByOffset'
 *   - fetchParameters: parameters passed to the fetch call
 *   - context: the context of the Service Data Provider variable at 
 *              the time the fetch call was made
 *
 * @param transformOptions a map of key values, where the keys are the
 *   names of the transform functions.
 *
 * @returns {*} the transformOptions either the same one passed in or 
 *   the final fixed up transform options
 */
PageModule.prototype.fixupTransformOptions = function (configuration, transformOptions) {
  var c = configuration;
  var to = transformOptions || {};
  var fetchByKeys = !!(c && c.capability === 'fetchByKeys');
 
  if (fetchByKeys) {
    var key = c.fetchParameters.keys[0];
    if (key &&
        (!to.query || (to.query && to.query.id !== c.fetchParameters.keys[0]))) {
      to.query = to.query || {};
      to.query.id = key;
    }
  }
  return to;
};

mergeTransformOptions function

1  {
2    "variables": {},
3    "chains": {
4      "fetchSingleIncidentChain": {
5        "variables": {
6          "configuration": {
7            "type": {
8              "hookHandler": "vb/RestHookHandler"
9            },
10           "description": "the configuration for the rest action",
11           "input": "fromCaller",
12           "required": true
13         },
14         "uriParameters": {
15           "type": "object",
16           "defaultValue": {
17             "id": "{{ $page.variables.incidentId }}"
18           }
19         }
20       },
21       "root": "fetchSingleIncidentAction",
22       "actions": {
23         "fetchSingleIncidentAction": {
24           "module": "vb/action/builtin/restAction",
25           "parameters": {
26             "endpoint": "fixitfast-service/getIncident",
27             "hookHandler": "{{ $variables.configuration.hookHandler }}",
28             "uriParams": "{{ $variables.uriParameters }}",
29             "responseType": "flow:incident",
30             "requestTransformFunctions": {
31               "query": "{{ $page.functions.queryIncidentById }}"
32             }
33           },
34           "outcomes": {
35             "success": "returnIncidentResponse",
36             "failure": "returnFailureResponse"
37           }
38         },
39       }
40     }
41   }
42 }

The external fetch action chain is configured as follows.

Line 4: the action chain used by the ServiceDataProvider.

Line 23: the RestAction, the chain calls to fetch a single incident by id.

Line 28: the 'uriParams' property of the RestAction is set to the page variable "incidentId".

  • The value of the "incidentId" variable might be different from what the caller passes in.

  • The mergeTransformOptions function above builds the query options containing the final id value.

Line 31: the requestTransformFunction.query maps to a query transform function that substitutes the endpoint URL with the final id value.

/**
 * query transform function that takes the id provided in the options 
 *   and expands the URL.
 * @param configuration
 * @param options
 * @returns {*}
 */
PageModule.prototype.queryIncidentById = function (configuration, options) {
  const c = configuration;
  if (options && options.id) {
    var result = URI.expand(c.endpointDefinition.url, { id: options.id });
    var newUrl = result.toString();
    if (newUrl !== c.url) {
      console.log(`typesDemo sample: replacing ${c.url} with ${newUrl}`);
    }
    c.url = newUrl;
  }
  return c;
};
Query transform function

Usage: When the same endpoint supports multiple fetch capabilities

Most list-of-value objects fall into this category. For example, to fetch both a list of territories and to fetch a subset of territories by their ids, the same endpoint is used:

  • fetchFirst capability: 

    • service/endpoint: fa-crm-service/getTerritories

    • GET /fndTerritories?finder=EnabledFlagFinder;BindEnabledFlag=Y

  • fetchByKeys capability: 

    • GET /fndTerritories?finder=EnabledFlagFinder;BindEnabledFlag=Y&q=TerritoryCode IN ('AE', 'AD', 'US')

In this case, a single ServiceDataProvider variable of type vb/ServiceDataProvider that multiplexes different fetch capabilities is the recommended approach. The ServiceDataProvider variable can then be used to bind to the list-of-values component.

Note:

It is recommended that service authors ensure that the service is configured to use the default business object REST API transforms.

vb/ServiceDataProvider Variables Configuration

The data returned by the service endpoint will look something like this:

{
  "items": [
    {
      "TerritoryCode": "AE",
      "AlternateTerritoryCode": "ar-AE",
      "TerritoryShortName": "United Arab Emirates",
      "CurrencyCode": "AED"
    },
    ...
  ],
  "count": 25,
  "hasMore": false,
  "limit": 25,
  "offset": 0,
}

The ServiceDataProvider variables for the fetchFirst and fetchByKeys capabilities will be configured as follows

sample-page.html Description
"territoriesSDPVar": {
  "type": "vb/ServiceDataProvider",
  "defaultValue: {
    "endpoint": "fa-crm-service/getTerritories",
    "keyAttributes": "TerritoryCode",
    "itemsPath": "items",
    "uriParameters": {
      "finder": "EnabledFlagFinder;BindEnabledFlag=Y"
    }
  }
}

A finder query parameter is applied to all queries going against the endpoint.

When no capabilities are set, the ServiceDataProvider variable is assumed to support a fetchFirst capability

Configuring a JET Select-Single in Design Time

  • Line 1: the value is bound to a variable that is an array of selected TerritoryCode keys.

  • Line 2: the data attribute is bound to the ServiceDataProvider variable.

1 <oj-select-single id="so11" value="{{ $variables.selectedTerritories }}" 
2              data="[[ $variables.territoriesSDPVar ]]" 
3              item-text='[[ "TerritoryShortName" ]]' 
4 </oj-select-single>

Usage: When a service provides a fetchByKeys capability, and DataProvider.containsKeys is called

The containsKeys() method can be called by components bound to a ServiceDataProvider variable that supports the 'fetchByKeys' capability. The default implementation of containsKeys() will call fetchByKeys() and return a oj.ContainsKeysResult object, as defined by the JET DataProvider contract. This implementation addresses the most common usecase.