src/menu/v1beta1/remote-v1.js

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * Copyright 2019, Momentum Ideas, Co. All rights reserved.
 *
 * Source and object computer code contained herein is the private intellectual
 * property of Bloombox, a California Limited Liability Corporation. Use of this
 * code in source form requires permission in writing before use or the
 * assembly, distribution, or publishing of derivative works, for commercial
 * purposes or any other purpose, from a duly authorized officer of Momentum
 * Ideas Co.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Bloombox: Menu RPC Client, v1beta1
 *
 * @fileoverview Provides an implementation of the Bloombox Menu RPC client,
 *               using the modern gRPC-based framework.
 */

/*global goog */

goog.require('bloombox.SERVICE_MODE');

goog.require('bloombox.logging.log');

goog.require('bloombox.menu.MenuAPI');
goog.require('bloombox.menu.ObservableMenu');
goog.require('bloombox.menu.RetrieveCallback');
goog.require('bloombox.menu.RetrieveOptions');
goog.require('bloombox.menu.processMenu');

goog.require('bloombox.rpc.metadata');

goog.require('proto.bloombox.services.menu.v1beta1.GetFeatured.Request');
goog.require('proto.bloombox.services.menu.v1beta1.GetMenu.Request');
goog.require('proto.bloombox.services.menu.v1beta1.GetMenu.Response');
goog.require('proto.bloombox.services.menu.v1beta1.GetProduct.Request');
goog.require('proto.bloombox.services.menu.v1beta1.MenuPromiseClient');
goog.require('proto.bloombox.services.menu.v1beta1.MenuStreamClient');
goog.require('proto.opencannabis.products.menu.section.Section');

goog.provide('bloombox.menu.v1beta1.RemoteService');


/**
 * Prepare a menu retrieve request, by taking the rendered/collapsed request
 * configuration and generating a `GetMenu.Request` object.
 *
 * @param {?bloombox.menu.RetrieveOptions=} config Rendered settings for this
 *        request, produced from override and global config.
 * @return {!proto.bloombox.services.menu.v1beta1.GetMenu.Request} Prepared
 *         request object to retrieve or stream a menu.
 */
function prepRetrieveRequest(config) {
  const resolved = config || bloombox.menu.RetrieveOptions.defaults();
  const request = new proto.bloombox.services.menu.v1beta1.GetMenu.Request();

  // copy in options
  if (resolved.full === true) request.setFull(true);
  if (resolved.fresh === true) request.setFresh(true);
  if (resolved.keysOnly === true) request.setKeysOnly(true);
  if (resolved.snapshot) request.setSnapshot(
    /** @type {string} */ (resolved.snapshot));
  if (resolved.fingerprint) request.setFingerprint(
    /** @type {string} */ (resolved.fingerprint));
  if (resolved.section !==
    proto.opencannabis.products.menu.section.Section.UNSPECIFIED)
    request.setSection(resolved.section);

  const scope = bloombox.rpc.context(resolved);
  const partnerCode = scope.partner;
  const locationCode = scope.location;
  const scopePath = `partner/${partnerCode}/location/${locationCode}`;
  request.setScope(scopePath);
  return request;
}


