Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

DS.InMemoryAdapter and DS.NullSerializer #512

Closed
wants to merge 12 commits into from

8 participants

@ahawkins

InMemoryAdapter & NullSerializer

EDIT: This PR is still WIP. I've pushed the code for feedback from the core team and community members.

I've given a lot of thought to this PR and the issues's surrounding it. I don't
think they are only related to my app, but to general practice for development
and testing. I've written this code and made this PR with these things in mind.
I will describe them in detail. Warning: this is going to be long.

Background

I think that development and testing are essentially the same environment. All
ember-data backed applications are going to have data dependence in tests. In
order to write effective tests we need to ensure these things work:

  1. Reset application data back to an empty state
  2. Create, add, and delete records from inside the application
  3. Create, add, and delete records from outside the application
  4. Inspect what get's sent back to the "server". I'll come back to this later.

Developers also need to do these same things in development to some degree.
Most of the time we just need some dummy data to develop against. This was
previously done by using DS.FixtureAdapter. More on that later.

Ember applications also enforce boundarys between different parts of the
application. The store is one boundary. The store also creates it's own
internal boundaries by using the adapter and serializer. I think these
boundaries are very imporant because they enable developers to forget about
what is happening on the other side. More importantly, boundaries should be
stubable or easily swapped out if you like. You can, and should, leverage
architectual boundaries as much as you can. It will make development much
better.

With those principles in mind, I set out to make it so. This is the solution I
came to.

The Existing Problem

I needed the absolute most simple possible store. All I wanted to do was create
Ember.Objects according to my application semantics and persist them in
memory. I needed to persist them in memory somewhere so I could run queries
against them. I wanted to put this in my in memory adapter because the server
would usually do it, but since there is a boundary between the server and app,
I wanted to leverage it and exploit it. The closest thing to this is
DS.FixtureAdapter. This works for very simple use cases but it does not
scale. I'll do my best to enumerabe the problems with using
DS.FixtureAdapter.

  1. Records are pushed onto global constants in multiple objects. This does not lend itself to setup/teardown use cases.
  2. Dated pushed into .FIXTURES must be compliant with whatever happens with the transforms. This can be confusing. Things just happen to work becuase of the transform implementations (since the current default serializer is DS.JSONSerializer).
  3. Continuation of point 2, but from a different perspective. The data in .FIXTURES does not mirror application data. Here's an example if you work with Dates you will have Date objects inside your application but you will have formatted strings inside .FIXTURES. You have to work with two different kinds of data. I think this is fundamentally flawed.
  4. Changes to DS.Model objects are not commited to .FIXTURES arrays. This makes it impossible to get full use out of the fixture adapter because changes are not persistent. Here's an example: You have a simple list of user fixtures. There are two users. "Tom" and "Yehuda". You find "Tom" and change his name to "Tom Dale". Now you need to do fire a findQuery for all the user's named "Tom Dale". You cannot do this because "Tom" is still persisted in User.FIXTURES. You can override create/update/delete record to do this if you like. I think this functionality is confusing to users. Hell, you can delete a record and find it again. That's kinda werid.
  5. DS.FixtureAdapter does not respect boundaries. Ember applications are MVC. That creates a familar set of boundaries. Ember-Data introduces a new boundary between your model and the data. It also introduces additional boundaries between data consumers and providers. Your application code only communicates with the store. This is the boundary and it should be respected. There is a reason the adapter is not exposed directly to the application. Using DS.FixtureAdapter forces you to work with the adapter instead of the store. Pushing data into .FIXTURES to load data is an antipattern. store.createRecord should be used instead. This point is also somewhat related to #4 because the code does not communicate changes in the store back to it's storage so must interact with .FIXTURES.

I've run into these problems by pushing the uses cases to it's limits. I don't
think the fixture adapter is bad, I just think it serves a limited number of
use cases. It's use cases can be modeled using an in memory store, similarly
to how you can construct a syncronous system from an asyncronous one, but not
the other way around.

These problems have led me to this solution.

Towards a Better Solution

I mucked with the fixture adapter and various things. I came up with these
use cases.

  1. Isolate tests from the store. Some tests needed to access the store, but not the application. I don't need an application to test the store. (Boundaries again). I do need a store that I can instantiate whenever I need to that has no dependencies.
  2. The store itself must be resetable. Integration tests will create data. The test will finish and the next one needs to run. Data must be wiped before the next test. There is also another problem at play here. There may be multiple references to the store through the application code. The store itself is instantiated in an injection then set on your router and controllers. This makes reassigning the store difficult because you have to know eveywhere it should be assigned. You can currently "reset" the store by destroying it. This is suboptimal.
  3. Provide the simplest possible "persistance" strategy. Here's my defintion of simple: what's passed to create/update get's stored in memory. That's all. No fancy tranforms or serialization magic. There is no need for these for these when you have control over the data format!
  4. Create/Update/Delete operations must be persisted in memory. This enables the querying use case described earlier.
  5. All internal state is localized to instance variables.
  6. Respect boundaries as much as possible. Work with the store and not the adapter, but expose parts of the adapter useful for testing (IE the records currently in memory).

