@bigtest/mirage
Version:
A client-side server to develop, test and prototype your app.
275 lines (233 loc) • 8.33 kB
JavaScript
import Association from './association';
import Collection from '../collection';
import PolymorphicCollection from '../polymorphic-collection';
import _assign from 'lodash/assign';
import _compact from 'lodash/compact';
import { capitalize, camelize, singularize } from '@bigtest/mirage';
import { toCollectionName } from '../../utils/normalize-name';
import assert from '../../assert';
/**
* @class HasMany
* @extends Association
* @constructor
* @public
*/
export default class HasMany extends Association {
/**
* @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() {
return `${singularize(camelize(this.key))}Ids`;
}
/**
* 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 = _assign(modelPrototype.hasManyAssociations, associationHash);
// 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.push(key);
modelPrototype.associationIdKeys.push(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[toCollectionName(type)].find(id);
});
tempChildren = new PolymorphicCollection(models);
} else {
tempChildren = 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[toCollectionName(type)].find(id);
});
collection = new PolymorphicCollection(models);
} else {
collection = new PolymorphicCollection(association.modelName);
}
} else {
if (this[foreignKey]) {
collection = 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(singularize(association.key)))}`] = 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[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(singularize(association.key)))}`] = 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[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[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();
});
}
}