!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);
            }
        })
    });
}();
Example #2
0
File: ORM.js Project: naval/related
!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;
        }
    });
}();