From c842ce8eacf9d463584b3875cddc698138a1730c Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Mon, 14 Dec 2015 16:54:19 +0600 Subject: [PATCH 01/35] `Attachment` transform Unit test courtesy of Matt Marcum @mattmarcum --- addon/transforms/attachment.js | 37 ++++++++++++++++++++ app/transforms/attachment.js | 1 + tests/unit/transforms/attachment-test.js | 44 ++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 addon/transforms/attachment.js create mode 100644 app/transforms/attachment.js create mode 100644 tests/unit/transforms/attachment-test.js diff --git a/addon/transforms/attachment.js b/addon/transforms/attachment.js new file mode 100644 index 0000000..7cbbea4 --- /dev/null +++ b/addon/transforms/attachment.js @@ -0,0 +1,37 @@ +import Ember from 'ember'; +import DS from 'ember-data'; + +const { isNone } = Ember; +const keys = Object.keys || Ember.keys; + +export default DS.Transform.extend({ + deserialize: function(serialized) { + if (isNone(serialized)) { return null; } + + return keys(serialized).map(function (attachmentName) { + return Ember.Object.create({ + name: attachmentName, + content_type: serialized[attachmentName]['content_type'], + data: serialized[attachmentName]['data'], + stub: serialized[attachmentName]['stub'], + }); + }); + }, + + serialize: function(deserialized) { + if (!Ember.isArray(deserialized)) { return null; } + + return deserialized.reduce(function (acc, attachment) { + const serialized = { + content_type: attachment.get('content_type'), + }; + if (attachment.get('stub')) { + serialized.stub = true; + } else { + serialized.data = attachment.get('data'); + } + acc[attachment.get('name')] = serialized; + return acc; + }, {}); + } +}); diff --git a/app/transforms/attachment.js b/app/transforms/attachment.js new file mode 100644 index 0000000..bf67f61 --- /dev/null +++ b/app/transforms/attachment.js @@ -0,0 +1 @@ +export { default } from 'ember-pouch/transforms/attachment'; diff --git a/tests/unit/transforms/attachment-test.js b/tests/unit/transforms/attachment-test.js new file mode 100644 index 0000000..c134015 --- /dev/null +++ b/tests/unit/transforms/attachment-test.js @@ -0,0 +1,44 @@ +import { moduleFor, test } from 'ember-qunit'; + +import Ember from 'ember'; + +let testSerializedData = { + 'test.txt': { + content_type: 'text/plain', + data: 'hello world!' + } +}; + +let testDeserializedData = [ + Ember.Object.create({ + name: 'test.txt', + content_type: 'text/plain', + data: 'hello world!' + }) +]; + +moduleFor('transform:attachment', 'Unit | Transform | attachment', {}); + +test('it serializes an attachment', function(assert) { + let transform = this.subject(); + assert.equal(transform.serialize(null), null); + assert.equal(transform.serialize(undefined), null); + + let serializedData = transform.serialize(testDeserializedData); + let name = testDeserializedData[0].name; + + assert.equal(serializedData[name].content_type, testSerializedData[name].content_type); + assert.equal(serializedData[name].data, testSerializedData[name].data); +}); + +test('it deserializes an attachment', function(assert) { + let transform = this.subject(); + assert.equal(transform.deserialize(null), null); + assert.equal(transform.deserialize(undefined), null); + + let deserializedData = transform.deserialize(testSerializedData); + + assert.equal(deserializedData[0].get('name'), testDeserializedData[0].get('name')); + assert.equal(deserializedData[0].get('content_type'), testDeserializedData[0].get('content_type')); + assert.equal(deserializedData[0].get('data'), testDeserializedData[0].get('data')); +}); From f11ba279e3e54640a75ec775e702a353023e5178 Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Mon, 14 Dec 2015 18:07:37 +0600 Subject: [PATCH 02/35] Describe usage of `Attachment` transform --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 09e58c6..2496ece 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,39 @@ PouchDB.debug.enable('*'); See the [PouchDB sync API](http://pouchdb.com/api.html#sync) for full usage instructions. +## Attachments + +`Ember-Pouch` provides an `attachment` transform for your models, which makes working with attachments is as simple as working with any other field. + +Add a `DS.attr('attachment')` field to your model: + +```js +// myapp/models/photo-album.js +export default DS.Model.extend({ + photos: DS.attr('attachment'); +}); +``` + +Here, instances of `PhotoAlbum` have a `photos` field, which is an array of plain `Ember.Object`s, which have a `.name` and `.content_type`. Non-stubbed attachment also have a `.data` field; and stubbed attachments have a `.stub` instead. +```handlebars + +``` + +Attach new files by adding an `Ember.Object` with a `.name`, `.content_type` and `.data` to array of attachments. + +```js +// somewhere in your controller/component: +myAlbum.get('photos').addObject(Ember.Object.create({ + 'name': 'kitten.jpg', + 'content_type': 'image/jpg', + 'data': data // ... can be a DOM File, Blob, or plain old String +})); +``` + ## Sample app Tom Dale's blog example using Ember CLI and EmberPouch: [broerse/ember-cli-blog](https://github.com/broerse/ember-cli-blog) From b6f074daf0327c71120a69ab1f56b465635d9d7d Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Mon, 14 Dec 2015 18:17:14 +0600 Subject: [PATCH 03/35] Test stubbed attachments --- tests/unit/transforms/attachment-test.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/unit/transforms/attachment-test.js b/tests/unit/transforms/attachment-test.js index c134015..345ce08 100644 --- a/tests/unit/transforms/attachment-test.js +++ b/tests/unit/transforms/attachment-test.js @@ -6,6 +6,10 @@ let testSerializedData = { 'test.txt': { content_type: 'text/plain', data: 'hello world!' + }, + 'stub.json': { + stub: true, + content_type: 'application/json' } }; @@ -14,6 +18,11 @@ let testDeserializedData = [ name: 'test.txt', content_type: 'text/plain', data: 'hello world!' + }), + Ember.Object.create({ + name: 'stub.json', + content_type: 'application/json', + stub: true }) ]; @@ -25,10 +34,15 @@ test('it serializes an attachment', function(assert) { assert.equal(transform.serialize(undefined), null); let serializedData = transform.serialize(testDeserializedData); - let name = testDeserializedData[0].name; + let name = testDeserializedData[0].get('name'); assert.equal(serializedData[name].content_type, testSerializedData[name].content_type); assert.equal(serializedData[name].data, testSerializedData[name].data); + + let stub = testDeserializedData[1].get('name'); + + assert.equal(serializedData[stub].content_type, testSerializedData[stub].content_type); + assert.equal(serializedData[stub].stub, true); }); test('it deserializes an attachment', function(assert) { @@ -41,4 +55,8 @@ test('it deserializes an attachment', function(assert) { assert.equal(deserializedData[0].get('name'), testDeserializedData[0].get('name')); assert.equal(deserializedData[0].get('content_type'), testDeserializedData[0].get('content_type')); assert.equal(deserializedData[0].get('data'), testDeserializedData[0].get('data')); + + assert.equal(deserializedData[1].get('name'), testDeserializedData[1].get('name')); + assert.equal(deserializedData[1].get('content_type'), testDeserializedData[1].get('content_type')); + assert.equal(deserializedData[1].get('stub'), true); }); From 3e5f36f375f3b355869d5f76a2193495d4547fac Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Wed, 23 Dec 2015 11:28:02 +0600 Subject: [PATCH 04/35] Clarify the use of base-64 encoded String attachments --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2496ece..54622dd 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Attach new files by adding an `Ember.Object` with a `.name`, `.content_type` and myAlbum.get('photos').addObject(Ember.Object.create({ 'name': 'kitten.jpg', 'content_type': 'image/jpg', - 'data': data // ... can be a DOM File, Blob, or plain old String + 'data': btoa('hello world') // base64-encoded `String`, or a DOM `Blob`, or a `File` })); ``` From c397928a5c5712b48aa54ddba70f35bc919bc8cf Mon Sep 17 00:00:00 2001 From: Martin Broerse Date: Tue, 30 Aug 2016 19:37:28 +0200 Subject: [PATCH 05/35] Update Bower dependencies (#137) --- blueprints/ember-pouch/index.js | 4 ++-- bower.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blueprints/ember-pouch/index.js b/blueprints/ember-pouch/index.js index 500ac7d..decd989 100644 --- a/blueprints/ember-pouch/index.js +++ b/blueprints/ember-pouch/index.js @@ -5,8 +5,8 @@ module.exports = { afterInstall: function() { return this.addBowerPackagesToProject([ - { name: 'pouchdb', target: '^3.5.0' }, - { name: 'relational-pouch', target: '^1.3.2'} + { name: 'pouchdb', target: '^5.4.5' }, + { name: 'relational-pouch', target: '^1.4.4'} ]); } }; diff --git a/bower.json b/bower.json index 4da0589..93933aa 100644 --- a/bower.json +++ b/bower.json @@ -33,7 +33,7 @@ "jquery": "1.11.3", "loader.js": "ember-cli/loader.js#3.4.0", "qunit": "~1.20.0", - "pouchdb": "^3.5.0", - "relational-pouch": "^1.3.2" + "pouchdb": "^5.4.5", + "relational-pouch": "^1.4.4" } } From 38f25cf3f906b95667d904fff4d1f90298ceb65a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 21 Jun 2016 17:09:03 -0400 Subject: [PATCH 06/35] Correct import of Ember Data model blueprint As suggested by @fsmanuel in #128 and #131. --- blueprints/pouch-model/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/blueprints/pouch-model/index.js b/blueprints/pouch-model/index.js index 3a7d2cd..ebb040a 100644 --- a/blueprints/pouch-model/index.js +++ b/blueprints/pouch-model/index.js @@ -1,3 +1,9 @@ -var EmberCliModelBlueprint = require('ember-cli/blueprints/model'); +var ModelBlueprint; -module.exports = EmberCliModelBlueprint; +try { + ModelBlueprint = require('ember-data/blueprints/model'); +} catch (e) { + ModelBlueprint = require('ember-cli/blueprints/model'); +} + +module.exports = ModelBlueprint; From c5355e61cb836e59995fc6806e7bd8dacfe59763 Mon Sep 17 00:00:00 2001 From: Martin Broerse Date: Tue, 30 Aug 2016 21:05:40 +0200 Subject: [PATCH 07/35] 3.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dd42981..cffe2e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-pouch", - "version": "3.2.1", + "version": "3.2.2", "description": "PouchDB adapter for Ember Data", "directories": { "doc": "doc", From 3c3366512f17984421b3cdd11d643456246a0357 Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Mon, 14 Dec 2015 18:07:37 +0600 Subject: [PATCH 08/35] Describe usage of `Attachment` transform --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index f346a11..d51d6ac 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,39 @@ export default Ember.Route.extend({ } } }); +``` + +## Attachments + +`Ember-Pouch` provides an `attachment` transform for your models, which makes working with attachments is as simple as working with any other field. + +Add a `DS.attr('attachment')` field to your model: + +```js +// myapp/models/photo-album.js +export default DS.Model.extend({ + photos: DS.attr('attachment'); +}); +``` + +Here, instances of `PhotoAlbum` have a `photos` field, which is an array of plain `Ember.Object`s, which have a `.name` and `.content_type`. Non-stubbed attachment also have a `.data` field; and stubbed attachments have a `.stub` instead. +```handlebars +
    + {{#each myalbum.photos as |photo|}} +
  • {{photo.name}}
  • + {{/each}} +
+``` +Attach new files by adding an `Ember.Object` with a `.name`, `.content_type` and `.data` to array of attachments. + +```js +// somewhere in your controller/component: +myAlbum.get('photos').addObject(Ember.Object.create({ + 'name': 'kitten.jpg', + 'content_type': 'image/jpg', + 'data': data // ... can be a DOM File, Blob, or plain old String +})); ``` ## Sample app From 95290bbcdde2c41ed12e606c6d1f186602729cf0 Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Wed, 17 Aug 2016 14:26:06 +0700 Subject: [PATCH 09/35] Keep the `length` and `digest` properties of a stub attachment --- addon/transforms/attachment.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addon/transforms/attachment.js b/addon/transforms/attachment.js index 7cbbea4..5881ca4 100644 --- a/addon/transforms/attachment.js +++ b/addon/transforms/attachment.js @@ -14,6 +14,8 @@ export default DS.Transform.extend({ content_type: serialized[attachmentName]['content_type'], data: serialized[attachmentName]['data'], stub: serialized[attachmentName]['stub'], + length: serialized[attachmentName]['length'], + digest: serialized[attachmentName]['digest'] }); }); }, @@ -27,6 +29,8 @@ export default DS.Transform.extend({ }; if (attachment.get('stub')) { serialized.stub = true; + serialized.length = attachment.get('length'); + serialized.digest = attachment.get('digest'); } else { serialized.data = attachment.get('data'); } From 94f995ea1924ec37a069afad57d01e8bda7c5597 Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Wed, 17 Aug 2016 15:04:02 +0700 Subject: [PATCH 10/35] Test that `digest` and `length` properties of a stub attachment are preserved --- tests/unit/transforms/attachment-test.js | 38 +++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/tests/unit/transforms/attachment-test.js b/tests/unit/transforms/attachment-test.js index 345ce08..cce726b 100644 --- a/tests/unit/transforms/attachment-test.js +++ b/tests/unit/transforms/attachment-test.js @@ -3,26 +3,32 @@ import { moduleFor, test } from 'ember-qunit'; import Ember from 'ember'; let testSerializedData = { - 'test.txt': { + 'hello.txt': { content_type: 'text/plain', - data: 'hello world!' + data: 'aGVsbG8gd29ybGQ=', + digest: "md5-7mkg+nM0HN26sZkLN8KVSA==" + // CouchDB doesn't add 'length' }, - 'stub.json': { + 'stub.txt': { stub: true, - content_type: 'application/json' - } + content_type: 'text/plain', + digest: "md5-7mkg+nM0HN26sZkLN8KVSA==", + length: 11 + }, }; let testDeserializedData = [ Ember.Object.create({ - name: 'test.txt', + name: 'hello.txt', content_type: 'text/plain', - data: 'hello world!' + data: 'aGVsbG8gd29ybGQ=', }), Ember.Object.create({ - name: 'stub.json', - content_type: 'application/json', - stub: true + name: 'stub.txt', + content_type: 'text/plain', + stub: true, + digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', + length: 11 }) ]; @@ -34,13 +40,14 @@ test('it serializes an attachment', function(assert) { assert.equal(transform.serialize(undefined), null); let serializedData = transform.serialize(testDeserializedData); - let name = testDeserializedData[0].get('name'); - assert.equal(serializedData[name].content_type, testSerializedData[name].content_type); - assert.equal(serializedData[name].data, testSerializedData[name].data); + let hello = testDeserializedData[0].get('name'); + assert.equal(hello, 'hello.txt'); + assert.equal(serializedData[hello].content_type, testSerializedData[hello].content_type); + assert.equal(serializedData[hello].data, testSerializedData[hello].data); let stub = testDeserializedData[1].get('name'); - + assert.equal(stub, 'stub.txt'); assert.equal(serializedData[stub].content_type, testSerializedData[stub].content_type); assert.equal(serializedData[stub].stub, true); }); @@ -55,8 +62,11 @@ test('it deserializes an attachment', function(assert) { assert.equal(deserializedData[0].get('name'), testDeserializedData[0].get('name')); assert.equal(deserializedData[0].get('content_type'), testDeserializedData[0].get('content_type')); assert.equal(deserializedData[0].get('data'), testDeserializedData[0].get('data')); + assert.equal(deserializedData[0].get('digest'), testDeserializedData[0].get('digest')); assert.equal(deserializedData[1].get('name'), testDeserializedData[1].get('name')); assert.equal(deserializedData[1].get('content_type'), testDeserializedData[1].get('content_type')); assert.equal(deserializedData[1].get('stub'), true); + assert.equal(deserializedData[1].get('digest'), testDeserializedData[1].get('digest')); + assert.equal(deserializedData[1].get('length'), testDeserializedData[1].get('length')); }); From f746fd8ebb07070b98abf09e305cffbc20241626 Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Wed, 17 Aug 2016 15:29:59 +0700 Subject: [PATCH 11/35] Add missing property on a test object --- tests/unit/transforms/attachment-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/transforms/attachment-test.js b/tests/unit/transforms/attachment-test.js index cce726b..614b398 100644 --- a/tests/unit/transforms/attachment-test.js +++ b/tests/unit/transforms/attachment-test.js @@ -22,6 +22,7 @@ let testDeserializedData = [ name: 'hello.txt', content_type: 'text/plain', data: 'aGVsbG8gd29ybGQ=', + digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==' }), Ember.Object.create({ name: 'stub.txt', From 3c9a0728525e17aad1c4b300efc0055b6141432a Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Wed, 17 Aug 2016 17:32:36 +0700 Subject: [PATCH 12/35] Deserialize "no attachments" into an empty array --- addon/transforms/attachment.js | 2 +- tests/unit/transforms/attachment-test.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/addon/transforms/attachment.js b/addon/transforms/attachment.js index 5881ca4..9c7c875 100644 --- a/addon/transforms/attachment.js +++ b/addon/transforms/attachment.js @@ -6,7 +6,7 @@ const keys = Object.keys || Ember.keys; export default DS.Transform.extend({ deserialize: function(serialized) { - if (isNone(serialized)) { return null; } + if (isNone(serialized)) { return []; } return keys(serialized).map(function (attachmentName) { return Ember.Object.create({ diff --git a/tests/unit/transforms/attachment-test.js b/tests/unit/transforms/attachment-test.js index 614b398..5741eba 100644 --- a/tests/unit/transforms/attachment-test.js +++ b/tests/unit/transforms/attachment-test.js @@ -39,6 +39,7 @@ test('it serializes an attachment', function(assert) { let transform = this.subject(); assert.equal(transform.serialize(null), null); assert.equal(transform.serialize(undefined), null); + assert.deepEqual(transform.serialize([]), {}); let serializedData = transform.serialize(testDeserializedData); @@ -55,8 +56,8 @@ test('it serializes an attachment', function(assert) { test('it deserializes an attachment', function(assert) { let transform = this.subject(); - assert.equal(transform.deserialize(null), null); - assert.equal(transform.deserialize(undefined), null); + assert.deepEqual(transform.deserialize(null), []); + assert.deepEqual(transform.deserialize(undefined), []); let deserializedData = transform.deserialize(testSerializedData); From 97ede1b93ee6e3c7ec29d3dad1c43b63ef9d7b5a Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Wed, 17 Aug 2016 18:07:15 +0700 Subject: [PATCH 13/35] Is fix a typo [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d51d6ac..167e6c1 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ export default Ember.Route.extend({ ## Attachments -`Ember-Pouch` provides an `attachment` transform for your models, which makes working with attachments is as simple as working with any other field. +`Ember-Pouch` provides an `attachment` transform for your models, which makes working with attachments as simple as working with any other field. Add a `DS.attr('attachment')` field to your model: From 6df6ee6951a6ac5573b7b6380e581cf253cc52c5 Mon Sep 17 00:00:00 2001 From: Stepan Stolyarov Date: Thu, 18 Aug 2016 13:16:49 +0700 Subject: [PATCH 14/35] Documentation for `defaultValue` of `attachment` --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 167e6c1..ac7cbe3 100644 --- a/README.md +++ b/README.md @@ -186,12 +186,16 @@ export default Ember.Route.extend({ `Ember-Pouch` provides an `attachment` transform for your models, which makes working with attachments as simple as working with any other field. -Add a `DS.attr('attachment')` field to your model: +Add a `DS.attr('attachment')` field to your model. Provide a default value for it to be an empty array. ```js // myapp/models/photo-album.js export default DS.Model.extend({ - photos: DS.attr('attachment'); + photos: DS.attr('attachment', { + defaultValue: function() { + return []; + } + }); }); ``` From 3c6162b147ad5fa8edb0df0ceff9580c5b5f06ff Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Wed, 17 Aug 2016 20:09:55 +1000 Subject: [PATCH 15/35] Return [] from serialize so that files can be added. - Use Ember.get so that POJOs can be used instead of Ember.Object instances --- addon/transforms/attachment.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/addon/transforms/attachment.js b/addon/transforms/attachment.js index 9c7c875..ae91da0 100644 --- a/addon/transforms/attachment.js +++ b/addon/transforms/attachment.js @@ -1,7 +1,10 @@ import Ember from 'ember'; import DS from 'ember-data'; -const { isNone } = Ember; +const { + get, + isNone +} = Ember; const keys = Object.keys || Ember.keys; export default DS.Transform.extend({ @@ -25,16 +28,16 @@ export default DS.Transform.extend({ return deserialized.reduce(function (acc, attachment) { const serialized = { - content_type: attachment.get('content_type'), + content_type: get(attachment, 'content_type'), }; - if (attachment.get('stub')) { + if (get(attachment, 'stub')) { serialized.stub = true; - serialized.length = attachment.get('length'); - serialized.digest = attachment.get('digest'); + serialized.length = get(attachment, 'length'); + serialized.digest = get(attachment, 'digest'); } else { - serialized.data = attachment.get('data'); + serialized.data = get(attachment, 'data'); } - acc[attachment.get('name')] = serialized; + acc[get(attachment, 'name')] = serialized; return acc; }, {}); } From 02ea2a0d8d98816c14db649a3ccfc9ab0ce7b7a6 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Wed, 17 Aug 2016 22:42:29 +1000 Subject: [PATCH 16/35] Provide two transforms: `attachment` and `attachments` --- addon/transforms/attachment.js | 38 ++++------------------------ addon/transforms/attachments.js | 44 +++++++++++++++++++++++++++++++++ app/transforms/attachments.js | 1 + 3 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 addon/transforms/attachments.js create mode 100644 app/transforms/attachments.js diff --git a/addon/transforms/attachment.js b/addon/transforms/attachment.js index ae91da0..6853681 100644 --- a/addon/transforms/attachment.js +++ b/addon/transforms/attachment.js @@ -1,44 +1,16 @@ import Ember from 'ember'; -import DS from 'ember-data'; +import AttachmentsTransform from './attachments'; const { - get, isNone } = Ember; -const keys = Object.keys || Ember.keys; -export default DS.Transform.extend({ +export default AttachmentsTransform.extend({ deserialize: function(serialized) { - if (isNone(serialized)) { return []; } - - return keys(serialized).map(function (attachmentName) { - return Ember.Object.create({ - name: attachmentName, - content_type: serialized[attachmentName]['content_type'], - data: serialized[attachmentName]['data'], - stub: serialized[attachmentName]['stub'], - length: serialized[attachmentName]['length'], - digest: serialized[attachmentName]['digest'] - }); - }); + return this._super(serialized).pop(); }, - serialize: function(deserialized) { - if (!Ember.isArray(deserialized)) { return null; } - - return deserialized.reduce(function (acc, attachment) { - const serialized = { - content_type: get(attachment, 'content_type'), - }; - if (get(attachment, 'stub')) { - serialized.stub = true; - serialized.length = get(attachment, 'length'); - serialized.digest = get(attachment, 'digest'); - } else { - serialized.data = get(attachment, 'data'); - } - acc[get(attachment, 'name')] = serialized; - return acc; - }, {}); + if (isNone(deserialized)) { return null; } + return this._super([deserialized]); } }); diff --git a/addon/transforms/attachments.js b/addon/transforms/attachments.js new file mode 100644 index 0000000..ae91da0 --- /dev/null +++ b/addon/transforms/attachments.js @@ -0,0 +1,44 @@ +import Ember from 'ember'; +import DS from 'ember-data'; + +const { + get, + isNone +} = Ember; +const keys = Object.keys || Ember.keys; + +export default DS.Transform.extend({ + deserialize: function(serialized) { + if (isNone(serialized)) { return []; } + + return keys(serialized).map(function (attachmentName) { + return Ember.Object.create({ + name: attachmentName, + content_type: serialized[attachmentName]['content_type'], + data: serialized[attachmentName]['data'], + stub: serialized[attachmentName]['stub'], + length: serialized[attachmentName]['length'], + digest: serialized[attachmentName]['digest'] + }); + }); + }, + + serialize: function(deserialized) { + if (!Ember.isArray(deserialized)) { return null; } + + return deserialized.reduce(function (acc, attachment) { + const serialized = { + content_type: get(attachment, 'content_type'), + }; + if (get(attachment, 'stub')) { + serialized.stub = true; + serialized.length = get(attachment, 'length'); + serialized.digest = get(attachment, 'digest'); + } else { + serialized.data = get(attachment, 'data'); + } + acc[get(attachment, 'name')] = serialized; + return acc; + }, {}); + } +}); diff --git a/app/transforms/attachments.js b/app/transforms/attachments.js new file mode 100644 index 0000000..2bbf4a8 --- /dev/null +++ b/app/transforms/attachments.js @@ -0,0 +1 @@ +export { default } from 'ember-pouch/transforms/attachments'; From b0fea2b90093a3e5d331ec453fc8f302282275c1 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Wed, 17 Aug 2016 23:30:15 +1000 Subject: [PATCH 17/35] Update unit test --- .../unit/transforms/{attachment-test.js => attachments-test.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/unit/transforms/{attachment-test.js => attachments-test.js} (97%) diff --git a/tests/unit/transforms/attachment-test.js b/tests/unit/transforms/attachments-test.js similarity index 97% rename from tests/unit/transforms/attachment-test.js rename to tests/unit/transforms/attachments-test.js index 5741eba..3a15c1b 100644 --- a/tests/unit/transforms/attachment-test.js +++ b/tests/unit/transforms/attachments-test.js @@ -33,7 +33,7 @@ let testDeserializedData = [ }) ]; -moduleFor('transform:attachment', 'Unit | Transform | attachment', {}); +moduleFor('transform:attachments', 'Unit | Transform | attachments', {}); test('it serializes an attachment', function(assert) { let transform = this.subject(); From 412311bf48cd984ac7cbe3ac48602a9a66913fb1 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Thu, 18 Aug 2016 23:20:06 +1000 Subject: [PATCH 18/35] Change serializer so that attachments are passed to and from relational-couch correctly --- addon/serializers/pouch.js | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/addon/serializers/pouch.js b/addon/serializers/pouch.js index ec5fdad..0cdec7c 100644 --- a/addon/serializers/pouch.js +++ b/addon/serializers/pouch.js @@ -1,5 +1,11 @@ +import Ember from 'ember'; import DS from 'ember-data'; +const { + get +} = Ember; +const keys = Object.keys || Ember.keys; + export default DS.RESTSerializer.extend({ _shouldSerializeHasMany: function() { return true; @@ -15,5 +21,41 @@ export default DS.RESTSerializer.extend({ if (!json[key]) { json[key] = []; } + }, + + _isAttachment(attribute) { + return ['attachment', 'attachments'].indexOf(attribute.type) !== -1; + }, + + serializeAttribute(snapshot, json, key, attribute) { + this._super(snapshot, json, key, attribute); + if (this._isAttachment(attribute)) { + // if provided, use the mapping provided by `attrs` in the serializer + var payloadKey = this._getMappedKey(key, snapshot.type); + if (payloadKey === key && this.keyForAttribute) { + payloadKey = this.keyForAttribute(key, 'serialize'); + } + // assign any attachments to the attachments property, so that relational-pouch + // will put these in the special CouchDB _attachments property + // this will conflict with any 'attachments' attr in the model + // suggest that #toRawDoc in relational-pouch should allow _attachments to be specified + json['attachments'] = Object.assign({}, json['attachments'], json[payloadKey]); + } + }, + + extractAttributes(modelClass, resourceHash) { + let attributes = this._super(modelClass, resourceHash); + let modelAttrs = get(modelClass, 'attributes'); + modelClass.eachTransformedAttribute(key => { + let attribute = modelAttrs.get(key); + if (this._isAttachment(attribute)) { + // put the corresponding _attachments entries from the response into the attribute + let fileNames = keys(attributes[key]); + fileNames.forEach(fileName => { + attributes[key][fileName] = resourceHash.attachments[fileName]; + }); + } + }); + return attributes; } }); From 1de66b4d492f7808dea0144f58611bed4aab909e Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Fri, 19 Aug 2016 15:58:34 +1000 Subject: [PATCH 19/35] Ensure that length is available when the attachment is first saved --- addon/transforms/attachments.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/addon/transforms/attachments.js b/addon/transforms/attachments.js index ae91da0..715533a 100644 --- a/addon/transforms/attachments.js +++ b/addon/transforms/attachments.js @@ -12,13 +12,14 @@ export default DS.Transform.extend({ if (isNone(serialized)) { return []; } return keys(serialized).map(function (attachmentName) { + let attachment = serialized[attachmentName]; return Ember.Object.create({ name: attachmentName, - content_type: serialized[attachmentName]['content_type'], - data: serialized[attachmentName]['data'], - stub: serialized[attachmentName]['stub'], - length: serialized[attachmentName]['length'], - digest: serialized[attachmentName]['digest'] + content_type: attachment.content_type, + data: attachment.data, + stub: attachment.stub, + length: attachment.length, + digest: attachment.digest, }); }); }, @@ -34,8 +35,10 @@ export default DS.Transform.extend({ serialized.stub = true; serialized.length = get(attachment, 'length'); serialized.digest = get(attachment, 'digest'); - } else { + } + else { serialized.data = get(attachment, 'data'); + serialized.length = get(attachment, 'length'); } acc[get(attachment, 'name')] = serialized; return acc; From e57abbc99edccebc777f8b723077d09d0aef6cb1 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Mon, 22 Aug 2016 14:38:12 +1000 Subject: [PATCH 20/35] Added integration test --- addon/serializers/pouch.js | 16 +++- tests/dummy/app/models/taco-recipe.js | 8 ++ tests/dummy/app/serializers/taco-recipe.js | 8 ++ tests/integration/serializers/pouch-test.js | 100 ++++++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 tests/dummy/app/models/taco-recipe.js create mode 100644 tests/dummy/app/serializers/taco-recipe.js create mode 100644 tests/integration/serializers/pouch-test.js diff --git a/addon/serializers/pouch.js b/addon/serializers/pouch.js index 0cdec7c..347e99e 100644 --- a/addon/serializers/pouch.js +++ b/addon/serializers/pouch.js @@ -35,11 +35,19 @@ export default DS.RESTSerializer.extend({ if (payloadKey === key && this.keyForAttribute) { payloadKey = this.keyForAttribute(key, 'serialize'); } - // assign any attachments to the attachments property, so that relational-pouch - // will put these in the special CouchDB _attachments property - // this will conflict with any 'attachments' attr in the model - // suggest that #toRawDoc in relational-pouch should allow _attachments to be specified + + // Merge any attachments in this attribute into the `attachments` property. + // relational-pouch will put these in the special CouchDB `_attachments` property + // of the document. + // This will conflict with any 'attachments' attr in the model. Suggest that + // #toRawDoc in relational-pouch should allow _attachments to be specified json['attachments'] = Object.assign({}, json['attachments'], json[payloadKey]); + json[payloadKey] = Object.keys(json[payloadKey]).reduce((attr, fileName) => { + attr[fileName] = Object.assign({}, json[payloadKey][fileName]); + delete attr[fileName].data; + delete attr[fileName].content_type; + return attr; + }, {}); } }, diff --git a/tests/dummy/app/models/taco-recipe.js b/tests/dummy/app/models/taco-recipe.js new file mode 100644 index 0000000..67677e2 --- /dev/null +++ b/tests/dummy/app/models/taco-recipe.js @@ -0,0 +1,8 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + rev: DS.attr('string'), + + coverImage: DS.attr('attachment'), + photos: DS.attr('attachments') +}); diff --git a/tests/dummy/app/serializers/taco-recipe.js b/tests/dummy/app/serializers/taco-recipe.js new file mode 100644 index 0000000..c74ddc3 --- /dev/null +++ b/tests/dummy/app/serializers/taco-recipe.js @@ -0,0 +1,8 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + attrs: { + coverImage: 'cover_image', + photos: { key: 'photo_gallery' } + } +}); diff --git a/tests/integration/serializers/pouch-test.js b/tests/integration/serializers/pouch-test.js new file mode 100644 index 0000000..5d6cfd9 --- /dev/null +++ b/tests/integration/serializers/pouch-test.js @@ -0,0 +1,100 @@ +import { test } from 'qunit'; +import moduleForIntegration from '../../helpers/module-for-acceptance'; + +import Ember from 'ember'; + +/* + * Tests attachments behavior for an app using the ember-pouch serializer. + */ + +moduleForIntegration('Integration | Serializer | Attachments'); + +let id = 'E'; +let coverImage = { + name: 'cover.jpg', + content_type: 'image/jpeg', + data: window.btoa('cover.jpg'), + length: 9 +}; +let photo1 = { + name: 'photo-1.jpg', + content_type: 'image/jpeg', + data: window.btoa('photo-1.jpg') +}; +let photo2 = { + name: 'photo-2.jpg', + content_type: 'image/jpeg', + data: window.btoa('photo-2.jpg') +}; + +test('puts attachments into the `attachments` property when saving', function (assert) { + assert.expect(10); + + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + var newRecipe = this.store().createRecord('taco-recipe', { + id, + coverImage: coverImage, + photos: [photo1, photo2] + }); + return newRecipe.save(); + }).then(() => { + return this.db().get('tacoRecipe_2_E'); + }).then((newDoc) => { + assert.deepEqual(newDoc._attachments, { + 'cover.jpg': { + digest: 'md5-SxxZx3KOKxy2X2yyCq9c+Q==', + content_type: 'image/jpeg', + revpos: undefined, + stub: true, + length: 9 + }, + 'photo-1.jpg': { + digest: 'md5-MafOMdm9kXWId0ruvo8sTA==', + content_type: 'image/jpeg', + revpos: undefined, + stub: true, + length: 11 + }, + 'photo-2.jpg': { + digest: 'md5-VNkFh9jG/28rwoFW9L910g==', + content_type: 'image/jpeg', + revpos: undefined, + stub: true, + length: 11 + } + }, 'attachments are placed into the _attachments property of the doc'); + assert.equal('cover_image' in newDoc.data, true, + 'respects the mapping provided by the serializer `attrs`' + ); + assert.deepEqual(newDoc.data.cover_image, { + 'cover.jpg': { + length: 9 + } + }, 'the attribute contains the file name'); + assert.equal(newDoc.data.cover_image['cover.jpg'].length, 9, + 'the attribute contains the length to avoid empty length when File objects are ' + + 'saved and have not been reloaded' + ); + assert.deepEqual(newDoc.data.photo_gallery, { + 'photo-1.jpg': {}, + 'photo-2.jpg': {} + }); + + var recordInStore = this.store().peekRecord('tacoRecipe', 'E'); + let coverImage = recordInStore.get('coverImage'); + assert.equal(coverImage.get('name'), coverImage.name); + assert.equal(coverImage.get('data'), coverImage.data); + + let photos = recordInStore.get('photos'); + assert.equal(photos.length, 2, '2 photos'); + assert.equal(photos[0].get('name'), photo1.name); + assert.equal(photos[0].get('data'), photo1.data); + + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); From 1445d0e861dee05a963ba28114c96ddb01a7e215 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Mon, 22 Aug 2016 15:55:14 +1000 Subject: [PATCH 21/35] Tests passing in PhantomJS --- addon/serializers/pouch.js | 9 ++-- tests/integration/serializers/pouch-test.js | 51 +++++++++------------ 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/addon/serializers/pouch.js b/addon/serializers/pouch.js index 347e99e..fa3cb8d 100644 --- a/addon/serializers/pouch.js +++ b/addon/serializers/pouch.js @@ -2,7 +2,8 @@ import Ember from 'ember'; import DS from 'ember-data'; const { - get + get, + assign } = Ember; const keys = Object.keys || Ember.keys; @@ -41,9 +42,9 @@ export default DS.RESTSerializer.extend({ // of the document. // This will conflict with any 'attachments' attr in the model. Suggest that // #toRawDoc in relational-pouch should allow _attachments to be specified - json['attachments'] = Object.assign({}, json['attachments'], json[payloadKey]); - json[payloadKey] = Object.keys(json[payloadKey]).reduce((attr, fileName) => { - attr[fileName] = Object.assign({}, json[payloadKey][fileName]); + json.attachments = assign(Object(), json.attachments || {}, json[payloadKey]); // jshint ignore:line + json[payloadKey] = keys(json[payloadKey]).reduce((attr, fileName) => { + attr[fileName] = assign(Object(), json[payloadKey][fileName]); // jshint ignore:line delete attr[fileName].data; delete attr[fileName].content_type; return attr; diff --git a/tests/integration/serializers/pouch-test.js b/tests/integration/serializers/pouch-test.js index 5d6cfd9..32af5dc 100644 --- a/tests/integration/serializers/pouch-test.js +++ b/tests/integration/serializers/pouch-test.js @@ -28,7 +28,7 @@ let photo2 = { }; test('puts attachments into the `attachments` property when saving', function (assert) { - assert.expect(10); + assert.expect(11); var done = assert.async(); Ember.RSVP.Promise.resolve().then(() => { @@ -41,29 +41,20 @@ test('puts attachments into the `attachments` property when saving', function (a }).then(() => { return this.db().get('tacoRecipe_2_E'); }).then((newDoc) => { - assert.deepEqual(newDoc._attachments, { - 'cover.jpg': { - digest: 'md5-SxxZx3KOKxy2X2yyCq9c+Q==', - content_type: 'image/jpeg', - revpos: undefined, - stub: true, - length: 9 - }, - 'photo-1.jpg': { - digest: 'md5-MafOMdm9kXWId0ruvo8sTA==', - content_type: 'image/jpeg', - revpos: undefined, - stub: true, - length: 11 - }, - 'photo-2.jpg': { - digest: 'md5-VNkFh9jG/28rwoFW9L910g==', - content_type: 'image/jpeg', - revpos: undefined, - stub: true, - length: 11 - } + function checkAttachment(attachments, fileName, value, message) { + delete attachments[fileName].revpos; + assert.deepEqual(attachments[fileName], value, message); + } + checkAttachment(newDoc._attachments, 'cover.jpg', { + digest: 'md5-SxxZx3KOKxy2X2yyCq9c+Q==', + content_type: 'image/jpeg', + stub: true, + length: 9 }, 'attachments are placed into the _attachments property of the doc'); + assert.deepEqual(Object.keys(newDoc._attachments).sort(), + [coverImage.name, photo1.name, photo2.name].sort(), + 'all attachments are included in the _attachments property of the doc' + ); assert.equal('cover_image' in newDoc.data, true, 'respects the mapping provided by the serializer `attrs`' ); @@ -82,14 +73,14 @@ test('puts attachments into the `attachments` property when saving', function (a }); var recordInStore = this.store().peekRecord('tacoRecipe', 'E'); - let coverImage = recordInStore.get('coverImage'); - assert.equal(coverImage.get('name'), coverImage.name); - assert.equal(coverImage.get('data'), coverImage.data); + let coverAttr = recordInStore.get('coverImage'); + assert.equal(coverAttr.get('name'), coverImage.name); + assert.equal(coverAttr.get('data'), coverImage.data); - let photos = recordInStore.get('photos'); - assert.equal(photos.length, 2, '2 photos'); - assert.equal(photos[0].get('name'), photo1.name); - assert.equal(photos[0].get('data'), photo1.data); + let photosAttr = recordInStore.get('photos'); + assert.equal(photosAttr.length, 2, '2 photos'); + assert.equal(photosAttr[0].get('name'), photo1.name); + assert.equal(photosAttr[0].get('data'), photo1.data); done(); }).catch((error) => { From c4e1550c631d48b3552994bf09902a527d0cd986 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Tue, 23 Aug 2016 16:05:18 +1000 Subject: [PATCH 22/35] Remove ember-data-1.13 from ember-try scenarios. Add Object.assign polyfill for PhantomJS when running tests. - Add Ember.run() to taco-salad adapter to avoid exceptions when running tests --- .travis.yml | 1 - addon/serializers/pouch.js | 5 ++--- bower.json | 3 ++- config/ember-try.js | 11 ----------- ember-cli-build.js | 4 ++++ tests/dummy/app/adapters/taco-salad.js | 4 +++- 6 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index c7a4ea5..c74d6b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ cache: - node_modules env: - - EMBER_TRY_SCENARIO=ember-data-1.13 - EMBER_TRY_SCENARIO=ember-data-2.0 - EMBER_TRY_SCENARIO=ember-data-2.1 - EMBER_TRY_SCENARIO=ember-data-2.2 diff --git a/addon/serializers/pouch.js b/addon/serializers/pouch.js index fa3cb8d..ee92d1b 100644 --- a/addon/serializers/pouch.js +++ b/addon/serializers/pouch.js @@ -3,7 +3,6 @@ import DS from 'ember-data'; const { get, - assign } = Ember; const keys = Object.keys || Ember.keys; @@ -42,9 +41,9 @@ export default DS.RESTSerializer.extend({ // of the document. // This will conflict with any 'attachments' attr in the model. Suggest that // #toRawDoc in relational-pouch should allow _attachments to be specified - json.attachments = assign(Object(), json.attachments || {}, json[payloadKey]); // jshint ignore:line + json.attachments = Object.assign({}, json.attachments || {}, json[payloadKey]); // jshint ignore:line json[payloadKey] = keys(json[payloadKey]).reduce((attr, fileName) => { - attr[fileName] = assign(Object(), json[payloadKey][fileName]); // jshint ignore:line + attr[fileName] = Object.assign({}, json[payloadKey][fileName]); // jshint ignore:line delete attr[fileName].data; delete attr[fileName].content_type; return attr; diff --git a/bower.json b/bower.json index 93933aa..a07d5e0 100644 --- a/bower.json +++ b/bower.json @@ -34,6 +34,7 @@ "loader.js": "ember-cli/loader.js#3.4.0", "qunit": "~1.20.0", "pouchdb": "^5.4.5", - "relational-pouch": "^1.4.4" + "relational-pouch": "^1.4.4", + "phantomjs-polyfill-object-assign": "chuckplantain/phantomjs-polyfill-object-assign" } } diff --git a/config/ember-try.js b/config/ember-try.js index ff666d3..238c384 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -1,16 +1,5 @@ module.exports = { scenarios: [ - { - name: 'ember-data-1.13', - dependencies: { - 'ember': '1.13.11', - 'ember-data': '1.13.15', - 'ember-cli-shims': '0.0.6' - }, - resolutions: { - 'ember': '1.13.11' - } - }, { name: 'ember-data-2.0', dependencies: { diff --git a/ember-cli-build.js b/ember-cli-build.js index c8c48f8..a764a76 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -14,5 +14,9 @@ module.exports = function(defaults) { behave. You most likely want to be modifying `./index.js` or app's build file */ + app.import('bower_components/phantomjs-polyfill-object-assign/object-assign-polyfill.js', { + type: 'test' + }); + return app.toTree(); }; diff --git a/tests/dummy/app/adapters/taco-salad.js b/tests/dummy/app/adapters/taco-salad.js index 9ec86b7..58d3cb2 100644 --- a/tests/dummy/app/adapters/taco-salad.js +++ b/tests/dummy/app/adapters/taco-salad.js @@ -33,7 +33,9 @@ export default Adapter.extend({ let store = this.get('store'); let recordTypeName = this.getRecordTypeName(store.modelFor(obj.type)); this.get('db').rel.find(recordTypeName, obj.id).then(function(doc){ - store.pushPayload(recordTypeName, doc); + Ember.run(function() { + store.pushPayload(recordTypeName, doc); + }); }); } }); From 44cd398ff03aa0563d787d00ebabb1a9aa992f39 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Tue, 30 Aug 2016 17:35:59 +1000 Subject: [PATCH 23/35] Check ember-data version and add install instructions for old ember-data/ember-cli --- README.md | 16 +++++++++++++++- index.js | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac7cbe3..9ce03ad 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,21 @@ For more on PouchDB, check out [pouchdb.com](http://pouchdb.com). ## Install and setup - ember install ember-pouch +```bash +ember install ember-pouch +``` + +For ember-data < 2.0: + +```bash +ember install ember-pouch@3.2.1 +``` + +For ember-cli < 1.13.0: + +```bash +npm install ember-pouch --save-dev +``` This provides - `import PouchDB from 'pouchdb'` diff --git a/index.js b/index.js index c56da1f..92b5254 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,7 @@ function satisfactoryEmberDataVersion(addon) { var checker = new VersionChecker(addon), bowerEmberData = checker.for('ember-data', 'bower'), npmEmberData = checker.for('ember-data', 'npm'); - return npmEmberData.isAbove('2.2.99') || bowerEmberData.isAbove('1.12.99'); + return npmEmberData.isAbove('2.2.99') || bowerEmberData.isAbove('1.13.16'); } module.exports = { From 400baaff1823f5233ff4afea45f5e8b91e114690 Mon Sep 17 00:00:00 2001 From: BT Date: Tue, 21 Jun 2016 13:25:40 +0800 Subject: [PATCH 24/35] implement glue code for query and queryRecord --- addon/adapters/pouch.js | 93 ++++++++++++- blueprints/ember-pouch/index.js | 4 +- bower.json | 3 +- index.js | 1 + tests/dummy/app/models/smasher.js | 10 ++ .../integration/adapters/pouch-basics-test.js | 131 ++++++++++++++++++ 6 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 tests/dummy/app/models/smasher.js diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index cff8735..f9acbf4 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -202,6 +202,58 @@ export default DS.RESTAdapter.extend({ return data; }, + /** + * Return key that conform to data adapter + * ex: 'name' become 'data.name' + */ + _dataKey: function(key) { + var dataKey ='data.' + key; + return ""+ dataKey + ""; + }, + + /** + * Returns the modified selector key to comform data key + * Ex: selector: {name: 'Mario'} wil become selector: {'data.name': 'Mario'} + */ + _buildSelector: function(selector) { + var dataSelector = {}; + var selectorKeys = []; + + for (var key in selector) { + if(selector.hasOwnProperty(key)){ + selectorKeys.push(key); + } + } + + selectorKeys.forEach(function(key) { + var dataKey = this._dataKey(key); + dataSelector[dataKey] = selector[key]; + }.bind(this)); + + return dataSelector; + }, + + /** + * Returns the modified sort key + * Ex: sort: ['series'] will become ['data.series'] + * Ex: sort: [{series: 'desc'}] will became [{'data.series': 'desc'}] + */ + _buildSort: function(sort) { + return sort.map(function (value) { + var sortKey = {}; + if (typeof value === 'object' && value !== null) { + for (var key in value) { + if(value.hasOwnProperty(key)){ + sortKey[this._dataKey(key)] = value[key]; + } + } + } else { + return this._dataKey(value); + } + return sortKey; + }.bind(this)); + }, + /** * Returns the string to use for the model name part of the PouchDB document * ID for records of the given ember-data type. @@ -229,10 +281,43 @@ export default DS.RESTAdapter.extend({ return this.get('db').rel.find(this.getRecordTypeName(type), ids); }, - findQuery: function(/* store, type, query */) { - throw new Error( - "findQuery not yet supported by ember-pouch. " + - "See https://github.com/nolanlawson/ember-pouch/issues/7."); + + query: function(store, type, query) { + this._init(store, type); + + var recordTypeName = this.getRecordTypeName(type); + var db = this.get('db'); + + var queryParams = { + selector: this._buildSelector(query.selector) + }; + + if (!Ember.isEmpty(query.sort)) { + queryParams.sort = this._buildSort(query.sort); + } + + return db.find(queryParams).then(function (payload) { + if (typeof payload === 'object' && payload !== null) { + var plural = pluralize(recordTypeName); + var results = {}; + + var rows = payload.docs.map((row) => { + var parsedId = db.rel.parseDocID(row._id); + if (!Ember.isEmpty(parsedId.id)) { + row.data.id = parsedId.id; + return row.data; + } + }); + + results[plural] = rows; + + return results; + } + }); + }, + + queryRecord: function(store, type, query) { + return this.query(store, type, query); }, /** diff --git a/blueprints/ember-pouch/index.js b/blueprints/ember-pouch/index.js index decd989..c96b647 100644 --- a/blueprints/ember-pouch/index.js +++ b/blueprints/ember-pouch/index.js @@ -1,4 +1,5 @@ 'use strict'; + "pouchdb-find": "^0.10.2" module.exports = { normalizeEntityName: function() {}, @@ -6,7 +7,8 @@ module.exports = { afterInstall: function() { return this.addBowerPackagesToProject([ { name: 'pouchdb', target: '^5.4.5' }, - { name: 'relational-pouch', target: '^1.4.4'} + { name: 'relational-pouch', target: '^1.4.4'}, + { name: 'pouchdb-find', target: '^0.10.2'} ]); } }; diff --git a/bower.json b/bower.json index a07d5e0..e32153e 100644 --- a/bower.json +++ b/bower.json @@ -35,6 +35,7 @@ "qunit": "~1.20.0", "pouchdb": "^5.4.5", "relational-pouch": "^1.4.4", - "phantomjs-polyfill-object-assign": "chuckplantain/phantomjs-polyfill-object-assign" + "phantomjs-polyfill-object-assign": "chuckplantain/phantomjs-polyfill-object-assign", + "pouchdb-find": "^0.10.2" } } diff --git a/index.js b/index.js index 92b5254..d0adc63 100644 --- a/index.js +++ b/index.js @@ -32,6 +32,7 @@ module.exports = { app.import(bowerDir + '/pouchdb/dist/pouchdb.js'); app.import(bowerDir + '/relational-pouch/dist/pouchdb.relational-pouch.js'); + app.import(bowerDir + '/pouchdb-find/dist/pouchdb.find.js'); app.import('vendor/ember-pouch/shim.js', { type: 'vendor', exports: { diff --git a/tests/dummy/app/models/smasher.js b/tests/dummy/app/models/smasher.js new file mode 100644 index 0000000..b8eca69 --- /dev/null +++ b/tests/dummy/app/models/smasher.js @@ -0,0 +1,10 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + rev: DS.attr('string'), + + name: DS.attr('string'), + series: DS.attr('string'), + debut: DS.attr(), +}); + diff --git a/tests/integration/adapters/pouch-basics-test.js b/tests/integration/adapters/pouch-basics-test.js index b92973b..9c6a018 100644 --- a/tests/integration/adapters/pouch-basics-test.js +++ b/tests/integration/adapters/pouch-basics-test.js @@ -59,6 +59,137 @@ test('can find one', function (assert) { }); }); +test('can query with sort', function (assert) { + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return this.db().createIndex({ index: { + fields: ['data.name'] } + }).then(() => { + return this.db().bulkDocs([ + { _id: 'smasher_2_mario', data: { name: 'Mario', series: 'Mario', debut: 1981 }}, + { _id: 'smasher_2_puff', data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }}, + { _id: 'smasher_2_link', data: { name: 'Link', series: 'Zelda', debut: 1986 }}, + { _id: 'smasher_2_dk', data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }}, + { _id: 'smasher_2_pika', data: { name: 'Pikachu', series: 'Pokemon', _id: 'pikachu', debut: 1996 }} + ]); + }); + }).then(() => { + return this.store().query('smasher', { + selector: {name: {$gt: ''}}, + sort: ['name'] + }); + }).then((found) => { + assert.equal(found.get('length'), 5, 'should returns all the smashers '); + assert.deepEqual(found.mapBy('id'), ['dk','puff','link','mario','pika'], + 'should have extracted the IDs correctly'); + assert.deepEqual(found.mapBy('name'), ['Donkey Kong', 'Jigglypuff', 'Link', 'Mario','Pikachu'], + 'should have extracted the attributes also'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('can query multi-field queries', function (assert) { + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return this.db().createIndex({ index: { + fields: ['data.series', 'data.debut'] } + }).then(() => { + return this.db().bulkDocs([ + { _id: 'smasher_2_mario', data: { name: 'Mario', series: 'Mario', debut: 1981 }}, + { _id: 'smasher_2_puff', data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }}, + { _id: 'smasher_2_link', data: { name: 'Link', series: 'Zelda', debut: 1986 }}, + { _id: 'smasher_2_dk', data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }}, + { _id: 'smasher_2_pika', data: { name: 'Pikachu', series: 'Pokemon', _id: 'pikachu', debut: 1996 }} + ]); + }); + }).then(() => { + return this.store().query('smasher', { + selector: {series: 'Mario' }, + sort: [ + {series: 'desc'}, + {debut: 'desc'}] + }); + }).then((found) => { + assert.equal(found.get('length'), 2, 'should have found the two smashers'); + assert.deepEqual(found.mapBy('id'), ['mario', 'dk'], + 'should have extracted the IDs correctly'); + assert.deepEqual(found.mapBy('name'), ['Mario', 'Donkey Kong'], + 'should have extracted the attributes also'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('can query one record', function (assert) { + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return this.db().createIndex({ index: { + fields: ['data.flavor'] } + }).then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor', ingredients: ['X', 'Y'] } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean', ingredients: ['Z'] } }, + { _id: 'foodItem_2_X', data: { name: 'pineapple' }}, + { _id: 'foodItem_2_Y', data: { name: 'pork loin' }}, + { _id: 'foodItem_2_Z', data: { name: 'black beans' }} + ]); + }); + }).then(() => { + return this.store().queryRecord('taco-soup', { + selector: {flavor: 'al pastor' } + }); + }).then((found) => { + assert.equal(found.get('flavor'), 'al pastor', + 'should have found the requested item'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('can query one associated records', function (assert) { + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return this.db().createIndex({ index: { + fields: ['data.flavor'] } + }).then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor', ingredients: ['X', 'Y'] } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean', ingredients: ['Z'] } }, + { _id: 'foodItem_2_X', data: { name: 'pineapple' }}, + { _id: 'foodItem_2_Y', data: { name: 'pork loin' }}, + { _id: 'foodItem_2_Z', data: { name: 'black beans' }} + ]); + }); + }).then(() => { + return this.store().queryRecord('taco-soup', { + selector: {flavor: 'al pastor' }}); + }).then((found) => { + assert.equal(found.get('flavor'), 'al pastor', + 'should have found the requested item'); + return found.get('ingredients'); + }).then((foundIngredients) => { + assert.deepEqual(foundIngredients.mapBy('id'), ['X', 'Y'], + 'should have found both associated items'); + assert.deepEqual(foundIngredients.mapBy('name'), ['pineapple', 'pork loin'], + 'should have fully loaded the associated items'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + test('can find associated records', function (assert) { assert.expect(3); From 2707d0189d7305bde954f800e45a463d266f6c87 Mon Sep 17 00:00:00 2001 From: BT Date: Sun, 4 Sep 2016 15:30:35 +0800 Subject: [PATCH 25/35] changed selector to filter --- addon/adapters/pouch.js | 2 +- tests/integration/adapters/pouch-basics-test.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index f9acbf4..310ea60 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -289,7 +289,7 @@ export default DS.RESTAdapter.extend({ var db = this.get('db'); var queryParams = { - selector: this._buildSelector(query.selector) + selector: this._buildSelector(query.filter) }; if (!Ember.isEmpty(query.sort)) { diff --git a/tests/integration/adapters/pouch-basics-test.js b/tests/integration/adapters/pouch-basics-test.js index 9c6a018..6612161 100644 --- a/tests/integration/adapters/pouch-basics-test.js +++ b/tests/integration/adapters/pouch-basics-test.js @@ -75,7 +75,7 @@ test('can query with sort', function (assert) { }); }).then(() => { return this.store().query('smasher', { - selector: {name: {$gt: ''}}, + filter: {name: {$gt: ''}}, sort: ['name'] }); }).then((found) => { @@ -108,7 +108,7 @@ test('can query multi-field queries', function (assert) { }); }).then(() => { return this.store().query('smasher', { - selector: {series: 'Mario' }, + filter: {series: 'Mario' }, sort: [ {series: 'desc'}, {debut: 'desc'}] @@ -143,7 +143,7 @@ test('can query one record', function (assert) { }); }).then(() => { return this.store().queryRecord('taco-soup', { - selector: {flavor: 'al pastor' } + filter: {flavor: 'al pastor' } }); }).then((found) => { assert.equal(found.get('flavor'), 'al pastor', @@ -172,7 +172,7 @@ test('can query one associated records', function (assert) { }); }).then(() => { return this.store().queryRecord('taco-soup', { - selector: {flavor: 'al pastor' }}); + filter: {flavor: 'al pastor' }}); }).then((found) => { assert.equal(found.get('flavor'), 'al pastor', 'should have found the requested item'); From ea6c8bf3ea21711878493a20e36e9920c6dfd8fb Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Mon, 5 Sep 2016 09:54:51 +1000 Subject: [PATCH 26/35] Updated README and changelog --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ce03ad..7e4e3ea 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [**Changelog**](#changelog) -Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 1.13+. +Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 2.0+. With Ember Pouch, all of your app's data is automatically saved on the client-side using IndexedDB or WebSQL, and you just keep using the regular [Ember Data `store` API](http://emberjs.com/api/data/classes/DS.Store.html#method_all). This data may be automatically synced to a remote CouchDB (or compatible servers) using PouchDB replication. @@ -434,6 +434,8 @@ And of course thanks to all our wonderful contributors, [here](https://github.co ## Changelog +* **4.0.0** + - Add support for [#Attachments](attachments) * **3.1.1** - Bugfix for hasMany relations by [@backspace](https://github.com/backspace) ([#111](https://github.com/nolanlawson/ember-pouch/pull/111)). * **3.1.0** From 99ac186fe9c6552498a3374396443e0eb5974989 Mon Sep 17 00:00:00 2001 From: Martin Broerse Date: Mon, 5 Sep 2016 06:37:22 +0200 Subject: [PATCH 27/35] Update README --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7e4e3ea..1fd9e04 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [**Changelog**](#changelog) -Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 2.0+. +Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 2.0+. For older Ember Data versions use Ember Pouch version 3.22. With Ember Pouch, all of your app's data is automatically saved on the client-side using IndexedDB or WebSQL, and you just keep using the regular [Ember Data `store` API](http://emberjs.com/api/data/classes/DS.Store.html#method_all). This data may be automatically synced to a remote CouchDB (or compatible servers) using PouchDB replication. @@ -25,13 +25,13 @@ ember install ember-pouch For ember-data < 2.0: ```bash -ember install ember-pouch@3.2.1 +ember install ember-pouch@3.2.2 ``` For ember-cli < 1.13.0: ```bash -npm install ember-pouch --save-dev +npm install ember-pouch@3.2.2 --save-dev ``` This provides @@ -435,7 +435,15 @@ And of course thanks to all our wonderful contributors, [here](https://github.co ## Changelog * **4.0.0** - - Add support for [#Attachments](attachments) + - Add support for Attachments [#135](https://github.com/nolanlawson/ember-pouch/pull/135) + - Implement glue code for query and queryRecord using pouchdb-find [#130](https://github.com/nolanlawson/ember-pouch/pull/130) +* **3.2.2** + - Update Bower dependencies [#137](https://github.com/nolanlawson/ember-pouch/pull/137) + - Correct import of Ember Data model blueprint [#131](https://github.com/nolanlawson/ember-pouch/pull/131) +* **3.2.1** + - Fix(Addon): Call super in init [#129](https://github.com/nolanlawson/ember-pouch/pull/129) +* **3.2.0** + - Make adapter call a hook when encountering a change for a record that is not yet loaded [#108](https://github.com/nolanlawson/ember-pouch/pull/108) * **3.1.1** - Bugfix for hasMany relations by [@backspace](https://github.com/backspace) ([#111](https://github.com/nolanlawson/ember-pouch/pull/111)). * **3.1.0** From a55a4bcb47a514cf88db444b0f15bf9d96480598 Mon Sep 17 00:00:00 2001 From: Martin Broerse Date: Tue, 6 Sep 2016 06:11:49 +0200 Subject: [PATCH 28/35] 4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cffe2e5..d5f15c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-pouch", - "version": "3.2.2", + "version": "4.0.0", "description": "PouchDB adapter for Ember Data", "directories": { "doc": "doc", From 81b3f82d2427579731e4941455dade01f3dbeb99 Mon Sep 17 00:00:00 2001 From: Martin Broerse Date: Tue, 6 Sep 2016 06:37:47 +0200 Subject: [PATCH 29/35] Fix Blueprint --- blueprints/ember-pouch/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/blueprints/ember-pouch/index.js b/blueprints/ember-pouch/index.js index c96b647..ec423cc 100644 --- a/blueprints/ember-pouch/index.js +++ b/blueprints/ember-pouch/index.js @@ -1,5 +1,4 @@ 'use strict'; - "pouchdb-find": "^0.10.2" module.exports = { normalizeEntityName: function() {}, From 088b9f1ea3a4c063728ed2e04e8031cb6a1ee6aa Mon Sep 17 00:00:00 2001 From: Martin Broerse Date: Tue, 6 Sep 2016 06:38:44 +0200 Subject: [PATCH 30/35] 4.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5f15c6..899f6ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-pouch", - "version": "4.0.0", + "version": "4.0.1", "description": "PouchDB adapter for Ember Data", "directories": { "doc": "doc", From 2f0c8a30ea73eecc09e69cbe801635ea1defdadc Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Wed, 14 Sep 2016 23:02:49 +1000 Subject: [PATCH 31/35] Added option to disable live sync and support for periodic polling for changes --- README.md | 20 ++- addon/adapters/pouch.js | 90 ++++++++++--- tests/dummy/app/adapters/hot-sauce.js | 14 +++ tests/dummy/app/models/hot-sauce.js | 6 + tests/helpers/async.js | 11 ++ .../pouch-default-change-watcher-test.js | 12 +- .../adapters/without-live-sync-test.js | 118 ++++++++++++++++++ 7 files changed, 242 insertions(+), 29 deletions(-) create mode 100644 tests/dummy/app/adapters/hot-sauce.js create mode 100644 tests/dummy/app/models/hot-sauce.js create mode 100644 tests/helpers/async.js create mode 100644 tests/integration/adapters/without-live-sync-test.js diff --git a/README.md b/README.md index 1fd9e04..e46f28f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [**Changelog**](#changelog) -Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 2.0+. For older Ember Data versions use Ember Pouch version 3.22. +Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 2.0+. For older Ember Data versions use Ember Pouch version 3.22. With Ember Pouch, all of your app's data is automatically saved on the client-side using IndexedDB or WebSQL, and you just keep using the regular [Ember Data `store` API](http://emberjs.com/api/data/classes/DS.Store.html#method_all). This data may be automatically synced to a remote CouchDB (or compatible servers) using PouchDB replication. @@ -99,6 +99,24 @@ PouchDB.debug.enable('*'); See the [PouchDB sync API](http://pouchdb.com/api.html#sync) for full usage instructions. +### Disable live sync + +PouchDB's live sync uses a long-polling socket, so you may find that you hit the browser's +limit of HTTP connections to a given host. In this case you may find that a periodic +sync is better for your needs. The following will poll for changes rather than using +live sync. + +```js +export default Adapter.extend({ + db: db, + liveSync: false, + syncInterval: 5000 +}); +``` + +You can also sync manually by calling `adapter.sync()`, which returns a Promise that is +fulfilled on the `complete` event from PouchDB. + ## EmberPouch Blueprints ### Model diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index 310ea60..05e9f8b 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -6,43 +6,104 @@ import { } from 'ember-pouch/utils'; const { + assert, run: { - bind + bind, + later, + cancel }, on, + observer, String: { pluralize, camelize, classify - } + }, + RSVP } = Ember; export default DS.RESTAdapter.extend({ coalesceFindRequests: true, + liveSync: true, + syncInterval: null, + + changes: null, + syncTimer: null, + lastSeq: null, // The change listener ensures that individual records are kept up to date // when the data in the database changes. This makes ember-data 2.0's record // reloading redundant. shouldReloadRecord: function () { return false; }, shouldBackgroundReloadRecord: function () { return false; }, - _onInit : on('init', function() { - this._startChangesToStoreListener(); - }), - _startChangesToStoreListener: function () { + _liveSyncHandler: observer('liveSync', 'db', function () { + if (this.changes) { + this.changes.cancel(); + } var db = this.get('db'); - if (db) { + if (this.get('liveSync') && db) { this.changes = db.changes({ since: 'now', live: true, returnDocs: false }).on('change', bind(this, 'onChange')); } + }), + _syncIntervalHandler: observer('syncInterval', 'db', function () { + cancel(this.syncTimer); + if (this.get('syncInterval')) { + assert("Only one of liveSync or syncInterval should be used for a given adapter", + !this.get('liveSync') + ); + this._scheduleSync(); + } + }), + _scheduleSync() { + cancel(this.syncTimer); + this.syncTimer = later(() => { + this.sync(); + this._scheduleSync(); + }, this.get('syncInterval')); }, - changeDb: function(db) { + sync() { + var db = this.get('db'); + if (!db) { + throw new Error("Can't sync without a db"); + } + return (this.lastSeq ? RSVP.resolve(this.lastSeq) : + db.info().then(info => info.update_seq) + ).then(sinceSeq => new RSVP.Promise((resolve, reject) => { + if (this.syncChanges) { + this.syncChanges.cancel(); + } + this.syncChanges = db.changes({ + since: sinceSeq, + returnDocs: false + }).on('change', bind(this, 'onChange')) + .on('complete', ev => { + this.lastSeq = ev.last_seq; + resolve(ev); + }) + .on('error', reject); + })); + }, + _startSyncing: on('init', function() { + this._liveSyncHandler(); + this._syncIntervalHandler(); + }), + _stopSyncing() { if (this.changes) { this.changes.cancel(); } - + if (this.syncTimer) { + cancel(this.syncTimer); + } + if (this.syncChanges) { + this.syncChanges.cancel(); + } + }, + changeDb: function(db) { + this._stopSyncing(); var store = this.store; var schema = this._schema || []; @@ -52,7 +113,6 @@ export default DS.RESTAdapter.extend({ this._schema = null; this.set('db', db); - this._startChangesToStoreListener(); }, onChange: function (change) { // If relational_pouch isn't initialized yet, there can't be any records @@ -107,9 +167,7 @@ export default DS.RESTAdapter.extend({ }, willDestroy: function() { - if (this.changes) { - this.changes.cancel(); - } + this._stopSyncing(); }, _init: function (store, type) { @@ -303,10 +361,8 @@ export default DS.RESTAdapter.extend({ var rows = payload.docs.map((row) => { var parsedId = db.rel.parseDocID(row._id); - if (!Ember.isEmpty(parsedId.id)) { - row.data.id = parsedId.id; - return row.data; - } + row.data.id = parsedId.id; + return row.data; }); results[plural] = rows; diff --git a/tests/dummy/app/adapters/hot-sauce.js b/tests/dummy/app/adapters/hot-sauce.js new file mode 100644 index 0000000..fb006b7 --- /dev/null +++ b/tests/dummy/app/adapters/hot-sauce.js @@ -0,0 +1,14 @@ +import { Adapter } from 'ember-pouch/index'; +import PouchDB from 'pouchdb'; + +function createDb() { + return new PouchDB('hot-sauces'); +} + +export default Adapter.extend({ + liveSync: false, + init() { + this._super(...arguments); + this.set('db', createDb()); + } +}); diff --git a/tests/dummy/app/models/hot-sauce.js b/tests/dummy/app/models/hot-sauce.js new file mode 100644 index 0000000..a67f03e --- /dev/null +++ b/tests/dummy/app/models/hot-sauce.js @@ -0,0 +1,6 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + rev: DS.attr('string'), + name: DS.attr('string') +}); diff --git a/tests/helpers/async.js b/tests/helpers/async.js new file mode 100644 index 0000000..808f6d2 --- /dev/null +++ b/tests/helpers/async.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +export function promiseToRunLater(callback, timeout) { + return new Ember.RSVP.Promise((resolve) => { + Ember.run.later(() => { + callback(); + resolve(); + }, timeout); + }); +} + diff --git a/tests/integration/adapters/pouch-default-change-watcher-test.js b/tests/integration/adapters/pouch-default-change-watcher-test.js index cf0285e..7e0fe43 100644 --- a/tests/integration/adapters/pouch-default-change-watcher-test.js +++ b/tests/integration/adapters/pouch-default-change-watcher-test.js @@ -1,6 +1,6 @@ import { test } from 'qunit'; import moduleForIntegration from '../../helpers/module-for-acceptance'; - +import { promiseToRunLater } from '../../helpers/async'; import Ember from 'ember'; /* @@ -23,15 +23,6 @@ moduleForIntegration('Integration | Adapter | Default Change Watcher', { } }); -function promiseToRunLater(callback, timeout) { - return new Ember.RSVP.Promise((resolve) => { - Ember.run.later(() => { - callback(); - resolve(); - }, timeout); - }); -} - test('a loaded instance automatically reflects directly-made database changes', function (assert) { assert.expect(2); var done = assert.async(); @@ -193,4 +184,3 @@ test('a new record is automatically loaded', function (assert) { }, 15); }).finally(done); }); - diff --git a/tests/integration/adapters/without-live-sync-test.js b/tests/integration/adapters/without-live-sync-test.js new file mode 100644 index 0000000..37dcb04 --- /dev/null +++ b/tests/integration/adapters/without-live-sync-test.js @@ -0,0 +1,118 @@ +import Ember from 'ember'; +import { test } from 'qunit'; +import moduleForIntegration from '../../helpers/module-for-acceptance'; +import { promiseToRunLater } from '../../helpers/async'; + +moduleForIntegration('Integration | Adapter | Without live sync', { + beforeEach(assert) { + var done = assert.async(); + + this.adapter = function adapter() { + return this.store().adapterFor('hot-sauce'); + }; + this.db = function db() { + return this.adapter().get('db'); + }; + Ember.RSVP.Promise.resolve().then(() => { + return this.db().bulkDocs([ + { _id: 'hotSauce_2_A', data: { name: 'Cholula' } }, + { _id: 'hotSauce_2_B', data: { name: 'Melbourne Hot Sauce' } }, + ]); + }).finally(done); + }, + afterEach(assert) { + var done = assert.async(); + this.db().destroy().then(() => { + Ember.run(() => this.adapter().destroy()); + done(); + }); + } +}); + +test('changes are not synced', function (assert) { + assert.expect(2); + var done = assert.async(); + + Ember.RSVP.resolve().then(() => { + return this.store().find('hot-sauce', 'A'); + }).then((hotSauce) => { + assert.equal('Cholula', hotSauce.get('name'), + 'the loaded instance should reflect the initial test data'); + + return this.db().get('hotSauce_2_A'); + }).then((hotSauceRecord) => { + hotSauceRecord.data.name = 'Death Sauce'; + return this.db().put(hotSauceRecord); + }).then(() => { + return promiseToRunLater(() => { + var alreadyLoadedHotSauce = this.store().peekRecord('hot-sauce', 'A'); + assert.equal(alreadyLoadedHotSauce.get('name'), 'Cholula', + 'the loaded instance should not automatically reflect the change in the database'); + }, 15); + }).finally(done); +}); + +test('changes can be manually synced', function (assert) { + assert.expect(3); + var done = assert.async(); + + this.adapter().set('liveSync', false); + Ember.RSVP.resolve().then(() => { + return this.adapter().sync(); // perform initial sync to get update_seq + }).then(() => { + return this.store().find('hot-sauce', 'A'); + }).then((hotSauce) => { + assert.equal('Cholula', hotSauce.get('name'), + 'the loaded instance should reflect the initial test data'); + return this.db().get('hotSauce_2_A'); + }).then((hotSauceRecord) => { + hotSauceRecord.data.name = 'Death Sauce'; + return this.db().put(hotSauceRecord); + }).then(() => { + return this.store().find('hot-sauce', 'A'); + }).then((hotSauce) => { + assert.equal('Cholula', hotSauce.get('name'), + 'the loaded instance does not reflect the changes'); + return this.adapter().sync(); + }) + .then(() => { + return promiseToRunLater(() => { + var alreadyLoadedHotSauce = this.store().peekRecord('hot-sauce', 'A'); + assert.equal(alreadyLoadedHotSauce.get('name'), 'Death Sauce', + 'the loaded instance reflects the change in the database'); + }, 500); + }).finally(done); +}); + +test('changes can be synced periodically', function (assert) { + assert.expect(3); + var done = assert.async(); + + this.adapter().set('liveSync', false); + Ember.RSVP.resolve().then(() => { + return this.adapter().sync(); // perform initial sync to get update_seq + }).then(() => { + return this.store().find('hot-sauce', 'A'); + }).then((hotSauce) => { + assert.equal('Cholula', hotSauce.get('name'), + 'the loaded instance should reflect the initial test data'); + return this.db().get('hotSauce_2_A'); + }).then((hotSauceRecord) => { + hotSauceRecord.data.name = 'Death Sauce'; + return this.db().put(hotSauceRecord); + }).then(() => { + return this.store().find('hot-sauce', 'A'); + }).then((hotSauce) => { + assert.equal('Cholula', hotSauce.get('name'), + 'the loaded instance does not reflect the changes'); + this.adapter().set('syncInterval', 100); + }) + .then(() => { + return promiseToRunLater(() => { + var alreadyLoadedHotSauce = this.store().peekRecord('hot-sauce', 'A'); + assert.equal(alreadyLoadedHotSauce.get('name'), 'Death Sauce', + 'the loaded instance reflects the change in the database'); + }, 500); + }).finally(done); +}); + From 9dc00e70094808ee5ebe59798ab79f17111f74a0 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Wed, 14 Sep 2016 23:04:41 +1000 Subject: [PATCH 32/35] Improve README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e46f28f..643d7d8 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ PouchDB.debug.enable('*'); See the [PouchDB sync API](http://pouchdb.com/api.html#sync) for full usage instructions. -### Disable live sync +### Live sync PouchDB's live sync uses a long-polling socket, so you may find that you hit the browser's limit of HTTP connections to a given host. In this case you may find that a periodic From 741bd75c956c456b0128e52334aba499caf2c80a Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Wed, 14 Sep 2016 23:26:21 +1000 Subject: [PATCH 33/35] Allow sync() to be used in conjunction with liveSync --- addon/adapters/pouch.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index 05e9f8b..a8e6bfa 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -27,7 +27,8 @@ export default DS.RESTAdapter.extend({ liveSync: true, syncInterval: null, - changes: null, + liveChanges: null, + syncChanges: null, syncTimer: null, lastSeq: null, @@ -37,12 +38,12 @@ export default DS.RESTAdapter.extend({ shouldReloadRecord: function () { return false; }, shouldBackgroundReloadRecord: function () { return false; }, _liveSyncHandler: observer('liveSync', 'db', function () { - if (this.changes) { - this.changes.cancel(); + if (this.liveChanges) { + this.liveChanges.cancel(); } var db = this.get('db'); if (this.get('liveSync') && db) { - this.changes = db.changes({ + this.liveChanges = db.changes({ since: 'now', live: true, returnDocs: false @@ -79,12 +80,13 @@ export default DS.RESTAdapter.extend({ this.syncChanges = db.changes({ since: sinceSeq, returnDocs: false - }).on('change', bind(this, 'onChange')) - .on('complete', ev => { - this.lastSeq = ev.last_seq; - resolve(ev); - }) - .on('error', reject); + }).on('complete', ev => { + this.lastSeq = ev.last_seq; + resolve(ev); + }).on('error', reject); + if (!this.get('liveSync')) { + this.syncChanges.on('change', bind(this, 'onChange')); + } })); }, _startSyncing: on('init', function() { @@ -92,8 +94,8 @@ export default DS.RESTAdapter.extend({ this._syncIntervalHandler(); }), _stopSyncing() { - if (this.changes) { - this.changes.cancel(); + if (this.liveChanges) { + this.liveChanges.cancel(); } if (this.syncTimer) { cancel(this.syncTimer); From 60f4e92326ca4589564b9ff0ab8dc983440a2fa5 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Thu, 15 Sep 2016 08:10:20 +1000 Subject: [PATCH 34/35] Improve README --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 643d7d8..68c931a 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,11 @@ See the [PouchDB sync API](http://pouchdb.com/api.html#sync) for full usage inst PouchDB's live sync uses a long-polling socket, so you may find that you hit the browser's limit of HTTP connections to a given host. In this case you may find that a periodic -sync is better for your needs. The following will poll for changes rather than using -live sync. +sync is better for your needs. + +If you are connecting directly to a database over HTTP without syncing to a local database +then ember-pouch's change detection will use a long-polling socket. The following will +poll for changes rather than using live sync. ```js export default Adapter.extend({ From b986642c1246dbc46a49dab3224752b425fbd427 Mon Sep 17 00:00:00 2001 From: Simon Wade Date: Fri, 23 Sep 2016 11:39:10 +1000 Subject: [PATCH 35/35] Fix Typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 68c931a..8612b17 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [**Changelog**](#changelog) -Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 2.0+. For older Ember Data versions use Ember Pouch version 3.22. +Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 2.0+. For older Ember Data versions use Ember Pouch version 3.2.2. With Ember Pouch, all of your app's data is automatically saved on the client-side using IndexedDB or WebSQL, and you just keep using the regular [Ember Data `store` API](http://emberjs.com/api/data/classes/DS.Store.html#method_all). This data may be automatically synced to a remote CouchDB (or compatible servers) using PouchDB replication. @@ -106,7 +106,7 @@ limit of HTTP connections to a given host. In this case you may find that a peri sync is better for your needs. If you are connecting directly to a database over HTTP without syncing to a local database -then ember-pouch's change detection will use a long-polling socket. The following will +then ember-pouch's change detection will use a long-polling socket. The following will poll for changes rather than using live sync. ```js