goog.scope(function() {
  /**
   * Defines an implementation of the Bloombox Menu API, which calls into modern
   * RPC dispatch via gRPC.
   *
   * @implements bloombox.menu.MenuAPI
   */
  bloombox.menu.v1beta1.RemoteService = (class MenuV1 {
    /**
     * Construct a new instance of the `v1beta1` Menu API service. The instance is
     * pre-configured with requisite top-level config and afterwards ready to make
     * calls out to remote services.
     *
     * @param {bloombox.config.JSConfig} sdkConfig JavaScript SDK config.
     */
    constructor(sdkConfig) {
      /**
       * Active JS SDK configuration.
       *
       * @const
       * @private
       * @type {bloombox.config.JSConfig}
       */
      this.sdkConfig = sdkConfig;

      /**
       * Service client, which is responsible for mediating calls between the RPC
       * server and the local RPC client.
       *
       * @const
       * @private
       * @type {proto.bloombox.services.menu.v1beta1.MenuPromiseClient}
       */
      this.client = (
        new proto.bloombox.services.menu.v1beta1.MenuPromiseClient(
          sdkConfig.endpoint,
          null,
          {'format': bloombox.SERVICE_MODE}));

      /**
       * Service client, which is responsible for managing live streaming of menu
       * updates from the server, as underlying menu data changes.
       *
       * @const
       * @type {proto.bloombox.services.menu.v1beta1.MenuStreamClient}
       */
      this.liveStream = (
        new proto.bloombox.services.menu.v1beta1.MenuStreamClient(
          sdkConfig.endpoint,
          null,
          {'format': 'text'}));  // @TODO binary for streaming
    }

    // -- Menu Retrieve -- //
    /**
     * Retrieve a full menu via Bloombox systems, using the new binary gRPC API
     * interface, for a given retail location. Before this method is called, the
     * user should setup their partnership information via the `setup` method,
     * including their partner code, location code, and API key.
     *
     * Once `setup` calls back, indicating the library is ready for use, a full
     * menu catalog can be fetched via this method, according to the options
     * specified in the `options` parameter.
     *
     * @export
     * @override
     * @param {?bloombox.menu.RetrieveCallback=} callback Function to dispatch once
     *        data is available for the underlying menu catalog.
     * @param {?bloombox.menu.RetrieveOptions=} options Configuration options for
     *        this menu retrieval operation. See type docs for more info.
     * @return {Promise<proto.bloombox.services.menu.v1beta1.GetMenu.Response>}
     *         Promise attached to the underlying RPC call.
     */
    retrieve(callback, options) {
      const resolved = options || bloombox.menu.RetrieveOptions.defaults();
      const request = prepRetrieveRequest(resolved);
      if (request.getKeysOnly() === true) {
        bloombox.logging.log(
          'Requesting keys-only menu...', {'options': options});
      }

      const operation = (
        this.client.retrieve(request, bloombox.rpc.metadata(this.sdkConfig)));
      operation.catch((err) => {
        if (callback) callback(null, err);
      });

      operation.then((resp) => {
        if (callback) {
          if (resp.hasCatalog()) {
            const deferred = bloombox.menu.processMenu(
              resp.getCatalog(), request.getKeysOnly());
            if (deferred) {
              deferred.addCallback(() => {
                callback(resp, null);
              });
              deferred.addErrback((err) => {
                bloombox.logging.error('Error persisting menu locally.',
                  {'err': err});
                callback(resp, null);
              });
            } else {
              callback(resp, null);
            }
          } else {
            callback(null, null);
          }
        }
      });
      return operation;
    }

    // -- API: Menu Stream -- //
    /**
     * Establish a stream over which we can receive menu change updates. Initially
     * a full menu payload is sent, to sync the client with the server's state,
     * and subsequently, delta updates are relayed as they occur in underlying
     * menu catalog storage.
     *
     * Depending on the settings passed in `config`, the delta payload will
     * reference a changed/added/deleted product by key, or enclose the full
     * product payload. Each time, an updated menu fingerprint is sent back.
     *
     * @export
     * @override
     * @param {?proto.opencannabis.products.menu.Menu=} localMenu Local-side
     *        menu to compare with the server. Fingerprint config setting is
     *        required if a local menu is provided, for comparison server-side.
     * @param {?bloombox.menu.RetrieveOptions=} config Options, or configuration,
     *        to apply in the scope of just this RPC operation. In some cases, a
     *        given API method may not apply or use all options. If left unset, a
     *        sensible set of default settings is generated and used.
     * @return {bloombox.menu.ObservableMenu} Observable menu object, which wraps
     *        a promise for the initial menu, and provides methods for subscribing
     *        to menu data changes (which are dispatched after being applied to
     *        any active local DB/caching engine).
     */
    stream(localMenu, config) {
      const resolved = config || bloombox.menu.RetrieveOptions.defaults();
      const request = prepRetrieveRequest(resolved);
      const operation = (
        this.liveStream.live(request, bloombox.rpc.metadata(this.sdkConfig)));

      // setup the observable menu and return
      return new bloombox.menu.ObservableMenu(
        operation, resolved.fingerprint, localMenu);
    }

    // -- API: Product Retrieval -- //
    /**
     * Fetch an individual product record, addressed by its unique product key,
     * which is comprised of the product's type, and the product's key ID (which
     * is an opaque string value provisioned when the product is created).
     *
     * Once either product data or a terminal error state are encountered, the
     * given callback, if provided, is dispatched, and the resulting promise is
     * fulfilled. If a result is available, it is passed in as the first
     * parameter of the callback, otherwise, an error is passed in as the second
     * parameter. In no case are two values passed.
     *
     * @export
     * @override
     * @param {proto.opencannabis.base.ProductKey} key Product key to fetch.
     * @param {?bloombox.menu.ProductCallback=} callback Callback to dispatch
     *        once either a result or terminal error state are reached.
     * @param {?bloombox.menu.RetrieveOptions=} config Configuration options to
     *        apply to this request.
     * @return {Promise<proto.bloombox.services.menu.v1beta1.GetProduct.Response>}
     *         Promise attached to the underlying RPC call.
     * @throws {bloombox.rpc.RPCException} If an error occurs preparing to send
     *         the underlying RPC, or during transmission.
     */
    product(key, callback, config) {
      const resolved = config || bloombox.menu.RetrieveOptions.defaults();
      const request = (
        new proto.bloombox.services.menu.v1beta1.GetProduct.Request());

      // copy in product key
      request.setKey(key);

      // resolve scope
      const scope = bloombox.rpc.context(resolved);
      request.setScope(`partner/${scope.partner}/location/${scope.location}`);

      if (resolved.fresh === true) request.setFresh(true);
      if (resolved.fingerprint) request.setFingerprint(resolved.fingerprint);
      const operation = (
        this.client.products(request, bloombox.rpc.metadata(this.sdkConfig)));

      operation.catch((err) => {
        if (callback) callback(null, err);
      });

      operation.then((response) => {
        if (callback) callback(response, null);
      });
      return operation;
    }

    // -- API: Featured Products -- //
    /**
     * Retrieve featured products for a given menu section. "Featured" products
     * are items with the "FEATURED" flag present in their product flags, as
     * indicated by staff or external systems via the Bloombox Dashboard.
     *
     * @export
     * @override
     * @param {?proto.opencannabis.products.menu.section.Section} section Menu
     *        section to fetch. If left unset, fetches across all sections.
     * @param {?bloombox.menu.FeaturedCallback=} callback Callback to dispatch
     *        once a dataset of products is available, or a terminal error is
     *        reached. Optional.
     * @param {?bloombox.menu.RetrieveOptions=} config Options, or configuration,
     *        to apply in the scope of just this RPC operation. In some cases, a
     *        given API method may not apply or use all options. If left unset, a
     *        sensible set of default settings is generated and used.
     * @return {Promise<proto.bloombox.services.menu.v1beta1.GetFeatured.Response>}
     *         Promise attached to the underlying RPC call.
     * @throws {bloombox.rpc.RPCException} If an error occurs preparing to send
     *         the underlying RPC, or during transmission.
     */
    featured(section, callback, config) {
      const resolved = config || bloombox.menu.RetrieveOptions.defaults();
      const request = (
        new proto.bloombox.services.menu.v1beta1.GetFeatured.Request());

      // resolve scope
      const scope = bloombox.rpc.context(resolved);
      request.setScope(`partners/${scope.partner}/locations/${scope.location}`);

      // copy in section, if specified
      if (section) request.setSection(section);

      // copy in options, if specified
      if (resolved.keysOnly) request.setKeysOnly(true);

      // fire it off
      const operation = (
        this.client.featured(request, bloombox.rpc.metadata(this.sdkConfig)));

      operation.then((response) => {
        if (callback) callback(response, null);
      });
      operation.catch((err) => {
        if (callback) callback(null, err);
      });
      return operation;
    }
  });
});