Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added first version of the elasticsearch adapter for Ember Data

  • Loading branch information...
commit 34809d5b53e7be0abe13d03d1c7a5c102419fd38 1 parent 3fe4de2
@karmi karmi authored
View
279 ember-data/lib/adapters/elasticsearch_adapter.js
@@ -0,0 +1,279 @@
+/**
+ This class provides an adapter for the Ember Data [https://github.com/emberjs/data]
+ persistence library for Ember.js, which stores data as JSON documents
+ in elasticsearch [http://elasticsearch.org].
+
+ ### Usage
+
+ Pass the adapter to a store, optionally providing elasticsearch URL:
+
+ var store = DS.Store.create({
+ revision: 4,
+ adapter: DS.ElasticSearchAdapter.create({url: "http://localhost:9200"})
+ });
+
+ Let's assume a standard Ember Data model, such as:
+
+ var Person = DS.Model.extend({
+ name: DS.attr('string')
+ });
+
+ You have to define an index and type for your model as the `url` attribute:
+
+ Person.reopenClass({
+ url: 'people/person'
+ });
+
+ Now you can use the standard Ember Data adapter interface
+ for creating, updating, destroying and finding records:
+
+ Person.createRecord({ id: 1, name: "John" });
+ store.commit();
+
+ var person = Person.find( 1);
+ person.get("name");
+
+ person.set("name", "Jonathan");
+ store.commit();
+
+ person.deleteRecord();
+ store.commit();
+
+ Don't forget, that in Ember Data, you have to call the `commit` method to persist the changes!
+
+ You can use the full elasticsearch's _Query DSL_ to find records:
+
+ var person = Person.find({query: { query_string: { query: 'john' } }});
+ person.get("name");
+
+ @extends DS.Adapter
+*/
+
+DS.ElasticSearchAdapter = DS.Adapter.extend({
+
+ /**
+ @field Default URL for elasticsearch
+ */
+ url: "http://localhost:9200",
+
+ /**
+ HTTP client
+
+ TODO: Refactor, simplify.
+ */
+ http: {
+ get: function(url, callback) {
+ return jQuery.ajax({
+ url: url,
+ type: 'GET',
+ dataType: 'json',
+ cache: true,
+
+ success: callback
+ });
+ },
+ post: function(url, payload, callback) {
+ return jQuery.ajax({
+ url: url,
+ type: 'POST',
+ data: JSON.stringify(payload),
+ dataType: 'json',
+ cache: true,
+
+ success: callback
+ });
+ },
+ put: function(url, payload, callback) {
+ return jQuery.ajax({
+ url: url,
+ type: 'PUT',
+ data: JSON.stringify(payload),
+ dataType: 'json',
+ cache: true,
+
+ success: callback
+ });
+ },
+ delete: function(url, payload, callback) {
+ return jQuery.ajax({
+ url: url,
+ type: 'DELETE',
+ data: JSON.stringify(payload),
+ dataType: 'json',
+ cache: true,
+
+ success: callback
+ });
+ }
+ },
+
+ /**
+ Loads a single record by ID:
+
+ store.find(Person, 1);
+ */
+ find: function(store, type, id) {
+ if (Ember.ENV.DEBUG) console.debug('find', store, type, id);
+ var url = [this.url, type.url, id].join('/');
+
+ this.http.get(url, function(data, textStatus, xhr) {
+ if (Ember.ENV.DEBUG) console.debug('elasticsearch (' + xhr.status + ') :', Ember.ENV.CI ? JSON.stringify(data) : data);
+ store.load(type, id, data['_source']);
+ });
+ },
+
+ /**
+ Loads all records (up to one million :):
+
+ store.findAll(Person);
+ */
+ findAll: function(store, type) {
+ var url = [this.url, type.url, '_search'].join('/');
+ var payload = {size: 1000000};
+
+ this.http.post(url, payload, function(data, textStatus, xhr) {
+ if (Ember.ENV.DEBUG) console.debug('elasticsearch (' + xhr.status + '):', Ember.ENV.CI ? JSON.stringify(data) : data);
+ store.loadMany(type, data['hits']['hits'].map( function(i) {
+ return Ember.Object.create(i['_source']).reopen({id: i._id, version: i._version})
+ } ));
+ });
+ },
+
+ /**
+ Loads a collection of records by IDs:
+
+ store.find(Person, [2, 3, 1]);
+ */
+ findMany: function(store, type, ids) {
+ if (Ember.ENV.DEBUG) console.debug('findMany', ids);
+
+ var url = [this.url, type.url, '_mget'].join('/');
+ var payload = {ids: ids};
+
+ this.http.post(url, payload, function(data, textStatus, xhr) {
+ if (Ember.ENV.DEBUG) console.debug('elasticsearch (' + xhr.status + '):', Ember.ENV.CI ? JSON.stringify(data) : data);
+ store.loadMany(type, data['docs'].map( function(i) { return i['_source'] } ));
+ });
+ },
+
+ /**
+ Loads a collection of records by a fulltext query:
+
+ store.findQuery(Person, {query: { query_string: { query: 'john' } }});
+
+ See the elasticsearch [documentation](http://elasticsearch.org/guide/reference/query-dsl) for more info.
+ */
+ findQuery: function(store, type, query, recordArray) {
+ if (Ember.ENV.DEBUG) console.debug('findQuery', query);
+
+ var url = [this.url, type.url, '_search'].join('/');
+
+ var payload = query;
+ // var payload = { query: { query_string: { query: 'John' } } };
+
+ this.http.post(url, payload, function(data, textStatus, xhr) {
+ if (Ember.ENV.DEBUG) console.debug('elasticsearch (' + xhr.status + '):', Ember.ENV.CI ? JSON.stringify(data) : data);
+ recordArray.load(data['hits']['hits'].map( function(i) { return i['_source'] } ));
+ });
+ },
+
+ /**
+ Creates a new record, persisted in elasticsearch:
+
+ store.createRecord(Person, { id: 1, name: "Alice" });
+ store.commit();
+
+ If you don't provide an ID for the object, elasticsearch will generate one:
+
+ var person = store.createRecord(Person, { name: "Anne" });
+ store.commit();
+
+ result.get('id')
+ // => 'abcDEF-abc123XYZ'
+ */
+ createRecord: function(store, type, record) {
+ if (Ember.ENV.DEBUG) console.debug('createRecord', type, record.toJSON(), 'id: ', record.get("id"));
+ var id = record.get("id") || null;
+ var url = [this.url, type.url, id].join('/');
+
+ var payload = record.toJSON();
+ var self = this;
+
+ this.http.post(url, payload, function(data, textStatus, xhr) {
+ if (Ember.ENV.DEBUG) console.debug('elasticsearch (' + xhr.status + '):', Ember.ENV.CI ? JSON.stringify(data) : data)
+ self.didCreateRecord(store, type, record, data);
+ });
+ },
+
+ didCreateRecord: function(store, type, record, json) {
+ var recordData = record.get('data');
+
+ if (record.get('id')) {
+ recordData.commit();
+ } else {
+ record.beginPropertyChanges();
+ record.set('id', json._id)
+ record.endPropertyChanges();
+ }
+
+ record.send('didCommit');
+ },
+
+ /**
+ Persists object changes to elasticsearch:
+
+ var person = store.find(Person, 1);
+ person.set("name", "Caroline");
+ store.commit();
+ */
+ updateRecord: function(store, type, record) {
+ if (Ember.ENV.DEBUG) console.debug('updateRecord', type, record.toJSON(), 'id:', record.get("id"));
+
+ var id = record.get("id");
+ var url = [this.url, type.url, id].join('/');
+
+ var payload = record.toJSON();
+ var self = this;
+
+ this.http.put(url, payload, function(data, textStatus, xhr) {
+ if (Ember.ENV.DEBUG) console.debug('elasticsearch (' + xhr.status + '):', Ember.ENV.CI ? JSON.stringify(data) : data)
+ self.didUpdateRecord(store, type, record, data);
+ });
+ },
+
+ didUpdateRecord: function(store, type, record, json) {
+ var recordData = record.get('data');
+
+ recordData.commit();
+
+ record.send('didChangeData');
+ record.send('didSaveData');
+ record.send('didCommit');
+ },
+
+ /**
+ Deletes the record from elasticsearch:
+
+ var person = store.find(Person, 1);
+ person.deleteRecord();
+ store.commit();
+ */
+ deleteRecord: function(store, type, record) {
+ if (Ember.ENV.DEBUG) console.debug('deleteRecord', type, record.toJSON(), 'id: ', record.get("id"));
+
+ var id = record.get("id");
+ var url = [this.url, type.url, id].join('/');
+
+ var self = this;
+
+ this.http.delete(url, {}, function(data, textStatus, xhr) {
+ if (Ember.ENV.DEBUG) console.debug('elasticsearch (' + xhr.status + '):', Ember.ENV.CI ? JSON.stringify(data) : data)
+ self.didDeleteRecord(store, type, record, data);
+ });
+ },
+
+ didDeleteRecord: function(store, type, record, json) {
+ store.didDeleteRecord(record);
+ }
+
+});
View
292 ember-data/test/elasticsearch_adapter_tests.js
@@ -0,0 +1,292 @@
+Ember.ENV['DEBUG'] = true;
+
+var get = Ember.get, set = Ember.set;
+
+var URL = "http://localhost:9200";
+
+var people_data = [
+ { id: 1, name: "John" },
+ { id: 2, name: "Mary" }
+];
+
+var store = DS.Store.create({
+ revision: 4,
+ adapter: DS.ElasticSearchAdapter.create({url: URL})
+});
+
+var Person = DS.Model.extend({
+ name: DS.attr('string'),
+
+ didLoad: function() {
+ // console.log(this.toString() + " finished loading.");
+ }
+});
+
+Person.reopenClass({
+ url: 'people-test/person'
+});
+
+QUnit.begin(function() {
+ // NOTE: Index cleanup done in Rake task
+ // try {
+ // jQuery.ajax({ url: [URL, 'people-test'].join('/'),
+ // type: 'DELETE',
+ // dataType: 'json',
+ // success: function() { console.log("Test index deleted.") }
+ // });
+ // } catch (e) {};
+
+ // Populate test index with data
+ //
+ var payload = people_data
+ .reduce(function(previous, current, index) {
+ // console.debug('previous:', previous, 'current:', current, index);
+ previous.push({index: {_index: "people-test", _type: "person", _id: current.id}});
+ previous.push(current);
+ return previous
+ }, [])
+ .map( function(i) { return JSON.stringify(i) } ).join("\n") + "\n";
+
+ jQuery.post([URL, "people-test/_bulk?refresh=true"].join('/'), payload, function(data) {
+ console.debug("Test data loaded.")
+ });
+});
+
+asyncTest("elasticsearch connection", function() {
+ jQuery.get(URL, function(data, textStatus, xhr) {
+ equal(textStatus, 'success', "is working");
+ start();
+ })
+});
+
+asyncTest("elasticsearch index", function() {
+ jQuery.get([URL, 'people-test', '_status'].join('/'), function(data, textStatus, xhr) {
+ equal(textStatus, 'success', "is working");
+ start();
+ })
+});
+
+module("Adapter", {});
+
+test( "has default URL", function() {
+ var store = DS.Store.create({
+ revision: 4,
+ adapter: DS.ElasticSearchAdapter.create()
+ });
+
+ equal(store.adapter.url, "http://localhost:9200")
+});
+
+test( "has custom URL", function() {
+ var store = DS.Store.create({
+ revision: 4,
+ adapter: DS.ElasticSearchAdapter.create({url: "http://search.example.com"})
+ });
+
+ equal(store.adapter.url, "http://search.example.com")
+});
+
+test( "mixes the find() method into models", function() {
+ ok ( Ember.canInvoke(Person, "find"), "mixes Ember Data methods into models" );
+
+ var result = Person.find('foobar');
+ ok( result.get("isInstance"), "is instance of Person");
+});
+
+module("Find", {
+ setup: function() {
+ // var payload = people_data
+ // .reduce(function(previous, current, index) {
+ // // console.debug('previous:', previous, 'current:', current, index);
+ // previous.push({index: {_index: "people-test", _type: "person", _id: current.id}});
+ // previous.push(current);
+ // return previous
+ // }, [])
+ // .map( function(i) { return JSON.stringify(i) } ).join("\n") + "\n"
+ // // payload.map( function(i) { return JSON.stringify(i) } ).join("\n") + "\n"
+ // // people_data.reduce(function(previous, current, index) {console.log('previous:', previous, 'current:', current, index); previous.push({index: {_index: "people-test", _type: "person", _id: current.id}}); previous.push(current); return previous}, [])
+ // // console.debug(payload)
+ // jQuery.post([URL, "people-test/_bulk?refresh=true"].join('/'), payload, function(data) {
+ // console.debug("Test data loaded.")
+ // });
+ },
+ teardown: function() {
+ // try {
+ // return jQuery.ajax({ url: [URL, 'people-test'].join("\n"),
+ // type: 'DELETE',
+ // dataType: 'json',
+ // success: function() { console.log("Test index deleted.") }
+ // });
+ // } catch (e) {}
+ }
+});
+
+asyncTest( "find() loads a single person", function() {
+ var result = store.find(Person, 1);
+
+ setTimeout(function() {
+ // console.debug('result:', result.get("name"))
+
+ equal( result.get('name'), "John", "has proper name value" );
+ start();
+ }, 100);
+});
+
+asyncTest( "find() does not load a missing person", function() {
+ var result = store.find(Person, 'foobar');
+
+ setTimeout(function() {
+ ok( ! result.get("isLoaded"), "record is not loaded (actual: "+result.get('stateManager.currentState.path')+")" );
+ start();
+ }, 100);
+});
+
+asyncTest( "findAll() loads all people", function() {
+ var results = store.findAll(Person);
+
+ setTimeout(function() {
+ // equal( results.get('length'), 2, "has two instances of Person" );
+ ok( results.get('length') >= 2, "has at least two instances of Person (actual: "+results.get('length')+")" );
+
+ // Use for "testing the tests":
+ // equal( get(results.objectAt(0), 'name'), "Mary", "has proper name value" );
+
+ start();
+ }, 100);
+});
+
+asyncTest( "findMany() loads people by IDs", function() {
+ var results = store.findMany(Person, [2, 1]);
+
+ setTimeout(function() {
+ equal( results.get('length'), 2, "has two instances of Person" );
+
+ equal( results.objectAt(0).get('name'), "Mary", "has proper name value" );
+ equal( results.objectAt(1).get('name'), "John", "has proper name value" );
+
+ start();
+ }, 100);
+});
+
+asyncTest( "findQuery() loads people by fulltext query", function() {
+ var query = {query: { query_string: { query: 'mary' } }};
+ var results = store.findQuery(Person, query);
+
+ setTimeout(function() {
+ equal( results.get('length'), 1, "has one instance of Person" );
+
+ equal( results.objectAt(0).get('name'), "Mary", "has proper name value" );
+
+ start();
+ }, 100);
+});
+
+
+module("Create", {});
+
+asyncTest( "creates a new person with ID", function() {
+ var created = store.createRecord(Person, { id: 3, name: "Alice" });
+ store.commit();
+
+ setTimeout(function() {
+ var result = store.find(Person, 3);
+ // console.debug('result:', result.toJSON())
+
+ ok( created.get("isLoaded"), "is loaded (actual: "+created.get('stateManager.currentState.path')+")" )
+ equal( result.get('name'), "Alice", "has proper name" );
+ equal( result.get('id'), "3", "has proper ID" );
+
+ start();
+
+ try {
+ return jQuery.ajax({ url: [URL, 'people-test', 'person', 3].join('/'), type: 'DELETE', dataType: 'json',
+ success: function() { console.log("Test person deleted.") }
+ });
+ } catch (e) {}
+ }, 100);
+});
+
+asyncTest( "creates a new person with autogenerated ID", function() {
+ var created = store.createRecord(Person, { name: "Bishop" });
+ store.commit();
+
+ setTimeout(function() {
+ var result = store.find(Person, created.get("id"));
+
+ setTimeout(function() {
+ // console.log('created', created.toJSON())
+ // console.debug('result:', result.toJSON())
+
+ ok( created.get("isLoaded"), "is loaded (actual: "+created.get('stateManager.currentState.path')+")" )
+ equal( result.get('name'), "Bishop", "has proper name value" );
+ equal( result.get('id'), created.get("id"), "has proper ID" );
+
+ start();
+
+ try {
+ return jQuery.ajax({ url: [URL, 'people-test', 'person', created.get("id")].join('/'), type: 'DELETE', dataType: 'json',
+ success: function() { console.log("Test person deleted.") }
+ });
+ } catch (e) {}
+ }, 100);
+ }, 100);
+});
+
+module("Update", {
+ setup: function() {
+ store.createRecord(Person, { id: 33, name: "Anne" });
+ store.commit();
+ }
+});
+
+asyncTest( "updates a person", function() {
+ var existing = store.find(Person, 33);
+
+ setTimeout(function() {
+ // console.debug('result:', existing.toJSON());
+
+ ok( existing.get("isLoaded"), "record is loaded (actual: "+existing.get('stateManager.currentState.path')+")" );
+
+ existing.set("name", "Brigitte");
+
+ ok( existing.get("isDirty"), "record is dirty (actual: "+existing.get('stateManager.currentState.path')+")" );
+
+ store.commit();
+
+ ok( existing.get("isSaving"), "record is being saved (actual: "+existing.get('stateManager.currentState.path')+")" );
+
+ var loaded = store.find(Person, 33);
+ equal( loaded.get('name'), "Brigitte", "has proper name value" );
+
+ start();
+ }, 100);
+});
+
+
+module("Delete", {
+ setup: function() {
+ store.createRecord(Person, { id: 99, name: "Bob" });
+ store.commit();
+ }
+});
+
+asyncTest( "deletes a person", function() {
+ var existing = store.find(Person, 99);
+
+ setTimeout(function() {
+ // console.debug('result:', existing.toJSON());
+
+ ok( existing.get("isLoaded"), "record is loaded (actual: "+existing.get('stateManager.currentState.path')+")" );
+
+ existing.deleteRecord();
+ store.commit();
+
+ ok( existing.get("isDeleted"), "record is deleted (actual: "+existing.get('stateManager.currentState.path')+")" );
+
+ var missing = store.find(Person, 99);
+
+ ok( missing.get("isDeleted"), "fresh record is deleted (actual: "+existing.get('stateManager.currentState.path')+")" );
+
+ start();
+ }, 100);
+});
View
4 tests/index.html
@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
- <title>ember-data-adapter-elasticsearch | QUnit Test Suite</title>
+ <title>ember-data-elasticsearch | QUnit Test Suite</title>
<link rel="stylesheet" href="lib/qunit.css" type="text/css" media="screen">
@@ -12,6 +12,8 @@
<script type="text/javascript" src="lib/ember-data.js"></script>
<script type="text/javascript" src="lib/qunit.js"></script>
+ <script type="text/javascript" src="../ember-data/lib/adapters/elasticsearch_adapter.js"></script>
+ <script type="text/javascript" src="../ember-data/test/elasticsearch_adapter_tests.js"></script>
</head>
<body>
<h1 id="qunit-header">ember-data-adapter-elasticsearch | QUnit Test Suite</h1>
Please sign in to comment.
Something went wrong with that request. Please try again.