!function() { var Class = require('ee-class') , log = require('ee-log') , EventEmitter = require('ee-event-emitter') , clone = require('clone') , asyncMethod = require('async-method'); module.exports = new Class({ inherits: EventEmitter , init: function(pager, response, requestConfig, responseData) { this.pager = pager; this.response = response; this.originalRequestConfig = requestConfig; this.responseData = responseData } /** * load the next set of items * * @param <function> optional callback, if omitted promise will be returned */ , next: asyncMethod(function(callback) { if (!this.pager.hasNextPage(this.response, this.responseData)) callback(new Error('Cannot load the next page, the last page was already loaded!')); else { var newRequest = clone(this.originalRequestConfig); this.pager.prepareNextRequest(this.originalRequestConfig, newRequest, this.response , this.responseData); this.emit('request', newRequest, callback); } }) /** * load the previous set of items * * @param <function> optional callback, if omitted promise will be returned */ , previous: asyncMethod(function(callback) { if (!this.pager.hasPreviousPage(this.response, this.responseData)) callback(new Error('Cannot load the previous page, the first page was already loaded!')); else { var newRequest = clone(this.originalRequestConfig); this.pager.preparePreviousRequest(this.originalRequestConfig, newRequest, this.response , this.responseData); this.emit('request', newRequest, callback); } }) }); }();
!function() { 'use strict'; var Class = require('ee-class') , log = require('ee-log') , type = require('ee-types') , EventEmitter = require('ee-event-emitter') , argv = require('ee-argv') , async = require('ee-async') , DBCluster = require('related-db-cluster') , Migration = require('./Migration') , Database = require('./Database') , StaticORM = require('./StaticORM') , Set = require('./Set') , ExtensionManager = require('./ExtensionManager') , ModelDefinition = require('./ModelDefinition') , debug = argv.has('debug-sql') , Promise = Promise || require('es6-promise').Promise , dev = argv.has('dev-orm') , asyncMethod = require('async-method') , Selector = require('./Selector') , ORM; // we need a single instance of the selector classs var selector = new Selector(); ORM = new Class({ inherits: EventEmitter // indicates if the orm was loaded , _loaded: false // indicates that the laoding process has started , _loading: false // grant the orm acces to the static // selector collection , _selector: selector // driver names , _driverNames: { postgres : 'related-postgres-connection' , mysql : 'related-mysql-connection' } /* * class constructor, initialize everything */ , init: function(options, pass, host, db, dbName, rdbmsType) { var opts; // the constructor accepts also simple conneciton paramters if (type.string(options) && type.string(pass) && type.string(db)) { // build options object opts = {}; opts[db] = {}; opts[db].type = rdbmsType || 'postgres'; opts[db].hosts = []; opts[db].database = dbName; opts[db].hosts.push({ host : host || '127.0.0.1' , username : options , password : pass , port : rdbmsType === 'mysql' ? 3306 : 5432 , mode : 'readwrite' }); options = opts; } // store my options Class.define(this, '_options', Class(options)); // list of databse defintions Class.define(this, '_dbs', Class([])); // all loaded extension Class.define(this, '_extensions', Class(new ExtensionManager(this))); // the actual database connections Class.define(this, '_databases', Class({})); // hosts without loaded db Class.define(this, '_noDB', Class([])); // indicators Class.define(this, '_loaded', Class(false).Writable()); Class.define(this, '_loading', Class(false).Writable()); // let the user create a new schema Class.define(this, 'Schema', Class(function(schemaName, callback) { return this.createSchema(schemaName, callback); }.bind(this))); // db connectivity this._initializeDatabases(options); } /** * creates a serializable migration object * * @param <string> version or filename */ , createMigration: function(version) { return new Migration(version); } /** * add a new configuration and reload the orm * * @partm <object> config * @param <function> optional callbac */ , addConfig: asyncMethod(function(config, callback) { if (!type.array(config)) config = [config]; if (!type.function(callback)) throw new Error('Please pass a config to the addConfig method!'); // db connectivity this._initializeDatabases(config); // reload this.reload(callback); }) /** * shuts the orm down, ends all connections * * @param <function> optional callbac */ , end: asyncMethod(function(callback) { Promise.all(Object.keys(this._databases).map(function(databaseName) { delete this[databaseName]; return this._databases[databaseName].end(); }.bind(this))).then(function() { callback(); }).catch(callback); }) /** * create a new database * * @param <object> database config or DBCluster instance * @param <string> database name * @param <function> optional callback, if not passed a promeise is returned */ , createDatabase: asyncMethod(function(config, databaseName, callback) { var db = this._getDBClusterInstance(config, true); // execute the query db.query({ mode: 'create' , query: { database: databaseName } , callback: function(err) { db.end(function() { callback(err); }.bind(this)); } }) }) /** * drop a database * * @param <object> database config or DBCluster instance * @param <string> database name * @param <function> optional callback, if not passed a promeise is returned */ , dropDatabase: asyncMethod(function(config, databaseName, callback) { var db = this._getDBClusterInstance(config, true); // execute the query db.query({ mode: 'drop' , query: { database: databaseName } , callback: function(err) { db.end(function() { callback(err); }.bind(this)); } }) }) /** * drop a schema * * @param <object> database config or DBCluster instance * @param <string> schema name * @param <function> optional callback, if not passed a promeise is returned */ , dropSchema: asyncMethod(function(config, schemaName, callback) { var db = this._getDBClusterInstance(config); // execute the query db.query({ mode: 'drop' , query: { schema: schemaName } , callback: function(err) { db.end(function() { callback(err); }.bind(this)); } }) }) /** * create a new schema * * @param <object> database config or DBCluster instance * @param <string> schema name * @param <function> optional callback, if not passed a promeise is returned */ , createSchema: asyncMethod(function(config, schemaName, callback) { var db = this._getDBClusterInstance(config); // execute the query db.query({ mode: 'create' , query: { schema: schemaName } , callback: function(err) { db.end(function() { callback(err); }.bind(this)); } }) }) /** * instantiartes a db clsuter instance * * @param <object> config * @param <Boolean> flag if the schema and dtabase property should be omitted */ , _getDBClusterInstance: function(config, noDatabase) { var db = new DBCluster({}, this._loadDriver(config.type)); config = this._prepareConfig(config, noDatabase); config.hosts.forEach(function(hostConfig){ db.addNode(hostConfig); }.bind(this)); return db; } /** * creates a copy of the config object, removes the db and schema * entry if the flag is set * * @param <object> config * @param <Boolean> flag if the schema and dtabase property should be omitted */ , _prepareConfig: function(config, excludeDatabase) { var copy = {}; Object.keys(config).forEach(function(key) { if (key === 'hosts') { copy.hosts = []; config.hosts.forEach(function(host) { var hostCopy = {}; Object.keys(host).forEach(function(hostKey) { hostCopy[hostKey] = host[hostKey]; }); if (!excludeDatabase) hostCopy.database = config.database || config.schema; copy.hosts.push(hostCopy); }); } else if (!excludeDatabase || (key !== 'schema' && key !== 'database')) { copy[key] = config[key]; } }); return copy; } /* * accepts extension to the orm */ , use: function(extension) { if (extension.isRelatedExtension && extension.isRelatedExtension()) { // check for selectors if (extension.hasSelectorExtensions()) { extension.getSelectorExtensions().forEach(this._selector.registerExtension.bind(this._selector)); } // extension may use the orm extension.setOrm(this); // register this._extensions.register(extension); } else { // legacy // old school extension if (!extension || !type.function(extension.isExtension) || !extension.isExtension()) throw new Error('cannot add extion to orm, it doesn\'t register itself as one!'); // register the extension this._extensions.register(extension); } return this; } /* * return a specific extension */ , getExtension: function(name) { return this._extensions.get(name); } /* * indicates if the orm was laoded already */ , isLoaded: function() { return !!this._loaded; } /* * the load method is mainly used when working with promises */ , load: function(callback) { if (callback) { if (this._loaded && !this._loading) callback(); else if (this._loading) this.once('load', callback); else this.reload(callback); } else { return new Promise(function(resolve, reject) { var cb = function(err) { if (err) reject(err); else resolve(this); }.bind(this); if (this._loaded && !this._loading) cb(); else if (this._loading) this.once('load', cb); else this.reload(cb); }.bind(this)); } } /** * rebuilds the orm from scratch, basically used * for integrating extensions very late */ , reload: asyncMethod(function(callback) { if (!this._loading) { this._loaded = false; process.nextTick(function() { this._initializeOrm(function(err) { this._loaded = true; this._loading = false; this.emit('load', err); if (callback) callback(err, this); }.bind(this)); }.bind(this)); } else { this.once('load', function() { this.relaod(callback); }.bind(this)); } }) /* * return the ORM object used to create filters & more */ , getORM: function() { return ORM; } /* * initializtes the orm, reads the db definition, checks if for relations * and their names */ , _initializeOrm: function(callback) { if (dev) log.debug('initializing ORM ...'); // inidcate that we're busy loading the db this._loading = true; async.each(this._dbs // remove existing , function(db, next) { if (this[db.databaseName] && this[db.databaseName].createTransaction) { if (dev) log.debug('removing existing db instance «'+db.databaseName+'»...'); delete this[db.databaseName]; } next(null, db); }.bind(this) // get definition from database , function(db, next) { this._databases[db.databaseName].describe([db.databaseName], function(err, databases){ if (dev) log.debug('got db definition for «'+db.databaseName+'»...'); if (err) next(err); else { // push config to next step next(null, db, databases[db.databaseName]); } }.bind(this)); }.bind(this) // initialize orm per databse , function(db, definition, next) { var newDefinition = {}; if (this[db.databaseName]) next(new Error('Failed to load ORM for database «'+db.databaseName+'», the name is reserved for the orm.').setName('ORMException')); else if (definition.schemaExists()) { // build the model definitions from the raw definitions Object.keys(definition).forEach(function(modelName) { newDefinition[modelName] = new ModelDefinition(definition[modelName]); }.bind(this)); // create names for mapping / reference accessor, handle duplicates this._manageAccessorNames(newDefinition, db); if (dev) log.debug('creating new db instance for «'+db.databaseName+'»...'); this[db.alias || db.databaseName] = new Database({ orm: this , definition: newDefinition , database: this._databases[db.databaseName] , timeouts: db.config.timeouts , databaseName: db.databaseName , extensions: this._extensions }); this[db.alias || db.databaseName].on('load', next); } else next(); }.bind(this) // check for errors , function(err, results){ if (dev) log.warn('all dbs loaded ...'); if (err) callback(err); else callback(); }.bind(this)); } /* * checks if there are names that are used twice, if so it * disables them for direct access */ , _manageAccessorNames: function(definition, db) { var timestamps = db.config.timestamps || {} , nestedSet = db.config.nestedSet || {}; Object.keys(definition).forEach(function(tablename){ var model = definition[tablename] , usedNames = {} , isNestedSet = 0; Object.keys(model.columns).forEach(function(columnName){ var column = model.columns[columnName] , name; if (column.mapsTo) { column.mapsTo.forEach(function(mapping){ name = mapping.name; if (name !== model.name) { if (model.columns[name]) { // the name is used by a column, cannot reference directly if (debug) log.warn('«'+model.name+'» cannot use the accessor «'+name+'», it\'s already used by another property'); mapping.useGenericAccessor = true; } else if (usedNames[name]) { // the name was used before by either a mapping or a reference // we cannot use it if (debug) log.warn('«'+model.name+'» cannot use the accessor «'+name+'», it\'s already used by another property'); usedNames[name].useGenericAccessor = true; mapping.useGenericAccessor = true; } else usedNames[name] = mapping; } }.bind(this)); } if (column.belongsTo) { column.belongsTo.forEach(function(beloning){ name = beloning.name; if (model.columns[name]) { if (debug) log.warn('«'+model.name+'» cannot use the accessor «'+name+'», it\'s already used by another property'); // the name is used by a column, cannot reference directly beloning.useGenericAccessor = true; } else if (usedNames[name]) { if (debug) log.warn('«'+model.name+'» cannot use the accessor «'+name+'», it\'s already used by another property'); // the name was used before by either a mapping or a reference // we cannot use it usedNames[name].useGenericAccessor = true; beloning.useGenericAccessor = true; } else usedNames[name] = beloning; }.bind(this)); } if (column.referencedModel && column.referencedModel.name !== model.name) { name = column.referencedModel.name; if (model.columns[name]) { if (debug) log.warn('«'+model.name+'» cannot use the accessor «'+name+'», it\'s already used by another property'); // the name is used by a column, cannot reference directly column.useGenericAccessor = true; } else if (usedNames[name]) { if (debug) log.warn('«'+model.name+'» cannot use the accessor «'+name+'», it\'s already used by another property'); // the name was used before by either a mapping or a reference // we cannot use it usedNames[name].useGenericAccessor = true; column.useGenericAccessor = true; } else usedNames[name] = column; } }.bind(this)); // did we find a nested set on the model? if (isNestedSet === 2) { Class.define(model, 'isNestedSet', Class(true).Enumerable()); Class.define(model, 'nestedSetLeft', Class(nestedSet.left).Enumerable()); Class.define(model, 'nestedSetRight', Class(nestedSet.right).Enumerable()); } }.bind(this)); } /* * returns the orm databse object */ , getDatabase: function(id){ if (!type.string(id) || !id.length) throw new Error('cannot return a db without knowing which on to return (argument 0 must be the db id!)'); return this._databases[id]; } /* * yeah, this one is required */ , doItAll: function() { log.wtf('hui'); } /* * initializes the db clusters, connectors to the database */ , _initializeDatabases: function(options) { if (type.array(options)) { options.forEach(function(config) { if (!type.string(config.type)) throw new Error('['+config.schema+'] > Database type not in config specified (type: \'mysql\' / \'postgres\')!'); if (!type.array(config.hosts) || !config.hosts.length) throw new Error('['+config.schema+'] > Please add at least one host per db in the config!'); // check if there is anythin to load // if the user didnt specify a schema nor a database // we should not load one if (!config.schema && !config.database) { config.noDatabase = true; this._noDB.push(new DBCluster({}, this._loadDriver(config.type))); } else { this._dbs.push({ databaseName : config.schema , config : config , alias : config.alias }); // load db cluster this._databases[config.schema] = this._getDBClusterInstance(config); } }.bind(this)); } else if (type.object(options)) { Object.keys(options).forEach(function(databaseName){ if (!type.string(options[databaseName].type)) throw new Error('['+databaseName+'] > Database type not in config specified (type: \'mysql\' / \'postgres\')!'); if (!type.array(options[databaseName].hosts) || !options[databaseName].hosts.length) throw new Error('['+databaseName+'] > Please add at least one host per db in the config!'); this._dbs.push({ databaseName : databaseName , config : options[databaseName] }); // load db cluster this._databases[databaseName] = this._getDBClusterInstance(options[databaseName]); }.bind(this)); } //else throw new Error('no database configuration present!'); } /** * load a connection driver by type * * @param <String> driver type */ , _loadDriver: function(type) { var driver; if (this._driverNames[type]) { try { driver = require(this._driverNames[type]); } catch (e) { throw new Error('Failed to load connection driver for type «'+type+'» :'+e); } return driver; } else throw new Error('Failed to load connection driver for type «'+type+'» !'); } }); // set static methods on the ORM constructor Class.implement(new StaticORM(), ORM); // export the set so extensions can make use of it ORM.Set = Set; // export the selector interface selector.applyTo(ORM); // export module.exports = ORM; }();
!function() { var Class = require('ee-class') , log = require('ee-log') , request = require('request') , type = require('ee-types') , asyncMethod = require('async-method') , Promise = (Promise || require('es6-promise').Promise) , LeakyBucket = require('leaky-bucket') , debug = require('ee-argv').has('debug-request-rate-limiter') || process.env['debug-request-rate-limiter'] , logId = 0; module.exports = new Class({ // default rate limit rate: 60 // interval fot the rate in seconds , interval: 60 // http status returned by http requests // if we need to back off for some time , backoffCode: 429 // how long to wait until we should // continue to send requests after we // got the backoff status from the peer // unit: seconds , backoffTime: 10 // how long can a request qait until it // should be retuned with an error // unit: seconds , maxWaitingTime: 300 // backoff status indicating if we're // currently in a backoff phase , backoffTimer: null /** * constructor * * @param <number|object> rate or config object */ , init: function(options) { // parse options if (type.number(options)) { this.rate = options; } else if (type.object(options) && options !== null) { if (type.number(options.rate)) this.rate = options.rate; if (type.number(options.interval)) this.interval = options.interval; if (type.number(options.backoffCode)) this.backoffCode = options.backoffCode; if (type.number(options.backoffTime)) this.backoffTime = options.backoffTime; else this.backoffTime = Math.round(this.rate/5); if (type.number(options.maxWaitingTime)) this.maxWaitingTime = options.maxWaitingTime; } // id for logging purposes this.logId = logId++; // set up the leaky bucket this.bucket = new LeakyBucket({ capacity : this.rate , maxWaitingTime : this.maxWaitingTime , interval : this.interval }); } /** * send rate limited requests * * @param <object> request configuration * @param <function> optional callback, if omitted * a promise is retuned */ , request: asyncMethod(function(config, callback) { if (debug) log.debug('[%s] Got request for url %s ...', this.logId, config.url); this.bucket.throttle(function(err) { if (err) { // fail, it would take way too long // to execute the requesz if (debug) log.info('[%s] Throttling of the request on %s failed, max allowed waiting time of %s seconds exceeded ...', this.logId, config.url, this.maxWaitingTime); callback(new Error('The request was not executed because it would not be scheduled within the max waiting time!')); } else { // execute the request if (debug) log.debug('[%s] Request on %s is firing now...', this.logId, config.url); this._request(config, callback); } }.bind(this)); }) /** * send a request * * @param <object> request config * @param <function> callback */ , _request: function(config, callback) { request(config, function(err, response, body) { if (err) { if (debug) log.warn('[%s] The request for the url %s failed: %s ...', this.logId, config.url, err.message); callback(err); } else if (response.statusCode === this.backoffCode) { if (debug) log.debug('[%s] The peer returned the backoff status code %s for the url %s, backing off ...', this.logId, this.backoffCode, config.url); // got the backoff status code, wait some time if (!this.backoffTimer) { this.backoffTimer = setTimeout(function() { this.backoffTimer = null; }.bind(this), this.backoffTime*1000); if (debug) log.info('[%s] Pausing the leaky bucket for %s seconds ...', this.logId, this.backoffTime); // pause the bucket this.bucket.pause(this.backoffTime); } // add the request at the beginning of the queue this.bucket.reAdd(function(err) { if (err) callback(new Error('The request was not executed because it would not be scheduled within the max waiting time!')); else this._request(config, callback); }.bind(this)); } else { if (debug) log.debug('[%s] The request for the url %s succeeded with the status %s ...', this.logId, config.url, response.statusCode); callback(null, response); } }.bind(this)); } }); }();
!function() { var Class = require('ee-class') , log = require('ee-log') , type = require('ee-types') , EventEmitter = require('ee-event-emitter') , asyncMethod = require('async-method'); /** * the query builder is used to create list and * bulk update calls */ module.exports = new Class({ inherits: EventEmitter , init: function(options) { // store parameters passed to the query builder this.parameters = options.parameters; // maybe we got an id, store it if (this.parameters && this.parameters.length) { this.id = type.object(this.parameters[0]) && this.parameters[0] !== null ? this.parameters.id : this.parameters[0]; } // store references to the parent query builder if (options.parent) this.parent = options.parent; } /** * execute the list function, * build the requests using the middlewares * passed in the configurations */ , list: asyncMethod(function(callback) { var request = {}; // http method request.method = this._list.method; // set the default headers request.headers = this._list.headers; // build url request.url = this._buildListUrl(); // let the middleware do the rest this._list.requestBuilder(request, this.parameters); // offset && limit if (type.number(this.getLimit())) this._list.limitBuilder(request, this.getLimit()); if (type.number(this.getOffset())) this._list.offsetBuilder(request, this.getOffset()); // send to the RestfulAPIClient class this.emitRequest(request, callback); }) // get the limit from thwe tree , getLimit: function() { if (type.number(this._limit)) return this._limit; else if (this.parent) return this.parent.getLimit(); else return null; } // get the offset from thwe tree , getOffset: function() { if (type.number(this._offset)) return this._offset; else if (this.parent) return this.parent.getOffset(); else return null; } /** * limit the range of the request */ , limit: function(limit) { this._limit = limit; return this; } /** * offset the range of the request */ , offset: function(offset) { this._offset = offset; return this; } , _buildListUrl: function() { var url = ''; if (this.parent) url += this.parent._buildListUrl(); if (this.id) return url + this._list.url + '/' + this.id; else return url + this._list.url; } }); }();