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

Use modelName instead of typeKey when available, but preserve existing ID generation behavior #64

Merged
merged 3 commits into from
Jun 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,16 @@ matrix:
- env: EMBER_TRY_SCENARIO=ember-canary

before_install:
- export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH
# After travis-ci/travis-ci#3225 is resolved, restore this and remove the
# manual download/install of PhantomJS 2.0.
# - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH
- mkdir 'phantomjs-2.0.0'
- cd 'phantomjs-2.0.0'
- curl -O 'https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2'
- tar xjvf 'phantomjs-2.0.0-ubuntu-12.04.tar.bz2'
- cd -
- export PATH=./phantomjs-2.0.0:$PATH

- "npm config set spin false"
- "npm install -g npm@^2"

Expand Down
62 changes: 46 additions & 16 deletions addon/adapters/pouch.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
on,
String: {
pluralize,
camelize,
classify
}
} = Ember;
Expand Down Expand Up @@ -72,21 +73,22 @@ export default DS.RESTAdapter.extend({
},

_init: function (type) {
var self = this;
var self = this,
recordTypeName = this.getRecordTypeName(type);
if (!this.db || typeof this.db !== 'object') {
throw new Error('Please set the `db` property on the adapter.');
}

if (!Ember.get(type, 'attributes').has('rev')) {
var modelName = classify(type.typeKey);
var modelName = classify(recordTypeName);
throw new Error('Please add a `rev` attribute of type `string`' +
' on the ' + modelName + ' model.');
}

this._schema = this._schema || [];

var singular = type.typeKey;
var plural = pluralize(type.typeKey);
var singular = recordTypeName;
var plural = pluralize(recordTypeName);

// check that we haven't already registered this model
for (var i = 0, len = this._schema.length; i < len; i++) {
Expand Down Expand Up @@ -131,7 +133,12 @@ export default DS.RESTAdapter.extend({

_recordToData: function (store, type, record) {
var data = {};
var serializer = store.serializerFor(type.typeKey);
// Though it would work to use the default recordTypeName for modelName &
// serializerKey here, these uses are conceptually distinct and may vary
// independently.
var modelName = type.modelName || type.typeKey;
var serializerKey = camelize(modelName);
var serializer = store.serializerFor(modelName);

var recordToStore = record;
// In Ember-Data beta.15, we need to take a snapshot. See issue #45.
Expand All @@ -149,7 +156,7 @@ export default DS.RESTAdapter.extend({
{includeId: true}
);

data = data[type.typeKey];
data = data[serializerKey];

// ember sets it to null automatically. don't need it.
if (data.rev === null) {
Expand All @@ -159,15 +166,37 @@ export default DS.RESTAdapter.extend({
return data;
},

/**
* Returns the string to use for the model name part of the PouchDB document
* ID for records of the given ember-data type.
*
* This method uses the camelized version of the model name in order to
* preserve data compatibility with older versions of ember-pouch. See
* nolanlawson/ember-pouch#63 for a discussion.
*
* You can override this to change the behavior. If you do, be aware that you
* need to execute a data migration to ensure that any existing records are
* moved to the new IDs.
*/
getRecordTypeName(type) {
if (type.modelName) {
return camelize(type.modelName);
} else {
// This branch can be removed when the library drops support for
// ember-data 1.0-beta17 and earlier.
return type.typeKey;
}
},

findAll: function(store, type /*, sinceToken */) {
// TODO: use sinceToken
this._init(type);
return this.db.rel.find(type.typeKey);
return this.db.rel.find(this.getRecordTypeName(type));
},

findMany: function(store, type, ids) {
this._init(type);
return this.db.rel.find(type.typeKey, ids);
return this.db.rel.find(this.getRecordTypeName(type), ids);
},

findQuery: function(/* store, type, query */) {
Expand All @@ -178,38 +207,39 @@ export default DS.RESTAdapter.extend({

find: function (store, type, id) {
this._init(type);
return this.db.rel.find(type.typeKey, id).then(function (payload) {
var recordTypeName = this.getRecordTypeName(type);
return this.db.rel.find(recordTypeName, id).then(function (payload) {
// Ember Data chokes on empty payload, this function throws
// an error when the requested data is not found
if (typeof payload === 'object' && payload !== null) {
var singular = type.typeKey;
var plural = pluralize(type.typeKey);
var singular = recordTypeName;
var plural = pluralize(recordTypeName);
var results = payload[singular] || payload[plural];
if (results && results.length > 0) {
return payload;
}
}
throw new Error('Not found: type "' + type.typeKey +
throw new Error('Not found: type "' + recordTypeName +
'" with id "' + id + '"');
});
},

createRecord: function(store, type, record) {
this._init(type);
var data = this._recordToData(store, type, record);
return this.db.rel.save(type.typeKey, data);
return this.db.rel.save(this.getRecordTypeName(type), data);
},

updateRecord: function (store, type, record) {
this._init(type);
var data = this._recordToData(store, type, record);
return this.db.rel.save(type.typeKey, data);
return this.db.rel.save(this.getRecordTypeName(type), data);
},

deleteRecord: function (store, type, record) {
this._init(type);
var data = this._recordToData(store, type, record);
return this.db.rel.del(type.typeKey, data)
return this.db.rel.del(this.getRecordTypeName(type), data)
.then(extractDeleteRecord);
}
});
});
13 changes: 13 additions & 0 deletions tests/dummy/app/adapters/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Adapter } from 'ember-pouch/index';
import PouchDB from 'pouchdb';

function createDb() {
return new PouchDB('ember-pouch-test');
}

export default Adapter.extend({
init() {
this._super(...arguments);
this.set('db', createDb());
}
});
7 changes: 7 additions & 0 deletions tests/dummy/app/models/taco-soup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import DS from 'ember-data';

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

flavor: DS.attr('string')
});
165 changes: 165 additions & 0 deletions tests/integration/adapters/pouch-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { module, test } from 'qunit';
import startApp from '../../helpers/start-app';

import Ember from 'ember';
/* globals PouchDB */

var App;

/*
* Tests basic CRUD behavior for an app using the ember-pouch adapter.
*/

module('adapter:pouch [integration]', {
beforeEach: function (assert) {
var done = assert.async();

// TODO: do this in a way that doesn't require duplicating the name of the
// test database here and in dummy/app/adapters/application.js. Importing
// the adapter directly doesn't work because of what seems like a resolver
// issue.
(new PouchDB('ember-pouch-test')).destroy().then(() => {
App = startApp();
done();
});
},

afterEach: function (assert) {
Ember.run(App, 'destroy');
}
});

function db() {
return adapter().get('db');
}

function adapter() {
// the default adapter in the dummy app is an ember-pouch adapter
return App.__container__.lookup('adapter:application');
}

function store() {
return App.__container__.lookup('store:main');
}

test('can find all', function (assert) {
assert.expect(3);

var done = assert.async();
Ember.RSVP.Promise.resolve().then(() => {
return db().bulkDocs([
{ _id: 'tacoSoup_2_A', data: { flavor: 'al pastor' } },
{ _id: 'tacoSoup_2_B', data: { flavor: 'black bean' } },
{ _id: 'burritoShake_2_X', data: { consistency: 'smooth' } }
]);
}).then(() => {
return store().find('taco-soup');
}).then((found) => {
assert.equal(found.get('length'), 2, 'should have found the two taco soup items only');
assert.deepEqual(found.mapBy('id'), ['A', 'B'],
'should have extracted the IDs correctly');
assert.deepEqual(found.mapBy('flavor'), ['al pastor', 'black bean'],
'should have extracted the attributes also');
done();
}).catch((error) => {
console.error('error in test', error);
assert.ok(false, 'error in test:' + error);
done();
});
});

test('can find one', function (assert) {
assert.expect(2);

var done = assert.async();
Ember.RSVP.Promise.resolve().then(() => {
return db().bulkDocs([
{ _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } },
{ _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } },
]);
}).then(() => {
return store().find('taco-soup', 'D');
}).then((found) => {
assert.equal(found.get('id'), 'D',
'should have found the requested item');
assert.deepEqual(found.get('flavor'), 'black bean',
'should have extracted the attributes also');
done();
}).catch((error) => {
console.error('error in test', error);
assert.ok(false, 'error in test:' + error);
done();
});
});

test('create a new record', function (assert) {
assert.expect(1);

var done = assert.async();
Ember.RSVP.Promise.resolve().then(() => {
var newSoup = store().createRecord('taco-soup', { id: 'E', flavor: 'balsamic' });
return newSoup.save();
}).then((saved) => {
return db().get('tacoSoup_2_E');
}).then((newDoc) => {
assert.equal(newDoc.data.flavor, 'balsamic', 'should have saved the attribute');
done();
}).catch((error) => {
console.error('error in test', error);
assert.ok(false, 'error in test:' + error);
done();
});
});

test('update an existing record', function (assert) {
assert.expect(1);

var done = assert.async();
Ember.RSVP.Promise.resolve().then(() => {
return db().bulkDocs([
{ _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } },
{ _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } },
]);
}).then(() => {
return store().find('taco-soup', 'C');
}).then((found) => {
found.set('flavor', 'pork');
return found.save();
}).then((saved) => {
return db().get('tacoSoup_2_C');
}).then((updatedDoc) => {
assert.equal(updatedDoc.data.flavor, 'pork', 'should have updated the attribute');
done();
}).catch((error) => {
console.error('error in test', error);
assert.ok(false, 'error in test:' + error);
done();
});
});

test('delete an existing record', function (assert) {
assert.expect(1);

var done = assert.async();
Ember.RSVP.Promise.resolve().then(() => {
return db().bulkDocs([
{ _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } },
{ _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } },
]);
}).then(() => {
return store().find('taco-soup', 'C');
}).then((found) => {
return found.destroyRecord();
}).then(() => {
return db().get('tacoSoup_2_C');
}).then((doc) => {
assert.ok(!doc, 'document should no longer exist');
}, (result) => {
assert.equal(result.status, 404, 'document should no longer exist');
done();
}).catch((error) => {
console.error('error in test', error);
assert.ok(false, 'error in test:' + error);
done();
});
});