UNPKG

jugglingdb

Version:

Node.js ORM for every database: redis, mysql, mongodb, postgres, sqlite, ...

378 lines (350 loc) 12.4 kB
/** * Dependencies */ const i8n = require('inflection'); const defineScope = require('./scope.js').defineScope; /** * Relations mixins for ./model.js */ const Model = require('./model.js'); Model.relationNameFor = function relationNameFor(foreignKey) { let found; Object.keys(this.relations).forEach(rel => { if (this.relations[rel].type === 'belongsTo' && this.relations[rel].keyFrom === foreignKey) { found = rel; } }); return found; }; /** * Declare hasMany relation * * @param {Model} anotherClass - class to has many * @param {Object} params - configuration {as:, foreignKey:} * @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});` */ Model.hasMany = function hasMany(anotherClass, params) { const thisClassName = this.modelName; params = params || {}; if (typeof anotherClass === 'string') { params.as = anotherClass; if (params.model) { anotherClass = params.model; } else { const anotherClassName = i8n.singularize(anotherClass).toLowerCase(); Object.keys(this.schema.models).forEach(name => { if (name.toLowerCase() === anotherClassName) { anotherClass = this.schema.models[name]; } }); } } const methodName = params.as || i8n.camelize(i8n.pluralize(anotherClass.modelName), true); const fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); this.relations[methodName] = { type: 'hasMany', keyFrom: 'id', keyTo: fk, modelTo: anotherClass, multiple: true }; // each instance of this class should have method named // pluralize(anotherClass.modelName) // which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb); const scopeMethods = { find, destroy }; let fk2; if (params.through) { // Append through relation, like modelTo this.relations[methodName].modelThrough = params.through; fk2 = i8n.camelize(anotherClass.modelName + '_id', true); scopeMethods.create = function create(data, done) { if (typeof data !== 'object') { done = data; data = {}; } const self = this; return anotherClass.create(data) .then(ac => { const d = {}; d[params.through.relationNameFor(fk)] = self; d[params.through.relationNameFor(fk2)] = ac; return params.through.create(d) .then(() => ac) .catch(e => { return ac.destroy() .then(() => { throw e; }); }); }) .then(ac => { if (typeof done === 'function') { done(null, ac); } else { return ac; } }) .catch(err => { if (typeof done === 'function') { done(err); } else { throw err; } }); }; scopeMethods.add = function(acInst, data, done) { if (typeof data === 'function') { done = data; data = {}; } if (typeof data === 'undefined') { data = {}; } const query = {}; query[fk] = this.id; data[params.through.relationNameFor(fk)] = this; query[fk2] = acInst.id || acInst; data[params.through.relationNameFor(fk2)] = acInst; return params.through.findOrCreate({ where: query }, data) .then(through => { if (typeof done === 'function') { done(null, through); } else { return through; } }) .catch(err => { if (typeof done === 'function') { done(err); } else { throw err; } }); }; scopeMethods.remove = function(acInst, done) { const q = {}; q[fk] = this.id; q[fk2] = acInst.id || acInst; return params.through.findOne({ where: q }) .then(d => { if (d) { return d.destroy(); } }) .then(() => { if (typeof done === 'function') { done(); } }) .catch(e => { if (typeof done === 'function') { done(e); } else { throw e; } }); }; delete scopeMethods.destroy; } defineScope(this.prototype, params.through || anotherClass, methodName, function() { const filter = {}; filter.where = {}; filter.where[fk] = this.id; if (params.through) { filter.collect = i8n.camelize(anotherClass.modelName, true); filter.include = filter.collect; } return filter; }, scopeMethods); if (!params.through) { // obviously, anotherClass should have attribute called `fk` anotherClass.schema.defineForeignKey(anotherClass.modelName, fk, this.modelName); } function find(id, cb) { const id_ = this.id; return anotherClass.find(id) .then(inst => { if (!inst) { throw new Error('Not found'); } if (inst[fk] && inst[fk].toString() === id_.toString()) { if (typeof cb === 'function') { cb(null, inst); } else { return inst; } } else { throw new Error('Permission denied'); } }) .catch(err => { if (typeof cb === 'function') { cb(err); } else { throw err; } }); } function destroy(id, cb) { const id_ = this.id; return anotherClass.find(id) .then(inst => { if (!inst) { throw new Error('Not found'); } if (inst[fk] && inst[fk].toString() === id_.toString()) { return inst.destroy() .then(() => { if (typeof cb === 'function') { cb(); } }); } throw new Error('Permission denied'); }) .catch(err => { if (typeof cb === 'function') { cb(err); } else { throw err; } }); } }; /** * Declare belongsTo relation * * @param {Class} anotherClass - class to belong * @param {Object} params - configuration {as: 'propertyName', foreignKey: 'keyName'} * * **Usage examples** * Suppose model Post have a *belongsTo* relationship with User (the author of the post). You could declare it this way: * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); * * When a post is loaded, you can load the related author with: * post.author(function(err, user) { * // the user variable is your user object * }); * * The related object is cached, so if later you try to get again the author, no additional request will be made. * But there is an optional boolean parameter in first position that set whether or not you want to reload the cache: * post.author(true, function(err, user) { * // The user is reloaded, even if it was already cached. * }); * * This optional parameter default value is false, so the related object will be loaded from cache if available. */ Model.belongsTo = function(anotherClass, params) { params = params || {}; if (typeof anotherClass === 'string') { params.as = anotherClass; if (params.model) { anotherClass = params.model; } else { const anotherClassName = anotherClass.toLowerCase(); Object.keys(this.schema.models).forEach(name => { if (name.toLowerCase() === anotherClassName) { anotherClass = this.schema.models[name]; } }); } } const methodName = params.as || i8n.camelize(anotherClass.modelName, true); const fk = params.foreignKey || methodName + 'Id'; this.relations[methodName] = { type: 'belongsTo', keyFrom: fk, keyTo: 'id', modelTo: anotherClass, multiple: false }; this.schema.defineForeignKey(this.modelName, fk, anotherClass.modelName); this.prototype.__finders__ = this.prototype.__finders__ || {}; this.prototype.__finders__[methodName] = function(id) { if (id === null || !this[fk]) { return Promise.resolve(null); } const fk_ = this[fk].toString(); return anotherClass.find(id) .then(inst => { if (!inst) { return null; } if (inst.id.toString() === fk_) { return inst; } throw new Error('Permission denied'); }); }; this.prototype[methodName] = function(p) { const self = this; const cachedValue = this.__cachedRelations && this.__cachedRelations[methodName]; // acts as setter if (p instanceof Model) { this.__cachedRelations[methodName] = p; return this.updateAttribute(fk, p.id); } // acts as async getter if (typeof cachedValue === 'undefined') { return this.__finders__[methodName].call(self, this[fk]) .then(inst => { self.__cachedRelations[methodName] = inst; if (typeof p === 'function') { p(null, inst); } else { return inst; } }); } // return cached relation if (typeof p === 'function') { p(null, cachedValue); } else { return Promise.resolve(cachedValue); } }; }; /** * Many-to-many relation * * Post.hasAndBelongsToMany('tags'); creates connection model 'PostTag' */ Model.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) { params = params || {}; const models = this.schema.models; if (typeof anotherClass === 'string') { params.as = anotherClass; if (params.model) { anotherClass = params.model; } else { anotherClass = lookupModel(i8n.singularize(anotherClass)) || anotherClass; } if (typeof anotherClass === 'string') { throw new Error('Could not find "' + anotherClass + '" relation for ' + this.modelName); } } if (!params.through) { const name1 = this.modelName + anotherClass.modelName; const name2 = anotherClass.modelName + this.modelName; params.through = lookupModel(name1) || lookupModel(name2) || this.schema.define(name1); } params.through.belongsTo(this); params.through.belongsTo(anotherClass); this.hasMany(anotherClass, { as: params.as, through: params.through }); function lookupModel(modelName) { const lookupClassName = modelName.toLowerCase(); let found; Object.keys(models).forEach(name => { if (name.toLowerCase() === lookupClassName) { found = models[name]; } }); return found; } };