Options
All
  • Public
  • Public/Protected
  • All
Menu

Functionality used to customize a DataLayer pipeline.

A DataLayer provides only basic functionality for fetching data. These utility methods help you extend the data pipeline with powerful features. They work by wrapping the DataLayer's fetch() method to process Requests and Responses based on various conditions you supply.

You should use this same approach in your own applications to provide easy cross-cutting functionality for data operations.

// datalayer.js

import rules from '~/config/proxy';

const proxy = data.createProxy();
const { fetch, createRequest } = data.createDataLayer(proxy);

proxy.use(...rules);

// we extend the functionality of `fetch` by wrapping
// it and returning a function with the same signature
// that proxies to the real fetch while adding custom
// error handling logic
function withCustomErrors(fetch) {
return async function useCustomErrors(request) {
return await fetch(request)
.catch(errors.rethrow({ app: 'my app' }));
};
}

let pipeline = fetch;
// add custom error handling
pipeline = withCustomErrors(pipeline);
// add default request headers
pipeline = data.utils.withHeaders(pipeline, {
'x-app-name': 'my-app'
});

export {
proxy,
createRequest,
fetch: pipeline // return our extended pipeline
}
// consumer.js

import { fetch, createRequest } from './datalayer.mjs';

const datacall = {
base: 'endpoint',
path: '/:id/info'
};

// automatically retry failed requests using
// an exponential falloff between attempts;
// the version of fetch we bring in has already
// been wrapped to add custom error data and
// request headers; this shows how we can combine
// cross-cutting logic with custom one-off logic
const load = data.utils.withRetry(fetch, data.utils.falloff());

export async function loadData(id) {
const params = { id };
const request = createRequest(datacall, params);
const response = await load(request)
.catch(errors.rethrow(errors.fatal(params)));
return response.data;
}

Index

