I'm migrating an ember application from ember-data v0.13 to v1.0.0 .
In version v0.13, RESTSerializer used to have a materialize callback that allowed me to map rails STI models to ember models.
So when I get a list of events with different types, i would convert each of them to the appropriate ember model
"events": [
{
"id": 1,
"type": "cash_inflow_event",
"time": "2012-05-31T00:00:00-03:00",
"value": 30000
},
{
"id": 2,
"type": "asset_bought_event",
"asset_id": 119,
"time": "2012-08-16T00:00:00-03:00",
"quantity": 100
}
]
Ember models
App.Event = DS.Model.extend({...})
App.AssetBoughtEventMixin = Em.Mixin.create({...})
App.AssetBoughtEvent = App.Event.extend(App.AssetBoughtEventMixin)
App.CashInflowEventMixin = Em.Mixin.create({...})
App.CashInflowEvent = App.Event.extend(App.CashInflowEventMixin)
Ember-data code v0.13 that created the STI-like ember models
App.RESTSerializer = DS.RESTSerializer.extend({
materialize:function (record, serialized, prematerialized) {
var type = serialized.type;
if (type) {
var mixin = App.get(type.classify() + 'Mixin');
var klass = App.get(type.classify());
record.constructor = klass;
record.reopen(mixin);
}
this._super(record, serialized, prematerialized);
},
rootForType:function (type) {
if (type.__super__.constructor == DS.Model) {
return this._super(type);
}
else {
return this._super(type.__super__.constructor);
}
}
});
How can I do the same thing in ember-data v1.0.0 ?
I think I've got a solution...
There's a setupData callback on models.
I did the following
App.Event = DS.Model.extend({
...
setupData:function (data, partial) {
var type = data.type;
if (type) {
var mixin = App.get(type.classify() + 'Mixin');
this.reopen(mixin);
}
delete data.type;
this._super(data, partial);
},
eachAttribute: function() {
if(this.get('type')){
var constructor = App.get(this.get('type').classify());
constructor.eachAttribute.apply(constructor, arguments);
}
this._super.apply(this, arguments);
}
});
Ember experts, is this a good idea??
First, since you are using Rails, you may want to use the ActiveModelAdapter & extend your custom serializer from its serializer:
App.ApplicationAdapter = DS.ActiveModelAdapter;
App.ApplicationSerializer = DS.ActiveModelSerializer.extend({...});
It looks like your custom serializer should override typeForRoot & probably normalize. Here is what those methods looks like now:
DS.ActiveModelSerializer#typeForRoot:
typeForRoot: function(root) {
var camelized = Ember.String.camelize(root);
return Ember.String.singularize(camelized);
}
DS.JSONSerializer#normalize:
normalize: function(type, hash) {
if (!hash) { return hash; }
this.applyTransforms(type, hash);
return hash;
}
Related
the serializer in unit test is not processing json at all, but it works as expected in the application. Yeah, I wrote it afterwards, but the question is - why it's not working? I tried also to create it in place, inherit from RESTSerializer, create models in place, but none of that worked.
Maybe someone can give a clue?
update
looks like everything begins in the
isPrimaryType: function (store, typeName, primaryTypeClass) {
var typeClass = store.modelFor(typeName);
return typeClass.modelName === primaryTypeClass.modelName;
},
last string returns false, because of primaryTypeClass.modelName is undefined
Serializer unit test
import DS from 'ember-data';
import { moduleForModel, test } from 'ember-qunit';
import setupStore from 'app/tests/helpers/setup-store';
import Profile from 'app/models/profile';
import Email from 'app/models/email';
import Address from 'app/models/address';
import ProfileSerializer from 'app/serializers/profile';
var env;
moduleForModel('profile', 'Unit | Serializer | profile', {
needs: ['serializer:profile', 'serializer:email', 'serializer:address', 'model:contactable', 'model:email', 'model:address'],
beforeEach: function () {
env = setupStore({
profile: Profile,
email: Email,
address: Address
});
env.registry.register('serializer:profile', ProfileSerializer);
env.profileSerializer = env.container.lookup('serializer:profile');
},
teardown: function() {
Ember.run(env.store, 'destroy');
}
});
test('it converts embedded records attributes', function(assert) {
// expect(3);
let payload = {
id: 1,
first_name: "Carlo",
last_name: "Schuppe",
company: "Metz-Witting",
birthday: "01-10-1985",
photo: null,
emails: [{address: "foo#bar.baz", id: 1, type: "main"}],
addresses: [{city: "Brooklyn", id: 1, type: "main"}]
},
parsed = {
"data":
{
"id":"1",
"type":"profile",
"attributes": { "firstName":"Carlo","lastName":"Schuppe","company":"Metz-Witting","birthday":"01-10-1985","photo":null },
"relationships": {
"emails": { "data": [{"id":"1","type":"email"}] },
"addresses": { "data": [{"id":"1","type":"address"}] }
}
},
"included":[
{"id":"1","type":"email","attributes":{"address":"foo#bar.baz", "kind": "main"},"relationships":{"contactable":{"data":{"type":"profile","id":"1"}}}},
{"id":"1","type":"address","attributes":{"city":"Brooklyn", "kind": "main"},"relationships":{"contactable":{"data":{"type":"profile","id":"1"}}}}
]
},
find, update, findAllRecordsJSON;
Ember.run(function() {
find = env.profileSerializer.normalizeResponse(env.store, Profile, payload, '1', 'findRecord');
// update = env.profileSerializer.normalizeResponse(env.store, Profile, payload, '1', 'updateRecord');
// findAllRecordsJSON = env.profileSerializer.normalizeResponse(env.store, Profile, payload, '1', 'findAll');
});
assert.deepEqual(find, parsed);
// assert.deepEqual(update, parsed);
// assert.deepEqual(findAllRecordsJSON, parsed);
});
setup_store.js
import Ember from 'ember';
import DS from 'ember-data';
// import ActiveModelAdapter from 'active-model-adapter';
// import ActiveModelSerializer from 'active-model-adapter/active-model-serializer';
export default function setupStore(options) {
var container, registry;
var env = {};
options = options || {};
if (Ember.Registry) {
registry = env.registry = new Ember.Registry();
container = env.container = registry.container();
} else {
container = env.container = new Ember.Container();
registry = env.registry = container;
}
env.replaceContainerNormalize = function replaceContainerNormalize(fn) {
if (env.registry) {
env.registry.normalize = fn;
} else {
env.container.normalize = fn;
}
};
var adapter = env.adapter = (options.adapter || '-default');
delete options.adapter;
if (typeof adapter !== 'string') {
env.registry.register('adapter:-ember-data-test-custom', adapter);
adapter = '-ember-data-test-custom';
}
for (var prop in options) {
registry.register('model:' + Ember.String.dasherize(prop), options[prop]);
}
registry.register('store:main', DS.Store.extend({
adapter: adapter
}));
registry.optionsForType('serializer', { singleton: false });
registry.optionsForType('adapter', { singleton: false });
registry.register('adapter:-default', DS.Adapter);
registry.register('serializer:-default', DS.JSONSerializer);
registry.register('serializer:-rest', DS.RESTSerializer);
registry.register('serializer:-rest-new', DS.RESTSerializer.extend({ isNewSerializerAPI: true }));
registry.register('adapter:-active-model', DS.ActiveModelAdapter);
registry.register('serializer:-active-model', DS.ActiveModelSerializer.extend({isNewSerializerAPI: true}));
registry.register('adapter:-rest', DS.RESTAdapter);
registry.injection('serializer', 'store', 'store:main');
registry.register('transform:string', DS.StringTransform);
registry.register('transform:number', DS.NumberTransform);
registry.register('transform:date', DS.DateTransform);
registry.register('transform:main', DS.Transform);
env.serializer = container.lookup('serializer:-default');
env.restSerializer = container.lookup('serializer:-rest');
env.restNewSerializer = container.lookup('serializer:-rest-new');
env.store = container.lookup('store:main');
env.adapter = env.store.get('defaultAdapter');
env.registry.register('serializer:-active-model', DS.ActiveModelSerializer.extend({isNewSerializerAPI: true}));
env.registry.register('adapter:-active-model', DS.ActiveModelAdapter);
env.registry.register('serializer:application', DS.ActiveModelSerializer.extend({isNewSerializerAPI: true}));
return env;
}
output
{
"data": null,
"included": []
}
I am using Ember-Data and one of my properties is a dictionary data structure. I'd like any update to this dictionary to be converted into an action which sets the parent Model into a "dirty" state.
So here's the config:
Model
export default DS.Model.extend({
// standard types
foo: DS.attr('string'),
bar: DS.attr('number'),
baz: DS.attr('boolean'),
// dictionary (aka, flexible set of name value pairs)
dictionary: DS.attr('object')
});
Transform
export default DS.Transform.extend({
deserialize: function(serialized) {
return Ember.Object.create(serialized);
},
serialize: function(deserialized) {
return deserialized;
}
});
This works and let's assume for a moment that the "dictionary" property is defined as:
{
one: { prop1: foo, prop2: bar, prop3: baz },
two: 2,
three: "howdy",
many: [{},{},{}]
}
This means that an Ember Object has four properties. These properties can be a string, a number, an array, or an object. What I'd like is to have some way of identifying any changes to this underlying basket of attributes so I can propagate that to the Model and have it adjust its state to "dirty".
TL;DR - Working JS Bin example
In order to accomplish this you have to do the following:
1. Deserialize the raw object and all its nested deep properties to Ember Objects so they could be Observable
2. Add observers to your model for all existing keys dynamically on every change of the raw object reference, because it can change its content and scheme.
3. Remove these dynamic observers on every raw object reference change and assign the new ones
4. All dynamic properties changes will set timestamp property so that controllers could listen to it
This is a "Deep" transform I wrote in order to accomplish (1):
// app/transforms/deep.js
export
default DS.Transform.extend({
deserializeRecursively: function(toTraverse) {
var hash;
if (Ember.isArray(toTraverse)) {
return Ember.A(toTraverse.map(function(item) {
return this.deserializeRecursively(item);
}, this));
} else if (!Ember.$.isPlainObject(toTraverse)) {
return toTraverse;
} else {
hash = this.generatePlainObject(Ember.keys(toTraverse), Ember.keys(toTraverse).map(function(key) {
return this.deserializeRecursively(Ember.get(toTraverse, key));
}, this));
return Ember.Object.create(hash);
}
},
deserialize: function(serialized) {
return this.deserializeRecursively(serialized);
},
serialize: function(deserialized) {
return deserialized;
},
generatePlainObject: function(keys, values) {
var ret = {};
keys.forEach(function(key, i) {
ret[key] = values[i];
});
return ret;
}
});
This is a mixin for Models with deep raw objects which accomplish (2) & (3) & (4)
// app/mixins/dynamic-observable.js
export
default Ember.Mixin.create({
propertiesToAnalyze: [],
registerRecursively: function(toTraverse, path, propsToObserve) {
if (Ember.isArray(toTraverse)) {
propsToObserve.addObject(path + '.#each');
if (toTraverse.length > 0) {
this.registerRecursively(toTraverse[0], path + '.#each', propsToObserve);
}
} else if (!(toTraverse instanceof Ember.Object)) {
propsToObserve.addObject(path);
} else {
Ember.keys(toTraverse).forEach(function(propertyName) {
this.registerRecursively(Ember.get(toTraverse, propertyName), path + '.' + propertyName, propsToObserve);
}, this);
}
},
addDynamicObserver: function(propertyNameToAnalyze) {
var propertyToAnalyze = this.get(propertyNameToAnalyze),
propsToObserve = Ember.A([]),
currentDynamicProps = this.get('currentDynamicProps'),
propsToRemove = currentDynamicProps.filter(function(prop) {
return new RegExp('^' + prop + '.').test(prop);
});
propsToRemove.forEach(function(prop) {
Ember.removeObserver(prop, this, dynamicPropertiesObserver)
}, this);
currentDynamicProps.removeObjects(propsToRemove);
this.registerRecursively(propertyToAnalyze, propertyNameToAnalyze, propsToObserve);
propsToObserve.forEach(function(prop) {
Ember.addObserver(this, prop, this, 'dynamicPropertiesObserver');
}, this);
currentDynamicProps.addObjects(propsToObserve);
},
dynamicPropertiesObserver: function(sender, key, value, rev) {
this.set('dynamicPropertyTimestamp', new Date().getTime())
},
addDynamicObservers: function() {
this.get('propertiesToAnalyze').forEach(this.addDynamicObserver, this);
},
init: function() {
this._super();
this.get('propertiesToAnalyze').forEach(function(prop) {
Ember.addObserver(this, prop, this, Ember.run.bind(this, this.addDynamicObserver, prop));
}, this);
},
dynamicPropertyTimestamp: null,
currentDynamicProps: Ember.A([])
});
This is how you use the mixin on a model:
// app/models/some-object.js
import DynamicObservable from 'app/mixins/dynamic-observable';
export
default DS.Model.extend(DynamicObservable, {
dictionary: DS.attr('deep'),
propertiesToAnalyze: ['dictionary']
});
Finally, this is an array controller which its model is an array of some-object models
export
default Ember.ArrayController.extend({
message: '',
observeDictionaries: function() {
this.set('message', 'A dictionary has been changed. change time: ' + new Date().getTime());
}.observes('#each.dynamicPropertyTimestamp')
});
Edited as I narrowed down issues....
I'm working on an app that is taking in an API - data as JSON. It is an array of groups, with "name", "id", and other such elements.
{
"status": "success",
"data": {
"groups": [
{
"id": 7100,
"name": "Test 12345",
"kind": "floor",
"parent_group_id": 7000,
"controlled_device_type_count": {},
"is_top_level": true
}
]
}}
I also have a livestream websocket - data as JSON stream. It should update the elements referenced in the first API. The two only share "id".
Livestream:
{
"group":{
"usage":{
"10":1,
"20":0,
"30":2,
"40":2
},
"last_change":"2014-03-24T05:56:10Z",
"id":7954
}}
**Updated...**My IndexRoute:
App.ApplicationAdapter = DS.RESTAdapter.extend({
extractArray: function(store, type, payload, id, requestType) {
payload = payload.data;
return this._super(store, type, payload, id, requestType);
}
});
App.IndexRoute = Ember.Route.extend({
sortProperties: ['id'],
sortAscending: true,
beforeModel: function() {
var socket = window.io.connect('http://localhost:8887');
var self = this;
socket.on('group_live_stream', function(data){
var dataObj = JSON.parse(data);
self.store.push('group',dataObj.group);
});
},
actions: {
toggleMenu: function() {
this.controller.toggleProperty('menuVisible');
this.controller.pushBody();
} },
activate: function() {
var self = this;
$.getJSON('http://localhost:3000/api/groups/top?subscribe=true').then(function(data) {
self.store.pushMany('group', data.data.groups);
});
},
model: function() {
return this.store.all('group');
}
});
Updated:
So now I see the livestream coming through - and for a brief, immediate second - I see the API data (group name) and then it disappears. I'm thinking it's because I'm just "pushMany"-ing records and deleting old ones instead of updating. I've heard/read that pushPayload might be my solution....but I can't figure it out. At all (and when I put it in, I just get an error: "Uncaught Error: No model was found for '0' " Help?!
Any thoughts?
Thanks so much!
ptep
Your API's payload is not in the format that ember-data expects (it looks for your payload in the root of your JSON by default). You'll need to override extractArray (and likely extractSingle) on your ApplicationAdapter something like this:
ApplicationAdapter = DS.RESTAdapter.extend({
extractArray: function(store, type, payload, id, requestType) {
payload = payload.data;
return this._super(store, type, payload, id, requestType);
}
});
I'm using Ember.js 1.0.0 and Ember-Data-beta2.
I have a model Product which belongsTo Company. When creating a product, the user can select which company it belongsTo in a dropdown menu. This adds "company":"25" to the form post, which for the most part is just what I want. Instead of "company", however, I want the form to submit "company_id". How can I change this?
From what I can tell, Ember-Data serializers only normalize incoming data, not outgoing data. Would this be handled in the adapter? If so, how do I communicate this convention to Ember?
Use the ActiveModelAdapter (see PR here):
App.ApplicationAdapter = DS.ActiveModelAdapter.extend();
Edit: This solution is outdated, use ActiveModelAdapter instead, as suggested by Panagiotis Panagi.
With the recent versions of ember-data you have to override the serializer, in order to get "rails freindly behaivor". Like so:
// See https://github.com/emberjs/data/blob/master/TRANSITION.md
// and http://discuss.emberjs.com/t/changes-with-ds-restadapter/2406/8
App.ApplicationSerializer = DS.RESTSerializer.extend({
normalize: function(type, hash, property) {
var normalized = {}, normalizedProp;
for (var prop in hash) {
if (prop.substr(-3) === '_id') {
// belongsTo relationships
normalizedProp = prop.slice(0, -3);
} else if (prop.substr(-4) === '_ids') {
// hasMany relationship
normalizedProp = Ember.String.pluralize(prop.slice(0, -4));
} else {
// regualarAttribute
normalizedProp = prop;
}
normalizedProp = Ember.String.camelize(normalizedProp);
normalized[normalizedProp] = hash[prop];
}
return this._super(type, normalized, property);
},
serialize: function(record, options) {
json = {}
record.eachAttribute(function(name) {
json[name.underscore()] = record.get(name)
})
record.eachRelationship(function(name, relationship) {
if (relationship.kind == 'hasMany') {
key = name.singularize().underscore() + '_ids'
json[key] = record.get(name).mapBy('id')
} else {
key = name.underscore() + '_id'
json[key] = record.get(name + '.id')
}
});
if (options && options.includeId) {
json.id = record.get('id')
}
return json
},
typeForRoot: function(root) {
var camelized = Ember.String.camelize(root);
return Ember.String.singularize(camelized);
},
serializeIntoHash: function(data, type, record, options) {
var root = Ember.String.decamelize(type.typeKey);
data[root] = this.serialize(record, options);
},
serializeAttribute: function(record, json, key, attribute) {
var attrs = Ember.get(this, 'attrs');
var value = Ember.get(record, key), type = attribute.type;
if (type) {
var transform = this.transformFor(type);
value = transform.serialize(value);
}
// if provided, use the mapping provided by `attrs` in
// the serializer
key = attrs && attrs[key] || Ember.String.decamelize(key);
json[key] = value;
}
});
I hope this helps.
I have two models:
App.User = DS.Model.create({
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.create({
user: DS.belongsTo('App.User')
});
When a user is deleted, it also will delete all its comments on the backend, so I should delete them from the client-side identity map.
I'm listing all the comments on the system from another place, so after deleting a user it would just crash.
Is there any way to specify this kind of dependency on the association? Thanks!
I use a mixin when I want to implement this behaviour. My models are defined as follows:
App.Post = DS.Model.extend(App.DeletesDependentRelationships, {
dependentRelationships: ['comments'],
comments: DS.hasMany('App.Comment'),
author: DS.belongsTo('App.User')
});
App.User = DS.Model.extend();
App.Comment = DS.Model.extend({
post: DS.belongsTo('App.Post')
});
The mixin itself:
App.DeletesDependentRelationships = Ember.Mixin.create({
// an array of relationship names to delete
dependentRelationships: null,
// set to 'delete' or 'unload' depending on whether or not you want
// to actually send the deletions to the server
deleteMethod: 'unload',
deleteRecord: function() {
var transaction = this.get('store').transaction();
transaction.add(this);
this.deleteDependentRelationships(transaction);
this._super();
},
deleteDependentRelationships: function(transaction) {
var self = this;
var klass = Ember.get(this.constructor.toString());
var fields = Ember.get(klass, 'fields');
this.get('dependentRelationships').forEach(function(name) {
var relationshipType = fields.get(name);
switch(relationshipType) {
case 'belongsTo': return self.deleteBelongsToRelationship(name, transaction);
case 'hasMany': return self.deleteHasManyRelationship(name, transaction);
}
});
},
deleteBelongsToRelationship: function(name, transaction) {
var record = this.get(name);
if (record) this.deleteOrUnloadRecord(record, transaction);
},
deleteHasManyRelationship: function(key, transaction) {
var self = this;
// deleting from a RecordArray doesn't play well with forEach,
// so convert to a normal array first
this.get(key).toArray().forEach(function(record) {
self.deleteOrUnloadRecord(record, transaction);
});
},
deleteOrUnloadRecord: function(record, transaction) {
var deleteMethod = this.get('deleteMethod');
if (deleteMethod === 'delete') {
transaction.add(record);
record.deleteRecord();
}
else if (deleteMethod === 'unload') {
var store = this.get('store');
store.unloadRecord(record);
}
}
});
Note that you can specify via deleteMethod whether or not you want to send the DELETE requests to your API. If your back-end is configured to delete dependent records automatically, then you will want to use the default.
Here's a jsfiddle that shows it in action.
A quick-and-dirty way would be to add the following to your user model
destroyRecord: ->
#get('comments').invoke('unloadRecord')
#_super()
I adapted the answer of #ahmacleod to work with ember-cli 2.13.1 and ember-data 2.13.0. I had an issue with nested relationships and the fact that after deleting an entity from the database its id was reused. This lead to conflicts with remnants in the ember-data model.
import Ember from 'ember';
export default Ember.Mixin.create({
dependentRelationships: null,
destroyRecord: function() {
this.deleteDependentRelationships();
return this._super()
.then(function (model) {
model.unloadRecord();
return model;
});
},
unloadRecord: function() {
this.deleteDependentRelationships();
this._super();
},
deleteDependentRelationships: function() {
var self = this;
var fields = Ember.get(this.constructor, 'fields');
this.get('dependentRelationships').forEach(function(name) {
self.deleteRelationship(name);
});
},
deleteRelationship (name) {
var self = this;
self.get(name).then(function (records) {
if (!records) {
return;
}
var reset = [];
if (!Ember.isArray(records)) {
records = [records];
reset = null;
}
records.forEach(function(record) {
if (record) {
record.unloadRecord();
}
});
self.set(name, reset);
});
},
});
Eventually, I had to set the relationship to [] (hasMany) or null (belongsTo). Else I would have run into the following error message:
Assertion Failed: You cannot update the id index of an InternalModel once set. Attempted to update <id>.
Maybe this is helpful for somebody else.