diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index 9aff528..5c6f1e8 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -1,6 +1,7 @@ import Ember from 'ember'; import DS from 'ember-data'; import { pluralize } from 'ember-inflector'; +import { v4 } from 'uuid'; //import BelongsToRelationship from 'ember-data/-private/system/relationships/state/belongs-to'; import { @@ -9,6 +10,20 @@ import { configFlagDisabled } from '../utils'; +function getRevFromSaveResult(records) { + let rev = null; + try { + rev = records[Object.keys(records)[0]][0].rev; + if (!rev || Object.keys(records).length > 1) { + // eslint-disable-next-line no-console + console.warn(`getRevFromSaveResult going to return ${rev}, but that may not be correct`); + } + } catch(e) { + throw Error(`Could not determine rev`); + } + return rev; +} + const { getOwner, run: { @@ -153,7 +168,7 @@ export default DS.RESTAdapter.extend({ willDestroy: function() { this._stopChangesListener(); }, - + _indexPromises: [], _init: function (store, type) { @@ -206,8 +221,9 @@ export default DS.RESTAdapter.extend({ relModel = (typeof rel.type === 'string' ? store.modelFor(rel.type) : rel.type); if (relModel) { let includeRel = true; - if (!('options' in rel)) rel.options = {}; - + if (!('options' in rel)) { + 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 } @@ -464,27 +480,47 @@ export default DS.RESTAdapter.extend({ }); }, + generateIdForRecord: function(/* store, type, inputProperties */) { + return v4(); + }, + createdRecords: {}, - createRecord: function(store, type, record) { - this._init(store, type); - var data = this._recordToData(store, type, record); - let rel = this.get('db').rel; - - let id = data.id; - if (!id) { - id = data.id = rel.uuid(); + createRecord: function(store, type, snapshot) { + const record = snapshot.record; + if (record._emberPouchSavePromise) { + const changes = record.changedAttributes(); + record._emberPouchSavePromise = record._emberPouchSavePromise.then(records => { + // If there have been changes since the document was created then we should update the record now + if (Object.keys(changes).length > 0) { + // Include latest rev to indicate that we're aware that data has changed since original request + // (otherwise a document update conflict error would be thrown by the DB) + snapshot._attributes.rev = getRevFromSaveResult(records); + return this.updateRecord(store, type, snapshot); + } + return records; + }); + return record._emberPouchSavePromise; } + + this._init(store, type); + var data = this._recordToData(store, type, snapshot); + const rel = this.get('db').rel; + const id = data.id; this.createdRecords[id] = true; - - return rel.save(this.getRecordTypeName(type), data).catch((e) => { - delete this.createdRecords[id]; - throw e; + Object.defineProperty(record, '_emberPouchSavePromise', { + enumerable: false, + writable: true, + value: rel.save(this.getRecordTypeName(type), data).catch((e) => { + delete this.createdRecords[id]; + throw e; + }), }); + return record._emberPouchSavePromise; }, - updateRecord: function (store, type, record) { + updateRecord: function (store, type, snapshot) { this._init(store, type); - var data = this._recordToData(store, type, record); + var data = this._recordToData(store, type, snapshot); return this.get('db').rel.save(this.getRecordTypeName(type), data); }, diff --git a/ember-cli-build.js b/ember-cli-build.js index 156e23f..03dfc9d 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -5,7 +5,15 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); module.exports = function(defaults) { let app = new EmberAddon(defaults, { - // Add options here + autoImport: { + webpack: { + node: { + global: true + } + }, + // We could use ember-auto-import for these, but index.js is already handling them + exclude: ['pouchdb', 'pouchdb-find', 'relational-pouch'] + } }); /* diff --git a/package.json b/package.json index cf44b4f..ad53b9f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "CouchDB" ], "engines": { - "node": "^4.5 || 6.* || >= 7.*" + "node": ">= 6.*" }, "author": "Nolan Lawson", "license": "Apache-2.0", @@ -62,9 +62,11 @@ "dependencies": { "broccoli-file-creator": "^2.1.1", "broccoli-stew": "^2.1.0", + "ember-auto-import": "^1.5.3", + "ember-cli-babel": "^7.7.3", "pouchdb": "^7.1.1", "relational-pouch": "^3.1.0", - "ember-cli-babel": "^7.7.3" + "uuid": "^3.3.3" }, "ember-addon": { "configPath": "tests/dummy/config" diff --git a/tests/integration/adapters/pouch-basics-test.js b/tests/integration/adapters/pouch-basics-test.js index 420e3bd..c24abfe 100644 --- a/tests/integration/adapters/pouch-basics-test.js +++ b/tests/integration/adapters/pouch-basics-test.js @@ -263,6 +263,27 @@ test('create a new record', function (assert) { }).finally(done); }); +test('update a newly created record before it has finished saving', function (assert) { + assert.expect(2); + + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + var newSoup = this.store().createRecord('taco-soup', { id: 'E', flavor: 'oops-wrong-flavor' }); + newSoup.save(); + newSoup.set('flavor', 'balsamic'); + return newSoup.save(); + }).then(() => { + return this.db().get('tacoSoup_2_E'); + }).then((newDoc) => { + assert.equal(newDoc.data.flavor, 'balsamic', 'should have saved the attribute'); + + var recordInStore = this.store().peekRecord('tacoSoup', 'E'); + assert.equal(newDoc._rev, recordInStore.get('rev'), + 'should have associated the ember-data record with the rev for the new record'); + + }).finally(done); +}); + test('creating an associated record stores a reference to it in the parent', function (assert) { assert.expect(1); @@ -367,14 +388,14 @@ test('eventually consistency - success', function (assert) { let result = [ foodItem.get('soup') .then(soup => assert.equal(soup.id, 'C')), - + promiseToRunLater(0) .then(() => { return this.db().bulkDocs([ {_id: 'tacoSoup_2_C', data: { flavor: 'test' } } ]);}), ]; - + return Ember.RSVP.all(result); }) .finally(done); @@ -395,13 +416,13 @@ test('eventually consistency - deleted', function (assert) { foodItem.get('soup') .then((soup) => assert.ok(soup === null, 'isDeleted')) .catch(() => assert.ok(true, 'isDeleted')), - + promiseToRunLater(100) .then(() => this.db().bulkDocs([ {_id: 'tacoSoup_2_C', _deleted: true } ])), ]; - + return Ember.RSVP.all(result); }) .finally(done); @@ -450,9 +471,9 @@ test('remote delete removes belongsTo relationship', function (assert) { .then((found) => { let id = "tacoSoup_2_" + found.id; let promise = this.adapter().waitForChangeWithID(id); - + this.db().remove(id, found.get('rev')); - + return promise; }).then(() => { return this.store().findRecord('food-item', 'Z');//Z should be updated now @@ -472,7 +493,7 @@ test('remote delete removes belongsTo relationship', function (assert) { test('remote delete removes hasMany relationship', function (assert) { assert.timeout(5000); assert.expect(3); - + let liveIngredients = null; var done = assert.async(); @@ -483,15 +504,15 @@ test('remote delete removes hasMany relationship', function (assert) { .then(found => found.get('ingredients'))//prime hasMany .then((ingredients) => { liveIngredients = ingredients;//save for later - + assert.equal(ingredients.length, 2, "should be 2 food items initially"); - + let itemToDelete = ingredients.toArray()[0]; let id = "foodItem_2_" + itemToDelete.id; let promise = this.adapter().waitForChangeWithID(id); - + this.db().remove(id, itemToDelete.get('rev')); - + return promise; }).then(() => { return this.store().findRecord('taco-soup', 'C');//get updated soup.ingredients @@ -514,7 +535,7 @@ module('not eventually consistent', { beforeEach: function() { assert.expect(2); assert.ok(config.emberPouch.eventuallyConsistent == false, 'eventuallyConsistent is false'); let done = assert.async(); - + Ember.RSVP.Promise.resolve().then(() => this.store().findRecord('food-item', 'non-existent') .then(() => assert.ok(false)) .catch(() => {