UNPKG

miragejs

Version:

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

1,898 lines (1,593 loc) 224 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var isPlainObject = require('lodash/isPlainObject'); var isFunction = require('lodash/isFunction'); var mapValues = require('lodash/mapValues'); var uniq = require('lodash/uniq'); var flatten = require('lodash/flatten'); var inflected = require('inflected'); var lowerFirst = require('lodash/lowerFirst'); var isEqual = require('lodash/isEqual'); var map = require('lodash/map'); var cloneDeep = require('lodash/cloneDeep'); var invokeMap = require('lodash/invokeMap'); var compact = require('lodash/compact'); var has = require('lodash/has'); var values = require('lodash/values'); var isEmpty = require('lodash/isEmpty'); var get = require('lodash/get'); var uniqBy = require('lodash/uniqBy'); var forIn = require('lodash/forIn'); var pick = require('lodash/pick'); var assign = require('lodash/assign'); var find = require('lodash/find'); var isInteger = require('lodash/isInteger'); require('@miragejs/pretender-node-polyfill/before'); var Pretender = require('pretender'); require('@miragejs/pretender-node-polyfill/after'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var isPlainObject__default = /*#__PURE__*/_interopDefaultLegacy(isPlainObject); var isFunction__default = /*#__PURE__*/_interopDefaultLegacy(isFunction); var mapValues__default = /*#__PURE__*/_interopDefaultLegacy(mapValues); var uniq__default = /*#__PURE__*/_interopDefaultLegacy(uniq); var flatten__default = /*#__PURE__*/_interopDefaultLegacy(flatten); var lowerFirst__default = /*#__PURE__*/_interopDefaultLegacy(lowerFirst); var isEqual__default = /*#__PURE__*/_interopDefaultLegacy(isEqual); var map__default = /*#__PURE__*/_interopDefaultLegacy(map); var cloneDeep__default = /*#__PURE__*/_interopDefaultLegacy(cloneDeep); var invokeMap__default = /*#__PURE__*/_interopDefaultLegacy(invokeMap); var compact__default = /*#__PURE__*/_interopDefaultLegacy(compact); var has__default = /*#__PURE__*/_interopDefaultLegacy(has); var values__default = /*#__PURE__*/_interopDefaultLegacy(values); var isEmpty__default = /*#__PURE__*/_interopDefaultLegacy(isEmpty); var get__default = /*#__PURE__*/_interopDefaultLegacy(get); var uniqBy__default = /*#__PURE__*/_interopDefaultLegacy(uniqBy); var forIn__default = /*#__PURE__*/_interopDefaultLegacy(forIn); var pick__default = /*#__PURE__*/_interopDefaultLegacy(pick); var assign__default = /*#__PURE__*/_interopDefaultLegacy(assign); var find__default = /*#__PURE__*/_interopDefaultLegacy(find); var isInteger__default = /*#__PURE__*/_interopDefaultLegacy(isInteger); var Pretender__default = /*#__PURE__*/_interopDefaultLegacy(Pretender); // jscs:disable disallowVar, requireArrayDestructuring /** @hide */ function referenceSort (edges) { let nodes = uniq__default["default"](flatten__default["default"](edges)); let cursor = nodes.length; let sorted = new Array(cursor); let visited = {}; let i = cursor; let visit = function (node, i, predecessors) { if (predecessors.indexOf(node) >= 0) { throw new Error(`Cyclic dependency in properties ${JSON.stringify(predecessors)}`); } if (visited[i]) { return; } else { visited[i] = true; } let outgoing = edges.filter(function (edge) { return edge && edge[0] === node; }); i = outgoing.length; if (i) { let preds = predecessors.concat(node); do { let pair = outgoing[--i]; let 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(); } let Factory = function () { this.build = function (sequence) { let object = {}; let topLevelAttrs = Object.assign({}, this.attrs); delete topLevelAttrs.afterCreate; Object.keys(topLevelAttrs).forEach(attr => { if (Factory.isTrait.call(this, attr)) { delete topLevelAttrs[attr]; } }); let keys = sortAttrs(topLevelAttrs, sequence); keys.forEach(function (key) { let buildAttrs, buildSingleValue; buildAttrs = function (attrs) { return mapValues__default["default"](attrs, buildSingleValue); }; buildSingleValue = value => { if (Array.isArray(value)) { return value.map(buildSingleValue); } else if (isPlainObject__default["default"](value)) { return buildAttrs(value); } else if (isFunction__default["default"](value)) { return value.call(topLevelAttrs, sequence); } else { return value; } }; let value = topLevelAttrs[key]; if (isFunction__default["default"](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. let newAttrs = Object.assign({}, this.attrs, attrs); let Subclass = function () { 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 ({ traits } = {}) { let afterCreateCallbacks = []; let attrs = this.attrs || {}; let traitCandidates; if (attrs.afterCreate) { afterCreateCallbacks.push(attrs.afterCreate); } if (Array.isArray(traits)) { traitCandidates = traits; } else { traitCandidates = Object.keys(attrs); } traitCandidates.filter(attr => { return this.isTrait(attr) && attrs[attr].extension.afterCreate; }).forEach(attr => { afterCreateCallbacks.push(attrs[attr].extension.afterCreate); }); return afterCreateCallbacks; }; Factory.isTrait = function (attrName) { let { attrs } = this; return isPlainObject__default["default"](attrs[attrName]) && attrs[attrName].__isTrait__ === true; }; function sortAttrs(attrs, sequence) { let Temp = function () {}; let obj = new Temp(); let refs = []; let property; Object.keys(attrs).forEach(function (key) { let value; Object.defineProperty(obj.constructor.prototype, key, { get() { refs.push([property, key]); return value; }, set(newValue) { value = newValue; }, enumerable: false, configurable: true }); }); Object.keys(attrs).forEach(function (key) { let value = attrs[key]; if (typeof value !== "function") { obj[key] = value; } }); Object.keys(attrs).forEach(function (key) { let value = attrs[key]; property = key; if (typeof value === "function") { obj[key] = value.call(obj, sequence); } refs.push([key]); }); return referenceSort(refs); } /** * @hide */ var Factory$1 = Factory; 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 */ class IdentityManager { constructor() { this._nextId = 1; this._ids = {}; } /** @method get @hide @private */ 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 */ set(uniqueIdentifier) { if (this._ids[uniqueIdentifier]) { throw new Error(`Attempting to use the ID ${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 */ inc() { let nextValue = this.get() + 1; this._nextId = nextValue; return nextValue; } /** Returns the next unique identifier. @method fetch @return {String} Unique identifier @public */ fetch() { let 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 */ reset() { this._nextId = 1; this._ids = {}; } } var IdentityManager$1 = IdentityManager; /** @hide */ let association = function (...traitsAndOverrides) { let __isAssociation__ = true; return { __isAssociation__, traitsAndOverrides }; }; var association$1 = association; let trait = function (extension) { let __isTrait__ = true; return { extension, __isTrait__ }; }; /** @hide */ var trait$1 = trait; const 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'] }); }); ``` */ class Response { constructor(code, headers = {}, data) { 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 204 Response that has data, but this is a violation of the HTTP spec and could lead to unexpected behavior. 204 responses should have no 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"; } } toRackResponse() { return [this.code, this.headers, this.data]; } } const camelizeCache = {}; const dasherizeCache = {}; const underscoreCache = {}; const capitalizeCache = {}; /** * @param {String} word * @hide */ function camelize(word) { if (typeof camelizeCache[word] !== "string") { let camelizedWord = inflected.camelize(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.) */ const camelized = camelizedWord.split("/").map(lowerFirst__default["default"]).join("/"); camelizeCache[word] = camelized; } return camelizeCache[word]; } /** * @param {String} word * @hide */ function dasherize(word) { if (typeof dasherizeCache[word] !== "string") { const dasherized = inflected.dasherize(underscore(word)); dasherizeCache[word] = dasherized; } return dasherizeCache[word]; } function underscore(word) { if (typeof underscoreCache[word] !== "string") { const underscored = inflected.underscore(word); underscoreCache[word] = underscored; } return underscoreCache[word]; } function capitalize(word) { if (typeof capitalizeCache[word] !== "string") { const capitalized = inflected.capitalize(word); capitalizeCache[word] = capitalized; } return capitalizeCache[word]; } /** @hide */ function isAssociation (object) { return isPlainObject__default["default"](object) && object.__isAssociation__ === true; } /* eslint no-console: 0 */ let 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) { let tmp = Error(message); if (stack) { tmp.stack = stack; } for (let idx = 0; idx < errorProps.length; idx++) { let prop = errorProps[idx]; if (["description", "message", "stack"].indexOf(prop) > -1) { this[prop] = `Mirage: ${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 */ class Association { constructor(modelName, opts) { /** 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 */ 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 */ isReflexive() { let isExplicitReflexive = !!(this.modelName === this.ownerModelName && this.opts.inverse); let 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 */ get isPolymorphic() { return this.opts.polymorphic; } /** Returns either the string `"hasMany"` or `"belongsTo"`, based on the association type. @accessor @type {String} @public */ get type() { 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 */ get foreignKey() { return this.getForeignKey(); } /** @hide */ get identifier() { throw new Error("Subclasses of Association must implement a getter for identifier"); } } const identifierCache$1 = {}; /** * The belongsTo association adds a fk to the owner of the association * * @class BelongsTo * @extends Association * @constructor * @public * @hide */ class BelongsTo extends Association { get identifier() { if (typeof identifierCache$1[this.name] !== "string") { const identifier = `${camelize(this.name)}Id`; identifierCache$1[this.name] = identifier; } return identifierCache$1[this.name]; } get type() { return "belongsTo"; } /** * @method getForeignKeyArray * @return {Array} Array of camelized name of the model owning the association * and foreign key for the association * @public */ getForeignKeyArray() { return [camelize(this.ownerModelName), this.getForeignKey()]; } /** * @method getForeignKey * @return {String} Foreign key for the association * @public */ getForeignKey() { // we reuse identifierCache because it's the same logic as get identifier if (typeof identifierCache$1[this.name] !== "string") { const foreignKey = `${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 */ addMethodsToModelClass(ModelClass, key) { let modelPrototype = ModelClass.prototype; let association = this; let foreignKey = this.getForeignKey(); let associationHash = { [key]: this }; modelPrototype.belongsToAssociations = Object.assign(modelPrototype.belongsToAssociations, associationHash); // update belongsToAssociationFks Object.keys(modelPrototype.belongsToAssociations).forEach(key => { const 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() { this._tempAssociations = this._tempAssociations || {}; let tempParent = this._tempAssociations[key]; let 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(id) { let 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 '${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 ${association.modelName} with id = ${id}`); } } this[key] = tempParent; } }); Object.defineProperty(modelPrototype, key, { /* object.parent - returns the associated parent */ get() { this._tempAssociations = this._tempAssociations || {}; let tempParent = this._tempAssociations[key]; let foreignKeyId = this[foreignKey]; let 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(model) { this._tempAssociations = this._tempAssociations || {}; this._tempAssociations[key] = model; if (model && model.hasInverseFor(association)) { let inverse = model.inverseFor(association); model.associate(this, inverse); } } }); /* object.newParent - creates a new unsaved associated parent TODO: document polymorphic */ modelPrototype[`new${capitalize(key)}`] = function (...args) { let modelName, attrs; if (association.isPolymorphic) { modelName = args[0]; attrs = args[1]; } else { modelName = association.modelName; attrs = args[0]; } let 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${capitalize(key)}`] = function (...args) { let modelName, attrs; if (association.isPolymorphic) { modelName = args[0]; attrs = args[1]; } else { modelName = association.modelName; attrs = args[0]; } let parent = association.schema[association.schema.toCollectionName(modelName)].create(attrs); this[key] = parent; this.save(); return parent.reload(); }; } /** * * * @public */ disassociateAllDependentsFromTarget(model) { let owner = this.ownerModelName; let fk; if (this.isPolymorphic) { fk = { type: model.modelName, id: model.id }; } else { fk = model.id; } let dependents = this.schema[this.schema.toCollectionName(owner)].where(potentialOwner => { let 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(dependent => { dependent.disassociate(model, this); dependent.save(); }); } } 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 */ class DbCollection { constructor(name, initialData, IdentityManager) { 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 */ 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 */ insert(data) { if (!Array.isArray(data)) { return this._insertRecord(data); } else { return map__default["default"](data, attrs => 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 */ find(ids) { if (Array.isArray(ids)) { let records = this._findRecords(ids).filter(Boolean).map(duplicate); // Return a copy return records; } else { let 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 */ findBy(query) { let 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 */ 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 */ firstOrCreate(query, attributesForCreate = {}) { let queryResult = this.where(query); let [record] = queryResult; if (record) { return record; } else { let mergedAttributes = Object.assign(attributesForCreate, query); let 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 */ update(target, attrs) { let records; if (typeof attrs === "undefined") { attrs = target; let changedRecords = []; this._records.forEach(record => { let oldRecord = Object.assign({}, record); this._updateRecord(record, attrs); if (!isEqual__default["default"](oldRecord, record)) { changedRecords.push(record); } }); return changedRecords; } else if (typeof target === "number" || typeof target === "string") { let id = target; let record = this._findRecord(id); this._updateRecord(record, attrs); return record; } else if (Array.isArray(target)) { let ids = target; records = this._findRecords(ids); records.forEach(record => { this._updateRecord(record, attrs); }); return records; } else if (typeof target === "object") { let query = target; records = this._findRecordsWhere(query); records.forEach(record => { this._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 */ remove(target) { let records; if (typeof target === "undefined") { this._records = []; this.identityManager.reset(); } else if (typeof target === "number" || typeof target === "string") { let record = this._findRecord(target); let index = this._records.indexOf(record); this._records.splice(index, 1); } else if (Array.isArray(target)) { records = this._findRecords(target); records.forEach(record => { let index = this._records.indexOf(record); this._records.splice(index, 1); }); } else if (typeof target === "object") { records = this._findRecordsWhere(target); records.forEach(record => { let index = this._records.indexOf(record); this._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 */ _findRecord(id) { id = id.toString(); return this._records.find(obj => obj.id === id); } /** @method _findRecordBy @param query @private @hide */ _findRecordBy(query) { return this._findRecordsWhere(query)[0]; } /** @method _findRecords @param ids @private @hide */ _findRecords(ids) { return ids.map(this._findRecord, this); } /** @method _findRecordsWhere @param query @private @hide */ _findRecordsWhere(query) { let records = this._records; function defaultQueryFunction(record) { let keys = Object.keys(query); return keys.every(function (key) { return String(record[key]) === String(query[key]); }); } let queryFunction = typeof query === "object" ? defaultQueryFunction : query; return records.filter(queryFunction); } /** @method _insertRecord @param data @private @hide */ _insertRecord(data) { let 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 */ _updateRecord(record, attrs) { let targetId = attrs && Object.prototype.hasOwnProperty.call(attrs, "id") ? attrs.id.toString() : null; let currentId = record.id; if (targetId && currentId !== targetId) { throw new Error("Updating the ID of a record is not permitted"); } for (let attr in attrs) { if (attr === "id") { continue; } record[attr] = attrs[attr]; } } } var DbCollection$1 = 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 */ class Db { constructor(initialData, identityManagers) { 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 */ loadData(data) { for (let key in data) { this.createCollection(key, cloneDeep__default["default"](data[key])); } } /** Logs out the contents of the Db. ```js server.db.dump() // { users: [ name: 'Yehuda', ... ``` @method dump @public */ dump() { return this._collections.reduce((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 */ createCollection(name, initialData) { if (!this[name]) { let IdentityManager = this.identityManagerFor(name); let newCollection = new DbCollection$1(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() { let recordsCopy = newCollection.all(); ["insert", "find", "findBy", "where", "update", "remove", "firstOrCreate"].forEach(function (method) { recordsCopy[method] = function () { return newCollection[method](...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, `_${name}`, { get() { let recordsCopy = []; ["insert", "find", "findBy", "where", "update", "remove", "firstOrCreate"].forEach(function (method) { recordsCopy[method] = function () { return newCollection[method](...arguments); }; }); return recordsCopy; } }); this._collections.push(newCollection); } else if (initialData) { this[name].insert(initialData); } return this; } /** @method createCollections @param ...collections @public @hide */ createCollections(...collections) { collections.forEach(c => this.createCollection(c)); } /** Removes all data from Mirage's database. @method emptyData @public */ emptyData() { this._collections.forEach(c => c.remove()); } /** @method identityManagerFor @param name @public @hide */ identityManagerFor(name) { return this._identityManagers[this._container.inflector.singularize(name)] || this._identityManagers.application || IdentityManager$1; } /** @method registerIdentityManagers @public @hide */ registerIdentityManagers(identityManagers) { this._identityManagers = identityManagers || {}; } } var Db$1 = 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 */ class Collection { constructor(modelName, models = []) { 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 */ get length() { return this.models.length; } /** 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 */ update(...args) { invokeMap__default["default"](this.models, "update", ...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 */ save() { invokeMap__default["default"](this.models, "save"); return this; } /** Reloads each model in the collection. ```js let posts = author.blogPosts; // ... posts.reload(); // reloads data for each post from the db ``` @method reload @return this @public */ reload() { invokeMap__default["default"](this.models, "reload"); return this; } /** Destroys the db record for all models in the collection. ```js let posts = user.blogPosts; posts.destroy(); // all posts removed from db ``` @method destroy @return this @public */ destroy() { invokeMap__default["default"](this.models, "destroy"); return this; } /** Adds a model to this collection. ```js posts.length; // 1 posts.add(newPost); posts.length; // 2 ``` @method add @param {Model} model @return this @public */ add(model) { this.models.push(model); return this; } /** Removes a model from this collection. ```js posts.length; // 5 let firstPost = posts.models[0]; posts.remove(firstPost); posts.save(); posts.length; // 4 ``` @method remove @param {Model} model @return this @public */ remove(model) { let match = this.models.find(m => m.toString() === model.toString()); if (match) { let i = this.models.indexOf(match); this.models.splice(i, 1); } return this; } /** Checks if the Collection includes the given model. ```js posts.includes(newPost); ``` Works by checking if the given model name and id exists in the Collection, making it a bit more flexible than strict object equality. ```js let post = server.create('post'); let programming = server.create('tag', { text: 'Programming' }); visit(`/posts/${post.id}`); click('.tag-selector'); click('.tag:contains(Programming)'); post.reload(); assert.ok(post.tags.includes(programming)); ``` @method includes @return {Boolean} @public */ includes(model) { return this.models.some(m => m.toString() === model.toString()); } /** 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). ```js let publishedPosts = user.posts.filter(post => post.isPublished); ``` @method filter @param {Function} f @return {Collection} @public */ filter(f) { let filteredModels = this.models.filter(f); return new Collection(this.modelName, filteredModels); } /** 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). ```js let postsByTitleAsc = user.posts.sort((a, b) => a.title > b.title ? 1 : -1 ); ``` @method sort @param {Function} f @return {Collection} @public */ sort(f) { let sortedModels = this.models.concat().sort(f); return new Collection(this.modelName, sortedModels); } /** Returns a new Collection with a subset of its models selected from `begin` to `end`. ```js let firstThreePosts = user.posts.slice(0, 3); ``` @method slice @param {Integer} begin @param {Integer} end @return {Collection} @public */ slice(...args) { let slicedModels = this.models.slice(...args); return new Collection(this.modelName, slicedModels); } /** Modifies the Collection by merging the models from another collection. ```js user.posts.mergeCollection(newPosts); user.posts.save(); ``` @method mergeCollection @param {Collection} collection @return this @public */ mergeCollection(collection) { this.models = this.models.concat(collection.models); return this; } /** Simple string representation of the collection and id. ```js user.posts.toString(); // collection:post(post:1,post:4) ``` @method toString @return {String} @public */ toString() { return `collection:${this.modelName}(${this.models.map(m => m.id).join(",")})`; } } /** * An array of models, returned from one of the schema query * methods (all, find, where). Knows how to update and destroy its models. * * Identical to Collection except it can contain a heterogeneous array of * models. Thus, it has no `modelName` property. This lets serializers and * other part