diff --git a/README.md b/README.md index 95e31a8..b8aa5bc 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,43 @@ export default Ember.Route.extend({ } } }); +``` + +## Attachments + +`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. 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', { + defaultValue: function() { + return []; + } + }); +}); +``` +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': btoa('hello world') // base64-encoded `String`, or a DOM `Blob`, or a `File` +})); ``` ## Sample app diff --git a/addon/transforms/attachment.js b/addon/transforms/attachment.js new file mode 100644 index 0000000..9c7c875 --- /dev/null +++ b/addon/transforms/attachment.js @@ -0,0 +1,41 @@ +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 []; } + + 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: attachment.get('content_type'), + }; + if (attachment.get('stub')) { + serialized.stub = true; + serialized.length = attachment.get('length'); + serialized.digest = attachment.get('digest'); + } 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..5741eba --- /dev/null +++ b/tests/unit/transforms/attachment-test.js @@ -0,0 +1,74 @@ +import { moduleFor, test } from 'ember-qunit'; + +import Ember from 'ember'; + +let testSerializedData = { + 'hello.txt': { + content_type: 'text/plain', + data: 'aGVsbG8gd29ybGQ=', + digest: "md5-7mkg+nM0HN26sZkLN8KVSA==" + // CouchDB doesn't add 'length' + }, + 'stub.txt': { + stub: true, + content_type: 'text/plain', + digest: "md5-7mkg+nM0HN26sZkLN8KVSA==", + length: 11 + }, +}; + +let testDeserializedData = [ + Ember.Object.create({ + name: 'hello.txt', + content_type: 'text/plain', + data: 'aGVsbG8gd29ybGQ=', + digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==' + }), + Ember.Object.create({ + name: 'stub.txt', + content_type: 'text/plain', + stub: true, + digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', + length: 11 + }) +]; + +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); + assert.deepEqual(transform.serialize([]), {}); + + let serializedData = transform.serialize(testDeserializedData); + + 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); +}); + +test('it deserializes an attachment', function(assert) { + let transform = this.subject(); + assert.deepEqual(transform.deserialize(null), []); + assert.deepEqual(transform.deserialize(undefined), []); + + 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')); + 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')); +});