UNPKG

jugglingdb

Version:

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

365 lines (336 loc) 12.2 kB
'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /** * Dependencies */ var i8n = require('inflection'); var defineScope = require('./scope.js').defineScope; /** * Relations mixins for ./model.js */ var Model = require('./model.js'); Model.relationNameFor = function relationNameFor(foreignKey) { var _this = this; var found = void 0; Object.keys(this.relations).forEach(function (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) { var _this2 = this; var thisClassName = this.modelName; params = params || {}; if (typeof anotherClass === 'string') { params.as = anotherClass; if (params.model) { anotherClass = params.model; } else { var anotherClassName = i8n.singularize(anotherClass).toLowerCase(); Object.keys(this.schema.models).forEach(function (name) { if (name.toLowerCase() === anotherClassName) { anotherClass = _this2.schema.models[name]; } }); } } var methodName = params.as || i8n.camelize(i8n.pluralize(anotherClass.modelName), true); var 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); var scopeMethods = { find: find, destroy: destroy }; var fk2 = void 0; 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 === 'undefined' ? 'undefined' : _typeof(data)) !== 'object') { done = data; data = {}; } var self = this; return anotherClass.create(data).then(function (ac) { var d = {}; d[params.through.relationNameFor(fk)] = self; d[params.through.relationNameFor(fk2)] = ac; return params.through.create(d).then(function () { return ac; }).catch(function (e) { return ac.destroy().then(function () { throw e; }); }); }).then(function (ac) { if (typeof done === 'function') { done(null, ac); } else { return ac; } }).catch(function (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 = {}; } var 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(function (through) { if (typeof done === 'function') { done(null, through); } else { return through; } }).catch(function (err) { if (typeof done === 'function') { done(err); } else { throw err; } }); }; scopeMethods.remove = function (acInst, done) { var q = {}; q[fk] = this.id; q[fk2] = acInst.id || acInst; return params.through.findOne({ where: q }).then(function (d) { if (d) { return d.destroy(); } }).then(function () { if (typeof done === 'function') { done(); } }).catch(function (e) { if (typeof done === 'function') { done(e); } else { throw e; } }); }; delete scopeMethods.destroy; } defineScope(this.prototype, params.through || anotherClass, methodName, function () { var 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) { var id_ = this.id; return anotherClass.find(id).then(function (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(function (err) { if (typeof cb === 'function') { cb(err); } else { throw err; } }); } function destroy(id, cb) { var id_ = this.id; return anotherClass.find(id).then(function (inst) { if (!inst) { throw new Error('Not found'); } if (inst[fk] && inst[fk].toString() === id_.toString()) { return inst.destroy().then(function () { if (typeof cb === 'function') { cb(); } }); } throw new Error('Permission denied'); }).catch(function (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) { var _this3 = this; params = params || {}; if (typeof anotherClass === 'string') { params.as = anotherClass; if (params.model) { anotherClass = params.model; } else { var anotherClassName = anotherClass.toLowerCase(); Object.keys(this.schema.models).forEach(function (name) { if (name.toLowerCase() === anotherClassName) { anotherClass = _this3.schema.models[name]; } }); } } var methodName = params.as || i8n.camelize(anotherClass.modelName, true); var 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); } var fk_ = this[fk].toString(); return anotherClass.find(id).then(function (inst) { if (!inst) { return null; } if (inst.id.toString() === fk_) { return inst; } throw new Error('Permission denied'); }); }; this.prototype[methodName] = function (p) { var self = this; var 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(function (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 || {}; var 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) { var name1 = this.modelName + anotherClass.modelName; var 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) { var lookupClassName = modelName.toLowerCase(); var found = void 0; Object.keys(models).forEach(function (name) { if (name.toLowerCase() === lookupClassName) { found = models[name]; } }); return found; } };