SlideShare a Scribd company logo
Normalizing with Ember
Data 1.0b
Jeremy Gillick
or
True Facts
of Using Data in Ember
I’m Jeremy
http://mozmonkey.com
https://github.com/jgillick/
https://linkedin.com/in/jgillick
I work at Nest
We love Ember
depending on the day
Ember Data is Great
Except when data feeds don’t conform
Serializers connect Raw
Data to Ember Data
{ … }
JSON
Serializer
Ember Data
Let’s talk about data
Ember prefers side loading
to nested JSON
But why?
For example
{!
"posts": [!
{!
"id": 5,!
"title":You won't believe what was hiding in this kid's locker",!
"body": "...",!
"author": {!
"name": "Jeremy Gillick",!
"role": "Author",!
"email": "spam-me@please.com"!
}!
}!
]!
}
{!
"posts": [!
{!
"id": 6,!
"title": "New Study: Apricots May Help Cure Glaucoma",!
"body": "...",!
"author": {!
"name": "Jeremy Gillick",!
"role": "Author",!
"email": "spam-me@please.com"!
}!
},!
{!
"id": 5,!
"title": "You won't believe what was hiding in this kid's locker",!
"body": "...",!
"author": {!
"name": "Jeremy Gillick",!
"role": "Author",!
"email": "spam-me@please.com"!
}!
}!
]!
}
For example
Redundant, adds feed bloat and
which one is the source of truth?
This is better
{!
"posts": [!
{!
"id": 4,!
"title": "New Study: Apricots May Help Cure Glaucoma",!
"body": "...",!
"author": 42!
},!
{!
"id": 5,!
"title": "You won't believe what was hiding in this kid's locker",!
"body": "...",!
"author": 42!
}!
],!
"users": [!
{!
"id": 42,!
"name": "Jeremy Gillick",!
"role": "Author",!
"email": "spam-me@please.com"!
}!
]!
}
Ember Data Expects
{!
"modelOneRecord": {!
...!
}!
"modelTwoRecords": [!
{ ... },!
{ ... }!
],!
"modelThreeRecords": [!
{ ... },!
{ ... }!
]!
}
No further nesting is allowed
Ember Data Expects
{!
"posts": [!
...!
],!
!
"users": [!
…!
]!
}
App.Post records
App.User records
Not all JSON APIs will be flat
A nested world
{!
"products": [!
{!
"name": "Robot",!
"description": "A robot may not injure a human being or...",!
"price": {!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", "black", "#E1563F"]!
}!
]!
}!
]!
}
Ember Data can’t process that
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": {!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
]!
}
{!
"products": [!
{!
"id": "product-1",!
"name": "Robot",!
"description": “...”,!
"price": "price-1",!
"size": "dimension-1",!
"options": [!
“options-1”!
]!
}!
],!
"prices": [!
{!
"id": "price-1",!
"value": 59.99,!
"currency": "USD"!
} !
]!
"dimensions": [ … ],!
"options": [ … ]!
}!
!
Flatten that feed
How do we do this?
With a custom Ember Data Serializer!
Two common ways
• Create ProductSerializer that manually converts the
JSON
• A lot of very specific code that you’ll have to repeat for all nested
JSON payloads.
• Build a generic serializer that automatically flattens
nested JSON objects
• Good, generic, DRY
Defining the model
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": {!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
]!
}
App.Product = DS.Model.extend({!
name: DS.attr('string'),!
description: DS.attr('string'),!
price: DS.belongsTo('Price'),!
size: DS.belongsTo('Dimension'),!
options: DS.hasMany('Option')!
});!
!
App.Price = DS.Model.extend({!
value: DS.attr('number'),!
currency: DS.attr('string')!
});!
!
App.Dimension = DS.Model.extend({!
height: DS.attr('number'),!
width: DS.attr('number'),!
depth: DS.attr('number')!
});!
!
App.Option = DS.Model.extend({!
name: DS.attr('string'),!
values: DS.attr()!
});
Steps
• Loop through all root JSON properties
• Determine which model they represent
• Get all the relationships for that model
• Side load any of those relationships
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": {!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
]!
}
App.Product
Relationships
• price
• size
• option
Side load
$$$ Profit $$$
JS Methods
extract: function(store, type, payload, id, requestType) { ... }
processRelationships: function(store, type, payload, hash) { ... }
sideloadRecord: function(store, type, payload, hash) { ... }
Create a Serializer
/**!
Deserialize a nested JSON payload into a flat object!
with sideloaded relationships that Ember Data can import.!
*/!
App.NestedSerializer = DS.RESTSerializer.extend({!
!
/**!
(overloaded method)!
Deserialize a JSON payload from the server.!
!
@method normalizePayload!
@param {Object} payload!
@return {Object} the normalized payload!
*/!
extract: function(store, type, payload, id, requestType) {!
return this._super(store, type, payload, id, requestType);!
}!
!
});
{!
"products": [!
{!
...!
}!
]!
}
extract: function(store, type, payload, id, requestType) {!
return this._super(store, type, payload, id, requestType);!
}
{!
"products": [!
{!
...!
}!
]!
}
extract: function(store, type, payload, id, requestType) {!
var rootKeys = Ember.keys(payload);!
!
// Loop through root properties and process their relationships!
rootKeys.forEach(function(key){!
!
}, this);!
!
return this._super(store, type, payload, id, requestType);!
}
extract: function(store, type, payload, id, requestType) {!
var rootKeys = Ember.keys(payload);!
!
// Loop through root properties and process their relationships!
rootKeys.forEach(function(key){!
var type = store.container!
! ! ! ! .lookupFactory('model:' + key.singularize());!
!
}, this);!
!
return this._super(store, type, payload, id, requestType);!
}
{!
"products": [!
{!
...!
}!
]!
}
{!
"products": [!
{!
...!
}!
]!
}
product
Singularize
container.lookup(‘model:product’)
App.Product
"products"
extract: function(store, type, payload, id, requestType) {!
var rootKeys = Ember.keys(payload);!
!
// Loop through root properties and process their relationships!
rootKeys.forEach(function(key){!
var type = store.container!
! ! ! ! .lookupFactory('model:' + key.singularize());!
!
}, this);!
!
return this._super(store, type, payload, id, requestType);!
}
{!
"products": [!
{!
...!
}!
]!
}
{!
"products": !
[!
{!
...!
}!
]!
}
extract: function(store, type, payload, id, requestType) {!
var rootKeys = Ember.keys(payload);!
!
// Loop through root properties and process their relationships!
rootKeys.forEach(function(key){!
var type = store.container!
.lookupFactory('model:' + key.singularize()),!
hash = payload[key];!
!
}, this);!
!
return this._super(store, type, payload, id, requestType);!
}
extract: function(store, type, payload, id, requestType) {!
var rootKeys = Ember.keys(payload);!
!
// Loop through root properties and process their relationships!
rootKeys.forEach(function(key){!
var type = store.container!
.lookupFactory('model:' + key.singularize()),!
hash = payload[key];!
!
// Sideload embedded relationships of this model hash!
if (type) {!
this.processRelationships(store, type, payload, hash);!
}!
}, this);!
!
return this._super(store, type, payload, id, requestType);!
}
{!
"products": !
[!
{!
...!
}!
]!
}
/**!
Process nested relationships on a single hash record!
!
@method extractRelationships!
@param {DS.Store} store!
@param {DS.Model} type!
@param {Object} payload The entire payload!
@param {Object} hash The hash for the record being processed!
@return {Object} The updated hash object!
*/!
processRelationships: function(store, type, payload, hash) {!
!
},
{!
"products": [!
{!
...!
}!
]!
}
processRelationships: function(store, type, payload, hash) {!
!
// If hash is an array, process each item in the array!
if (hash instanceof Array) {!
hash.forEach(function(item, i){!
hash[i] = this.processRelationships(store, type, payload, item);!
}, this);!
}!
!
return hash;!
},
{!
"products": [!
{!
...!
}!
]!
}
processRelationships: function(store, type, payload, hash) {!
!
// If hash is an array, process each item in the array!
if (hash instanceof Array) {!
hash.forEach(function(item, i){!
hash[i] = this.processRelationships(store, type, payload, item);!
}, this);!
}!
!
else {!
!
}!
!
return hash;!
},
{!
"products": [!
{!
...!
}!
]!
}
processRelationships: function(store, type, payload, hash) {!
!
// If hash is an array, process each item in the array!
if (hash instanceof Array) {!
hash.forEach(function(item, i){!
hash[i] = this.processRelationships(store, type, payload, item);!
}, this);!
}!
!
else {!
!
// Find all relationships in this model!
type.eachRelationship(function(key, relationship) {!
!
}, this);!
}!
!
return hash;!
},
!
App.Product.eachRelationship(function(key, relationship) {!
!
!
}, this);!
App.Product = DS.Model.extend({!
name: DS.attr('string'),!
description: DS.attr('string'),!
price: DS.belongsTo('Price'),!
size: DS.belongsTo('Dimension'),!
options: DS.hasMany('Option')!
});
key = 'price'!
relationship = {!
"type": App.Price,!
"kind": "belongsTo",!
...!
}
key = 'size'!
relationship = {!
"type": App.Dimension,!
"kind": "belongsTo",!
...!
}
key = 'options'!
relationship = {!
"type": App.Option,!
"kind": "hasMany",!
...!
}
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": !
{!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
]!
}
processRelationships: function(store, type, payload, hash) {!
!
// If hash is an array, process each item in the array!
if (hash instanceof Array) {!
hash.forEach(function(item, i){!
hash[i] = this.processRelationships(store, type, payload, item);!
}, this);!
}!
!
else {!
!
// Find all relationships in this model!
type.eachRelationship(function(key, relationship) {!
var related = hash[key]; // The hash for this relationship!
!
}, this);!
}!
!
return hash;!
},
processRelationships: function(store, type, payload, hash) {!
!
// If hash is an array, process each item in the array!
if (hash instanceof Array) {!
hash.forEach(function(item, i){!
hash[i] = this.processRelationships(store, type, payload, item);!
}, this);!
}!
!
else {!
!
// Find all relationships in this model!
type.eachRelationship(function(key, relationship) {!
var related = hash[key], // The hash for this relationship!
relType = relationship.type; // The model for this relationship
!
}, this);!
}!
!
return hash;!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": !
{!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
]!
}
App.Price
processRelationships: function(store, type, payload, hash) {!
!
// If hash is an array, process each item in the array!
if (hash instanceof Array) {!
hash.forEach(function(item, i){!
hash[i] = this.processRelationships(store, type, payload, item);!
}, this);!
}!
!
else {!
!
// Find all relationships in this model!
type.eachRelationship(function(key, relationship) {!
var related = hash[key], !
relType = relationship.type;!
!
hash[key] = this.sideloadRecord(store, relType, payload, related);!
!
}, this);!
}!
!
return hash;!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": !
{!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
]!
}
/**!
Sideload a record hash to the payload!
!
@method sideloadRecord!
@param {DS.Store} store!
@param {DS.Model} type!
@param {Object} payload The entire payload!
@param {Object} hash The record hash object!
@return {Object} The ID of the record(s) sideloaded!
*/!
sideloadRecord: function(store, type, payload, hash) {!
!
},
sideloadRecord: function(store, type, payload, hash) {!
var id, sideLoadkey, sideloadArr, serializer;!
!
// If hash is an array, sideload each item in the array!
if (hash instanceof Array) {!
id = [];!
hash.forEach(function(item, i){!
id[i] = this.sideloadRecord(store, type, payload, item);!
}, this);!
}!
!
return id;!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": !
{!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
]!
}
sideloadRecord: function(store, type, payload, hash) {!
var id, sideLoadkey, sideloadArr, serializer;!
!
// If hash is an array, sideload each item in the array!
if (hash instanceof Array) {!
id = [];!
hash.forEach(function(item, i){!
id[i] = this.sideloadRecord(store, type, payload, item);!
}, this);!
}!
// Sideload record!
else if (typeof hash === 'object') {!
!
}!
return id;!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": !
{!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
]!
}
sideloadRecord: function(store, type, payload, hash) {!
var id, sideLoadkey, sideloadArr, serializer;!
!
// If hash is an array, sideload each item in the array!
if (hash instanceof Array) {!
id = [];!
hash.forEach(function(item, i){!
id[i] = this.sideloadRecord(store, type, payload, item);!
}, this);!
}!
// Sideload record!
else if (typeof hash === 'object') {!
sideLoadkey = type.typeKey.pluralize(); !
sideloadArr = payload[sideLoadkey] || [];!
}!
!
return id;!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": !
{!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
],!
"prices": [!
]!
}
sideloadRecord: function(store, type, payload, hash) {!
var id, sideLoadkey, sideloadArr, serializer;!
!
// If hash is an array, sideload each item in the array!
if (hash instanceof Array) {!
id = [];!
hash.forEach(function(item, i){!
id[i] = this.sideloadRecord(store, type, payload, item);!
}, this);!
}!
// Sideload record!
else if (typeof hash === 'object') {!
sideLoadkey = type.typeKey.pluralize(); !
sideloadArr = payload[sideLoadkey] || [];!
id = this.generateID(store, type, hash);!
}!
!
return id;!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": !
{!
"id": “generated-1",!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
],!
"prices": [!
]!
}
Every record needs an ID
sideloadRecord: function(store, type, payload, hash) {!
var id, sideLoadkey, sideloadArr, serializer;!
!
// If hash is an array, sideload each item in the array!
if (hash instanceof Array) {!
id = [];!
hash.forEach(function(item, i){!
id[i] = this.sideloadRecord(store, type, payload, item);!
}, this);!
}!
// Sideload record!
else if (typeof hash === 'object') {!
sideLoadkey = type.typeKey.pluralize(); !
sideloadArr = payload[sideLoadkey] || [];!
!
// Sideload, if it's not already sideloaded!
if (sideloadArr.findBy('id', id) === undefined){!
sideloadArr.push(hash);!
payload[sideLoadkey] = sideloadArr;!
}!
}!
!
return id;!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": !
{!
"id": “generated-1",!
"value": 59.99,!
"currency": "USD"!
},!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
],!
"prices": [!
{!
"id": “generated-1",!
"value": 59.99,!
"currency": "USD"!
}!
]!
}
sideloadRecord: function(store, type, payload, hash) {!
var id, sideLoadkey, sideloadArr, serializer;!
!
// If hash is an array, sideload each item in the array!
if (hash instanceof Array) {!
id = [];!
hash.forEach(function(item, i){!
id[i] = this.sideloadRecord(store, type, payload, item);!
}, this);!
}!
// Sideload record!
else if (typeof hash === 'object') {!
sideLoadkey = type.typeKey.pluralize(); !
sideloadArr = payload[sideLoadkey] || [];!
!
// Sideload, if it's not already sideloaded!
if (sideloadArr.findBy('id', id) === undefined){!
sideloadArr.push(hash);!
payload[sideLoadkey] = sideloadArr;!
}!
}!
!
return id;!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": "generated-1",!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
],!
"prices": [!
{!
"id": "generated-1",!
"value": 59.99,!
"currency": "USD"!
}!
]!
}
processRelationships: function(store, type, payload, hash) {!
...!
hash[key] = this.sideloadRecord(store, relType, payload, related);!
...!
},
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": "generated-1",!
"size": "generated-2",!
"options": [!
“generated-3”!
]!
}!
],!
"prices": [!
{!
"id": "generated-1",!
"value": 59.99,!
"currency": "USD"!
}!
],!
"dimensions": [{!
"id": "generated-2",!
"height": 24,!
"width": 12,!
"depth": 14!
}],!
"options": [ !
{!
"id": "generated-3",!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}
{!
"products": [!
{!
"name": "Robot",!
"description": "...",!
"price": "generated-1",!
"size": {!
"height": 24,!
"width": 12,!
"depth": 14!
},!
"options": [!
{!
"name": "Color",!
"values": ["silver", !
"black", !
"#E1563F"]!
}!
]!
}!
],!
"prices": [!
{!
"id": "generated-1",!
"value": 59.99,!
"currency": "USD"!
}!
]!
}
Apply the Serializer
App.ApplicationSerializer = App.NestedSerializer;
App.ProductSerializer = App.NestedSerializer.extend({});
- OR -
Now for a demo
http://emberjs.jsbin.com/neriyi/edit
http://emberjs.jsbin.com/neriyi/edit
Questions?
http://www.slideshare.net/JeremyGillick/normalizing-data

More Related Content

PDF
Es.next
PDF
Alfresco tech talk live public api episode 64
PDF
Leveraging the Power of Graph Databases in PHP
PDF
Leveraging the Power of Graph Databases in PHP
ODP
Php 102: Out with the Bad, In with the Good
KEY
Developing With Django
PDF
Penetration Testing with Improved Input Vector Identification
PDF
Assetic (OSCON)
Es.next
Alfresco tech talk live public api episode 64
Leveraging the Power of Graph Databases in PHP
Leveraging the Power of Graph Databases in PHP
Php 102: Out with the Bad, In with the Good
Developing With Django
Penetration Testing with Improved Input Vector Identification
Assetic (OSCON)

What's hot (12)

PDF
Assetic (Zendcon)
PPTX
Why everyone like ruby
PDF
20th.陈晓鸣 百度海量日志分析架构及处理经验分享
PPTX
GraphQL
PPTX
CSS: A Slippery Slope to the Backend
KEY
Learning How To Use Jquery #3
PDF
Elixir + Neo4j
PDF
Learn You a Functional JavaScript for Great Good
PDF
NoSQL & MongoDB
PDF
Tearing the Sofa Apart: CouchDB and CouchApps from a Beginner's Perspective
KEY
Djangocon
KEY
Arel - Ruby Relational Algebra
Assetic (Zendcon)
Why everyone like ruby
20th.陈晓鸣 百度海量日志分析架构及处理经验分享
GraphQL
CSS: A Slippery Slope to the Backend
Learning How To Use Jquery #3
Elixir + Neo4j
Learn You a Functional JavaScript for Great Good
NoSQL & MongoDB
Tearing the Sofa Apart: CouchDB and CouchApps from a Beginner's Perspective
Djangocon
Arel - Ruby Relational Algebra
Ad

Viewers also liked (17)

PPTX
Quadrant holdings issa asad
PPTX
Detecting Reconnaissance Through Packet Forensics by Shashank Nigam
DOC
Gajendra_Resume1
PPT
Change Management 13 things to consider
PDF
Public business law
PPT
νεο λυκειο
PPTX
Top 5 reasons to explore Saona Island with Lifestyle Holidays Vacation Club
PDF
More about health
PDF
PPTX
Lifestyle Holidays Vacation Club Announces Whale Season In The Dominican Repu...
PPTX
Software Testing
PPT
Understanding patient privacy 1
PPTX
Brigade panorama
PPTX
The Best of Puerto Plata to See This Fall Revealed by Lifestyle Holidays Vac...
PPTX
iPad Communication Apps
PDF
Eyeonhome
PDF
Home smart home
Quadrant holdings issa asad
Detecting Reconnaissance Through Packet Forensics by Shashank Nigam
Gajendra_Resume1
Change Management 13 things to consider
Public business law
νεο λυκειο
Top 5 reasons to explore Saona Island with Lifestyle Holidays Vacation Club
More about health
Lifestyle Holidays Vacation Club Announces Whale Season In The Dominican Repu...
Software Testing
Understanding patient privacy 1
Brigade panorama
The Best of Puerto Plata to See This Fall Revealed by Lifestyle Holidays Vac...
iPad Communication Apps
Eyeonhome
Home smart home
Ad

Similar to Feed Normalization with Ember Data 1.0 (20)

PDF
"Writing Maintainable JavaScript". Jon Bretman, Badoo
PDF
Plugin jQuery, Design Patterns
PDF
Replacing Oracle with MongoDB for a templating application at the Bavarian go...
PDF
MongoDB Munich 2012: MongoDB for official documents in Bavaria
PDF
NUS iOS Swift Talk
PDF
Javascript patterns
PDF
HadoopとMongoDBを活用したソーシャルアプリのログ解析
PDF
RubyConf Portugal 2014 - Why ruby must go!
PDF
Finding Restfulness - Madrid.rb April 2014
PPTX
RESTful APIs: Promises & lies
PDF
FrontInBahia 2014: 10 dicas de desempenho para apps mobile híbridas
PDF
Ushahidi
PPTX
Beautiful REST+JSON APIs with Ion
PDF
ScotRuby - Dark side of ruby
PDF
RedDot Ruby Conf 2014 - Dark side of ruby
PDF
Swift Basics
KEY
The go-start webframework (GTUG Vienna 27.03.2012)
KEY
AMD - Why, What and How
PDF
Ingo Muschenetz: Titanium Studio Deep Dive
"Writing Maintainable JavaScript". Jon Bretman, Badoo
Plugin jQuery, Design Patterns
Replacing Oracle with MongoDB for a templating application at the Bavarian go...
MongoDB Munich 2012: MongoDB for official documents in Bavaria
NUS iOS Swift Talk
Javascript patterns
HadoopとMongoDBを活用したソーシャルアプリのログ解析
RubyConf Portugal 2014 - Why ruby must go!
Finding Restfulness - Madrid.rb April 2014
RESTful APIs: Promises & lies
FrontInBahia 2014: 10 dicas de desempenho para apps mobile híbridas
Ushahidi
Beautiful REST+JSON APIs with Ion
ScotRuby - Dark side of ruby
RedDot Ruby Conf 2014 - Dark side of ruby
Swift Basics
The go-start webframework (GTUG Vienna 27.03.2012)
AMD - Why, What and How
Ingo Muschenetz: Titanium Studio Deep Dive

Recently uploaded (20)

PDF
Best Practices for Testing and Debugging Shopify Third-Party API Integrations...
DOCX
Unit-3 cyber security network security of internet system
PPTX
introduction about ICD -10 & ICD-11 ppt.pptx
PPTX
E -tech empowerment technologies PowerPoint
PPTX
SAP Ariba Sourcing PPT for learning material
PDF
Paper PDF World Game (s) Great Redesign.pdf
PDF
Tenda Login Guide: Access Your Router in 5 Easy Steps
PPTX
international classification of diseases ICD-10 review PPT.pptx
PDF
Cloud-Scale Log Monitoring _ Datadog.pdf
PDF
💰 𝐔𝐊𝐓𝐈 𝐊𝐄𝐌𝐄𝐍𝐀𝐍𝐆𝐀𝐍 𝐊𝐈𝐏𝐄𝐑𝟒𝐃 𝐇𝐀𝐑𝐈 𝐈𝐍𝐈 𝟐𝟎𝟐𝟓 💰
PDF
Introduction to the IoT system, how the IoT system works
PDF
The Internet -By the Numbers, Sri Lanka Edition
PDF
SASE Traffic Flow - ZTNA Connector-1.pdf
PDF
FINAL CALL-6th International Conference on Networks & IOT (NeTIOT 2025)
PPTX
Introuction about ICD -10 and ICD-11 PPT.pptx
PPTX
presentation_pfe-universite-molay-seltan.pptx
PPTX
Power Point - Lesson 3_2.pptx grad school presentation
PPTX
Digital Literacy And Online Safety on internet
PPTX
artificial intelligence overview of it and more
PDF
WebRTC in SignalWire - troubleshooting media negotiation
Best Practices for Testing and Debugging Shopify Third-Party API Integrations...
Unit-3 cyber security network security of internet system
introduction about ICD -10 & ICD-11 ppt.pptx
E -tech empowerment technologies PowerPoint
SAP Ariba Sourcing PPT for learning material
Paper PDF World Game (s) Great Redesign.pdf
Tenda Login Guide: Access Your Router in 5 Easy Steps
international classification of diseases ICD-10 review PPT.pptx
Cloud-Scale Log Monitoring _ Datadog.pdf
💰 𝐔𝐊𝐓𝐈 𝐊𝐄𝐌𝐄𝐍𝐀𝐍𝐆𝐀𝐍 𝐊𝐈𝐏𝐄𝐑𝟒𝐃 𝐇𝐀𝐑𝐈 𝐈𝐍𝐈 𝟐𝟎𝟐𝟓 💰
Introduction to the IoT system, how the IoT system works
The Internet -By the Numbers, Sri Lanka Edition
SASE Traffic Flow - ZTNA Connector-1.pdf
FINAL CALL-6th International Conference on Networks & IOT (NeTIOT 2025)
Introuction about ICD -10 and ICD-11 PPT.pptx
presentation_pfe-universite-molay-seltan.pptx
Power Point - Lesson 3_2.pptx grad school presentation
Digital Literacy And Online Safety on internet
artificial intelligence overview of it and more
WebRTC in SignalWire - troubleshooting media negotiation

Feed Normalization with Ember Data 1.0

  • 1. Normalizing with Ember Data 1.0b Jeremy Gillick
  • 2. or
  • 3. True Facts of Using Data in Ember
  • 5. I work at Nest
  • 7. Ember Data is Great Except when data feeds don’t conform
  • 8. Serializers connect Raw Data to Ember Data { … } JSON Serializer Ember Data
  • 10. Ember prefers side loading to nested JSON But why?
  • 11. For example {! "posts": [! {! "id": 5,! "title":You won't believe what was hiding in this kid's locker",! "body": "...",! "author": {! "name": "Jeremy Gillick",! "role": "Author",! "email": "spam-me@please.com"! }! }! ]! }
  • 12. {! "posts": [! {! "id": 6,! "title": "New Study: Apricots May Help Cure Glaucoma",! "body": "...",! "author": {! "name": "Jeremy Gillick",! "role": "Author",! "email": "spam-me@please.com"! }! },! {! "id": 5,! "title": "You won't believe what was hiding in this kid's locker",! "body": "...",! "author": {! "name": "Jeremy Gillick",! "role": "Author",! "email": "spam-me@please.com"! }! }! ]! } For example Redundant, adds feed bloat and which one is the source of truth?
  • 13. This is better {! "posts": [! {! "id": 4,! "title": "New Study: Apricots May Help Cure Glaucoma",! "body": "...",! "author": 42! },! {! "id": 5,! "title": "You won't believe what was hiding in this kid's locker",! "body": "...",! "author": 42! }! ],! "users": [! {! "id": 42,! "name": "Jeremy Gillick",! "role": "Author",! "email": "spam-me@please.com"! }! ]! }
  • 14. Ember Data Expects {! "modelOneRecord": {! ...! }! "modelTwoRecords": [! { ... },! { ... }! ],! "modelThreeRecords": [! { ... },! { ... }! ]! } No further nesting is allowed
  • 15. Ember Data Expects {! "posts": [! ...! ],! ! "users": [! …! ]! } App.Post records App.User records
  • 16. Not all JSON APIs will be flat
  • 17. A nested world {! "products": [! {! "name": "Robot",! "description": "A robot may not injure a human being or...",! "price": {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", "black", "#E1563F"]! }! ]! }! ]! }
  • 18. Ember Data can’t process that
  • 19. {! "products": [! {! "name": "Robot",! "description": "...",! "price": {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ]! } {! "products": [! {! "id": "product-1",! "name": "Robot",! "description": “...”,! "price": "price-1",! "size": "dimension-1",! "options": [! “options-1”! ]! }! ],! "prices": [! {! "id": "price-1",! "value": 59.99,! "currency": "USD"! } ! ]! "dimensions": [ … ],! "options": [ … ]! }! ! Flatten that feed
  • 20. How do we do this? With a custom Ember Data Serializer!
  • 21. Two common ways • Create ProductSerializer that manually converts the JSON • A lot of very specific code that you’ll have to repeat for all nested JSON payloads. • Build a generic serializer that automatically flattens nested JSON objects • Good, generic, DRY
  • 22. Defining the model {! "products": [! {! "name": "Robot",! "description": "...",! "price": {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ]! } App.Product = DS.Model.extend({! name: DS.attr('string'),! description: DS.attr('string'),! price: DS.belongsTo('Price'),! size: DS.belongsTo('Dimension'),! options: DS.hasMany('Option')! });! ! App.Price = DS.Model.extend({! value: DS.attr('number'),! currency: DS.attr('string')! });! ! App.Dimension = DS.Model.extend({! height: DS.attr('number'),! width: DS.attr('number'),! depth: DS.attr('number')! });! ! App.Option = DS.Model.extend({! name: DS.attr('string'),! values: DS.attr()! });
  • 23. Steps • Loop through all root JSON properties • Determine which model they represent • Get all the relationships for that model • Side load any of those relationships
  • 24. {! "products": [! {! "name": "Robot",! "description": "...",! "price": {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ]! } App.Product Relationships • price • size • option Side load $$$ Profit $$$
  • 25. JS Methods extract: function(store, type, payload, id, requestType) { ... } processRelationships: function(store, type, payload, hash) { ... } sideloadRecord: function(store, type, payload, hash) { ... }
  • 26. Create a Serializer /**! Deserialize a nested JSON payload into a flat object! with sideloaded relationships that Ember Data can import.! */! App.NestedSerializer = DS.RESTSerializer.extend({! ! /**! (overloaded method)! Deserialize a JSON payload from the server.! ! @method normalizePayload! @param {Object} payload! @return {Object} the normalized payload! */! extract: function(store, type, payload, id, requestType) {! return this._super(store, type, payload, id, requestType);! }! ! });
  • 27. {! "products": [! {! ...! }! ]! } extract: function(store, type, payload, id, requestType) {! return this._super(store, type, payload, id, requestType);! }
  • 28. {! "products": [! {! ...! }! ]! } extract: function(store, type, payload, id, requestType) {! var rootKeys = Ember.keys(payload);! ! // Loop through root properties and process their relationships! rootKeys.forEach(function(key){! ! }, this);! ! return this._super(store, type, payload, id, requestType);! }
  • 29. extract: function(store, type, payload, id, requestType) {! var rootKeys = Ember.keys(payload);! ! // Loop through root properties and process their relationships! rootKeys.forEach(function(key){! var type = store.container! ! ! ! ! .lookupFactory('model:' + key.singularize());! ! }, this);! ! return this._super(store, type, payload, id, requestType);! } {! "products": [! {! ...! }! ]! }
  • 31. extract: function(store, type, payload, id, requestType) {! var rootKeys = Ember.keys(payload);! ! // Loop through root properties and process their relationships! rootKeys.forEach(function(key){! var type = store.container! ! ! ! ! .lookupFactory('model:' + key.singularize());! ! }, this);! ! return this._super(store, type, payload, id, requestType);! } {! "products": [! {! ...! }! ]! }
  • 32. {! "products": ! [! {! ...! }! ]! } extract: function(store, type, payload, id, requestType) {! var rootKeys = Ember.keys(payload);! ! // Loop through root properties and process their relationships! rootKeys.forEach(function(key){! var type = store.container! .lookupFactory('model:' + key.singularize()),! hash = payload[key];! ! }, this);! ! return this._super(store, type, payload, id, requestType);! }
  • 33. extract: function(store, type, payload, id, requestType) {! var rootKeys = Ember.keys(payload);! ! // Loop through root properties and process their relationships! rootKeys.forEach(function(key){! var type = store.container! .lookupFactory('model:' + key.singularize()),! hash = payload[key];! ! // Sideload embedded relationships of this model hash! if (type) {! this.processRelationships(store, type, payload, hash);! }! }, this);! ! return this._super(store, type, payload, id, requestType);! } {! "products": ! [! {! ...! }! ]! }
  • 34. /**! Process nested relationships on a single hash record! ! @method extractRelationships! @param {DS.Store} store! @param {DS.Model} type! @param {Object} payload The entire payload! @param {Object} hash The hash for the record being processed! @return {Object} The updated hash object! */! processRelationships: function(store, type, payload, hash) {! ! },
  • 35. {! "products": [! {! ...! }! ]! } processRelationships: function(store, type, payload, hash) {! ! // If hash is an array, process each item in the array! if (hash instanceof Array) {! hash.forEach(function(item, i){! hash[i] = this.processRelationships(store, type, payload, item);! }, this);! }! ! return hash;! },
  • 36. {! "products": [! {! ...! }! ]! } processRelationships: function(store, type, payload, hash) {! ! // If hash is an array, process each item in the array! if (hash instanceof Array) {! hash.forEach(function(item, i){! hash[i] = this.processRelationships(store, type, payload, item);! }, this);! }! ! else {! ! }! ! return hash;! },
  • 37. {! "products": [! {! ...! }! ]! } processRelationships: function(store, type, payload, hash) {! ! // If hash is an array, process each item in the array! if (hash instanceof Array) {! hash.forEach(function(item, i){! hash[i] = this.processRelationships(store, type, payload, item);! }, this);! }! ! else {! ! // Find all relationships in this model! type.eachRelationship(function(key, relationship) {! ! }, this);! }! ! return hash;! },
  • 38. ! App.Product.eachRelationship(function(key, relationship) {! ! ! }, this);! App.Product = DS.Model.extend({! name: DS.attr('string'),! description: DS.attr('string'),! price: DS.belongsTo('Price'),! size: DS.belongsTo('Dimension'),! options: DS.hasMany('Option')! }); key = 'price'! relationship = {! "type": App.Price,! "kind": "belongsTo",! ...! } key = 'size'! relationship = {! "type": App.Dimension,! "kind": "belongsTo",! ...! } key = 'options'! relationship = {! "type": App.Option,! "kind": "hasMany",! ...! }
  • 39. {! "products": [! {! "name": "Robot",! "description": "...",! "price": ! {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ]! } processRelationships: function(store, type, payload, hash) {! ! // If hash is an array, process each item in the array! if (hash instanceof Array) {! hash.forEach(function(item, i){! hash[i] = this.processRelationships(store, type, payload, item);! }, this);! }! ! else {! ! // Find all relationships in this model! type.eachRelationship(function(key, relationship) {! var related = hash[key]; // The hash for this relationship! ! }, this);! }! ! return hash;! },
  • 40. processRelationships: function(store, type, payload, hash) {! ! // If hash is an array, process each item in the array! if (hash instanceof Array) {! hash.forEach(function(item, i){! hash[i] = this.processRelationships(store, type, payload, item);! }, this);! }! ! else {! ! // Find all relationships in this model! type.eachRelationship(function(key, relationship) {! var related = hash[key], // The hash for this relationship! relType = relationship.type; // The model for this relationship ! }, this);! }! ! return hash;! }, {! "products": [! {! "name": "Robot",! "description": "...",! "price": ! {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ]! } App.Price
  • 41. processRelationships: function(store, type, payload, hash) {! ! // If hash is an array, process each item in the array! if (hash instanceof Array) {! hash.forEach(function(item, i){! hash[i] = this.processRelationships(store, type, payload, item);! }, this);! }! ! else {! ! // Find all relationships in this model! type.eachRelationship(function(key, relationship) {! var related = hash[key], ! relType = relationship.type;! ! hash[key] = this.sideloadRecord(store, relType, payload, related);! ! }, this);! }! ! return hash;! }, {! "products": [! {! "name": "Robot",! "description": "...",! "price": ! {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ]! }
  • 42. /**! Sideload a record hash to the payload! ! @method sideloadRecord! @param {DS.Store} store! @param {DS.Model} type! @param {Object} payload The entire payload! @param {Object} hash The record hash object! @return {Object} The ID of the record(s) sideloaded! */! sideloadRecord: function(store, type, payload, hash) {! ! },
  • 43. sideloadRecord: function(store, type, payload, hash) {! var id, sideLoadkey, sideloadArr, serializer;! ! // If hash is an array, sideload each item in the array! if (hash instanceof Array) {! id = [];! hash.forEach(function(item, i){! id[i] = this.sideloadRecord(store, type, payload, item);! }, this);! }! ! return id;! }, {! "products": [! {! "name": "Robot",! "description": "...",! "price": ! {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ]! }
  • 44. sideloadRecord: function(store, type, payload, hash) {! var id, sideLoadkey, sideloadArr, serializer;! ! // If hash is an array, sideload each item in the array! if (hash instanceof Array) {! id = [];! hash.forEach(function(item, i){! id[i] = this.sideloadRecord(store, type, payload, item);! }, this);! }! // Sideload record! else if (typeof hash === 'object') {! ! }! return id;! }, {! "products": [! {! "name": "Robot",! "description": "...",! "price": ! {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ]! }
  • 45. sideloadRecord: function(store, type, payload, hash) {! var id, sideLoadkey, sideloadArr, serializer;! ! // If hash is an array, sideload each item in the array! if (hash instanceof Array) {! id = [];! hash.forEach(function(item, i){! id[i] = this.sideloadRecord(store, type, payload, item);! }, this);! }! // Sideload record! else if (typeof hash === 'object') {! sideLoadkey = type.typeKey.pluralize(); ! sideloadArr = payload[sideLoadkey] || [];! }! ! return id;! }, {! "products": [! {! "name": "Robot",! "description": "...",! "price": ! {! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ],! "prices": [! ]! }
  • 46. sideloadRecord: function(store, type, payload, hash) {! var id, sideLoadkey, sideloadArr, serializer;! ! // If hash is an array, sideload each item in the array! if (hash instanceof Array) {! id = [];! hash.forEach(function(item, i){! id[i] = this.sideloadRecord(store, type, payload, item);! }, this);! }! // Sideload record! else if (typeof hash === 'object') {! sideLoadkey = type.typeKey.pluralize(); ! sideloadArr = payload[sideLoadkey] || [];! id = this.generateID(store, type, hash);! }! ! return id;! }, {! "products": [! {! "name": "Robot",! "description": "...",! "price": ! {! "id": “generated-1",! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ],! "prices": [! ]! } Every record needs an ID
  • 47. sideloadRecord: function(store, type, payload, hash) {! var id, sideLoadkey, sideloadArr, serializer;! ! // If hash is an array, sideload each item in the array! if (hash instanceof Array) {! id = [];! hash.forEach(function(item, i){! id[i] = this.sideloadRecord(store, type, payload, item);! }, this);! }! // Sideload record! else if (typeof hash === 'object') {! sideLoadkey = type.typeKey.pluralize(); ! sideloadArr = payload[sideLoadkey] || [];! ! // Sideload, if it's not already sideloaded! if (sideloadArr.findBy('id', id) === undefined){! sideloadArr.push(hash);! payload[sideLoadkey] = sideloadArr;! }! }! ! return id;! }, {! "products": [! {! "name": "Robot",! "description": "...",! "price": ! {! "id": “generated-1",! "value": 59.99,! "currency": "USD"! },! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ],! "prices": [! {! "id": “generated-1",! "value": 59.99,! "currency": "USD"! }! ]! }
  • 48. sideloadRecord: function(store, type, payload, hash) {! var id, sideLoadkey, sideloadArr, serializer;! ! // If hash is an array, sideload each item in the array! if (hash instanceof Array) {! id = [];! hash.forEach(function(item, i){! id[i] = this.sideloadRecord(store, type, payload, item);! }, this);! }! // Sideload record! else if (typeof hash === 'object') {! sideLoadkey = type.typeKey.pluralize(); ! sideloadArr = payload[sideLoadkey] || [];! ! // Sideload, if it's not already sideloaded! if (sideloadArr.findBy('id', id) === undefined){! sideloadArr.push(hash);! payload[sideLoadkey] = sideloadArr;! }! }! ! return id;! }, {! "products": [! {! "name": "Robot",! "description": "...",! "price": "generated-1",! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ],! "prices": [! {! "id": "generated-1",! "value": 59.99,! "currency": "USD"! }! ]! } processRelationships: function(store, type, payload, hash) {! ...! hash[key] = this.sideloadRecord(store, relType, payload, related);! ...! },
  • 49. {! "products": [! {! "name": "Robot",! "description": "...",! "price": "generated-1",! "size": "generated-2",! "options": [! “generated-3”! ]! }! ],! "prices": [! {! "id": "generated-1",! "value": 59.99,! "currency": "USD"! }! ],! "dimensions": [{! "id": "generated-2",! "height": 24,! "width": 12,! "depth": 14! }],! "options": [ ! {! "id": "generated-3",! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! } {! "products": [! {! "name": "Robot",! "description": "...",! "price": "generated-1",! "size": {! "height": 24,! "width": 12,! "depth": 14! },! "options": [! {! "name": "Color",! "values": ["silver", ! "black", ! "#E1563F"]! }! ]! }! ],! "prices": [! {! "id": "generated-1",! "value": 59.99,! "currency": "USD"! }! ]! }
  • 50. Apply the Serializer App.ApplicationSerializer = App.NestedSerializer; App.ProductSerializer = App.NestedSerializer.extend({}); - OR -
  • 51. Now for a demo