UNPKG

23.8 kBJavaScriptView Raw
1/**
2 @module @ember-data/serializer
3*/
4
5import { makeArray } from '@ember/array';
6import { assert, deprecate, warn } from '@ember/debug';
7import { camelize } from '@ember/string';
8import { isNone, typeOf } from '@ember/utils';
9import { DEBUG } from '@glimmer/env';
10
11import { singularize } from 'ember-inflector';
12
13import JSONSerializer from '@ember-data/serializer/json';
14import { normalizeModelName } from '@ember-data/store';
15import { coerceId } from '@ember-data/store/-private';
16
17import { modelHasAttributeOrRelationshipNamedType } from './-private';
18
19/**
20 Normally, applications will use the `RESTSerializer` by implementing
21 the `normalize` method.
22
23 This allows you to do whatever kind of munging you need and is
24 especially useful if your server is inconsistent and you need to
25 do munging differently for many different kinds of responses.
26
27 See the `normalize` documentation for more information.
28
29 ## Across the Board Normalization
30
31 There are also a number of hooks that you might find useful to define
32 across-the-board rules for your payload. These rules will be useful
33 if your server is consistent, or if you're building an adapter for
34 an infrastructure service, like Firebase, and want to encode service
35 conventions.
36
37 For example, if all of your keys are underscored and all-caps, but
38 otherwise consistent with the names you use in your models, you
39 can implement across-the-board rules for how to convert an attribute
40 name in your model to a key in your JSON.
41
42 ```app/serializers/application.js
43 import RESTSerializer from '@ember-data/serializer/rest';
44 import { underscore } from '@ember/string';
45
46 export default class ApplicationSerializer extends RESTSerializer {
47 keyForAttribute(attr, method) {
48 return underscore(attr).toUpperCase();
49 }
50 }
51 ```
52
53 You can also implement `keyForRelationship`, which takes the name
54 of the relationship as the first parameter, the kind of
55 relationship (`hasMany` or `belongsTo`) as the second parameter, and
56 the method (`serialize` or `deserialize`) as the third parameter.
57
58 @class RESTSerializer
59 @extends JSONSerializer
60*/
61const RESTSerializer = JSONSerializer.extend({
62 /**
63 `keyForPolymorphicType` can be used to define a custom key when
64 serializing and deserializing a polymorphic type. By default, the
65 returned key is `${key}Type`.
66
67 Example
68
69 ```app/serializers/post.js
70 import RESTSerializer from '@ember-data/serializer/rest';
71
72 export default class ApplicationSerializer extends RESTSerializer {
73 keyForPolymorphicType(key, relationship) {
74 let relationshipKey = this.keyForRelationship(key);
75
76 return 'type-' + relationshipKey;
77 }
78 }
79 ```
80
81 @method keyForPolymorphicType
82 @param {String} key
83 @param {String} typeClass
84 @param {String} method
85 @return {String} normalized key
86 */
87 keyForPolymorphicType(key, typeClass, method) {
88 let relationshipKey = this.keyForRelationship(key);
89
90 return `${relationshipKey}Type`;
91 },
92
93 /**
94 Normalizes a part of the JSON payload returned by
95 the server. You should override this method, munge the hash
96 and call super if you have generic normalization to do.
97
98 It takes the type of the record that is being normalized
99 (as a Model class), the property where the hash was
100 originally found, and the hash to normalize.
101
102 For example, if you have a payload that looks like this:
103
104 ```js
105 {
106 "post": {
107 "id": 1,
108 "title": "Rails is omakase",
109 "comments": [ 1, 2 ]
110 },
111 "comments": [{
112 "id": 1,
113 "body": "FIRST"
114 }, {
115 "id": 2,
116 "body": "Rails is unagi"
117 }]
118 }
119 ```
120
121 The `normalize` method will be called three times:
122
123 * With `App.Post`, `"posts"` and `{ id: 1, title: "Rails is omakase", ... }`
124 * With `App.Comment`, `"comments"` and `{ id: 1, body: "FIRST" }`
125 * With `App.Comment`, `"comments"` and `{ id: 2, body: "Rails is unagi" }`
126
127 You can use this method, for example, to normalize underscored keys to camelized
128 or other general-purpose normalizations. You will only need to implement
129 `normalize` and manipulate the payload as desired.
130
131 For example, if the `IDs` under `"comments"` are provided as `_id` instead of
132 `id`, you can specify how to normalize just the comments:
133
134 ```app/serializers/post.js
135 import RESTSerializer from '@ember-data/serializer/rest';
136
137 export default class ApplicationSerializer extends RESTSerializer {
138 normalize(model, hash, prop) {
139 if (prop === 'comments') {
140 hash.id = hash._id;
141 delete hash._id;
142 }
143
144 return super.normalize(...arguments);
145 }
146 }
147 ```
148
149 On each call to the `normalize` method, the third parameter (`prop`) is always
150 one of the keys that were in the original payload or in the result of another
151 normalization as `normalizeResponse`.
152
153 @method normalize
154 @param {Model} modelClass
155 @param {Object} resourceHash
156 @param {String} prop
157 @return {Object}
158 */
159
160 /**
161 Normalizes an array of resource payloads and returns a JSON-API Document
162 with primary data and, if any, included data as `{ data, included }`.
163
164 @method _normalizeArray
165 @param {Store} store
166 @param {String} modelName
167 @param {Object} arrayHash
168 @param {String} prop
169 @return {Object}
170 @private
171 */
172 _normalizeArray(store, modelName, arrayHash, prop) {
173 let documentHash = {
174 data: [],
175 included: [],
176 };
177
178 let modelClass = store.modelFor(modelName);
179 let serializer = store.serializerFor(modelName);
180
181 makeArray(arrayHash).forEach(hash => {
182 let { data, included } = this._normalizePolymorphicRecord(store, hash, prop, modelClass, serializer);
183 documentHash.data.push(data);
184 if (included) {
185 documentHash.included.push(...included);
186 }
187 });
188
189 return documentHash;
190 },
191
192 _normalizePolymorphicRecord(store, hash, prop, primaryModelClass, primarySerializer) {
193 let serializer = primarySerializer;
194 let modelClass = primaryModelClass;
195
196 let primaryHasTypeAttribute = modelHasAttributeOrRelationshipNamedType(primaryModelClass);
197
198 if (!primaryHasTypeAttribute && hash.type) {
199 // Support polymorphic records in async relationships
200 let modelName = this.modelNameFromPayloadKey(hash.type);
201
202 if (store._hasModelFor(modelName)) {
203 serializer = store.serializerFor(modelName);
204 modelClass = store.modelFor(modelName);
205 }
206 }
207
208 return serializer.normalize(modelClass, hash, prop);
209 },
210
211 /*
212 @method _normalizeResponse
213 @param {Store} store
214 @param {Model} primaryModelClass
215 @param {Object} payload
216 @param {String|Number} id
217 @param {String} requestType
218 @param {Boolean} isSingle
219 @return {Object} JSON-API Document
220 @private
221 */
222 _normalizeResponse(store, primaryModelClass, payload, id, requestType, isSingle) {
223 let documentHash = {
224 data: null,
225 included: [],
226 };
227
228 let meta = this.extractMeta(store, primaryModelClass, payload);
229 if (meta) {
230 assert(
231 'The `meta` returned from `extractMeta` has to be an object, not "' + typeOf(meta) + '".',
232 typeOf(meta) === 'object'
233 );
234 documentHash.meta = meta;
235 }
236
237 let keys = Object.keys(payload);
238
239 for (var i = 0, length = keys.length; i < length; i++) {
240 var prop = keys[i];
241 var modelName = prop;
242 var forcedSecondary = false;
243
244 /*
245 If you want to provide sideloaded records of the same type that the
246 primary data you can do that by prefixing the key with `_`.
247
248 Example
249
250 ```
251 {
252 users: [
253 { id: 1, title: 'Tom', manager: 3 },
254 { id: 2, title: 'Yehuda', manager: 3 }
255 ],
256 _users: [
257 { id: 3, title: 'Tomster' }
258 ]
259 }
260 ```
261
262 This forces `_users` to be added to `included` instead of `data`.
263 */
264 if (prop.charAt(0) === '_') {
265 forcedSecondary = true;
266 modelName = prop.substr(1);
267 }
268
269 var typeName = this.modelNameFromPayloadKey(modelName);
270 if (!store._hasModelFor(typeName)) {
271 warn(this.warnMessageNoModelForKey(modelName, typeName), false, {
272 id: 'ds.serializer.model-for-key-missing',
273 });
274 continue;
275 }
276
277 var isPrimary = !forcedSecondary && this.isPrimaryType(store, typeName, primaryModelClass);
278 var value = payload[prop];
279
280 if (value === null) {
281 continue;
282 }
283
284 if (DEBUG) {
285 let isQueryRecordAnArray = requestType === 'queryRecord' && isPrimary && Array.isArray(value);
286 let message =
287 'The adapter returned an array for the primary data of a `queryRecord` response. This is deprecated as `queryRecord` should return a single record.';
288
289 deprecate(message, !isQueryRecordAnArray, {
290 id: 'ds.serializer.rest.queryRecord-array-response',
291 until: '3.0',
292 url:
293 'https://deprecations.emberjs.com/ember-data/v2.x/#toc_store-queryrecord-array-response-with-restserializer',
294 });
295 }
296
297 /*
298 Support primary data as an object instead of an array.
299
300 Example
301
302 ```
303 {
304 user: { id: 1, title: 'Tom', manager: 3 }
305 }
306 ```
307 */
308 if (isPrimary && !Array.isArray(value)) {
309 let { data, included } = this._normalizePolymorphicRecord(store, value, prop, primaryModelClass, this);
310 documentHash.data = data;
311 if (included) {
312 documentHash.included.push(...included);
313 }
314 continue;
315 }
316
317 let { data, included } = this._normalizeArray(store, typeName, value, prop);
318
319 if (included) {
320 documentHash.included.push(...included);
321 }
322
323 if (isSingle) {
324 data.forEach(resource => {
325 /*
326 Figures out if this is the primary record or not.
327
328 It's either:
329
330 1. The record with the same ID as the original request
331 2. If it's a newly created record without an ID, the first record
332 in the array
333 */
334 let isUpdatedRecord = isPrimary && coerceId(resource.id) === id;
335 let isFirstCreatedRecord = isPrimary && !id && !documentHash.data;
336
337 if (isFirstCreatedRecord || isUpdatedRecord) {
338 documentHash.data = resource;
339 } else {
340 documentHash.included.push(resource);
341 }
342 });
343 } else {
344 if (isPrimary) {
345 documentHash.data = data;
346 } else {
347 if (data) {
348 documentHash.included.push(...data);
349 }
350 }
351 }
352 }
353
354 return documentHash;
355 },
356
357 isPrimaryType(store, modelName, primaryModelClass) {
358 return normalizeModelName(modelName) === primaryModelClass.modelName;
359 },
360
361 /**
362 This method allows you to push a payload containing top-level
363 collections of records organized per type.
364
365 ```js
366 {
367 "posts": [{
368 "id": "1",
369 "title": "Rails is omakase",
370 "author", "1",
371 "comments": [ "1" ]
372 }],
373 "comments": [{
374 "id": "1",
375 "body": "FIRST"
376 }],
377 "users": [{
378 "id": "1",
379 "name": "@d2h"
380 }]
381 }
382 ```
383
384 It will first normalize the payload, so you can use this to push
385 in data streaming in from your server structured the same way
386 that fetches and saves are structured.
387
388 @method pushPayload
389 @param {Store} store
390 @param {Object} payload
391 */
392 pushPayload(store, payload) {
393 let documentHash = {
394 data: [],
395 included: [],
396 };
397
398 for (var prop in payload) {
399 var modelName = this.modelNameFromPayloadKey(prop);
400 if (!store._hasModelFor(modelName)) {
401 warn(this.warnMessageNoModelForKey(prop, modelName), false, {
402 id: 'ds.serializer.model-for-key-missing',
403 });
404 continue;
405 }
406 var type = store.modelFor(modelName);
407 var typeSerializer = store.serializerFor(type.modelName);
408
409 makeArray(payload[prop]).forEach(hash => {
410 let { data, included } = typeSerializer.normalize(type, hash, prop);
411 documentHash.data.push(data);
412 if (included) {
413 documentHash.included.push(...included);
414 }
415 });
416 }
417
418 store.push(documentHash);
419 },
420
421 /**
422 This method is used to convert each JSON root key in the payload
423 into a modelName that it can use to look up the appropriate model for
424 that part of the payload.
425
426 For example, your server may send a model name that does not correspond with
427 the name of the model in your app. Let's take a look at an example model,
428 and an example payload:
429
430 ```app/models/post.js
431 import Model from '@ember-data/model';
432
433 export default class Post extends Model {}
434 ```
435
436 ```javascript
437 {
438 "blog/post": {
439 "id": "1
440 }
441 }
442 ```
443
444 Ember Data is going to normalize the payload's root key for the modelName. As a result,
445 it will try to look up the "blog/post" model. Since we don't have a model called "blog/post"
446 (or a file called app/models/blog/post.js in ember-cli), Ember Data will throw an error
447 because it cannot find the "blog/post" model.
448
449 Since we want to remove this namespace, we can define a serializer for the application that will
450 remove "blog/" from the payload key whenver it's encountered by Ember Data:
451
452 ```app/serializers/application.js
453 import RESTSerializer from '@ember-data/serializer/rest';
454
455 export default class ApplicationSerializer extends RESTSerializer {
456 modelNameFromPayloadKey(payloadKey) {
457 if (payloadKey === 'blog/post') {
458 return super.modelNameFromPayloadKey(payloadKey.replace('blog/', ''));
459 } else {
460 return super.modelNameFromPayloadKey(payloadKey);
461 }
462 }
463 }
464 ```
465
466 After refreshing, Ember Data will appropriately look up the "post" model.
467
468 By default the modelName for a model is its
469 name in dasherized form. This means that a payload key like "blogPost" would be
470 normalized to "blog-post" when Ember Data looks up the model. Usually, Ember Data
471 can use the correct inflection to do this for you. Most of the time, you won't
472 need to override `modelNameFromPayloadKey` for this purpose.
473
474 @method modelNameFromPayloadKey
475 @param {String} key
476 @return {String} the model's modelName
477 */
478 modelNameFromPayloadKey(key) {
479 return singularize(normalizeModelName(key));
480 },
481
482 // SERIALIZE
483
484 /**
485 Called when a record is saved in order to convert the
486 record into JSON.
487
488 By default, it creates a JSON object with a key for
489 each attribute and belongsTo relationship.
490
491 For example, consider this model:
492
493 ```app/models/comment.js
494 import Model, { attr, belongsTo } from '@ember-data/model';
495
496 export default class Comment extends Model {
497 @attr title
498 @attr body
499
500 @belongsTo('user') author
501 }
502 ```
503
504 The default serialization would create a JSON object like:
505
506 ```js
507 {
508 "title": "Rails is unagi",
509 "body": "Rails? Omakase? O_O",
510 "author": 12
511 }
512 ```
513
514 By default, attributes are passed through as-is, unless
515 you specified an attribute type (`attr('date')`). If
516 you specify a transform, the JavaScript value will be
517 serialized when inserted into the JSON hash.
518
519 By default, belongs-to relationships are converted into
520 IDs when inserted into the JSON hash.
521
522 ## IDs
523
524 `serialize` takes an options hash with a single option:
525 `includeId`. If this option is `true`, `serialize` will,
526 by default include the ID in the JSON object it builds.
527
528 The adapter passes in `includeId: true` when serializing
529 a record for `createRecord`, but not for `updateRecord`.
530
531 ## Customization
532
533 Your server may expect a different JSON format than the
534 built-in serialization format.
535
536 In that case, you can implement `serialize` yourself and
537 return a JSON hash of your choosing.
538
539 ```app/serializers/post.js
540 import RESTSerializer from '@ember-data/serializer/rest';
541
542 export default class ApplicationSerializer extends RESTSerializer {
543 serialize(snapshot, options) {
544 let json = {
545 POST_TTL: snapshot.attr('title'),
546 POST_BDY: snapshot.attr('body'),
547 POST_CMS: snapshot.hasMany('comments', { ids: true })
548 };
549
550 if (options.includeId) {
551 json.POST_ID_ = snapshot.id;
552 }
553
554 return json;
555 }
556 }
557 ```
558
559 ## Customizing an App-Wide Serializer
560
561 If you want to define a serializer for your entire
562 application, you'll probably want to use `eachAttribute`
563 and `eachRelationship` on the record.
564
565 ```app/serializers/application.js
566 import RESTSerializer from '@ember-data/serializer/rest';
567 import { pluralize } from 'ember-inflector';
568
569 export default class ApplicationSerializer extends RESTSerializer {
570 serialize(snapshot, options) {
571 let json = {};
572
573 snapshot.eachAttribute(function(name) {
574 json[serverAttributeName(name)] = snapshot.attr(name);
575 });
576
577 snapshot.eachRelationship(function(name, relationship) {
578 if (relationship.kind === 'hasMany') {
579 json[serverHasManyName(name)] = snapshot.hasMany(name, { ids: true });
580 }
581 });
582
583 if (options.includeId) {
584 json.ID_ = snapshot.id;
585 }
586
587 return json;
588 }
589 }
590
591 function serverAttributeName(attribute) {
592 return attribute.underscore().toUpperCase();
593 }
594
595 function serverHasManyName(name) {
596 return serverAttributeName(singularize(name)) + "_IDS";
597 }
598 ```
599
600 This serializer will generate JSON that looks like this:
601
602 ```js
603 {
604 "TITLE": "Rails is omakase",
605 "BODY": "Yep. Omakase.",
606 "COMMENT_IDS": [ 1, 2, 3 ]
607 }
608 ```
609
610 ## Tweaking the Default JSON
611
612 If you just want to do some small tweaks on the default JSON,
613 you can call super first and make the tweaks on the returned
614 JSON.
615
616 ```app/serializers/post.js
617 import RESTSerializer from '@ember-data/serializer/rest';
618
619 export default class ApplicationSerializer extends RESTSerializer {
620 serialize(snapshot, options) {
621 let json = super.serialize(snapshot, options);
622
623 json.subject = json.title;
624 delete json.title;
625
626 return json;
627 }
628 }
629 ```
630
631 @method serialize
632 @param {Snapshot} snapshot
633 @param {Object} options
634 @return {Object} json
635 */
636 serialize(snapshot, options) {
637 return this._super(...arguments);
638 },
639
640 /**
641 You can use this method to customize the root keys serialized into the JSON.
642 The hash property should be modified by reference (possibly using something like _.extend)
643 By default the REST Serializer sends the modelName of a model, which is a camelized
644 version of the name.
645
646 For example, your server may expect underscored root objects.
647
648 ```app/serializers/application.js
649 import RESTSerializer from '@ember-data/serializer/rest';
650 import { decamelize } from '@ember/string';
651
652 export default class ApplicationSerializer extends RESTSerializer {
653 serializeIntoHash(data, type, record, options) {
654 let root = decamelize(type.modelName);
655 data[root] = this.serialize(record, options);
656 }
657 }
658 ```
659
660 @method serializeIntoHash
661 @param {Object} hash
662 @param {Model} typeClass
663 @param {Snapshot} snapshot
664 @param {Object} options
665 */
666 serializeIntoHash(hash, typeClass, snapshot, options) {
667 let normalizedRootKey = this.payloadKeyFromModelName(typeClass.modelName);
668 hash[normalizedRootKey] = this.serialize(snapshot, options);
669 },
670
671 /**
672 You can use `payloadKeyFromModelName` to override the root key for an outgoing
673 request. By default, the RESTSerializer returns a camelized version of the
674 model's name.
675
676 For a model called TacoParty, its `modelName` would be the string `taco-party`. The RESTSerializer
677 will send it to the server with `tacoParty` as the root key in the JSON payload:
678
679 ```js
680 {
681 "tacoParty": {
682 "id": "1",
683 "location": "Matthew Beale's House"
684 }
685 }
686 ```
687
688 For example, your server may expect dasherized root objects:
689
690 ```app/serializers/application.js
691 import RESTSerializer from '@ember-data/serializer/rest';
692 import { dasherize } from '@ember/string';
693
694 export default class ApplicationSerializer extends RESTSerializer {
695 payloadKeyFromModelName(modelName) {
696 return dasherize(modelName);
697 }
698 }
699 ```
700
701 Given a `TacoParty` model, calling `save` on it would produce an outgoing
702 request like:
703
704 ```js
705 {
706 "taco-party": {
707 "id": "1",
708 "location": "Matthew Beale's House"
709 }
710 }
711 ```
712
713 @method payloadKeyFromModelName
714 @param {String} modelName
715 @return {String}
716 */
717 payloadKeyFromModelName(modelName) {
718 return camelize(modelName);
719 },
720
721 /**
722 You can use this method to customize how polymorphic objects are serialized.
723 By default the REST Serializer creates the key by appending `Type` to
724 the attribute and value from the model's camelcased model name.
725
726 @method serializePolymorphicType
727 @param {Snapshot} snapshot
728 @param {Object} json
729 @param {Object} relationship
730 */
731 serializePolymorphicType(snapshot, json, relationship) {
732 let key = relationship.key;
733 let typeKey = this.keyForPolymorphicType(key, relationship.type, 'serialize');
734 let belongsTo = snapshot.belongsTo(key);
735
736 if (isNone(belongsTo)) {
737 json[typeKey] = null;
738 } else {
739 json[typeKey] = camelize(belongsTo.modelName);
740 }
741 },
742
743 /**
744 You can use this method to customize how a polymorphic relationship should
745 be extracted.
746
747 @method extractPolymorphicRelationship
748 @param {Object} relationshipType
749 @param {Object} relationshipHash
750 @param {Object} relationshipOptions
751 @return {Object}
752 */
753 extractPolymorphicRelationship(relationshipType, relationshipHash, relationshipOptions) {
754 let { key, resourceHash, relationshipMeta } = relationshipOptions;
755
756 // A polymorphic belongsTo relationship can be present in the payload
757 // either in the form where the `id` and the `type` are given:
758 //
759 // {
760 // message: { id: 1, type: 'post' }
761 // }
762 //
763 // or by the `id` and a `<relationship>Type` attribute:
764 //
765 // {
766 // message: 1,
767 // messageType: 'post'
768 // }
769 //
770 // The next code checks if the latter case is present and returns the
771 // corresponding JSON-API representation. The former case is handled within
772 // the base class JSONSerializer.
773 let isPolymorphic = relationshipMeta.options.polymorphic;
774 let typeProperty = this.keyForPolymorphicType(key, relationshipType, 'deserialize');
775
776 if (isPolymorphic && resourceHash[typeProperty] !== undefined && typeof relationshipHash !== 'object') {
777 let type = this.modelNameFromPayloadKey(resourceHash[typeProperty]);
778 return {
779 id: relationshipHash,
780 type: type,
781 };
782 }
783
784 return this._super(...arguments);
785 },
786});
787
788if (DEBUG) {
789 RESTSerializer.reopen({
790 warnMessageNoModelForKey(prop, typeKey) {
791 return (
792 'Encountered "' +
793 prop +
794 '" in payload, but no model was found for model name "' +
795 typeKey +
796 '" (resolved model name using ' +
797 this.constructor.toString() +
798 '.modelNameFromPayloadKey("' +
799 prop +
800 '"))'
801 );
802 },
803 });
804}
805
806export { EmbeddedRecordsMixin } from './-private';
807
808export default RESTSerializer;