miragejs
Version:
A client-side server to help you build, test and demo your JavaScript app
348 lines (300 loc) • 9.82 kB
JavaScript
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();
});
}
}