miragejs
Version:
A client-side server to help you build, test and demo your JavaScript app
1,816 lines (1,542 loc) • 248 kB
JavaScript
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 '@miragejs/pretender-node-polyfill/before';
import Pretender from 'pretender';
import '@miragejs/pretender-node-polyfill/after';
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';
function _typeof(obj) {
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 {
Date.prototype.toString.call(Reflect.construct(Date, [], 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;
}
return _assertThisInitialized(self);
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
}
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterableToArray(iter) {
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
}
function _iterableToArrayLimit(arr, i) {
if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) {
return;
}
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_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 _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance");
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
// 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 are used to define relationships between your Models.
@class Association
@constructor
@public
*/
var Association =
/*#__PURE__*/
function () {
function Association(modelName, opts) {
_classCallCheck(this, Association);
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 key pointing to the association
this.key = ""; // 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 true if the association is reflexive.
@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;
}
}, {
key: "isPolymorphic",
get: function get() {
return this.opts.polymorphic;
}
/**
@hide
*/
}, {
key: "identifier",
get: function get() {
throw new Error("Subclasses of Association must implement a getter for identifier");
}
}]);
return Association;
}();
var identifierCache = {};
/**
* 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);
function BelongsTo() {
_classCallCheck(this, BelongsTo);
return _possibleConstructorReturn(this, _getPrototypeOf(BelongsTo).apply(this, arguments));
}
_createClass(BelongsTo, [{
key: "getForeignKeyArray",
/**
* @method getForeignKeyArray
* @return {Array} Array of camelized name of the model owning the association
* and foreign key for the association
* @public
*/
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[this.key] !== "string") {
var foreignKey = "".concat(camelize(this.key), "Id");
identifierCache[this.key] = foreignKey;
}
return identifierCache[this.key];
}
/**
* 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.key, "' 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();
});
}
}, {
key: "identifier",
get: function get() {
if (typeof identifierCache[this.key] !== "string") {
var identifier = "".concat(camelize(this.key), "Id");
identifierCache[this.key] = identifier;
}
return identifierCache[this.key];
}
}]);
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(key, cloneDeep(data[key]));
}
}
/**
Logs out the contents of the Db.
```js
server.db.dump() // { users: [ name: 'Yehuda', ...
```
@method dump
@public
*/
}, {
key: "dump",
value: function dump() {
return this._collections.reduce(function (data, collection) {
data[collection.name] = collection.all();
return data;
}, {});
}
/**
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.
@method createCollection
@param name
@param initialData (optional)
@public
*/
}, {
key: "createCollection",
value: function createCollection(name, initialData) {
if (!this[name]) {
var _IdentityManager = this.identityManagerFor(name);
var newCollection = new DbCollection(name, initialData, _IdentityManager); // Public API has a convenient array interface. It comes at the cost of
// returning a copy of all records to avoid accidental mutations.
Object.defineProperty(this, name, {
get: function get() {
var recordsCopy = newCollection.all();
["insert", "find", "findBy", "where", "update", "remove", "firstOrCreate"].forEach(function (method) {
recordsCopy[method] = function () {
return newCollection[method].apply(newCollection, arguments);
};
});
return recordsCopy;
}
}); // Private API does not have the array interface. This means internally, only
// db-collection methods can be used. This is so records aren't copied redundantly
// internally, which leads to accidental O(n^2) operations (e.g., createList).
Object.defineProperty(this, "_".concat(name), {
get: function get() {
var recordsCopy = [];
["insert", "find", "findBy", "where", "update", "remove", "firstOrCreate"].forEach(function (method) {
recordsCopy[method] = function () {
return newCollection[method].apply(newCollection, arguments);
};
});
return recordsCopy;
}
});
this._collections.push(newCollection);
} else if (initialData) {
this[name].insert(initialData);
}
return this;
}
/**
@method createCollections
@param ...collections
@public
@hide
*/
}, {
key: "createCollections",
value: function createCollections() {
var _this = this;
for (var _len = arguments.length, collections = new Array(_len), _key = 0; _key < _len; _key++) {
collections[_key] = arguments[_key];
}
collections.forEach(function (c) {
return _this.createCollection(c);
});
}
/**
Removes all data from Mirage's database.
@method emptyData
@public
*/
}, {
key: "emptyData",
value: function emptyData() {
this._collections.forEach(function (c) {
return c.remove();
});
}
/**
@method identityManagerFor
@param name
@public
@hide
*/
}, {
key: "identityManagerFor",
value: function identityManagerFor(name) {
return this._identityManagers[this._container.inflector.singularize(name)] || this._identityManagers.application || IdentityManager;
}
/**
@method registerIdentityManagers
@public
@hide
*/
}, {
key: "registerIdentityManagers",
value: function registerIdentityManagers(identityManagers) {
this._identityManagers = identityManagers || {};
}
}]);
return Db;
}();
/**
Collections represent arrays of models. They are returned by a hasMany association, or by one of the ModelClass query methods:
```js
let posts = user.blogPosts;
let posts = schema.blogPosts.all();
let posts = schema.blogPosts.find([1, 2, 4]);
let posts = schema.blogPosts.where({ published: true });
```
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.
@class Collection
@constructor
@public
*/
var Collection =
/*#__PURE__*/
function () {
function Collection(modelName) {
var models = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
_classCallCheck(this, Collection);
assert(modelName && typeof modelName === "string", "You must pass a `modelName` into a Collection");
/**
The dasherized model name this Collection represents.
```js
let posts = user.blogPosts;
posts.modelName; // "blog-post"
```
The model name is separate from the actual models, since Collections can be empty.
@property modelName
@type {String}
@public
*/
this.modelName = modelName;
/**
The underlying plain JavaScript array of Models in this Collection.
```js
posts.models // [ post:1, post:2, ... ]
```
While Collections have many array-ish methods like `filter` and `sort`, it
can be useful to work with the plain array if you want to work with methods
like `map`, or use the `[]` accessor.
For example, in testing you might want to assert against a model from the
collection:
```js
let newPost = user.posts.models[0].title;
assert.equal(newPost, "My first post");
```
@property models
@type {Array}
@public
*/
this.models = models;
}
/**
The number of models in the collection.
```js
user.posts.length; // 2
```
@property length
@type {Integer}
@public
*/
_createClass(Collection, [{
key: "update",
/**
Updates each model in the collection, and immediately persists all changes to the db.
```js
let posts = user.blogPosts;
posts.update('published', true); // the db was updated for all posts
```
@method update
@param key
@param val
@return this
@public
*/
value: function update() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
invokeMap.apply(void 0, [this.models, "update"].concat(args));
return this;
}
/**
Saves all models in the collection.
```js
let posts = user.blogPosts;
posts.models[0].published = true;
posts.save(); // all posts saved to db
```
@method save
@return this
@public
*/
}, {
key: "save",
value: function save() {
invokeMap(this.models, "save");
return this;
}
/**
Reloads each model in the collection.
```js
let posts = author.blogPosts;