1 | import DS from 'ember-data';
|
2 | import Ember from 'ember';
|
3 |
|
4 | /**
|
5 | @module ember-data
|
6 | */
|
7 |
|
8 | const {
|
9 | singularize,
|
10 | classify,
|
11 | decamelize,
|
12 | camelize,
|
13 | underscore
|
14 | } = Ember.String;
|
15 |
|
16 | const {
|
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 | */
|
108 | var 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 |
|
334 | function 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 |
|
361 | export default ActiveModelSerializer;
|