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
+
+ {{#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': 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'));
+});