After looking at this problem (and struggling with implemenations) I came to
the conclusion that Ember-Data needs two things: A simple in memory adapter and
a null serializer. The in memory adapter is a slight refactoring of the
existing fixture adapter. The null serializer is an implemenation of the null
object pattern for serializers.

DS.InMemoryAdapter and DS.NullSerializer

I mentioned that DS.InMemoryAdapter is a refactoring of DS.FixtureAdapter.
In fact the only two differences:

  1. create/update/delete commits are kept in memory
  2. loaded records are kept in an internal Ember.Map instead in global .FIXTURE arrays.

DS.NullSerializer is more fun. It simply ignores everything. It ignores
transformations and mappings. Why do we need a null serializer? Using a null
serializer allows whatever is passed into create/update to exist in memory.
There is no need to work with two different sets of data. This enables you to
simply shove whatever you want into the store without having to worry about
anything. Using DS.NullSerializer in combination with DS.InMemoryAdapter
allows you to create store as essentially an array of objects. This is the
simplest possible thing that could work. Most importantly: You can work
directly with application level objects inside the store AND you don't have to
follow any semantics at all.
You simply create DS.Model objects you want
and call store.commit() and things are saved.

Here's an example:

var store = DS.create({
  adapter: DS.InMemoryAdapter.create(); // Uses DS.NullSerializer by default
});

// let's build some crazy stuff, but first here's my real world 
// use case

// Ember.DateTime is an example of a complex object that is
// no supported in the default tranforms. It's not part of
// Ember itself. It is a separate project.

store.createRecord(Email, {
  id: '1',
  sentAt: Ember.DateTime.create()
});
store.commit();

var email = store.find(Email, 1);

var email.get('sentAt') // Ember.DateTime. Exactly what I put on it.
                        // No fuss no muss
// Writing an integration test
// assuming the store from the previous example

module("Login", {
  // ensures each test exeuctes with no data
  // or existing state
  setup: function() {
    App.get('store').reset();
  },

  teardown: function() {
    App.get('store').reset();
  },
});

test("the user is logged in", function() {
  // do stuff
});

You can also use DS.InMemoryStore as a mock. It implements no behavior so you
can effectively substitute it for anything dependent on a store.

Why This is Good

I belive these changes are good for many reasons. There are two main reasons:

  1. DS.InMemoryStore is completely portable can be instantiated, reset, and torn down as needed--wether it's in tests or in development.
  2. Completely disregards problems and API's that exist for when you don't have constrol of the data your application needs. If you control the date you have no need for transforms and maps. You just construct the object you want and save it.

I also thing these things are more inline with how Ember applications should
be architected. It pains me to see DS.FixtureAdapter and custom serializers
with maps and transforms, or hell, even bending fixtures to match REST semantics.
All of that is complete nonsense. Leverage the boundaries to make your development
easier. Work with application level objects and not some other abstraction. Remember:
the adapter's role is to provide you with application objects! So why not just tell
it to store the ones you create?

Change Summary

  • Create DS.InMemoryAdapter as described above. It can still simulate remote responses. That behavior is off by default.
  • Create DS.NullSerializer.
  • breaking: Remove DS.FixtureAdapter since it's a subset of DS.InMemoryAdapter. The existence of both adapters is confusing.
  • implement DS.Serializer.transformFor to make implementing the null serializer easy as pie.
  • Add support for error and validations in DS.InMemoryAdapter.

Ongoing Concerns

  • More tests around associations
  • Implement map ignoring.
@ppcano

This is absolutely useful, I has also missed the features to manage the situation you mention.

