UNPKG

miragejs

Version:

A client-side server to help you build, test and demo your JavaScript app

1,797 lines (1,517 loc) 271 kB
import isPlainObject from 'lodash/isPlainObject'; import isFunction from 'lodash/isFunction'; import mapValues from 'lodash/mapValues'; import uniq from 'lodash/uniq'; import flatten from 'lodash/flatten'; import { camelize as camelize$1, dasherize as dasherize$1, underscore as underscore$1, capitalize as capitalize$1, singularize, pluralize } from 'inflected'; import lowerFirst from 'lodash/lowerFirst'; import isEqual from 'lodash/isEqual'; import map from 'lodash/map'; import cloneDeep from 'lodash/cloneDeep'; import invokeMap from 'lodash/invokeMap'; import compact from 'lodash/compact'; import has from 'lodash/has'; import values from 'lodash/values'; import isEmpty from 'lodash/isEmpty'; import get from 'lodash/get'; import uniqBy from 'lodash/uniqBy'; import forIn from 'lodash/forIn'; import pick from 'lodash/pick'; import assign from 'lodash/assign'; import find from 'lodash/find'; import isInteger from 'lodash/isInteger'; import '@miragejs/pretender-node-polyfill/before'; import Pretender from 'pretender'; import '@miragejs/pretender-node-polyfill/after'; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } // jscs:disable disallowVar, requireArrayDestructuring /** @hide */ function referenceSort (edges) { var nodes = uniq(flatten(edges)); var cursor = nodes.length; var sorted = new Array(cursor); var visited = {}; var i = cursor; var visit = function visit(node, i, predecessors) { if (predecessors.indexOf(node) >= 0) { throw new Error("Cyclic dependency in properties ".concat(JSON.stringify(predecessors))); } if (visited[i]) { return; } else { visited[i] = true; } var outgoing = edges.filter(function (edge) { return edge && edge[0] === node; }); i = outgoing.length; if (i) { var preds = predecessors.concat(node); do { var pair = outgoing[--i]; var child = pair[1]; if (child) { visit(child, nodes.indexOf(child), preds); } } while (i); } sorted[--cursor] = node; }; while (i--) { if (!visited[i]) { visit(nodes[i], i, []); } } return sorted.reverse(); } var Factory = function Factory() { this.build = function (sequence) { var _this = this; var object = {}; var topLevelAttrs = Object.assign({}, this.attrs); delete topLevelAttrs.afterCreate; Object.keys(topLevelAttrs).forEach(function (attr) { if (Factory.isTrait.call(_this, attr)) { delete topLevelAttrs[attr]; } }); var keys = sortAttrs(topLevelAttrs, sequence); keys.forEach(function (key) { var buildAttrs, _buildSingleValue; buildAttrs = function buildAttrs(attrs) { return mapValues(attrs, _buildSingleValue); }; _buildSingleValue = function buildSingleValue(value) { if (Array.isArray(value)) { return value.map(_buildSingleValue); } else if (isPlainObject(value)) { return buildAttrs(value); } else if (isFunction(value)) { return value.call(topLevelAttrs, sequence); } else { return value; } }; var value = topLevelAttrs[key]; if (isFunction(value)) { object[key] = value.call(object, sequence); } else { object[key] = _buildSingleValue(value); } }); return object; }; }; Factory.extend = function (attrs) { // Merge the new attributes with existing ones. If conflict, new ones win. var newAttrs = Object.assign({}, this.attrs, attrs); var Subclass = function Subclass() { this.attrs = newAttrs; Factory.call(this); }; // Copy extend Subclass.extend = Factory.extend; Subclass.extractAfterCreateCallbacks = Factory.extractAfterCreateCallbacks; Subclass.isTrait = Factory.isTrait; // Store a reference on the class for future subclasses Subclass.attrs = newAttrs; return Subclass; }; Factory.extractAfterCreateCallbacks = function () { var _this2 = this; var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, traits = _ref.traits; var afterCreateCallbacks = []; var attrs = this.attrs || {}; var traitCandidates; if (attrs.afterCreate) { afterCreateCallbacks.push(attrs.afterCreate); } if (Array.isArray(traits)) { traitCandidates = traits; } else { traitCandidates = Object.keys(attrs); } traitCandidates.filter(function (attr) { return _this2.isTrait(attr) && attrs[attr].extension.afterCreate; }).forEach(function (attr) { afterCreateCallbacks.push(attrs[attr].extension.afterCreate); }); return afterCreateCallbacks; }; Factory.isTrait = function (attrName) { var attrs = this.attrs; return isPlainObject(attrs[attrName]) && attrs[attrName].__isTrait__ === true; }; function sortAttrs(attrs, sequence) { var Temp = function Temp() {}; var obj = new Temp(); var refs = []; var property; Object.keys(attrs).forEach(function (key) { var value; Object.defineProperty(obj.constructor.prototype, key, { get: function get() { refs.push([property, key]); return value; }, set: function set(newValue) { value = newValue; }, enumerable: false, configurable: true }); }); Object.keys(attrs).forEach(function (key) { var value = attrs[key]; if (typeof value !== "function") { obj[key] = value; } }); Object.keys(attrs).forEach(function (key) { var value = attrs[key]; property = key; if (typeof value === "function") { obj[key] = value.call(obj, sequence); } refs.push([key]); }); return referenceSort(refs); } function isNumber(n) { return (+n).toString() === n.toString(); } /** 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. 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. A custom IdentityManager must implement these methods: - `fetch`, which must return an identifier not yet used - `set`, which is called with an `id` of a record being insert into Mirage's database - `reset`, which should reset database to initial state Check out the advanced guide on Mocking UUIDs to see a complete example of a custom IdentityManager. @class IdentityManager @constructor @public */ var IdentityManager = /*#__PURE__*/function () { function IdentityManager() { _classCallCheck(this, IdentityManager); this._nextId = 1; this._ids = {}; } /** @method get @hide @private */ _createClass(IdentityManager, [{ key: "get", value: function get() { return this._nextId; } /** Registers `uniqueIdentifier` as used. This method should throw is `uniqueIdentifier` has already been taken. @method set @param {String|Number} uniqueIdentifier @public */ }, { key: "set", value: function set(uniqueIdentifier) { if (this._ids[uniqueIdentifier]) { throw new Error("Attempting to use the ID ".concat(uniqueIdentifier, ", but it's already been used")); } if (isNumber(uniqueIdentifier) && +uniqueIdentifier >= this._nextId) { this._nextId = +uniqueIdentifier + 1; } this._ids[uniqueIdentifier] = true; } /** @method inc @hide @private */ }, { key: "inc", value: function inc() { var nextValue = this.get() + 1; this._nextId = nextValue; return nextValue; } /** Returns the next unique identifier. @method fetch @return {String} Unique identifier @public */ }, { key: "fetch", value: function fetch() { var id = this.get(); this._ids[id] = true; this.inc(); return id.toString(); } /** Resets the identity manager, marking all unique identifiers as available. @method reset @public */ }, { key: "reset", value: function reset() { this._nextId = 1; this._ids = {}; } }]); return IdentityManager; }(); /** @hide */ var association = function association() { var __isAssociation__ = true; for (var _len = arguments.length, traitsAndOverrides = new Array(_len), _key = 0; _key < _len; _key++) { traitsAndOverrides[_key] = arguments[_key]; } return { __isAssociation__: __isAssociation__, traitsAndOverrides: traitsAndOverrides }; }; var trait = function trait(extension) { var __isTrait__ = true; return { extension: extension, __isTrait__: __isTrait__ }; }; var warn = console.warn; // eslint-disable-line no-console /** You can use this class when you want more control over your route handlers response. Pass the `code`, `headers` and `data` into the constructor and return an instance from any route handler. ```js import { Response } from 'miragejs'; this.get('/users', () => { return new Response(400, { some: 'header' }, { errors: [ 'name cannot be blank'] }); }); ``` */ var Response = /*#__PURE__*/function () { function Response(code) { var headers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var data = arguments.length > 2 ? arguments[2] : undefined; _classCallCheck(this, Response); this.code = code; this.headers = headers; // Default data for "undefined 204" responses to empty string (no content) if (code === 204) { if (data !== undefined && data !== "") { warn("Mirage: One of your route handlers is returning a custom\n 204 Response that has data, but this is a violation of the HTTP spec\n and could lead to unexpected behavior. 204 responses should have no\n content (an empty string) as their body."); } else { this.data = ""; } // Default data for "empty untyped" responses to empty JSON object } else if ((data === undefined || data === "") && !Object.prototype.hasOwnProperty.call(this.headers, "Content-Type")) { this.data = {}; } else { this.data = data; } // Default "untyped" responses to application/json if (code !== 204 && !Object.prototype.hasOwnProperty.call(this.headers, "Content-Type")) { this.headers["Content-Type"] = "application/json"; } } _createClass(Response, [{ key: "toRackResponse", value: function toRackResponse() { return [this.code, this.headers, this.data]; } }]); return Response; }(); var camelizeCache = {}; var dasherizeCache = {}; var underscoreCache = {}; var capitalizeCache = {}; /** * @param {String} word * @hide */ function camelize(word) { if (typeof camelizeCache[word] !== "string") { var camelizedWord = camelize$1(underscore(word), false); /* The `ember-inflector` package's version of camelize lower-cases the first word after a slash, e.g. camelize('my-things/nice-watch'); // 'myThings/niceWatch' The `inflected` package doesn't, so we make that change here to not break existing functionality. (This affects the name of the schema collections.) */ var camelized = camelizedWord.split("/").map(lowerFirst).join("/"); camelizeCache[word] = camelized; } return camelizeCache[word]; } /** * @param {String} word * @hide */ function dasherize(word) { if (typeof dasherizeCache[word] !== "string") { var dasherized = dasherize$1(underscore(word)); dasherizeCache[word] = dasherized; } return dasherizeCache[word]; } function underscore(word) { if (typeof underscoreCache[word] !== "string") { var underscored = underscore$1(word); underscoreCache[word] = underscored; } return underscoreCache[word]; } function capitalize(word) { if (typeof capitalizeCache[word] !== "string") { var capitalized = capitalize$1(word); capitalizeCache[word] = capitalized; } return capitalizeCache[word]; } /** @hide */ function isAssociation (object) { return isPlainObject(object) && object.__isAssociation__ === true; } /* eslint no-console: 0 */ var errorProps = ["description", "fileName", "lineNumber", "message", "name", "number", "stack"]; /** @hide */ function assert(bool, text) { if (typeof bool === "string" && !text) { // console.error(`Mirage: ${bool}`); throw new MirageError(bool); } if (!bool) { // console.error(`Mirage: ${text}`); throw new MirageError(text.replace(/^ +/gm, "") || "Assertion failed"); } } /** @public @hide Copied from ember-metal/error */ function MirageError(message, stack) { var tmp = Error(message); if (stack) { tmp.stack = stack; } for (var idx = 0; idx < errorProps.length; idx++) { var prop = errorProps[idx]; if (["description", "message", "stack"].indexOf(prop) > -1) { this[prop] = "Mirage: ".concat(tmp[prop]); } else { this[prop] = tmp[prop]; } } } MirageError.prototype = Object.create(Error.prototype); /** Associations represent relationships between your Models. The `hasMany` and `belongsTo` helpers are how you actually define relationships: ```js import { createServer, Model, hasMany, belongsTo } createServer({ models: { user: Model.extend({ comments: hasMany() }), comments: Model.extend({ user: belongsTo() }) } }) ``` View [the Relationships](https://miragejs.com/docs/main-concepts/relationships/) guide to learn more about setting up relationships. Each usage of the helper registers an Association (either a `HasMany` association or `BelongsTo` association) with your server's `Schema`. You can access these associations using either the `schema.associationsFor()` method, or the `associations` property on individual model instances. You can then introspect the associations to do things like dynamically build up your JSON response in your serializers. @class Association @constructor @public */ var Association = /*#__PURE__*/function () { function Association(modelName, opts) { _classCallCheck(this, Association); /** The modelName of the associated model. For example, given this configuration ```js createServer({ models: { user: Model, comment: Model.extend({ user: belongsTo() }) } }) ``` the association's `modelName` would be `user`. Note that an association's `modelName` and the `name` can be different. This is because Mirage supports multiple relationships of the same type: ```js createServer({ models: { user: Model, comment: Model.extend({ author: belongsTo('user'), reviewer: belongsTo('user') }) } }) ``` For both these relationships, the `modelName` is `user`, but the first association has a `name` of `author` while the second has a `name` of `reviewer`. @property @type {String} @public */ this.modelName = undefined; // hack to add ESDOC info. Any better way? if (_typeof(modelName) === "object") { // Received opts only this.modelName = undefined; this.opts = modelName; } else { // The modelName of the association. (Might not be passed in - set later // by schema). this.modelName = modelName ? dasherize(modelName) : ""; this.opts = opts || {}; } /** The name of the association, which comes from the property name that was used to define it. For example, given this server definition ```js createServer({ models: { user: Model, comment: Model.extend({ author: belongsTo('user') }) } }) ``` the association's `name` would be `author`. The name is used by Mirage to define foreign keys on the model (`comment.authorId` in this case), among other things. @property @type {String} @public */ this.name = ""; // The modelName that owns this association this.ownerModelName = ""; } /** A setter for schema, since we don't have a reference at constuction time. @method setSchema @public @hide */ _createClass(Association, [{ key: "setSchema", value: function setSchema(schema) { this.schema = schema; } /** Returns a Boolean that's true if the association is self-referential, i.e. if a model has an association with itself. For example, given ```js createServer({ models: { user: Model.extend({ friends: hasMany('user') }) } }) ``` then ```js server.schema.associationsFor('user').friends.isReflexive // true ``` @method isReflexive @return {Boolean} @public */ }, { key: "isReflexive", value: function isReflexive() { var isExplicitReflexive = !!(this.modelName === this.ownerModelName && this.opts.inverse); var isImplicitReflexive = !!(this.opts.inverse === undefined && this.ownerModelName === this.modelName); return isExplicitReflexive || isImplicitReflexive; } /** Returns a Boolean that's true if the association is polymorphic: For example, given ```js createServer({ models: { comment: Model.extend({ commentable: belongsTo({ polymorphic: true }) }) } }) ``` then ```js server.schema.associationsFor('comment').commentable.isPolymorphic // true ``` Check out [the guides on polymorphic associations](https://miragejs.com/docs/main-concepts/relationships/#polymorphic) to learn more. @accessor isPolymorphic @type {Boolean} @public */ }, { key: "isPolymorphic", get: function get() { return this.opts.polymorphic; } /** Returns either the string `"hasMany"` or `"belongsTo"`, based on the association type. @accessor @type {String} @public */ }, { key: "type", get: function get() { throw new Error("Subclasses of Association must implement a getter for type"); } /** Returns the name used for the association's foreign key. ```js let server = createServer({ models: { user: Model, post: Model.extend({ fineAuthor: belongsTo("user"), comments: hasMany() }), comment: Model } }); let associations = server.associationsFor('post') associations.fineAuthor.foreignKey // fineAuthorId associations.comments.foreignKey // commentIds ``` @accessor @type {String} @public */ }, { key: "foreignKey", get: function get() { return this.getForeignKey(); } /** @hide */ }, { key: "identifier", get: function get() { throw new Error("Subclasses of Association must implement a getter for identifier"); } }]); return Association; }(); var identifierCache$1 = {}; /** * The belongsTo association adds a fk to the owner of the association * * @class BelongsTo * @extends Association * @constructor * @public * @hide */ var BelongsTo = /*#__PURE__*/function (_Association) { _inherits(BelongsTo, _Association); var _super = _createSuper(BelongsTo); function BelongsTo() { _classCallCheck(this, BelongsTo); return _super.apply(this, arguments); } _createClass(BelongsTo, [{ key: "identifier", get: function get() { if (typeof identifierCache$1[this.name] !== "string") { var identifier = "".concat(camelize(this.name), "Id"); identifierCache$1[this.name] = identifier; } return identifierCache$1[this.name]; } }, { key: "type", get: function get() { return "belongsTo"; } /** * @method getForeignKeyArray * @return {Array} Array of camelized name of the model owning the association * and foreign key for the association * @public */ }, { key: "getForeignKeyArray", value: function getForeignKeyArray() { return [camelize(this.ownerModelName), this.getForeignKey()]; } /** * @method getForeignKey * @return {String} Foreign key for the association * @public */ }, { key: "getForeignKey", value: function getForeignKey() { // we reuse identifierCache because it's the same logic as get identifier if (typeof identifierCache$1[this.name] !== "string") { var foreignKey = "".concat(camelize(this.name), "Id"); identifierCache$1[this.name] = foreignKey; } return identifierCache$1[this.name]; } /** * Registers belongs-to association defined by given key on given model, * defines getters / setters for associated parent and associated parent's id, * adds methods for creating unsaved parent record and creating a saved one * * @method addMethodsToModelClass * @param {Function} ModelClass * @param {String} key the named key for the association * @public */ }, { key: "addMethodsToModelClass", value: function addMethodsToModelClass(ModelClass, key) { var modelPrototype = ModelClass.prototype; var association = this; var foreignKey = this.getForeignKey(); var associationHash = _defineProperty({}, key, this); modelPrototype.belongsToAssociations = Object.assign(modelPrototype.belongsToAssociations, associationHash); // update belongsToAssociationFks Object.keys(modelPrototype.belongsToAssociations).forEach(function (key) { var value = modelPrototype.belongsToAssociations[key]; modelPrototype.belongsToAssociationFks[value.getForeignKey()] = value; }); // Add to target's dependent associations array this.schema.addDependentAssociation(this, this.modelName); // TODO: look how this is used. Are these necessary, seems like they could be gotten from the above? // Or we could use a single data structure to store this information? modelPrototype.associationKeys.add(key); modelPrototype.associationIdKeys.add(foreignKey); Object.defineProperty(modelPrototype, foreignKey, { /* object.parentId - returns the associated parent's id */ get: function get() { this._tempAssociations = this._tempAssociations || {}; var tempParent = this._tempAssociations[key]; var id; if (tempParent === null) { id = null; } else { if (association.isPolymorphic) { if (tempParent) { id = { id: tempParent.id, type: tempParent.modelName }; } else { id = this.attrs[foreignKey]; } } else { if (tempParent) { id = tempParent.id; } else { id = this.attrs[foreignKey]; } } } return id; }, /* object.parentId = (parentId) - sets the associated parent via id */ set: function set(id) { var tempParent; if (id === null) { tempParent = null; } else if (id !== undefined) { if (association.isPolymorphic) { assert(_typeof(id) === "object", "You're setting an ID on the polymorphic association '".concat(association.name, "' but you didn't pass in an object. Polymorphic IDs need to be in the form { type, id }.")); tempParent = association.schema[association.schema.toCollectionName(id.type)].find(id.id); } else { tempParent = association.schema[association.schema.toCollectionName(association.modelName)].find(id); assert(tempParent, "Couldn't find ".concat(association.modelName, " with id = ").concat(id)); } } this[key] = tempParent; } }); Object.defineProperty(modelPrototype, key, { /* object.parent - returns the associated parent */ get: function get() { this._tempAssociations = this._tempAssociations || {}; var tempParent = this._tempAssociations[key]; var foreignKeyId = this[foreignKey]; var model = null; if (tempParent) { model = tempParent; } else if (foreignKeyId !== null) { if (association.isPolymorphic) { model = association.schema[association.schema.toCollectionName(foreignKeyId.type)].find(foreignKeyId.id); } else { model = association.schema[association.schema.toCollectionName(association.modelName)].find(foreignKeyId); } } return model; }, /* object.parent = (parentModel) - sets the associated parent via model I want to jot some notes about hasInverseFor. There used to be an association.inverse() check, but adding polymorphic associations complicated this. `comment.commentable`, you can't easily check for an inverse since `comments: hasMany()` could be on any model. Instead of making it very complex and looking for an inverse on the association in isoaltion, it was much simpler to ask the model being passed in if it had an inverse for the setting model and with its association. */ set: function set(model) { this._tempAssociations = this._tempAssociations || {}; this._tempAssociations[key] = model; if (model && model.hasInverseFor(association)) { var inverse = model.inverseFor(association); model.associate(this, inverse); } } }); /* object.newParent - creates a new unsaved associated parent TODO: document polymorphic */ modelPrototype["new".concat(capitalize(key))] = function () { var modelName, attrs; if (association.isPolymorphic) { modelName = arguments.length <= 0 ? undefined : arguments[0]; attrs = arguments.length <= 1 ? undefined : arguments[1]; } else { modelName = association.modelName; attrs = arguments.length <= 0 ? undefined : arguments[0]; } var parent = association.schema[association.schema.toCollectionName(modelName)]["new"](attrs); this[key] = parent; return parent; }; /* object.createParent - creates a new saved associated parent, and immediately persists both models TODO: document polymorphic */ modelPrototype["create".concat(capitalize(key))] = function () { var modelName, attrs; if (association.isPolymorphic) { modelName = arguments.length <= 0 ? undefined : arguments[0]; attrs = arguments.length <= 1 ? undefined : arguments[1]; } else { modelName = association.modelName; attrs = arguments.length <= 0 ? undefined : arguments[0]; } var parent = association.schema[association.schema.toCollectionName(modelName)].create(attrs); this[key] = parent; this.save(); return parent.reload(); }; } /** * * * @public */ }, { key: "disassociateAllDependentsFromTarget", value: function disassociateAllDependentsFromTarget(model) { var _this = this; var owner = this.ownerModelName; var fk; if (this.isPolymorphic) { fk = { type: model.modelName, id: model.id }; } else { fk = model.id; } var dependents = this.schema[this.schema.toCollectionName(owner)].where(function (potentialOwner) { var id = potentialOwner[_this.getForeignKey()]; if (!id) { return false; } if (_typeof(id) === "object") { return id.type === fk.type && id.id === fk.id; } else { return id === fk; } }); dependents.models.forEach(function (dependent) { dependent.disassociate(model, _this); dependent.save(); }); } }]); return BelongsTo; }(Association); function duplicate(data) { if (Array.isArray(data)) { return data.map(duplicate); } else { return Object.assign({}, data); } } /** 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`. `DbCollections` have names, like `users`, which you use to access the collection from the `Db` object. Suppose you had a `user` model defined, and the following data had been inserted into your database (either through factories or fixtures): ```js export default [ { id: 1, name: 'Zelda' }, { id: 2, name: 'Link' } ]; ``` Then `db.contacts` would return this array. @class DbCollection @constructor @public */ var DbCollection = /*#__PURE__*/function () { function DbCollection(name, initialData, IdentityManager) { _classCallCheck(this, DbCollection); this.name = name; this._records = []; this.identityManager = new IdentityManager(); if (initialData) { this.insert(initialData); } } /** * Returns a copy of the data, to prevent inadvertent data manipulation. * @method all * @public * @hide */ _createClass(DbCollection, [{ key: "all", value: function all() { return duplicate(this._records); } /** Inserts `data` into the collection. `data` can be a single object or an array of objects. Returns the inserted record. ```js // Insert a single record let link = db.users.insert({ name: 'Link', age: 173 }); link; // { id: 1, name: 'Link', age: 173 } // Insert an array let users = db.users.insert([ { name: 'Zelda', age: 142 }, { name: 'Epona', age: 58 }, ]); users; // [ { id: 2, name: 'Zelda', age: 142 }, { id: 3, name: 'Epona', age: 58 } ] ``` @method insert @param data @public */ }, { key: "insert", value: function insert(data) { var _this = this; if (!Array.isArray(data)) { return this._insertRecord(data); } else { return map(data, function (attrs) { return _this._insertRecord(attrs); }); } } /** Returns a single record from the `collection` if `ids` is a single id, or an array of records if `ids` is an array of ids. Note each id can be an int or a string, but integer ids as strings (e.g. the string “1”) will be treated as integers. ```js // Given users = [{id: 1, name: 'Link'}, {id: 2, name: 'Zelda'}] db.users.find(1); // {id: 1, name: 'Link'} db.users.find([1, 2]); // [{id: 1, name: 'Link'}, {id: 2, name: 'Zelda'}] ``` @method find @param ids @public */ }, { key: "find", value: function find(ids) { if (Array.isArray(ids)) { var records = this._findRecords(ids).filter(Boolean).map(duplicate); // Return a copy return records; } else { var record = this._findRecord(ids); if (!record) { return null; } // Return a copy return duplicate(record); } } /** Returns the first model from `collection` that matches the key-value pairs in the `query` object. Note that a string comparison is used. `query` is a POJO. ```js // Given users = [ { id: 1, name: 'Link' }, { id: 2, name: 'Zelda' } ] db.users.findBy({ name: 'Link' }); // { id: 1, name: 'Link' } ``` @method find @param query @public */ }, { key: "findBy", value: function findBy(query) { var record = this._findRecordBy(query); if (!record) { return null; } // Return a copy return duplicate(record); } /** Returns an array of models from `collection` that match the key-value pairs in the `query` object. Note that a string comparison is used. `query` is a POJO. ```js // Given users = [ { id: 1, name: 'Link' }, { id: 2, name: 'Zelda' } ] db.users.where({ name: 'Zelda' }); // [ { id: 2, name: 'Zelda' } ] ``` @method where @param query @public */ }, { key: "where", value: function where(query) { return this._findRecordsWhere(query).map(duplicate); } /** Finds the first record matching the provided _query_ in `collection`, or creates a new record using a merge of the `query` and optional `attributesForCreate`. Often times you may have a pattern like the following in your API stub: ```js // Given users = [ // { id: 1, name: 'Link' }, // { id: 2, name: 'Zelda' } // ] // Create Link if he doesn't yet exist let records = db.users.where({ name: 'Link' }); let record; if (records.length > 0) { record = records[0]; } else { record = db.users.insert({ name: 'Link' }); } ``` You can now replace this with the following: ```js let record = db.users.firstOrCreate({ name: 'Link' }); ``` An extended example using *attributesForCreate*: ```js let record = db.users.firstOrCreate({ name: 'Link' }, { evil: false }); ``` @method firstOrCreate @param query @param attributesForCreate @public */ }, { key: "firstOrCreate", value: function firstOrCreate(query) { var attributesForCreate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var queryResult = this.where(query); var _queryResult = _slicedToArray(queryResult, 1), record = _queryResult[0]; if (record) { return record; } else { var mergedAttributes = Object.assign(attributesForCreate, query); var createdRecord = this.insert(mergedAttributes); return createdRecord; } } /** Updates one or more records in the collection. If *attrs* is the only arg present, updates all records in the collection according to the key-value pairs in *attrs*. 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*. Returns the updated record or records. ```js // Given users = [ // {id: 1, name: 'Link'}, // {id: 2, name: 'Zelda'} // ] db.users.update({name: 'Ganon'}); // db.users = [{id: 1, name: 'Ganon'}, {id: 2, name: 'Ganon'}] db.users.update(1, {name: 'Young Link'}); // db.users = [{id: 1, name: 'Young Link'}, {id: 2, name: 'Zelda'}] db.users.update({name: 'Link'}, {name: 'Epona'}); // db.users = [{id: 1, name: 'Epona'}, {id: 2, name: 'Zelda'}] ``` @method update @param target @param attrs @public */ }, { key: "update", value: function update(target, attrs) { var _this2 = this; var records; if (typeof attrs === "undefined") { attrs = target; var changedRecords = []; this._records.forEach(function (record) { var oldRecord = Object.assign({}, record); _this2._updateRecord(record, attrs); if (!isEqual(oldRecord, record)) { changedRecords.push(record); } }); return changedRecords; } else if (typeof target === "number" || typeof target === "string") { var id = target; var record = this._findRecord(id); this._updateRecord(record, attrs); return record; } else if (Array.isArray(target)) { var ids = target; records = this._findRecords(ids); records.forEach(function (record) { _this2._updateRecord(record, attrs); }); return records; } else if (_typeof(target) === "object") { var query = target; records = this._findRecordsWhere(query); records.forEach(function (record) { _this2._updateRecord(record, attrs); }); return records; } } /** Removes one or more records in *collection*. 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. ```js // Given users = [ // {id: 1, name: 'Link'}, // {id: 2, name: 'Zelda'} // ] db.users.remove(); // db.users = [] db.users.remove(1); // db.users = [{id: 2, name: 'Zelda'}] db.users.remove({name: 'Zelda'}); // db.users = [{id: 1, name: 'Link'}] ``` @method remove @param target @public */ }, { key: "remove", value: function remove(target) { var _this3 = this; var records; if (typeof target === "undefined") { this._records = []; this.identityManager.reset(); } else if (typeof target === "number" || typeof target === "string") { var record = this._findRecord(target); var index = this._records.indexOf(record); this._records.splice(index, 1); } else if (Array.isArray(target)) { records = this._findRecords(target); records.forEach(function (record) { var index = _this3._records.indexOf(record); _this3._records.splice(index, 1); }); } else if (_typeof(target) === "object") { records = this._findRecordsWhere(target); records.forEach(function (record) { var index = _this3._records.indexOf(record); _this3._records.splice(index, 1); }); } } /* Private methods. These return the actual db objects, whereas the public API query methods return copies. */ /** @method _findRecord @param id @private @hide */ }, { key: "_findRecord", value: function _findRecord(id) { id = id.toString(); return this._records.find(function (obj) { return obj.id === id; }); } /** @method _findRecordBy @param query @private @hide */ }, { key: "_findRecordBy", value: function _findRecordBy(query) { return this._findRecordsWhere(query)[0]; } /** @method _findRecords @param ids @private @hide */ }, { key: "_findRecords", value: function _findRecords(ids) { return ids.map(this._findRecord, this); } /** @method _findRecordsWhere @param query @private @hide */ }, { key: "_findRecordsWhere", value: function _findRecordsWhere(query) { var records = this._records; function defaultQueryFunction(record) { var keys = Object.keys(query); return keys.every(function (key) { return String(record[key]) === String(query[key]); }); } var queryFunction = _typeof(query) === "object" ? defaultQueryFunction : query; return records.filter(queryFunction); } /** @method _insertRecord @param data @private @hide */ }, { key: "_insertRecord", value: function _insertRecord(data) { var attrs = duplicate(data); if (attrs && (attrs.id === undefined || attrs.id === null)) { attrs.id = this.identityManager.fetch(attrs); } else { attrs.id = attrs.id.toString(); this.identityManager.set(attrs.id); } this._records.push(attrs); return duplicate(attrs); } /** @method _updateRecord @param record @param attrs @private @hide */ }, { key: "_updateRecord", value: function _updateRecord(record, attrs) { var targetId = attrs && Object.prototype.hasOwnProperty.call(attrs, "id") ? attrs.id.toString() : null; var currentId = record.id; if (targetId && currentId !== targetId) { throw new Error("Updating the ID of a record is not permitted"); } for (var attr in attrs) { if (attr === "id") { continue; } record[attr] = attrs[attr]; } } }]); return DbCollection; }(); /** 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. Access the db from your route handlers via `schema.db`. You can access individual DbCollections by using `schema.db.name`: ```js schema.db.users // would return, e.g., [ { id: 1, name: 'Yehuda' }, { id: 2, name: 'Tom '} ] ``` @class Db @constructor @public */ var Db = /*#__PURE__*/function () { function Db(initialData, identityManagers) { _classCallCheck(this, Db); this._collections = []; this.registerIdentityManagers(identityManagers); if (initialData) { this.loadData(initialData); } } /** Loads an object of data into Mirage's database. The keys of the object correspond to the DbCollections, and the values are arrays of records. ```js server.db.loadData({ users: [ { name: 'Yehuda' }, { name: 'Tom' } ] }); ``` As with `db.collection.insert`, IDs will automatically be created for records that don't have them. @method loadData @param {Object} data - Data to load @public */ _createClass(Db, [{ key: "loadData", value: function loadData(data) { for (var key in data) { this.createCollection(ke