UNPKG

@bigtest/mirage

Version:

A client-side server to develop, test and prototype your app.

349 lines (297 loc) 9.76 kB
import { pluralize, camelize, dasherize } from '@bigtest/mirage'; import { toCollectionName, toModelName } from '../utils/normalize-name'; import Association from './associations/association'; import Collection from './collection'; import _assign from 'lodash/assign'; import _forIn from 'lodash/forIn'; import _includes from 'lodash/includes'; import assert from '../assert'; /** * @class Schema * @constructor * @public */ export default class Schema { constructor(db, modelsMap = {}) { assert(db, 'A schema requires a db'); this.db = db; this._registry = {}; this._dependentAssociations = { polymorphic: [] }; this.registerModels(modelsMap); } /** * @method registerModels * @param hash * @public */ registerModels(hash = {}) { _forIn(hash, (model, key) => { this.registerModel(key, hash[key]); }); } /** * @method registerModel * @param type * @param ModelClass * @public */ registerModel(type, ModelClass) { let camelizedModelName = camelize(type); let modelName = dasherize(camelizedModelName); // Avoid mutating original class, because we may want to reuse it across many tests ModelClass = ModelClass.extend(); // Store model & fks in registry // TODO: don't think this is needed anymore this._registry[camelizedModelName] = this._registry[camelizedModelName] || { class: null, foreignKeys: [] }; // we may have created this key before, if another model added fks to it this._registry[camelizedModelName].class = ModelClass; // TODO: set here, remove from model#constructor ModelClass.prototype._schema = this; ModelClass.prototype.modelName = modelName; // Set up associations ModelClass.prototype.hasManyAssociations = {}; // a registry of the model's hasMany associations. Key is key from model definition, value is association instance itself ModelClass.prototype.belongsToAssociations = {}; // a registry of the model's belongsTo associations. Key is key from model definition, value is association instance itself ModelClass.prototype.associationKeys = []; // ex: address.user, user.addresses ModelClass.prototype.associationIdKeys = []; // ex: address.user_id, user.address_ids ModelClass.prototype.dependentAssociations = []; // a registry of associations that depend on this model, needed for deletion cleanup. let fksAddedFromThisModel = {}; for (let associationProperty in ModelClass.prototype) { if (ModelClass.prototype[associationProperty] instanceof Association) { let association = ModelClass.prototype[associationProperty]; association.key = associationProperty; association.modelName = association.modelName || toModelName(associationProperty); association.ownerModelName = modelName; association.setSchema(this); // Update the registry with this association's foreign keys. This is // essentially our "db migration", since we must know about the fks. let [fkHolder, fk] = association.getForeignKeyArray(); fksAddedFromThisModel[fkHolder] = fksAddedFromThisModel[fkHolder] || []; assert( !_includes(fksAddedFromThisModel[fkHolder], fk), `Your '${type}' model definition has multiple possible inverse relationships of type '${fkHolder}'. Please read the associations guide and specify explicit inverses: http://www.ember-cli-mirage.com/docs/v0.2.x/models/#associations` ); fksAddedFromThisModel[fkHolder].push(fk); this._addForeignKeyToRegistry(fkHolder, fk); // Augment the Model's class with any methods added by this association association.addMethodsToModelClass(ModelClass, associationProperty); } } // Create a db collection for this model, if doesn't exist let collection = toCollectionName(modelName); if (!this.db[collection]) { this.db.createCollection(collection); } // Create the entity methods this[collection] = { camelizedModelName, new: (attrs) => this.new(camelizedModelName, attrs), create: (attrs) => this.create(camelizedModelName, attrs), all: (attrs) => this.all(camelizedModelName, attrs), find: (attrs) => this.find(camelizedModelName, attrs), findBy: (attrs) => this.findBy(camelizedModelName, attrs), where: (attrs) => this.where(camelizedModelName, attrs), first: (attrs) => this.first(camelizedModelName, attrs) }; return this; } /** * @method modelFor * @param type * @public */ modelFor(type) { return this._registry[type]; } /** * @method new * @param type * @param attrs * @public */ new(type, attrs) { return this._instantiateModel(dasherize(type), attrs); } /** * @method create * @param type * @param attrs * @public */ create(type, attrs) { return this.new(type, attrs).save(); } /** * @method all * @param type * @public */ all(type) { let collection = this._collectionForType(type); return this._hydrate(collection, dasherize(type)); } /** * @method find * @param type * @param ids * @public */ find(type, ids) { let collection = this._collectionForType(type); let records = collection.find(ids); if (Array.isArray(ids)) { assert( records.length === ids.length, `Couldn't find all ${pluralize(type)} with ids: (${ids.join(',')}) (found ${records.length} results, but was looking for ${ids.length})` ); } return this._hydrate(records, dasherize(type)); } /** * @method findBy * @param type * @param attributeName * @public */ findBy(type, query) { let collection = this._collectionForType(type); let records = collection.findBy(query); return this._hydrate(records, dasherize(type)); } /** * @method where * @param type * @param query * @public */ where(type, query) { let collection = this._collectionForType(type); let records = collection.where(query); return this._hydrate(records, dasherize(type)); } /** * @method first * @param type * @public */ first(type) { let collection = this._collectionForType(type); let [record] = collection; return this._hydrate(record, dasherize(type)); } modelClassFor(modelName) { return this._registry[camelize(modelName)].class.prototype; } /* This method updates the dependentAssociations registry, which is used to keep track of which models depend on a given association. It's used when deleting models - their dependents need to be looked up and foreign keys updated. For example, schema = { post: Model.extend(), comment: Model.extend({ post: belongsTo() }) }; comment1.post = post1; ... post1.destroy() Deleting this post should clear out comment1's foreign key. Polymorphic associations can have _any_ other model as a dependent, so we handle them separately. */ addDependentAssociation(association, modelName) { if (association.isPolymorphic) { this._dependentAssociations.polymorphic.push(association); } else { this._dependentAssociations[modelName] = this._dependentAssociations[modelName] || []; this._dependentAssociations[modelName].push(association); } } dependentAssociationsFor(modelName) { let directDependents = this._dependentAssociations[modelName] || []; let polymorphicAssociations = this._dependentAssociations.polymorphic || []; return directDependents.concat(polymorphicAssociations); } associationsFor(modelName) { let modelClass = this.modelClassFor(modelName); return _assign({}, modelClass.belongsToAssociations, modelClass.hasManyAssociations); } /* Private methods */ /** * @method _collectionForType * @param type * @private */ _collectionForType(type) { let collection = toCollectionName(type); assert( this.db[collection], `You're trying to find model(s) of type ${type} but this collection doesn't exist in the database.` ); return this.db[collection]; } /** * @method _addForeignKeyToRegistry * @param type * @param fk * @private */ _addForeignKeyToRegistry(type, fk) { this._registry[type] = this._registry[type] || { class: null, foreignKeys: [] }; let fks = this._registry[type].foreignKeys; if (!_includes(fks, fk)) { fks.push(fk); } } /** * @method _instantiateModel * @param modelName * @param attrs * @private */ _instantiateModel(modelName, attrs) { let ModelClass = this._modelFor(modelName); let fks = this._foreignKeysFor(modelName); return new ModelClass(this, modelName, attrs, fks); } /** * @method _modelFor * @param modelName * @private */ _modelFor(modelName) { return this._registry[camelize(modelName)].class; } /** * @method _foreignKeysFor * @param modelName * @private */ _foreignKeysFor(modelName) { return this._registry[camelize(modelName)].foreignKeys; } /** * Takes a record and returns a model, or an array of records * and returns a collection. * * @method _hydrate * @param records * @param modelName * @private */ _hydrate(records, modelName) { if (Array.isArray(records)) { let models = records.map(function(record) { return this._instantiateModel(modelName, record); }, this); return new Collection(modelName, models); } else if (records) { return this._instantiateModel(modelName, records); } else { return null; } } }