From 369bd5f257cf19928b185a2ad616e883fcb782f7 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 16 Feb 2013 15:47:10 +0000 Subject: [PATCH] [#314,couchdb][s]: remove couchdb backend as now in its own repo at https://github.com/Recline/backend.couchdb. --- _includes/backend-list.html | 2 +- demos/couchdb/app.js | 73 ----- demos/couchdb/index.html | 40 --- demos/index.html | 4 +- docs/src/backend.couchdb.html | 432 -------------------------- src/backend.couchdb.js | 563 ---------------------------------- 6 files changed, 3 insertions(+), 1111 deletions(-) delete mode 100755 demos/couchdb/app.js delete mode 100755 demos/couchdb/index.html delete mode 100644 docs/src/backend.couchdb.html delete mode 100755 src/backend.couchdb.js diff --git a/_includes/backend-list.html b/_includes/backend-list.html index 54fe15c2..04867a21 100644 --- a/_includes/backend-list.html +++ b/_includes/backend-list.html @@ -5,6 +5,6 @@
  • elasticsearch: ElasticSearch
  • dataproxy: DataProxy (CSV and XLS on the Web)
  • ckan: CKAN – support for CKAN datastore
  • -
  • couchdb: CouchDB
  • +
  • couchdb: CouchDB
  • memory: Memory (local data)
  • diff --git a/demos/couchdb/app.js b/demos/couchdb/app.js deleted file mode 100755 index c02fe9e7..00000000 --- a/demos/couchdb/app.js +++ /dev/null @@ -1,73 +0,0 @@ -jQuery(function($) { - window.dataExplorer = null; - window.explorerDiv = $('.data-explorer-here'); - - var queryParameters = recline.View.parseQueryString(decodeURIComponent(window.location.search)); - - var dataset = new recline.Model.Dataset({ - db_url: queryParameters['url'] || '/couchdb/yourcouchdb', - view_url: queryParameters['view_url'] || '/couchdb/yourcouchdb/_design/yourdesigndoc/_view/yourview', - backend: 'couchdb', - query_options: { - 'key': '_id' - } - }); - - dataset.fetch().done(function(dataset) { - console.log('records: ' + dataset.records); - }); - - createExplorer(dataset); -}); - -// make Explorer creation / initialization in a function so we can call it -// again and again -var createExplorer = function(dataset, state) { - // remove existing data explorer view - var reload = false; - if (window.dataExplorer) { - window.dataExplorer.remove(); - reload = true; - } - window.dataExplorer = null; - var $el = $('
    '); - $el.appendTo(window.explorerDiv); - - var views = [ - { - id: 'grid', - label: 'Grid', - view: new recline.View.SlickGrid({ - model: dataset - }), - }, - { - id: 'graph', - label: 'Graph', - view: new recline.View.Graph({ - model: dataset - }), - }, - { - id: 'map', - label: 'Map', - view: new recline.View.Map({ - model: dataset - }), - }, - { - id: 'transform', - label: 'Transform', - view: new recline.View.Transform({ - model: dataset - }) - } - ]; - - window.dataExplorer = new recline.View.MultiView({ - model: dataset, - el: $el, - state: state, - views: views - }); -} diff --git a/demos/couchdb/index.html b/demos/couchdb/index.html deleted file mode 100755 index 953ef283..00000000 --- a/demos/couchdb/index.html +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: container -title: CouchDB Multiview - Demos -recline-deps: true -root: ../../ ---- - - - -
    -

    Instructions

    -

    To use this demo you will need a CouchDB instance running and accessible over HTTP. You should then pass the following 2 query parameters to this page:

    - -
    url: url-to-your-couchdb-instance
    -view_url: url-to-your-couchdb-view
    -
    -Example:
    -http://path-to-this-page/?url=/mycouchdb/&view_url=/mycouchdb/_design/yourdesigndoc/_view/yourview
    -
    - -

    Note that if the CouchDB database is not running on the same domain as this page then the host it is on must support CORS – the simplest approach here is probably to set up a reverse proxy or proxy so your CouchDB database appears on the local domain at e.g. /mycouchdb/.

    -
    - -

    Demo

    - -
    - -
    - - - - diff --git a/demos/index.html b/demos/index.html index 4e2f9160..740bde58 100644 --- a/demos/index.html +++ b/demos/index.html @@ -95,13 +95,13 @@ root: ../
    -

    CouchDB Demo

    +

    CouchDB Demo

    Using CouchDB with Recline Multiview to provide an elegant powerful browser for a CouchDB database and view.

    - +
    diff --git a/docs/src/backend.couchdb.html b/docs/src/backend.couchdb.html deleted file mode 100644 index 17de2e69..00000000 --- a/docs/src/backend.couchdb.html +++ /dev/null @@ -1,432 +0,0 @@ - backend.couchdb.js

    backend.couchdb.js

    this.recline = this.recline || {};
    -this.recline.Backend = this.recline.Backend || {};
    -this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {};
    -
    -(function($, my) {  
    -my.__type__ = 'couchdb';

    CouchDB Wrapper

    - -

    Connecting to [CouchDB] (http://www.couchdb.apache.org/) endpoints. -@param {String} endpoint: url for CouchDB database, e.g. for Couchdb running -on localhost:5984 with database // ckan-std it would be:

    - -

    TODO Add user/password arguments for couchdb authentication support.

    - -

    See the example how to use this in: "demos/couchdb/"

      my.CouchDBWrapper = function(db_url, view_url, options) { 
    -    var self = this;
    -    self.endpoint = db_url;
    -    self.view_url = (view_url) ? view_url : db_url+'/'+'_all_docs';
    -    self.options = _.extend({
    -        dataType: 'json'
    -      },
    -      options);
    -
    -    this._makeRequest = function(data, headers) {
    -      var extras = {};
    -      if (headers) {
    -        extras = {
    -          beforeSend: function(req) {
    -            _.each(headers, function(value, key) {
    -              req.setRequestHeader(key, value);
    -            });
    -          }
    -        };
    -      }
    -      data = _.extend(extras, data);
    -      return $.ajax(data);
    -    };

    mapping

    - -

    Get mapping for this database. -Assume all docs in the view have the same schema so -limit query to single result.

    - -

    @return promise compatible deferred object.

        this.mapping = function() {
    -      var schemaUrl = self.view_url + '?limit=1&include_docs=true';
    -      var jqxhr = self._makeRequest({
    -        url: schemaUrl,
    -        dataType: self.options.dataType
    -      });
    -      return jqxhr;
    -    };

    get

    - -

    Get record corresponding to specified id

    - -

    @return promise compatible deferred object.

        this.get = function(_id) {
    -      var base = self.endpoint + '/' + _id;
    -      return self._makeRequest({
    -        url: base,
    -        dataType: 'json'
    -      });
    -    };

    upsert

    - -

    create / update a record to CouchDB backend

    - -

    @param {Object} doc an object to insert to the index. -@return deferred supporting promise API

        this.upsert = function(doc) {
    -      var data = JSON.stringify(doc);
    -      url = self.endpoint;
    -      if (doc._id) {
    -        url += '/' + doc._id;
    -      }

    use a PUT, not a POST to update the document: -http://wiki.apache.org/couchdb/HTTPDocumentAPI#POST

          return self._makeRequest({
    -        url: url,
    -        type: 'PUT',
    -        data: data,
    -        dataType: 'json',
    -        contentType: 'application/json'
    -      });
    -    };

    delete

    - -

    Delete a record from the CouchDB backend.

    - -

    @param {Object} id id of object to delete -@return deferred supporting promise API

        this.remove = function(_id) {
    -      url = self.endpoint;
    -      url += '/' + _id;
    -      return self._makeRequest({
    -        url: url,
    -        type: 'DELETE',
    -        dataType: 'json'
    -      });
    -    };

    _normalizeQuery

    - -

    Convert the query object from Elastic Search format to a -Couchdb View API compatible format. -See: http://wiki.apache.org/couchdb/HTTPviewAPI

        this._normalizeQuery = function(queryObj) {
    -      var out = queryObj && queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
    -      delete out.sort;
    -      delete out.query;
    -      delete out.filters;
    -      delete out.fields;
    -      delete out.facets;
    -      out['skip'] = out.from || 0;
    -      out['limit'] = out.size || 100;
    -      delete out.from;
    -      delete out.size;
    -      out['include_docs'] = true;      
    -      return out;
    -    };

    query

    - -

    @param {Object} recline.Query instance. -@param {Object} additional couchdb view query options. -@return deferred supporting promise API

        this.query = function(query_object, query_options) {
    -      var norm_q = self._normalizeQuery(query_object);
    -      var url = self.view_url;
    -      var q = _.extend(query_options, norm_q);
    -
    -      var jqxhr = self._makeRequest({
    -        url: url,
    -        data: JSON.stringify(q),
    -        dataType: self.options.dataType,
    -      });
    -      return jqxhr;
    -    }
    -  };

    CouchDB Backend

    - -

    Backbone connector for a CouchDB backend.

    - -
    var dataset = new recline.Model.Dataset({
    -  db_url: path-to-couchdb-database e.g. '/couchdb/mydb',        
    -  view_url: path-to-couchdb-database-view e.g. '/couchdb/mydb/_design/design1/_views/view1',
    -  backend: 'couchdb',
    -  query_options: {
    -    'key': '_id'
    -  }
    -});
    -
    -backend.query(query, dataset.toJSON()).done(function () { ... });
    -
    - -

    Alternatively:

    - -
    var dataset = new recline.Model.Dataset({ ... }, 'couchdb');
    -dataset.fetch();
    -var results = dataset.query(query_obj);
    -
    - -

    Additionally, the Dataset instance may define three methods:

    - -

    function recordupdate (record, document) { ... } - function recorddelete (record, document) { ... } - function record_create (record, document) { ... }

    - -

    Where record is the JSON representation of the Record/Document instance -and document is the JSON document stored in couchdb. -When alldocs view is used (default), a record is the same as a document -so these methods need not be defined. -They are most useful when using a custom view that performs a map-reduce -operation on each document to yield a record. Hence, when the record is -created, updated or deleted, an inverse operation must be performed on the original -document.

    - -

    @param {string} url of couchdb database. -@param {string} (optional) url of couchdb view. default:db_url/alldocs -@param {Object} (optional) query options accepted by couchdb views.

      my.couchOptions = {};

    fetch

    - -

    @param {object} dataset json object with the dburl, viewurl, and query_options args. -@return promise object that resolves to the document mapping.

      my.fetch = function (dataset) {
    -    var db_url    = dataset.db_url;
    -    var view_url  = dataset.view_url;
    -    var cdb       = new my.CouchDBWrapper(db_url, view_url);
    -    var dfd       = $.Deferred();

    if 'doc' attribute is present, return schema of that -else return schema of 'value' attribute which contains -the map-reduced document.

        cdb.mapping().done(function(result) {
    -      var row = result.rows[0];
    -      var keys = [];
    -      if (view_url.search("_all_docs") !== -1) {
    -        keys = _.keys(row['doc']);
    -        keys = _.filter(keys, function (k) { return k.charAt(0) !== '_' });
    -      }
    -      else {
    -        keys = _.keys(row['value']);
    -      }
    -
    -      var fieldData = _.map(keys, function(k) {
    -        return { 'id' : k };
    -      });     
    -      dfd.resolve({
    -        fields: fieldData
    -      });
    -    })
    -    .fail(function(arguments) {
    -      dfd.reject(arguments);
    -    });
    -    return dfd.promise();
    -  };

    save

    - -

    Iterate through all the changes and save them to the server. -N.B. This method is asynchronous and attempts to do multiple -operation concurrently. This can be problematic when more than -one operation is requested on the same document (as in the case -of bulk column transforms).

    - -

    @param {object} lists of create, update, delete. -@param {object} dataset json object.

    my.save = function (changes, dataset) {
    -  var dfd       = $.Deferred();
    -  var total     = changes.creates.length + changes.updates.length + changes.deletes.length;  
    -  var results   = {'done': [], 'fail': [] };
    -
    -  var decr_cb = function () { total -= 1; }
    -  var resolve_cb = function () { if (total == 0) dfd.resolve(results); }
    -
    -  for (var i in changes.creates) {
    -    var new_doc = changes.creates[i];
    -    var succ_cb = function (msg) {results.done.push({'op': 'create', 'record': new_doc, 'reason': ''}); }
    -    var fail_cb = function (msg) {results.fail.push({'op': 'create', 'record': new_doc, 'reason': msg}); }
    -
    -    _createDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]);
    -  }
    -
    -  for (var i in changes.updates) {
    -    var new_doc = changes.updates[i];
    -    var succ_cb = function (msg) {results.done.push({'op': 'update', 'record': new_doc, 'reason': ''}); }
    -    var fail_cb = function (msg) {results.fail.push({'op': 'update', 'record': new_doc, 'reason': msg}); }
    -
    -    _updateDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]);
    -  }
    -
    -  for (var i in changes.deletes) {
    -    var old_doc = changes.deletes[i];
    -    var succ_cb = function (msg) {results.done.push({'op': 'delete', 'record': old_doc, 'reason': ''}); }
    -    var fail_cb = function (msg) {results.fail.push({'op': 'delete', 'record': old_doc, 'reason': msg}); }
    -
    -    _deleteDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]);
    -  }
    -
    -  return dfd.promise();
    -};

    query

    - -

    fetch the data from the couchdb view and filter it. -@param {Object} recline.Dataset instance -@param {Object} recline.Query instance.

    my.query = function(queryObj, dataset) {
    -  var dfd           = $.Deferred();
    -  var db_url        = dataset.db_url;
    -  var view_url      = dataset.view_url;
    -  var query_options = dataset.query_options;
    -
    -  var cdb = new my.CouchDBWrapper(db_url, view_url); 
    -  var cdb_q = cdb._normalizeQuery(queryObj, query_options);
    -
    -  cdb.query(queryObj, query_options).done(function(records){
    -
    -    var query_result = { hits: [], total: 0 };
    -    _.each(records.rows, function(record) {
    -      var doc = {};
    -      if (record.hasOwnProperty('doc')) {
    -        doc = record['doc'];

    couchdb uses _id to identify documents, Backbone models use id. -we add this fix so backbone.Model works correctly.

            doc['id'] = doc['_id'];
    -      }
    -      else {
    -        doc = record['value'];

    using dunder to create compound id. need something more robust. -couchdb uses _id to identify documents, Backbone models use id. -we add this fix so backbone.Model works correctly.

            doc['_id'] = doc['id'] = record['id'] + '__' + record['key']; 
    -      }
    -      query_result.total += 1;
    -      query_result.hits.push(doc);
    -    });

    the following block is borrowed verbatim from recline.backend.Memory -search (with filtering, faceting, and sorting) should be factored -out into a separate library.

        query_result.hits = _applyFilters(query_result.hits, queryObj);
    -    query_result.hits = _applyFreeTextQuery(query_result.hits, queryObj);

    not complete sorting!

        _.each(queryObj.sort, function(sortObj) {
    -      var fieldName = _.keys(sortObj)[0];
    -      query_result.hits = _.sortBy(query_result.hits, function(doc) {
    -        var _out = doc[fieldName];
    -        return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
    -      });
    -    });
    -    query_result.total  = query_result.hits.length;
    -    query_result.facets = _computeFacets(query_result.hits, queryObj);
    -    query_result.hits = query_result.hits.slice(cdb_q.skip, cdb_q.skip + cdb_q.limit+1);
    -    dfd.resolve(query_result);
    -
    -  });
    -  
    -  return dfd.promise();
    -};

    in place filtering

    _applyFilters = function(results, queryObj) {
    -  _.each(queryObj.filters, function(filter) {
    -    results = _.filter(results, function(doc) {
    -      var fieldId = _.keys(filter.term)[0];
    -      return (doc[fieldId] == filter.term[fieldId]);
    -    });
    -  });
    -  return results;
    -};

    we OR across fields but AND across terms in query string

    _applyFreeTextQuery = function(results, queryObj) {
    -  if (queryObj.q) {
    -    var terms = queryObj.q.split(' ');
    -    results = _.filter(results, function(rawdoc) {
    -      var matches = true;
    -      _.each(terms, function(term) {
    -        var foundmatch = false;      
    -        _.each(_.keys(rawdoc), function(field) {
    -          var value = rawdoc[field];
    -          if (value !== null) { value = value.toString(); }

    TODO regexes?

              foundmatch = foundmatch || (value === term);

    TODO: early out (once we are true should break to spare unnecessary testing) -if (foundmatch) return true;

            });
    -        matches = matches && foundmatch;

    TODO: early out (once false should break to spare unnecessary testing) -if (!matches) return false;

          });
    -      return matches;
    -    });
    -  }
    -  return results;
    -};
    -
    -_computeFacets = function(records, queryObj) {
    -  var facetResults = {};
    -  if (!queryObj.facets) {
    -    return facetResults;
    -  }
    -  _.each(queryObj.facets, function(query, facetId) {

    TODO: remove dependency on recline.Model

        facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
    -    facetResults[facetId].termsall = {};
    -  });

    faceting

      _.each(records, function(doc) {
    -    _.each(queryObj.facets, function(query, facetId) {
    -      var fieldId = query.terms.field;
    -      var val = doc[fieldId];
    -      var tmp = facetResults[facetId];
    -      if (val) {
    -        tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
    -      } else {
    -        tmp.missing = tmp.missing + 1;
    -      }
    -    });
    -  });
    -  _.each(queryObj.facets, function(query, facetId) {
    -    var tmp = facetResults[facetId];
    -    var terms = _.map(tmp.termsall, function(count, term) {
    -      return { term: term, count: count };
    -    });
    -    tmp.terms = _.sortBy(terms, function(item) {

    want descending order

          return -item.count;
    -    });
    -    tmp.terms = tmp.terms.slice(0, 10);
    -  });
    -  return facetResults;
    -};
    -     
    -_createDocument  = function (new_doc, dataset) {
    -  var dfd      = $.Deferred();
    -  var db_url   = dataset.db_url;
    -  var view_url = dataset.view_url;
    -  var _id      = new_doc['id'];
    -  var cdb      = new my.CouchDBWrapper(db_url, view_url);
    -
    -  delete new_doc['id']; 
    -
    -  if (view_url.search('_all_docs') !== -1) {
    -    jqxhr = cdb.get(_id);
    -  }
    -  else {
    -    _id = new_doc['_id'].split('__')[0];
    -    jqxhr = cdb.get(_id);
    -  }
    -
    -  jqxhr.done(function(old_doc){
    -    if (dataset.record_create)
    -      new_doc = dataset.record_create(new_doc, old_doc);
    -    new_doc = _.extend(old_doc, new_doc);
    -    new_doc['_id'] = _id;
    -    dfd.resolve(cdb.upsert(new_doc));            
    -  }).fail(function(args){
    -    dfd.reject(args);
    -  });
    -
    -  return dfd.promise();
    -};
    -
    -_updateDocument = function (new_doc, dataset) {
    -  var dfd      = $.Deferred();
    -  var db_url   = dataset.db_url;
    -  var view_url = dataset.view_url;
    -  var _id      = new_doc['id'];
    -  var cdb      = new my.CouchDBWrapper(db_url, view_url);
    -
    -  delete new_doc['id']; 
    -
    -  if (view_url.search('_all_docs') !== -1) {
    -    jqxhr = cdb.get(_id);
    -  }
    -  else {
    -    _id = new_doc['_id'].split('__')[0];
    -    jqxhr = cdb.get(_id);
    -  }
    -
    -  jqxhr.done(function(old_doc){
    -    if (dataset.record_update)
    -      new_doc = dataset.record_update(new_doc, old_doc);
    -    new_doc = _.extend(old_doc, new_doc);
    -    new_doc['_id'] = _id;
    -    dfd.resolve(cdb.upsert(new_doc));            
    -  }).fail(function(args){
    -    dfd.reject(args);
    -  });
    -
    -  return dfd.promise();
    -};
    -
    -_deleteDocument = function (del_doc, dataset) {
    -  var dfd      = $.Deferred();
    -  var db_url   = dataset.db_url;
    -  var view_url = dataset.view_url;
    -  var _id      = del_doc['id'];
    -  var cdb      = new my.CouchDBWrapper(db_url, view_url);
    -
    -  if (view_url.search('_all_docs') !== -1) 
    -    return cdb.remove(_id);
    -  else {
    -    _id = model.get('_id').split('__')[0];
    -    var jqxhr = cdb.get(_id);
    -
    -    jqxhr.done(function(old_doc){
    -      if (dataset.record_delete)
    -        old_doc = dataset.record_delete(del_doc, old_doc);
    -      if (_.isNull(del_doc))
    -        dfd.resolve(cdb.remove(_id)); // XXX is this the right thing to do?
    -      else {

    couchdb uses _id to identify documents, Backbone models use id. -we should remove it before sending it to the server.

            old_doc['_id'] = _id;
    -        delete old_doc['id'];
    -        dfd.resolve(cdb.upsert(old_doc)); 
    -      }
    -    }).fail(function(args){
    -      dfd.reject(args);
    -    });
    -    return dfd.promise();
    -    }
    -};
    -}(jQuery, this.recline.Backend.CouchDB));
    -
    -
    \ No newline at end of file diff --git a/src/backend.couchdb.js b/src/backend.couchdb.js deleted file mode 100755 index 994cfc9b..00000000 --- a/src/backend.couchdb.js +++ /dev/null @@ -1,563 +0,0 @@ -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.CouchDB = this.recline.Backend.CouchDB || {}; - -(function($, my) { -my.__type__ = 'couchdb'; - -// use either jQuery or Underscore Deferred depending on what is available -var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; - - // ## CouchDB Wrapper - // - // Connecting to [CouchDB] (http://www.couchdb.apache.org/) endpoints. - // @param {String} endpoint: url for CouchDB database, e.g. for Couchdb running - // on localhost:5984 with database // ckan-std it would be: - // - // - // TODO Add user/password arguments for couchdb authentication support. - // - // See the example how to use this in: "demos/couchdb/" - my.CouchDBWrapper = function(db_url, view_url, options) { - var self = this; - self.endpoint = db_url; - self.view_url = (view_url) ? view_url : db_url+'/'+'_all_docs'; - self.options = _.extend({ - dataType: 'json' - }, - options); - - this._makeRequest = function(data, headers) { - var extras = {}; - if (headers) { - extras = { - beforeSend: function(req) { - _.each(headers, function(value, key) { - req.setRequestHeader(key, value); - }); - } - }; - } - data = _.extend(extras, data); - return $.ajax(data); - }; - - // ### mapping - // - // Get mapping for this database. - // Assume all docs in the view have the same schema so - // limit query to single result. - // - // @return promise compatible deferred object. - this.mapping = function() { - var schemaUrl = self.view_url + '?limit=1&include_docs=true'; - var jqxhr = self._makeRequest({ - url: schemaUrl, - dataType: self.options.dataType - }); - return jqxhr; - }; - - // ### get - // - // Get record corresponding to specified id - // - // @return promise compatible deferred object. - this.get = function(_id) { - var base = self.endpoint + '/' + _id; - return self._makeRequest({ - url: base, - dataType: 'json' - }); - }; - - // ### upsert - // - // create / update a record to CouchDB backend - // - // @param {Object} doc an object to insert to the index. - // @return deferred supporting promise API - this.upsert = function(doc) { - var data = JSON.stringify(doc); - url = self.endpoint; - if (doc._id) { - url += '/' + doc._id; - } - // use a PUT, not a POST to update the document: - // http://wiki.apache.org/couchdb/HTTP_Document_API#POST - return self._makeRequest({ - url: url, - type: 'PUT', - data: data, - dataType: 'json', - contentType: 'application/json' - }); - }; - - // ### delete - // - // Delete a record from the CouchDB backend. - // - // @param {Object} id id of object to delete - // @return deferred supporting promise API - this.remove = function(_id) { - url = self.endpoint; - url += '/' + _id; - return self._makeRequest({ - url: url, - type: 'DELETE', - dataType: 'json' - }); - }; - - // ### _normalizeQuery - // - // Convert the query object from Elastic Search format to a - // Couchdb View API compatible format. - // See: http://wiki.apache.org/couchdb/HTTP_view_API - // - this._normalizeQuery = function(queryObj) { - var out = queryObj && queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj); - delete out.sort; - delete out.query; - delete out.filters; - delete out.fields; - delete out.facets; - out['skip'] = out.from || 0; - out['limit'] = out.size || 100; - delete out.from; - delete out.size; - out['include_docs'] = true; - return out; - }; - - // ### query - // - // @param {Object} recline.Query instance. - // @param {Object} additional couchdb view query options. - // @return deferred supporting promise API - this.query = function(query_object, query_options) { - var norm_q = self._normalizeQuery(query_object); - var url = self.view_url; - var q = _.extend(query_options, norm_q); - - var jqxhr = self._makeRequest({ - url: url, - data: JSON.stringify(q), - dataType: self.options.dataType, - }); - return jqxhr; - } - }; - - // ## CouchDB Backend - // - // Backbone connector for a CouchDB backend. - // - // var dataset = new recline.Model.Dataset({ - // db_url: path-to-couchdb-database e.g. '/couchdb/mydb', - // view_url: path-to-couchdb-database-view e.g. '/couchdb/mydb/_design/design1/_views/view1', - // backend: 'couchdb', - // query_options: { - // 'key': '_id' - // } - // }); - // - // backend.query(query, dataset.toJSON()).done(function () { ... }); - // - // Alternatively: - // - // var dataset = new recline.Model.Dataset({ ... }, 'couchdb'); - // dataset.fetch(); - // var results = dataset.query(query_obj); - // - // Additionally, the Dataset instance may define three methods: - // - // function record_update (record, document) { ... } - // function record_delete (record, document) { ... } - // function record_create (record, document) { ... } - // - // Where `record` is the JSON representation of the Record/Document instance - // and `document` is the JSON document stored in couchdb. - // When _all_docs view is used (default), a record is the same as a document - // so these methods need not be defined. - // They are most useful when using a custom view that performs a map-reduce - // operation on each document to yield a record. Hence, when the record is - // created, updated or deleted, an inverse operation must be performed on the original - // document. - // - // @param {string} url of couchdb database. - // @param {string} (optional) url of couchdb view. default:`db_url`/_all_docs - // @param {Object} (optional) query options accepted by couchdb views. - // - - my.couchOptions = {}; - - // ### fetch - // @param {object} dataset json object with the db_url, view_url, and query_options args. - // @return promise object that resolves to the document mapping. - my.fetch = function (dataset) { - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var cdb = new my.CouchDBWrapper(db_url, view_url); - var dfd = new Deferred(); - - // if 'doc' attribute is present, return schema of that - // else return schema of 'value' attribute which contains - // the map-reduced document. - cdb.mapping().done(function(result) { - var row = result.rows[0]; - var keys = []; - if (view_url.search("_all_docs") !== -1) { - keys = _.keys(row['doc']); - keys = _.filter(keys, function (k) { return k.charAt(0) !== '_' }); - } - else { - keys = _.keys(row['value']); - } - - var fieldData = _.map(keys, function(k) { - return { 'id' : k }; - }); - dfd.resolve({ - fields: fieldData - }); - }) - .fail(function(arguments) { - dfd.reject(arguments); - }); - return dfd.promise(); - }; - -// ### save -// -// Iterate through all the changes and save them to the server. -// N.B. This method is asynchronous and attempts to do multiple -// operation concurrently. This can be problematic when more than -// one operation is requested on the same document (as in the case -// of bulk column transforms). -// -// @param {object} lists of create, update, delete. -// @param {object} dataset json object. -// -// -my.save = function (changes, dataset) { - var dfd = new Deferred(); - var total = changes.creates.length + changes.updates.length + changes.deletes.length; - var results = {'done': [], 'fail': [] }; - - var decr_cb = function () { total -= 1; } - var resolve_cb = function () { if (total == 0) dfd.resolve(results); } - - for (var i in changes.creates) { - var new_doc = changes.creates[i]; - var succ_cb = function (msg) {results.done.push({'op': 'create', 'record': new_doc, 'reason': ''}); } - var fail_cb = function (msg) {results.fail.push({'op': 'create', 'record': new_doc, 'reason': msg}); } - - _createDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]); - } - - for (var i in changes.updates) { - var new_doc = changes.updates[i]; - var succ_cb = function (msg) {results.done.push({'op': 'update', 'record': new_doc, 'reason': ''}); } - var fail_cb = function (msg) {results.fail.push({'op': 'update', 'record': new_doc, 'reason': msg}); } - - _updateDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]); - } - - for (var i in changes.deletes) { - var old_doc = changes.deletes[i]; - var succ_cb = function (msg) {results.done.push({'op': 'delete', 'record': old_doc, 'reason': ''}); } - var fail_cb = function (msg) {results.fail.push({'op': 'delete', 'record': old_doc, 'reason': msg}); } - - _deleteDocument(new_doc, dataset).then([decr_cb, succ_cb, resolve_cb], [decr_cb, fail_cb, resolve_cb]); - } - - return dfd.promise(); -}; - - -// ### query -// -// fetch the data from the couchdb view and filter it. -// @param {Object} recline.Dataset instance -// @param {Object} recline.Query instance. -my.query = function(queryObj, dataset) { - var dfd = new Deferred(); - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var query_options = dataset.query_options; - - var cdb = new my.CouchDBWrapper(db_url, view_url); - var cdb_q = cdb._normalizeQuery(queryObj, query_options); - - cdb.query(queryObj, query_options).done(function(records){ - - var query_result = { hits: [], total: 0 }; - _.each(records.rows, function(record) { - var doc = {}; - if (record.hasOwnProperty('doc')) { - doc = record['doc']; - // couchdb uses _id to identify documents, Backbone models use id. - // we add this fix so backbone.Model works correctly. - doc['id'] = doc['_id']; - } - else { - doc = record['value']; - // using dunder to create compound id. need something more robust. - // couchdb uses _id to identify documents, Backbone models use id. - // we add this fix so backbone.Model works correctly. - doc['_id'] = doc['id'] = record['id'] + '__' + record['key']; - } - query_result.total += 1; - query_result.hits.push(doc); - }); - - // the following block is borrowed verbatim from recline.backend.Memory - // search (with filtering, faceting, and sorting) should be factored - // out into a separate library. - query_result.hits = _applyFilters(query_result.hits, queryObj); - query_result.hits = _applyFreeTextQuery(query_result.hits, queryObj); - // not complete sorting! - _.each(queryObj.sort, function(sortObj) { - var fieldName = sortObj.field; - query_result.hits = _.sortBy(query_result.hits, function(doc) { - var _out = doc[fieldName]; - return _out; - }); - if (sortObj.order == 'desc') { - query_result.hits.reverse(); - } - }); - query_result.total = query_result.hits.length; - query_result.facets = _computeFacets(query_result.hits, queryObj); - query_result.hits = query_result.hits.slice(cdb_q.skip, cdb_q.skip + cdb_q.limit+1); - dfd.resolve(query_result); - - }); - - return dfd.promise(); -}; - -// in place filtering -_applyFilters = function(results, queryObj) { - var filters = queryObj.filters; - // register filters - var filterFunctions = { - term : term, - range : range, - geo_distance : geo_distance - }; - var dataParsers = { - integer: function (e) { return parseFloat(e, 10); }, - 'float': function (e) { return parseFloat(e, 10); }, - string : function (e) { return e.toString() }, - date : function (e) { return new Date(e).valueOf() }, - datetime : function (e) { return new Date(e).valueOf() } - }; - - function getDataParser(filter) { - //sample = results[0][filter.field]); - var fieldType = 'string'; - return dataParsers[fieldType]; - } - - // filter records - return _.filter(results, function (record) { - var passes = _.map(filters, function (filter) { - return filterFunctions[filter.type](record, filter); - }); - - // return only these records that pass all filters - return _.all(passes, _.identity); - }); - - // filters definitions - function term(record, filter) { - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var term = parse(filter.term); - - return (value === term); - } - - function range(record, filter) { - var startnull = (filter.start == null || filter.start === ''); - var stopnull = (filter.stop == null || filter.stop === ''); - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var start = parse(filter.start); - var stop = parse(filter.stop); - - // if at least one end of range is set do not allow '' to get through - // note that for strings '' <= {any-character} e.g. '' <= 'a' - if ((!startnull || !stopnull) && value === '') { - return false; - } - return ((startnull || value >= start) && (stopnull || value <= stop)); - } - - function geo_distance() { - // TODO code here - } -}; - -// we OR across fields but AND across terms in query string -_applyFreeTextQuery = function(results, queryObj) { - if (queryObj.q) { - var terms = queryObj.q.split(' '); - results = _.filter(results, function(rawdoc) { - var matches = true; - _.each(terms, function(term) { - var foundmatch = false; - _.each(_.keys(rawdoc), function(field) { - var value = rawdoc[field]; - if (value !== null) { value = value.toString(); } - // TODO regexes? - foundmatch = foundmatch || (value === term); - // TODO: early out (once we are true should break to spare unnecessary testing) - // if (foundmatch) return true; - }); - matches = matches && foundmatch; - // TODO: early out (once false should break to spare unnecessary testing) - // if (!matches) return false; - }); - return matches; - }); - } - return results; -}; - -_computeFacets = function(records, queryObj) { - var facetResults = {}; - if (!queryObj.facets) { - return facetResults; - } - _.each(queryObj.facets, function(query, facetId) { - // TODO: remove dependency on recline.Model - facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON(); - facetResults[facetId].termsall = {}; - }); - // faceting - _.each(records, function(doc) { - _.each(queryObj.facets, function(query, facetId) { - var fieldId = query.terms.field; - var val = doc[fieldId]; - var tmp = facetResults[facetId]; - if (val) { - tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1; - } else { - tmp.missing = tmp.missing + 1; - } - }); - }); - _.each(queryObj.facets, function(query, facetId) { - var tmp = facetResults[facetId]; - var terms = _.map(tmp.termsall, function(count, term) { - return { term: term, count: count }; - }); - tmp.terms = _.sortBy(terms, function(item) { - // want descending order - return -item.count; - }); - tmp.terms = tmp.terms.slice(0, 10); - }); - return facetResults; -}; - -//Define random Id for new records without _id -function randomId(length, chars) { - var mask = ''; - if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; - if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - if (chars.indexOf('#') > -1) mask += '0123456789'; - if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; - var result = ''; - for (var i = length; i > 0; --i) result += mask[Math.round(Math.random() * (mask.length - 1))]; - return result; -} - -_createDocument = function (new_doc, dataset) { - var dfd = new Deferred(); - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var _id = new_doc['id']; - var cdb = new my.CouchDBWrapper(db_url, view_url); - - delete new_doc['id']; - - if (dataset.record_create) - new_doc = dataset.record_create(new_doc); - if (_id !== 1 && _id !== undefined) { - new_doc['_id'] = _id; - } - else { - new_doc['_id'] = randomId(32, '#a'); - } - dfd.resolve(cdb.upsert(new_doc)); - - return dfd.promise(); -}; - -_updateDocument = function (new_doc, dataset) { - var dfd = new Deferred(); - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var _id = new_doc['id']; - var cdb = new my.CouchDBWrapper(db_url, view_url); - - delete new_doc['id']; - - if (view_url.search('_all_docs') !== -1) { - jqxhr = cdb.get(_id); - } - else { - _id = new_doc['_id'].split('__')[0]; - jqxhr = cdb.get(_id); - } - - jqxhr.done(function(old_doc){ - if (dataset.record_update) - new_doc = dataset.record_update(new_doc, old_doc); - new_doc = _.extend(old_doc, new_doc); - new_doc['_id'] = _id; - dfd.resolve(cdb.upsert(new_doc)); - }).fail(function(args){ - dfd.reject(args); - }); - - return dfd.promise(); -}; - -_deleteDocument = function (del_doc, dataset) { - var dfd = new Deferred(); - var db_url = dataset.db_url; - var view_url = dataset.view_url; - var _id = del_doc['id']; - var cdb = new my.CouchDBWrapper(db_url, view_url); - - if (view_url.search('_all_docs') !== -1) - return cdb.remove(_id); - else { - _id = model.get('_id').split('__')[0]; - var jqxhr = cdb.get(_id); - - jqxhr.done(function(old_doc){ - if (dataset.record_delete) - old_doc = dataset.record_delete(del_doc, old_doc); - if (_.isNull(del_doc)) - dfd.resolve(cdb.remove(_id)); // XXX is this the right thing to do? - else { - // couchdb uses _id to identify documents, Backbone models use id. - // we should remove it before sending it to the server. - old_doc['_id'] = _id; - delete old_doc['id']; - dfd.resolve(cdb.upsert(old_doc)); - } - }).fail(function(args){ - dfd.reject(args); - }); - return dfd.promise(); - } -}; -}(jQuery, this.recline.Backend.CouchDB));