Source: argos-sdk/src/Store/SData.js

/* Copyright (c) 2010, Sage Software, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */
import declare from 'dojo/_base/declare';
import lang from 'dojo/_base/lang';
import Deferred from 'dojo/_base/Deferred';
import QueryResults from 'dojo/store/util/QueryResults';
import convert from '../Convert';
import utility from '../Utility';

/**
 * @class argos.Store.SData
 * @classdesc SData is an extension of dojo.store that is tailored to handling SData parameters, requests,
 * and pre-handling the responses.
 */
const __class = declare('argos.Store.SData', null, /** @lends argos.Store.SData# */ {
  doDateConversion: false,

  /* todo: is this the appropriate name for the expansion scope? */
  scope: null,
  where: null,
  select: null,
  include: null,
  orderBy: null,
  service: null,
  request: null,
  queryName: null,
  queryArgs: null,
  entityName: null,
  contractName: null,
  resourceKind: null,
  resourceProperty: null,
  resourcePredicate: null,
  applicationName: null,
  dataSet: null,
  executeQueryAs: null,
  executeGetAs: null,

  itemsProperty: '$resources',
  idProperty: '$key',
  labelProperty: '$descriptor',
  entityProperty: '$name',
  versionProperty: '$etag',

  /**
   * @constructs
   */
  constructor: function constructor(props) {
    lang.mixin(this, props);
  },
  _createRemoveRequest: function createRemoveRequest(object) {
    const request = new Sage.SData.Client.SDataSingleResourceRequest(this.service);
    let id = object && object.$key;

    id = id || utility.expand(this.scope || this.resourcePredicate);

    if (/(\s+)/.test(id)) {
      request.setResourceSelector(id);
    } else {
      request.setResourceSelector(`'${id}'`);
    }
    if (this.resourceKind) {
      request.setResourceKind(this.resourceKind);
    }
    if (this.contractName) {
      request.setContractName(this.contractName);
    }

    return request;
  },
  _createEntryRequest: function _createEntryRequest(identity, getOptions) {
    let request = utility.expand(this, getOptions.request || this.request);
    let id = identity;
    if (request) {
      request = request.clone();
    } else {
      id = id || utility.expand(this.scope || this, getOptions.resourcePredicate || this.resourcePredicate);

      const contractName = utility.expand(this.scope || this, getOptions.contractName || this.contractName);
      const resourceKind = utility.expand(this.scope || this, getOptions.resourceKind || this.resourceKind);
      const dataSet = utility.expand(this.scope || this, getOptions.dataSet || this.dataSet);
      const resourceProperty = utility.expand(this.scope || this, getOptions.resourceProperty || this.resourceProperty);
      let resourcePredicate;

      if (id) {
        resourcePredicate = /\s+/.test(id) ? id : `'${id}'`;
      }

      if (resourceProperty) {
        request = new Sage.SData.Client.SDataResourcePropertyRequest(this.service)
          .setResourceProperty(resourceProperty)
          .setResourceSelector(resourcePredicate);
      } else {
        request = new Sage.SData.Client.SDataSingleResourceRequest(this.service)
          .setResourceSelector(resourcePredicate);
      }

      if (contractName) {
        request.setContractName(contractName);
      }

      if (resourceKind) {
        request.setResourceKind(resourceKind);
      }

      if (dataSet) {
        request.setDataSet(dataSet);
      }
    }

    const select = utility.expand(this.scope || this, getOptions.select || this.select);
    const include = utility.expand(this.scope || this, getOptions.include || this.include);

    if (select && select.length > 0) {
      request.setQueryArg('select', select.join(','));
    }

    if (include && include.length > 0) {
      request.setQueryArg('include', include.join(','));
    }

    return request;
  },
  _createFeedRequest: function _createFeedRequest(q, queryOptions) {
    let request = utility.expand(this, queryOptions.request || this.request);
    if (request) {
      request = request.clone();
    } else {
      const queryName = utility.expand(this.scope || this, queryOptions.queryName || this.queryName);
      const contractName = utility.expand(this.scope || this, queryOptions.contractName || this.contractName);
      const resourceKind = utility.expand(this.scope || this, queryOptions.resourceKind || this.resourceKind);
      const resourceProperty = utility.expand(this.scope || this, queryOptions.resourceProperty || this.resourceProperty);
      const resourcePredicate = utility.expand(this.scope || this, queryOptions.resourcePredicate || this.resourcePredicate);
      const applicationName = utility.expand(this.scope || this, queryOptions.applicationName || this.applicationName);
      const dataSet = utility.expand(this.scope || this, queryOptions.dataSet || this.dataSet);
      const queryArgs = utility.expand(this.scope || this, queryOptions.queryArgs || this.queryArgs);

      if (queryName) {
        request = new Sage.SData.Client.SDataNamedQueryRequest(this.service)
          .setQueryName(queryName);

        if (resourcePredicate) {
          request.getUri().setCollectionPredicate(resourcePredicate);
        }
      } else if (resourceProperty) {
        request = new Sage.SData.Client.SDataResourcePropertyRequest(this.service)
          .setResourceProperty(resourceProperty)
          .setResourceSelector(resourcePredicate);
      } else {
        request = new Sage.SData.Client.SDataResourceCollectionRequest(this.service);
      }

      if (contractName) {
        request.setContractName(contractName);
      }

      if (resourceKind) {
        request.setResourceKind(resourceKind);
      }

      if (applicationName) {
        request.setApplicationName(applicationName);
      }

      if (dataSet) {
        request.setDataSet(dataSet);
      }

      if (queryArgs) {
        for (const arg in queryArgs) {
          if (queryArgs.hasOwnProperty(arg)) {
            request.setQueryArg(arg, queryArgs[arg]);
          }
        }
      }
    }

    const select = utility.expand(this.scope || this, queryOptions.select || this.select);
    const include = utility.expand(this.scope || this, queryOptions.include || this.include);
    const orderBy = utility.expand(this.scope || this, queryOptions.sort || this.orderBy);

    if (select && select.length > 0) {
      request.setQueryArg('select', select.join(','));
    }

    if (include && include.length > 0) {
      request.setQueryArg('include', include.join(','));
    }

    if (orderBy) {
      if (typeof orderBy === 'string') {
        request.setQueryArg('orderby', orderBy);
      } else if (orderBy.length > 0) {
        const order = [];
        orderBy.forEach(function forEach(v) {
          if (v.descending) {
            this.push(`${v.attribute} desc`);
          } else {
            this.push(v.attribute);
          }
        }, order);

        request.setQueryArg('orderby', order.join(','));
      }
    }

    const where = utility.expand(this.scope || this, queryOptions.where || this.where);
    const conditions = [];

    if (where) {
      conditions.push(where);
    }

    const query = utility.expand(this.scope || this, q);

    if (query) {
      conditions.push(query);
    }

    if (conditions.length > 0) {
      request.setQueryArg('where', `(${conditions.join(') and (')})`);
    }

    if (typeof queryOptions.start !== 'undefined') {
      request.setQueryArg(Sage.SData.Client.SDataUri.QueryArgNames.StartIndex, queryOptions.start + 1);
    }

    if (typeof queryOptions.count !== 'undefined') {
      request.setQueryArg(Sage.SData.Client.SDataUri.QueryArgNames.Count, queryOptions.count);
    }

    return request;
  },
  _onCancel: function _onCancel(/* deferred*/) {},
  _onRequestFeedSuccess: function _onRequestFeedSuccess(queryDeferred, feed) {
    if (feed) {
      const items = lang.getObject(this.itemsProperty, false, feed);
      const total = typeof feed.$totalResults === 'number' ? feed.$totalResults : -1;

      queryDeferred.total = total;
      queryDeferred.resolve(items);
    } else {
      const error = new Error('The feed result is invalid.');
      queryDeferred.reject(error);
    }
  },
  _onRequestEntrySuccess: function _onRequestEntrySuccess(deferred, entry) {
    if (entry) {
      deferred.resolve(this.doDateConversion ? this._handleDateConversion(entry) : entry);
    } else {
      const error = new Error('The entry result is invalid.');
      deferred.reject(error);
    }
  },
  _onRequestFailure: function _onRequestFailure(deferred, xhr, xhrOptions) {
    const error = new Error(`An error occurred requesting: ${xhrOptions.url}`);

    error.xhr = xhr;
    error.status = xhr.status;
    error.aborted = false;
    error.url = xhrOptions.url;

    deferred.reject(error);
  },
  _onRequestAbort: function _onRequestAbort(deferred, xhr, xhrOptions) {
    const error = new Error(`An error occurred requesting: ${xhrOptions.url}`);

    error.xhr = xhr;
    error.status = 0;
    error.responseText = null;
    error.aborted = true;

    deferred.reject(error);
  },
  _handleDateConversion: function _handleDateConversion(entry) {
    for (const prop in entry) {
      if (convert.isDateString(entry[prop])) {
        entry[prop] = convert.toDateFromString(entry[prop]);
      }
    }

    return entry;
  },
  get: function get(id, getOptions /* sdata only */) {
    const handle = {};
    const deferred = new Deferred();
    const request = this._createEntryRequest(id, getOptions || {});
    const method = this.executeGetAs ? request[this.executeGetAs] : request.read;

    handle.value = method.call(request, {
      success: this._onRequestEntrySuccess.bind(this, deferred),
      failure: this._onRequestFailure.bind(this, deferred),
      aborted: this._onRequestAbort.bind(this, deferred),
    });

    return deferred;
  },
  /**
   * Returns an object's identity using this.idProperty
   * @param {Object} object The object to get the identity from
   * @returns {String|Number}
   */
  getIdentity: function getIdentity(object) {
    return lang.getObject(this.idProperty, false, object);
  },
  /**
   * Returns an object's label using this.labelProperty
   * @param {Object} object The object to get the label from
   * @returns {String}
   */
  getLabel: function getLabel(object) {
    return lang.getObject(this.labelProperty, false, object);
  },
  /**
   * Returns an object's entity using this.entityProperty
   * @param {Object} object The object to get the entity from
   * @returns {String|Object}
   */
  getEntity: function getEntity(object) {
    return lang.getObject(this.entityProperty, false, object);
  },
  /**
   * Returns an object's version using this.versionProperty
   * @param {Object} object The object to get the version from
   * @returns {String}
   */
  getVersion: function getVersion(object) {
    return lang.getObject(this.versionProperty, false, object);
  },
  /**
   * Stores an object.
   * @param {Object} object The object to store.
   * @param {Object} putOptions Additional directives for storing objects.
   * @param {String|Number} putOptions.id
   * @param {String|Object} putOptions.entity
   * @param {String} putOptions.version
   * @param {Boolean} putOptions.overwrite
   * @returns {String|Number}
   */
  put: function put(object, putOptions = {}) {
    const id = putOptions.id || this.getIdentity(object);
    const entity = putOptions.entity || this.entityName;
    const version = putOptions.version || this.getVersion(object);
    const atom = !this.service.isJsonEnabled();

    if (id) {
      object.$key = id;
    }

    if (entity && atom) {
      object.$name = entity;
    }

    if (version) {
      object.$etag = version;
    }

    const handle = {};
    const deferred = new Deferred();
    const request = this._createEntryRequest(id, putOptions);
    const method = putOptions.overwrite ? request.update : request.create;

    handle.value = method.call(request, object, {
      success: this._onTransmitEntrySuccess.bind(this, deferred),
      failure: this._onRequestFailure.bind(this, deferred),
      aborted: this._onRequestAbort.bind(this, deferred),
    });

    return deferred;
  },
  _onTransmitEntrySuccess: function _onTransmitEntrySuccess(deferred, entry) {
    deferred.resolve(this.doDateConversion ? this._handleDateConversion(entry) : entry);
  },
  /**
   * Creates an object, throws an error if the object already exists.
   * @param {Object} object The object to store
   * @param {Object} addOptions Additional directives for creating objects
   * @param {Boolean} addOptions.overwrite
   */
  add: function add(object, addOptions = {}) {
    addOptions.overwrite = false;
    return this.put(object, addOptions);
  },

  /**
   * Deletes an entry
   *
   * @param {Object} entry The entry to be removed.
   * @param {Object} removeOptions additional directives for removing options.
   *
   */
  remove: function remove(object) {
    const request = this._createRemoveRequest(object);
    const handle = {};
    const deferred = new Deferred();
    const method = request.delete;

    handle.value = method.call(request, object, {
      success: this._onTransmitEntrySuccess.bind(this, deferred),
      failure: this._onRequestFailure.bind(this, deferred),
      aborted: this._onRequestAbort.bind(this, deferred),
    });

    return deferred;
  },
  /**
   * Queries the store for objects. This does not alter the store, but returns a
   * set of data from the store.
   *
   * @param {String|Object|Function} query The query to use for retrieving objects from the store.
   * @param {Object} queryOptions
   * @returns {dojo.store.api.Store.QueryResults}
   *
   */
  query: function query(q, queryOptions) {
    const handle = {};
    const queryDeferred = new Deferred(this._onCancel.bind(this, handle));
    const request = this._createFeedRequest(q, queryOptions || {});

    queryDeferred.total = -1;
    const options = {
      success: this._onRequestFeedSuccess.bind(this, queryDeferred),
      failure: this._onRequestFailure.bind(this, queryDeferred),
      aborted: this._onRequestAbort.bind(this, queryDeferred),
      httpMethodOverride: queryOptions && queryOptions.httpMethodOverride,
    };

    let method = request.read;
    if (this.executeQueryAs) {
      method = request[this.executeQueryAs];
    } else if (request instanceof Sage.SData.Client.SDataResourcePropertyRequest) {
      method = request.readFeed;
    } else if (request instanceof Sage.SData.Client.SDataServiceOperationRequest) {
      method = request.execute;
      handle.value = method.call(request, this.entry, options);
      return QueryResults(queryDeferred); // eslint-disable-line
    }

    handle.value = method.call(request, options);
    return QueryResults(queryDeferred); // eslint-disable-line
  },
  /**
   * Not implemented in this store.
   */
  transaction: function transaction() {},
  /**
   * Not implemented in this store.
   */
  getChildren: function getChildren(/* parent, options*/) {},
  /**
   * Returns any metadata about the object. This may include attribution,
   * cache directives, history, or version information.
   *
   * @param {Object} object The object to return metadata for.
   * @return {Object} Object containing the metadata.
   * @return {String|Number} return.id
   * @return {String} return.label
   * @return {String|Object} return.entity
   * @return {String} return.version
   */
  getMetadata: function getMetadata(object) {
    if (object) {
      return {
        id: this.getIdentity(object),
        label: this.getLabel(object),
        entity: this.getEntity(object),
        version: this.getVersion(object),
      };
    }

    return null;
  },
});

export default __class;