UNPKG

9.44 kBJavaScriptView Raw
1import DS from 'ember-data';
2import Ember from 'ember';
3
4/**
5 @module ember-data
6 */
7
8const {
9 singularize,
10 classify,
11 decamelize,
12 camelize,
13 underscore
14} = Ember.String;
15
16const {
17 RESTSerializer,
18 normalizeModelName
19} = DS;
20
21/**
22 The ActiveModelSerializer is a subclass of the RESTSerializer designed to integrate
23 with a JSON API that uses an underscored naming convention instead of camelCasing.
24 It has been designed to work out of the box with the
25 [active\_model\_serializers](http://github.com/rails-api/active_model_serializers)
26 Ruby gem. This Serializer expects specific settings using ActiveModel::Serializers,
27 `embed :ids, embed_in_root: true` which sideloads the records.
28
29 This serializer extends the DS.RESTSerializer by making consistent
30 use of the camelization, decamelization and pluralization methods to
31 normalize the serialized JSON into a format that is compatible with
32 a conventional Rails backend and Ember Data.
33
34 ## JSON Structure
35
36 The ActiveModelSerializer expects the JSON returned from your server
37 to follow the REST adapter conventions substituting underscored keys
38 for camelcased ones.
39
40 ### Conventional Names
41
42 Attribute names in your JSON payload should be the underscored versions of
43 the attributes in your Ember.js models.
44
45 For example, if you have a `Person` model:
46
47 ```js
48 App.FamousPerson = DS.Model.extend({
49 firstName: DS.attr('string'),
50 lastName: DS.attr('string'),
51 occupation: DS.attr('string')
52 });
53 ```
54
55 The JSON returned should look like this:
56
57 ```js
58 {
59 "famous_person": {
60 "id": 1,
61 "first_name": "Barack",
62 "last_name": "Obama",
63 "occupation": "President"
64 }
65 }
66 ```
67
68 Let's imagine that `Occupation` is just another model:
69
70 ```js
71 App.Person = DS.Model.extend({
72 firstName: DS.attr('string'),
73 lastName: DS.attr('string'),
74 occupation: DS.belongsTo('occupation')
75 });
76
77 App.Occupation = DS.Model.extend({
78 name: DS.attr('string'),
79 salary: DS.attr('number'),
80 people: DS.hasMany('person')
81 });
82 ```
83
84 The JSON needed to avoid extra server calls, should look like this:
85
86 ```js
87 {
88 "people": [{
89 "id": 1,
90 "first_name": "Barack",
91 "last_name": "Obama",
92 "occupation_id": 1
93 }],
94
95 "occupations": [{
96 "id": 1,
97 "name": "President",
98 "salary": 100000,
99 "person_ids": [1]
100 }]
101 }
102 ```
103
104 @class ActiveModelSerializer
105 @namespace DS
106 @extends DS.RESTSerializer
107*/
108var ActiveModelSerializer = RESTSerializer.extend({
109 // SERIALIZE
110
111 /**
112 Converts camelCased attributes to underscored when serializing.
113
114 @method keyForAttribute
115 @param {String} attribute
116 @return String
117 */
118 keyForAttribute: function(attr) {
119 return decamelize(attr);
120 },
121
122 /**
123 Underscores relationship names and appends "_id" or "_ids" when serializing
124 relationship keys.
125
126 @method keyForRelationship
127 @param {String} relationshipModelName
128 @param {String} kind
129 @return String
130 */
131 keyForRelationship: function(relationshipModelName, kind) {
132 var key = decamelize(relationshipModelName);
133 if (kind === "belongsTo") {
134 return key + "_id";
135 } else if (kind === "hasMany") {
136 return singularize(key) + "_ids";
137 } else {
138 return key;
139 }
140 },
141
142 /**
143 `keyForLink` can be used to define a custom key when deserializing link
144 properties. The `ActiveModelSerializer` camelizes link keys by default.
145
146 @method keyForLink
147 @param {String} key
148 @param {String} kind `belongsTo` or `hasMany`
149 @return {String} normalized key
150 */
151 keyForLink: function(key, relationshipKind) {
152 return camelize(key);
153 },
154
155 /*
156 Does not serialize hasMany relationships by default.
157 */
158 serializeHasMany: Ember.K,
159
160 /**
161 Underscores the JSON root keys when serializing.
162
163 @method payloadKeyFromModelName
164 @param {String} modelName
165 @return {String}
166 */
167 payloadKeyFromModelName: function(modelName) {
168 return underscore(decamelize(modelName));
169 },
170
171 /**
172 Serializes a polymorphic type as a fully capitalized model name.
173
174 @method serializePolymorphicType
175 @param {DS.Snapshot} snapshot
176 @param {Object} json
177 @param {Object} relationship
178 */
179 serializePolymorphicType: function(snapshot, json, relationship) {
180 var key = relationship.key;
181 var belongsTo = snapshot.belongsTo(key);
182 var jsonKey = underscore(key + "_type");
183
184 if (Ember.isNone(belongsTo)) {
185 json[jsonKey] = null;
186 } else {
187 json[jsonKey] = classify(belongsTo.modelName).replace('/', '::');
188 }
189 },
190
191 // EXTRACT
192
193 /**
194 Add extra step to `DS.RESTSerializer.normalize` so links are normalized.
195
196 If your payload looks like:
197
198 ```js
199 {
200 "post": {
201 "id": 1,
202 "title": "Rails is omakase",
203 "links": { "flagged_comments": "api/comments/flagged" }
204 }
205 }
206 ```
207
208 The normalized version would look like this
209
210 ```js
211 {
212 "post": {
213 "id": 1,
214 "title": "Rails is omakase",
215 "links": { "flaggedComments": "api/comments/flagged" }
216 }
217 }
218 ```
219
220 @method normalize
221 @param {subclass of DS.Model} typeClass
222 @param {Object} hash
223 @param {String} prop
224 @return Object
225 */
226 normalize: function(typeClass, hash, prop) {
227 this.normalizeLinks(hash);
228 return this._super(typeClass, hash, prop);
229 },
230
231 /**
232 Convert `snake_cased` links to `camelCase`
233
234 @method normalizeLinks
235 @param {Object} data
236 */
237
238 normalizeLinks: function(data) {
239 if (data.links) {
240 var links = data.links;
241
242 for (var link in links) {
243 var camelizedLink = camelize(link);
244
245 if (camelizedLink !== link) {
246 links[camelizedLink] = links[link];
247 delete links[link];
248 }
249 }
250 }
251 },
252
253 /**
254 Normalize the polymorphic type from the JSON.
255
256 Normalize:
257 ```js
258 {
259 id: "1"
260 minion: { type: "evil_minion", id: "12"}
261 }
262 ```
263
264 To:
265 ```js
266 {
267 id: "1"
268 minion: { type: "evilMinion", id: "12"}
269 }
270 ```
271
272 @param {Subclass of DS.Model} typeClass
273 @method normalizeRelationships
274 @private
275 */
276 normalizeRelationships: function(typeClass, hash) {
277
278 if (this.keyForRelationship) {
279 typeClass.eachRelationship(function(key, relationship) {
280 var payloadKey, payload;
281 if (relationship.options.polymorphic) {
282 payloadKey = this.keyForAttribute(key, "deserialize");
283 payload = hash[payloadKey];
284 if (payload && payload.type) {
285 payload.type = this.modelNameFromPayloadKey(payload.type);
286 } else if (payload && relationship.kind === "hasMany") {
287 payload.forEach((single) => single.type = this.modelNameFromPayloadKey(single.type));
288 }
289 } else {
290 payloadKey = this.keyForRelationship(key, relationship.kind, "deserialize");
291 if (!hash.hasOwnProperty(payloadKey)) { return; }
292 payload = hash[payloadKey];
293 }
294
295 hash[key] = payload;
296
297 if (key !== payloadKey) {
298 delete hash[payloadKey];
299 }
300 }, this);
301 }
302 },
303
304 extractRelationships: function(modelClass, resourceHash) {
305 modelClass.eachRelationship(function (key, relationshipMeta) {
306 var relationshipKey = this.keyForRelationship(key, relationshipMeta.kind, "deserialize");
307
308 // prefer the format the AMS gem expects, e.g.:
309 // relationship: {id: id, type: type}
310 if (relationshipMeta.options.polymorphic) {
311 extractPolymorphicRelationships(key, relationshipMeta, resourceHash, relationshipKey);
312 }
313 // If the preferred format is not found, use {relationship_name_id, relationship_name_type}
314 if (resourceHash.hasOwnProperty(relationshipKey) && typeof resourceHash[relationshipKey] !== 'object') {
315 var polymorphicTypeKey = this.keyForRelationship(key) + '_type';
316 if (resourceHash[polymorphicTypeKey] && relationshipMeta.options.polymorphic) {
317 let id = resourceHash[relationshipKey];
318 let type = resourceHash[polymorphicTypeKey];
319 delete resourceHash[polymorphicTypeKey];
320 delete resourceHash[relationshipKey];
321 resourceHash[relationshipKey] = { id: id, type: type };
322 }
323 }
324 },this);
325 return this._super.apply(this, arguments);
326 },
327
328 modelNameFromPayloadKey: function(key) {
329 var convertedFromRubyModule = singularize(key.replace('::', '/'));
330 return normalizeModelName(convertedFromRubyModule);
331 }
332});
333
334function extractPolymorphicRelationships(key, relationshipMeta, resourceHash, relationshipKey) {
335 let polymorphicKey = decamelize(key);
336 if (polymorphicKey in resourceHash && typeof resourceHash[polymorphicKey] === 'object') {
337 if (relationshipMeta.kind === 'belongsTo') {
338 let hash = resourceHash[polymorphicKey];
339 let {id, type} = hash;
340 resourceHash[relationshipKey] = {id, type};
341 // otherwise hasMany
342 } else {
343 let hashes = resourceHash[polymorphicKey];
344
345 if (!hashes) {
346 return;
347 }
348
349 // TODO: replace this with map when ActiveModelAdapter branches for Ember Data 2.0
350 var array = [];
351 for (let i = 0, length = hashes.length; i < length; i++) {
352 let hash = hashes[i];
353 let {id, type} = hash;
354 array.push({id, type});
355 }
356 resourceHash[relationshipKey] = array;
357 }
358 }
359}
360
361export default ActiveModelSerializer;