Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dontsave hasmany #159

Merged
merged 27 commits into from
Nov 28, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fe105be
pouchdb find with indexes for hasmany where possible
jlami Oct 19, 2016
0958207
camelize model name for find and change unit tests to remove hasMany …
jlami Oct 19, 2016
c7fb200
simpler selector, unit tests also deleteIndex required
jlami Oct 20, 2016
6de0e2d
options.dontsave for hasMany relationships to prevent saving and asyn…
jlami Oct 21, 2016
a0bcd1f
_shouldSerializeHasMany with dontsave option
jlami Oct 25, 2016
5550999
nested module for save and dontsave hasmany tests
jlami Oct 26, 2016
20470c5
hasMany in bulkdocs based on config
jlami Oct 27, 2016
3e2b2e3
relationships links in ember-pouch instead of relational-pouch
jlami Oct 27, 2016
d25a279
findHasMany to relational-pouch for synchronous lookup
jlami Oct 27, 2016
8ae2470
sync/async tests
jlami Oct 28, 2016
28f7d52
timeout tests
jlami Oct 28, 2016
e5bc313
promise serialised?
jlami Oct 30, 2016
3893851
JSHint cleanup
jlami Oct 30, 2016
03a86e7
relationa-pouch dontsave-hasmany branch
jlami Oct 30, 2016
1c3edfb
travis fix: no getOwner for ember < 2.3
jlami Oct 30, 2016
f71bfbf
getOwner through ember-getowner-polyfill
jlami Oct 30, 2016
12be91b
use config for serializer too
jlami Nov 1, 2016
487dcec
only use link for hasMany relationships
jlami Nov 2, 2016
73e1823
dont add hasmany to schema if dontsave&&async, queryInverse for donts…
jlami Nov 3, 2016
11a54c5
include all relationships to build schema, parseRelDoc for findhasman…
jlami Nov 3, 2016
3c2687c
jshint missing ;
jlami Nov 3, 2016
2f40539
no async options on model
jlami Nov 3, 2016
a52c870
test base Model for setting async and dontsave on create, since appli…
jlami Nov 4, 2016
bf0fc82
set relation config on base adapter instead of model
jlami Nov 4, 2016
dd13380
local dev cleanup
jlami Nov 6, 2016
730dea5
relational-pouch 1.4.5
jlami Nov 21, 2016
958ee06
dontsave hasMany documentation
jlami Nov 28, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,19 @@ ENV.emberPouch.remoteDb = 'http://localhost:5984/my_couch';

EmberPouch supports both `hasMany` and `belongsTo` relationships.

