API Docs for: 0.1.0.ee3e9e64
Show:

File: addon/adapters/solr.js

/**
  @module solr
*/

import Ember from 'ember';
import DS from 'ember-data';
import {
  SolrSearchHandler,
  SolrRealTimeGetHandler,
  SolrUpdateHandler,
  SolrDeleteHandler
} from 'ember-solr/lib/handlers';

import SolrCommitType from 'ember-solr/lib/commit-type';
import SolrRequest from 'ember-solr/lib/request';
import SolrUpdateMode from 'ember-solr/lib/update-mode';
import bigNumberStringify from 'ember-solr/lib/big-number-stringify';
import ConcurrentModificationError from 'ember-solr/concurrent-modification-error';

const forEach = Ember.ArrayPolyfills.forEach,
      get = Ember.get,
      set = Ember.set;

/**
  Ember Data Adapter for Apache Solr.
  @class SolrAdapter
  @extends DS.Adapter
*/
export default DS.Adapter.extend({
  /**
    The base URL where the Solr instance is hosted.
    This property is typically configured by setting
    `ENV.solrBaseURL` in your `config/environment.js`
    file.
    @property baseURL
    @type {string}
    @default '/solr'
  */
  baseURL: '/solr',

  /**
    When enabled, sends a `commit` command to Solr in update
    requests to commit the index synchronously and
    block the request until commit has completed.

    There are many considerations when choosing to
    enable this feature. Consult the Solr
    [documentation](https://wiki.apache.org/solr/SolrConfigXml#Update_Handler_Section)
    on autoCommit, and softAutoCommit.

    See also {{#crossLink "SolrAdapter/commitWithinMilliseconds:property"}}{{/crossLink}}.

    @property commit
    @type {SolrCommitType}
    @default SolrCommitType.None
  */
  commit: SolrCommitType.None,

  /**
    When set, sends a `commitWithin` command to Solr
    in update requests to have the update committed
    within a time limit (in milliseconds). Solr will
    aggregate multiple pending writes into a single
    commit to reduce overhead and improve performance.

    This property, when set, takes precedence over
    {{#crossLink "SolrAdapter/commit:property"}}{{/crossLink}}.

    There are many considerations when choosing to
    enable this feature. Consult the Solr
    [documentation](https://wiki.apache.org/solr/SolrConfigXml#Update_Handler_Section)
    on autoCommit, and softAutoCommit.

    In Solr 4 and later, commitWithin is handled by default
    as a soft commit. See [UpdateHandlers in SolrConfig](https://cwiki.apache.org/confluence/display/solr/UpdateHandlers+in+SolrConfig#UpdateHandlersinSolrConfig-commitWithin).

    See also {{#crossLink "SolrAdapter/commit:property"}}{{/crossLink}}.

    @property commitWithinMilliseconds
    @type {number} milliseconds
    @default undefined
  */
  commitWithinMilliseconds: undefined,

  /**
    Specifies a default Solr Core to send requests
    to. If no default core is configured, this adapter
    will not include a core in the request URI path
    and the Solr server will use its own default.
    @property defaultCore
    @type {string}
    @default null
  */
  defaultCore: null,

  /**
    Sets the default serializer for this adapter.
    Uses {{#crossLink "SolrSerializer"}}{{/crossLink}} by default.
    @property defaultSerializer
    @type {string}
    @default '-solr'
  */
  defaultSerializer: '-solr',

  /**
    Sets the data type for jQuery ajax requests.
    Either `json` or `jsonp` are supported.
    `jsonp` is provided as the default to allow cross-origin
    requests to succeed without needing special customization
    of the Solr server.
    @property dataType
    @type {string}
    @default 'jsonp'
  */
  dataType: 'jsonp',

  /**
    Enables or disables sending requests to Solr's
    Real Time Get handler. Note that this handler is
    disabled by default on many Solr servers.

    Real Time Get allows retrieval of documents that
    have not yet been committed by retrieving them from
    the update log.

    If you are using SolrCloud, it is generally safe to
    enable this feature.

    @property enableRealTimeGet
    @type {boolean}
    @default false
  */
  enableRealTimeGet: false,

  /**
    Sets the concurrency mode for how updates are sent
    to Solr.

    @property updateMode
    @type {SolrUpdateMode}
    @default SolrUpdateType.None
  */
  updateMode: SolrUpdateMode.None,

  /**
    Find a record by its unique ID.

    @method find
  */
  find: function(store, type, id) {
    var request = this.buildRequest(store, type, 'find', id);

    return this.executeRequest(request);
  },

  /**
    Find all documents of a type.

    @method findAll
  */
  findAll: function(store, type, sinceToken) {
    console.log('findAll since', sinceToken);
    var request = this.buildRequest(store, type, 'findAll');

    return this.executeRequest(request);
  },

  /**
    Find multiple documents in a single request.

    @method findMany
  */
  findMany: function(store, type, ids) {
    var request = this.buildRequest(store, type, 'findMany', ids);

    return this.executeRequest(request);
  },

  /**
    Find one or more records by arbitrary query

    The query hash should include the key `q` with
    an appropriate Solr query to execute. If this key
    is not specified, `*:*` will be used to match all
    documents.

    The query hash may include the keys `limit` and/or
    `offset` to override the Solr request handler's
    page size and retrieve rows from a given offset.

    @method findQuery
  */
  findQuery: function(store, type, query) {
    var request = this.buildRequest(store, type, 'findQuery', query);

    return this.executeRequest(request);
  },

  createRecord: function(store, type, snapshot) {
    return this.update(store, type, snapshot, 'createRecord');
  },

  updateRecord: function(store, type, snapshot) {
    return this.update(store, type, snapshot, 'updateRecord');
  },

  deleteRecord: function(store, type, snapshot) {
    return this.update(store, type, snapshot, 'deleteRecord');
  },

  update: function(store, type, snapshot, operation) {
    var options = {
      includeId: true,
      updateMode: get(this, 'updateMode')
    };

    var doc = this.serialize(snapshot, options);

    var request = this.buildRequest(store, type, operation, doc);

    var promise = this.executeRequest(request);

    if (operation === 'deleteRecord') {
      return promise;
    }

    var self = this;

    return promise.then(function() {
      return self.find(store, type, snapshot.id);
    });
  },

  serialize: function(snapshot, options) {
    var store = snapshot.record.store;
    var serializer = store.serializerFor(snapshot.typeKey);
    return serializer.serialize(snapshot, options);
  },

  /**
    Builds a request to send to Solr.

    @method buildRequest
    @param {instance of DS.Store} store
    @param {subclass of DS.Model} type the model type
    @param {string} operation one of `find`, `findQuery`, etc.
    @param {data} data to be sent in the request
    @return {SolrRequest} request
    @protected
  */
  buildRequest: function(store, type, operation, data) {
    var handler = this.handlerForType(type, operation);

    handler.prepare(this, store, type, operation, data);

    return SolrRequest.create({
      core: this.coreForType(type, operation),
      handler: handler,
      data: get(handler, 'data')
    });
  },

  /**
    Determines which Solr Core should handle queries for
    a given type and oepration. By default,
    {{#crossLink "SolrAdapter/defaultCore:property"}}{{/crossLink}}
    is used.
    @method coreForType
    @param {subclass of DS.Model} type
    @param {String} operation
    @return {String} core name
    @protected
  */
  coreForType: function() {
    return get(this, 'defaultCore');
  },

  /**
    Determines the [unique key](https://wiki.apache.org/solr/UniqueKey)
    for a given type. Default Solr schemas use the canonical field `id`
    and this method defaults to the same field.
    @method uniqueKeyForType
    @param {subclass of DS.Model} type
    @return {String}
    @protected
  */
  uniqueKeyForType: function() {
    return 'id';
  },

  /**
    Determines which Solr Core should handle queries for
    a given type and operation.

    When
    {{#crossLink "SolrAdapter/enableRealTimeGet:property"}}{{/crossLink}}
    is set to `true`, this method will choose RealTimeGet
    for `find` and `findMany` operations.

    Override this method to customize the path and type
    of handler that should be used for given operations.

    @method handlerForType
    @param {subclass of DS.Model} type
    @param {String} operation
    @return {SolrRequestHandler} handler instance
    @protected
  */
  handlerForType: function(type, operation) {
    var enableRealTimeGet = get(this, 'enableRealTimeGet');

    if (enableRealTimeGet &&
        (operation === 'find' || operation === 'findMany')) {
      return SolrRealTimeGetHandler.create();
    }

    if (operation === 'updateRecord' || operation === 'createRecord') {
      return SolrUpdateHandler.create();
    }

    if (operation === 'deleteRecord') {
      return SolrDeleteHandler.create();
    }

    return SolrSearchHandler.create();
  },

  /**
    Builds an optional filter query (`fq`) to include in search requests.
    If multiple models are stored in the same Solr Core, applying
    an appropriate filter query will ensure only the documents of
    the appropriate type are included.
    Example
    ```javascript
    App.ApplicationAdapter = SolrAdapter.extend({
      filterQueryForType: function(type) {
        return 'doc_type:' + type;
      }
    });
    ```
    See [CommonQueryParameters](https://wiki.apache.org/solr/CommonQueryParameters#fq).
    @method filterQueryForType
    @param {String} type
    @param {String} operation
    @return {String} a filter query or `null`
    @protected
  */

  /**
    Builds a complete URL and initiates
    an AJAX request to Solr.

    @method executeRequest
    @param {SolrRequest} request
    @return {Promise} promise
    @protected
  */
  executeRequest: function(request) {
    var URL = this.combinePath(
      get(this, 'baseURL'),
      get(request, 'core'),
      get(request, 'handler.path'));

    return this.ajax(URL, get(request, 'method'), get(request, 'options'));
  },

  /**
    Joins two or more strings into a path delimited
    by forward slashes without adding redundant slashes.
    Any number of arguments can be passed into this method.
    @method combinePath
    @param {string} path1
    @param {string} path2
    @return {string}
    @protected
  */
  combinePath: function(path1) {
    var s = path1;

    for (var i = 1; i < arguments.length; i++) {
      var part = arguments[i];
      if (!part) {
        continue;
      }

      if (s[s.length - 1] !== '/' && part[0] !== '/') {
        s += '/';
      } else if (s[s.length - 1] === '/' && part[0] === '/') {
        part = part.substring(1);
      }

      s += part;
    }

    return s;
  },

  /**
    Takes a URL, an HTTP method and a hash of data, and makes an
    HTTP request.
    When the server responds with a payload, Ember Data will call into `extractSingle`
    or `extractArray` (depending on whether the original query was for one record or
    many records).
    By default, `ajax` method has the following behavior:
    * It sets the response `dataType` to `"json"`
    * If the HTTP method is not `"GET"`, it sets the `Content-Type` to be
      `application/json; charset=utf-8`
    * If the HTTP method is not `"GET"`, it stringifies the data passed in. The
      data is the serialized record in the case of a save.
    * Registers success and failure handlers.
    @method ajax
    @private
    @param {String} url
    @param {String} type The request type GET, POST, PUT, DELETE etc.
    @param {Object} options
    @return {Promise} promise
    @protected
  */
  ajax: function(url, type, options) {
    var adapter = this;

    return new Ember.RSVP.Promise(function(resolve, reject) {
      var hash = adapter.ajaxOptions(url, type, options);

      hash.converters = {
        'text json': function(text) {
          return BigNumberJSON.parse(text);
        }
      };

      hash.success = function(json, textStatus, jqXHR) {
        json = adapter.ajaxSuccess(jqXHR, json);
        if (json instanceof DS.InvalidError) {
          Ember.run(null, reject, json);
        } else {
          Ember.run(null, resolve, json);
        }
      };

      hash.error = function(jqXHR, textStatus, errorThrown) {
        Ember.run(null, reject, adapter.ajaxError(jqXHR, jqXHR.responseText, errorThrown));
      };

      Ember.$.ajax(hash);
    }, 'Solr: SolrAdapter#ajax ' + type + ' to ' + url);
  },

  /**
    @method ajaxOptions
    @private
    @param {String} url
    @param {String} type The request type GET, POST, PUT, DELETE etc.
    @param {Object} options
    @return {Object}
    @protected
  */
  ajaxOptions: function(url, type, options) {
    var hash = options || {};
    hash.url = url;
    hash.type = type;
    hash.dataType = get(this, 'dataType') || 'json';
    hash.context = this;
    hash.traditional = true;

    if (hash.dataType.indexOf('jsonp') === 0) {
      hash.jsonp = 'json.wrf';
      if (type !== 'GET') {
        set(hash, 'data', {'stream.body': bigNumberStringify(hash.data)});
        hash.type = 'GET'; // JSON-P can only use GET
      }
    }

    if (hash.data && hash.type !== 'GET') {
      hash.contentType = 'application/json; charset=utf-8';
      set(hash, 'data', bigNumberStringify(hash.data));
    }

    var headers = get(this, 'headers');
    if (headers !== undefined) {
      hash.beforeSend = function (xhr) {
        forEach.call(Ember.keys(headers), function(key) {
          xhr.setRequestHeader(key, headers[key]);
        });
      };
    }

    return hash;
  },

  /**
    Takes an ajax response, and returns the json payload.
    By default this hook just returns the jsonPayload passed to it.
    You might want to override it in two cases:
    1. Your API might return useful results in the request headers.
    If you need to access these, you can override this hook to copy them
    from jqXHR to the payload object so they can be processed in you serializer.
    2. Your API might return errors as successful responses with status code
    200 and an Errors text or object. You can return a DS.InvalidError from
    this hook and it will automatically reject the promise and put your record
    into the invalid state.
    @method ajaxSuccess
    @param  {Object} jqXHR
    @param  {Object} jsonPayload
    @return {Object} jsonPayload
    @protected
  */
  ajaxSuccess: function(jqXHR, jsonPayload) {
    return jsonPayload;
  },

  /**
    Takes an ajax response, and returns an error payload.
    Returning a `DS.InvalidError` from this method will cause the
    record to transition into the `invalid` state and make the
    `errors` object available on the record.
    This function should return the entire payload as received from the
    server.  Error object extraction and normalization of model errors
    should be performed by `extractErrors` on the serializer.
    Example
    ```javascript
    App.ApplicationAdapter = DS.RESTAdapter.extend({
      ajaxError: function(jqXHR) {
        var error = this._super(jqXHR);
        if (jqXHR && jqXHR.status === 422) {
          var jsonErrors = Ember.$.parseJSON(jqXHR.responseText);
          return new DS.InvalidError(jsonErrors);
        } else {
          return error;
        }
      }
    });
    ```
    Note: As a correctness optimization, the default implementation of
    the `ajaxError` method strips out the `then` method from jquery's
    ajax response (jqXHR). This is important because the jqXHR's
    `then` method fulfills the promise with itself resulting in a
    circular "thenable" chain which may cause problems for some
    promise libraries.
    @method ajaxError
    @param  {Object} jqXHR
    @param  {Object} responseText
    @return {Object} jqXHR
    @protected
  */
  ajaxError: function(jqXHR, responseText, errorThrown) {
    var isObject = jqXHR !== null && typeof jqXHR === 'object';

    if (!isObject) {
      return jqXHR;
    }

    if (jqXHR.status === 409) {
      var message = 'version conflict';
      if (jqXHR.responseJSON && jqXHR.responseJSON.error && jqXHR.responseJSON.error.msg) {
        message = jqXHR.responseJSON.error.msg;
      }
      return new ConcurrentModificationError(message);
    }

    jqXHR.then = null;
    if (!jqXHR.errorThrown) {
      if (typeof errorThrown === 'string') {
        jqXHR.errorThrown = new Error(errorThrown);
      } else {
        jqXHR.errorThrown = errorThrown;
      }
    }

    return jqXHR;
  }
});