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. This data may be automatically synced to a remote CouchDB (or compatible servers) using PouchDB replication.
What's the point?
-
You don't need to write any server-side logic. Just use CouchDB.
-
Data syncs automatically.
-
Your app works offline, and requests are super fast, because they don't need the network.
For more on PouchDB, check out pouchdb.com.
ember install ember-pouch
For ember-data < 2.0:
ember install ember-pouch@3.2.2
For ember-cli < 1.13.0:
npm install ember-pouch@3.2.2 --save-dev
This provides
import PouchDB from 'pouchdb'
import {Model, Adapter, Serializer} from 'ember-pouch'
Ember-Pouch
requires you to add a rev: DS.attr('string')
field to all your models. This is for PouchDB/CouchDB to handle revisions:
// app/models/todo.js
import DS from 'ember-data';
export default DS.Model.extend({
title : DS.attr('string'),
isCompleted : DS.attr('boolean'),
rev : DS.attr('string') // <-- Add this to all your models
});
If you like, you can also use Model
from Ember-Pouch
that ships with the rev
attribute:
// app/models/todo.js
import DS from 'ember-data';
import { Model } from 'ember-pouch';
export default Model.extend({
title : DS.attr('string'),
isCompleted : DS.attr('boolean')
});
A local PouchDB that syncs with a remote CouchDB looks like this:
// app/adapters/application.js
import PouchDB from 'pouchdb';
import { Adapter } from 'ember-pouch';
var remote = new PouchDB('http://localhost:5984/my_couch');
var db = new PouchDB('local_pouch');
db.sync(remote, {
live: true, // do a live, ongoing sync
retry: true // retry if the connection is lost
});
export default Adapter.extend({
db: db
});
You can also turn on debugging:
import PouchDB from 'pouchdb';
PouchDB.debug.enable('*');
See the PouchDB sync API for full usage instructions.
In order to create a model run the following command from the command line:
ember g pouch-model <model-name>
Replace <model-name>
with the name of your model and the file will automatically be generated for you.
You can now create an adapter using ember-cli's blueprint functionality. Once you've installed ember-pouch
into your ember-cli app you can run the following command to automatically generate an application adapter.
ember g pouch-adapter application
Now you can store your localDb and remoteDb names in your ember-cli's config. Just add the following keys to the ENV
object:
ENV.emberPouch.localDb = 'test';
ENV.emberPouch.remoteDb = 'http://localhost:5984/my_couch';
EmberPouch supports both hasMany
and belongsTo
relationships.
To be more in line with the normal ember data way of saving hasMany
- belongsTo
relationships, ember-pouch now has an option to not save the child ids on the hasMany
side. This prevents the extra need to save the hasMany
side as explained below. For a more detailed explanation please read the relational-pouch documentation
This new mode can be selected for a hasMany
relationship by specifying the option dontsave: true
on the relationship. An application wide setting named ENV.emberpouch.dontsavehasmany
can also be set to true
to make all hasMany
relationships behave this way.
Using this mode does impose a slight runtime overhead, since this will use db.find
and database indexes to search for the child ids. The indexes are created automatically for you. But large changes to the model might require you to clean up old, unused indexes.
When you do save child ids on the hasMany
side, you have to follow the directions below to make sure the data is saved correctly.
When saving a hasMany
- belongsTo
relationship, both sides of the relationship (the child and the parent) must be saved. Note that the parent needs to have been saved at least once prior to adding children to it.
// app/routes/post/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model(params){
//We are getting a post that already exists
return this.store.findRecord('post', params.post_id);
},
actions:{
addComment(comment, author){
//Create the comment
const comment = this.store.createRecord('comment',{
comment: comment,
author: author
});
//Get our post
const post = this.controller.get('model');
//Add our comment to our existing post
post.get('comments').pushObject(comment);
//Save the child then the parent
comment.save().then(() => post.save());
}
}
});
When removing a hasMany
- belongsTo
relationship, the children must be removed prior to the parent being removed.
// app/routes/posts/admin/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model(){
//We are getting all posts for some sort of list
return this.store.findAll('post');
},
actions:{
deletePost(post){
//collect the promises for deletion
let deletedComments = [];
//get and destroy the posts comments
post.get('comments').then((comments) => {
comments.map((comment) => {
deletedComments.push(comment.destroyRecord());
});
});
//Wait for comments to be destroyed then destroy the post
Ember.RSVP.all(deletedComments).then(() => {
post.destroyRecord();
});
}
}
});
query and queryRecord are relying on pouchdb-find
Create an index if it doesn't exist.
// app/adapters/application.js
function createDb() {
...
db.createIndex({
index: {
fields: ['data.name']
}
}).then((result) => {
// {'result': 'created'} index was created
});
return db;
};
Find all docs where doc.name === 'Mario'
// app/routes/smasher/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.query('smasher', {
filter: { name: 'Mario' }
});
}
});
Find all docs where doc.name === 'Mario' and doc.debut > 1990:
// app/routes/smasher/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.query('smasher', {
filter: {
name: 'Mario'
debut: { $gt: 1990 }
}
});
}
});
Sorted by doc.debut descending.
// app/routes/smasher/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.query('smasher', {
filter: {
name: 'Mario',
debut: { '$gte': null }
},
sort: [
{ debut: 'desc' }
]
})
}
});
Note that this query would require a custom index including both fields data.name
and data.debut
. Any field in sort
must also be included in filter
. Only $eq
, $gt
, $gte
, $lt
, and $lte
can be used when matching a custom index.
Find one document where doc.name === 'Mario'
// app/routes/smasher/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.queryRecord('smasher', {
filter: { name: 'Mario' }
});
}
});
Ember-Pouch
provides an attachments
transform for your models, which makes working with attachments as simple as working with any other field.
Add a DS.attr('attachments')
field to your model. Provide a default value for it to be an empty array.
// myapp/models/photo-album.js
export default DS.Model.extend({
photos: DS.attr('attachments', {
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.
Attach new files by adding an Ember.Object
with a .name
, .content_type
and .data
to array of attachments.
// 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`
}));
Tom Dale's blog example using Ember CLI and EmberPouch: broerse/ember-cli-blog
Currently PouchDB doesn't use LocalStorage unless you include an experimental plugin. Amazingly, this is only necessary to support IE ≤ 9.0 and Opera Mini. It's recommended you read more about this, what storage mechanisms modern browsers now support, and using SQLite in Cordova on the PouchDB adapters page.
From day one, CouchDB and its protocol have been designed to be always Available and handle Partitioning over the network well (AP in the CAP theorem). PouchDB/CouchDB gives you a solid way to manage conflicts. It is "eventually consistent," but CouchDB has an API for listening to changes to the database, which can be then pushed down to the client in real-time.
To learn more about how CouchDB sync works, check out the PouchDB guide to replication.
Out of the box, ember-pouch includes a PouchDB change listener that automatically updates any records your app has loaded when they change due to a sync. It also unloads records that are removed due to a sync.
However, ember-pouch does not automatically load new records that arrive during a sync. The records are saved in the local database, but ember-data is not told to load them into memory. Automatically loading every new record works well with a small number of records and a limited number of models. As an app grows, automatically loading every record will negatively impact app responsiveness during syncs (especially the first sync). To avoid puzzling slowdowns, ember-pouch only automatically reloads records you have already used ember-data to load.
If you have a model or two that you know will always have a small number of records, you can tell ember-data to automatically load them into memory as they arrive. Your PouchAdapter subclass has a method unloadedDocumentChanged
, which is called when a document is received during sync that has not been loaded into the ember-data store. In your subclass, you can implement the following to load it automatically:
unloadedDocumentChanged: function(obj) {
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);
});
},
With PouchDB, you also get access to a whole host of PouchDB plugins.
For example, to use the pouchdb-authentication
plugin, follow the install instructions and import it in your Brocfile.js
:
app.import('bower_components/pouchdb-authentication/dist/pouchdb.authentication.js');
Ember Pouch is really just a thin layer of Ember-y goodness over Relational Pouch. Before you file an issue, check to see if it's more appropriate to file over there.
If you want to go completely offline-first, you'll also need an HTML5 appcache.manifest with broccoli-manifest. This will allow your HTML/CSS/JS assets to load even if the user is offline. Plus your users can "add to homescreen" on a mobile device (iOS/Android).
An easy way to secure your Ember Pouch-using app is to ensure that data can only be fetched from CouchDB – not from some other server (e.g. in an XSS attack).
You can use the content-security-policy plugin to enable Content Security Policy in Ember CLI. You also will have to set the CSP HTTP header on your backend in production.
To use, add a Content Security Policy whitelist entry to /config/environment.js
:
ENV.contentSecurityPolicy = {
"connect-src": "'self' http://your_couch_host.com:5984"
};
To automatically set up your remote CouchDB to use CORS, you can use the plugin add-cors-to-couchdb:
npm install -g add-cors-to-couchdb
add-cors-to-couchdb http://your_couch_host.com:5984 -u your_username -p your_password
Ember-data can be slow to load large numbers of records which have lots of relationships. If you run into this problem, you can define multiple models and have them all point to the same set of records by defining documentType
on the model class. Example (in an ember-cli app):
// app/models/post.js
import DS from 'ember-data';
import { Model } from 'ember-pouch';
export default Model.extend({
title: DS.attr('string'),
text: DS.attr('string'),
author: DS.belongsTo('author'),
comments: DS.hasMany('comments')
});
// app/models/post-summary.js
import DS from 'ember-data';
import { Model } from 'ember-pouch';
var PostSummary = Model.extend({
title: DS.attr('string'),
});
PostSummary.reopenClass({
documentType: 'post'
})
export default PostSummary;
The value for documentType
is the camelCase version of the primary model name.
For best results, only create/update records using the full model definition. Treat the others as read-only.
In some cases it might diserable (security related, where you want a given user to only have some informations stored on his computer) to have multiple databases for the same model of data.
Ember-Pouch
allows you to dynamically change the database a model is using by calling the function changeDb
on the adapter.
function changeProjectDatabase(dbName, dbUser, dbPassword) {
// CouchDB is serving at http://localhost:5455
let remote = new PouchDB('http://localhost:5455/' + dbName);
// here we are using pouchdb-authentication for credential supports
remote.login( dbUser, dbPassword).then(
function (user) {
let db = new PouchDB(dbName)
db.sync(remote, {live:true, retry:true})
// grab the adapter, it can be any ember-pouch adapter.
let adapter = this.store.adapterFor('project');
// this is where we told the adapter to change the current database.
adapter.changeDb(db);
}
)
}
git clone
this repositorynpm install
bower install
ember server
- Visit your app at http://localhost:4200.
ember test
ember test --server
ember build
For more information on using ember-cli, visit http://www.ember-cli.com/.
This project was originally based on the ember-data-hal-adapter by @locks, and I really benefited from his guidance during its creation.
And of course thanks to all our wonderful contributors, here and in Relational Pouch!
- 4.2.7
- Fix
_shouldSerializeHasMany
deprecation #184
- Fix
- 4.2.6
- 4.2.5
- 4.2.4
- Fix attachments typo in README #170
- 4.2.3
- Update pouchdb to the latest version
- Minor typofix #166
- 4.2.2
- Update pouchdb to the latest version
- 4.2.1
- Fix
Init
some more - Fix
Init
_super.Init
error
- Fix
- 4.2.0
- Switch to npm versions
- 4.1.0
- async is now true when not specified for relationships
- hasMany relationship can have option dontsave
- 4.0.3
- Fixes #158
- 4.0.2
- 4.0.0
- 3.2.2
- 3.2.1
- Fix(Addon): Call super in init #129
- 3.2.0
- Make adapter call a hook when encountering a change for a record that is not yet loaded #108
- 3.1.1
- Bugfix for hasMany relations by @backspace (#111).
- 3.1.0
- Database can now be dynamically switched on the adapter (#89). Thanks to @olivierchatry for this!
- Various bugfixes by @backspace, @jkleinsc, @rsutphin, @mattmarcum, @broerse, and @olivierchatry. See the full commit log for details. Thank you!
- 3.0.1
- Add blueprints for model and adapter (see above for details). Thanks @mattmarcum (#101, #102) and @backspace (#103).
- 3.0.0
- Update for compatibility with Ember & Ember-Data 2.0+. The adapter now supports Ember & Ember-Data 1.13.x and 2.x only.
- 2.0.3
- Use Ember.get to reference the PouchDB instance property in the adapter (
db
), allowing it to be injected (#84). Thanks to @jkleinsc! - Indicate to ember-data 1.13+ that reloading individual ember-pouch records is never necessary (due to the change watcher that keeps them up to date as they are modified) (#79, #83).
- Use Ember.get to reference the PouchDB instance property in the adapter (
- 2.0.2 - Use provide
findRecord
for ember-data 1.13 and later thanks to @OleRoel (#72) - 2.0.1 - Fixed #62 thanks to @rsutphin (deprecated
typekey
in Ember-Data 1.0.0-beta.18) - 2.0.0 - Ember CLI support, due to some amazing support by @fsmanuel! Bower and npm support are deprecated now; you are recommended to use Ember CLI instead.
- 1.2.5 - Last release with regular Bower/npm support via bundle javascript in the
dist/
directory. - 1.0.0 - First release