UNPKG

203 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
7var isPlainObject = _interopDefault(require('lodash.isplainobject'));
8var isFunction = _interopDefault(require('lodash.isfunction'));
9var mapValues = _interopDefault(require('lodash.mapvalues'));
10var uniq = _interopDefault(require('lodash.uniq'));
11var flatten = _interopDefault(require('lodash.flatten'));
12require('@miragejs/pretender-node-polyfill/before');
13var Pretender = _interopDefault(require('pretender'));
14require('@miragejs/pretender-node-polyfill/after');
15var inflected = require('inflected');
16var lowerFirst = _interopDefault(require('lodash.lowerfirst'));
17var isEqual = _interopDefault(require('lodash.isequal'));
18var map = _interopDefault(require('lodash.map'));
19var cloneDeep = _interopDefault(require('lodash.clonedeep'));
20var invokeMap = _interopDefault(require('lodash.invokemap'));
21var compact = _interopDefault(require('lodash.compact'));
22var has = _interopDefault(require('lodash.has'));
23var values = _interopDefault(require('lodash.values'));
24var isEmpty = _interopDefault(require('lodash.isempty'));
25var get = _interopDefault(require('lodash.get'));
26var uniqBy = _interopDefault(require('lodash.uniqby'));
27var forIn = _interopDefault(require('lodash.forin'));
28var pick = _interopDefault(require('lodash.pick'));
29var assign = _interopDefault(require('lodash.assign'));
30var find = _interopDefault(require('lodash.find'));
31var isInteger = _interopDefault(require('lodash.isinteger'));
32
33// jscs:disable disallowVar, requireArrayDestructuring
34/**
35 @hide
36*/
37
38function referenceSort (edges) {
39 let nodes = uniq(flatten(edges));
40 let cursor = nodes.length;
41 let sorted = new Array(cursor);
42 let visited = {};
43 let i = cursor;
44
45 let visit = function (node, i, predecessors) {
46 if (predecessors.indexOf(node) >= 0) {
47 throw new Error(`Cyclic dependency in properties ${JSON.stringify(predecessors)}`);
48 }
49
50 if (visited[i]) {
51 return;
52 } else {
53 visited[i] = true;
54 }
55
56 let outgoing = edges.filter(function (edge) {
57 return edge && edge[0] === node;
58 });
59 i = outgoing.length;
60
61 if (i) {
62 let preds = predecessors.concat(node);
63
64 do {
65 let pair = outgoing[--i];
66 let child = pair[1];
67
68 if (child) {
69 visit(child, nodes.indexOf(child), preds);
70 }
71 } while (i);
72 }
73
74 sorted[--cursor] = node;
75 };
76
77 while (i--) {
78 if (!visited[i]) {
79 visit(nodes[i], i, []);
80 }
81 }
82
83 return sorted.reverse();
84}
85
86let Factory = function () {
87 this.build = function (sequence) {
88 let object = {};
89 let topLevelAttrs = Object.assign({}, this.attrs);
90 delete topLevelAttrs.afterCreate;
91 Object.keys(topLevelAttrs).forEach(attr => {
92 if (Factory.isTrait.call(this, attr)) {
93 delete topLevelAttrs[attr];
94 }
95 });
96 let keys = sortAttrs(topLevelAttrs, sequence);
97 keys.forEach(function (key) {
98 let buildAttrs, buildSingleValue;
99
100 buildAttrs = function (attrs) {
101 return mapValues(attrs, buildSingleValue);
102 };
103
104 buildSingleValue = value => {
105 if (Array.isArray(value)) {
106 return value.map(buildSingleValue);
107 } else if (isPlainObject(value)) {
108 return buildAttrs(value);
109 } else if (isFunction(value)) {
110 return value.call(topLevelAttrs, sequence);
111 } else {
112 return value;
113 }
114 };
115
116 let value = topLevelAttrs[key];
117
118 if (isFunction(value)) {
119 object[key] = value.call(object, sequence);
120 } else {
121 object[key] = buildSingleValue(value);
122 }
123 });
124 return object;
125 };
126};
127
128Factory.extend = function (attrs) {
129 // Merge the new attributes with existing ones. If conflict, new ones win.
130 let newAttrs = Object.assign({}, this.attrs, attrs);
131
132 let Subclass = function () {
133 this.attrs = newAttrs;
134 Factory.call(this);
135 }; // Copy extend
136
137
138 Subclass.extend = Factory.extend;
139 Subclass.extractAfterCreateCallbacks = Factory.extractAfterCreateCallbacks;
140 Subclass.isTrait = Factory.isTrait; // Store a reference on the class for future subclasses
141
142 Subclass.attrs = newAttrs;
143 return Subclass;
144};
145
146Factory.extractAfterCreateCallbacks = function ({
147 traits
148} = {}) {
149 let afterCreateCallbacks = [];
150 let attrs = this.attrs || {};
151 let traitCandidates;
152
153 if (attrs.afterCreate) {
154 afterCreateCallbacks.push(attrs.afterCreate);
155 }
156
157 if (Array.isArray(traits)) {
158 traitCandidates = traits;
159 } else {
160 traitCandidates = Object.keys(attrs);
161 }
162
163 traitCandidates.filter(attr => {
164 return this.isTrait(attr) && attrs[attr].extension.afterCreate;
165 }).forEach(attr => {
166 afterCreateCallbacks.push(attrs[attr].extension.afterCreate);
167 });
168 return afterCreateCallbacks;
169};
170
171Factory.isTrait = function (attrName) {
172 let {
173 attrs
174 } = this;
175 return isPlainObject(attrs[attrName]) && attrs[attrName].__isTrait__ === true;
176};
177
178function sortAttrs(attrs, sequence) {
179 let Temp = function () {};
180
181 let obj = new Temp();
182 let refs = [];
183 let property;
184 Object.keys(attrs).forEach(function (key) {
185 let value;
186 Object.defineProperty(obj.constructor.prototype, key, {
187 get() {
188 refs.push([property, key]);
189 return value;
190 },
191
192 set(newValue) {
193 value = newValue;
194 },
195
196 enumerable: false,
197 configurable: true
198 });
199 });
200 Object.keys(attrs).forEach(function (key) {
201 let value = attrs[key];
202
203 if (typeof value !== "function") {
204 obj[key] = value;
205 }
206 });
207 Object.keys(attrs).forEach(function (key) {
208 let value = attrs[key];
209 property = key;
210
211 if (typeof value === "function") {
212 obj[key] = value.call(obj, sequence);
213 }
214
215 refs.push([key]);
216 });
217 return referenceSort(refs);
218}
219
220function isNumber(n) {
221 return (+n).toString() === n.toString();
222}
223/**
224 By default Mirage uses autoincrementing numbers starting with `1` as IDs for records. This can be customized by implementing one or more IdentityManagers for your application.
225
226 An IdentityManager is a class that's responsible for generating unique identifiers. You can define a custom identity manager for your entire application, as well as on a per-model basis.
227
228 A custom IdentityManager must implement these methods:
229
230 - `fetch`, which must return an identifier not yet used
231 - `set`, which is called with an `id` of a record being insert into Mirage's database
232 - `reset`, which should reset database to initial state
233
234 Check out the advanced guide on Mocking UUIDs to see a complete example of a custom IdentityManager.
235
236 @class IdentityManager
237 @constructor
238 @public
239*/
240
241
242class IdentityManager {
243 constructor() {
244 this._nextId = 1;
245 this._ids = {};
246 }
247 /**
248 @method get
249 @hide
250 @private
251 */
252
253
254 get() {
255 return this._nextId;
256 }
257 /**
258 Registers `uniqueIdentifier` as used.
259 This method should throw is `uniqueIdentifier` has already been taken.
260 @method set
261 @param {String|Number} uniqueIdentifier
262 @public
263 */
264
265
266 set(uniqueIdentifier) {
267 if (this._ids[uniqueIdentifier]) {
268 throw new Error(`Attempting to use the ID ${uniqueIdentifier}, but it's already been used`);
269 }
270
271 if (isNumber(uniqueIdentifier) && +uniqueIdentifier >= this._nextId) {
272 this._nextId = +uniqueIdentifier + 1;
273 }
274
275 this._ids[uniqueIdentifier] = true;
276 }
277 /**
278 @method inc
279 @hide
280 @private
281 */
282
283
284 inc() {
285 let nextValue = this.get() + 1;
286 this._nextId = nextValue;
287 return nextValue;
288 }
289 /**
290 Returns the next unique identifier.
291 @method fetch
292 @return {String} Unique identifier
293 @public
294 */
295
296
297 fetch() {
298 let id = this.get();
299 this._ids[id] = true;
300 this.inc();
301 return id.toString();
302 }
303 /**
304 Resets the identity manager, marking all unique identifiers as available.
305 @method reset
306 @public
307 */
308
309
310 reset() {
311 this._nextId = 1;
312 this._ids = {};
313 }
314
315}
316
317/**
318 @hide
319*/
320let association = function (...traitsAndOverrides) {
321 let __isAssociation__ = true;
322 return {
323 __isAssociation__,
324 traitsAndOverrides
325 };
326};
327
328let trait = function (extension) {
329 let __isTrait__ = true;
330 return {
331 extension,
332 __isTrait__
333 };
334};
335
336const warn = console.warn; // eslint-disable-line no-console
337
338/**
339 You can use this class when you want more control over your route handlers response.
340
341 Pass the `code`, `headers` and `data` into the constructor and return an instance from any route handler.
342
343 ```js
344 import { Response } from 'miragejs';
345
346 this.get('/users', () => {
347 return new Response(400, { some: 'header' }, { errors: [ 'name cannot be blank'] });
348 });
349 ```
350*/
351
352class Response {
353 constructor(code, headers = {}, data) {
354 this.code = code;
355 this.headers = headers; // Default data for "undefined 204" responses to empty string (no content)
356
357 if (code === 204) {
358 if (data !== undefined && data !== "") {
359 warn(`Mirage: One of your route handlers is returning a custom
360 204 Response that has data, but this is a violation of the HTTP spec
361 and could lead to unexpected behavior. 204 responses should have no
362 content (an empty string) as their body.`);
363 } else {
364 this.data = "";
365 } // Default data for "empty untyped" responses to empty JSON object
366
367 } else if ((data === undefined || data === "") && !Object.prototype.hasOwnProperty.call(this.headers, "Content-Type")) {
368 this.data = {};
369 } else {
370 this.data = data;
371 } // Default "untyped" responses to application/json
372
373
374 if (code !== 204 && !Object.prototype.hasOwnProperty.call(this.headers, "Content-Type")) {
375 this.headers["Content-Type"] = "application/json";
376 }
377 }
378
379 toRackResponse() {
380 return [this.code, this.headers, this.data];
381 }
382
383}
384
385const camelizeCache = {};
386const dasherizeCache = {};
387const underscoreCache = {};
388const capitalizeCache = {};
389/**
390 * @param {String} word
391 * @hide
392 */
393
394function camelize(word) {
395 if (typeof camelizeCache[word] !== "string") {
396 let camelizedWord = inflected.camelize(underscore(word), false);
397 /*
398 The `ember-inflector` package's version of camelize lower-cases the first
399 word after a slash, e.g.
400 camelize('my-things/nice-watch'); // 'myThings/niceWatch'
401 The `inflected` package doesn't, so we make that change here to not break
402 existing functionality. (This affects the name of the schema collections.)
403 */
404
405
406 const camelized = camelizedWord.split("/").map(lowerFirst).join("/");
407 camelizeCache[word] = camelized;
408 }
409
410 return camelizeCache[word];
411}
412/**
413 * @param {String} word
414 * @hide
415 */
416
417function dasherize(word) {
418 if (typeof dasherizeCache[word] !== "string") {
419 const dasherized = inflected.dasherize(underscore(word));
420
421 dasherizeCache[word] = dasherized;
422 }
423
424 return dasherizeCache[word];
425}
426function underscore(word) {
427 if (typeof underscoreCache[word] !== "string") {
428 const underscored = inflected.underscore(word);
429
430 underscoreCache[word] = underscored;
431 }
432
433 return underscoreCache[word];
434}
435function capitalize(word) {
436 if (typeof capitalizeCache[word] !== "string") {
437 const capitalized = inflected.capitalize(word);
438
439 capitalizeCache[word] = capitalized;
440 }
441
442 return capitalizeCache[word];
443}
444
445/**
446 @hide
447*/
448
449function isAssociation (object) {
450 return isPlainObject(object) && object.__isAssociation__ === true;
451}
452
453/* eslint no-console: 0 */
454let errorProps = ["description", "fileName", "lineNumber", "message", "name", "number", "stack"];
455/**
456 @hide
457*/
458
459function assert(bool, text) {
460 if (typeof bool === "string" && !text) {
461 // console.error(`Mirage: ${bool}`);
462 throw new MirageError(bool);
463 }
464
465 if (!bool) {
466 // console.error(`Mirage: ${text}`);
467 throw new MirageError(text.replace(/^ +/gm, "") || "Assertion failed");
468 }
469}
470/**
471 @public
472 @hide
473 Copied from ember-metal/error
474*/
475
476function MirageError(message, stack) {
477 let tmp = Error(message);
478
479 if (stack) {
480 tmp.stack = stack;
481 }
482
483 for (let idx = 0; idx < errorProps.length; idx++) {
484 let prop = errorProps[idx];
485
486 if (["description", "message", "stack"].indexOf(prop) > -1) {
487 this[prop] = `Mirage: ${tmp[prop]}`;
488 } else {
489 this[prop] = tmp[prop];
490 }
491 }
492}
493MirageError.prototype = Object.create(Error.prototype);
494
495/**
496 Associations are used to define relationships between your Models.
497
498 @class Association
499 @constructor
500 @public
501*/
502
503class Association {
504 constructor(modelName, opts) {
505 if (typeof modelName === "object") {
506 // Received opts only
507 this.modelName = undefined;
508 this.opts = modelName;
509 } else {
510 // The modelName of the association. (Might not be passed in - set later
511 // by schema).
512 this.modelName = modelName ? dasherize(modelName) : "";
513 this.opts = opts || {};
514 } // The key pointing to the association
515
516
517 this.key = ""; // The modelName that owns this association
518
519 this.ownerModelName = "";
520 }
521 /**
522 A setter for schema, since we don't have a reference at constuction time.
523 @method setSchema
524 @public
525 @hide
526 */
527
528
529 setSchema(schema) {
530 this.schema = schema;
531 }
532 /**
533 Returns true if the association is reflexive.
534 @method isReflexive
535 @return {Boolean}
536 @public
537 */
538
539
540 isReflexive() {
541 let isExplicitReflexive = !!(this.modelName === this.ownerModelName && this.opts.inverse);
542 let isImplicitReflexive = !!(this.opts.inverse === undefined && this.ownerModelName === this.modelName);
543 return isExplicitReflexive || isImplicitReflexive;
544 }
545
546 get isPolymorphic() {
547 return this.opts.polymorphic;
548 }
549 /**
550 @hide
551 */
552
553
554 get identifier() {
555 throw new Error("Subclasses of Association must implement a getter for identifier");
556 }
557
558}
559
560const identifierCache = {};
561/**
562 * The belongsTo association adds a fk to the owner of the association
563 *
564 * @class BelongsTo
565 * @extends Association
566 * @constructor
567 * @public
568 * @hide
569 */
570
571class BelongsTo extends Association {
572 get identifier() {
573 if (typeof identifierCache[this.key] !== "string") {
574 const identifier = `${camelize(this.key)}Id`;
575 identifierCache[this.key] = identifier;
576 }
577
578 return identifierCache[this.key];
579 }
580 /**
581 * @method getForeignKeyArray
582 * @return {Array} Array of camelized name of the model owning the association
583 * and foreign key for the association
584 * @public
585 */
586
587
588 getForeignKeyArray() {
589 return [camelize(this.ownerModelName), this.getForeignKey()];
590 }
591 /**
592 * @method getForeignKey
593 * @return {String} Foreign key for the association
594 * @public
595 */
596
597
598 getForeignKey() {
599 // we reuse identifierCache because it's the same logic as get identifier
600 if (typeof identifierCache[this.key] !== "string") {
601 const foreignKey = `${camelize(this.key)}Id`;
602 identifierCache[this.key] = foreignKey;
603 }
604
605 return identifierCache[this.key];
606 }
607 /**
608 * Registers belongs-to association defined by given key on given model,
609 * defines getters / setters for associated parent and associated parent's id,
610 * adds methods for creating unsaved parent record and creating a saved one
611 *
612 * @method addMethodsToModelClass
613 * @param {Function} ModelClass
614 * @param {String} key the named key for the association
615 * @public
616 */
617
618
619 addMethodsToModelClass(ModelClass, key) {
620 let modelPrototype = ModelClass.prototype;
621 let association = this;
622 let foreignKey = this.getForeignKey();
623 let associationHash = {
624 [key]: this
625 };
626 modelPrototype.belongsToAssociations = Object.assign(modelPrototype.belongsToAssociations, associationHash); // update belongsToAssociationFks
627
628 Object.keys(modelPrototype.belongsToAssociations).forEach(key => {
629 const value = modelPrototype.belongsToAssociations[key];
630 modelPrototype.belongsToAssociationFks[value.getForeignKey()] = value;
631 }); // Add to target's dependent associations array
632
633 this.schema.addDependentAssociation(this, this.modelName); // TODO: look how this is used. Are these necessary, seems like they could be gotten from the above?
634 // Or we could use a single data structure to store this information?
635
636 modelPrototype.associationKeys.add(key);
637 modelPrototype.associationIdKeys.add(foreignKey);
638 Object.defineProperty(modelPrototype, foreignKey, {
639 /*
640 object.parentId
641 - returns the associated parent's id
642 */
643 get() {
644 this._tempAssociations = this._tempAssociations || {};
645 let tempParent = this._tempAssociations[key];
646 let id;
647
648 if (tempParent === null) {
649 id = null;
650 } else {
651 if (association.isPolymorphic) {
652 if (tempParent) {
653 id = {
654 id: tempParent.id,
655 type: tempParent.modelName
656 };
657 } else {
658 id = this.attrs[foreignKey];
659 }
660 } else {
661 if (tempParent) {
662 id = tempParent.id;
663 } else {
664 id = this.attrs[foreignKey];
665 }
666 }
667 }
668
669 return id;
670 },
671
672 /*
673 object.parentId = (parentId)
674 - sets the associated parent via id
675 */
676 set(id) {
677 let tempParent;
678
679 if (id === null) {
680 tempParent = null;
681 } else if (id !== undefined) {
682 if (association.isPolymorphic) {
683 assert(typeof id === "object", `You're setting an ID on the polymorphic association '${association.key}' but you didn't pass in an object. Polymorphic IDs need to be in the form { type, id }.`);
684 tempParent = association.schema[association.schema.toCollectionName(id.type)].find(id.id);
685 } else {
686 tempParent = association.schema[association.schema.toCollectionName(association.modelName)].find(id);
687 assert(tempParent, `Couldn't find ${association.modelName} with id = ${id}`);
688 }
689 }
690
691 this[key] = tempParent;
692 }
693
694 });
695 Object.defineProperty(modelPrototype, key, {
696 /*
697 object.parent
698 - returns the associated parent
699 */
700 get() {
701 this._tempAssociations = this._tempAssociations || {};
702 let tempParent = this._tempAssociations[key];
703 let foreignKeyId = this[foreignKey];
704 let model = null;
705
706 if (tempParent) {
707 model = tempParent;
708 } else if (foreignKeyId !== null) {
709 if (association.isPolymorphic) {
710 model = association.schema[association.schema.toCollectionName(foreignKeyId.type)].find(foreignKeyId.id);
711 } else {
712 model = association.schema[association.schema.toCollectionName(association.modelName)].find(foreignKeyId);
713 }
714 }
715
716 return model;
717 },
718
719 /*
720 object.parent = (parentModel)
721 - sets the associated parent via model
722 I want to jot some notes about hasInverseFor. There used to be an
723 association.inverse() check, but adding polymorphic associations
724 complicated this. `comment.commentable`, you can't easily check for an
725 inverse since `comments: hasMany()` could be on any model.
726 Instead of making it very complex and looking for an inverse on the
727 association in isoaltion, it was much simpler to ask the model being
728 passed in if it had an inverse for the setting model and with its
729 association.
730 */
731 set(model) {
732 this._tempAssociations = this._tempAssociations || {};
733 this._tempAssociations[key] = model;
734
735 if (model && model.hasInverseFor(association)) {
736 let inverse = model.inverseFor(association);
737 model.associate(this, inverse);
738 }
739 }
740
741 });
742 /*
743 object.newParent
744 - creates a new unsaved associated parent
745 TODO: document polymorphic
746 */
747
748 modelPrototype[`new${capitalize(key)}`] = function (...args) {
749 let modelName, attrs;
750
751 if (association.isPolymorphic) {
752 modelName = args[0];
753 attrs = args[1];
754 } else {
755 modelName = association.modelName;
756 attrs = args[0];
757 }
758
759 let parent = association.schema[association.schema.toCollectionName(modelName)].new(attrs);
760 this[key] = parent;
761 return parent;
762 };
763 /*
764 object.createParent
765 - creates a new saved associated parent, and immediately persists both models
766 TODO: document polymorphic
767 */
768
769
770 modelPrototype[`create${capitalize(key)}`] = function (...args) {
771 let modelName, attrs;
772
773 if (association.isPolymorphic) {
774 modelName = args[0];
775 attrs = args[1];
776 } else {
777 modelName = association.modelName;
778 attrs = args[0];
779 }
780
781 let parent = association.schema[association.schema.toCollectionName(modelName)].create(attrs);
782 this[key] = parent;
783 this.save();
784 return parent.reload();
785 };
786 }
787 /**
788 *
789 *
790 * @public
791 */
792
793
794 disassociateAllDependentsFromTarget(model) {
795 let owner = this.ownerModelName;
796 let fk;
797
798 if (this.isPolymorphic) {
799 fk = {
800 type: model.modelName,
801 id: model.id
802 };
803 } else {
804 fk = model.id;
805 }
806
807 let dependents = this.schema[this.schema.toCollectionName(owner)].where(potentialOwner => {
808 let id = potentialOwner[this.getForeignKey()];
809
810 if (!id) {
811 return false;
812 }
813
814 if (typeof id === "object") {
815 return id.type === fk.type && id.id === fk.id;
816 } else {
817 return id === fk;
818 }
819 });
820 dependents.models.forEach(dependent => {
821 dependent.disassociate(model, this);
822 dependent.save();
823 });
824 }
825
826}
827
828function duplicate(data) {
829 if (Array.isArray(data)) {
830 return data.map(duplicate);
831 } else {
832 return Object.assign({}, data);
833 }
834}
835/**
836 Mirage's `Db` has many `DbCollections`, which are equivalent to tables from traditional databases. They store specific types of data, for example `users` and `posts`.
837
838 `DbCollections` have names, like `users`, which you use to access the collection from the `Db` object.
839
840 Suppose you had a `user` model defined, and the following data had been inserted into your database (either through factories or fixtures):
841
842 ```js
843 export default [
844 { id: 1, name: 'Zelda' },
845 { id: 2, name: 'Link' }
846 ];
847 ```
848
849 Then `db.contacts` would return this array.
850
851 @class DbCollection
852 @constructor
853 @public
854 */
855
856
857class DbCollection {
858 constructor(name, initialData, IdentityManager) {
859 this.name = name;
860 this._records = [];
861 this.identityManager = new IdentityManager();
862
863 if (initialData) {
864 this.insert(initialData);
865 }
866 }
867 /**
868 * Returns a copy of the data, to prevent inadvertent data manipulation.
869 * @method all
870 * @public
871 * @hide
872 */
873
874
875 all() {
876 return duplicate(this._records);
877 }
878 /**
879 Inserts `data` into the collection. `data` can be a single object
880 or an array of objects. Returns the inserted record.
881 ```js
882 // Insert a single record
883 let link = db.users.insert({ name: 'Link', age: 173 });
884 link; // { id: 1, name: 'Link', age: 173 }
885 // Insert an array
886 let users = db.users.insert([
887 { name: 'Zelda', age: 142 },
888 { name: 'Epona', age: 58 },
889 ]);
890 users; // [ { id: 2, name: 'Zelda', age: 142 }, { id: 3, name: 'Epona', age: 58 } ]
891 ```
892 @method insert
893 @param data
894 @public
895 */
896
897
898 insert(data) {
899 if (!Array.isArray(data)) {
900 return this._insertRecord(data);
901 } else {
902 return map(data, attrs => this._insertRecord(attrs));
903 }
904 }
905 /**
906 Returns a single record from the `collection` if `ids` is a single
907 id, or an array of records if `ids` is an array of ids. Note
908 each id can be an int or a string, but integer ids as strings
909 (e.g. the string “1”) will be treated as integers.
910 ```js
911 // Given users = [{id: 1, name: 'Link'}, {id: 2, name: 'Zelda'}]
912 db.users.find(1); // {id: 1, name: 'Link'}
913 db.users.find([1, 2]); // [{id: 1, name: 'Link'}, {id: 2, name: 'Zelda'}]
914 ```
915 @method find
916 @param ids
917 @public
918 */
919
920
921 find(ids) {
922 if (Array.isArray(ids)) {
923 let records = this._findRecords(ids).filter(Boolean).map(duplicate); // Return a copy
924
925
926 return records;
927 } else {
928 let record = this._findRecord(ids);
929
930 if (!record) {
931 return null;
932 } // Return a copy
933
934
935 return duplicate(record);
936 }
937 }
938 /**
939 Returns the first model from `collection` that matches the
940 key-value pairs in the `query` object. Note that a string
941 comparison is used. `query` is a POJO.
942 ```js
943 // Given users = [ { id: 1, name: 'Link' }, { id: 2, name: 'Zelda' } ]
944 db.users.findBy({ name: 'Link' }); // { id: 1, name: 'Link' }
945 ```
946 @method find
947 @param query
948 @public
949 */
950
951
952 findBy(query) {
953 let record = this._findRecordBy(query);
954
955 if (!record) {
956 return null;
957 } // Return a copy
958
959
960 return duplicate(record);
961 }
962 /**
963 Returns an array of models from `collection` that match the
964 key-value pairs in the `query` object. Note that a string
965 comparison is used. `query` is a POJO.
966 ```js
967 // Given users = [ { id: 1, name: 'Link' }, { id: 2, name: 'Zelda' } ]
968 db.users.where({ name: 'Zelda' }); // [ { id: 2, name: 'Zelda' } ]
969 ```
970 @method where
971 @param query
972 @public
973 */
974
975
976 where(query) {
977 return this._findRecordsWhere(query).map(duplicate);
978 }
979 /**
980 Finds the first record matching the provided _query_ in
981 `collection`, or creates a new record using a merge of the
982 `query` and optional `attributesForCreate`.
983 Often times you may have a pattern like the following in your API stub:
984 ```js
985 // Given users = [
986 // { id: 1, name: 'Link' },
987 // { id: 2, name: 'Zelda' }
988 // ]
989 // Create Link if he doesn't yet exist
990 let records = db.users.where({ name: 'Link' });
991 let record;
992 if (records.length > 0) {
993 record = records[0];
994 } else {
995 record = db.users.insert({ name: 'Link' });
996 }
997 ```
998 You can now replace this with the following:
999 ```js
1000 let record = db.users.firstOrCreate({ name: 'Link' });
1001 ```
1002 An extended example using *attributesForCreate*:
1003 ```js
1004 let record = db.users.firstOrCreate({ name: 'Link' }, { evil: false });
1005 ```
1006 @method firstOrCreate
1007 @param query
1008 @param attributesForCreate
1009 @public
1010 */
1011
1012
1013 firstOrCreate(query, attributesForCreate = {}) {
1014 let queryResult = this.where(query);
1015 let [record] = queryResult;
1016
1017 if (record) {
1018 return record;
1019 } else {
1020 let mergedAttributes = Object.assign(attributesForCreate, query);
1021 let createdRecord = this.insert(mergedAttributes);
1022 return createdRecord;
1023 }
1024 }
1025 /**
1026 Updates one or more records in the collection.
1027 If *attrs* is the only arg present, updates all records in the collection according to the key-value pairs in *attrs*.
1028 If *target* is present, restricts updates to those that match *target*. If *target* is a number or string, finds a single record whose id is *target* to update. If *target* is a POJO, queries *collection* for records that match the key-value pairs in *target*, and updates their *attrs*.
1029 Returns the updated record or records.
1030 ```js
1031 // Given users = [
1032 // {id: 1, name: 'Link'},
1033 // {id: 2, name: 'Zelda'}
1034 // ]
1035 db.users.update({name: 'Ganon'}); // db.users = [{id: 1, name: 'Ganon'}, {id: 2, name: 'Ganon'}]
1036 db.users.update(1, {name: 'Young Link'}); // db.users = [{id: 1, name: 'Young Link'}, {id: 2, name: 'Zelda'}]
1037 db.users.update({name: 'Link'}, {name: 'Epona'}); // db.users = [{id: 1, name: 'Epona'}, {id: 2, name: 'Zelda'}]
1038 ```
1039 @method update
1040 @param target
1041 @param attrs
1042 @public
1043 */
1044
1045
1046 update(target, attrs) {
1047 let records;
1048
1049 if (typeof attrs === "undefined") {
1050 attrs = target;
1051 let changedRecords = [];
1052
1053 this._records.forEach(record => {
1054 let oldRecord = Object.assign({}, record);
1055
1056 this._updateRecord(record, attrs);
1057
1058 if (!isEqual(oldRecord, record)) {
1059 changedRecords.push(record);
1060 }
1061 });
1062
1063 return changedRecords;
1064 } else if (typeof target === "number" || typeof target === "string") {
1065 let id = target;
1066
1067 let record = this._findRecord(id);
1068
1069 this._updateRecord(record, attrs);
1070
1071 return record;
1072 } else if (Array.isArray(target)) {
1073 let ids = target;
1074 records = this._findRecords(ids);
1075 records.forEach(record => {
1076 this._updateRecord(record, attrs);
1077 });
1078 return records;
1079 } else if (typeof target === "object") {
1080 let query = target;
1081 records = this._findRecordsWhere(query);
1082 records.forEach(record => {
1083 this._updateRecord(record, attrs);
1084 });
1085 return records;
1086 }
1087 }
1088 /**
1089 Removes one or more records in *collection*.
1090 If *target* is undefined, removes all records. If *target* is a number or string, removes a single record using *target* as id. If *target* is a POJO, queries *collection* for records that match the key-value pairs in *target*, and removes them from the collection.
1091 ```js
1092 // Given users = [
1093 // {id: 1, name: 'Link'},
1094 // {id: 2, name: 'Zelda'}
1095 // ]
1096 db.users.remove(); // db.users = []
1097 db.users.remove(1); // db.users = [{id: 2, name: 'Zelda'}]
1098 db.users.remove({name: 'Zelda'}); // db.users = [{id: 1, name: 'Link'}]
1099 ```
1100 @method remove
1101 @param target
1102 @public
1103 */
1104
1105
1106 remove(target) {
1107 let records;
1108
1109 if (typeof target === "undefined") {
1110 this._records = [];
1111 this.identityManager.reset();
1112 } else if (typeof target === "number" || typeof target === "string") {
1113 let record = this._findRecord(target);
1114
1115 let index = this._records.indexOf(record);
1116
1117 this._records.splice(index, 1);
1118 } else if (Array.isArray(target)) {
1119 records = this._findRecords(target);
1120 records.forEach(record => {
1121 let index = this._records.indexOf(record);
1122
1123 this._records.splice(index, 1);
1124 });
1125 } else if (typeof target === "object") {
1126 records = this._findRecordsWhere(target);
1127 records.forEach(record => {
1128 let index = this._records.indexOf(record);
1129
1130 this._records.splice(index, 1);
1131 });
1132 }
1133 }
1134 /*
1135 Private methods.
1136 These return the actual db objects, whereas the public
1137 API query methods return copies.
1138 */
1139
1140 /**
1141 @method _findRecord
1142 @param id
1143 @private
1144 @hide
1145 */
1146
1147
1148 _findRecord(id) {
1149 id = id.toString();
1150 return this._records.find(obj => obj.id === id);
1151 }
1152 /**
1153 @method _findRecordBy
1154 @param query
1155 @private
1156 @hide
1157 */
1158
1159
1160 _findRecordBy(query) {
1161 return this._findRecordsWhere(query)[0];
1162 }
1163 /**
1164 @method _findRecords
1165 @param ids
1166 @private
1167 @hide
1168 */
1169
1170
1171 _findRecords(ids) {
1172 return ids.map(this._findRecord, this);
1173 }
1174 /**
1175 @method _findRecordsWhere
1176 @param query
1177 @private
1178 @hide
1179 */
1180
1181
1182 _findRecordsWhere(query) {
1183 let records = this._records;
1184
1185 function defaultQueryFunction(record) {
1186 let keys = Object.keys(query);
1187 return keys.every(function (key) {
1188 return String(record[key]) === String(query[key]);
1189 });
1190 }
1191
1192 let queryFunction = typeof query === "object" ? defaultQueryFunction : query;
1193 return records.filter(queryFunction);
1194 }
1195 /**
1196 @method _insertRecord
1197 @param data
1198 @private
1199 @hide
1200 */
1201
1202
1203 _insertRecord(data) {
1204 let attrs = duplicate(data);
1205
1206 if (attrs && (attrs.id === undefined || attrs.id === null)) {
1207 attrs.id = this.identityManager.fetch(attrs);
1208 } else {
1209 attrs.id = attrs.id.toString();
1210 this.identityManager.set(attrs.id);
1211 }
1212
1213 this._records.push(attrs);
1214
1215 return duplicate(attrs);
1216 }
1217 /**
1218 @method _updateRecord
1219 @param record
1220 @param attrs
1221 @private
1222 @hide
1223 */
1224
1225
1226 _updateRecord(record, attrs) {
1227 let targetId = attrs && Object.prototype.hasOwnProperty.call(attrs, "id") ? attrs.id.toString() : null;
1228 let currentId = record.id;
1229
1230 if (targetId && currentId !== targetId) {
1231 throw new Error("Updating the ID of a record is not permitted");
1232 }
1233
1234 for (let attr in attrs) {
1235 if (attr === "id") {
1236 continue;
1237 }
1238
1239 record[attr] = attrs[attr];
1240 }
1241 }
1242
1243}
1244
1245/**
1246 Your Mirage server has a database which you can interact with in your route handlers. You’ll typically use models to interact with your database data, but you can always reach into the db directly in the event you want more control.
1247
1248 Access the db from your route handlers via `schema.db`.
1249
1250 You can access individual DbCollections by using `schema.db.name`:
1251
1252 ```js
1253 schema.db.users // would return, e.g., [ { id: 1, name: 'Yehuda' }, { id: 2, name: 'Tom '} ]
1254 ```
1255
1256 @class Db
1257 @constructor
1258 @public
1259 */
1260
1261class Db {
1262 constructor(initialData, identityManagers) {
1263 this._collections = [];
1264 this.registerIdentityManagers(identityManagers);
1265
1266 if (initialData) {
1267 this.loadData(initialData);
1268 }
1269 }
1270 /**
1271 Loads an object of data into Mirage's database.
1272 The keys of the object correspond to the DbCollections, and the values are arrays of records.
1273 ```js
1274 server.db.loadData({
1275 users: [
1276 { name: 'Yehuda' },
1277 { name: 'Tom' }
1278 ]
1279 });
1280 ```
1281 As with `db.collection.insert`, IDs will automatically be created for records that don't have them.
1282 @method loadData
1283 @param {Object} data - Data to load
1284 @public
1285 */
1286
1287
1288 loadData(data) {
1289 for (let key in data) {
1290 this.createCollection(key, cloneDeep(data[key]));
1291 }
1292 }
1293 /**
1294 Logs out the contents of the Db.
1295 ```js
1296 server.db.dump() // { users: [ name: 'Yehuda', ...
1297 ```
1298 @method dump
1299 @public
1300 */
1301
1302
1303 dump() {
1304 return this._collections.reduce((data, collection) => {
1305 data[collection.name] = collection.all();
1306 return data;
1307 }, {});
1308 }
1309 /**
1310 Add an empty collection named _name_ to your database. Typically you won’t need to do this yourself, since collections are automatically created for any models you have defined.
1311 @method createCollection
1312 @param name
1313 @param initialData (optional)
1314 @public
1315 */
1316
1317
1318 createCollection(name, initialData) {
1319 if (!this[name]) {
1320 let IdentityManager = this.identityManagerFor(name);
1321 let newCollection = new DbCollection(name, initialData, IdentityManager); // Public API has a convenient array interface. It comes at the cost of
1322 // returning a copy of all records to avoid accidental mutations.
1323
1324 Object.defineProperty(this, name, {
1325 get() {
1326 let recordsCopy = newCollection.all();
1327 ["insert", "find", "findBy", "where", "update", "remove", "firstOrCreate"].forEach(function (method) {
1328 recordsCopy[method] = function () {
1329 return newCollection[method](...arguments);
1330 };
1331 });
1332 return recordsCopy;
1333 }
1334
1335 }); // Private API does not have the array interface. This means internally, only
1336 // db-collection methods can be used. This is so records aren't copied redundantly
1337 // internally, which leads to accidental O(n^2) operations (e.g., createList).
1338
1339 Object.defineProperty(this, `_${name}`, {
1340 get() {
1341 let recordsCopy = [];
1342 ["insert", "find", "findBy", "where", "update", "remove", "firstOrCreate"].forEach(function (method) {
1343 recordsCopy[method] = function () {
1344 return newCollection[method](...arguments);
1345 };
1346 });
1347 return recordsCopy;
1348 }
1349
1350 });
1351
1352 this._collections.push(newCollection);
1353 } else if (initialData) {
1354 this[name].insert(initialData);
1355 }
1356
1357 return this;
1358 }
1359 /**
1360 @method createCollections
1361 @param ...collections
1362 @public
1363 @hide
1364 */
1365
1366
1367 createCollections(...collections) {
1368 collections.forEach(c => this.createCollection(c));
1369 }
1370 /**
1371 Removes all data from Mirage's database.
1372 @method emptyData
1373 @public
1374 */
1375
1376
1377 emptyData() {
1378 this._collections.forEach(c => c.remove());
1379 }
1380 /**
1381 @method identityManagerFor
1382 @param name
1383 @public
1384 @hide
1385 */
1386
1387
1388 identityManagerFor(name) {
1389 return this._identityManagers[this._container.inflector.singularize(name)] || this._identityManagers.application || IdentityManager;
1390 }
1391 /**
1392 @method registerIdentityManagers
1393 @public
1394 @hide
1395 */
1396
1397
1398 registerIdentityManagers(identityManagers) {
1399 this._identityManagers = identityManagers || {};
1400 }
1401
1402}
1403
1404/**
1405 Collections represent arrays of models. They are returned by a hasMany association, or by one of the ModelClass query methods:
1406
1407 ```js
1408 let posts = user.blogPosts;
1409 let posts = schema.blogPosts.all();
1410 let posts = schema.blogPosts.find([1, 2, 4]);
1411 let posts = schema.blogPosts.where({ published: true });
1412 ```
1413
1414 Note that there is also a `PolymorphicCollection` class that is identical to `Collection`, except it can contain a heterogeneous array of models. Thus, it has no `modelName` property. This lets serializers and other parts of the system interact with it differently.
1415
1416 @class Collection
1417 @constructor
1418 @public
1419*/
1420
1421class Collection {
1422 constructor(modelName, models = []) {
1423 assert(modelName && typeof modelName === "string", "You must pass a `modelName` into a Collection");
1424 /**
1425 The dasherized model name this Collection represents.
1426 ```js
1427 let posts = user.blogPosts;
1428 posts.modelName; // "blog-post"
1429 ```
1430 The model name is separate from the actual models, since Collections can be empty.
1431 @property modelName
1432 @type {String}
1433 @public
1434 */
1435
1436 this.modelName = modelName;
1437 /**
1438 The underlying plain JavaScript array of Models in this Collection.
1439 ```js
1440 posts.models // [ post:1, post:2, ... ]
1441 ```
1442 While Collections have many array-ish methods like `filter` and `sort`, it
1443 can be useful to work with the plain array if you want to work with methods
1444 like `map`, or use the `[]` accessor.
1445 For example, in testing you might want to assert against a model from the
1446 collection:
1447 ```js
1448 let newPost = user.posts.models[0].title;
1449 assert.equal(newPost, "My first post");
1450 ```
1451 @property models
1452 @type {Array}
1453 @public
1454 */
1455
1456 this.models = models;
1457 }
1458 /**
1459 The number of models in the collection.
1460 ```js
1461 user.posts.length; // 2
1462 ```
1463 @property length
1464 @type {Integer}
1465 @public
1466 */
1467
1468
1469 get length() {
1470 return this.models.length;
1471 }
1472 /**
1473 Updates each model in the collection, and immediately persists all changes to the db.
1474 ```js
1475 let posts = user.blogPosts;
1476 posts.update('published', true); // the db was updated for all posts
1477 ```
1478 @method update
1479 @param key
1480 @param val
1481 @return this
1482 @public
1483 */
1484
1485
1486 update(...args) {
1487 invokeMap(this.models, "update", ...args);
1488 return this;
1489 }
1490 /**
1491 Saves all models in the collection.
1492 ```js
1493 let posts = user.blogPosts;
1494 posts.models[0].published = true;
1495 posts.save(); // all posts saved to db
1496 ```
1497 @method save
1498 @return this
1499 @public
1500 */
1501
1502
1503 save() {
1504 invokeMap(this.models, "save");
1505 return this;
1506 }
1507 /**
1508 Reloads each model in the collection.
1509 ```js
1510 let posts = author.blogPosts;
1511 // ...
1512 posts.reload(); // reloads data for each post from the db
1513 ```
1514 @method reload
1515 @return this
1516 @public
1517 */
1518
1519
1520 reload() {
1521 invokeMap(this.models, "reload");
1522 return this;
1523 }
1524 /**
1525 Destroys the db record for all models in the collection.
1526 ```js
1527 let posts = user.blogPosts;
1528 posts.destroy(); // all posts removed from db
1529 ```
1530 @method destroy
1531 @return this
1532 @public
1533 */
1534
1535
1536 destroy() {
1537 invokeMap(this.models, "destroy");
1538 return this;
1539 }
1540 /**
1541 Adds a model to this collection.
1542 ```js
1543 posts.length; // 1
1544 posts.add(newPost);
1545 posts.length; // 2
1546 ```
1547 @method add
1548 @param {Model} model
1549 @return this
1550 @public
1551 */
1552
1553
1554 add(model) {
1555 this.models.push(model);
1556 return this;
1557 }
1558 /**
1559 Removes a model from this collection.
1560 ```js
1561 posts.length; // 5
1562 let firstPost = posts.models[0];
1563 posts.remove(firstPost);
1564 posts.save();
1565 posts.length; // 4
1566 ```
1567 @method remove
1568 @param {Model} model
1569 @return this
1570 @public
1571 */
1572
1573
1574 remove(model) {
1575 let match = this.models.find(m => m.toString() === model.toString());
1576
1577 if (match) {
1578 let i = this.models.indexOf(match);
1579 this.models.splice(i, 1);
1580 }
1581
1582 return this;
1583 }
1584 /**
1585 Checks if the Collection includes the given model.
1586 ```js
1587 posts.includes(newPost);
1588 ```
1589 Works by checking if the given model name and id exists in the Collection,
1590 making it a bit more flexible than strict object equality.
1591 ```js
1592 let post = server.create('post');
1593 let programming = server.create('tag', { text: 'Programming' });
1594 visit(`/posts/${post.id}`);
1595 click('.tag-selector');
1596 click('.tag:contains(Programming)');
1597 post.reload();
1598 assert.ok(post.tags.includes(programming));
1599 ```
1600 @method includes
1601 @return {Boolean}
1602 @public
1603 */
1604
1605
1606 includes(model) {
1607 return this.models.some(m => m.toString() === model.toString());
1608 }
1609 /**
1610 Returns a new Collection with its models filtered according to the provided [callback function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).
1611 ```js
1612 let publishedPosts = user.posts.filter(post => post.isPublished);
1613 ```
1614 @method filter
1615 @param {Function} f
1616 @return {Collection}
1617 @public
1618 */
1619
1620
1621 filter(f) {
1622 let filteredModels = this.models.filter(f);
1623 return new Collection(this.modelName, filteredModels);
1624 }
1625 /**
1626 Returns a new Collection with its models sorted according to the provided [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters).
1627 ```js
1628 let postsByTitleAsc = user.posts.sort((a, b) => {
1629 return b.title < a.title;
1630 });
1631 ```
1632 @method sort
1633 @param {Function} f
1634 @return {Collection}
1635 @public
1636 */
1637
1638
1639 sort(f) {
1640 let sortedModels = this.models.concat().sort(f);
1641 return new Collection(this.modelName, sortedModels);
1642 }
1643 /**
1644 Returns a new Collection with a subset of its models selected from `begin` to `end`.
1645 ```js
1646 let firstThreePosts = user.posts.slice(0, 3);
1647 ```
1648 @method slice
1649 @param {Integer} begin
1650 @param {Integer} end
1651 @return {Collection}
1652 @public
1653 */
1654
1655
1656 slice(...args) {
1657 let slicedModels = this.models.slice(...args);
1658 return new Collection(this.modelName, slicedModels);
1659 }
1660 /**
1661 Modifies the Collection by merging the models from another collection.
1662 ```js
1663 user.posts.mergeCollection(newPosts);
1664 user.posts.save();
1665 ```
1666 @method mergeCollection
1667 @param {Collection} collection
1668 @return this
1669 @public
1670 */
1671
1672
1673 mergeCollection(collection) {
1674 this.models = this.models.concat(collection.models);
1675 return this;
1676 }
1677 /**
1678 Simple string representation of the collection and id.
1679 ```js
1680 user.posts.toString(); // collection:post(post:1,post:4)
1681 ```
1682 @method toString
1683 @return {String}
1684 @public
1685 */
1686
1687
1688 toString() {
1689 return `collection:${this.modelName}(${this.models.map(m => m.id).join(",")})`;
1690 }
1691
1692}
1693
1694/**
1695 * An array of models, returned from one of the schema query
1696 * methods (all, find, where). Knows how to update and destroy its models.
1697 *
1698 * Identical to Collection except it can contain a heterogeneous array of
1699 * models. Thus, it has no `modelName` property. This lets serializers and
1700 * other parts of the system interact with it differently.
1701 *
1702 * @class PolymorphicCollection
1703 * @constructor
1704 * @public
1705 * @hide
1706 */
1707
1708class PolymorphicCollection {
1709 constructor(models = []) {
1710 this.models = models;
1711 }
1712 /**
1713 * Number of models in the collection.
1714 *
1715 * @property length
1716 * @type Number
1717 * @public
1718 */
1719
1720
1721 get length() {
1722 return this.models.length;
1723 }
1724 /**
1725 * Updates each model in the collection (persisting immediately to the db).
1726 * @method update
1727 * @param key
1728 * @param val
1729 * @return this
1730 * @public
1731 */
1732
1733
1734 update(...args) {
1735 invokeMap(this.models, "update", ...args);
1736 return this;
1737 }
1738 /**
1739 * Destroys the db record for all models in the collection.
1740 * @method destroy
1741 * @return this
1742 * @public
1743 */
1744
1745
1746 destroy() {
1747 invokeMap(this.models, "destroy");
1748 return this;
1749 }
1750 /**
1751 * Saves all models in the collection.
1752 * @method save
1753 * @return this
1754 * @public
1755 */
1756
1757
1758 save() {
1759 invokeMap(this.models, "save");
1760 return this;
1761 }
1762 /**
1763 * Reloads each model in the collection.
1764 * @method reload
1765 * @return this
1766 * @public
1767 */
1768
1769
1770 reload() {
1771 invokeMap(this.models, "reload");
1772 return this;
1773 }
1774 /**
1775 * Adds a model to this collection
1776 *
1777 * @method add
1778 * @return this
1779 * @public
1780 */
1781
1782
1783 add(model) {
1784 this.models.push(model);
1785 return this;
1786 }
1787 /**
1788 * Removes a model to this collection
1789 *
1790 * @method remove
1791 * @return this
1792 * @public
1793 */
1794
1795
1796 remove(model) {
1797 let match = this.models.find(m => isEqual(m.attrs, model.attrs));
1798
1799 if (match) {
1800 let i = this.models.indexOf(match);
1801 this.models.splice(i, 1);
1802 }
1803
1804 return this;
1805 }
1806 /**
1807 * Checks if the collection includes the model
1808 *
1809 * @method includes
1810 * @return boolean
1811 * @public
1812 */
1813
1814
1815 includes(model) {
1816 return this.models.some(m => isEqual(m.attrs, model.attrs));
1817 }
1818 /**
1819 * @method filter
1820 * @param f
1821 * @return {Collection}
1822 * @public
1823 */
1824
1825
1826 filter(f) {
1827 let filteredModels = this.models.filter(f);
1828 return new PolymorphicCollection(filteredModels);
1829 }
1830 /**
1831 * @method sort
1832 * @param f
1833 * @return {Collection}
1834 * @public
1835 */
1836
1837
1838 sort(f) {
1839 let sortedModels = this.models.concat().sort(f);
1840 return new PolymorphicCollection(sortedModels);
1841 }
1842 /**
1843 * @method slice
1844 * @param {Integer} begin
1845 * @param {Integer} end
1846 * @return {Collection}
1847 * @public
1848 */
1849
1850
1851 slice(...args) {
1852 let slicedModels = this.models.slice(...args);
1853 return new PolymorphicCollection(slicedModels);
1854 }
1855 /**
1856 * @method mergeCollection
1857 * @param collection
1858 * @return this
1859 * @public
1860 */
1861
1862
1863 mergeCollection(collection) {
1864 this.models = this.models.concat(collection.models);
1865 return this;
1866 }
1867 /**
1868 * Simple string representation of the collection and id.
1869 * @method toString
1870 * @return {String}
1871 * @public
1872 */
1873
1874
1875 toString() {
1876 return `collection:${this.modelName}(${this.models.map(m => m.id).join(",")})`;
1877 }
1878
1879}
1880
1881const identifierCache$1 = {};
1882/**
1883 * @class HasMany
1884 * @extends Association
1885 * @constructor
1886 * @public
1887 * @hide
1888 */
1889
1890class HasMany extends Association {
1891 get identifier() {
1892 if (typeof identifierCache$1[this.key] !== "string") {
1893 const identifier = `${camelize(this._container.inflector.singularize(this.key))}Ids`;
1894 identifierCache$1[this.key] = identifier;
1895 }
1896
1897 return identifierCache$1[this.key];
1898 }
1899 /**
1900 * @method getForeignKeyArray
1901 * @return {Array} Array of camelized model name of associated objects
1902 * and foreign key for the object owning the association
1903 * @public
1904 */
1905
1906
1907 getForeignKeyArray() {
1908 return [camelize(this.ownerModelName), this.getForeignKey()];
1909 }
1910 /**
1911 * @method getForeignKey
1912 * @return {String} Foreign key for the object owning the association
1913 * @public
1914 */
1915
1916
1917 getForeignKey() {
1918 // we reuse identifierCache because it's the same logic as get identifier
1919 if (typeof identifierCache$1[this.key] !== "string") {
1920 const foreignKey = `${this._container.inflector.singularize(camelize(this.key))}Ids`;
1921 identifierCache$1[this.key] = foreignKey;
1922 }
1923
1924 return identifierCache$1[this.key];
1925 }
1926 /**
1927 * Registers has-many association defined by given key on given model,
1928 * defines getters / setters for associated records and associated records' ids,
1929 * adds methods for creating unsaved child records and creating saved ones
1930 *
1931 * @method addMethodsToModelClass
1932 * @param {Function} ModelClass
1933 * @param {String} key
1934 * @public
1935 */
1936
1937
1938 addMethodsToModelClass(ModelClass, key) {
1939 let modelPrototype = ModelClass.prototype;
1940 let association = this;
1941 let foreignKey = this.getForeignKey();
1942 let associationHash = {
1943 [key]: this
1944 };
1945 modelPrototype.hasManyAssociations = Object.assign(modelPrototype.hasManyAssociations, associationHash); // update hasManyAssociationFks
1946
1947 Object.keys(modelPrototype.hasManyAssociations).forEach(key => {
1948 const value = modelPrototype.hasManyAssociations[key];
1949 modelPrototype.hasManyAssociationFks[value.getForeignKey()] = value;
1950 }); // Add to target's dependent associations array
1951
1952 this.schema.addDependentAssociation(this, this.modelName); // TODO: look how this is used. Are these necessary, seems like they could be gotten from the above?
1953 // Or we could use a single data structure to store this information?
1954
1955 modelPrototype.associationKeys.add(key);
1956 modelPrototype.associationIdKeys.add(foreignKey);
1957 Object.defineProperty(modelPrototype, foreignKey, {
1958 /*
1959 object.childrenIds
1960 - returns an array of the associated children's ids
1961 */
1962 get() {
1963 this._tempAssociations = this._tempAssociations || {};
1964 let tempChildren = this._tempAssociations[key];
1965 let ids = [];
1966
1967 if (tempChildren) {
1968 if (association.isPolymorphic) {
1969 ids = tempChildren.models.map(model => ({
1970 type: model.modelName,
1971 id: model.id
1972 }));
1973 } else {
1974 ids = tempChildren.models.map(model => model.id);
1975 }
1976 } else {
1977 ids = this.attrs[foreignKey] || [];
1978 }
1979
1980 return ids;
1981 },
1982
1983 /*
1984 object.childrenIds = ([childrenIds...])
1985 - sets the associated children (via id)
1986 */
1987 set(ids) {
1988 let tempChildren;
1989
1990 if (ids === null) {
1991 tempChildren = [];
1992 } else if (ids !== undefined) {
1993 assert(Array.isArray(ids), `You must pass an array in when setting ${foreignKey} on ${this}`);
1994
1995 if (association.isPolymorphic) {
1996 assert(ids.every(el => {
1997 return typeof el === "object" && typeof el.type !== undefined && typeof el.id !== undefined;
1998 }), `You must pass in an array of polymorphic identifiers (objects of shape { type, id }) when setting ${foreignKey} on ${this}`);
1999 let models = ids.map(({
2000 type,
2001 id
2002 }) => {
2003 return association.schema[association.schema.toCollectionName(type)].find(id);
2004 });
2005 tempChildren = new PolymorphicCollection(models);
2006 } else {
2007 tempChildren = association.schema[association.schema.toCollectionName(association.modelName)].find(ids);
2008 }
2009 }
2010
2011 this[key] = tempChildren;
2012 }
2013
2014 });
2015 Object.defineProperty(modelPrototype, key, {
2016 /*
2017 object.children
2018 - returns an array of associated children
2019 */
2020 get() {
2021 this._tempAssociations = this._tempAssociations || {};
2022 let collection = null;
2023
2024 if (this._tempAssociations[key]) {
2025 collection = this._tempAssociations[key];
2026 } else {
2027 if (association.isPolymorphic) {
2028 if (this[foreignKey]) {
2029 let polymorphicIds = this[foreignKey];
2030 let models = polymorphicIds.map(({
2031 type,
2032 id
2033 }) => {
2034 return association.schema[association.schema.toCollectionName(type)].find(id);
2035 });
2036 collection = new PolymorphicCollection(models);
2037 } else {
2038 collection = new PolymorphicCollection(association.modelName);
2039 }
2040 } else {
2041 if (this[foreignKey]) {
2042 collection = association.schema[association.schema.toCollectionName(association.modelName)].find(this[foreignKey]);
2043 } else {
2044 collection = new Collection(association.modelName);
2045 }
2046 }
2047
2048 this._tempAssociations[key] = collection;
2049 }
2050
2051 return collection;
2052 },
2053
2054 /*
2055 object.children = [model1, model2, ...]
2056 - sets the associated children (via array of models or Collection)
2057 */
2058 set(models) {
2059 if (models instanceof Collection || models instanceof PolymorphicCollection) {
2060 models = models.models;
2061 }
2062
2063 models = models ? compact(models) : [];
2064 this._tempAssociations = this._tempAssociations || {};
2065 let collection;
2066
2067 if (association.isPolymorphic) {
2068 collection = new PolymorphicCollection(models);
2069 } else {
2070 collection = new Collection(association.modelName, models);
2071 }
2072
2073 this._tempAssociations[key] = collection;
2074 models.forEach(model => {
2075 if (model.hasInverseFor(association)) {
2076 let inverse = model.inverseFor(association);
2077 model.associate(this, inverse);
2078 }
2079 });
2080 }
2081
2082 });
2083 /*
2084 object.newChild
2085 - creates a new unsaved associated child
2086 */
2087
2088 modelPrototype[`new${capitalize(camelize(this._container.inflector.singularize(association.key)))}`] = function (...args) {
2089 let modelName, attrs;
2090
2091 if (association.isPolymorphic) {
2092 modelName = args[0];
2093 attrs = args[1];
2094 } else {
2095 modelName = association.modelName;
2096 attrs = args[0];
2097 }
2098
2099 let child = association.schema[association.schema.toCollectionName(modelName)].new(attrs);
2100 let children = this[key].models;
2101 children.push(child);
2102 this[key] = children;
2103 return child;
2104 };
2105 /*
2106 object.createChild
2107 - creates a new saved associated child, and immediately persists both models
2108 TODO: forgot why this[key].add(child) doesn't work, most likely
2109 because these external APIs trigger saving cascades. Should probably
2110 have an internal method like this[key]._add.
2111 */
2112
2113
2114 modelPrototype[`create${capitalize(camelize(this._container.inflector.singularize(association.key)))}`] = function (...args) {
2115 let modelName, attrs;
2116
2117 if (association.isPolymorphic) {
2118 modelName = args[0];
2119 attrs = args[1];
2120 } else {
2121 modelName = association.modelName;
2122 attrs = args[0];
2123 }
2124
2125 let child = association.schema[association.schema.toCollectionName(modelName)].create(attrs);
2126 let children = this[key].models;
2127 children.push(child);
2128 this[key] = children;
2129 this.save();
2130 return child.reload();
2131 };
2132 }
2133 /**
2134 *
2135 *
2136 * @public
2137 */
2138
2139
2140 disassociateAllDependentsFromTarget(model) {
2141 let owner = this.ownerModelName;
2142 let fk;
2143
2144 if (this.isPolymorphic) {
2145 fk = {
2146 type: model.modelName,
2147 id: model.id
2148 };
2149 } else {
2150 fk = model.id;
2151 }
2152
2153 let dependents = this.schema[this.schema.toCollectionName(owner)].where(potentialOwner => {
2154 let currentIds = potentialOwner[this.getForeignKey()]; // Need this check because currentIds could be null
2155
2156 return currentIds && currentIds.find(id => {
2157 if (typeof id === "object") {
2158 return id.type === fk.type && id.id === fk.id;
2159 } else {
2160 return id === fk;
2161 }
2162 });
2163 });
2164 dependents.models.forEach(dependent => {
2165 dependent.disassociate(model, this);
2166 dependent.save();
2167 });
2168 }
2169
2170}
2171
2172const pathModelClassCache = {};
2173/**
2174 @hide
2175*/
2176
2177class BaseRouteHandler {
2178 getModelClassFromPath(fullPath) {
2179 if (!fullPath) {
2180 return;
2181 }
2182
2183 if (typeof pathModelClassCache[fullPath] !== "string") {
2184 let path = fullPath.split("/");
2185 let lastPath;
2186
2187 for (let i = path.length - 1; i >= 0; i--) {
2188 const segment = path[i];
2189
2190 if (segment.length && segment[0] !== ":") {
2191 lastPath = segment;
2192 break;
2193 }
2194 }
2195
2196 pathModelClassCache[fullPath] = dasherize(camelize(this._container.inflector.singularize(lastPath)));
2197 }
2198
2199 return pathModelClassCache[fullPath];
2200 }
2201
2202 _getIdForRequest(request, jsonApiDoc) {
2203 let id;
2204
2205 if (request && request.params && request.params.id) {
2206 id = request.params.id;
2207 } else if (jsonApiDoc && jsonApiDoc.data && jsonApiDoc.data.id) {
2208 id = jsonApiDoc.data.id;
2209 }
2210
2211 return id;
2212 }
2213
2214 _getJsonApiDocForRequest(request, modelName) {
2215 let body;
2216
2217 if (request && request.requestBody) {
2218 body = JSON.parse(request.requestBody);
2219 }
2220
2221 return this.serializerOrRegistry.normalize(body, modelName);
2222 }
2223
2224 _getAttrsForRequest(request, modelName) {
2225 let json = this._getJsonApiDocForRequest(request, modelName);
2226
2227 let id = this._getIdForRequest(request, json);
2228
2229 let attrs = {};
2230 assert(json.data && (json.data.attributes || json.data.type || json.data.relationships), `You're using a shorthand or #normalizedRequestAttrs, but your serializer's normalize function did not return a valid JSON:API document. Consult the docs for the normalize hook on the Serializer class.`);
2231
2232 if (json.data.attributes) {
2233 attrs = Object.keys(json.data.attributes).reduce((sum, key) => {
2234 sum[camelize(key)] = json.data.attributes[key];
2235 return sum;
2236 }, {});
2237 }
2238
2239 if (json.data.relationships) {
2240 Object.keys(json.data.relationships).forEach(relationshipName => {
2241 let relationship = json.data.relationships[relationshipName];
2242 let modelClass = this.schema.modelClassFor(modelName);
2243 let association = modelClass.associationFor(camelize(relationshipName));
2244 let valueForRelationship;
2245 assert(association, `You're passing the relationship '${relationshipName}' to the '${modelName}' model via a ${request.method} to '${request.url}', but you did not define the '${relationshipName}' association on the '${modelName}' model.`);
2246
2247 if (association.isPolymorphic) {
2248 valueForRelationship = relationship.data;
2249 } else if (association instanceof HasMany) {
2250 valueForRelationship = relationship.data && relationship.data.map(rel => rel.id);
2251 } else {
2252 valueForRelationship = relationship.data && relationship.data.id;
2253 }
2254
2255 attrs[association.identifier] = valueForRelationship;
2256 }, {});
2257 }
2258
2259 if (id) {
2260 attrs.id = id;
2261 }
2262
2263 return attrs;
2264 }
2265
2266 _getAttrsForFormRequest({
2267 requestBody
2268 }) {
2269 let attrs;
2270 let urlEncodedParts = [];
2271 assert(requestBody && typeof requestBody === "string", `You're using the helper method #normalizedFormData, but the request body is empty or not a valid url encoded string.`);
2272 urlEncodedParts = requestBody.split("&");
2273 attrs = urlEncodedParts.reduce((a, urlEncodedPart) => {
2274 let [key, value] = urlEncodedPart.split("=");
2275 a[key] = decodeURIComponent(value.replace(/\+/g, " "));
2276 return a;
2277 }, {});
2278 return attrs;
2279 }
2280
2281}
2282
2283/**
2284 * @hide
2285 */
2286
2287class FunctionRouteHandler extends BaseRouteHandler {
2288 constructor(schema, serializerOrRegistry, userFunction, path, server) {
2289 super(server);
2290 this.schema = schema;
2291 this.serializerOrRegistry = serializerOrRegistry;
2292 this.userFunction = userFunction;
2293 this.path = path;
2294 }
2295
2296 handle(request) {
2297 return this.userFunction(this.schema, request);
2298 }
2299
2300 setRequest(request) {
2301 this.request = request;
2302 }
2303
2304 serialize(response, serializerType) {
2305 let serializer;
2306
2307 if (serializerType) {
2308 serializer = this.serializerOrRegistry.serializerFor(serializerType, {
2309 explicit: true
2310 });
2311 } else {
2312 serializer = this.serializerOrRegistry;
2313 }
2314
2315 return serializer.serialize(response, this.request);
2316 }
2317
2318 normalizedRequestAttrs(modelName = null) {
2319 let {
2320 path,
2321 request,
2322 request: {
2323 requestHeaders
2324 }
2325 } = this;
2326 let attrs;
2327 let lowerCaseHeaders = {};
2328
2329 for (let header in requestHeaders) {
2330 lowerCaseHeaders[header.toLowerCase()] = requestHeaders[header];
2331 }
2332
2333 if (/x-www-form-urlencoded/.test(lowerCaseHeaders["content-type"])) {
2334 attrs = this._getAttrsForFormRequest(request);
2335 } else {
2336 if (modelName) {
2337 assert(dasherize(modelName) === modelName, `You called normalizedRequestAttrs('${modelName}'), but normalizedRequestAttrs was intended to be used with the dasherized version of the model type. Please change this to normalizedRequestAttrs('${dasherize(modelName)}').`);
2338 } else {
2339 modelName = this.getModelClassFromPath(path);
2340 }
2341
2342 assert(this.schema.hasModelForModelName(modelName), `You're using a shorthand or the #normalizedRequestAttrs helper but the detected model of '${modelName}' does not exist. You might need to pass in the correct modelName as the first argument to #normalizedRequestAttrs.`);
2343 attrs = this._getAttrsForRequest(request, modelName);
2344 }
2345
2346 return attrs;
2347 }
2348
2349}
2350
2351/**
2352 * @hide
2353 */
2354class ObjectRouteHandler {
2355 constructor(schema, serializerOrRegistry, object) {
2356 this.schema = schema;
2357 this.serializerOrRegistry = serializerOrRegistry;
2358 this.object = object;
2359 }
2360
2361 handle()
2362 /* request */
2363 {
2364 return this.object;
2365 }
2366
2367}
2368
2369/**
2370 @hide
2371*/
2372
2373class BaseShorthandRouteHandler extends BaseRouteHandler {
2374 constructor(schema, serializerOrRegistry, shorthand, path, options = {}) {
2375 super();
2376 shorthand = shorthand || this.getModelClassFromPath(path);
2377 this.schema = schema;
2378 this.serializerOrRegistry = serializerOrRegistry;
2379 this.shorthand = shorthand;
2380 this.options = options;
2381 let type = Array.isArray(shorthand) ? "array" : typeof shorthand;
2382
2383 if (type === "string") {
2384 let modelClass = this.schema[this.schema.toCollectionName(shorthand)];
2385
2386 this.handle = request => {
2387 return this.handleStringShorthand(request, modelClass);
2388 };
2389 } else if (type === "array") {
2390 let modelClasses = shorthand.map(modelName => this.schema[this.schema.toCollectionName(modelName)]);
2391
2392 this.handle = request => {
2393 return this.handleArrayShorthand(request, modelClasses);
2394 };
2395 }
2396 } // handleStringShorthand() {
2397 //
2398 // }
2399 //
2400 // handleArrayShorthand() {
2401 //
2402 // }
2403
2404
2405}
2406
2407/**
2408 * @hide
2409 */
2410
2411class GetShorthandRouteHandler extends BaseShorthandRouteHandler {
2412 /*
2413 Retrieve a model/collection from the db.
2414 Examples:
2415 this.get('/contacts', 'contact');
2416 this.get('/contacts/:id', 'contact');
2417 */
2418 handleStringShorthand(request, modelClass) {
2419 let modelName = this.shorthand;
2420 let camelizedModelName = camelize(modelName);
2421 assert(modelClass, `The route handler for ${request.url} is trying to access the ${camelizedModelName} model, but that model doesn't exist.`);
2422
2423 let id = this._getIdForRequest(request);
2424
2425 if (id) {
2426 let model = modelClass.find(id);
2427
2428 if (!model) {
2429 return new Response(404);
2430 } else {
2431 return model;
2432 }
2433 } else if (this.options.coalesce) {
2434 let ids = this.serializerOrRegistry.getCoalescedIds(request, camelizedModelName);
2435
2436 if (ids) {
2437 return modelClass.find(ids);
2438 }
2439 }
2440
2441 return modelClass.all();
2442 }
2443 /*
2444 Retrieve an array of collections from the db.
2445 Ex: this.get('/home', ['contacts', 'pictures']);
2446 */
2447
2448
2449 handleArrayShorthand(request, modelClasses) {
2450 let keys = this.shorthand;
2451
2452 let id = this._getIdForRequest(request);
2453 /*
2454 If the first key is singular and we have an id param in
2455 the request, we're dealing with the version of the shorthand
2456 that has a parent model and several has-many relationships.
2457 We throw an error, because the serializer is the appropriate
2458 place for this now.
2459 */
2460
2461
2462 assert(!id || this._container.inflector.singularize(keys[0]) !== keys[0], `It looks like you're using the "Single record with
2463 related records" version of the array shorthand, in addition to opting
2464 in to the model layer. This shorthand was made when there was no
2465 serializer layer. Now that you're using models, please ensure your
2466 relationships are defined, and create a serializer for the parent
2467 model, adding the relationships there.`);
2468 return modelClasses.map(modelClass => modelClass.all());
2469 }
2470
2471}
2472
2473/**
2474 * @hide
2475 */
2476
2477class PostShorthandRouteHandler extends BaseShorthandRouteHandler {
2478 /*
2479 Push a new model of type *camelizedModelName* to the db.
2480 For example, this will push a 'user':
2481 this.post('/contacts', 'user');
2482 */
2483 handleStringShorthand(request, modelClass) {
2484 let modelName = this.shorthand;
2485 let camelizedModelName = camelize(modelName);
2486 assert(modelClass, `The route handler for ${request.url} is trying to access the ${camelizedModelName} model, but that model doesn't exist.`);
2487
2488 let attrs = this._getAttrsForRequest(request, modelClass.camelizedModelName);
2489
2490 return modelClass.create(attrs);
2491 }
2492
2493}
2494
2495/**
2496 * @hide
2497 */
2498
2499class PutShorthandRouteHandler extends BaseShorthandRouteHandler {
2500 /*
2501 Update an object from the db, specifying the type.
2502 this.put('/contacts/:id', 'user');
2503 */
2504 handleStringShorthand(request, modelClass) {
2505 let modelName = this.shorthand;
2506 let camelizedModelName = camelize(modelName);
2507 assert(modelClass, `The route handler for ${request.url} is trying to access the ${camelizedModelName} model, but that model doesn't exist.`);
2508
2509 let id = this._getIdForRequest(request);
2510
2511 let model = modelClass.find(id);
2512
2513 if (!model) {
2514 return new Response(404);
2515 }
2516
2517 let attrs = this._getAttrsForRequest(request, modelClass.camelizedModelName);
2518
2519 return model.update(attrs);
2520 }
2521
2522}
2523
2524/**
2525 * @hide
2526 */
2527
2528class DeleteShorthandRouteHandler extends BaseShorthandRouteHandler {
2529 /*
2530 Remove the model from the db of type *camelizedModelName*.
2531 This would remove the user with id :id:
2532 Ex: this.del('/contacts/:id', 'user');
2533 */
2534 handleStringShorthand(request, modelClass) {
2535 let modelName = this.shorthand;
2536 let camelizedModelName = camelize(modelName);
2537 assert(modelClass, `The route handler for ${request.url} is trying to access the ${camelizedModelName} model, but that model doesn't exist.`);
2538
2539 let id = this._getIdForRequest(request);
2540
2541 let model = modelClass.find(id);
2542
2543 if (!model) {
2544 return new Response(404);
2545 }
2546
2547 model.destroy();
2548 }
2549 /*
2550 Remove the model and child related models from the db.
2551 This would remove the contact with id `:id`, as well
2552 as this contact's addresses and phone numbers.
2553 Ex: this.del('/contacts/:id', ['contact', 'addresses', 'numbers');
2554 */
2555
2556
2557 handleArrayShorthand(request, modelClasses) {
2558 let id = this._getIdForRequest(request);
2559
2560 let parent = modelClasses[0].find(id);
2561 let childTypes = modelClasses.slice(1).map(modelClass => this._container.inflector.pluralize(modelClass.camelizedModelName)); // Delete related children
2562
2563 childTypes.forEach(type => parent[type].destroy());
2564 parent.destroy();
2565 }
2566
2567}
2568
2569/**
2570 * @hide
2571 */
2572
2573class HeadShorthandRouteHandler extends BaseShorthandRouteHandler {
2574 /*
2575 Retrieve a model/collection from the db.
2576 Examples:
2577 this.head('/contacts', 'contact');
2578 this.head('/contacts/:id', 'contact');
2579 */
2580 handleStringShorthand(request, modelClass) {
2581 let modelName = this.shorthand;
2582 let camelizedModelName = camelize(modelName);
2583 assert(modelClass, `The route handler for ${request.url} is trying to access the ${camelizedModelName} model, but that model doesn't exist.`);
2584
2585 let id = this._getIdForRequest(request);
2586
2587 if (id) {
2588 let model = modelClass.find(id);
2589
2590 if (!model) {
2591 return new Response(404);
2592 } else {
2593 return new Response(204);
2594 }
2595 } else if (this.options.coalesce && request.queryParams && request.queryParams.ids) {
2596 let model = modelClass.find(request.queryParams.ids);
2597
2598 if (!model) {
2599 return new Response(404);
2600 } else {
2601 return new Response(204);
2602 }
2603 } else {
2604 return new Response(204);
2605 }
2606 }
2607
2608}
2609
2610const DEFAULT_CODES = {
2611 get: 200,
2612 put: 204,
2613 post: 201,
2614 delete: 204
2615};
2616
2617function createHandler({
2618 verb,
2619 schema,
2620 serializerOrRegistry,
2621 path,
2622 rawHandler,
2623 options
2624}) {
2625 let handler;
2626 let args = [schema, serializerOrRegistry, rawHandler, path, options];
2627 let type = typeof rawHandler;
2628
2629 if (type === "function") {
2630 handler = new FunctionRouteHandler(...args);
2631 } else if (type === "object" && rawHandler) {
2632 handler = new ObjectRouteHandler(...args);
2633 } else if (verb === "get") {
2634 handler = new GetShorthandRouteHandler(...args);
2635 } else if (verb === "post") {
2636 handler = new PostShorthandRouteHandler(...args);
2637 } else if (verb === "put" || verb === "patch") {
2638 handler = new PutShorthandRouteHandler(...args);
2639 } else if (verb === "delete") {
2640 handler = new DeleteShorthandRouteHandler(...args);
2641 } else if (verb === "head") {
2642 handler = new HeadShorthandRouteHandler(...args);
2643 }
2644
2645 return handler;
2646}
2647/**
2648 * @hide
2649 */
2650
2651
2652class RouteHandler {
2653 constructor({
2654 schema,
2655 verb,
2656 rawHandler,
2657 customizedCode,
2658 options,
2659 path,
2660 serializerOrRegistry
2661 }) {
2662 this.verb = verb;
2663 this.customizedCode = customizedCode;
2664 this.serializerOrRegistry = serializerOrRegistry;
2665 this.handler = createHandler({
2666 verb,
2667 schema,
2668 path,
2669 serializerOrRegistry,
2670 rawHandler,
2671 options
2672 });
2673 }
2674
2675 handle(request) {
2676 return this._getMirageResponseForRequest(request).then(mirageResponse => this.serialize(mirageResponse, request)).then(serializedMirageResponse => {
2677 return serializedMirageResponse.toRackResponse();
2678 });
2679 }
2680
2681 _getMirageResponseForRequest(request) {
2682 let result;
2683
2684 try {
2685 /*
2686 We need to do this for the #serialize convenience method. Probably is
2687 a better way.
2688 */
2689 if (this.handler instanceof FunctionRouteHandler) {
2690 this.handler.setRequest(request);
2691 }
2692
2693 result = this.handler.handle(request);
2694 } catch (e) {
2695 if (e instanceof MirageError) {
2696 result = new Response(500, {}, e);
2697 } else {
2698 let message = e.message || e;
2699 result = new Response(500, {}, {
2700 message,
2701 stack: `Mirage: Your ${request.method} handler for the url ${request.url} threw an error:\n\n${e.stack || e}`
2702 });
2703 }
2704 }
2705
2706 return this._toMirageResponse(result);
2707 }
2708
2709 _toMirageResponse(result) {
2710 let mirageResponse;
2711 return new Promise((resolve, reject) => {
2712 Promise.resolve(result).then(response => {
2713 if (response instanceof Response) {
2714 mirageResponse = result;
2715 } else {
2716 let code = this._getCodeForResponse(response);
2717
2718 mirageResponse = new Response(code, {}, response);
2719 }
2720
2721 resolve(mirageResponse);
2722 }).catch(reject);
2723 });
2724 }
2725
2726 _getCodeForResponse(response) {
2727 let code;
2728
2729 if (this.customizedCode) {
2730 code = this.customizedCode;
2731 } else {
2732 code = DEFAULT_CODES[this.verb]; // Returning any data for a 204 is invalid
2733
2734 if (code === 204 && response !== undefined && response !== "") {
2735 code = 200;
2736 }
2737 }
2738
2739 return code;
2740 }
2741
2742 serialize(mirageResponse, request) {
2743 mirageResponse.data = this.serializerOrRegistry.serialize(mirageResponse.data, request);
2744 return mirageResponse;
2745 }
2746
2747}
2748
2749/**
2750 @hide
2751*/
2752
2753function extend(protoProps, staticProps) {
2754 class Child extends this {
2755 constructor(...args) {
2756 super(...args); // The constructor function for the new subclass is optionally defined by you
2757 // in your `extend` definition
2758
2759 if (protoProps && has(protoProps, "constructor")) {
2760 protoProps.constructor.call(this, ...args);
2761 }
2762 }
2763
2764 } // Add static properties to the constructor function, if supplied.
2765
2766
2767 Object.assign(Child, this, staticProps); // Add prototype properties (instance properties) to the subclass,
2768 // if supplied.
2769
2770 if (protoProps) {
2771 Object.assign(Child.prototype, protoProps);
2772 }
2773
2774 return Child;
2775}
2776
2777/**
2778 Models wrap your database, and allow you to define relationships.
2779
2780 **Class vs. instance methods**
2781
2782 The methods documented below apply to _instances_ of models, but you'll typically use the `Schema` to access the model _class_, which can be used to find or create instances.
2783
2784 You can find the Class methods documented under the `Schema` API docs.
2785
2786 **Accessing properties and relationships**
2787
2788 You can access properites (fields) and relationships directly off of models.
2789
2790 ```js
2791 user.name; // 'Sam'
2792 user.team; // Team model
2793 user.teamId; // Team id (foreign key)
2794 ```
2795
2796 Mirage Models are schemaless in their attributes, but their relationship schema is known.
2797
2798 For example,
2799
2800 ```js
2801 let user = schema.users.create();
2802 user.attrs // { }
2803 user.name // undefined
2804
2805 let user = schema.users.create({ name: 'Sam' });
2806 user.attrs // { name: 'Sam' }
2807 user.name // 'Sam'
2808 ```
2809
2810 However, if a `user` has a `posts` relationships defined,
2811
2812 ```js
2813 let user = schema.users.create();
2814 user.posts // returns an empty Posts Collection
2815 ```
2816
2817 @class Model
2818 @constructor
2819 @public
2820 */
2821
2822class Model {
2823 // TODO: schema and modelName now set statically at registration, need to remove
2824
2825 /*
2826 Notes:
2827 - We need to pass in modelName, because models are created with
2828 .extend and anonymous functions, so you cannot use
2829 reflection to find the name of the constructor.
2830 */
2831 constructor(schema, modelName, attrs, fks) {
2832 assert(schema, "A model requires a schema");
2833 assert(modelName, "A model requires a modelName");
2834 this._schema = schema;
2835 this.modelName = modelName;
2836 this.fks = fks || [];
2837 /**
2838 Returns the attributes of your model.
2839 ```js
2840 let post = schema.blogPosts.find(1);
2841 post.attrs; // {id: 1, title: 'Lorem Ipsum', publishedAt: '2012-01-01 10:00:00'}
2842 ```
2843 Note that you can also access individual attributes directly off a model, e.g. `post.title`.
2844 @property attrs
2845 @public
2846 */
2847
2848 this.attrs = {};
2849 attrs = attrs || {}; // Ensure fks are there
2850
2851 this.fks.forEach(fk => {
2852 this.attrs[fk] = attrs[fk] !== undefined ? attrs[fk] : null;
2853 });
2854 Object.keys(attrs).forEach(name => {
2855 const value = attrs[name];
2856
2857 this._validateAttr(name, value);
2858
2859 this._setupAttr(name, value);
2860
2861 this._setupRelationship(name, value);
2862 });
2863 return this;
2864 }
2865 /**
2866 Create or saves the model.
2867 ```js
2868 let post = blogPosts.new({ title: 'Lorem ipsum' });
2869 post.id; // null
2870 post.save();
2871 post.id; // 1
2872 post.title = 'Hipster ipsum'; // db has not been updated
2873 post.save(); // ...now the db is updated
2874 ```
2875 @method save
2876 @return this
2877 @public
2878 */
2879
2880
2881 save() {
2882 let collection = this._schema.toInternalCollectionName(this.modelName);
2883
2884 if (this.isNew()) {
2885 // Update the attrs with the db response
2886 this.attrs = this._schema.db[collection].insert(this.attrs); // Ensure the id getter/setter is set
2887
2888 this._definePlainAttribute("id");
2889 } else {
2890 this._schema.isSaving[this.toString()] = true;
2891
2892 this._schema.db[collection].update(this.attrs.id, this.attrs);
2893 }
2894
2895 this._saveAssociations();
2896
2897 this._schema.isSaving[this.toString()] = false;
2898 return this;
2899 }
2900 /**
2901 Updates the record in the db.
2902 ```js
2903 let post = blogPosts.find(1);
2904 post.update('title', 'Hipster ipsum'); // the db was updated
2905 post.update({
2906 title: 'Lorem ipsum',
2907 created_at: 'before it was cool'
2908 });
2909 ```
2910 @method update
2911 @param {String} key
2912 @param {String} val
2913 @return this
2914 @public
2915 */
2916
2917
2918 update(key, val) {
2919 let attrs;
2920
2921 if (key == null) {
2922 return this;
2923 }
2924
2925 if (typeof key === "object") {
2926 attrs = key;
2927 } else {
2928 (attrs = {})[key] = val;
2929 }
2930
2931 Object.keys(attrs).forEach(function (attr) {
2932 if (!this.associationKeys.has(attr) && !this.associationIdKeys.has(attr)) {
2933 this._definePlainAttribute(attr);
2934 }
2935
2936 this[attr] = attrs[attr];
2937 }, this);
2938 this.save();
2939 return this;
2940 }
2941 /**
2942 Destroys the db record.
2943 ```js
2944 let post = blogPosts.find(1);
2945 post.destroy(); // removed from the db
2946 ```
2947 @method destroy
2948 @public
2949 */
2950
2951
2952 destroy() {
2953 if (this.isSaved()) {
2954 this._disassociateFromDependents();
2955
2956 let collection = this._schema.toInternalCollectionName(this.modelName);
2957
2958 this._schema.db[collection].remove(this.attrs.id);
2959 }
2960 }
2961 /**
2962 Boolean, true if the model has not been persisted yet to the db.
2963 ```js
2964 let post = blogPosts.new({title: 'Lorem ipsum'});
2965 post.isNew(); // true
2966 post.id; // null
2967 post.save(); // true
2968 post.isNew(); // false
2969 post.id; // 1
2970 ```
2971 @method isNew
2972 @return {Boolean}
2973 @public
2974 */
2975
2976
2977 isNew() {
2978 let hasDbRecord = false;
2979 let hasId = this.attrs.id !== undefined && this.attrs.id !== null;
2980
2981 if (hasId) {
2982 let collectionName = this._schema.toInternalCollectionName(this.modelName);
2983
2984 let record = this._schema.db[collectionName].find(this.attrs.id);
2985
2986 if (record) {
2987 hasDbRecord = true;
2988 }
2989 }
2990
2991 return !hasDbRecord;
2992 }
2993 /**
2994 Boolean, opposite of `isNew`
2995 @method isSaved
2996 @return {Boolean}
2997 @public
2998 */
2999
3000
3001 isSaved() {
3002 return !this.isNew();
3003 }
3004 /**
3005 Reload a model's data from the database.
3006 ```js
3007 let post = blogPosts.find(1);
3008 post.attrs; // {id: 1, title: 'Lorem ipsum'}
3009 post.title = 'Hipster ipsum';
3010 post.title; // 'Hipster ipsum';
3011 post.reload(); // true
3012 post.title; // 'Lorem ipsum'
3013 ```
3014 @method reload
3015 @return this
3016 @public
3017 */
3018
3019
3020 reload() {
3021 if (this.id) {
3022 let collection = this._schema.toInternalCollectionName(this.modelName);
3023
3024 let attrs = this._schema.db[collection].find(this.id);
3025
3026 Object.keys(attrs).filter(function (attr) {
3027 return attr !== "id";
3028 }).forEach(function (attr) {
3029 this.attrs[attr] = attrs[attr];
3030 }, this);
3031 } // Clear temp associations
3032
3033
3034 this._tempAssociations = {};
3035 return this;
3036 }
3037
3038 toJSON() {
3039 return this.attrs;
3040 }
3041 /**
3042 Returns the association for the given key
3043 @method associationFor
3044 @param key
3045 @public
3046 @hide
3047 */
3048
3049
3050 associationFor(key) {
3051 return this._schema.associationsFor(this.modelName)[key];
3052 }
3053 /**
3054 Returns this model's inverse association for the given
3055 model-type-association pair, if it exists.
3056 Example:
3057 post: Model.extend({
3058 comments: hasMany()
3059 }),
3060 comments: Model.extend({
3061 post: belongsTo()
3062 })
3063 post.inversefor(commentsPostAssociation) would return the
3064 `post.comments` association object.
3065 Originally we had association.inverse() but that became impossible with
3066 the addition of polymorphic models. Consider the following:
3067 post: Model.extend({
3068 comments: hasMany()
3069 }),
3070 picture: Model.extend({
3071 comments: hasMany()
3072 }),
3073 comments: Model.extend({
3074 commentable: belongsTo({ polymorphic: true })
3075 })
3076 `commentable.inverse()` is ambiguous - does it return
3077 `post.comments` or `picture.comments`? Instead we need to ask each model
3078 if it has an inverse for a given association. post.inverseFor(commentable)
3079 is no longer ambiguous.
3080 @method hasInverseFor
3081 @param {String} modelName The model name of the class we're scanning
3082 @param {ORM/Association} association
3083 @return {ORM/Association}
3084 @public
3085 @hide
3086 */
3087
3088
3089 inverseFor(association) {
3090 return this._explicitInverseFor(association) || this._implicitInverseFor(association);
3091 }
3092 /**
3093 Finds the inverse for an association that explicity defines it's inverse
3094 @private
3095 @hide
3096 */
3097
3098
3099 _explicitInverseFor(association) {
3100 this._checkForMultipleExplicitInverses(association);
3101
3102 let associations = this._schema.associationsFor(this.modelName);
3103
3104 let inverse = association.opts.inverse;
3105 let candidate = inverse ? associations[inverse] : null;
3106 let matchingPolymorphic = candidate && candidate.isPolymorphic;
3107 let matchingInverse = candidate && candidate.modelName === association.ownerModelName;
3108 let candidateInverse = candidate && candidate.opts.inverse;
3109
3110 if (candidateInverse && candidate.opts.inverse !== association.key) {
3111 assert(false, `You specified an inverse of ${inverse} for ${association.key}, but it does not match ${candidate.modelName} ${candidate.key}'s inverse`);
3112 }
3113
3114 return matchingPolymorphic || matchingInverse ? candidate : null;
3115 }
3116 /**
3117 Ensures multiple explicit inverses don't exist on the current model
3118 for the given association.
3119 TODO: move this to compile-time check
3120 @private
3121 @hide
3122 */
3123
3124
3125 _checkForMultipleExplicitInverses(association) {
3126 let associations = this._schema.associationsFor(this.modelName);
3127
3128 let matchingExplicitInverses = Object.keys(associations).filter(key => {
3129 let candidate = associations[key];
3130 let modelMatches = association.ownerModelName === candidate.modelName;
3131 let inverseKeyMatches = association.key === candidate.opts.inverse;
3132 return modelMatches && inverseKeyMatches;
3133 });
3134 assert(matchingExplicitInverses.length <= 1, `The ${this.modelName} model has defined multiple explicit inverse associations for the ${association.ownerModelName}.${association.key} association.`);
3135 }
3136 /**
3137 Finds if there is an inverse for an association that does not
3138 explicitly define one.
3139 @private
3140 @hide
3141 */
3142
3143
3144 _implicitInverseFor(association) {
3145 let associations = this._schema.associationsFor(this.modelName);
3146
3147 let modelName = association.ownerModelName;
3148 return values(associations).filter(candidate => candidate.modelName === modelName).reduce((inverse, candidate) => {
3149 let candidateInverse = candidate.opts.inverse;
3150 let candidateIsImplicitInverse = candidateInverse === undefined;
3151 let candidateIsExplicitInverse = candidateInverse === association.key;
3152 let candidateMatches = candidateIsImplicitInverse || candidateIsExplicitInverse;
3153
3154 if (candidateMatches) {
3155 // Need to move this check to compile-time init
3156 assert(!inverse, `The ${this.modelName} model has multiple possible inverse associations for the ${association.ownerModelName}.${association.key} association.`);
3157 inverse = candidate;
3158 }
3159
3160 return inverse;
3161 }, null);
3162 }
3163 /**
3164 Returns whether this model has an inverse association for the given
3165 model-type-association pair.
3166 @method hasInverseFor
3167 @param {String} modelName
3168 @param {ORM/Association} association
3169 @return {Boolean}
3170 @public
3171 @hide
3172 */
3173
3174
3175 hasInverseFor(association) {
3176 return !!this.inverseFor(association);
3177 }
3178 /**
3179 Used to check if models match each other. If models are saved, we check model type
3180 and id, since they could have other non-persisted properties that are different.
3181 @public
3182 @hide
3183 */
3184
3185
3186 alreadyAssociatedWith(model, association) {
3187 let {
3188 key
3189 } = association;
3190 let associatedModelOrCollection = this[key];
3191
3192 if (associatedModelOrCollection && model) {
3193 if (associatedModelOrCollection instanceof Model) {
3194 if (associatedModelOrCollection.isSaved() && model.isSaved()) {
3195 return associatedModelOrCollection.toString() === model.toString();
3196 } else {
3197 return associatedModelOrCollection === model;
3198 }
3199 } else {
3200 return associatedModelOrCollection.includes(model);
3201 }
3202 }
3203 }
3204
3205 associate(model, association) {
3206 if (this.alreadyAssociatedWith(model, association)) {
3207 return;
3208 }
3209
3210 let {
3211 key
3212 } = association;
3213
3214 if (association instanceof HasMany) {
3215 if (!this[key].includes(model)) {
3216 this[key].add(model);
3217 }
3218 } else {
3219 this[key] = model;
3220 }
3221 }
3222
3223 disassociate(model, association) {
3224 let fk = association.getForeignKey();
3225
3226 if (association instanceof HasMany) {
3227 let i;
3228
3229 if (association.isPolymorphic) {
3230 let found = this[fk].find(({
3231 type,
3232 id
3233 }) => type === model.modelName && id === model.id);
3234 i = found && this[fk].indexOf(found);
3235 } else {
3236 i = this[fk].map(key => key.toString()).indexOf(model.id.toString());
3237 }
3238
3239 if (i > -1) {
3240 this.attrs[fk].splice(i, 1);
3241 }
3242 } else {
3243 this.attrs[fk] = null;
3244 }
3245 }
3246 /**
3247 @hide
3248 */
3249
3250
3251 get isSaving() {
3252 return this._schema.isSaving[this.toString()];
3253 } // Private
3254
3255 /**
3256 model.attrs represents the persistable attributes, i.e. your db
3257 table fields.
3258 @method _setupAttr
3259 @param attr
3260 @param value
3261 @private
3262 @hide
3263 */
3264
3265
3266 _setupAttr(attr, value) {
3267 const isAssociation = this.associationKeys.has(attr) || this.associationIdKeys.has(attr);
3268
3269 if (!isAssociation) {
3270 this.attrs[attr] = value; // define plain getter/setters for non-association keys
3271
3272 this._definePlainAttribute(attr);
3273 }
3274 }
3275 /**
3276 Define getter/setter for a plain attribute
3277 @method _definePlainAttribute
3278 @param attr
3279 @private
3280 @hide
3281 */
3282
3283
3284 _definePlainAttribute(attr) {
3285 // Ensure the property hasn't already been defined
3286 let existingProperty = Object.getOwnPropertyDescriptor(this, attr);
3287
3288 if (existingProperty && existingProperty.get) {
3289 return;
3290 } // Ensure the attribute is on the attrs hash
3291
3292
3293 if (!Object.prototype.hasOwnProperty.call(this.attrs, attr)) {
3294 this.attrs[attr] = null;
3295 } // Define the getter/setter
3296
3297
3298 Object.defineProperty(this, attr, {
3299 get() {
3300 return this.attrs[attr];
3301 },
3302
3303 set(val) {
3304 this.attrs[attr] = val;
3305 return this;
3306 }
3307
3308 });
3309 }
3310 /**
3311 Foreign keys get set on attrs directly (to avoid potential recursion), but
3312 model references use the setter.
3313 *
3314 We validate foreign keys during instantiation.
3315 *
3316 @method _setupRelationship
3317 @param attr
3318 @param value
3319 @private
3320 @hide
3321 */
3322
3323
3324 _setupRelationship(attr, value) {
3325 const isFk = this.associationIdKeys.has(attr) || this.fks.includes(attr);
3326 const isAssociation = this.associationKeys.has(attr);
3327
3328 if (isFk) {
3329 if (value !== undefined && value !== null) {
3330 this._validateForeignKeyExistsInDatabase(attr, value);
3331 }
3332
3333 this.attrs[attr] = value;
3334 }
3335
3336 if (isAssociation) {
3337 this[attr] = value;
3338 }
3339 }
3340 /**
3341 @method _validateAttr
3342 @private
3343 @hide
3344 */
3345
3346
3347 _validateAttr(key, value) {
3348 // Verify attr passed in for associations is actually an association
3349 {
3350 if (this.associationKeys.has(key)) {
3351 let association = this.associationFor(key);
3352 let isNull = value === null;
3353
3354 if (association instanceof HasMany) {
3355 let isCollection = value instanceof Collection || value instanceof PolymorphicCollection;
3356 let isArrayOfModels = Array.isArray(value) && value.every(item => item instanceof Model);
3357 assert(isCollection || isArrayOfModels || isNull, `You're trying to create a ${this.modelName} model and you passed in "${value}" under the ${key} key, but that key is a HasMany relationship. You must pass in a Collection, PolymorphicCollection, array of Models, or null.`);
3358 } else if (association instanceof BelongsTo) {
3359 assert(value instanceof Model || isNull, `You're trying to create a ${this.modelName} model and you passed in "${value}" under the ${key} key, but that key is a BelongsTo relationship. You must pass in a Model or null.`);
3360 }
3361 }
3362 } // Verify attrs passed in for association foreign keys are actually fks
3363
3364 {
3365 if (this.associationIdKeys.has(key)) {
3366 if (key.endsWith("Ids")) {
3367 let isArray = Array.isArray(value);
3368 let isNull = value === null;
3369 assert(isArray || isNull, `You're trying to create a ${this.modelName} model and you passed in "${value}" under the ${key} key, but that key is a foreign key for a HasMany relationship. You must pass in an array of ids or null.`);
3370 }
3371 }
3372 } // Verify no undefined associations are passed in
3373
3374 {
3375 let isModelOrCollection = value instanceof Model || value instanceof Collection || value instanceof PolymorphicCollection;
3376 let isArrayOfModels = Array.isArray(value) && value.length && value.every(item => item instanceof Model);
3377
3378 if (isModelOrCollection || isArrayOfModels) {
3379 let modelOrCollection = value;
3380 assert(this.associationKeys.has(key), `You're trying to create a ${this.modelName} model and you passed in a ${modelOrCollection.toString()} under the ${key} key, but you haven't defined that key as an association on your model.`);
3381 }
3382 }
3383 }
3384 /**
3385 Originally we validated this via association.setId method, but it triggered
3386 recursion. That method is designed for updating an existing model's ID so
3387 this method is needed during instantiation.
3388 *
3389 @method _validateForeignKeyExistsInDatabase
3390 @private
3391 @hide
3392 */
3393
3394
3395 _validateForeignKeyExistsInDatabase(foreignKeyName, foreignKeys) {
3396 if (Array.isArray(foreignKeys)) {
3397 let association = this.hasManyAssociationFks[foreignKeyName];
3398 let found;
3399
3400 if (association.isPolymorphic) {
3401 found = foreignKeys.map(({
3402 type,
3403 id
3404 }) => {
3405 return this._schema.db[this._schema.toInternalCollectionName(type)].find(id);
3406 });
3407 found = compact(found);
3408 } else {
3409 found = this._schema.db[this._schema.toInternalCollectionName(association.modelName)].find(foreignKeys);
3410 }
3411
3412 let foreignKeyLabel = association.isPolymorphic ? foreignKeys.map(fk => `${fk.type}:${fk.id}`).join(",") : foreignKeys;
3413 assert(found.length === foreignKeys.length, `You're instantiating a ${this.modelName} that has a ${foreignKeyName} of ${foreignKeyLabel}, but some of those records don't exist in the database.`);
3414 } else {
3415 let association = this.belongsToAssociationFks[foreignKeyName];
3416 let found;
3417
3418 if (association.isPolymorphic) {
3419 found = this._schema.db[this._schema.toInternalCollectionName(foreignKeys.type)].find(foreignKeys.id);
3420 } else {
3421 found = this._schema.db[this._schema.toInternalCollectionName(association.modelName)].find(foreignKeys);
3422 }
3423
3424 let foreignKeyLabel = association.isPolymorphic ? `${foreignKeys.type}:${foreignKeys.id}` : foreignKeys;
3425 assert(found, `You're instantiating a ${this.modelName} that has a ${foreignKeyName} of ${foreignKeyLabel}, but that record doesn't exist in the database.`);
3426 }
3427 }
3428 /**
3429 Update associated children when saving a collection
3430 *
3431 @method _saveAssociations
3432 @private
3433 @hide
3434 */
3435
3436
3437 _saveAssociations() {
3438 this._saveBelongsToAssociations();
3439
3440 this._saveHasManyAssociations();
3441 }
3442
3443 _saveBelongsToAssociations() {
3444 values(this.belongsToAssociations).forEach(association => {
3445 this._disassociateFromOldInverses(association);
3446
3447 this._saveNewAssociates(association);
3448
3449 this._associateWithNewInverses(association);
3450 });
3451 }
3452
3453 _saveHasManyAssociations() {
3454 values(this.hasManyAssociations).forEach(association => {
3455 this._disassociateFromOldInverses(association);
3456
3457 this._saveNewAssociates(association);
3458
3459 this._associateWithNewInverses(association);
3460 });
3461 }
3462
3463 _disassociateFromOldInverses(association) {
3464 if (association instanceof HasMany) {
3465 this._disassociateFromHasManyInverses(association);
3466 } else if (association instanceof BelongsTo) {
3467 this._disassociateFromBelongsToInverse(association);
3468 }
3469 } // Disassociate currently persisted models that are no longer associated
3470
3471
3472 _disassociateFromHasManyInverses(association) {
3473 let {
3474 key
3475 } = association;
3476 let fk = association.getForeignKey();
3477 let tempAssociation = this._tempAssociations && this._tempAssociations[key];
3478 let associateIds = this.attrs[fk];
3479
3480 if (tempAssociation && associateIds) {
3481 let models;
3482
3483 if (association.isPolymorphic) {
3484 models = associateIds.map(({
3485 type,
3486 id
3487 }) => {
3488 return this._schema[this._schema.toCollectionName(type)].find(id);
3489 });
3490 } else {
3491 // TODO: prob should initialize hasMany fks with []
3492 models = this._schema[this._schema.toCollectionName(association.modelName)].find(associateIds || []).models;
3493 }
3494
3495 models.filter(associate => // filter out models that are already being saved
3496 !associate.isSaving && // filter out models that will still be associated
3497 !tempAssociation.includes(associate) && associate.hasInverseFor(association)).forEach(associate => {
3498 let inverse = associate.inverseFor(association);
3499 associate.disassociate(this, inverse);
3500 associate.save();
3501 });
3502 }
3503 }
3504 /*
3505 Disassociate currently persisted models that are no longer associated.
3506 Example:
3507 post: Model.extend({
3508 comments: hasMany()
3509 }),
3510 comment: Model.extend({
3511 post: belongsTo()
3512 })
3513 Assume `this` is comment:1. When saving, if comment:1 is no longer
3514 associated with post:1, we need to remove comment:1 from post:1.comments.
3515 In this example `association` would be `comment.post`.
3516 */
3517
3518
3519 _disassociateFromBelongsToInverse(association) {
3520 let {
3521 key
3522 } = association;
3523 let fk = association.getForeignKey();
3524 let tempAssociation = this._tempAssociations && this._tempAssociations[key];
3525 let associateId = this.attrs[fk];
3526
3527 if (tempAssociation !== undefined && associateId) {
3528 let associate;
3529
3530 if (association.isPolymorphic) {
3531 associate = this._schema[this._schema.toCollectionName(associateId.type)].find(associateId.id);
3532 } else {
3533 associate = this._schema[this._schema.toCollectionName(association.modelName)].find(associateId);
3534 }
3535
3536 if (associate.hasInverseFor(association)) {
3537 let inverse = associate.inverseFor(association);
3538 associate.disassociate(this, inverse);
3539
3540 associate._updateInDb(associate.attrs);
3541 }
3542 }
3543 } // Find all other models that depend on me and update their foreign keys
3544
3545
3546 _disassociateFromDependents() {
3547 this._schema.dependentAssociationsFor(this.modelName).forEach(association => {
3548 association.disassociateAllDependentsFromTarget(this);
3549 });
3550 }
3551
3552 _saveNewAssociates(association) {
3553 let {
3554 key
3555 } = association;
3556 let fk = association.getForeignKey();
3557 let tempAssociate = this._tempAssociations && this._tempAssociations[key];
3558
3559 if (tempAssociate !== undefined) {
3560 this.__isSavingNewChildren = true;
3561 delete this._tempAssociations[key];
3562
3563 if (tempAssociate instanceof Collection) {
3564 tempAssociate.models.filter(model => !model.isSaving).forEach(child => {
3565 child.save();
3566 });
3567
3568 this._updateInDb({
3569 [fk]: tempAssociate.models.map(child => child.id)
3570 });
3571 } else if (tempAssociate instanceof PolymorphicCollection) {
3572 tempAssociate.models.filter(model => !model.isSaving).forEach(child => {
3573 child.save();
3574 });
3575
3576 this._updateInDb({
3577 [fk]: tempAssociate.models.map(child => {
3578 return {
3579 type: child.modelName,
3580 id: child.id
3581 };
3582 })
3583 });
3584 } else {
3585 // Clearing the association
3586 if (tempAssociate === null) {
3587 this._updateInDb({
3588 [fk]: null
3589 }); // Self-referential
3590
3591 } else if (this.equals(tempAssociate)) {
3592 this._updateInDb({
3593 [fk]: this.id
3594 }); // Non-self-referential
3595
3596 } else if (!tempAssociate.isSaving) {
3597 // Save the tempAssociate and update the local reference
3598 tempAssociate.save();
3599
3600 this._syncTempAssociations(tempAssociate);
3601
3602 let fkValue;
3603
3604 if (association.isPolymorphic) {
3605 fkValue = {
3606 id: tempAssociate.id,
3607 type: tempAssociate.modelName
3608 };
3609 } else {
3610 fkValue = tempAssociate.id;
3611 }
3612
3613 this._updateInDb({
3614 [fk]: fkValue
3615 });
3616 }
3617 }
3618
3619 this.__isSavingNewChildren = false;
3620 }
3621 }
3622 /*
3623 Step 3 in saving associations.
3624 Example:
3625 // initial state
3626 post.author = steinbeck;
3627 // new state
3628 post.author = twain;
3629 1. Disassociate from old inverse (remove post from steinbeck.posts)
3630 2. Save new associates (if twain.isNew, save twain)
3631 -> 3. Associate with new inverse (add post to twain.posts)
3632 */
3633
3634
3635 _associateWithNewInverses(association) {
3636 if (!this.__isSavingNewChildren) {
3637 let modelOrCollection = this[association.key];
3638
3639 if (modelOrCollection instanceof Model) {
3640 this._associateModelWithInverse(modelOrCollection, association);
3641 } else if (modelOrCollection instanceof Collection || modelOrCollection instanceof PolymorphicCollection) {
3642 modelOrCollection.models.forEach(model => {
3643 this._associateModelWithInverse(model, association);
3644 });
3645 }
3646
3647 delete this._tempAssociations[association.key];
3648 }
3649 }
3650
3651 _associateModelWithInverse(model, association) {
3652 if (model.hasInverseFor(association)) {
3653 let inverse = model.inverseFor(association);
3654 let inverseFk = inverse.getForeignKey();
3655 let ownerId = this.id;
3656
3657 if (inverse instanceof BelongsTo) {
3658 let newId;
3659
3660 if (inverse.isPolymorphic) {
3661 newId = {
3662 type: this.modelName,
3663 id: ownerId
3664 };
3665 } else {
3666 newId = ownerId;
3667 }
3668
3669 this._schema.db[this._schema.toInternalCollectionName(model.modelName)].update(model.id, {
3670 [inverseFk]: newId
3671 });
3672 } else {
3673 let inverseCollection = this._schema.db[this._schema.toInternalCollectionName(model.modelName)];
3674
3675 let currentIdsForInverse = inverseCollection.find(model.id)[inverse.getForeignKey()] || [];
3676 let newIdsForInverse = Object.assign([], currentIdsForInverse);
3677 let newId, alreadyAssociatedWith;
3678
3679 if (inverse.isPolymorphic) {
3680 newId = {
3681 type: this.modelName,
3682 id: ownerId
3683 };
3684 alreadyAssociatedWith = newIdsForInverse.some(key => key.type == this.modelName && key.id == ownerId);
3685 } else {
3686 newId = ownerId;
3687 alreadyAssociatedWith = newIdsForInverse.includes(ownerId);
3688 }
3689
3690 if (!alreadyAssociatedWith) {
3691 newIdsForInverse.push(newId);
3692 }
3693
3694 inverseCollection.update(model.id, {
3695 [inverseFk]: newIdsForInverse
3696 });
3697 }
3698 }
3699 } // Used to update data directly, since #save and #update can retrigger saves,
3700 // which can cause cycles with associations.
3701
3702
3703 _updateInDb(attrs) {
3704 this.attrs = this._schema.db[this._schema.toInternalCollectionName(this.modelName)].update(this.attrs.id, attrs);
3705 }
3706 /*
3707 Super gnarly: after we save this tempAssociate, we we need to through
3708 all other tempAssociates for a reference to this same model, and
3709 update it. Otherwise those other references are stale, which could
3710 cause a bug when they are subsequently saved.
3711 This only works for belongsTo right now, should add hasMany logic to it.
3712 See issue #1613: https://github.com/samselikoff/ember-cli-mirage/pull/1613
3713 */
3714
3715
3716 _syncTempAssociations(tempAssociate) {
3717 Object.keys(this._tempAssociations).forEach(key => {
3718 if (this._tempAssociations[key] && this._tempAssociations[key].toString() === tempAssociate.toString()) {
3719 this._tempAssociations[key] = tempAssociate;
3720 }
3721 });
3722 }
3723 /**
3724 Simple string representation of the model and id.
3725 ```js
3726 let post = blogPosts.find(1);
3727 post.toString(); // "model:blogPost:1"
3728 ```
3729 @method toString
3730 @return {String}
3731 @public
3732 */
3733
3734
3735 toString() {
3736 let idLabel = this.id ? `(${this.id})` : "";
3737 return `model:${this.modelName}${idLabel}`;
3738 }
3739 /**
3740 Checks the equality of this model and the passed-in model
3741 *
3742 @method equals
3743 @return boolean
3744 @public
3745 @hide
3746 */
3747
3748
3749 equals(model) {
3750 return this.toString() === model.toString();
3751 }
3752
3753}
3754
3755Model.extend = extend;
3756
3757Model.findBelongsToAssociation = function (associationType) {
3758 return this.prototype.belongsToAssociations[associationType];
3759};
3760
3761/**
3762 Serializers are responsible for formatting your route handler's response.
3763
3764 The application serializer will apply to every response. To make specific customizations, define per-model serializers.
3765
3766 ```js
3767 import { Server, RestSerializer } from 'miragejs';
3768
3769 new Server({
3770 serializers: {
3771 application: RestSerializer,
3772 user: RestSerializer.extend({
3773 // user-specific customizations
3774 })
3775 }
3776 })
3777 ```
3778
3779 Any Model or Collection returned from a route handler will pass through the serializer layer. Highest priority will be given to a model-specific serializer, then the application serializer, then the default serializer.
3780
3781 Mirage ships with three named serializers:
3782
3783 - **JSONAPISerializer**, to simulate JSON:API compliant API servers:
3784
3785 ```js
3786 import { Server, JSONAPISerializer } from 'miragejs';
3787
3788 new Server({
3789 serializers: {
3790 application: JSONAPISerializer
3791 }
3792 })
3793 ```
3794
3795 - **ActiveModelSerializer**, to mock Rails APIs that use AMS-style responses:
3796
3797 ```js
3798 import { Server, ActiveModelSerializer } from 'miragejs';
3799
3800 new Server({
3801 serializers: {
3802 application: JSONAPISerializer
3803 }
3804 })
3805 ```
3806
3807 - **RestSerializer**, a good starting point for many generic REST APIs:
3808
3809 ```js
3810 import { Server, RestSerializer } from 'miragejs';
3811
3812 new Server({
3813 serializers: {
3814 application: JSONAPISerializer
3815 }
3816 })
3817 ```
3818
3819 Additionally, Mirage has a basic Serializer class which you can customize using the hooks documented below:
3820
3821 ```js
3822 import { Server, Serializer } from 'miragejs';
3823
3824 new Server({
3825 serializers: {
3826 application: JSONAPISerializer
3827 }
3828 })
3829 ```
3830
3831 When writing model-specific serializers, remember to extend from your application serializer so shared logic is used by your model-specific classes:
3832
3833 ```js
3834 import { Server, Serializer } from 'miragejs';
3835
3836 const ApplicationSerializer = Serializer.extend()
3837
3838 new Server({
3839 serializers: {
3840 application: ApplicationSerializer,
3841 blogPost: ApplicationSerializer.extend({
3842 include: ['comments']
3843 })
3844 }
3845 })
3846 ```
3847
3848 @class Serializer
3849 @constructor
3850 @public
3851*/
3852
3853class Serializer {
3854 constructor(registry, type, request = {}) {
3855 this.registry = registry;
3856 this.type = type;
3857 this.request = request;
3858 /**
3859 Use this property on a model serializer to whitelist attributes that will be used in your JSON payload.
3860 For example, if you had a `blog-post` model in your database that looked like
3861 ```
3862 {
3863 id: 1,
3864 title: 'Lorem ipsum',
3865 createdAt: '2014-01-01 10:00:00',
3866 updatedAt: '2014-01-03 11:42:12'
3867 }
3868 ```
3869 and you just wanted `id` and `title`, you could write
3870 ```js
3871 Serializer.extend({
3872 attrs: ['id', 'title']
3873 });
3874 ```
3875 and the payload would look like
3876 ```
3877 {
3878 id: 1,
3879 title: 'Lorem ipsum'
3880 }
3881 ```
3882 @property attrs
3883 @public
3884 */
3885
3886 this.attrs = this.attrs || undefined; // this is just here so I can add the doc comment. Better way?
3887
3888 /**
3889 Use this property on a model serializer to specify related models you'd like to include in your JSON payload. (These can be considered default server-side includes.)
3890 For example, if you had an `author` with many `blog-post`s and you wanted to sideload these, specify so in the `include` key:
3891 ```js
3892 new Server({
3893 models: {
3894 author: Model.extend({
3895 blogPosts: hasMany()
3896 })
3897 },
3898 serializers: {
3899 author: Serializer.extend({
3900 include: ['blogPosts']
3901 });
3902 }
3903 })
3904 ```
3905 Now a response to a request for an author would look like this:
3906 ```
3907 GET /authors/1
3908 {
3909 author: {
3910 id: 1,
3911 name: 'Link',
3912 blogPostIds: [1, 2]
3913 },
3914 blogPosts: [
3915 {id: 1, authorId: 1, title: 'Lorem'},
3916 {id: 2, authorId: 1, title: 'Ipsum'}
3917 ]
3918 }
3919 ```
3920 You can also define `include` as a function so it can be determined dynamically:
3921 ```js
3922 Serializer.extend({
3923 include: function(request) {
3924 if (request.queryParams.posts) {
3925 return ['blogPosts'];
3926 } else {
3927 return [];
3928 }
3929 }
3930 });
3931 ```
3932 **Query param includes for JSONAPISerializer**
3933 The JSONAPISerializer supports the use of `include` query parameter to return compound documents out of the box.
3934 For example, if your app makes the following request
3935 ```
3936 GET /api/authors?include=blogPosts
3937 ```
3938 the `JSONAPISerializer` will inspect the query params of the request, see that the blogPosts relationship is present, and then proceed as if this relationship was specified directly in the include: [] array on the serializer itself.
3939 Note that, in accordance with the spec, Mirage gives precedence to an ?include query param over a default include: [] array that you might have specified directly on the serializer. Default includes will still be in effect, however, if a request does not have an ?include query param.
3940 Also note that default includes specified with the `include: []` array can only take a single model; they cannot take dot-separated paths to nested relationships.
3941 If you'd like to set a default dot-separated (nested) include path for a resource, you have to do it at the route level by setting a default value for `request.queryParams`:
3942 ```js
3943 this.get('/users', function(schema, request) => {
3944 request.queryParams = request.queryParams || {};
3945 if (!request.queryParams.include) {
3946 request.queryParams.include = 'blog-posts.comments';
3947 }
3948 // rest of route handler logic
3949 });
3950 ```
3951 @property include
3952 @public
3953 */
3954
3955 this.include = this.include || []; // this is just here so I can add the doc comment. Better way?
3956
3957 /**
3958 Set whether your JSON response should have a root key in it.
3959 *Doesn't apply to JSONAPISerializer.*
3960 Defaults to true, so a request for an author looks like:
3961 ```
3962 GET /authors/1
3963 {
3964 author: {
3965 id: 1,
3966 name: 'Link'
3967 }
3968 }
3969 ```
3970 Setting `root` to false disables this:
3971 ```js
3972 Serializer.extend({
3973 root: false
3974 });
3975 ```
3976 Now the response looks like:
3977 ```
3978 GET /authors/1
3979 {
3980 id: 1,
3981 name: 'Link'
3982 }
3983 ```
3984 @property root
3985 @public
3986 */
3987
3988 this.root = this.root || undefined; // this is just here so I can add the doc comment. Better way?
3989
3990 /**
3991 Set whether related models should be embedded or sideloaded.
3992 *Doesn't apply to JSONAPISerializer.*
3993 By default this false, so relationships are sideloaded:
3994 ```
3995 GET /authors/1
3996 {
3997 author: {
3998 id: 1,
3999 name: 'Link',
4000 blogPostIds: [1, 2]
4001 },
4002 blogPosts: [
4003 { id: 1, authorId: 1, title: 'Lorem' },
4004 { id: 2, authorId: 1, title: 'Ipsum' }
4005 ]
4006 }
4007 ```
4008 Setting `embed` to true will embed related records:
4009 ```js
4010 Serializer.extend({
4011 embed: true
4012 });
4013 ```
4014 Now the response looks like:
4015 ```
4016 GET /authors/1
4017 {
4018 author: {
4019 id: 1,
4020 name: 'Link',
4021 blogPosts: [
4022 { id: 1, authorId: 1, title: 'Lorem' },
4023 { id: 2, authorId: 1, title: 'Ipsum' }
4024 ]
4025 }
4026 }
4027 ```
4028 */
4029
4030 this.embed = this.embed || undefined; // this is just here so I can add the doc comment. Better way?
4031
4032 /**
4033 Use this to define how your serializer handles serializing relationship keys. It can take one of three values:
4034 - `included`, which is the default, will serialize the ids of a relationship if that relationship is included (sideloaded) along with the model or collection in the response
4035 - `always` will always serialize the ids of all relationships for the model or collection in the response
4036 - `never` will never serialize the ids of relationships for the model or collection in the response
4037 _Note: this feature was added in 0.2.2._
4038 @property serializeIds
4039 @public
4040 */
4041
4042 this.serializeIds = this.serializeIds || undefined; // this is just here so I can add the doc comment. Better way?
4043 }
4044 /**
4045 Override this method to implement your own custom serialize function. *response* is whatever was returned from your route handler, and *request* is the Pretender request object.
4046 Returns a plain JavaScript object or array, which Mirage uses as the response data to your app's XHR request.
4047 You can also override this method, call super, and manipulate the data before Mirage responds with it. This is a great place to add metadata, or for one-off operations that don't fit neatly into any of Mirage's other abstractions:
4048 ```js
4049 serialize(object, request) {
4050 // This is how to call super, as Mirage borrows [Backbone's implementation of extend](http://backbonejs.org/#Model-extend)
4051 let json = Serializer.prototype.serialize.apply(this, arguments);
4052 // Add metadata, sort parts of the response, etc.
4053 return json;
4054 }
4055 ```
4056 @param primaryResource
4057 @param request
4058 @return { Object } the json response
4059 */
4060
4061
4062 serialize(primaryResource
4063 /* , request */
4064 ) {
4065 return this.buildPayload(primaryResource);
4066 }
4067 /**
4068 This method is used by the POST and PUT shorthands. These shorthands expect a valid JSON:API document as part of the request, so that they know how to create or update the appropriate resouce. The *normalize* method allows you to transform your request body into a JSON:API document, which lets you take advantage of the shorthands when you otherwise may not be able to.
4069 Note that this method is a noop if you're using JSON:API already, since request payloads sent along with POST and PUT requests will already be in the correct format.
4070 Take a look at the included `ActiveModelSerializer`'s normalize method for an example.
4071 @method normalize
4072 @param json
4073 @public
4074 */
4075
4076
4077 normalize(json) {
4078 return json;
4079 }
4080
4081 buildPayload(primaryResource, toInclude, didSerialize, json) {
4082 if (!primaryResource && isEmpty(toInclude)) {
4083 return json;
4084 } else if (primaryResource) {
4085 let [resourceHash, newIncludes] = this.getHashForPrimaryResource(primaryResource);
4086 let newDidSerialize = this.isCollection(primaryResource) ? primaryResource.models : [primaryResource];
4087 return this.buildPayload(undefined, newIncludes, newDidSerialize, resourceHash);
4088 } else {
4089 let nextIncludedResource = toInclude.shift();
4090 let [resourceHash, newIncludes] = this.getHashForIncludedResource(nextIncludedResource);
4091 let newToInclude = newIncludes.filter(resource => {
4092 return !didSerialize.map(m => m.toString()).includes(resource.toString());
4093 }).concat(toInclude);
4094 let newDidSerialize = (this.isCollection(nextIncludedResource) ? nextIncludedResource.models : [nextIncludedResource]).concat(didSerialize);
4095 let newJson = this.mergePayloads(json, resourceHash);
4096 return this.buildPayload(undefined, newToInclude, newDidSerialize, newJson);
4097 }
4098 }
4099
4100 getHashForPrimaryResource(resource) {
4101 let [hash, addToIncludes] = this.getHashForResource(resource);
4102 let hashWithRoot;
4103
4104 if (this.root) {
4105 assert(!(resource instanceof PolymorphicCollection), `The base Serializer class cannot serialize a top-level PolymorphicCollection when root is true, since PolymorphicCollections have no type.`);
4106 let serializer = this.serializerFor(resource.modelName);
4107 let rootKey = serializer.keyForResource(resource);
4108 hashWithRoot = {
4109 [rootKey]: hash
4110 };
4111 } else {
4112 hashWithRoot = hash;
4113 }
4114
4115 return [hashWithRoot, addToIncludes];
4116 }
4117
4118 getHashForIncludedResource(resource) {
4119 let hashWithRoot, addToIncludes;
4120
4121 if (resource instanceof PolymorphicCollection) {
4122 hashWithRoot = {};
4123 addToIncludes = resource.models;
4124 } else {
4125 let serializer = this.serializerFor(resource.modelName);
4126 let [hash, newModels] = serializer.getHashForResource(resource); // Included resources always have a root, and are always pushed to an array.
4127
4128 let rootKey = serializer.keyForRelationship(resource.modelName);
4129 hashWithRoot = Array.isArray(hash) ? {
4130 [rootKey]: hash
4131 } : {
4132 [rootKey]: [hash]
4133 };
4134 addToIncludes = newModels;
4135 }
4136
4137 return [hashWithRoot, addToIncludes];
4138 }
4139
4140 getHashForResource(resource, removeForeignKeys = false, didSerialize = {}, lookupSerializer = false) {
4141 let hash, serializer;
4142
4143 if (!lookupSerializer) {
4144 serializer = this; // this is used for embedded responses
4145 } // PolymorphicCollection lacks a modelName, but is dealt with in the map
4146 // by looking up the serializer on a per-model basis
4147
4148
4149 if (lookupSerializer && resource.modelName) {
4150 serializer = this.serializerFor(resource.modelName);
4151 }
4152
4153 if (this.isModel(resource)) {
4154 hash = serializer._hashForModel(resource, removeForeignKeys, didSerialize);
4155 } else {
4156 hash = resource.models.map(m => {
4157 let modelSerializer = serializer;
4158
4159 if (!modelSerializer) {
4160 // Can't get here if lookupSerializer is false, so look it up
4161 modelSerializer = this.serializerFor(m.modelName);
4162 }
4163
4164 return modelSerializer._hashForModel(m, removeForeignKeys, didSerialize);
4165 });
4166 }
4167
4168 if (this.embed) {
4169 return [hash, []];
4170 } else {
4171 let addToIncludes = uniqBy(compact(flatten(serializer.getKeysForIncluded().map(key => {
4172 if (this.isCollection(resource)) {
4173 return resource.models.map(m => m[key]);
4174 } else {
4175 return resource[key];
4176 }
4177 }))), m => m.toString());
4178 return [hash, addToIncludes];
4179 }
4180 }
4181 /*
4182 Merges new resource hash into json. If json already has root key,
4183 pushes value of resourceHash onto that key.
4184 For example,
4185 json = {
4186 post: { id: 1, title: 'Lorem Ipsum', comment_ids: [1, 3] },
4187 comments: [
4188 { id: 1, text: 'foo' }
4189 ]
4190 };
4191 resourceHash = {
4192 comments: [
4193 { id: 2, text: 'bar' }
4194 ]
4195 };
4196 would yield
4197 {
4198 post: { id: 1, title: 'Lorem Ipsum', comment_ids: [1, 3] },
4199 comments: [
4200 { id: 1, text: 'foo' },
4201 { id: 2, text: 'bar' }
4202 ]
4203 };
4204 */
4205
4206
4207 mergePayloads(json, resourceHash) {
4208 let newJson;
4209 let [resourceHashKey] = Object.keys(resourceHash);
4210
4211 if (json[resourceHashKey]) {
4212 newJson = json;
4213 newJson[resourceHashKey] = json[resourceHashKey].concat(resourceHash[resourceHashKey]);
4214 } else {
4215 newJson = Object.assign(json, resourceHash);
4216 }
4217
4218 return newJson;
4219 }
4220
4221 keyForResource(resource) {
4222 let {
4223 modelName
4224 } = resource;
4225 return this.isModel(resource) ? this.keyForModel(modelName) : this.keyForCollection(modelName);
4226 }
4227 /**
4228 Used to define a custom key when serializing a primary model of modelName *modelName*. For example, the default Serializer will return something like the following:
4229 ```
4230 GET /blogPosts/1
4231 {
4232 blogPost: {
4233 id: 1,
4234 title: 'Lorem ipsum'
4235 }
4236 }
4237 ```
4238 If your API uses hyphenated keys, you could overwrite `keyForModel`:
4239 ```js
4240 // serializers/application.js
4241 export default Serializer.extend({
4242 keyForModel(modelName) {
4243 return hyphenate(modelName);
4244 }
4245 });
4246 ```
4247 Now the response will look like
4248 ```
4249 {
4250 'blog-post': {
4251 id: 1,
4252 title: 'Lorem ipsum'
4253 }
4254 }
4255 ```
4256 @method keyForModel
4257 @param modelName
4258 @public
4259 */
4260
4261
4262 keyForModel(modelName) {
4263 return camelize(modelName);
4264 }
4265 /**
4266 Used to customize the key when serializing a primary collection. By default this pluralizes the return value of `keyForModel`.
4267 For example, by default the following request may look like:
4268 ```
4269 GET /blogPosts
4270 {
4271 blogPosts: [
4272 {
4273 id: 1,
4274 title: 'Lorem ipsum'
4275 },
4276 ...
4277 ]
4278 }
4279 ```
4280 If your API hyphenates keys, you could overwrite `keyForCollection`:
4281 ```js
4282 // serializers/application.js
4283 export default Serializer.extend({
4284 keyForCollection(modelName) {
4285 return this._container.inflector.pluralize(dasherize(modelName));
4286 }
4287 });
4288 ```
4289 Now the response would look like:
4290 ```
4291 {
4292 'blog-posts': [
4293 {
4294 id: 1,
4295 title: 'Lorem ipsum'
4296 },
4297 ...
4298 ]
4299 }
4300 ```
4301 @method keyForCollection
4302 @param modelName
4303 @public
4304 */
4305
4306
4307 keyForCollection(modelName) {
4308 return this._container.inflector.pluralize(this.keyForModel(modelName));
4309 }
4310
4311 _hashForModel(model, removeForeignKeys, didSerialize = {}) {
4312 let attrs = this._attrsForModel(model);
4313
4314 if (removeForeignKeys) {
4315 model.fks.forEach(fk => {
4316 delete attrs[fk];
4317 });
4318 }
4319
4320 if (this.embed) {
4321 let newDidSerialize = Object.assign({}, didSerialize);
4322 newDidSerialize[model.modelName] = newDidSerialize[model.modelName] || {};
4323 newDidSerialize[model.modelName][model.id] = true;
4324 this.getKeysForIncluded().forEach(key => {
4325 let associatedResource = model[key];
4326
4327 if (associatedResource && !get(newDidSerialize, `${associatedResource.modelName}.${associatedResource.id}`)) {
4328 let [associatedResourceHash] = this.getHashForResource(associatedResource, true, newDidSerialize, true);
4329 let formattedKey = this.keyForEmbeddedRelationship(key);
4330 attrs[formattedKey] = associatedResourceHash;
4331
4332 if (this.isModel(associatedResource)) {
4333 let fk = `${camelize(key)}Id`;
4334 delete attrs[fk];
4335 }
4336 }
4337 });
4338 return attrs;
4339 } else {
4340 return this._maybeAddAssociationIds(model, attrs);
4341 }
4342 }
4343 /**
4344 @method _attrsForModel
4345 @param model
4346 @private
4347 @hide
4348 */
4349
4350
4351 _attrsForModel(model) {
4352 let attrs = {};
4353
4354 if (this.attrs) {
4355 attrs = this.attrs.reduce((memo, attr) => {
4356 memo[attr] = model[attr];
4357 return memo;
4358 }, {});
4359 } else {
4360 attrs = Object.assign(attrs, model.attrs);
4361 } // Remove fks
4362
4363
4364 model.fks.forEach(key => delete attrs[key]);
4365 return this._formatAttributeKeys(attrs);
4366 }
4367 /**
4368 @method _maybeAddAssociationIds
4369 @param model
4370 @param attrs
4371 @private
4372 @hide
4373 */
4374
4375
4376 _maybeAddAssociationIds(model, attrs) {
4377 let newHash = Object.assign({}, attrs);
4378
4379 if (this.serializeIds === "always") {
4380 model.associationKeys.forEach(key => {
4381 let resource = model[key];
4382 let association = model.associationFor(key);
4383
4384 if (this.isCollection(resource)) {
4385 let formattedKey = this.keyForRelationshipIds(key);
4386 newHash[formattedKey] = model[`${this._container.inflector.singularize(key)}Ids`];
4387 } else if (this.isModel(resource) && association.isPolymorphic) {
4388 let formattedTypeKey = this.keyForPolymorphicForeignKeyType(key);
4389 let formattedIdKey = this.keyForPolymorphicForeignKeyId(key);
4390 newHash[formattedTypeKey] = model[`${key}Id`].type;
4391 newHash[formattedIdKey] = model[`${key}Id`].id;
4392 } else if (resource) {
4393 let formattedKey = this.keyForForeignKey(key);
4394 newHash[formattedKey] = model[`${key}Id`];
4395 }
4396 });
4397 } else if (this.serializeIds === "included") {
4398 this.getKeysForIncluded().forEach(key => {
4399 let resource = model[key];
4400 let association = model.associationFor(key);
4401
4402 if (this.isCollection(resource)) {
4403 let formattedKey = this.keyForRelationshipIds(key);
4404 newHash[formattedKey] = model[`${this._container.inflector.singularize(key)}Ids`];
4405 } else if (this.isModel(resource) && association.isPolymorphic) {
4406 let formattedTypeKey = this.keyForPolymorphicForeignKeyType(key);
4407 let formattedIdKey = this.keyForPolymorphicForeignKeyId(key);
4408 newHash[formattedTypeKey] = model[`${key}Id`].type;
4409 newHash[formattedIdKey] = model[`${key}Id`].id;
4410 } else if (this.isModel(resource)) {
4411 let formattedKey = this.keyForForeignKey(key);
4412 newHash[formattedKey] = model[`${key}Id`];
4413 }
4414 });
4415 }
4416
4417 return newHash;
4418 }
4419 /**
4420 Used to customize how a model's attribute is formatted in your JSON payload.
4421 By default, model attributes are camelCase:
4422 ```
4423 GET /authors/1
4424 {
4425 author: {
4426 firstName: 'Link',
4427 lastName: 'The WoodElf'
4428 }
4429 }
4430 ```
4431 If your API expects snake case, you could write the following:
4432 ```js
4433 // serializers/application.js
4434 export default Serializer.extend({
4435 keyForAttribute(attr) {
4436 return underscore(attr);
4437 }
4438 });
4439 ```
4440 Now the response would look like:
4441 ```
4442 {
4443 author: {
4444 first_name: 'Link',
4445 last_name: 'The WoodElf'
4446 }
4447 }
4448 ```
4449 @method keyForAttribute
4450 @param attr
4451 @public
4452 */
4453
4454
4455 keyForAttribute(attr) {
4456 return attr;
4457 }
4458 /**
4459 Use this hook to format the key for collections related to this model. *modelName* is the named parameter for the relationship.
4460 For example, if you're serializing an `author` that
4461 sideloads many `blogPosts`, the default response will look like:
4462 ```
4463 {
4464 author: {...},
4465 blogPosts: [...]
4466 }
4467 ```
4468 Overwrite `keyForRelationship` to format this key:
4469 ```js
4470 // serializers/application.js
4471 export default Serializer.extend({
4472 keyForRelationship(modelName) {
4473 return underscore(modelName);
4474 }
4475 });
4476 ```
4477 Now the response will look like this:
4478 ```
4479 {
4480 author: {...},
4481 blog_posts: [...]
4482 }
4483 ```
4484 @method keyForRelationship
4485 @param modelName
4486 @public
4487 */
4488
4489
4490 keyForRelationship(modelName) {
4491 return camelize(this._container.inflector.pluralize(modelName));
4492 }
4493 /**
4494 Like `keyForRelationship`, but for embedded relationships.
4495 @method keyForEmbeddedRelationship
4496 @param attributeName
4497 @public
4498 */
4499
4500
4501 keyForEmbeddedRelationship(attributeName) {
4502 return camelize(attributeName);
4503 }
4504 /**
4505 Use this hook to format the key for the IDS of a `hasMany` relationship
4506 in this model's JSON representation.
4507 For example, if you're serializing an `author` that
4508 sideloads many `blogPosts`, by default your `author` JSON would include a `blogPostIds` key:
4509 ```
4510 {
4511 author: {
4512 id: 1,
4513 blogPostIds: [1, 2, 3]
4514 },
4515 blogPosts: [...]
4516 }
4517 ```
4518 Overwrite `keyForRelationshipIds` to format this key:
4519 ```js
4520 // serializers/application.js
4521 export default Serializer.extend({
4522 keyForRelationshipIds(relationship) {
4523 return underscore(relationship) + '_ids';
4524 }
4525 });
4526 ```
4527 Now the response will look like:
4528 ```
4529 {
4530 author: {
4531 id: 1,
4532 blog_post_ids: [1, 2, 3]
4533 },
4534 blogPosts: [...]
4535 }
4536 ```
4537 @method keyForRelationshipIds
4538 @param modelName
4539 @public
4540 */
4541
4542
4543 keyForRelationshipIds(relationshipName) {
4544 return `${this._container.inflector.singularize(camelize(relationshipName))}Ids`;
4545 }
4546 /**
4547 Like `keyForRelationshipIds`, but for `belongsTo` relationships.
4548 For example, if you're serializing a `blogPost` that sideloads one `author`,
4549 your `blogPost` JSON would include a `authorId` key:
4550 ```
4551 {
4552 blogPost: {
4553 id: 1,
4554 authorId: 1
4555 },
4556 author: ...
4557 }
4558 ```
4559 Overwrite `keyForForeignKey` to format this key:
4560 ```js
4561 // serializers/application.js
4562 export default Serializer.extend({
4563 keyForForeignKey(relationshipName) {
4564 return underscore(relationshipName) + '_id';
4565 }
4566 });
4567 ```
4568 Now the response will look like:
4569 ```js
4570 {
4571 blogPost: {
4572 id: 1,
4573 author_id: 1
4574 },
4575 author: ...
4576 }
4577 ```
4578 @method keyForForeignKey
4579 @param relationshipName
4580 @public
4581 */
4582
4583
4584 keyForForeignKey(relationshipName) {
4585 return `${camelize(relationshipName)}Id`;
4586 }
4587 /**
4588 Polymorphic relationships are represented with type-id pairs.
4589 Given the following model
4590 ```js
4591 Model.extend({
4592 commentable: belongsTo({ polymorphic: true })
4593 });
4594 ```
4595 the default Serializer would produce
4596 ```js
4597 {
4598 comment: {
4599 id: 1,
4600 commentableType: 'post',
4601 commentableId: '1'
4602 }
4603 }
4604 ```
4605 This hook controls how the `id` field (`commentableId` in the above example)
4606 is serialized. By default it camelizes the relationship and adds `Id` as a suffix.
4607 @method keyForPolymorphicForeignKeyId
4608 @param {String} relationshipName
4609 @return {String}
4610 @public
4611 */
4612
4613
4614 keyForPolymorphicForeignKeyId(relationshipName) {
4615 return `${camelize(relationshipName)}Id`;
4616 }
4617 /**
4618 Polymorphic relationships are represented with type-id pairs.
4619 Given the following model
4620 ```js
4621 Model.extend({
4622 commentable: belongsTo({ polymorphic: true })
4623 });
4624 ```
4625 the default Serializer would produce
4626 ```js
4627 {
4628 comment: {
4629 id: 1,
4630 commentableType: 'post',
4631 commentableId: '1'
4632 }
4633 }
4634 ```
4635 This hook controls how the `type` field (`commentableType` in the above example)
4636 is serialized. By default it camelizes the relationship and adds `Type` as a suffix.
4637 @method keyForPolymorphicForeignKeyType
4638 @param {String} relationshipName
4639 @return {String}
4640 @public
4641 */
4642
4643
4644 keyForPolymorphicForeignKeyType(relationshipName) {
4645 return `${camelize(relationshipName)}Type`;
4646 }
4647 /**
4648 @method isModel
4649 @param object
4650 @return {Boolean}
4651 @public
4652 @hide
4653 */
4654
4655
4656 isModel(object) {
4657 return object instanceof Model;
4658 }
4659 /**
4660 @method isCollection
4661 @param object
4662 @return {Boolean}
4663 @public
4664 @hide
4665 */
4666
4667
4668 isCollection(object) {
4669 return object instanceof Collection || object instanceof PolymorphicCollection;
4670 }
4671 /**
4672 @method isModelOrCollection
4673 @param object
4674 @return {Boolean}
4675 @public
4676 @hide
4677 */
4678
4679
4680 isModelOrCollection(object) {
4681 return this.isModel(object) || this.isCollection(object);
4682 }
4683 /**
4684 @method serializerFor
4685 @param type
4686 @public
4687 @hide
4688 */
4689
4690
4691 serializerFor(type) {
4692 return this.registry.serializerFor(type);
4693 }
4694
4695 getKeysForIncluded() {
4696 return isFunction(this.include) ? this.include(this.request) : this.include;
4697 }
4698 /**
4699 Foo bar.
4700 @property schema
4701 @public
4702 @hide
4703 */
4704
4705
4706 get schema() {
4707 return this.registry.schema;
4708 }
4709 /**
4710 @method _formatAttributeKeys
4711 @param attrs
4712 @private
4713 @hide
4714 */
4715
4716
4717 _formatAttributeKeys(attrs) {
4718 let formattedAttrs = {};
4719
4720 for (let key in attrs) {
4721 let formattedKey = this.keyForAttribute(key);
4722 formattedAttrs[formattedKey] = attrs[key];
4723 }
4724
4725 return formattedAttrs;
4726 }
4727
4728 getCoalescedIds()
4729 /* request */
4730 {}
4731
4732} // Defaults
4733
4734
4735Serializer.prototype.include = [];
4736Serializer.prototype.root = true;
4737Serializer.prototype.embed = false;
4738Serializer.prototype.serializeIds = "included"; // can be 'included', 'always', or 'never'
4739
4740Serializer.extend = extend;
4741
4742/**
4743 The JSONAPISerializer. Subclass of Serializer.
4744
4745 @class JSONAPISerializer
4746 @constructor
4747 @public
4748 */
4749
4750class JSONAPISerializer extends Serializer {
4751 constructor() {
4752 super(...arguments);
4753 /**
4754 By default, JSON:API's linkage data is only added for relationships that are being included in the current request.
4755 That means given an `author` model with a `posts` relationship, a GET request to /authors/1 would return a JSON:API document with an empty `relationships` hash:
4756 ```js
4757 {
4758 data: {
4759 type: 'authors',
4760 id: '1',
4761 attributes: { ... }
4762 }
4763 }
4764 ```
4765 but a request to GET /authors/1?include=posts would have linkage data added (in addition to the included resources):
4766 ```js
4767 {
4768 data: {
4769 type: 'authors',
4770 id: '1',
4771 attributes: { ... },
4772 relationships: {
4773 data: [
4774 { type: 'posts', id: '1' },
4775 { type: 'posts', id: '2' },
4776 { type: 'posts', id: '3' }
4777 ]
4778 }
4779 },
4780 included: [ ... ]
4781 }
4782 ```
4783 To add the linkage data for all relationships, you could set `alwaysIncludeLinkageData` to `true`:
4784 ```js
4785 JSONAPISerializer.extend({
4786 alwaysIncludeLinkageData: true
4787 });
4788 ```
4789 Then, a GET to /authors/1 would respond with
4790 ```js
4791 {
4792 data: {
4793 type: 'authors',
4794 id: '1',
4795 attributes: { ... },
4796 relationships: {
4797 posts: {
4798 data: [
4799 { type: 'posts', id: '1' },
4800 { type: 'posts', id: '2' },
4801 { type: 'posts', id: '3' }
4802 ]
4803 }
4804 }
4805 }
4806 }
4807 ```
4808 even though the related `posts` are not included in the same document.
4809 You can also use the `links` method (on the Serializer base class) to add relationship links (which will always be added regardless of the relationship is being included document), or you could use `shouldIncludeLinkageData` for more granular control.
4810 For more background on the behavior of this API, see [this blog post](http://www.ember-cli-mirage.com/blog/changing-mirages-default-linkage-data-behavior-1475).
4811 @property alwaysIncludeLinkageData
4812 @type {Boolean}
4813 @public
4814 */
4815
4816 this.alwaysIncludeLinkageData = this.alwaysIncludeLinkageData || undefined; // this is just here so I can add the doc comment. Better way?
4817 } // Don't think this is used?
4818
4819
4820 keyForModel(modelName) {
4821 return dasherize(modelName);
4822 } // Don't think this is used?
4823
4824
4825 keyForCollection(modelName) {
4826 return dasherize(modelName);
4827 }
4828 /**
4829 Used to customize the key for an attribute. By default, compound attribute names are dasherized.
4830 For example, the JSON:API document for a `post` model with a `commentCount` attribute would be:
4831 ```js
4832 {
4833 data: {
4834 id: 1,
4835 type: 'posts',
4836 attributes: {
4837 'comment-count': 28
4838 }
4839 }
4840 }
4841 ```
4842 @method keyForAttribute
4843 @param {String} attr
4844 @return {String}
4845 @public
4846 */
4847
4848
4849 keyForAttribute(attr) {
4850 return dasherize(attr);
4851 }
4852 /**
4853 Used to customize the key for a relationships. By default, compound relationship names are dasherized.
4854 For example, the JSON:API document for an `author` model with a `blogPosts` relationship would be:
4855 ```js
4856 {
4857 data: {
4858 id: 1,
4859 type: 'author',
4860 attributes: {
4861 ...
4862 },
4863 relationships: {
4864 'blog-posts': {
4865 ...
4866 }
4867 }
4868 }
4869 }
4870 ```
4871 @method keyForRelationship
4872 @param {String} key
4873 @return {String}
4874 @public
4875 */
4876
4877
4878 keyForRelationship(key) {
4879 return dasherize(key);
4880 }
4881 /**
4882 Use this hook to add top-level `links` data to JSON:API resource objects. The argument is the model being serialized.
4883 ```js
4884 // serializers/author.js
4885 import { JSONAPISerializer } from 'miragejs';
4886 export default JSONAPISerializer.extend({
4887 links(author) {
4888 return {
4889 'posts': {
4890 related: `/api/authors/${author.id}/posts`
4891 }
4892 };
4893 }
4894 });
4895 ```
4896 @method links
4897 @param model
4898 */
4899
4900
4901 links() {}
4902
4903 getHashForPrimaryResource(resource) {
4904 this._createRequestedIncludesGraph(resource);
4905
4906 let resourceHash = this.getHashForResource(resource);
4907 let hashWithRoot = {
4908 data: resourceHash
4909 };
4910 let addToIncludes = this.getAddToIncludesForResource(resource);
4911 return [hashWithRoot, addToIncludes];
4912 }
4913
4914 getHashForIncludedResource(resource) {
4915 let serializer = this.serializerFor(resource.modelName);
4916 let hash = serializer.getHashForResource(resource);
4917 let hashWithRoot = {
4918 included: this.isModel(resource) ? [hash] : hash
4919 };
4920 let addToIncludes = [];
4921
4922 if (!this.hasQueryParamIncludes()) {
4923 addToIncludes = this.getAddToIncludesForResource(resource);
4924 }
4925
4926 return [hashWithRoot, addToIncludes];
4927 }
4928
4929 getHashForResource(resource) {
4930 let hash;
4931
4932 if (this.isModel(resource)) {
4933 hash = this.getResourceObjectForModel(resource);
4934 } else {
4935 hash = resource.models.map(m => this.getResourceObjectForModel(m));
4936 }
4937
4938 return hash;
4939 }
4940 /*
4941 Returns a flat unique list of resources that need to be added to includes
4942 */
4943
4944
4945 getAddToIncludesForResource(resource) {
4946 let relationshipPaths;
4947
4948 if (this.hasQueryParamIncludes()) {
4949 relationshipPaths = this.request.queryParams.include.split(",");
4950 } else {
4951 let serializer = this.serializerFor(resource.modelName);
4952 relationshipPaths = serializer.getKeysForIncluded();
4953 }
4954
4955 return this.getAddToIncludesForResourceAndPaths(resource, relationshipPaths);
4956 }
4957
4958 getAddToIncludesForResourceAndPaths(resource, relationshipPaths) {
4959 let includes = [];
4960 relationshipPaths.forEach(path => {
4961 let relationshipNames = path.split(".");
4962 let newIncludes = this.getIncludesForResourceAndPath(resource, ...relationshipNames);
4963 includes.push(newIncludes);
4964 });
4965 return uniqBy(compact(flatten(includes)), m => m.toString());
4966 }
4967
4968 getIncludesForResourceAndPath(resource, ...names) {
4969 let nameForCurrentResource = camelize(names.shift());
4970 let includes = [];
4971 let modelsToAdd = [];
4972
4973 if (this.isModel(resource)) {
4974 let relationship = resource[nameForCurrentResource];
4975
4976 if (this.isModel(relationship)) {
4977 modelsToAdd = [relationship];
4978 } else if (this.isCollection(relationship)) {
4979 modelsToAdd = relationship.models;
4980 }
4981 } else {
4982 resource.models.forEach(model => {
4983 let relationship = model[nameForCurrentResource];
4984
4985 if (this.isModel(relationship)) {
4986 modelsToAdd.push(relationship);
4987 } else if (this.isCollection(relationship)) {
4988 modelsToAdd = modelsToAdd.concat(relationship.models);
4989 }
4990 });
4991 }
4992
4993 includes = includes.concat(modelsToAdd);
4994
4995 if (names.length) {
4996 modelsToAdd.forEach(model => {
4997 includes = includes.concat(this.getIncludesForResourceAndPath(model, ...names));
4998 });
4999 }
5000
5001 return includes;
5002 }
5003
5004 getResourceObjectForModel(model) {
5005 let attrs = this._attrsForModel(model, true);
5006
5007 delete attrs.id;
5008 let hash = {
5009 type: this.typeKeyForModel(model),
5010 id: model.id,
5011 attributes: attrs
5012 };
5013 return this._maybeAddRelationshipsToResourceObjectForModel(hash, model);
5014 }
5015
5016 _maybeAddRelationshipsToResourceObjectForModel(hash, model) {
5017 const relationships = {};
5018 model.associationKeys.forEach(key => {
5019 let relationship = model[key];
5020 let relationshipKey = this.keyForRelationship(key);
5021 let relationshipHash = {};
5022
5023 if (this.hasLinksForRelationship(model, key)) {
5024 let serializer = this.serializerFor(model.modelName);
5025 let links = serializer.links(model);
5026 relationshipHash.links = links[key];
5027 }
5028
5029 if (this.alwaysIncludeLinkageData || this.shouldIncludeLinkageData(key, model) || this._relationshipIsIncludedForModel(key, model)) {
5030 let data = null;
5031
5032 if (this.isModel(relationship)) {
5033 data = {
5034 type: this.typeKeyForModel(relationship),
5035 id: relationship.id
5036 };
5037 } else if (this.isCollection(relationship)) {
5038 data = relationship.models.map(model => {
5039 return {
5040 type: this.typeKeyForModel(model),
5041 id: model.id
5042 };
5043 });
5044 }
5045
5046 relationshipHash.data = data;
5047 }
5048
5049 if (!isEmpty(relationshipHash)) {
5050 relationships[relationshipKey] = relationshipHash;
5051 }
5052 });
5053
5054 if (!isEmpty(relationships)) {
5055 hash.relationships = relationships;
5056 }
5057
5058 return hash;
5059 }
5060
5061 hasLinksForRelationship(model, relationshipKey) {
5062 let serializer = this.serializerFor(model.modelName);
5063 let links = serializer.links && serializer.links(model);
5064 return links && links[relationshipKey] != null;
5065 }
5066 /*
5067 This code (and a lot of this serializer) need to be re-worked according to
5068 the graph logic...
5069 */
5070
5071
5072 _relationshipIsIncludedForModel(relationshipKey, model) {
5073 if (this.hasQueryParamIncludes()) {
5074 let graph = this.request._includesGraph;
5075
5076 let graphKey = this._graphKeyForModel(model); // Find the resource in the graph
5077
5078
5079 let graphResource; // Check primary data
5080
5081 if (graph.data[graphKey]) {
5082 graphResource = graph.data[graphKey]; // Check includes
5083 } else if (graph.included[this._container.inflector.pluralize(model.modelName)]) {
5084 graphResource = graph.included[this._container.inflector.pluralize(model.modelName)][graphKey];
5085 } // If the model's in the graph, check if relationshipKey should be included
5086
5087
5088 return graphResource && graphResource.relationships && Object.prototype.hasOwnProperty.call(graphResource.relationships, dasherize(relationshipKey));
5089 } else {
5090 let relationshipPaths = this.getKeysForIncluded();
5091 return relationshipPaths.includes(relationshipKey);
5092 }
5093 }
5094 /*
5095 This is needed for _relationshipIsIncludedForModel - see the note there for
5096 more background.
5097 If/when we can refactor this serializer, the logic in this method would
5098 probably be the basis for the new overall json/graph creation.
5099 */
5100
5101
5102 _createRequestedIncludesGraph(primaryResource, secondaryResource = null) {
5103 let graph = {
5104 data: {}
5105 };
5106
5107 if (this.isModel(primaryResource)) {
5108 let primaryResourceKey = this._graphKeyForModel(primaryResource);
5109
5110 graph.data[primaryResourceKey] = {};
5111
5112 this._addPrimaryModelToRequestedIncludesGraph(graph, primaryResource);
5113 } else if (this.isCollection(primaryResource)) {
5114 primaryResource.models.forEach(model => {
5115 let primaryResourceKey = this._graphKeyForModel(model);
5116
5117 graph.data[primaryResourceKey] = {};
5118
5119 this._addPrimaryModelToRequestedIncludesGraph(graph, model);
5120 });
5121 } // Hack :/ Need to think of a better palce to put this if
5122 // refactoring json:api serializer.
5123
5124
5125 this.request._includesGraph = graph;
5126 }
5127
5128 _addPrimaryModelToRequestedIncludesGraph(graph, model) {
5129 if (this.hasQueryParamIncludes()) {
5130 let graphKey = this._graphKeyForModel(model);
5131
5132 let queryParamIncludes = this.getQueryParamIncludes();
5133 queryParamIncludes.split(",").forEach(includesPath => {
5134 // includesPath is post.comments, for example
5135 graph.data[graphKey].relationships = graph.data[graphKey].relationships || {};
5136 let relationshipKeys = includesPath.split(".").map(dasherize);
5137 let relationshipKey = relationshipKeys[0];
5138 let graphRelationshipKey = relationshipKey;
5139 let normalizedRelationshipKey = camelize(relationshipKey);
5140 let hasAssociation = model.associationKeys.has(normalizedRelationshipKey);
5141 assert(hasAssociation, `You tried to include "${relationshipKey}" with ${model} but no association named "${normalizedRelationshipKey}" is defined on the model.`);
5142 let relationship = model[normalizedRelationshipKey];
5143 let relationshipData;
5144
5145 if (this.isModel(relationship)) {
5146 relationshipData = this._graphKeyForModel(relationship);
5147 } else if (this.isCollection(relationship)) {
5148 relationshipData = relationship.models.map(this._graphKeyForModel);
5149 } else {
5150 relationshipData = null;
5151 }
5152
5153 graph.data[graphKey].relationships[graphRelationshipKey] = relationshipData;
5154
5155 if (relationship) {
5156 this._addResourceToRequestedIncludesGraph(graph, relationship, relationshipKeys.slice(1));
5157 }
5158 });
5159 }
5160 }
5161
5162 _addResourceToRequestedIncludesGraph(graph, resource, relationshipNames) {
5163 graph.included = graph.included || {};
5164 let models = this.isCollection(resource) ? resource.models : [resource];
5165 models.forEach(model => {
5166 let collectionName = this._container.inflector.pluralize(model.modelName);
5167
5168 graph.included[collectionName] = graph.included[collectionName] || {};
5169
5170 this._addModelToRequestedIncludesGraph(graph, model, relationshipNames);
5171 });
5172 }
5173
5174 _addModelToRequestedIncludesGraph(graph, model, relationshipNames) {
5175 let collectionName = this._container.inflector.pluralize(model.modelName);
5176
5177 let resourceKey = this._graphKeyForModel(model);
5178
5179 graph.included[collectionName][resourceKey] = graph.included[collectionName][resourceKey] || {};
5180
5181 if (relationshipNames.length) {
5182 this._addResourceRelationshipsToRequestedIncludesGraph(graph, collectionName, resourceKey, model, relationshipNames);
5183 }
5184 }
5185 /*
5186 Lot of the same logic here from _addPrimaryModelToRequestedIncludesGraph, could refactor & share
5187 */
5188
5189
5190 _addResourceRelationshipsToRequestedIncludesGraph(graph, collectionName, resourceKey, model, relationshipNames) {
5191 graph.included[collectionName][resourceKey].relationships = graph.included[collectionName][resourceKey].relationships || {};
5192 let relationshipName = relationshipNames[0];
5193 let relationship = model[camelize(relationshipName)];
5194 let relationshipData;
5195
5196 if (this.isModel(relationship)) {
5197 relationshipData = this._graphKeyForModel(relationship);
5198 } else if (this.isCollection(relationship)) {
5199 relationshipData = relationship.models.map(this._graphKeyForModel);
5200 }
5201
5202 graph.included[collectionName][resourceKey].relationships[relationshipName] = relationshipData;
5203
5204 if (relationship) {
5205 this._addResourceToRequestedIncludesGraph(graph, relationship, relationshipNames.slice(1));
5206 }
5207 }
5208
5209 _graphKeyForModel(model) {
5210 return `${model.modelName}:${model.id}`;
5211 }
5212
5213 getQueryParamIncludes() {
5214 return get(this, "request.queryParams.include");
5215 }
5216
5217 hasQueryParamIncludes() {
5218 return !!this.getQueryParamIncludes();
5219 }
5220 /**
5221 Used to customize the `type` field of the document. By default, pluralizes and dasherizes the model's `modelName`.
5222 For example, the JSON:API document for a `blogPost` model would be:
5223 ```js
5224 {
5225 data: {
5226 id: 1,
5227 type: 'blog-posts'
5228 }
5229 }
5230 ```
5231 @method typeKeyForModel
5232 @param {Model} model
5233 @return {String}
5234 @public
5235 */
5236
5237
5238 typeKeyForModel(model) {
5239 return dasherize(this._container.inflector.pluralize(model.modelName));
5240 }
5241
5242 getCoalescedIds(request) {
5243 let ids = request.queryParams && request.queryParams["filter[id]"];
5244
5245 if (typeof ids === "string") {
5246 return ids.split(",");
5247 }
5248
5249 return ids;
5250 }
5251 /**
5252 Allows for per-relationship inclusion of linkage data. Use this when `alwaysIncludeLinkageData` is not granular enough.
5253 ```js
5254 export default JSONAPISerializer.extend({
5255 shouldIncludeLinkageData(relationshipKey, model) {
5256 if (relationshipKey === 'author' || relationshipKey === 'ghostWriter') {
5257 return true;
5258 }
5259 return false;
5260 }
5261 });
5262 ```
5263 @method shouldIncludeLinkageData
5264 @param {String} relationshipKey
5265 @param {Model} model
5266 @return {Boolean}
5267 @public
5268 */
5269
5270
5271 shouldIncludeLinkageData(relationshipKey, model) {
5272 return false;
5273 }
5274
5275}
5276
5277JSONAPISerializer.prototype.alwaysIncludeLinkageData = false;
5278
5279/**
5280 * @hide
5281 */
5282
5283class SerializerRegistry {
5284 constructor(schema, serializerMap = {}, server) {
5285 this.schema = schema;
5286 this._serializerMap = serializerMap;
5287 }
5288
5289 normalize(payload, modelName) {
5290 return this.serializerFor(modelName).normalize(payload);
5291 }
5292
5293 serialize(response, request) {
5294 this.request = request;
5295
5296 if (this._isModelOrCollection(response)) {
5297 let serializer = this.serializerFor(response.modelName);
5298 return serializer.serialize(response, request);
5299 } else if (Array.isArray(response) && response.some(this._isCollection)) {
5300 return response.reduce((json, collection) => {
5301 let serializer = this.serializerFor(collection.modelName);
5302
5303 if (serializer.embed) {
5304 json[this._container.inflector.pluralize(collection.modelName)] = serializer.serialize(collection, request);
5305 } else {
5306 json = Object.assign(json, serializer.serialize(collection, request));
5307 }
5308
5309 return json;
5310 }, {});
5311 } else {
5312 return response;
5313 }
5314 }
5315
5316 serializerFor(type, {
5317 explicit = false
5318 } = {}) {
5319 let SerializerForResponse = type && this._serializerMap && this._serializerMap[camelize(type)];
5320
5321 if (explicit) {
5322 assert(!!SerializerForResponse, `You passed in ${type} as an explicit serializer type but that serializer doesn't exist.`);
5323 } else {
5324 SerializerForResponse = SerializerForResponse || this._serializerMap.application || Serializer;
5325 assert(!SerializerForResponse || SerializerForResponse.prototype.embed || SerializerForResponse.prototype.root || new SerializerForResponse() instanceof JSONAPISerializer, "You cannot have a serializer that sideloads (embed: false) and disables the root (root: false).");
5326 }
5327
5328 return new SerializerForResponse(this, type, this.request);
5329 }
5330
5331 _isModel(object) {
5332 return object instanceof Model;
5333 }
5334
5335 _isCollection(object) {
5336 return object instanceof Collection || object instanceof PolymorphicCollection;
5337 }
5338
5339 _isModelOrCollection(object) {
5340 return this._isModel(object) || this._isCollection(object);
5341 }
5342
5343 registerSerializers(newSerializerMaps) {
5344 let currentSerializerMap = this._serializerMap || {};
5345 this._serializerMap = Object.assign(currentSerializerMap, newSerializerMaps);
5346 }
5347
5348 getCoalescedIds(request, modelName) {
5349 return this.serializerFor(modelName).getCoalescedIds(request);
5350 }
5351
5352}
5353
5354const collectionNameCache = {};
5355const internalCollectionNameCache = {};
5356const modelNameCache = {};
5357/**
5358 The primary use of the `Schema` class is to use it to find Models and Collections via the `Model` class methods.
5359
5360 The `Schema` is most often accessed via the first parameter to a route handler:
5361
5362 ```js
5363 this.get('posts', schema => {
5364 return schema.posts.where({ isAdmin: false });
5365 });
5366 ```
5367
5368 It is also available from the `.schema` property of a `server` instance:
5369
5370 ```js
5371 server.schema.users.create({ name: 'Yehuda' });
5372 ```
5373
5374 To work with the Model or Collection returned from one of the methods below, refer to the instance methods in the API docs for the `Model` and `Collection` classes.
5375
5376 @class Schema
5377 @constructor
5378 @public
5379 */
5380
5381class Schema {
5382 constructor(db, modelsMap = {}) {
5383 assert(db, "A schema requires a db");
5384 /**
5385 Returns Mirage's database. See the `Db` docs for the db's API.
5386 @property db
5387 @type {Object}
5388 @public
5389 */
5390
5391 this.db = db;
5392 this._registry = {};
5393 this._dependentAssociations = {
5394 polymorphic: []
5395 };
5396 this.registerModels(modelsMap);
5397 this.isSaving = {}; // a hash of models that are being saved, used to avoid cycles
5398 }
5399 /**
5400 @method registerModels
5401 @param hash
5402 @public
5403 @hide
5404 */
5405
5406
5407 registerModels(hash = {}) {
5408 forIn(hash, (model, key) => {
5409 this.registerModel(key, hash[key]);
5410 });
5411 }
5412 /**
5413 @method registerModel
5414 @param type
5415 @param ModelClass
5416 @public
5417 @hide
5418 */
5419
5420
5421 registerModel(type, ModelClass) {
5422 let camelizedModelName = camelize(type);
5423 let modelName = dasherize(camelizedModelName); // Avoid mutating original class, because we may want to reuse it across many tests
5424
5425 ModelClass = ModelClass.extend(); // Store model & fks in registry
5426 // TODO: don't think this is needed anymore
5427
5428 this._registry[camelizedModelName] = this._registry[camelizedModelName] || {
5429 class: null,
5430 foreignKeys: []
5431 }; // we may have created this key before, if another model added fks to it
5432
5433 this._registry[camelizedModelName].class = ModelClass; // TODO: set here, remove from model#constructor
5434
5435 ModelClass.prototype._schema = this;
5436 ModelClass.prototype.modelName = modelName; // Set up associations
5437
5438 ModelClass.prototype.hasManyAssociations = {}; // a registry of the model's hasMany associations. Key is key from model definition, value is association instance itself
5439
5440 ModelClass.prototype.hasManyAssociationFks = {}; // a lookup table to get the hasMany association by foreignKey
5441
5442 ModelClass.prototype.belongsToAssociations = {}; // a registry of the model's belongsTo associations. Key is key from model definition, value is association instance itself
5443
5444 ModelClass.prototype.belongsToAssociationFks = {}; // a lookup table to get the belongsTo association by foreignKey
5445
5446 ModelClass.prototype.associationKeys = new Set(); // ex: address.user, user.addresses
5447
5448 ModelClass.prototype.associationIdKeys = new Set(); // ex: address.user_id, user.address_ids
5449
5450 ModelClass.prototype.dependentAssociations = []; // a registry of associations that depend on this model, needed for deletion cleanup.
5451
5452 let fksAddedFromThisModel = {};
5453
5454 for (let associationProperty in ModelClass.prototype) {
5455 if (ModelClass.prototype[associationProperty] instanceof Association) {
5456 let association = ModelClass.prototype[associationProperty];
5457 association.key = associationProperty;
5458 association.modelName = association.modelName || this.toModelName(associationProperty);
5459 association.ownerModelName = modelName;
5460 association.setSchema(this); // Update the registry with this association's foreign keys. This is
5461 // essentially our "db migration", since we must know about the fks.
5462
5463 let [fkHolder, fk] = association.getForeignKeyArray();
5464 fksAddedFromThisModel[fkHolder] = fksAddedFromThisModel[fkHolder] || [];
5465 assert(!fksAddedFromThisModel[fkHolder].includes(fk), `Your '${type}' model definition has multiple possible inverse relationships of type '${fkHolder}'. Please use explicit inverses.`);
5466 fksAddedFromThisModel[fkHolder].push(fk);
5467
5468 this._addForeignKeyToRegistry(fkHolder, fk); // Augment the Model's class with any methods added by this association
5469
5470
5471 association.addMethodsToModelClass(ModelClass, associationProperty);
5472 }
5473 } // Create a db collection for this model, if doesn't exist
5474
5475
5476 let collection = this.toCollectionName(modelName);
5477
5478 if (!this.db[collection]) {
5479 this.db.createCollection(collection);
5480 } // Create the entity methods
5481
5482
5483 this[collection] = {
5484 camelizedModelName,
5485 new: attrs => this.new(camelizedModelName, attrs),
5486 create: attrs => this.create(camelizedModelName, attrs),
5487 all: attrs => this.all(camelizedModelName, attrs),
5488 find: attrs => this.find(camelizedModelName, attrs),
5489 findBy: attrs => this.findBy(camelizedModelName, attrs),
5490 findOrCreateBy: attrs => this.findOrCreateBy(camelizedModelName, attrs),
5491 where: attrs => this.where(camelizedModelName, attrs),
5492 none: attrs => this.none(camelizedModelName, attrs),
5493 first: attrs => this.first(camelizedModelName, attrs)
5494 };
5495 return this;
5496 }
5497 /**
5498 @method modelFor
5499 @param type
5500 @public
5501 @hide
5502 */
5503
5504
5505 modelFor(type) {
5506 return this._registry[type];
5507 }
5508 /**
5509 Create a new unsaved model instance with attributes *attrs*.
5510 ```js
5511 let post = blogPosts.new({ title: 'Lorem ipsum' });
5512 post.title; // Lorem ipsum
5513 post.id; // null
5514 post.isNew(); // true
5515 ```
5516 @method new
5517 @param type
5518 @param attrs
5519 @public
5520 */
5521
5522
5523 new(type, attrs) {
5524 return this._instantiateModel(dasherize(type), attrs);
5525 }
5526 /**
5527 Create a new model instance with attributes *attrs*, and insert it into the database.
5528 ```js
5529 let post = blogPosts.create({title: 'Lorem ipsum'});
5530 post.title; // Lorem ipsum
5531 post.id; // 1
5532 post.isNew(); // false
5533 ```
5534 @method create
5535 @param type
5536 @param attrs
5537 @public
5538 */
5539
5540
5541 create(type, attrs) {
5542 return this.new(type, attrs).save();
5543 }
5544 /**
5545 Return all models in the database.
5546 ```js
5547 let posts = blogPosts.all();
5548 // [post:1, post:2, ...]
5549 ```
5550 @method all
5551 @param type
5552 @public
5553 */
5554
5555
5556 all(type) {
5557 let collection = this.collectionForType(type);
5558 return this._hydrate(collection, dasherize(type));
5559 }
5560 /**
5561 Return an empty collection of type `type`.
5562 @method none
5563 @param type
5564 @public
5565 */
5566
5567
5568 none(type) {
5569 return this._hydrate([], dasherize(type));
5570 }
5571 /**
5572 Return one or many models in the database by id.
5573 ```js
5574 let post = blogPosts.find(1);
5575 let posts = blogPosts.find([1, 3, 4]);
5576 ```
5577 @method find
5578 @param type
5579 @param ids
5580 @public
5581 */
5582
5583
5584 find(type, ids) {
5585 let collection = this.collectionForType(type);
5586 let records = collection.find(ids);
5587
5588 if (Array.isArray(ids)) {
5589 assert(records.length === ids.length, `Couldn't find all ${this._container.inflector.pluralize(type)} with ids: (${ids.join(",")}) (found ${records.length} results, but was looking for ${ids.length})`);
5590 }
5591
5592 return this._hydrate(records, dasherize(type));
5593 }
5594 /**
5595 Returns the first model in the database that matches the key-value pairs in `attrs`. Note that a string comparison is used.
5596 ```js
5597 let post = blogPosts.findBy({ published: true });
5598 ```
5599 N.B. This will return `null` if the schema doesn't have any matching record.
5600 @method findBy
5601 @param type
5602 @param attributeName
5603 @public
5604 */
5605
5606
5607 findBy(type, query) {
5608 let collection = this.collectionForType(type);
5609 let record = collection.findBy(query);
5610 return this._hydrate(record, dasherize(type));
5611 }
5612 /**
5613 Returns the first model in the database that matches the key-value pairs in `attrs`, or creates a record with the attributes if one is not found.
5614 ```js
5615 // Find the first published blog post, or create a new one.
5616 let post = blogPosts.findOrCreateBy({ published: true });
5617 ```
5618 @method findOrCreateBy
5619 @param type
5620 @param attributeName
5621 @public
5622 */
5623
5624
5625 findOrCreateBy(type, attrs) {
5626 let collection = this.collectionForType(type);
5627 let record = collection.findBy(attrs);
5628 let model;
5629
5630 if (!record) {
5631 model = this.create(type, attrs);
5632 } else {
5633 model = this._hydrate(record, dasherize(type));
5634 }
5635
5636 return model;
5637 }
5638 /**
5639 Return an ORM/Collection, which represents an array of models from the database matching `query`.
5640 If `query` is an object, its key-value pairs will be compared against records using string comparison.
5641 `query` can also be a compare function.
5642 ```js
5643 let posts = blogPosts.where({ published: true });
5644 let posts = blogPosts.where(post => post.published === true);
5645 ```
5646 @method where
5647 @param type
5648 @param query
5649 @public
5650 */
5651
5652
5653 where(type, query) {
5654 let collection = this.collectionForType(type);
5655 let records = collection.where(query);
5656 return this._hydrate(records, dasherize(type));
5657 }
5658 /**
5659 Returns the first model in the database.
5660 ```js
5661 let post = blogPosts.first();
5662 ```
5663 N.B. This will return `null` if the schema doesn't contain any records.
5664 @method first
5665 @param type
5666 @public
5667 */
5668
5669
5670 first(type) {
5671 let collection = this.collectionForType(type);
5672 let record = collection[0];
5673 return this._hydrate(record, dasherize(type));
5674 }
5675 /**
5676 @method modelClassFor
5677 @param modelName
5678 @public
5679 @hide
5680 */
5681
5682
5683 modelClassFor(modelName) {
5684 let model = this._registry[camelize(modelName)];
5685
5686 assert(model, `Model not registered: ${modelName}`);
5687 return model.class.prototype;
5688 }
5689 /*
5690 This method updates the dependentAssociations registry, which is used to
5691 keep track of which models depend on a given association. It's used when
5692 deleting models - their dependents need to be looked up and foreign keys
5693 updated.
5694 For example,
5695 schema = {
5696 post: Model.extend(),
5697 comment: Model.extend({
5698 post: belongsTo()
5699 })
5700 };
5701 comment1.post = post1;
5702 ...
5703 post1.destroy()
5704 Deleting this post should clear out comment1's foreign key.
5705 Polymorphic associations can have _any_ other model as a dependent, so we
5706 handle them separately.
5707 */
5708
5709
5710 addDependentAssociation(association, modelName) {
5711 if (association.isPolymorphic) {
5712 this._dependentAssociations.polymorphic.push(association);
5713 } else {
5714 this._dependentAssociations[modelName] = this._dependentAssociations[modelName] || [];
5715
5716 this._dependentAssociations[modelName].push(association);
5717 }
5718 }
5719
5720 dependentAssociationsFor(modelName) {
5721 let directDependents = this._dependentAssociations[modelName] || [];
5722 let polymorphicAssociations = this._dependentAssociations.polymorphic || [];
5723 return directDependents.concat(polymorphicAssociations);
5724 }
5725
5726 associationsFor(modelName) {
5727 let modelClass = this.modelClassFor(modelName);
5728 return Object.assign({}, modelClass.belongsToAssociations, modelClass.hasManyAssociations);
5729 }
5730
5731 hasModelForModelName(modelName) {
5732 return this.modelFor(camelize(modelName));
5733 }
5734 /*
5735 Private methods
5736 */
5737
5738 /**
5739 @method collectionForType
5740 @param type
5741 @private
5742 @hide
5743 */
5744
5745
5746 collectionForType(type) {
5747 let collection = this.toCollectionName(type);
5748 assert(this.db[collection], `You're trying to find model(s) of type ${type} but this collection doesn't exist in the database.`);
5749 return this.db[collection];
5750 }
5751
5752 toCollectionName(type) {
5753 if (typeof collectionNameCache[type] !== "string") {
5754 let modelName = dasherize(type);
5755 const collectionName = camelize(this._container.inflector.pluralize(modelName));
5756 collectionNameCache[type] = collectionName;
5757 }
5758
5759 return collectionNameCache[type];
5760 } // This is to get at the underlying Db collection. Poorly named... need to
5761 // refactor to DbTable or something.
5762
5763
5764 toInternalCollectionName(type) {
5765 if (typeof internalCollectionNameCache[type] !== "string") {
5766 const internalCollectionName = `_${this.toCollectionName(type)}`;
5767 internalCollectionNameCache[type] = internalCollectionName;
5768 }
5769
5770 return internalCollectionNameCache[type];
5771 }
5772
5773 toModelName(type) {
5774 if (typeof modelNameCache[type] !== "string") {
5775 let dasherized = dasherize(type);
5776
5777 const modelName = this._container.inflector.singularize(dasherized);
5778
5779 modelNameCache[type] = modelName;
5780 }
5781
5782 return modelNameCache[type];
5783 }
5784 /**
5785 @method _addForeignKeyToRegistry
5786 @param type
5787 @param fk
5788 @private
5789 @hide
5790 */
5791
5792
5793 _addForeignKeyToRegistry(type, fk) {
5794 this._registry[type] = this._registry[type] || {
5795 class: null,
5796 foreignKeys: []
5797 };
5798 let fks = this._registry[type].foreignKeys;
5799
5800 if (!fks.includes(fk)) {
5801 fks.push(fk);
5802 }
5803 }
5804 /**
5805 @method _instantiateModel
5806 @param modelName
5807 @param attrs
5808 @private
5809 @hide
5810 */
5811
5812
5813 _instantiateModel(modelName, attrs) {
5814 let ModelClass = this._modelFor(modelName);
5815
5816 let fks = this._foreignKeysFor(modelName);
5817
5818 return new ModelClass(this, modelName, attrs, fks);
5819 }
5820 /**
5821 @method _modelFor
5822 @param modelName
5823 @private
5824 @hide
5825 */
5826
5827
5828 _modelFor(modelName) {
5829 return this._registry[camelize(modelName)].class;
5830 }
5831 /**
5832 @method _foreignKeysFor
5833 @param modelName
5834 @private
5835 @hide
5836 */
5837
5838
5839 _foreignKeysFor(modelName) {
5840 return this._registry[camelize(modelName)].foreignKeys;
5841 }
5842 /**
5843 Takes a record and returns a model, or an array of records
5844 and returns a collection.
5845 *
5846 @method _hydrate
5847 @param records
5848 @param modelName
5849 @private
5850 @hide
5851 */
5852
5853
5854 _hydrate(records, modelName) {
5855 if (Array.isArray(records)) {
5856 let models = records.map(function (record) {
5857 return this._instantiateModel(modelName, record);
5858 }, this);
5859 return new Collection(modelName, models);
5860 } else if (records) {
5861 return this._instantiateModel(modelName, records);
5862 } else {
5863 return null;
5864 }
5865 }
5866
5867}
5868
5869const classes = {
5870 Db,
5871 Association,
5872 RouteHandler,
5873 BaseRouteHandler,
5874 Serializer,
5875 SerializerRegistry,
5876 Schema
5877};
5878let defaultInflector = {
5879 singularize: inflected.singularize,
5880 pluralize: inflected.pluralize
5881};
5882/**
5883 Lightweight DI container for customizable objects that are needed by
5884 deeply nested classes.
5885
5886 @class Container
5887 @hide
5888 */
5889
5890class Container {
5891 constructor() {
5892 this.inflector = defaultInflector;
5893 }
5894
5895 register(key, value) {
5896 this[key] = value;
5897 }
5898
5899 create(className, ...args) {
5900 let Class = classes[className];
5901 Class.prototype._container = this;
5902 return new Class(...args);
5903 }
5904
5905}
5906/**
5907 These are side effects. We give each class a default container so it can be
5908 easily unit tested.
5909
5910 We should remove these once we have test coverage and can refactor to a proper
5911 DI system.
5912*/
5913
5914
5915let defaultContainer = new Container();
5916Db.prototype._container = defaultContainer;
5917Association.prototype._container = defaultContainer;
5918BaseRouteHandler.prototype._container = defaultContainer;
5919RouteHandler.prototype._container = defaultContainer;
5920Serializer.prototype._container = defaultContainer;
5921SerializerRegistry.prototype._container = defaultContainer;
5922Schema.prototype._container = defaultContainer;
5923
5924/* eslint no-console: 0 */
5925const isPluralForModelCache = {};
5926/**
5927 * Creates a new Pretender instance.
5928 *
5929 * @method createPretender
5930 * @param {Server} server
5931 * @return {Object} A new Pretender instance.
5932 * @public
5933 */
5934
5935function createPretender(server) {
5936 if (typeof window !== "undefined") {
5937 return new Pretender(function () {
5938 this.passthroughRequest = function (verb, path, request) {
5939 if (server.shouldLog()) {
5940 console.log(`Mirage: Passthrough request for ${verb.toUpperCase()} ${request.url}`);
5941 }
5942 };
5943
5944 this.handledRequest = function (verb, path, request) {
5945 if (server.shouldLog()) {
5946 console.groupCollapsed(`Mirage: [${request.status}] ${verb.toUpperCase()} ${request.url}`);
5947 let {
5948 requestBody,
5949 responseText
5950 } = request;
5951 let loggedRequest, loggedResponse;
5952
5953 try {
5954 loggedRequest = JSON.parse(requestBody);
5955 } catch (e) {
5956 loggedRequest = requestBody;
5957 }
5958
5959 try {
5960 loggedResponse = JSON.parse(responseText);
5961 } catch (e) {
5962 loggedResponse = responseText;
5963 }
5964
5965 console.groupCollapsed("Response");
5966 console.log(loggedResponse);
5967 console.groupEnd();
5968 console.groupCollapsed("Request (data)");
5969 console.log(loggedRequest);
5970 console.groupEnd();
5971 console.groupCollapsed("Request (raw)");
5972 console.log(request);
5973 console.groupEnd();
5974 console.groupEnd();
5975 }
5976 };
5977
5978 let originalCheckPassthrough = this.checkPassthrough;
5979
5980 this.checkPassthrough = function (request) {
5981 let shouldPassthrough = server.passthroughChecks.some(passthroughCheck => passthroughCheck(request));
5982
5983 if (shouldPassthrough) {
5984 let url = request.url.includes("?") ? request.url.substr(0, request.url.indexOf("?")) : request.url;
5985 this[request.method.toLowerCase()](url, this.passthrough);
5986 }
5987
5988 return originalCheckPassthrough.apply(this, arguments);
5989 };
5990
5991 this.unhandledRequest = function (verb, path) {
5992 path = decodeURI(path);
5993 assert(`Your app tried to ${verb} '${path}', but there was no route defined to handle this request. Define a route for this endpoint in your routes() config. Did you forget to define a namespace?`);
5994 };
5995 }, {
5996 trackRequests: server.shouldTrackRequests()
5997 });
5998 }
5999}
6000
6001const defaultRouteOptions = {
6002 coalesce: false,
6003 timing: undefined
6004};
6005const defaultInflector$1 = {
6006 singularize: inflected.singularize,
6007 pluralize: inflected.pluralize
6008};
6009/**
6010 @hide
6011*/
6012
6013const defaultPassthroughs = ["http://localhost:0/chromecheckurl", // mobile chrome
6014"http://localhost:30820/socket.io", // electron
6015request => {
6016 return /.+\.hot-update.json$/.test(request.url);
6017}];
6018/**
6019 * Determine if the object contains a valid option.
6020 *
6021 * @method isOption
6022 * @param {Object} option An object with one option value pair.
6023 * @return {Boolean} True if option is a valid option, false otherwise.
6024 * @private
6025 */
6026
6027function isOption(option) {
6028 if (!option || typeof option !== "object") {
6029 return false;
6030 }
6031
6032 let allOptions = Object.keys(defaultRouteOptions);
6033 let optionKeys = Object.keys(option);
6034
6035 for (let i = 0; i < optionKeys.length; i++) {
6036 let key = optionKeys[i];
6037
6038 if (allOptions.indexOf(key) > -1) {
6039 return true;
6040 }
6041 }
6042
6043 return false;
6044}
6045/**
6046 * Extract arguments for a route.
6047 *
6048 * @method extractRouteArguments
6049 * @param {Array} args Of the form [options], [object, code], [function, code]
6050 * [shorthand, options], [shorthand, code, options]
6051 * @return {Array} [handler (i.e. the function, object or shorthand), code,
6052 * options].
6053 * @private
6054 */
6055
6056
6057function extractRouteArguments(args) {
6058 let [lastArg] = args.splice(-1);
6059
6060 if (isOption(lastArg)) {
6061 lastArg = assign({}, defaultRouteOptions, lastArg);
6062 } else {
6063 args.push(lastArg);
6064 lastArg = defaultRouteOptions;
6065 }
6066
6067 let t = 2 - args.length;
6068
6069 while (t-- > 0) {
6070 args.push(undefined);
6071 }
6072
6073 args.push(lastArg);
6074 return args;
6075}
6076/**
6077 The Mirage server.
6078
6079 Note that `this` within your `routes` function refers to the server instance, which is the same instance that `server` refers to in your tests.
6080
6081 @class Server
6082 @public
6083*/
6084
6085
6086class Server {
6087 constructor(options = {}) {
6088 this._container = new Container();
6089 this.config(options);
6090 /**
6091 Returns the Mirage Db instance.
6092 @property db
6093 @return Db
6094 */
6095
6096 this.db = this.db || undefined;
6097 /**
6098 Returns the Mirage Schema (ORM) instance.
6099 @property schema
6100 @return Schema
6101 */
6102
6103 this.schema = this.schema || undefined;
6104 }
6105
6106 config(config = {}) {
6107 this.passthroughChecks = this.passthroughChecks || [];
6108 let didOverrideConfig = config.environment && this.environment && this.environment !== config.environment;
6109 assert(!didOverrideConfig, "You cannot modify Mirage's environment once the server is created");
6110 this.environment = config.environment || this.environment || "development";
6111
6112 if (config.routes) {
6113 assert(!config.baseConfig, "The routes option is an alias for the baseConfig option. You can't pass both options into your server definition.");
6114 config.baseConfig = config.routes;
6115 }
6116
6117 if (config.seeds) {
6118 assert(!config.scenarios, "The seeds option is an alias for the scenarios.default option. You can't pass both options into your server definition.");
6119 config.scenarios = {
6120 default: config.seeds
6121 };
6122 }
6123
6124 this._config = config;
6125 /**
6126 Set the base namespace used for all routes defined with `get`, `post`, `put` or `del`.
6127 For example,
6128 ```js
6129 new Server({
6130 routes() {
6131 this.namespace = '/api';
6132 // this route will handle the URL '/api/contacts'
6133 this.get('/contacts', 'contacts');
6134 }
6135 })
6136 ```
6137 Note that only routes defined after `this.namespace` are affected. This is useful if you have a few one-off routes that you don't want under your namespace:
6138 ```js
6139 new Server({
6140 routes() {
6141 // this route handles /auth
6142 this.get('/auth', function() { ...});
6143 this.namespace = '/api';
6144 // this route will handle the URL '/api/contacts'
6145 this.get('/contacts', 'contacts');
6146 };
6147 })
6148 ```
6149 If your app is loaded from the filesystem vs. a server (e.g. via Cordova or Electron vs. `localhost` or `https://yourhost.com/`), you will need to explicitly define a namespace. Likely values are `/` (if requests are made with relative paths) or `https://yourhost.com/api/...` (if requests are made to a defined server).
6150 For a sample implementation leveraging a configured API host & namespace, check out [this issue comment](https://github.com/miragejs/ember-cli-mirage/issues/497#issuecomment-183458721).
6151 @property namespace
6152 @type String
6153 @public
6154 */
6155
6156 this.namespace = this.namespace || config.namespace || "";
6157 /**
6158 * Mirage needs an inflector ({ singularize, pluralize }) to be passed in constructor
6159 */
6160
6161 this.inflector = config.inflector || defaultInflector$1;
6162
6163 this._container.register("inflector", this.inflector);
6164 /**
6165 Sets a string to prefix all route handler URLs with.
6166 Useful if your app makes API requests to a different port.
6167 ```js
6168 new Server({
6169 routes() {
6170 this.urlPrefix = 'http://localhost:8080'
6171 }
6172 })
6173 ```
6174 */
6175
6176
6177 this.urlPrefix = this.urlPrefix || config.urlPrefix || "";
6178 /**
6179 Set the number of milliseconds for the the Server's response time.
6180 By default there's a 400ms delay during development, and 0 delay in testing (so your tests run fast).
6181 ```js
6182 new Server({
6183 routes() {
6184 this.timing = 400; // default
6185 }
6186 })
6187 ```
6188 To set the timing for individual routes, see the `timing` option for route handlers.
6189 @property timing
6190 @type Number
6191 @public
6192 */
6193
6194 this.timing = this.timing || config.timing || 400;
6195 /**
6196 Set to `true` or `false` to explicitly specify logging behavior.
6197 By default, server responses are logged in non-testing environments. Logging is disabled by default in testing, so as not to clutter CI test runner output.
6198 For example, to enable logging in tests, write the following:
6199 ```js
6200 test('I can view all users', function() {
6201 server.logging = true;
6202 server.create('user');
6203 visit('/users');
6204 // ...
6205 });
6206 ```
6207 You can also write a custom log message using the [Pretender server's `handledRequest` hook](https://github.com/pretenderjs/pretender#handled-requests). (You can access the pretender server from your Mirage server via `server.pretender`.)
6208 To override,
6209 ```js
6210 new Server({
6211 routes() {
6212 this.pretender.handledRequest = function(verb, path, request) {
6213 let { responseText } = request;
6214 // log request and response data
6215 }
6216 }
6217 })
6218 ```
6219 @property logging
6220 @return {Boolean}
6221 @public
6222 */
6223
6224 this.logging = this.logging !== undefined ? this.logging : undefined;
6225 this.testConfig = this.testConfig || undefined;
6226 this.trackRequests = config.trackRequests;
6227
6228 this._defineRouteHandlerHelpers();
6229
6230 if (this.db) {
6231 this.db.registerIdentityManagers(config.identityManagers);
6232 } else {
6233 this.db = this._container.create("Db", undefined, config.identityManagers);
6234 }
6235
6236 if (this.schema) {
6237 this.schema.registerModels(config.models);
6238 this.serializerOrRegistry.registerSerializers(config.serializers || {});
6239 } else {
6240 this.schema = this._container.create("Schema", this.db, config.models);
6241 this.serializerOrRegistry = this._container.create("SerializerRegistry", this.schema, config.serializers);
6242 }
6243
6244 let hasFactories = this._hasModulesOfType(config, "factories");
6245
6246 let hasDefaultScenario = config.scenarios && Object.prototype.hasOwnProperty.call(config.scenarios, "default");
6247 let didOverridePretenderConfig = config.trackRequests !== undefined && this.pretender;
6248 assert(!didOverridePretenderConfig, "You cannot modify Pretender's request tracking once the server is created");
6249 /**
6250 Mirage uses [pretender.js](https://github.com/trek/pretender) as its xhttp interceptor. In your Mirage config, `this.pretender` refers to the actual Pretender instance, so any config options that work there will work here as well.
6251 ```js
6252 new Server({
6253 routes() {
6254 this.pretender.handledRequest = (verb, path, request) => {
6255 console.log(`Your server responded to ${path}`);
6256 }
6257 }
6258 })
6259 ```
6260 Refer to [Pretender's docs](https://github.com/pretenderjs/pretender) if you want to change any options on your Pretender instance.
6261 @property pretender
6262 @return {Object} The Pretender instance
6263 @public
6264 */
6265
6266 this.pretender = this.pretender || config.pretender || createPretender(this);
6267
6268 if (config.baseConfig) {
6269 this.loadConfig(config.baseConfig);
6270 }
6271
6272 if (this.isTest()) {
6273 if (config.testConfig) {
6274 this.loadConfig(config.testConfig);
6275 }
6276
6277 if (typeof window !== "undefined") {
6278 window.server = this; // TODO: Better way to inject server into test env
6279 }
6280 }
6281
6282 if (this.isTest() && hasFactories) {
6283 this.loadFactories(config.factories);
6284 } else if (!this.isTest() && hasDefaultScenario) {
6285 this.loadFactories(config.factories);
6286 config.scenarios.default(this);
6287 } else {
6288 this.loadFixtures();
6289 }
6290
6291 let useDefaultPassthroughs = typeof config.useDefaultPassthroughs !== "undefined" ? config.useDefaultPassthroughs : true;
6292
6293 if (useDefaultPassthroughs) {
6294 this._configureDefaultPassthroughs();
6295 }
6296 }
6297 /**
6298 * Determines if the current environment is the testing environment.
6299 *
6300 * @method isTest
6301 * @return {Boolean} True if the environment is 'test', false otherwise.
6302 * @public
6303 * @hide
6304 */
6305
6306
6307 isTest() {
6308 return this.environment === "test";
6309 }
6310 /**
6311 Determines if the server should log.
6312 @method shouldLog
6313 @return The value of this.logging if defined, or false if in the testing environment,
6314 true otherwise.
6315 @public
6316 @hide
6317 */
6318
6319
6320 shouldLog() {
6321 return typeof this.logging !== "undefined" ? this.logging : !this.isTest();
6322 }
6323 /**
6324 * Determines if the server should track requests.
6325 *
6326 * @method shouldTrackRequests
6327 * @return The value of this.trackRequests if defined, false otherwise.
6328 * @public
6329 * @hide
6330 */
6331
6332
6333 shouldTrackRequests() {
6334 return Boolean(this.trackRequests);
6335 }
6336 /**
6337 * Load the configuration given, setting timing to 0 if in the test
6338 * environment.
6339 *
6340 * @method loadConfig
6341 * @param {Object} config The configuration to load.
6342 * @public
6343 * @hide
6344 */
6345
6346
6347 loadConfig(config) {
6348 config.call(this);
6349 this.timing = this.isTest() ? 0 : this.timing || 0;
6350 }
6351 /**
6352 By default, if your app makes a request that is not defined in your server config, Mirage will throw an error. You can use `passthrough` to whitelist requests, and allow them to pass through your Mirage server to the actual network layer.
6353 <aside>
6354 <p>Note: Put all passthrough config at the bottom of your routes, to give your route handlers precedence.</p>
6355 </aside>
6356 To ignore paths on your current host (as well as configured `namespace`), use a leading `/`:
6357 ```js
6358 this.passthrough('/addresses');
6359 ```
6360 You can also pass a list of paths, or call `passthrough` multiple times:
6361 ```js
6362 this.passthrough('/addresses', '/contacts');
6363 this.passthrough('/something');
6364 this.passthrough('/else');
6365 ```
6366 These lines will allow all HTTP verbs to pass through. If you want only certain verbs to pass through, pass an array as the last argument with the specified verbs:
6367 ```js
6368 this.passthrough('/addresses', ['post']);
6369 this.passthrough('/contacts', '/photos', ['get']);
6370 ```
6371 You can pass a function to `passthrough` to do a runtime check on whether or not the request should be handled by Mirage. If the function returns `true` Mirage will not handle the request and let it pass through.
6372 ```js
6373 this.passthrough(request => {
6374 return request.queryParams.skipMirage;
6375 });
6376 ```
6377 If you want all requests on the current domain to pass through, simply invoke the method with no arguments:
6378 ```js
6379 this.passthrough();
6380 ```
6381 Note again that the current namespace (i.e. any `namespace` property defined above this call) will be applied.
6382 You can also allow other-origin hosts to passthrough. If you use a fully-qualified domain name, the `namespace` property will be ignored. Use two * wildcards to match all requests under a path:
6383 ```js
6384 this.passthrough('http://api.foo.bar/**');
6385 this.passthrough('http://api.twitter.com/v1/cards/**');
6386 ```
6387 In versions of Pretender prior to 0.12, `passthrough` only worked with jQuery >= 2.x. As long as you're on Pretender@0.12 or higher, you should be all set.
6388 @method passthrough
6389 @param {String} [...paths] Any number of paths to whitelist
6390 @param {Array} options Unused
6391 @public
6392 */
6393
6394
6395 passthrough(...paths) {
6396 // this only works in browser-like environments for now. in node users will have to configure
6397 // their own interceptor if they are using one.
6398 if (typeof window !== "undefined") {
6399 let verbs = ["get", "post", "put", "delete", "patch", "options", "head"];
6400 let lastArg = paths[paths.length - 1];
6401
6402 if (paths.length === 0) {
6403 paths = ["/**", "/"];
6404 } else if (Array.isArray(lastArg)) {
6405 verbs = paths.pop();
6406 }
6407
6408 paths.forEach(path => {
6409 if (typeof path === "function") {
6410 this.passthroughChecks.push(path);
6411 } else {
6412 verbs.forEach(verb => {
6413 let fullPath = this._getFullPath(path);
6414
6415 this.pretender[verb](fullPath, this.pretender.passthrough);
6416 });
6417 }
6418 });
6419 }
6420 }
6421 /**
6422 By default, `fixtures` will be loaded during testing if you don't have factories defined, and during development if you don't have `seeds` defined. You can use `loadFixtures()` to also load fixture files in either of these environments, in addition to using factories to seed your database.
6423 `server.loadFixtures()` loads all the files, and `server.loadFixtures(file1, file2...)` loads selective fixture files.
6424 For example, in a test you may want to start out with all your fixture data loaded:
6425 ```js
6426 test('I can view the photos', function() {
6427 server.loadFixtures();
6428 server.createList('photo', 10);
6429 visit('/');
6430 andThen(() => {
6431 equal( find('img').length, 10 );
6432 });
6433 });
6434 ```
6435 or in development, you may want to load a few reference fixture files, and use factories to define the rest of your data:
6436 ```js
6437 new Server({
6438 ...,
6439 seeds(server) {
6440 server.loadFixtures('countries', 'states');
6441 let author = server.create('author');
6442 server.createList('post', 10, {author_id: author.id});
6443 }
6444 })
6445 ```
6446 @method loadFixtures
6447 @param {String} [...args] The name of the fixture to load.
6448 @public
6449 */
6450
6451
6452 loadFixtures(...args) {
6453 let {
6454 fixtures
6455 } = this._config;
6456
6457 if (args.length) {
6458 let camelizedArgs = args.map(camelize);
6459 let missingKeys = camelizedArgs.filter(key => !fixtures[key]);
6460
6461 if (missingKeys.length) {
6462 throw new Error(`Fixtures not found: ${missingKeys.join(", ")}`);
6463 }
6464
6465 fixtures = pick(fixtures, ...camelizedArgs);
6466 }
6467
6468 this.db.loadData(fixtures);
6469 }
6470 /*
6471 Factory methods
6472 */
6473
6474 /**
6475 * Load factories into Mirage's database.
6476 *
6477 * @method loadFactories
6478 * @param {Object} factoryMap
6479 * @public
6480 * @hide
6481 */
6482
6483
6484 loadFactories(factoryMap = {}) {
6485 // Store a reference to the factories
6486 let currentFactoryMap = this._factoryMap || {};
6487 this._factoryMap = assign(currentFactoryMap, factoryMap); // Create a collection for each factory
6488
6489 Object.keys(factoryMap).forEach(type => {
6490 let collectionName = this.schema.toCollectionName(type);
6491 this.db.createCollection(collectionName);
6492 });
6493 }
6494 /**
6495 * Get the factory for a given type.
6496 *
6497 * @method factoryFor
6498 * @param {String} type
6499 * @private
6500 * @hide
6501 */
6502
6503
6504 factoryFor(type) {
6505 let camelizedType = camelize(type);
6506
6507 if (this._factoryMap && this._factoryMap[camelizedType]) {
6508 return this._factoryMap[camelizedType];
6509 }
6510 }
6511
6512 build(type, ...traitsAndOverrides) {
6513 let traits = traitsAndOverrides.filter(arg => arg && typeof arg === "string");
6514 let overrides = find(traitsAndOverrides, arg => isPlainObject(arg));
6515 let camelizedType = camelize(type); // Store sequence for factory type as instance variable
6516
6517 this.factorySequences = this.factorySequences || {};
6518 this.factorySequences[camelizedType] = this.factorySequences[camelizedType] + 1 || 0;
6519 let OriginalFactory = this.factoryFor(type);
6520
6521 if (OriginalFactory) {
6522 OriginalFactory = OriginalFactory.extend({});
6523 let attrs = OriginalFactory.attrs || {};
6524
6525 this._validateTraits(traits, OriginalFactory, type);
6526
6527 let mergedExtensions = this._mergeExtensions(attrs, traits, overrides);
6528
6529 this._mapAssociationsFromAttributes(type, attrs, overrides);
6530
6531 this._mapAssociationsFromAttributes(type, mergedExtensions);
6532
6533 let Factory = OriginalFactory.extend(mergedExtensions);
6534 let factory = new Factory();
6535 let sequence = this.factorySequences[camelizedType];
6536 return factory.build(sequence);
6537 } else {
6538 return overrides;
6539 }
6540 }
6541
6542 buildList(type, amount, ...traitsAndOverrides) {
6543 assert(isInteger(amount), `second argument has to be an integer, you passed: ${typeof amount}`);
6544 let list = [];
6545 const buildArgs = [type, ...traitsAndOverrides];
6546
6547 for (let i = 0; i < amount; i++) {
6548 list.push(this.build.apply(this, buildArgs));
6549 }
6550
6551 return list;
6552 }
6553 /**
6554 Generates a single model of type *type*, inserts it into the database (giving it an id), and returns the data that was
6555 added.
6556 ```js
6557 test("I can view a contact's details", function() {
6558 let contact = server.create('contact');
6559 visit('/contacts/' + contact.id);
6560 andThen(() => {
6561 equal( find('h1').text(), 'The contact is Link');
6562 });
6563 });
6564 ```
6565 You can override the attributes from the factory definition with a
6566 hash passed in as the second parameter. For example, if we had this factory
6567 ```js
6568 export default Factory.extend({
6569 name: 'Link'
6570 });
6571 ```
6572 we could override the name like this:
6573 ```js
6574 test("I can view the contacts", function() {
6575 server.create('contact', {name: 'Zelda'});
6576 visit('/');
6577 andThen(() => {
6578 equal( find('p').text(), 'Zelda' );
6579 });
6580 });
6581 ```
6582 @method create
6583 @param type the singularized type of the model
6584 @param traitsAndOverrides
6585 @public
6586 */
6587
6588
6589 create(type, ...options) {
6590 assert(this._modelOrFactoryExistsForType(type), `You called server.create('${type}') but no model or factory was found. Make sure you're passing in the singularized version of the model or factory name.`); // When there is a Model defined, we should return an instance
6591 // of it instead of returning the bare attributes.
6592
6593 let traits = options.filter(arg => arg && typeof arg === "string");
6594 let overrides = find(options, arg => isPlainObject(arg));
6595 let collectionFromCreateList = find(options, arg => arg && Array.isArray(arg));
6596 let attrs = this.build(type, ...traits, overrides);
6597 let modelOrRecord;
6598
6599 if (this.schema && this.schema[this.schema.toCollectionName(type)]) {
6600 let modelClass = this.schema[this.schema.toCollectionName(type)];
6601 modelOrRecord = modelClass.create(attrs);
6602 } else {
6603 let collection, collectionName;
6604
6605 if (collectionFromCreateList) {
6606 collection = collectionFromCreateList;
6607 } else {
6608 collectionName = this.schema ? this.schema.toInternalCollectionName(type) : `_${this.inflector.pluralize(type)}`;
6609 collection = this.db[collectionName];
6610 }
6611
6612 assert(collection, `You called server.create('${type}') but no model or factory was found.`);
6613 modelOrRecord = collection.insert(attrs);
6614 }
6615
6616 let OriginalFactory = this.factoryFor(type);
6617
6618 if (OriginalFactory) {
6619 OriginalFactory.extractAfterCreateCallbacks({
6620 traits
6621 }).forEach(afterCreate => {
6622 afterCreate(modelOrRecord, this);
6623 });
6624 }
6625
6626 return modelOrRecord;
6627 }
6628 /**
6629 Creates *amount* models of type *type*, optionally overriding the attributes from the factory with *attrs*.
6630 Returns the array of records that were added to the database.
6631 Here's an example from a test:
6632 ```js
6633 test("I can view the contacts", function() {
6634 server.createList('contact', 5);
6635 let youngContacts = server.createList('contact', 5, {age: 15});
6636 visit('/');
6637 andThen(function() {
6638 equal(currentRouteName(), 'index');
6639 equal( find('p').length, 10 );
6640 });
6641 });
6642 ```
6643 And one from setting up your development database:
6644 ```js
6645 new Server({
6646 seeds(server) {
6647 let contact = server.create('contact')
6648 server.createList('address', 5, { contact })
6649 }
6650 })
6651 ```
6652 @method createList
6653 @param type
6654 @param amount
6655 @param traitsAndOverrides
6656 @public
6657 */
6658
6659
6660 createList(type, amount, ...traitsAndOverrides) {
6661 assert(this._modelOrFactoryExistsForType(type), `You called server.createList('${type}') but no model or factory was found. Make sure you're passing in the singularized version of the model or factory name.`);
6662 assert(isInteger(amount), `second argument has to be an integer, you passed: ${typeof amount}`);
6663 let list = [];
6664 let collectionName = this.schema ? this.schema.toInternalCollectionName(type) : `_${this.inflector.pluralize(type)}`;
6665 let collection = this.db[collectionName];
6666 const createArguments = [type, ...traitsAndOverrides, collection];
6667
6668 for (let i = 0; i < amount; i++) {
6669 list.push(this.create.apply(this, createArguments));
6670 }
6671
6672 return list;
6673 }
6674 /**
6675 Shutdown the server and stop intercepting network requests.
6676 @method shutdown
6677 @public
6678 */
6679
6680
6681 shutdown() {
6682 if (typeof window !== "undefined") {
6683 this.pretender.shutdown();
6684 }
6685
6686 if (typeof window !== "undefined" && this.environment === "test") {
6687 window.server = undefined;
6688 }
6689 }
6690
6691 resource(resourceName, {
6692 only,
6693 except,
6694 path
6695 } = {}) {
6696 resourceName = this.inflector.pluralize(resourceName);
6697 path = path || `/${resourceName}`;
6698 only = only || [];
6699 except = except || [];
6700
6701 if (only.length > 0 && except.length > 0) {
6702 throw "cannot use both :only and :except options";
6703 }
6704
6705 let actionsMethodsAndsPathsMappings = {
6706 index: {
6707 methods: ["get"],
6708 path: `${path}`
6709 },
6710 show: {
6711 methods: ["get"],
6712 path: `${path}/:id`
6713 },
6714 create: {
6715 methods: ["post"],
6716 path: `${path}`
6717 },
6718 update: {
6719 methods: ["put", "patch"],
6720 path: `${path}/:id`
6721 },
6722 delete: {
6723 methods: ["del"],
6724 path: `${path}/:id`
6725 }
6726 };
6727 let allActions = Object.keys(actionsMethodsAndsPathsMappings);
6728 let actions = only.length > 0 && only || except.length > 0 && allActions.filter(action => except.indexOf(action) === -1) || allActions;
6729 actions.forEach(action => {
6730 let methodsWithPath = actionsMethodsAndsPathsMappings[action];
6731 methodsWithPath.methods.forEach(method => {
6732 return path === resourceName ? this[method](methodsWithPath.path) : this[method](methodsWithPath.path, resourceName);
6733 });
6734 });
6735 }
6736 /**
6737 *
6738 * @private
6739 * @hide
6740 */
6741
6742
6743 _defineRouteHandlerHelpers() {
6744 [["get"], ["post"], ["put"], ["delete", "del"], ["patch"], ["head"], ["options"]].forEach(([verb, alias]) => {
6745 this[verb] = (path, ...args) => {
6746 let [rawHandler, customizedCode, options] = extractRouteArguments(args);
6747 return this._registerRouteHandler(verb, path, rawHandler, customizedCode, options);
6748 };
6749
6750 if (alias) {
6751 this[alias] = this[verb];
6752 }
6753 });
6754 }
6755
6756 _serialize(body) {
6757 if (typeof body === "string") {
6758 return body;
6759 } else {
6760 return JSON.stringify(body);
6761 }
6762 }
6763
6764 _registerRouteHandler(verb, path, rawHandler, customizedCode, options) {
6765 let routeHandler = this._container.create("RouteHandler", {
6766 schema: this.schema,
6767 verb,
6768 rawHandler,
6769 customizedCode,
6770 options,
6771 path,
6772 serializerOrRegistry: this.serializerOrRegistry
6773 });
6774
6775 let fullPath = this._getFullPath(path);
6776
6777 let timing = options.timing !== undefined ? options.timing : () => this.timing;
6778
6779 if (this.pretender) {
6780 return this.pretender[verb](fullPath, request => {
6781 return routeHandler.handle(request).then(mirageResponse => {
6782 let [code, headers, response] = mirageResponse;
6783 return [code, headers, this._serialize(response)];
6784 });
6785 }, timing);
6786 }
6787 }
6788 /**
6789 *
6790 * @private
6791 * @hide
6792 */
6793
6794
6795 _hasModulesOfType(modules, type) {
6796 let modulesOfType = modules[type];
6797 return modulesOfType ? Object.keys(modulesOfType).length > 0 : false;
6798 }
6799 /**
6800 * Builds a full path for Pretender to monitor based on the `path` and
6801 * configured options (`urlPrefix` and `namespace`).
6802 *
6803 * @private
6804 * @hide
6805 */
6806
6807
6808 _getFullPath(path) {
6809 path = path[0] === "/" ? path.slice(1) : path;
6810 let fullPath = "";
6811 let urlPrefix = this.urlPrefix ? this.urlPrefix.trim() : "";
6812 let namespace = ""; // if there is a urlPrefix and a namespace
6813
6814 if (this.urlPrefix && this.namespace) {
6815 if (this.namespace[0] === "/" && this.namespace[this.namespace.length - 1] === "/") {
6816 namespace = this.namespace.substring(0, this.namespace.length - 1).substring(1);
6817 }
6818
6819 if (this.namespace[0] === "/" && this.namespace[this.namespace.length - 1] !== "/") {
6820 namespace = this.namespace.substring(1);
6821 }
6822
6823 if (this.namespace[0] !== "/" && this.namespace[this.namespace.length - 1] === "/") {
6824 namespace = this.namespace.substring(0, this.namespace.length - 1);
6825 }
6826
6827 if (this.namespace[0] !== "/" && this.namespace[this.namespace.length - 1] !== "/") {
6828 namespace = this.namespace;
6829 }
6830 } // if there is a namespace and no urlPrefix
6831
6832
6833 if (this.namespace && !this.urlPrefix) {
6834 if (this.namespace[0] === "/" && this.namespace[this.namespace.length - 1] === "/") {
6835 namespace = this.namespace.substring(0, this.namespace.length - 1);
6836 }
6837
6838 if (this.namespace[0] === "/" && this.namespace[this.namespace.length - 1] !== "/") {
6839 namespace = this.namespace;
6840 }
6841
6842 if (this.namespace[0] !== "/" && this.namespace[this.namespace.length - 1] === "/") {
6843 let namespaceSub = this.namespace.substring(0, this.namespace.length - 1);
6844 namespace = `/${namespaceSub}`;
6845 }
6846
6847 if (this.namespace[0] !== "/" && this.namespace[this.namespace.length - 1] !== "/") {
6848 namespace = `/${this.namespace}`;
6849 }
6850 } // if no namespace
6851
6852
6853 if (!this.namespace) {
6854 namespace = "";
6855 } // check to see if path is a FQDN. if so, ignore any urlPrefix/namespace that was set
6856
6857
6858 if (/^https?:\/\//.test(path)) {
6859 fullPath += path;
6860 } else {
6861 // otherwise, if there is a urlPrefix, use that as the beginning of the path
6862 if (urlPrefix.length) {
6863 fullPath += urlPrefix[urlPrefix.length - 1] === "/" ? urlPrefix : `${urlPrefix}/`;
6864 } // add the namespace to the path
6865
6866
6867 fullPath += namespace; // add a trailing slash to the path if it doesn't already contain one
6868
6869 if (fullPath[fullPath.length - 1] !== "/") {
6870 fullPath += "/";
6871 } // finally add the configured path
6872
6873
6874 fullPath += path; // if we're making a same-origin request, ensure a / is prepended and
6875 // dedup any double slashes
6876
6877 if (!/^https?:\/\//.test(fullPath)) {
6878 fullPath = `/${fullPath}`;
6879 fullPath = fullPath.replace(/\/+/g, "/");
6880 }
6881 }
6882
6883 return fullPath;
6884 }
6885 /**
6886 *
6887 * @private
6888 * @hide
6889 */
6890
6891
6892 _configureDefaultPassthroughs() {
6893 defaultPassthroughs.forEach(passthroughUrl => {
6894 this.passthrough(passthroughUrl);
6895 });
6896 }
6897 /**
6898 *
6899 * @private
6900 * @hide
6901 */
6902
6903
6904 _typeIsPluralForModel(typeOrCollectionName) {
6905 if (typeof isPluralForModelCache[typeOrCollectionName] !== "boolean") {
6906 let modelOrFactoryExists = this._modelOrFactoryExistsForTypeOrCollectionName(typeOrCollectionName);
6907
6908 let isPlural = typeOrCollectionName === this.inflector.pluralize(typeOrCollectionName);
6909 let isUncountable = this.inflector.singularize(typeOrCollectionName) === this.inflector.pluralize(typeOrCollectionName);
6910 const isPluralForModel = isPlural && !isUncountable && modelOrFactoryExists;
6911 isPluralForModelCache[typeOrCollectionName] = isPluralForModel;
6912 }
6913
6914 return isPluralForModelCache[typeOrCollectionName];
6915 }
6916 /**
6917 *
6918 * @private
6919 * @hide
6920 */
6921
6922
6923 _modelOrFactoryExistsForType(type) {
6924 let modelExists = this.schema && this.schema.modelFor(camelize(type));
6925 let dbCollectionExists = this.db[this.schema.toInternalCollectionName(type)];
6926 return (modelExists || dbCollectionExists) && !this._typeIsPluralForModel(type);
6927 }
6928 /**
6929 *
6930 * @private
6931 * @hide
6932 */
6933
6934
6935 _modelOrFactoryExistsForTypeOrCollectionName(typeOrCollectionName) {
6936 let modelExists = this.schema && this.schema.modelFor(camelize(typeOrCollectionName));
6937 let dbCollectionExists = this.db[this.schema.toInternalCollectionName(typeOrCollectionName)];
6938 return modelExists || dbCollectionExists;
6939 }
6940 /**
6941 *
6942 * @private
6943 * @hide
6944 */
6945
6946
6947 _validateTraits(traits, factory, type) {
6948 traits.forEach(traitName => {
6949 if (!factory.isTrait(traitName)) {
6950 throw new Error(`'${traitName}' trait is not registered in '${type}' factory`);
6951 }
6952 });
6953 }
6954 /**
6955 *
6956 * @private
6957 * @hide
6958 */
6959
6960
6961 _mergeExtensions(attrs, traits, overrides) {
6962 let allExtensions = traits.map(traitName => {
6963 return attrs[traitName].extension;
6964 });
6965 allExtensions.push(overrides || {});
6966 return allExtensions.reduce((accum, extension) => {
6967 return assign(accum, extension);
6968 }, {});
6969 }
6970 /**
6971 *
6972 * @private
6973 * @hide
6974 */
6975
6976
6977 _mapAssociationsFromAttributes(modelName, attributes, overrides = {}) {
6978 Object.keys(attributes || {}).filter(attr => {
6979 return isAssociation(attributes[attr]);
6980 }).forEach(attr => {
6981 let modelClass = this.schema.modelClassFor(modelName);
6982 let association = modelClass.associationFor(attr);
6983 assert(association && association instanceof BelongsTo, `You're using the \`association\` factory helper on the '${attr}' attribute of your ${modelName} factory, but that attribute is not a \`belongsTo\` association.`);
6984 let isSelfReferentialBelongsTo = association && association instanceof BelongsTo && association.modelName === modelName;
6985 assert(!isSelfReferentialBelongsTo, `You're using the association() helper on your ${modelName} factory for ${attr}, which is a belongsTo self-referential relationship. You can't do this as it will lead to infinite recursion. You can move the helper inside of a trait and use it selectively.`);
6986 let isPolymorphic = association && association.opts && association.opts.polymorphic;
6987 assert(!isPolymorphic, `You're using the association() helper on your ${modelName} factory for ${attr}, which is a polymorphic relationship. This is not currently supported.`);
6988 let factoryAssociation = attributes[attr];
6989 let foreignKey = `${camelize(attr)}Id`;
6990
6991 if (!overrides[attr]) {
6992 attributes[foreignKey] = this.create(association.modelName, ...factoryAssociation.traitsAndOverrides).id;
6993 }
6994
6995 delete attributes[attr];
6996 });
6997 }
6998
6999}
7000
7001var ActiveModelSerializer = Serializer.extend({
7002 serializeIds: "always",
7003 normalizeIds: true,
7004
7005 keyForModel(type) {
7006 return underscore(type);
7007 },
7008
7009 keyForAttribute(attr) {
7010 return underscore(attr);
7011 },
7012
7013 keyForRelationship(type) {
7014 return this._container.inflector.pluralize(underscore(type));
7015 },
7016
7017 keyForEmbeddedRelationship(attributeName) {
7018 return underscore(attributeName);
7019 },
7020
7021 keyForRelationshipIds(type) {
7022 return `${underscore(this._container.inflector.singularize(type))}_ids`;
7023 },
7024
7025 keyForForeignKey(relationshipName) {
7026 return `${underscore(relationshipName)}_id`;
7027 },
7028
7029 keyForPolymorphicForeignKeyId(relationshipName) {
7030 return `${underscore(relationshipName)}_id`;
7031 },
7032
7033 keyForPolymorphicForeignKeyType(relationshipName) {
7034 return `${underscore(relationshipName)}_type`;
7035 },
7036
7037 normalize(payload) {
7038 let type = Object.keys(payload)[0];
7039 let attrs = payload[type];
7040 let modelName = camelize(type);
7041 let modelClass = this.schema.modelClassFor(modelName);
7042 let {
7043 belongsToAssociations,
7044 hasManyAssociations
7045 } = modelClass;
7046 let belongsToKeys = Object.keys(belongsToAssociations);
7047 let hasManyKeys = Object.keys(hasManyAssociations);
7048 let jsonApiPayload = {
7049 data: {
7050 type: this._container.inflector.pluralize(type),
7051 attributes: {}
7052 }
7053 };
7054
7055 if (attrs.id) {
7056 jsonApiPayload.data.id = attrs.id;
7057 }
7058
7059 let relationships = {};
7060 Object.keys(attrs).forEach(key => {
7061 if (key !== "id") {
7062 if (this.normalizeIds) {
7063 if (belongsToKeys.includes(key)) {
7064 let association = belongsToAssociations[key];
7065 let associationModel = association.modelName;
7066 relationships[dasherize(key)] = {
7067 data: {
7068 type: associationModel,
7069 id: attrs[key]
7070 }
7071 };
7072 } else if (hasManyKeys.includes(key)) {
7073 let association = hasManyAssociations[key];
7074 let associationModel = association.modelName;
7075 let data = attrs[key].map(id => {
7076 return {
7077 type: associationModel,
7078 id
7079 };
7080 });
7081 relationships[dasherize(key)] = {
7082 data
7083 };
7084 } else {
7085 jsonApiPayload.data.attributes[dasherize(key)] = attrs[key];
7086 }
7087 } else {
7088 jsonApiPayload.data.attributes[dasherize(key)] = attrs[key];
7089 }
7090 }
7091 });
7092
7093 if (Object.keys(relationships).length) {
7094 jsonApiPayload.data.relationships = relationships;
7095 }
7096
7097 return jsonApiPayload;
7098 },
7099
7100 getCoalescedIds(request) {
7101 return request.queryParams && request.queryParams.ids;
7102 }
7103
7104});
7105
7106var restSerializer = ActiveModelSerializer.extend({
7107 serializeIds: "always",
7108
7109 keyForModel(type) {
7110 return camelize(type);
7111 },
7112
7113 keyForAttribute(attr) {
7114 return camelize(attr);
7115 },
7116
7117 keyForRelationship(type) {
7118 return camelize(this._container.inflector.pluralize(type));
7119 },
7120
7121 keyForEmbeddedRelationship(attributeName) {
7122 return camelize(attributeName);
7123 },
7124
7125 keyForRelationshipIds(type) {
7126 return camelize(this._container.inflector.pluralize(type));
7127 },
7128
7129 keyForForeignKey(relationshipName) {
7130 return camelize(this._container.inflector.singularize(relationshipName));
7131 },
7132
7133 getCoalescedIds(request) {
7134 return request.queryParams && request.queryParams.ids;
7135 }
7136
7137});
7138
7139/**
7140 UUID generator
7141
7142 @hide
7143*/
7144function uuid () {
7145 return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
7146 let r = Math.random() * 16 | 0;
7147 let v = c === "x" ? r : r & 0x3 | 0x8;
7148 return v.toString(16);
7149 });
7150}
7151
7152/**
7153 @hide
7154*/
7155
7156function hasMany(...args) {
7157 return new HasMany(...args);
7158}
7159/**
7160 @hide
7161*/
7162
7163
7164function belongsTo(...args) {
7165 return new BelongsTo(...args);
7166}
7167var index = {
7168 Factory,
7169 Response,
7170 hasMany,
7171 belongsTo
7172};
7173
7174exports.ActiveModelSerializer = ActiveModelSerializer;
7175exports.Collection = Collection;
7176exports.Factory = Factory;
7177exports.IdentityManager = IdentityManager;
7178exports.JSONAPISerializer = JSONAPISerializer;
7179exports.Model = Model;
7180exports.Response = Response;
7181exports.RestSerializer = restSerializer;
7182exports.Serializer = Serializer;
7183exports.Server = Server;
7184exports._Db = Db;
7185exports._DbCollection = DbCollection;
7186exports._RouteHandler = RouteHandler;
7187exports._SerializerRegistry = SerializerRegistry;
7188exports._assert = assert;
7189exports._ormAssociationsAssociation = Association;
7190exports._ormAssociationsBelongsTo = BelongsTo;
7191exports._ormAssociationsHasMany = HasMany;
7192exports._ormPolymorphicCollection = PolymorphicCollection;
7193exports._ormSchema = Schema;
7194exports._routeHandlersBase = BaseRouteHandler;
7195exports._routeHandlersFunction = FunctionRouteHandler;
7196exports._routeHandlersObject = ObjectRouteHandler;
7197exports._routeHandlersShorthandsBase = BaseShorthandRouteHandler;
7198exports._routeHandlersShorthandsDelete = DeleteShorthandRouteHandler;
7199exports._routeHandlersShorthandsGet = GetShorthandRouteHandler;
7200exports._routeHandlersShorthandsHead = HeadShorthandRouteHandler;
7201exports._routeHandlersShorthandsPost = PostShorthandRouteHandler;
7202exports._routeHandlersShorthandsPut = PutShorthandRouteHandler;
7203exports._utilsExtend = extend;
7204exports._utilsInflectorCamelize = camelize;
7205exports._utilsInflectorCapitalize = capitalize;
7206exports._utilsInflectorDasherize = dasherize;
7207exports._utilsInflectorUnderscore = underscore;
7208exports._utilsIsAssociation = isAssociation;
7209exports._utilsReferenceSort = referenceSort;
7210exports._utilsUuid = uuid;
7211exports.association = association;
7212exports.belongsTo = belongsTo;
7213exports.default = index;
7214exports.defaultPassthroughs = defaultPassthroughs;
7215exports.hasMany = hasMany;
7216exports.trait = trait;
7217//# sourceMappingURL=mirage-cjs.js.map