UNPKG

miragejs

Version:

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

348 lines (300 loc) 9.82 kB
import Association from "./association"; import Collection from "../collection"; import PolymorphicCollection from "../polymorphic-collection"; import compact from "lodash/compact"; import { capitalize, camelize } from "../../utils/inflector"; import assert from "@lib/assert"; const identifierCache = {}; /** * @class HasMany * @extends Association * @constructor * @public * @hide */ export default class HasMany extends Association { get identifier() { if (typeof identifierCache[this.name] !== "string") { const identifier = `${camelize( this._container.inflector.singularize(this.name) )}Ids`; identifierCache[this.name] = identifier; } return identifierCache[this.name]; } get type() { return "hasMany"; } /** * @method getForeignKeyArray * @return {Array} Array of camelized model name of associated objects * and foreign key for the object owning the association * @public */ getForeignKeyArray() { return [camelize(this.ownerModelName), this.getForeignKey()]; } /** * @method getForeignKey * @return {String} Foreign key for the object owning the association * @public */ getForeignKey() { // we reuse identifierCache because it's the same logic as get identifier if (typeof identifierCache[this.name] !== "string") { const foreignKey = `${this._container.inflector.singularize( camelize(this.name) )}Ids`; identifierCache[this.name] = foreignKey; } return identifierCache[this.name]; } /** * Registers has-many association defined by given key on given model, * defines getters / setters for associated records and associated records' ids, * adds methods for creating unsaved child records and creating saved ones * * @method addMethodsToModelClass * @param {Function} ModelClass * @param {String} key * @public */ addMethodsToModelClass(ModelClass, key) { let modelPrototype = ModelClass.prototype; let association = this; let foreignKey = this.getForeignKey(); let associationHash = { [key]: this }; modelPrototype.hasManyAssociations = Object.assign( modelPrototype.hasManyAssociations, associationHash ); // update hasManyAssociationFks Object.keys(modelPrototype.hasManyAssociations).forEach((key) => { const value = modelPrototype.hasManyAssociations[key]; modelPrototype.hasManyAssociationFks[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.childrenIds - returns an array of the associated children's ids */ get() { this._tempAssociations = this._tempAssociations || {}; let tempChildren = this._tempAssociations[key]; let ids = []; if (tempChildren) { if (association.isPolymorphic) { ids = tempChildren.models.map((model) => ({ type: model.modelName, id: model.id, })); } else { ids = tempChildren.models.map((model) => model.id); } } else { ids = this.attrs[foreignKey] || []; } return ids; }, /* object.childrenIds = ([childrenIds...]) - sets the associated children (via id) */ set(ids) { let tempChildren; if (ids === null) { tempChildren = []; } else if (ids !== undefined) { assert( Array.isArray(ids), `You must pass an array in when setting ${foreignKey} on ${this}` ); if (association.isPolymorphic) { assert( ids.every((el) => { return ( typeof el === "object" && typeof el.type !== undefined && typeof el.id !== undefined ); }), `You must pass in an array of polymorphic identifiers (objects of shape { type, id }) when setting ${foreignKey} on ${this}` ); let models = ids.map(({ type, id }) => { return association.schema[ association.schema.toCollectionName(type) ].find(id); }); tempChildren = new PolymorphicCollection(models); } else { tempChildren = association.schema[ association.schema.toCollectionName(association.modelName) ].find(ids); } } this[key] = tempChildren; }, }); Object.defineProperty(modelPrototype, key, { /* object.children - returns an array of associated children */ get() { this._tempAssociations = this._tempAssociations || {}; let collection = null; if (this._tempAssociations[key]) { collection = this._tempAssociations[key]; } else { if (association.isPolymorphic) { if (this[foreignKey]) { let polymorphicIds = this[foreignKey]; let models = polymorphicIds.map(({ type, id }) => { return association.schema[ association.schema.toCollectionName(type) ].find(id); }); collection = new PolymorphicCollection(models); } else { collection = new PolymorphicCollection(association.modelName); } } else { if (this[foreignKey]) { collection = association.schema[ association.schema.toCollectionName(association.modelName) ].find(this[foreignKey]); } else { collection = new Collection(association.modelName); } } this._tempAssociations[key] = collection; } return collection; }, /* object.children = [model1, model2, ...] - sets the associated children (via array of models or Collection) */ set(models) { if ( models instanceof Collection || models instanceof PolymorphicCollection ) { models = models.models; } models = models ? compact(models) : []; this._tempAssociations = this._tempAssociations || {}; let collection; if (association.isPolymorphic) { collection = new PolymorphicCollection(models); } else { collection = new Collection(association.modelName, models); } this._tempAssociations[key] = collection; models.forEach((model) => { if (model.hasInverseFor(association)) { let inverse = model.inverseFor(association); model.associate(this, inverse); } }); }, }); /* object.newChild - creates a new unsaved associated child */ modelPrototype[ `new${capitalize( camelize(this._container.inflector.singularize(association.name)) )}` ] = function (...args) { let modelName, attrs; if (association.isPolymorphic) { modelName = args[0]; attrs = args[1]; } else { modelName = association.modelName; attrs = args[0]; } let child = association.schema[association.schema.toCollectionName(modelName)].new( attrs ); let children = this[key].models; children.push(child); this[key] = children; return child; }; /* object.createChild - creates a new saved associated child, and immediately persists both models TODO: forgot why this[key].add(child) doesn't work, most likely because these external APIs trigger saving cascades. Should probably have an internal method like this[key]._add. */ modelPrototype[ `create${capitalize( camelize(this._container.inflector.singularize(association.name)) )}` ] = function (...args) { let modelName, attrs; if (association.isPolymorphic) { modelName = args[0]; attrs = args[1]; } else { modelName = association.modelName; attrs = args[0]; } let child = association.schema[ association.schema.toCollectionName(modelName) ].create(attrs); let children = this[key].models; children.push(child); this[key] = children; this.save(); return child.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 currentIds = potentialOwner[this.getForeignKey()]; // Need this check because currentIds could be null return ( currentIds && currentIds.find((id) => { 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(); }); } }