UNPKG

vitamin

Version:

Data Mapper library for Node.js applications

898 lines (703 loc) 24.3 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _invalidRelationObject = require('./errors/invalid-relation-object'); var _invalidRelationObject2 = _interopRequireDefault(_invalidRelationObject); var _vitaminEvents = require('vitamin-events'); var _vitaminEvents2 = _interopRequireDefault(_vitaminEvents); var _base = require('./relations/base'); var _base2 = _interopRequireDefault(_base); var _collection = require('./collection'); var _collection2 = _interopRequireDefault(_collection); var _registry = require('./registry'); var _registry2 = _interopRequireDefault(_registry); var _bluebird = require('bluebird'); var _bluebird2 = _interopRequireDefault(_bluebird); var _model = require('./model'); var _model2 = _interopRequireDefault(_model); var _query = require('./query'); var _query2 = _interopRequireDefault(_query); var _underscore = require('underscore'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * @class Mapper */ var _class = function () { /** * Mapper class constructor * * @param {Object} options * @constructor */ function _class() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _classCallCheck(this, _class); this.events = {}; this.methods = {}; this.statics = {}; this.relations = {}; this.attributes = {}; this.defaults = null; this.tableName = null; this.primaryKey = 'id'; this.timestamps = false; this.connection = 'default'; this.createdAtColumn = 'created_at'; this.updatedAtColumn = 'updated_at'; (0, _underscore.extend)(this, options); // set up the model class for this mapper this._setupModel(options.modelClass || _model2.default); // set up the event emitter this.emitter = new _vitaminEvents2.default(); this._registerEvents(this.events); } /** * Inheritance helper * * @param {Object} props * @param {Object} statics * @return constructor function * @deprecated */ _createClass(_class, [{ key: 'mapper', /** * Get the model mapper from the registry * * @param {String} name * @return mapper */ value: function mapper(name) { return _registry2.default.get(name); } /** * Get the model default attributes * * @return function or plain object */ }, { key: 'getDefaults', value: function getDefaults() { var _this2 = this; return this.defaults ? this.defaults : function () { return (0, _underscore.reduce)(_this2.attributes, function (memo, config, attr) { if ((0, _underscore.has)(config, 'defaultValue')) memo[attr] = (0, _underscore.result)(config, 'defaultValue'); return memo; }, {}); }; } /** * Load the given relationships of the given models * * @param {Model|Array} model * @param {Array} relations * @return promise */ }, { key: 'load', value: function load(model) { var _newQuery; // TODO deprecate the use of single model, always call it with an array var models = (0, _underscore.isArray)(model) ? model : [model]; for (var _len = arguments.length, relations = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { relations[_key - 1] = arguments[_key]; } return (_newQuery = this.newQuery()).withRelated.apply(_newQuery, relations).loadRelated(models).return(model); } /** * Create a new record into the database * * @param {Object} attrs * @param {Array} returning * @return promise */ }, { key: 'create', value: function create(attrs) { var returning = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ['*']; return this.save(this.newInstance(attrs), returning); } /** * Create many instances of the related model * * @param {Array} records * @parma {Array} returning * @return promise */ }, { key: 'createMany', value: function createMany(records) { var _this3 = this; var returning = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ['*']; return _bluebird2.default.map(records, function (attrs) { return _this3.create(attrs, returning); }); } /** * Save the model in the database * * @param {Model} model * @param {Array} returning * @return promise */ }, { key: 'save', value: function save(model) { var _this4 = this, _arguments = arguments; var returning = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ['*']; return _bluebird2.default.resolve(model).tap(function () { return _this4.emitter.emit('saving', model); }).tap(function () { return model.exists ? _this4._update.apply(_this4, _arguments) : _this4._insert.apply(_this4, _arguments); }).tap(function () { return _this4.emitter.emit('saved', model); }); } /** * Save the given models * * @param {Array} models * @parma {Array} returning * @return promise */ }, { key: 'saveMany', value: function saveMany(models) { var _this5 = this; var returning = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ['*']; return _bluebird2.default.map(models, function (model) { return _this5.save(model, returning); }); } /** * Delete the model from the database * * @param {Model} model * @return promise */ }, { key: 'destroy', value: function destroy(model) { var _this6 = this; return _bluebird2.default.resolve(model).tap(function () { return _this6.emitter.emit('deleting', model); }).tap(function () { return _this6.newQuery().where(_this6.primaryKey, model.getId()).destroy(); }).tap(function () { return _this6.emitter.emit('deleted', model); }); } /** * Delete the given models from the database * * @param {Array} models * @return promise */ }, { key: 'destroyMany', value: function destroyMany(models) { var _this7 = this; return _bluebird2.default.map(models, function (model) { return _this7.destroy(model); }); } /** * Get a fresh timestamp for the model * * @return string ISO time */ }, { key: 'freshTimestamp', value: function freshTimestamp() { return new Date().toISOString(); } /** * Update the model's update timestamp * * @param {Model} model * @return promise */ }, { key: 'touch', value: function touch(model) { if (this.timestamps && this.updatedAtColumn) { return this.save(model.set(this.updatedAtColumn, this.freshTimestamp())); } return _bluebird2.default.resolve(model); } /** * Touch the given models * * @param {Array} models * @return promise */ }, { key: 'touchMany', value: function touchMany(models) { var _this8 = this; return _bluebird2.default.map(models, function (model) { return _this8.touch(model); }); } /** * Create a new model instance * * @param {Object} data * @param {Boolean} exists * @return model instance */ }, { key: 'newInstance', value: function newInstance() { var _modelClass; var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var exists = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; return (_modelClass = this.modelClass).make.apply(_modelClass, arguments); } /** * Get the model query builder * * @return Query instance */ }, { key: 'newQuery', value: function newQuery() { var client = _registry2.default.connection(this.connection); var query = new _query2.default(client.queryBuilder()); return query.from(this.tableName).setModel(this); } /** * Create a new collection of models * * @param {Array} models * @return Collection instance */ }, { key: 'newCollection', value: function newCollection() { var models = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; return new _collection2.default(models).setMapper(this); } /** * Helper to create a collection of models * * @param {Array} records * @return collection */ }, { key: 'createModels', value: function createModels(records) { var _this9 = this; return this.newCollection((0, _underscore.map)(records, function (data) { return _this9.newInstance(data, true); })); } /** * Create the relationship mapper * * @param {String} name * return relation instance */ }, { key: 'getRelation', value: function getRelation(name) { if (this.relations[name]) { var relation = this.relations[name].call(this); if (relation instanceof _base2.default) return relation.setName(name); } throw new _invalidRelationObject2.default(name); } /** * Define a has-one relationship * * @param {Model} related * @param {String} fk target model foreign key * @param {String} pk parent model primary key * @return relation */ }, { key: 'hasOne', value: function hasOne(related) { var fk = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var pk = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var HasOne = require('./relations/has-one').default; if (!pk) pk = this.primaryKey; if (!fk) fk = this.name + '_id'; if ((0, _underscore.isString)(related)) related = this.mapper(related); return new HasOne(this, related, fk, pk); } /** * Define a morph-one relationship * * @param {Model} related * @param {String} name of the morph * @param {String} type target model type column * @param {String} fk target model foreign key * @param {String} pk parent model primary key * @return relation */ }, { key: 'morphOne', value: function morphOne(related, name) { var type = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var fk = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; var pk = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null; var MorphOne = require('./relations/morph-one').default; if (!pk) pk = this.primaryKey; if (!type) type = name + '_type'; if (!fk) fk = name + '_id'; if ((0, _underscore.isString)(related)) related = this.mapper(related); return new MorphOne(this, related, type, fk, pk); } /** * Define a has-many relationship * * @param {Model} related * @param {String} fk target model foreign key * @param {String} pk parent model primary key * @return relation */ }, { key: 'hasMany', value: function hasMany(related) { var fk = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var pk = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var HasMany = require('./relations/has-many').default; if (!pk) pk = this.primaryKey; if (!fk) fk = this.name + '_id'; if ((0, _underscore.isString)(related)) related = this.mapper(related); return new HasMany(this, related, fk, pk); } /** * Define a morph-many relationship * * @param {Model} related * @param {String} name of the morph * @param {String} type target model type column * @param {String} fk target model foreign key * @param {String} pk parent model primary key * @return relation */ }, { key: 'morphMany', value: function morphMany(related, name) { var type = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var fk = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; var pk = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null; var MorphMany = require('./relations/morph-many').default; if (!pk) pk = this.primaryKey; if (!type) type = name + '_type'; if (!fk) fk = name + '_id'; if ((0, _underscore.isString)(related)) related = this.mapper(related); return new MorphMany(this, related, type, fk, pk); } /** * Define a belongs-to relationship * * @param {Model} related * @param {String} fk parent model foreign key * @param {String} pk target model primary key * @return relation */ }, { key: 'belongsTo', value: function belongsTo(related) { var fk = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var pk = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var BelongsTo = require('./relations/belongs-to').default; if ((0, _underscore.isString)(related)) related = this.mapper(related); if (!pk) pk = related.primaryKey; if (!fk) fk = related.name + '_id'; return new BelongsTo(this, related, fk, pk); } /** * Define a morph-to relationship * * @param {String} name of the morph * @param {String} type column name * @param {String} fk * @param {String} pk * @return relation */ }, { key: 'morphTo', value: function morphTo(name) { var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var fk = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var pk = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; var MorphTo = require('./relations/morph-to').default; if (!fk) fk = name + '_id'; if (!pk) pk = this.primaryKey; if (!type) type = name + '_type'; return new MorphTo(this, type, fk, pk); } /** * Define a belongs-to-many relationship * * @param {Model} related * @param {String} pivot table name * @param {String} pfk parent model foreign key * @param {String} tfk target model foreign key * @return relation */ }, { key: 'belongsToMany', value: function belongsToMany(related, pivot) { var pfk = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var tfk = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; var BelongsToMany = require('./relations/belongs-to-many').default; if ((0, _underscore.isString)(related)) related = this.mapper(related); if (!pfk) pfk = this.name + '_id'; if (!tfk) tfk = related.name + '_id'; return new BelongsToMany(this, related, pivot, pfk, tfk); } /** * Define a morph-to-many relationship * * @param {Model} related * @param {String} pivot table name * @param {String} name of the morph * @param {String} type column name * @param {String} pfk parent model foreign key * @param {String} tfk target model foreign key * @return relation */ }, { key: 'morphToMany', value: function morphToMany(related, pivot, name) { var type = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; var pfk = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null; var tfk = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; var MorphToMany = require('./relations/morph-to-many').default; if ((0, _underscore.isString)(related)) related = this.mapper(related); if (!pfk) pfk = name + '_id'; if (!type) type = name + '_type'; if (!tfk) tfk = related.name + '_id'; return new MorphToMany(this, related, pivot, type, pfk, tfk); } /** * Define the inverse of morph-to-many relationship * * @param {Model} related * @param {String} pivot table name * @param {String} name of the morph * @param {String} type column name * @param {String} pfk parent model foreign key * @param {String} tfk target model foreign key * @return relation */ }, { key: 'morphedByMany', value: function morphedByMany(related, pivot, name) { var type = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; var pfk = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null; var tfk = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; if (!tfk) tfk = name + '_id'; if (!pfk) pfk = this.name + '_id'; return this.morphToMany(related, pivot, name, type, pfk, tfk); } /** * Add a listener for the given event * * @param {String} event * @param {Function} fn * @return this model */ }, { key: 'on', value: function on(event, fn) { var _emitter; (_emitter = this.emitter).on.apply(_emitter, arguments); return this; } /** * Remove an event listener * * @param {String} event * @param {Function} fn * @return this model */ }, { key: 'off', value: function off(event, fn) { var _emitter2; (_emitter2 = this.emitter).off.apply(_emitter2, arguments); return this; } /** * Trigger an event with arguments * * @param {String} event * @param {Array} args * @return promise */ }, { key: 'emit', value: function emit(event) { var _emitter3; for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } return (_emitter3 = this.emitter).emit.apply(_emitter3, arguments); } /** * Override it to register shared events between mappers * * @param {Object} events * @private */ }, { key: '_registerEvents', value: function _registerEvents() { var _this10 = this; var events = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; (0, _underscore.each)(events, function (listener, name) { ((0, _underscore.isArray)(listener) ? listener : [listener]).forEach(function (fn) { return _this10.emitter.on(name, fn); }); }); } /** * Perform a model insert operation * * @param {Model} model * @param {String|Array} returning * @return promise * @private */ }, { key: '_insert', value: function _insert(model, returning) { var _this11 = this; return _bluebird2.default.resolve(model).tap(function () { return _this11.emitter.emit('creating', model); }).tap(function () { return _this11.timestamps && _this11._updateTimestamps(model); }).tap(function () { return _this11.newQuery().insert(model.getData(), returning).then(function (res) { return _this11._emulateReturning(model, res, returning); }).then(function (res) { return model.setData(res, true); }); }).tap(function () { return _this11.emitter.emit('created', model); }); } /** * Perform a model update operation * * @param {Model} model * @param {String|Array} returning * @return promise * @private */ }, { key: '_update', value: function _update(model, returning) { var _this12 = this; return _bluebird2.default.resolve(model).tap(function () { return _this12.emitter.emit('updating', model); }).tap(function () { return _this12.timestamps && _this12._updateTimestamps(model); }).tap(function () { return _this12.newQuery().where(_this12.primaryKey, model.getId()).update(model.getDirty(), returning).then(function (res) { return _this12._emulateReturning(model, res, returning); }).then(function (res) { return model.setData(res, true); }); }).tap(function () { return _this12.emitter.emit('updated', model); }); } /** * Emulate the `returning` SQL clause * * @param {Model} model * @param {Array} result * @param {Array} columns * @private */ }, { key: '_emulateReturning', value: function _emulateReturning(model, result) { var columns = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ['*']; var id = result[0]; if ((0, _underscore.isObject)(id)) return id;else { var qb = this.newQuery().toBase(); if (model.exists) id = model.getId(); // resolve with a plain object to populate the model data return qb.where(this.primaryKey, id).first(columns); } } /** * Update the creation and update timestamps * * @param {Model} model * @private */ }, { key: '_updateTimestamps', value: function _updateTimestamps(model) { var time = this.freshTimestamp(); var useCreatedAt = !!this.createdAtColumn; var useUpdatedAt = !!this.updatedAtColumn; if (useUpdatedAt && !model.isDirty(this.updatedAtColumn)) { model.set(this.updatedAtColumn, time); } if (useCreatedAt && !model.exists && !model.isDirty(this.createdAtColumn)) { model.set(this.createdAtColumn, time); } } /** * Set up the model class * * @param {Model} model constructor * @private */ }, { key: '_setupModel', value: function _setupModel(model) { var _this = this; var proto = (0, _underscore.clone)(this.methods); // add prototype properties proto.mapper = this; proto.defaults = this.getDefaults(); proto.idAttribute = this.primaryKey; // add relationship accessors (0, _underscore.each)(this.relations, function (_, name) { proto[name] = function () { return _this.getRelation(name).addConstraints(this); }; }); this.modelClass = model.extend(proto, this.statics); } }], [{ key: 'extend', value: function extend() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var statics = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var parent = this; var child = function child() { parent.apply(this, arguments); }; // use custom constructor if ((0, _underscore.has)(props, 'constructor')) child = props.constructor; // set the prototype chain to inherit from `parent` child.prototype = Object.create(parent.prototype, { constructor: { value: child, writable: true, configurable: true } }); // add static and instance properties (0, _underscore.extend)(child, statics); (0, _underscore.extend)(child.prototype, props); // fix extending static properties Object.setPrototypeOf ? Object.setPrototypeOf(child, parent) : child.__proto__ = parent; return child; } }]); return _class; }(); exports.default = _class;