Functions

  • Creates a RetryFunction that will retry a failed request the specified number of times, using the given exponential base as a falloff interval.

    see

    withRetry()

    example
    import { fetch, createRequest } from '~/path/to/datalayer';

    const getUser = {
    base: 'users',
    method: 'GET',
    path: '/users/:id',
    };

    const attempt = data.utils.withRetry(fetch, data.utils.falloff(5, 100));

    export async function loadUserData(id) {
    const params = { id };
    const request = createRequest(getUser, params);
    const response = await attempt(request)
    .catch(errors.rethrow(params));
    return response.data;
    }

    Parameters

    • times: number = 3

      The number of times to retry the failed data operation before giving up.

    • base: number = 200

      The base number of milliseconds to use as the exponential falloff period. On each invocation of the retry function, the falloff period will be increased by a power of 2 and multiplied by this base. For example:

      • retry #1: 200ms
      • retry #2: 400ms
      • retry #3: 800ms

    Returns RetryFunction

    Invoked to determine whether the data operation should be retried.

  • tokenize(url?: string, params?: Record<string, any>): string
  • Replaces tokens in a string with values from the provided lookup, and then appends any unmatched key-value pairs in the lookup as a querystring.

    NOTE: Nested objects will not be serialized; if you need to pass complex objects to your endpoint, you should be doing it through the request body; alternatively, use JSON.stringify on the object yourself before passing it to serialize.

    NOTE: different falsy values are treated differently when appending to the querystring:

    input params result string
    { key: false } key=false
    { key: null } key
    { key: undefined } (no output)
    example
    const url = tokenize('/clients/:id/apps', {id: '0012391'});
    assert.equals(url, '/clients/0012391/apps');
    example
    const url = tokenize('/my/endpoint', {offset: 1, pagesize: 20});
    assert.equals(url, '/my/endpoint?offset=1&pagesize=20');
    example
    const url = tokenize('/users/:guid/clients', {
    guid: '00123456789123456789',
    order: ['displayName', 'branch']
    });
    assert.equals(url, '/users/00123456789123456789/clients?order=displayName&order=branch');

    Parameters

    • url: string = ''

      A URL that may contain tokens in the :name format. Any tokens found in this format will be replaced with corresponding named values in the params argument.

    • params: Record<string, any> = {}

      Values to use to replace named tokens in the URL. Any unmatched values will be appended to the URL as a properly encoded querystring.

    Returns string

    A tokenized string with additional URL-encoded values appened to the querystring.

  • Wraps a fetch method so a reauthentication method is invoked if a Response status 401 is returned. The reauthentication method is responsible for ensuring future requests are authenticated.

    One way to do this is by adding a ProxyRule to the Proxy that sets an Authorize header to an updated value on specific Requests. A simpler approach is to rely on a Set-Cookie response header to update the cookies sent with future requests (if withCredentials is true).

    throws

    Argument reauthenticate must be a function.

    example
    import { fetch, createRequest } from '~/path/to/datalayer';

    const getUserToken = {
    base: 'auth',
    path: '/refreshToken',
    withCredentials: true,
    };

    // a 401 has occurred; get a new JWT token
    // for the current user
    async function reauthenticate() {
    // a Set-Cookie response header will update
    // the JWT that we send on future requests,
    // so we don't need to do anything else here
    return await fetch(createRequest(getUserToken));
    }

    export const pipeline = data.utils.withAuthentication(fetch, reauthenticate);

    Parameters

    • fetch: Fetch

      The fetch method to wrap.

    • reauthenticate: Reauthenticate

      Method to invoke to reauthenticate the user.

    Returns Fetch

    The wrapped fetch method.

  • Wraps the fetch method to cache successful Responses within a data pipeline.

    NOTE: You can easily create Store-backed data caches for this method using asDataCache().

    see

    asDataCache()

    throws

    An invalid cache was provided to withCache.

    example
    import { createRequest, fetch } from '~/path/to/datalayer';

    const getReportHistory = {
    method: 'GET',
    base: 'reports',
    path: '/history/:id'
    };

    // NOTE: use withEncryption(store, options) if the response
    // might contain personal or sensitive information that you
    // wish to keep secret
    const store = stores.indexedDB({ store: 'reports' });
    const attempt = data.utils.withCache(fetch, stores.utils.asDataCache(store));

    export async function loadReportHistory(id) {
    const params = { id };
    const request = createRequest(getReportHistory, params);
    const response = await attempt(request)
    .catch(errors.rethrow(params));
    return response.data;
    }

    Parameters

    Returns Fetch

    The wrapped fetch method.

  • Wraps the fetch operation to wait for a network connection prior to attempting the data operation.

    throws

    Argument reconnect must be a function.

    example
    import { showDialog, connectToNetwork } from '~/path/to/dialogs';
    import { fetch, createRequest } from '~/path/to/datalayer';

    const operation = {
    base: 'my-app',
    path: '/path/to/data'
    };

    async function waitForConnect() {
    return await showDialog(connectToNetwork, { modal: true });
    }

    const attempt = data.utils.withConnectivity(fetch, waitForConnect);

    export async function loadData() {
    const request = createRequest(operation);
    const response = await attempt(request);
    return response.data;
    }

    Parameters

    • fetch: Fetch

      The fetch method to wrap.

    • reconnect: Reconnect

      Returns a Promise that resolves when the user's network connection has been re-established.

    Returns Fetch

    The wrapped fetch method.

  • Invokes the specified method when a call is aborted (Response status = 0).

    throws

    Argument diagnostics must be a function.

    example
    import { noop } from 'lodash';
    import { fetch, createRequest } from '~/path/to/datalayer';
    import { tracker } from '~/path/to/tracker';
    import operations from '~/config/diagnostics.mjson';

    function trackResults(results) {
    tracker.event('diagnostics run', { results });
    }

    function asResultPromise(request) {
    const fromResponse = (response) =>
    `${request.url}: ${response.status} (${response.statusText})`;
    const fromError = (error) => fromResponse(error.response);
    return fetch(request).then(fromResponse, fromError).catch(noop);
    }

    function performNetworkTests(request) {
    // only perform diagnostics for calls that
    // were aborted between the user and our servers
    if (!request.url.includes('mydomain.com')) return;
    const requests = operations.map(createRequest);
    const responses = requests.map(asResultPromise);
    Promise.all(responses).then(trackResults);
    }

    export const fetchWithDiagnostics =
    data.utils.withDiagnostics(fetch, performNetworkTests);

    Parameters

    • fetch: Fetch

      The fetch method to wrap.

    • diagnostics: Diagnostics

      Invoked when a data call is aborted.

    Returns Fetch

    The wrapped fetch method.

  • Applies default Request headers.

    example
    import { fetch, createRequest } from '~/path/to/datalayer';

    const operation = {
    base: 'my-app',
    path: '/users/:id'
    };

    const attempt = data.utils.withHeaders(fetch, {
    'x-app-name': 'my-app',
    'x-platform': 'android',
    });

    export function getUser(id) {
    const params = { id };
    const request = createRequest(operation, params);
    const response = await attempt(request);
    return response.data;
    }

    Parameters

    • fetch: Fetch

      The fetch method to wrap.

    • headers: {} = {}

      The headers to apply to the Request headers collection. Request headers with will override any default headers with the same names specified here.

      Returns Fetch

      The wrapped fetch method.

    • Wraps fetch to provide automatic retry functionality when the operation fails. You can provide pre-defined retry logic using existing RetryFunction factories or by passing your own custom RetryFunction to this method.

      see

      falloff()

      example
      import { fetch, createRequest } from '~/path/to/datalayer';

      const getUser = {
      base: 'users',
      method: 'GET',
      path: '/users/:id',
      };

      function retryOnAbort(request, response) {
      // aborted and timed-out calls
      // should return status 0
      if (response.status === 0)
      return Promise.resolve(); // retry
      return Promise.reject(); // don't retry
      }

      const attempt = data.utils.withRetry(fetch, retryOnAbort);

      export async function loadUserData(id) {
      const params = { id };
      const request = createRequest(getUser, params);
      const response = await attempt(request)
      .catch(errors.rethrow(params));
      return response.data;
      }

      Parameters

      • fetch: Fetch

        The operation to wrap.

      • retry: RetryFunction

        Invoked to determine whether the data operation should be retried.

      • retries: Map<any, any> = ...

      Returns Fetch

      The wrapped fetch method.

    • Coordinates and synchronizes access to the data pipeline through the specified ManualResetSignal or AutoResetSignal.

      example
      // delay calls until the user is authenticated

      import fetch from 'path/to/datalayer';

      const authenticated = signals.manualReset(false);
      export const pipeline = data.utils.withSignal(fetch, authenticated);

      export function setAuthenticated() {
      authenticated.set();
      }
      example
      // prevent concurrent data calls from reaching the
      // server out of order by enforcing a sequence (i.e.
      // the first call must complete before a second call
      // is sent)

      import fetch from 'path/to/datalayer';

      // start the pipeline in a signaled state; each call will
      // automatically reset the signal, which will be set again
      // when the call completes, allowing the next call to proceed
      export const pipeline = data.utils.withSignal(fetch, signals.autoReset(true));

      Parameters

      Returns Fetch

      The wrapped fetch method.

    • Wraps the given fetch method to add optional request and response transformations to the data pipeline.

      example
      import pako from 'pako'; // compression
      import { fetch, createRequest } from '~/path/to/datalayer';

      const saveUser = {
      method: 'POST',
      base: 'my-app',
      path: '/users/:id',
      headers: {
      'content-encoding': 'deflate'
      }
      };

      // NOTE: `request` and `response` are optional;
      // see the Transformer documentation for details
      const compressor = {
      request(body, headers) {
      const json = JSON.stringify(body);
      if (headers['content-encoding'] === 'deflate')
      return pako.deflate(json);
      return json;
      }
      };

      const attempt = data.utils.withTransform(fetch, compressor);

      export async function saveUserData(id, user) {
      const params = { id };
      const request = createRequest(saveUser, params, user);
      const response = await attempt(request);
      return response.meta.headers['e-tag'];
      }

      Parameters

      • fetch: Fetch

        The fetch method to wrap.

      • transformer: Transformer

        Determines how the Request and Response should be transformed before being passed to the next stage in the data pipeline.

      Returns Fetch

      The wrapped fetch method.

    • Adds Cross-Site Request Forgery protection to Requests made to the same origin where the app is hosted. For this to work, the server and client agree on a unique token to identify the user and prevent cross-site requests from being sent without the user's knowledge.

      By default, this method reads the XSRF-TOKEN cookie value sent by the server and adds it as the X-XSRF-TOKEN request header on any requests sent to the same origin. You can configure the cookie and/or the header name by passing your own values in the options argument. You can also specify a whitelist of cross-origin hostnames the header should be sent to (e.g. to subdomains of the host domain). See XSRFOptions for details.

      Read more about XSRF and implementation best practices here.

      see

      OWASP XSRF Cheat Sheet

      example
      import { fetch } from '~/path/to/datalayer';

      export const safeFetch = data.utils.withXSRF(fetch);
      example
      import { fetch } from '~/path/to/datalayer';

      export const safeFetch = data.utils.withXSRF(fetch, {
      cookie: 'XSRF-MY-APP',
      header: 'X-XSRF-MY-APP',
      hosts: ['*.my-app.com']
      });

      Parameters

      • fetch: Fetch

        The fetch method to wrap.

      • options: XSRFOptions = {}

        Optional overrides for cookie and header names.

      Returns Fetch

      The wrapped fetch method.