### Saving
### Don't save hasMany child ids

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](https://github.com/nolanlawson/relational-pouch#dont-save-hasmany)

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.

### Saving child ids

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

#### Adding entries

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.

Expand Down Expand Up @@ -163,7 +175,7 @@ export default Ember.Route.extend({

```

### Removing
#### Removing child ids

When removing a `hasMany` - `belongsTo` relationship, the children must be removed prior to the parent being removed.

Expand Down Expand Up @@ -498,6 +510,9 @@ This project was originally based on the [ember-data-hal-adapter](https://github
And of course thanks to all our wonderful contributors, [here](https://github.com/nolanlawson/ember-pouch/graphs/contributors) and [in Relational Pouch](https://github.com/nolanlawson/relational-pouch/graphs/contributors)!

## Changelog
* **4.1.0**
- async is now true when not specified for relationships
- hasMany relationship can have option dontsave
* **4.0.3**
- Fixes [#158](https://github.com/nolanlawson/ember-pouch/pull/158)
* **4.0.2**
Expand Down
79 changes: 51 additions & 28 deletions addon/adapters/pouch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Ember from 'ember';
import DS from 'ember-data';
import getOwner from 'ember-getowner-polyfill';

import {
extractDeleteRecord
Expand Down Expand Up @@ -146,10 +147,11 @@ export default DS.RESTAdapter.extend({
if (type.documentType) {
schemaDef['documentType'] = type.documentType;
}


let config = getOwner(this).resolveRegistration('config:environment');
let dontsavedefault = config['emberpouch'] && config['emberpouch']['dontsavehasmany'];
// else it's new, so update
this._schema.push(schemaDef);

// check all the subtypes
// We check the type of `rel.type`because with ember-data beta 19
// `rel.type` switched from DS.Model to string
Expand All @@ -161,14 +163,40 @@ export default DS.RESTAdapter.extend({
var relDef = {},
relModel = (typeof rel.type === 'string' ? store.modelFor(rel.type) : rel.type);
if (relModel) {
relDef[rel.kind] = {
type: self.getRecordTypeName(relModel),
options: rel.options
};
if (!schemaDef.relations) {
schemaDef.relations = {};
let includeRel = true;
rel.options = rel.options || {};
if (typeof(rel.options.async) === "undefined") {
rel.options.async = config.emberpouch && !Ember.isEmpty(config.emberpouch.async) ? config.emberpouch.async : true;//default true from https://github.com/emberjs/data/pull/3366
}
let options = Object.create(rel.options);
if (rel.kind === 'hasMany' && (options.dontsave || typeof(options.dontsave) === 'undefined' && dontsavedefault)) {
let inverse = type.inverseFor(rel.key, store);
if (inverse) {
if (inverse.kind === 'belongsTo') {
self.get('db').createIndex({index: { fields: ['data.' + inverse.name, '_id'] }});
if (options.async) {
includeRel = false;
} else {
options.queryInverse = inverse.name;
}
} else {
console.warn(type.modelName + " has a relationship with name " + rel.key + " that is many to many with type " + rel.type + ". This is not supported");
}
} else {
console.warn(type.modelName + " has a hasMany relationship with name " + rel.key + " that has no inverse.");
}
}
schemaDef.relations[rel.key] = relDef;

if (includeRel) {
relDef[rel.kind] = {
type: self.getRecordTypeName(relModel),
options: options
};
if (!schemaDef.relations) {
schemaDef.relations = {};
}
schemaDef.relations[rel.key] = relDef;
}
self._init(store, relModel);
}
});
Expand Down Expand Up @@ -280,7 +308,19 @@ export default DS.RESTAdapter.extend({
this._init(store, type);
return this.get('db').rel.find(this.getRecordTypeName(type), ids);
},


findHasMany: function(store, record, link, rel) {
let inverse = record.type.inverseFor(rel.key, store);
if (inverse && inverse.kind === 'belongsTo') {
return this.get('db').rel.findHasMany(camelize(rel.type), inverse.name, record.id);
}
else {
console.warn("Can't find " + rel.key);
let result = {};
result[pluralize(rel.type)] = [];
return result;//data;
}
},

query: function(store, type, query) {
this._init(store, type);
Expand All @@ -296,24 +336,7 @@ export default DS.RESTAdapter.extend({
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;
}
});
return db.find(queryParams).then(pouchRes => db.rel.parseRelDocs(recordTypeName, pouchRes.docs));
},

queryRecord: function(store, type, query) {
Expand Down
47 changes: 38 additions & 9 deletions addon/serializers/pouch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Ember from 'ember';
import DS from 'ember-data';
import getOwner from 'ember-getowner-polyfill';

const {
get,
Expand All @@ -8,20 +9,36 @@ const keys = Object.keys || Ember.keys;
const assign = Object.assign || Ember.assign;

export default DS.RESTSerializer.extend({
_shouldSerializeHasMany: function() {
return true;

init: function() {
this._super(...arguments);

let config = getOwner(this).resolveRegistration('config:environment');
this.dontsavedefault = config['emberpouch'] && config['emberpouch']['dontsavehasmany'];
},

_getDontsave(relationship) {
return !Ember.isEmpty(relationship.options.dontsave) ? relationship.options.dontsave : this.dontsavedefault;
},

_shouldSerializeHasMany: function(snapshot, key, relationship) {
let dontsave = this._getDontsave(relationship);
let result = !dontsave;
return result;
},

// This fixes a failure in Ember Data 1.13 where an empty hasMany
// was saving as undefined rather than [].
serializeHasMany(snapshot, json, relationship) {
this._super.apply(this, arguments);

const key = relationship.key;

if (!json[key]) {
json[key] = [];
}
if (this._shouldSerializeHasMany(snapshot, relationship.key, relationship)) {
this._super.apply(this, arguments);

const key = relationship.key;

if (!json[key]) {
json[key] = [];
}
}
},

_isAttachment(attribute) {
Expand Down Expand Up @@ -66,5 +83,17 @@ export default DS.RESTSerializer.extend({
}
});
return attributes;
},

extractRelationships(modelClass) {
let relationships = this._super(...arguments);

modelClass.eachRelationship((key, relationshipMeta) => {
if (relationshipMeta.kind === 'hasMany' && this._getDontsave(relationshipMeta) && !!relationshipMeta.options.async) {
relationships[key] = { links: { related: key } };
}
});

return relationships;
}
});
2 changes: 1 addition & 1 deletion blueprints/ember-pouch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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.5'},
{ name: 'pouchdb-find', target: '^0.10.2'}
]);
}
Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ember": "~2.8.0",
"ember-cli-shims": "0.1.1",
"pouchdb": "^5.4.5",
"relational-pouch": "^1.4.4",
"relational-pouch": "^1.4.5",
"pouchdb-find": "^0.10.3",
"phantomjs-polyfill-object-assign": "chuckplantain/phantomjs-polyfill-object-assign"
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"ember-data": "^2.8.0",
"ember-disable-prototype-extensions": "^1.1.0",
"ember-export-application-global": "^1.0.5",
"ember-getowner-polyfill": "1.0.1",
"ember-load-initializers": "^0.5.1",
"ember-resolver": "^2.0.3",
"loader.js": "^4.0.1"
Expand Down
14 changes: 14 additions & 0 deletions tests/dummy/app/adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Adapter} from 'ember-pouch';
import config from 'dummy/config/environment';

export default Adapter.extend({
_init(store, type) {
type.eachRelationship((name, rel) => {
rel.options.async = config.emberpouch.async;
if (rel.kind === 'hasMany') {
rel.options.dontsave = config.emberpouch.dontsavehasmany;
}
});
this._super(...arguments);
},
});
2 changes: 1 addition & 1 deletion tests/dummy/app/adapters/application.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Adapter } from 'ember-pouch';
import Adapter from 'dummy/adapter';
import PouchDB from 'pouchdb';
import config from 'dummy/config/environment';
import Ember from 'ember';
Expand Down
2 changes: 1 addition & 1 deletion tests/dummy/app/adapters/taco-salad.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Adapter } from 'ember-pouch/index';
import Adapter from 'dummy/adapter';
import PouchDB from 'pouchdb';
import config from 'dummy/config/environment';
import Ember from 'ember';
Expand Down
7 changes: 3 additions & 4 deletions tests/dummy/app/models/food-item.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import DS from 'ember-data';
import {Model} from 'ember-pouch';

// N.b.: awkward model name is to test getRecordTypeName

export default DS.Model.extend({
rev: DS.attr('string'),

export default Model.extend({
name: DS.attr('string'),
soup: DS.belongsTo('taco-soup', { async: true })
soup: DS.belongsTo('taco-soup')
});
5 changes: 2 additions & 3 deletions tests/dummy/app/models/smasher.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import DS from 'ember-data';
import {Model} from 'ember-pouch';

export default DS.Model.extend({
rev: DS.attr('string'),

export default Model.extend({
name: DS.attr('string'),
series: DS.attr('string'),
debut: DS.attr(),
Expand Down
5 changes: 2 additions & 3 deletions tests/dummy/app/models/taco-recipe.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import DS from 'ember-data';
import {Model} from 'ember-pouch';

export default DS.Model.extend({
rev: DS.attr('string'),

export default Model.extend({
coverImage: DS.attr('attachment'),
photos: DS.attr('attachments')
});
7 changes: 3 additions & 4 deletions tests/dummy/app/models/taco-salad.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import DS from 'ember-data';
import {Model} from 'ember-pouch';

export default DS.Model.extend({
rev: DS.attr('string'),

export default Model.extend({
flavor: DS.attr('string'),
ingredients: DS.hasMany('food-item', { async: true })
ingredients: DS.hasMany('food-item')
});
7 changes: 3 additions & 4 deletions tests/dummy/app/models/taco-soup.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import DS from 'ember-data';
import {Model} from 'ember-pouch';

export default DS.Model.extend({
rev: DS.attr('string'),

export default Model.extend({
flavor: DS.attr('string'),
ingredients: DS.hasMany('food-item', { async: true })
ingredients: DS.hasMany('food-item')
});
23 changes: 17 additions & 6 deletions tests/helpers/module-for-pouch-acceptance.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@ import config from 'dummy/config/environment';
import Ember from 'ember';
/* globals PouchDB */

export default function(name, options = {}) {
export default function(name, options = {}, nested = undefined) {
module(name, {
beforeEach(assert) {
var done = assert.async();

Ember.RSVP.Promise.resolve().then(() => {
return (new PouchDB(config.emberpouch.localDb)).destroy();

let db = new PouchDB(config.emberpouch.localDb);

return db.getIndexes().then(data => {
return Ember.RSVP.all(data.indexes.map(
index => {
return index.ddoc ? (db.deleteIndex(index)) : (Ember.RSVP.resolve());
}));
}).then(() => db.destroy());
}).then(() => {
this.application = startApp();
this.application = startApp();

this.lookup = function (item) {
return this.application.__container__.lookup(item);
Expand All @@ -38,15 +46,18 @@ export default function(name, options = {}) {
if (options.beforeEach) {
options.beforeEach.apply(this, arguments);
}
}).finally(done);
done();
});
},

afterEach() {
destroyApp(this.application);
if (this.application) {
destroyApp(this.application);
}

if (options.afterEach) {
options.afterEach.apply(this, arguments);
}
}
});
}, nested);
}
Loading