This is a quick blog with the enhanced version of the ORDS transform function that has been discussed in blog. I have augmented below new filter capabilities:

  • Filters data that starts with certain characters.
  • Filters data that ends with certain characters.

Note: I will keep on updating the VB ORDS transform function in this blog with new features.

ORDS Transform Function Code:


define(['urijs/URI'], function (URI) {
'use strict';

   const addQParam = (url, toAdd) => {
        const query = URI(url).search(true);

        if (query.q) {
            toAdd = Object.assign(JSON.parse(query.q), toAdd);
        }
        query.q = JSON.stringify(toAdd);
        return URI(url).search(query).toString();
      };

    class Request {
         /**
          * @typedef {Object} Configuration
          * @property {Object} fetchConfiguration configuration for the current fetch call
          * @property {Object} endpointDefinition metadata for the endpoint
          * @property {function} readOnlyParameters: Path and query parameters. These are not writable.
          * @property {Object} initConfig map of other configuration passed into the request. The
          * 'initConfig' exactly matches the 'init' parameter of the request.
          * @property {string} url full url of the request.
          */

        /**
         * @typedef {Object} TransformsContext a transforms context object that can be used by authors of transform
         * functions to store contextual information for the duration of the request.
         */

        /**
         * filter builds filter expression query parameter using either the deprecated
         * filterCriteria array or filterCriterion object set on the options.
         * @param {Configuration} configuration
         * @param {Object} options the JSON payload that defines the filterCriterion
         * @param {TransformsContext} transformscontext a transforms context object that can be used by authors of transform
         * functions to store contextual information for the duration of the request.
         * @returns {Configuration} configuration object, the url looks like ?filter=foo eq bar
         */
         filter(configuration, options, transformsContext) {

           let filterCriterion = options;
        if (filterCriterion === undefined) {
            return configuration;
        }

        // following code is just an example and it assumes that
        // the SDP does not have any preconfigured filter criterion and
        // that there is only single "vb-textFilterAttributes" attribute set
        if (transformsContext['vb-textFilterAttributes'] !== undefined) {
            // in case the filtering comes from single select ui component
            // the search criteria details come in transformsContext
            const searchText = options && (options.text || options.value); // For comboox options object contains value not text
            const textFilterAttributes = transformsContext && transformsContext['vb-textFilterAttributes'];
            filterCriterion = textFilterAttributes.map(el => ({attribute: el, op: "$co", value: searchText }));
        }

        let operation;
        let criteria = filterCriterion.criteria;

        if (criteria === undefined || criteria.length === 0) {
            if (filterCriterion instanceof Array && filterCriterion.every(el => el.attribute && el.op && el.value)) { // If conditon to handle if there are more than one "vb-textFilterAttributes" attribute
                criteria = filterCriterion;
                operation = "$or";
            } else if (filterCriterion instanceof Object){
               criteria = [filterCriterion];
                operation = "$and";
            }
            else {
                return configuration;
            }
        }

        function transformOperator(fop) {
            switch (fop) {
                case '$co':
                    return '$instr';
                case '$le':
                    return '$lte';
                case '$ge':
                    return '$gte';
                case '$sw':
                    return '$like';
                case '$ew':
                  return '$like';
                default:
                    return fop;
            }
        }

        function isEmpty(val) {
            return (val === undefined || val === null || val === '');
        }

        if (filterCriterion && criteria && criteria.length > 0) {
            const q = [];

            criteria.forEach(function (item) {
                if (item.value === undefined || item.value === '%%' || item.value.length === 0) {
                    return;
                }
                
                if(item.op === '$ew') item.value = encodeURIComponent(`%${item.value}`);
                if(item.op === '$sw') item.value = encodeURIComponent(`${item.value}%`);

                const queryItem = {};

                //Below function and If condition for date
                
                function isDate(date) {
                  const regex = /\d{4}-\d{2}-\d{2}/;
                  const validDate = regex.test(date) && !isNaN((new Date(item.value)));
                  return validDate;
                }
                if(isDate(item.value)) {
                  const date = new Date(item.value).toISOString();
                  item.value = {
                  $date: date
                    };                  
                  }
                
                queryItem[transformOperator(item.op)] = item.value;

                const queryJSON = {};
                queryJSON[item.attribute] = queryItem;

                q.push(queryJSON);
            });

            if (q.length > 0) {
                const query = {};

                if (operation === undefined) {
                    operation = filterCriterion.op;
                }
                if (operation === undefined) {
                    operation = "$and";
                }

                query[operation] = q;

                // ORDS query URL is for example:
                // .../ords/hr/emp?q={"$or":[{"ename":{"$instr":"martin"}},{"job":{"$like":"%developer%"}}]}
                configuration.url = addQParam(configuration.url, query);
            }
        }

        return configuration;
        };


        /**
         * @typedef {Object} PaginateOptions
         * @property {number} iterationLimit
         * @property {number} offset which item the response should begin from
         * @property {String} pagingState
         * @property {number} size how many items should be returned
         */

        /**
         * pagination function appends limit and offset parameters to the url
         * @param {Configuration} configuration
         * @param {PaginateOptions} options
         * @param {TransformsContext} transformscontext
         * @returns {Configuration} configuration object.
         */
        paginate(configuration, options, transformscontext) {
        
            
        let newUrl = configuration.url;
        if (options && options.size) {
          newUrl = URI(newUrl).addSearch({limit: options.size, offset: options.offset}).toString();
        }
        configuration.url = newUrl;
        return configuration;
        }

        /**
         * sort 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.
         * @param {Configuration} configuration
         * @param {Array} options
         * @param {TransformsContext} transformscontext
         * @returns {Configuration} configuration object, the url looks like ?orderBy=foo:asc
         */
        sort(configuration, options, transformscontext) {
         
            if (Array.isArray(options) && options.length > 0) {

              const sortCriterias = options.reduce((acc, el) => {
                if(!el) return acc;
                const dir = el.direction === 'descending' ? 'DESC' : 'ASC';
                return {...acc, 
                  [el.attribute]: dir
                };

              }, {});
    
              const sort = '"$orderby":' + JSON.stringify(sortCriterias);
              let newUrl = configuration.url;
              let query = URI(newUrl).search(true);
                if (query.q) {
                  query.q = '{'+sort+','+query.q.substr(1);
                } else {
                  query.q = '{'+sort+'}';
                }
              newUrl = URI(newUrl).search(query).toString();
              configuration.url = newUrl;
            
            // const firstItem = options[0];
            // if (firstItem.attribute) {
            //     const dir = firstItem.direction === 'descending' ? 'DESC' : 'ASC';
            //     let newUrl = configuration.url;
            //     const sort = '"$orderby":{"'+firstItem.attribute+'":"'+dir+'"}';
            //     let query = URI(newUrl).search(true);
            //     if (query.q) {
            //       query.q = '{'+sort+','+query.q.substr(1);
            //     } else {
            //       query.q = '{'+sort+'}';
            //     }
            //     // ORDS sort URL is for example:
            //     // ...ords/hr/emp?q={"$orderby":{"sal":"ASC"}}
            //     // BUT: sorting is applied after filter() method above so sorting
            //     // needs to be inserted into existing q param if filtering is on
            //     newUrl = URI(newUrl).search(query).toString();
            //     configuration.url = newUrl;
            // }
        }
            return configuration;
        }

        /**
         * query function
         * @param {Configuration} configuration
         * @param {object} options
         * @param {TransformsContext} transformscontext
         * @returns {Configuration} configuration object
         */
        /*query(configuration, options, transformscontext) {
            
            const c = configuration;
            if (options && options.search) {
                let newUrl = c.url;
                newUrl = URI(newUrl).addSearch( options.search, 'faq' ).toString(); // appends 'faq' to the search term
                c.url = newUrl;
            }
            return c;
        }*/

        /**
         * select typically uses the 'responseType' to construct a query parameter to select and expand
         * the fields returned from the service
         * 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} configuration
         * @param {object} options
         * @param {TransformsContext} transformscontext
         */
        /*select(configuration, options, context) {
            
            const queryParamExists = (url, name) => {
                const q = url.indexOf('?');
                if (q >= 0) {
                    return (url.indexOf(`?${name}`) === q) || (url.indexOf(`&${name}`) > q);
                }
                return false;
            };

            // the options should contain a 'type' object, to override
            const 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
            const typeToInspect = (options && options.type && (options.type.items || options.type));
            if(typeToInspect && typeToInspect === Object) {
                const fields = 'TODO: query parameters'; // just an example; query parameter construction is left to the developer

                if(fields) {
                  c.url = URI(c.url).addSearch('fields', fields).toString();
                }
            }
            return c;
        }*/

        /**
         * fetchByKeys 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.
         * @param {Configuration} configuration
         * @param {object} options
         * @param {TransformsContext} transformscontext
         */
        fetchByKeys(configuration, transformOptions) {
          
            const c = configuration;
            const to = transformOptions || {};
            const fetchByKeys = !!(c && c.capability === 'fetchByKeys'); // this tells us that the current fetch call is a fetchByKeys

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

        /**
         * body is used to build or tweak the body for the fetch request. With some endpoints 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.
         * @param {Configuration} configuration
         * @param {object} options
         * @param {TransformsContext} transformscontext
         */
        /*body(configuration, options, transformsContext) {
            
            const c = configuration;
            if (options && Object.keys(options).length > 0) {
                c.initConfig.body = c.initConfig.body || {};
                // update body
            }
            return c;
        }*/
    };

    class Response {
        /**
         * @typedef {Object} PaginateResponse
         * @property {number} totalSize optional what the totalSize of the result is (the total count of the records in
         * the service endpoint).
         * @property {boolean} hasMore usually required, the paginate response transform function is relied upon to
         * inform the ServiceDataProvider when to stop requesting to fetch more data. Indicates whether there are more
         * records to fetch
         * @property {String} pagingState optional. This can be used to store any paging state specific to the paging
         * capability supported by the endpoint. This property can be used in the response paginate transform function
         * to set an additional paging state. This will then be passed as is to the request paginate transform function
         * for the next fetch call.
         */

        /**
         * paginate is called with the response so this function can process it and return an object with
         * properties set.
         * @param {object} result
         * @param {TransformsContext} transformscontext
         * @return {PaginateResponse}
         */
        paginate(result, transformscontext) {
            
           const tr = {};
 
        if (result && result.body) {
            const cb = result.body;
            // ORDS does not support "totalCount" but only "hasMore"
            tr.hasMore = cb.hasMore;
        }
        return tr;
        }

        /**
         * body is called last, after all the other response transforms have been called. It is a hook for authors
         * to transform the response body or build an entirely new one.
         * @param {object} result
         * @param {TransformsContext} transformscontext
         * @return {object}
         */
        /*body(result) {
            
            let tr = {};
            if (result.body) {
                tr = result.body;
            }

            // as a example store some random aggregation data
            tr.aggregation = { example: 4 };

            return tr;
        }*/
    }

    return {
        request: Request,
        response: Response
   };
});