packages/ember-data/lib/adapters/in_memory_adapter.js
@@ -0,0 +1,122 @@
+require("ember-data/system/adapter");
+require("ember-data/serializers/null_serializer");
+
+var get = Ember.get;
+
+DS.InMemoryAdapter = DS.Adapter.extend({
+ serializer: DS.NullSerializer,
+
+ simulateRemoteReseponse: false,
@kurko
kurko added a note

Typo? Reseponse

@kurko
kurko added a note

Pardon my ignorance, but if it's a typo and no tests failed, does it mean there are missing tests? Or maybe it's a flag for an Ajax mechanism (which I'm unaware of), which then make it difficult to test in the current PR?

Yes. I need to port over the tests from the fixture adapter as unit test for the this adapter. Those tests would fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@kurko

I think it's perfectly valid. It's something similar to in-memory SQLite data in Rails projects, except that its main use case, as far as I can see, is to setup data before a test, have the data available ubiquitously, then reset it later. I'm not sure how useful it would be in development though.

I'm +1 for this.

I think that development and testing are essentially the same environment.

I don't think development and test environment are the same mainly because test data are temporary, being reset after each test, whereas that would be unfeasible in the development environment.

@mikegrassotti

+1 - Awesome work, totally needed.

I've spent the past few weeks exploring how to use konacha for integration testing our ember app. DS.FixtureAdapter is OK for very simple specs but is not gonna work beyond that. This forces us to run most of our integration tests against the full-stack by spinning up a test-rails server, using the RestAdapter and wiping the db between tests. Total overkill as 90% of these specs are ember-specific.

This PR is a really elegant approach, looking forward to kicking the tires.

@ahawkins

This forces us to run most of our integration tests against the full-stack by spinning up a test-rails server, using the RestAdapter and wiping the db between tests. Total overkill as 90% of these specs are ember-specific.

@grasscode Ya. That's bad. I couldn't even do that if I wanted to since our api and frontend app are completely separate projects. No easy way to run integration tests between the client and the server.

I don't think development and test environment are the same mainly because test data are temporary, being reset after each test, whereas that would be unfeasible in the development environment.

@kurko Development and test are the same in that they both need temporal data. The data is wiped between every test. In development you need fresh data for each page reload. You won't be wiping the data in development, but you do need an easy way to push data in once and use it throughout development.

@dagda1

Huge + 1 for this. It addresses the following which make testing painful:

  1. I cannot adequately teardown the store after each test.
  2. I get caught up in semantics and ceremony when all I want to do is work with plain old objects for my tests, e.g. making sure belongsTo keys are user_id in my sample data.
  3. All I can do is load the FIXTURES array.

This will make our testing lives so much easier.

@ahawkins

I get caught up in semantics and ceremony when all I want to do is work with plain old objects for my tests, e.g. making sure belongsTo keys are user_id in my sample data.

@dagda1. Same thing for development. Who needs freaking semantics :)

@ahawkins

I am going to add error support to this as well.

@walter

Just a heads up for those dealing with this sort of problem (API call stubbing) while this issue's code solidifies. Here's the technique I'm trying:

I believe I have this working with a very simple test, but I'm just getting into the code that needs this. I'll let you know how it progresses.

@drogus

@walter the problem with this approach is that you're bound to the API that you're using at the moment, so if you change the API you need to change your tests, which should not be the case most of the time

Also, bumping this PR, because it would be really cool to have something like that.

@walter

@drogus, yep. Just mentioning for those looking for something they can use while this gets fleshed out. I've actually switched DS.FixtureAdapter for the moment. I wouldn't recommend Sinon.JS's FakeServer unless you absolutely need it.

@ahawkins

@tomdale @wycats I think we have a problem. In master the FixtureAdapter uses JSONSerializer. It also bypasses adapter.didFindQuery and those things. adapterDidFindQuery uses the loader which does all the crazy crap to handle all the loading--most notably expecting there to be a root element. So when FixtureAdapter is updated to work the same way as RESTAdapter with didFindXXX things blow up. It seems the existing FixtureAdapter got aroudn this by interacting with the store and RecordArrays directly. I don't know if this is a good thing.

This is the problem. Say you have this in FIXTURES

App.Person.FIXTURES = [{
    id: 'wycats',
    firstName: "Yehuda",
    lastName: "Katz",

    height: 65
  },

  {
    id: 'ebryn',
    firstName: "Erik",
    lastName: "Brynjolffsosysdfon",

    height: 70
  }];

This doesn't work because the JSON serializer is thinking: I need to sideload the "firstName", "lastName", and "height" associations.

It really expects this:

App.Person.FIXTURES = [{
    person: {
      id: 'wycats',
      firstName: "Yehuda",
      lastName: "Katz",

      height: 65
  },

  {
    person: {
      id: 'ebryn',
      firstName: "Erik",
      lastName: "Brynjolffsosysdfon",

      height: 70
    }
  }];

I'm not sure how to proceed at this point.

EDIT: I've pushed my commits. All the tests in fixture_adapter_test.js are failing for this reason. Everything else is backward compatible at this point.

twinturbo added some commits
@ahawkins

@tchak PR's have fixed most of this.

@ahawkins ahawkins closed this
@ryanjm

@twinturbo - Can you link to which PR's fixed this issue? I'm trying to figure out how to properly test my app and it would be helpful mock out the Adapter.

@ahawkins

@ryanjm the current fixture adapter generally handles the serialization cases described in this PR. #728 brings in the rest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 25, 2013
  1. First test passing

    twinturbo authored
  2. First meaningful test

    twinturbo authored
  3. Update and delete

    twinturbo authored
  4. Add find test

    twinturbo authored
  5. findQuery implemented

    twinturbo authored
  6. Add findAll

    twinturbo authored
  7. Test default findQuery and fix jslint

    twinturbo authored
  8. Tests passing after rebase

    twinturbo authored
  9. Begin work on FixtureAdapter backwards compat

    twinturbo authored
  10. More work on migrating FixtureAdapter

    twinturbo authored
  11. Cleanup

    twinturbo authored
Commits on Jan 26, 2013
  1. Begin docs

    twinturbo authored
This page is out of date. Refresh to see the latest.
View
249 packages/ember-data/lib/adapters/fixture_adapter.js
@@ -1,138 +1,223 @@
-require("ember-data/core");
require("ember-data/system/adapter");
+require("ember-data/serializers/rest_serializer");
var get = Ember.get;
-DS.FixtureAdapter = DS.Adapter.extend({
+/**
+ `DS.FixtureAdapter` is an adapter that loads records from memory.
+ Its primarily used for development and testing. You can also use
+ `DS.FixtureAdapter` while working on the API but are not ready to
+ integrate yet. It is a fully functioning adapter. All CRUD methods
+ are implemented. You can also implement query logic that a remote
+ system would do. Its possible to do develop your entire application
+ with `DS.FixtureAdapter`.
+
+ ## Preloading Data
+
+ `DS.FixtureAdapter` has a `storeRecord` method. This method is used
+ to make data available, but not load it. This is equivalent to creating
+ the data in a remote system. `storeRecord` calls `recordsForType`.
+ `recordsForType` must an array for data objects. `recordsForType` looks
+ in the `FIXTURES` array by default. You can override this if you like.
+ Assume you have a basic person model. You can load the data in two ways:
+
+ ```javascript
+ adapter = DS.FixtureAdapter.create()
+
+ adapter.storeRecord(App.Person, {
+ id: "1"
+ name: "Adam Hawkins"
+ handle: "twinturbo"
+ });
+
+ // You can also set .FIXTURES if you like
+
+ App.Person.FIXTURES = [{
+ id: "1"
+ name: "Adam Hawkins"
+ handle: "twinturbo"
+ }];
+ ```
+
+ ## Data Format
+
+ The data format is dictated by the serializer. `DS.JSONSerializer` is used
+ by default. This means the objects passed to `storeRecord` or placed into
+ `FIXTURES` must be something that `DS.JSONSerializer` expects.
+ `DS.JSONSerializer` allows you to sideload and embedded associations.
+ `DS.FixtureAdapter` does not set these up for you. You must determine the
+ correct data format based on the serializer. The data must be serializable
+ and deserializable. When records are commited their serialized format is
+ updated in memory. Finding them again will deserialize and load the record
+ again.
+
+ ## Advanced Usage
+
+ `DS.FixtureAdapter` was primarily used for storing simulated JSON responses.
+ It was a place holder for an existing REST API. You can turn this on its
+ head if you like. `DS.JSONSerializer` has its own semantics. These are useful
+ when handling JSON. However you may not be handling JSON. Picture this use case.
+ You are just starting a new app. You don't know how the API would work. You don't
+ know anything about how the data is persisted. Actually, you don't care about the
+ the backend at all. It's not important for you. You just need something to
+ store data when `commit` is called. You can use `DS.FixtureAdapter` with
+ `DS.PassThroughSerializer` for this case. `DS.FixtureAdapter` is a simple in memory
+ store. You can remove data semantics by using `DS.PassThroughSerializer`.
+ `DS.PassThroughSerializer` has no semantics. Whatever is given to it is
+ simply returned. This creates one big difference: You store application objects
+ and not serialized representations of them. Here's an example.
+
+ ```javascript
+ var adapter = DS.FixtureAdapter.create({
+ serializer: DS.PassThroughSerializer
+ });
+
+ var Person = DS.Model.extend({
+ profile: DS.attr('object');
+ });
+
+ var adam = Person.createRecord();
+
+ adam.set('profile', Ember.Object.create({
+ music: ['trance', 'baleric'],
+ skills: ['ruby', 'javascript']
+ }));
+
+ adam.commit();
+
+ var records = store.recordForType(Person);
+
+ var adamInMemory = records[0];
+ // adamInMemory is the same object that was
+ // commited. the `profile` is an Ember.Object
+ // and not a basic object.
+ ```
+*/
+DS.FixtureAdapter = DS.Adapter.extend({
simulateRemoteResponse: true,
- latency: 50,
-
- /*
- Implement this method in order to provide data associated with a type
- */
- fixturesForType: function(type) {
- if (type.FIXTURES) {
- var fixtures = Ember.A(type.FIXTURES);
- return fixtures.map(function(fixture){
- if(!fixture.id){
- throw new Error('the id property must be defined for fixture %@'.fmt(fixture));
- }
- fixture.id = fixture.id + '';
- return fixture;
- });
- }
- return null;
- },
+ latency: 100,
- /*
- Implement this method in order to query fixtures data
- */
- queryFixtures: function(fixtures, query, type) {
- return fixtures;
- },
+ recordsForType: function(type) {
+ if(Ember.isNone(type.FIXTURES)) type.FIXTURES = [];
- /*
- Implement this method in order to provide provide json for CRUD methods
- */
- mockJSON: function(type, record) {
- return this.serialize(record, { includeId: true });
+ return type.FIXTURES;
},
- /*
- Adapter methods
- */
- generateIdForRecord: function(store, record) {
- return Ember.guidFor(record);
+ queryRecords: function(records, query) {
+ return records;
},
- find: function(store, type, id) {
- var fixtures = this.fixturesForType(type);
+ storeRecord: function(type, record) {
+ var records = this.recordsForType(type);
- Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+ this.deleteLoadedRecord(type, record);
- if (fixtures) {
- fixtures = fixtures.findProperty('id', id);
- }
+ records.push(record);
+ },
+
+ find: function(store, type, id) {
+ var records = this.recordsForType(type);
+ var record = this.findRecordById(records, id);
- if (fixtures) {
+ if (record) {
+ var adapter = this;
this.simulateRemoteCall(function() {
- store.load(type, fixtures);
+ adapter.didFindRecord(store, type, record, id);
}, store, type);
}
},
- findMany: function(store, type, ids) {
- var fixtures = this.fixturesForType(type);
+ findQuery: function(store, type, query, array) {
+ var records = this.recordsForType(type);
- Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+ var results = this.queryRecords(records, query);
- if (fixtures) {
- fixtures = fixtures.filter(function(item) {
- return ids.indexOf(item.id) !== -1;
- });
- }
+ if (results) {
+ var adapter = this;
- if (fixtures) {
this.simulateRemoteCall(function() {
- store.loadMany(type, fixtures);
+ adapter.didFindQuery(store, type, results, array);
}, store, type);
}
},
findAll: function(store, type) {
- var fixtures = this.fixturesForType(type);
-
- Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+ var records = this.recordsForType(type);
+ var adapter = this;
this.simulateRemoteCall(function() {
- store.loadMany(type, fixtures);
- store.didUpdateAll(type);
+ adapter.didFindAll(store, type, records);
}, store, type);
},
- findQuery: function(store, type, query, array) {
- var fixtures = this.fixturesForType(type);
-
- Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
-
- fixtures = this.queryFixtures(fixtures, query, type);
-
- if (fixtures) {
- this.simulateRemoteCall(function() {
- array.load(fixtures);
- }, store, type);
- }
- },
-
createRecord: function(store, type, record) {
- var fixture = this.mockJSON(type, record);
+ var inMemoryRecord = this.serialize(record, { includeId: true });
- fixture.id = this.generateIdForRecord(store, record);
+ this.storeRecord(type, inMemoryRecord);
+
+ var adapter = this;
this.simulateRemoteCall(function() {
- store.didSaveRecord(record, fixture);
+ adapter.didCreateRecord(store, type, record, inMemoryRecord);
}, store, type, record);
},
updateRecord: function(store, type, record) {
- var fixture = this.mockJSON(type, record);
+ var inMemoryRecord = this.serialize(record, { includeId: true });
+
+ this.storeRecord(type, inMemoryRecord);
+
+ var adapter = this;
this.simulateRemoteCall(function() {
- store.didSaveRecord(record, fixture);
+ adapter.didSaveRecord(store, type, record, inMemoryRecord);
}, store, type, record);
},
deleteRecord: function(store, type, record) {
+ this.deleteLoadedRecord(type, record);
+
+ var adapter = this;
+
this.simulateRemoteCall(function() {
- store.didSaveRecord(record);
+ adapter.didSaveRecord(store, type, record);
}, store, type, record);
},
- /*
- @private
- */
+ deleteLoadedRecord: function(type, record) {
+ var id = this.extractId(type, record);
+
+ var existingRecord = this.findExistingRecord(type, record);
+
+ if(existingRecord) {
+ var records = this.recordsForType(type, record);
+ var index = records.indexOf(existingRecord);
+ records.splice(index, 1);
+ return true;
+ }
+ },
+
+ findExistingRecord: function(type, record) {
+ var records = this.recordsForType(type);
+ var id = this.extractId(type, record);
+
+ return this.findRecordById(records, id);
+ },
+
+ findRecordById: function(records, id) {
+ var adapter = this;
+
+ return records.find(function(r) {
+ if(''+get(r, 'id') === ''+id) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+ },
+
simulateRemoteCall: function(callback, store, type, record) {
if (get(this, 'simulateRemoteResponse')) {
setTimeout(callback, get(this, 'latency'));
View
1  packages/ember-data/lib/main.js
@@ -26,4 +26,5 @@ require("ember-data/system/relationships");
require("ember-data/system/application_ext");
require("ember-data/system/serializer");
require("ember-data/system/adapter");
+require("ember-data/serializers/pass_through_serializer");
require("ember-data/adapters");
View
58 packages/ember-data/lib/serializers/pass_through_serializer.js
@@ -0,0 +1,58 @@
+require('ember-data/system/serializer');
+
+var get = Ember.get, set = Ember.set;
+
+DS.PassThroughSerializer = DS.Serializer.extend({
+ extractId: function(type, hash) {
+ var primaryKey = this._primaryKey(type);
+
+ if (hash.hasOwnProperty(primaryKey)) {
+ // Ensure that we coerce IDs to strings so that record
+ // IDs remain consistent between application runs; especially
+ // if the ID is serialized and later deserialized from the URL,
+ // when type information will have been lost.
+ return hash[primaryKey]+'';
+ } else {
+ return null;
+ }
+ },
+
+ extractMany: function(loader, objects, type, records) {
+ var references = [];
+
+ for (var i = 0; i < objects.length; i++) {
+ var reference = this.extractRecordRepresentation(loader, type, objects[i]);
+ references.push(reference);
+ }
+
+ loader.populateArray(references);
+ },
+
+ extract: function(loader, object, type, record) {
+ this.extractRecordRepresentation(loader, type, object);
+ },
+
+ extractAttribute: function(type, hash, attributeName) {
+ return hash[attributeName];
+ },
+
+ createSerializedForm: function() {
+ return {};
+ },
+
+ addId: function(data, key, id) {
+ data[key] = id;
+ },
+
+ addAttribute: function(hash, key, value) {
+ hash[key] = value;
+ },
+
+ deserializeValue: function(value, attributeType) {
+ return value;
+ },
+
+ serializeValue: function(value, attributeType) {
+ return value;
+ }
+});
View
217 packages/ember-data/tests/integration/fixture_adapter_and_pass_through_serializer_test.js
@@ -0,0 +1,217 @@
+var get = Ember.get, set = Ember.set;
+var App, ComplexObject, Person, store, adapter;
+
+ComplexObject = Ember.Object.extend({
+
+});
+
+module("FixtureAdapter & PassThroughSerializer", {
+ setup: function() {
+ App = Ember.Namespace.create();
+
+ App.Person = DS.Model.extend({
+ name: DS.attr('string'),
+ profile: DS.attr('object'),
+ });
+
+ App.Person.FIXTURES = [];
+
+ adapter = DS.FixtureAdapter.create({
+ simulateRemoteResponse: false,
+ serializer: DS.PassThroughSerializer.create()
+ });
+
+ store = DS.Store.create({ adapter: adapter });
+ },
+
+ teardown: function() {
+ adapter.destroy();
+ store.destroy();
+ }
+});
+
+test("records are persisted as is", function() {
+ var attributes = {
+ id: '1',
+ name: "Adam Hawkins",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript'],
+ music: 'Trance'
+ })
+ };
+
+ store.createRecord(App.Person, attributes);
+ store.commit();
+
+ var adam = store.find(App.Person, 1);
+
+ equal(adam.get('name'), attributes.name, 'Attribute materialized');
+ equal(adam.get('profile'), attributes.profile, 'Complex object materialized');
+
+ var inMemoryRecords = adapter.recordsForType(App.Person);
+ equal(inMemoryRecords.length, 1, "In memory objects updated");
+
+ var inMemoryProfile = inMemoryRecords[0].profile;
+ ok(inMemoryProfile instanceof Ember.Object, 'Complex objects persisted in memory');
+ equal(inMemoryProfile.skills, adam.get('profile.skills'));
+ equal(inMemoryProfile.music, adam.get('profile.music'));
+});
+
+test("records are updated as is", function() {
+ var attributes = {
+ id: '1',
+ name: "Adam Hawkins",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript'],
+ music: 'Trance'
+ })
+ };
+
+ store.createRecord(App.Person, attributes);
+ store.commit();
+
+ var adam = store.find(App.Person, 1);
+
+ adam.set('name', 'Adam Andrew Hawkins');
+ store.commit();
+
+ equal(adam.get('name'), 'Adam Andrew Hawkins', 'Attribute materialized');
+
+ var inMemoryRecords = adapter.recordsForType(App.Person);
+ equal(inMemoryRecords.length, 1, "In memory objects updated");
+
+ var inMemoryObject = inMemoryRecords[0];
+
+ equal(inMemoryObject.name, adam.get('name'), 'Changes saved to in memory records');
+});
+
+test("records are deleted", function() {
+ var attributes = {
+ id: '1',
+ name: "Adam Hawkins",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript'],
+ music: 'Trance'
+ })
+ };
+
+ store.createRecord(App.Person, attributes);
+ store.commit();
+
+ var adam = store.find(App.Person, 1);
+ adam.deleteRecord();
+ store.commit();
+
+ var inMemoryRecords = adapter.recordsForType(App.Person);
+ equal(inMemoryRecords.length, 0, "In memory objects updated");
+});
+
+test("find queries loaded records", function() {
+ var attributes = {
+ id: '1',
+ name: "Adam Hawkins",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript'],
+ music: 'Trance'
+ })
+ };
+
+ adapter.storeRecord(App.Person, attributes);
+
+ var adam = store.find(App.Person, 1);
+
+ equal(adam.get('name'), attributes.name, 'Attribute materialized');
+ equal(adam.get('profile'), attributes.profile, 'Complex object materialized');
+});
+
+test("findQuery returns all records by default", function() {
+ var adamsAttributes = {
+ id: '1',
+ name: "Adam Hawkins",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript'],
+ music: 'Trance'
+ })
+ };
+
+ var paulsAttributes = {
+ id: '2',
+ name: "Paul Chavard",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript', 'French'],
+ music: 'Funny french stuff'
+ })
+ };
+
+ adapter.storeRecord(App.Person, adamsAttributes);
+ adapter.storeRecord(App.Person, paulsAttributes);
+
+ var results = store.find(App.Person, {skill: 'French'});
+
+ equal(results.get('length'), 2, 'Records loaded correctly');
+});
+
+test("findQuery is implemented with a method to override", function() {
+ adapter.queryRecords = function(records, query) {
+ return records.filter(function(record) {
+ return record.profile.get('skills').contains(query.skill);
+ });
+ };
+
+ var adamsAttributes = {
+ id: '1',
+ name: "Adam Hawkins",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript'],
+ music: 'Trance'
+ })
+ };
+
+ var paulsAttributes = {
+ id: '2',
+ name: "Paul Chavard",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript', 'French'],
+ music: 'Funny french stuff'
+ })
+ };
+
+ adapter.storeRecord(App.Person, adamsAttributes);
+ adapter.storeRecord(App.Person, paulsAttributes);
+
+ var results = store.find(App.Person, {skill: 'French'});
+
+ equal(results.get('length'), 1, 'Records filtered correctly');
+
+ var paul = results.get('firstObject');
+ equal(paul.get('name'), 'Paul Chavard');
+});
+
+test("findAll is implemented", function() {
+ var adamsAttributes = {
+ id: '1',
+ name: "Adam Hawkins",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript'],
+ music: 'Trance'
+ })
+ };
+
+ var paulsAttributes = {
+ id: '2',
+ name: "Paul Chavard",
+ profile: ComplexObject.create({
+ skills: ['ruby', 'javascript', 'French'],
+ music: 'Funny french stuff'
+ })
+ };
+
+ adapter.storeRecord(App.Person, adamsAttributes);
+ adapter.storeRecord(App.Person, paulsAttributes);
+
+ var results = store.find(App.Person);
+
+ equal(results.get('length'), 2, "All records returned");
+ equal(get(results, 'isUpdating'), false, "results not updating");
+});
+
View
42 packages/ember-data/tests/unit/fixture_adapter_test.js
@@ -1,30 +1,32 @@
var get = Ember.get, set = Ember.set;
-var store, Person;
+var store, Person, App;
module("DS.FixtureAdapter", {
setup: function() {
- store = DS.Store.create({
- adapter: 'DS.FixtureAdapter'
- });
+ App = Ember.Namespace.create();
- Person = DS.Model.extend({
+ App.Person = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
height: DS.attr('number')
});
+
+ store = DS.Store.create({
+ adapter: DS.FixtureAdapter.create()
+ });
},
teardown: function() {
Ember.run(function() {
store.destroy();
});
store = null;
- Person = null;
+ App.Person = null;
}
});
test("should load data for a type asynchronously when it is requested", function() {
- Person.FIXTURES = [{
+ App.Person.FIXTURES = [{
id: 'wycats',
firstName: "Yehuda",
lastName: "Katz",
@@ -42,7 +44,7 @@ test("should load data for a type asynchronously when it is requested", function
stop();
- var ebryn = store.find(Person, 'ebryn');
+ var ebryn = store.find(App.Person, 'ebryn');
equal(get(ebryn, 'isLoaded'), false, "record from fixtures is returned in the loading state");
@@ -55,7 +57,7 @@ test("should load data for a type asynchronously when it is requested", function
stop();
- var wycats = store.find(Person, 'wycats');
+ var wycats = store.find(App.Person, 'wycats');
wycats.then(function() {
clearTimeout(timer);
start();
@@ -79,7 +81,7 @@ test("should load data for a type asynchronously when it is requested", function
test("should create record asynchronously when it is committed", function() {
stop();
- var paul = store.createRecord(Person, {firstName: 'Paul', lastName: 'Chavard', height: 70});
+ var paul = store.createRecord(App.Person, {firstName: 'Paul', lastName: 'Chavard', height: 70});
paul.on('didCreate', function() {
clearTimeout(timer);
@@ -101,7 +103,7 @@ test("should create record asynchronously when it is committed", function() {
test("should update record asynchronously when it is committed", function() {
stop();
- var paul = store.findByClientId(Person, store.load(Person, 1, {firstName: 'Paul', lastName: 'Chavard', height: 70}).clientId);
+ var paul = store.findByClientId(App.Person, store.load(App.Person, 1, {firstName: 'Paul', lastName: 'Chavard', height: 70}).clientId);
paul.set('height', 80);
@@ -124,7 +126,7 @@ test("should update record asynchronously when it is committed", function() {
test("should delete record asynchronously when it is committed", function() {
stop();
- var paul = store.findByClientId(Person, store.load(Person, 1, { firstName: 'Paul', lastName: 'Chavard', height: 70}).clientId);
+ var paul = store.findByClientId(App.Person, store.load(App.Person, 1, { firstName: 'Paul', lastName: 'Chavard', height: 70}).clientId);
paul.deleteRecord();
@@ -147,14 +149,14 @@ test("should delete record asynchronously when it is committed", function() {
test("should follow isUpdating semantics", function() {
stop();
- Person.FIXTURES = [{
+ App.Person.FIXTURES = [{
id: "twinturbo",
firstName: "Adam",
lastName: "Hawkins",
height: 65
}];
- var result = store.findAll(Person);
+ var result = store.findAll(App.Person);
result.addObserver('isUpdating', function() {
clearTimeout(timer);
@@ -172,14 +174,14 @@ test("should follow isUpdating semantics", function() {
test("should coerce integer ids into string", function() {
stop();
- Person.FIXTURES = [{
+ App.Person.FIXTURES = [{
id: 1,
firstName: "Adam",
lastName: "Hawkins",
height: 65
}];
- var result = Person.find("1");
+ var result = App.Person.find("1");
result.then(function() {
clearTimeout(timer);
@@ -196,15 +198,13 @@ test("should coerce integer ids into string", function() {
test("should throw if ids are not defined in the FIXTURES", function() {
- Person.FIXTURES = [{
+ App.Person.FIXTURES = [{
firstName: "Adam",
lastName: "Hawkins",
height: 65
}];
raises(function(){
- Person.find("1");
+ App.Person.find("1");
}, /the id property must be defined for fixture/);
-
-
-});
+});
View
35 packages/ember-data/tests/unit/serializers/pass_through_serializer_test.js
@@ -0,0 +1,35 @@
+var get = Ember.get, set = Ember.set;
+
+var serializer;
+
+module("DS.PassThroughSerializer", {
+ setup: function() {
+ serializer = DS.PassThroughSerializer.create();
+
+ serializer.transforms = {};
+
+ serializer.registerTransform('unobtainium', {
+ serialize: function(value) {
+ return 'serialize';
+ },
+
+ deserialize: function(value) {
+ return 'deserialize';
+ }
+ });
+ },
+
+ teardown: function() {
+ serializer.destroy();
+ }
+});
+
+test("ignores transforms", function() {
+ var value;
+
+ value = serializer.deserializeValue('unknown', 'unobtainium');
+ equal(value, 'unknown', "the deserialize transform was not called");
+
+ value = serializer.serializeValue('unknown', 'unobtainium');
+ equal(value, 'unknown', "the serialize transform was not called");
+});
Something went wrong with that request. Please try again.