<commit_before>var loki = require('lokijs'),
q = require('q'),
fs = require('fs'),
_ = require('lodash'),
common = require('@screeps/common'),
config = common.configManager.config;

/**
 * @type Loki
 */
var db;

Object.assign(config.storage, {
    dbOptions: {autosave: true, autosaveInterval: 10000},
    getDb() {
        try {
            fs.statSync(process.env.DB_PATH);
        }
        catch (e) {
            fs.writeFileSync(process.env.DB_PATH, '');
        }
        return new loki(process.env.DB_PATH, config.storage.dbOptions);
    },
    loadDb() {
        db = config.storage.getDb();
        return q.ninvoke(db, 'loadDatabase', {})
            .then(upgradeDb);
    }
});

function upgradeDb() {
    if(!db.databaseVersion) {
        return;
    }
    if(db.databaseVersion < 2) {
        console.log("Upgrading database to version 2");

        var users = db.getCollection('users').find({money: {$gt: 0}});
        users.forEach(user => {
            user.money = Math.round(user.money * 1000);
            db.getCollection('users').update(user);
        });

        var orders = db.getCollection('market.orders').find({});
        orders.forEach(order => {
            console.log(order._id, order.price);
            order.price = Math.round(order.price * 1000);
            return db.getCollection('market.orders').update(order);
        });
    }

    db.databaseVersion = 2;
}

function updateDocument(doc, update) {
    if (update.$set) {
        _.extend(doc, update.$set);
    }
    if (update.$merge) {
        _.merge(doc, update.$merge);
    }
    if (update.$inc) {
        _.forEach(update.$inc, (val, key) => doc[key] = (doc[key] || 0) + val);
    }
    if (update.$unset) {
        for (var j in update.$unset) {
            delete doc[j];
        }
    }
    if(update.$addToSet) {
        for(let i in update.$addToSet) {
            if(!doc[i]) {
                doc[i] = [];
            }
            if(doc[i].indexOf(update.$addToSet[i]) == -1) {
                doc[i].push(update.$addToSet[i]);
            }
        }
    }
    if(update.$pull) {
        for(let i in update.$addToSet) {
            if(!doc[i]) {
                continue;
            }
            var idx = doc[i].indexOf(update.$pull[i]);
            if(idx != -1) {
                doc[i].splice(idx, 1);
            }
        }
    }
}

setInterval(function envCleanExpired() {
    var env = db.getCollection('env');
    var values = env.get(1);
    var expiration = env.get(2);
    var dirty = false;
    if (expiration) {
        for (var name in expiration.data) {
            if (Date.now() > expiration.data[name]) {
                dirty = true;
                if (values.data[name]) {
                    delete values.data[name];
                }
                delete expiration.data[name];
            }
        }
    }
    if (dirty) {
        env.update(values);
        env.update(expiration);
    }
}, 10000);

function getRandomString() {
    for (var val = Math.floor(Math.random() * 0x10000).toString(16); val.length < 4; val = '0' + val);
    return val;
}

function genId(obj) {
    var id = getRandomString() + Date.now().toString(16).slice(4) + getRandomString();
    if (obj && !obj._id) {
        obj._id = id;
    }
    return id;
}

function getOrAddCollection(collectionName) {
    var collection = db.getCollection(collectionName);
    if (!collection) {
        collection = db.addCollection(collectionName);
    }
    collection.ensureUniqueIndex('_id');
    switch(collectionName) {
        case 'rooms.objects': {
            collection.ensureIndex('room');
            collection.ensureIndex('user');
            break;
        }
        case 'rooms.intents': {
            collection.ensureIndex('room');
            break;
        }
        case 'users': {
            collection.ensureIndex('username');
            break;
        }
        case 'rooms.flags': {
            collection.ensureIndex('room');
            collection.ensureIndex('user');
            break;
        }
        case 'rooms.terrain': {
            collection.ensureIndex('room');
            break;
        }
        case 'transactions': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.code': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.console': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.money': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.notifications': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.resources': {
            collection.ensureIndex('user');
            break;
        }
    }
    return collection;
}

function recursReplaceNeNull(val) {
    if(!_.isObject(val)) {
        return;
    }

    for(var i in val) {
        if (_.isEqual(val[i], {$ne: null}) && !val.$and) {
            val.$and = [{[i]: {$ne: null}}, {[i]: {$ne: undefined}}];
            delete val[i];
        }
        if (_.isEqual(val[i], {$eq: null}) && !val.$or) {
            val.$or = [{[i]: {$eq: null}}, {[i]: {$eq: undefined}}];
            delete val[i];
        }
        recursReplaceNeNull(val[i]);
    }
}

module.exports = {

    dbRequest(collectionName, method, argsArray, cb) {
        try {
            var collection = getOrAddCollection(collectionName);
            if (method == 'insert') {
                if (_.isArray(argsArray[0])) {
                    argsArray[0].forEach(genId);
                }
                else {
                    genId(argsArray[0]);
                }
            }

            if(method == 'find' || method == 'findOne' || method == 'count' || method == 'removeWhere') {
                recursReplaceNeNull(argsArray[0]);
            }

            var result = collection[method].apply(collection, argsArray);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbUpdate(collectionName, query, update, params, cb) {
        try {
            recursReplaceNeNull(query);
            var collection = getOrAddCollection(collectionName);
            var result = [];
            if (Object.keys(query).length == 1 && query._id && _.isString(query._id)) {
                var found = collection.by('_id', query._id);
                if(found) {
                    result = [found];
                }
            }
            else {
                result = collection.find(query);
            }
            if (result.length) {
                result.forEach(doc => {
                    updateDocument(doc, update);
                    collection.update(doc);
                });
                cb(null, {modified: result.length});
            }
            else if (params && params.upsert) {
                var item = {};
                if (query.$and) {
                    query.$and.forEach(i => _.extend(item, i));
                }
                else {
                    _.extend(item, query);
                }
                updateDocument(item, update);
                genId(item);
                collection.insert(item);
                cb(null, {inserted: 1});
            }
            else {
                cb(null, {});
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbBulk(collectionName, bulk, cb) {
        try {
            var collection = getOrAddCollection(collectionName), result;
            bulk.forEach(i => {
                switch (i.op) {
                    case 'update': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            updateDocument(result, i.update);
                            collection.update(result);
                        }
                        break;
                    }
                    case 'insert': {
                        genId(i.data);
                        collection.insert(i.data);
                        break;
                    }
                    case 'remove': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            collection.remove(result);
                        }
                        break;
                    }
                }
            });
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbFindEx(collectionName, query, opts, cb) {
        try {
            recursReplaceNeNull(query);
            var collection = getOrAddCollection(collectionName);
            var chain = collection.chain().find(query);
            if (opts.sort) {
                for (var field in opts.sort) {
                    chain = chain.simplesort(field, opts.sort[field] == -1);
                }
            }
            if (opts.offset) {
                chain = chain.offset(opts.offset);
            }
            if (opts.limit) {
                chain = chain.limit(opts.limit);
            }
            cb(null, chain.data());
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvGet(name, cb) {
        try {
            var item = db.getCollection('env').get(1) || {data: {}};
            cb(null, item.data[name]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvMget(names, cb) {
        try {
            var item = db.getCollection('env').get(1) || {data: {}};
            var result = names.map(name => item.data[name]);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvSet(name, value, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = value;
                env.update(values);
            }
            else {
                values = {data: {[name]: value}};
                env.insert(values);
            }
            cb && cb(null, value);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvExpire(name, seconds, cb) {
        try {
            var env = db.getCollection('env');
            var expiration = env.get(2);
            if (expiration) {
                expiration.data[name] = Date.now() + seconds * 1000;
                env.update(expiration);
            }
            else {
                expiration = {data: {[name]: Date.now() + seconds * 1000}};
                env.insert(expiration);
            }
            cb && cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvSetex(name, seconds, value, cb) {
        try {
            module.exports.dbEnvSet(name, value);
            module.exports.dbEnvExpire(name, seconds);
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvTtl(name, cb) {
        try {
            var env = db.getCollection('env');
            var expiration = env.get(2);
            if (!expiration || !expiration.data[name] || expiration.data[name] < Date.now()) {
                cb(null, -1);
                return;
            }
            cb(null, (expiration.data[name] - Date.now()) / 1000);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvDel(name, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values && values.data[name]) {
                delete values.data[name];
                cb(null, 1);
            }
            else {
                cb(null, 0);
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHget(name, field, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values && values.data && values.data[name]) {
                cb(null, values.data[name][field]);
            }
            else {
                cb(null);
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHset(name, field, value, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = values.data[name] || {};
                values.data[name][field] = value;
                env.update(values);
            }
            else {
                values = {data: {[name]: {[field]: value}}};
                env.insert(values);
            }
            cb(null, values.data[name][field]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHmget(name, fields, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1) || {data: {}};
            values.data[name] = values.data[name] || {};
            var result = fields.map(i => values.data[name][i]);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHmset(name, data, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = values.data[name] || {};
                _.extend(values.data[name], data);
                env.update(values);
            }
            else {
                values = {data: {[name]: data}};
                env.insert(values);
            }
            cb(null, values.data[name]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbResetAllData(cb) {
        try {
            db.loadJSON(JSON.stringify(require('../db.original')));
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    }
};<commit_msg>remove debug output<commit_after>var loki = require('lokijs'),
q = require('q'),
fs = require('fs'),
_ = require('lodash'),
common = require('@screeps/common'),
config = common.configManager.config;

/**
 * @type Loki
 */
var db;

Object.assign(config.storage, {
    dbOptions: {autosave: true, autosaveInterval: 10000},
    getDb() {
        try {
            fs.statSync(process.env.DB_PATH);
        }
        catch (e) {
            fs.writeFileSync(process.env.DB_PATH, '');
        }
        return new loki(process.env.DB_PATH, config.storage.dbOptions);
    },
    loadDb() {
        db = config.storage.getDb();
        return q.ninvoke(db, 'loadDatabase', {})
            .then(upgradeDb);
    }
});

function upgradeDb() {
    if(!db.databaseVersion) {
        return;
    }
    if(db.databaseVersion < 2) {
        console.log("Upgrading database to version 2");

        var users = db.getCollection('users').find({money: {$gt: 0}});
        users.forEach(user => {
            user.money = Math.round(user.money * 1000);
            db.getCollection('users').update(user);
        });

        var orders = db.getCollection('market.orders').find({});
        orders.forEach(order => {
            order.price = Math.round(order.price * 1000);
            return db.getCollection('market.orders').update(order);
        });
    }

    db.databaseVersion = 2;
}

function updateDocument(doc, update) {
    if (update.$set) {
        _.extend(doc, update.$set);
    }
    if (update.$merge) {
        _.merge(doc, update.$merge);
    }
    if (update.$inc) {
        _.forEach(update.$inc, (val, key) => doc[key] = (doc[key] || 0) + val);
    }
    if (update.$unset) {
        for (var j in update.$unset) {
            delete doc[j];
        }
    }
    if(update.$addToSet) {
        for(let i in update.$addToSet) {
            if(!doc[i]) {
                doc[i] = [];
            }
            if(doc[i].indexOf(update.$addToSet[i]) == -1) {
                doc[i].push(update.$addToSet[i]);
            }
        }
    }
    if(update.$pull) {
        for(let i in update.$addToSet) {
            if(!doc[i]) {
                continue;
            }
            var idx = doc[i].indexOf(update.$pull[i]);
            if(idx != -1) {
                doc[i].splice(idx, 1);
            }
        }
    }
}

setInterval(function envCleanExpired() {
    var env = db.getCollection('env');
    var values = env.get(1);
    var expiration = env.get(2);
    var dirty = false;
    if (expiration) {
        for (var name in expiration.data) {
            if (Date.now() > expiration.data[name]) {
                dirty = true;
                if (values.data[name]) {
                    delete values.data[name];
                }
                delete expiration.data[name];
            }
        }
    }
    if (dirty) {
        env.update(values);
        env.update(expiration);
    }
}, 10000);

function getRandomString() {
    for (var val = Math.floor(Math.random() * 0x10000).toString(16); val.length < 4; val = '0' + val);
    return val;
}

function genId(obj) {
    var id = getRandomString() + Date.now().toString(16).slice(4) + getRandomString();
    if (obj && !obj._id) {
        obj._id = id;
    }
    return id;
}

function getOrAddCollection(collectionName) {
    var collection = db.getCollection(collectionName);
    if (!collection) {
        collection = db.addCollection(collectionName);
    }
    collection.ensureUniqueIndex('_id');
    switch(collectionName) {
        case 'rooms.objects': {
            collection.ensureIndex('room');
            collection.ensureIndex('user');
            break;
        }
        case 'rooms.intents': {
            collection.ensureIndex('room');
            break;
        }
        case 'users': {
            collection.ensureIndex('username');
            break;
        }
        case 'rooms.flags': {
            collection.ensureIndex('room');
            collection.ensureIndex('user');
            break;
        }
        case 'rooms.terrain': {
            collection.ensureIndex('room');
            break;
        }
        case 'transactions': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.code': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.console': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.money': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.notifications': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.resources': {
            collection.ensureIndex('user');
            break;
        }
    }
    return collection;
}

function recursReplaceNeNull(val) {
    if(!_.isObject(val)) {
        return;
    }

    for(var i in val) {
        if (_.isEqual(val[i], {$ne: null}) && !val.$and) {
            val.$and = [{[i]: {$ne: null}}, {[i]: {$ne: undefined}}];
            delete val[i];
        }
        if (_.isEqual(val[i], {$eq: null}) && !val.$or) {
            val.$or = [{[i]: {$eq: null}}, {[i]: {$eq: undefined}}];
            delete val[i];
        }
        recursReplaceNeNull(val[i]);
    }
}

module.exports = {

    dbRequest(collectionName, method, argsArray, cb) {
        try {
            var collection = getOrAddCollection(collectionName);
            if (method == 'insert') {
                if (_.isArray(argsArray[0])) {
                    argsArray[0].forEach(genId);
                }
                else {
                    genId(argsArray[0]);
                }
            }

            if(method == 'find' || method == 'findOne' || method == 'count' || method == 'removeWhere') {
                recursReplaceNeNull(argsArray[0]);
            }

            var result = collection[method].apply(collection, argsArray);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbUpdate(collectionName, query, update, params, cb) {
        try {
            recursReplaceNeNull(query);
            var collection = getOrAddCollection(collectionName);
            var result = [];
            if (Object.keys(query).length == 1 && query._id && _.isString(query._id)) {
                var found = collection.by('_id', query._id);
                if(found) {
                    result = [found];
                }
            }
            else {
                result = collection.find(query);
            }
            if (result.length) {
                result.forEach(doc => {
                    updateDocument(doc, update);
                    collection.update(doc);
                });
                cb(null, {modified: result.length});
            }
            else if (params && params.upsert) {
                var item = {};
                if (query.$and) {
                    query.$and.forEach(i => _.extend(item, i));
                }
                else {
                    _.extend(item, query);
                }
                updateDocument(item, update);
                genId(item);
                collection.insert(item);
                cb(null, {inserted: 1});
            }
            else {
                cb(null, {});
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbBulk(collectionName, bulk, cb) {
        try {
            var collection = getOrAddCollection(collectionName), result;
            bulk.forEach(i => {
                switch (i.op) {
                    case 'update': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            updateDocument(result, i.update);
                            collection.update(result);
                        }
                        break;
                    }
                    case 'insert': {
                        genId(i.data);
                        collection.insert(i.data);
                        break;
                    }
                    case 'remove': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            collection.remove(result);
                        }
                        break;
                    }
                }
            });
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbFindEx(collectionName, query, opts, cb) {
        try {
            recursReplaceNeNull(query);
            var collection = getOrAddCollection(collectionName);
            var chain = collection.chain().find(query);
            if (opts.sort) {
                for (var field in opts.sort) {
                    chain = chain.simplesort(field, opts.sort[field] == -1);
                }
            }
            if (opts.offset) {
                chain = chain.offset(opts.offset);
            }
            if (opts.limit) {
                chain = chain.limit(opts.limit);
            }
            cb(null, chain.data());
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvGet(name, cb) {
        try {
            var item = db.getCollection('env').get(1) || {data: {}};
            cb(null, item.data[name]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvMget(names, cb) {
        try {
            var item = db.getCollection('env').get(1) || {data: {}};
            var result = names.map(name => item.data[name]);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvSet(name, value, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = value;
                env.update(values);
            }
            else {
                values = {data: {[name]: value}};
                env.insert(values);
            }
            cb && cb(null, value);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvExpire(name, seconds, cb) {
        try {
            var env = db.getCollection('env');
            var expiration = env.get(2);
            if (expiration) {
                expiration.data[name] = Date.now() + seconds * 1000;
                env.update(expiration);
            }
            else {
                expiration = {data: {[name]: Date.now() + seconds * 1000}};
                env.insert(expiration);
            }
            cb && cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvSetex(name, seconds, value, cb) {
        try {
            module.exports.dbEnvSet(name, value);
            module.exports.dbEnvExpire(name, seconds);
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvTtl(name, cb) {
        try {
            var env = db.getCollection('env');
            var expiration = env.get(2);
            if (!expiration || !expiration.data[name] || expiration.data[name] < Date.now()) {
                cb(null, -1);
                return;
            }
            cb(null, (expiration.data[name] - Date.now()) / 1000);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvDel(name, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values && values.data[name]) {
                delete values.data[name];
                cb(null, 1);
            }
            else {
                cb(null, 0);
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHget(name, field, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values && values.data && values.data[name]) {
                cb(null, values.data[name][field]);
            }
            else {
                cb(null);
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHset(name, field, value, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = values.data[name] || {};
                values.data[name][field] = value;
                env.update(values);
            }
            else {
                values = {data: {[name]: {[field]: value}}};
                env.insert(values);
            }
            cb(null, values.data[name][field]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHmget(name, fields, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1) || {data: {}};
            values.data[name] = values.data[name] || {};
            var result = fields.map(i => values.data[name][i]);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHmset(name, data, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = values.data[name] || {};
                _.extend(values.data[name], data);
                env.update(values);
            }
            else {
                values = {data: {[name]: data}};
                env.insert(values);
            }
            cb(null, values.data[name]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbResetAllData(cb) {
        try {
            db.loadJSON(JSON.stringify(require('../db.original')));
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    }
};<|endoftext|>


<commit_before>var config = require('./config');
var MongoClient = require('mongodb').MongoClient;
var util = require('util');
var us = require('underscore');

//The prefix of the URL used to connect to the database.  The name of the database (account) is appended to the end
var dbURLPrefix = config.database.scheme + '://' + config.database.host + ':' + config.database.port + '/';

//An object used to cache the database information for realms and accounts. It should look something like:
var dbClients = {};
//var dbClientSample = {
//    "realmNameOrAccountName": {
//        "touched": 1409243420271,
//        "database": "theDatabaseConnectionReference",
//        "accountName": "linkFromRealmToAccount"
//    }
//}

//An interval function that sweeps through the list of dbClients and removes any inactive ones.
var cacheCleaner;

//If using the shared connection approach where all databases use a single pool, then you need
//to have an intiial database reference.
var initialDB;

//The database collection that holds the realm and account information.  This should only be kept
//around until we have a valid service call although it may be valid for testing purposes.
var accountsCollection;

//Determine which connection strategy to use and set the appropriate function.
var getClientStrategy = config.database.sharedConnections ? getClientShared : getClient;

//These are the default options for a db instance (rather than server, replicate set, and mongos)
//var dbOptions = {
//    db: {
//        bufferMaxEntries: -1,
//        retryMiliSeconds: 5000,
//        numberOfRetries: 5
//    }
//};
var dbOptions = {
    db: {
        bufferMaxEntries: -1,
        retryMiliSeconds: 5000,
        numberOfRetries: 5
    }
};

//
var delayedProcessingTimeout = 20;

// Map of accountName keyed booleans indicating a connection attempt is in progress.
var currentlyProcessing = { };

//Debugging counters
var stats = {
    strategy: (config.database.sharedConnections ? 'shared' : 'not shared'),
    collectionRequests: 0,
    cacheHits: 0,
    cacheDelays: 0,
    dbClientCreated: 0,
    dbClientDestroyed: 0,
    collectionCreated: 0,
    collectionFound: 0
};

//The callback returns a MongoDB client connection to the database associated with the account.  This
//approach creates a new client per database and opens a connection pool for each (default 5).  So:
//   total connections used = active databases * 5
//This creates a greater number of connections than the shared strategy below - which creates a single
//pool and shares them with all databases.  Because of this, this function also kicks off an interval
//cache cleaning function to close client connections that haven't been used for a configurable duration.
function getClient(accountName, cb) {

    //var logger = require('./loggers').getLogger();

    //Start a cleaner for db connections that haven't been used in awhile
    if (!cacheCleaner) {
        cacheCleaner = setInterval(cleanDBClientCache, config.database.clientExpiry);
    }

    if (currentlyProcessing[accountName]) {

        stats.cacheDelays ++;
        setTimeout(getClient.bind(this, accountName, cb), delayedProcessingTimeout);

    } else {

        currentlyProcessing[accountName] = true;

        //If there is a cached dbClient using the account name,
        //update the time it was requested and return it.
        var dbClient = dbClients[accountName];
        if (dbClient) {
            dbClient.touched = Date.now();
            //logger.debug('db client retrieved from cache');
            stats.cacheHits += 1;
            currentlyProcessing[accountName] = false;
            return cb(null, dbClient.database);
        }

        //Looks like we don't currently have a database connection so make a new one.
        var dbURL = dbURLPrefix + accountName;
        //logger.debug('connecting to ' + dbURL);

        //Make the actual connection.  We may want to check with Auth service as to
        //whether this is a real account and only create it it should be created.
        MongoClient.connect(dbURL, dbOptions, function (err, db) {

            if (err) {
                //logger.error('problem connecting to MongoDB @ ' + dbURL, err);
                currentlyProcessing[accountName] = false;
                return cb(err, null);
            }

            //Cache the new connection and return it.  We cache it under both the realm
            //name and the account name as all realms will use the same connection pool.
            dbClients[accountName] = {
                touched: Date.now(),
                database: db
            };
            //logger.debug('new db client added to cache: ' + accountName);
            stats.dbClientCreated += 1;
            currentlyProcessing[accountName] = false;
            return cb(null, db);
        });
    }
}
module.exports.getClient = getClient;

//As per the rules for MongoDB database naming:
//
//   http://docs.mongodb.org/manual/reference/limits/#naming-restrictions
//
//This function:
//  - converts spaces to underscores
//  - removes any other illegal characters
//  - ensures the string length is less than 64 characters
//  - converts everything to lowercase
function getSafeDatabaseName(accountName) {
    return accountName.split(' ').join('_').replace(/[\\\/\.\"]/g, '').substring(0, 63).toLowerCase();
}
module.exports.getSafeDatabaseName = getSafeDatabaseName;

function dumpStats() {
    //var logger = require('./loggers').getLogger();
    console.log(JSON.stringify(stats, null, 4));
}
module.exports.dumpStats = dumpStats;

function cleanDBClientCache() {

    //var logger = require('./loggers').getLogger();

    us.each(dbClients, function (dbClient, dbClientKey, dbClients) {

        var touched = dbClient.touched;

        //See if the database connection hasn't been used over the configured expiry duration.
        var timeLeft = (touched + config.database.clientExpiry) - Date.now();
        //logger.debug('[' + dbClientKey + ']  expired: ' + (timeLeft < 0) + '  left: ' + timeLeft);

        if (timeLeft <= 0) {

            //Need to remove the client from the cache and close the connection.
            dbClient.database.close();

            delete dbClients[dbClientKey];
//            dbClients[dbClientKey] = null;

            stats.dbClientDestroyed += 1;
        }
    });
    //dumpStats();
}

function getClientShared(accountName, cb) {

    //var logger = require('./loggers').getLogger();

    //If we have not yet established a single database connection, then do it now.
    if (!initialDB) {

        var dbURL = dbURLPrefix + accountName;
        //logger.debug('connecting to initial database ' + dbURL);

        //Make the actual connection.  We may want to check with Auth service as to
        //whether this is a real account and only create it it should be created.
        MongoClient.connect(dbURL, dbOptions, function (err, db) {

            if (err) {
                //logger.error('problem connecting to MongoDB @ ' + dbURL, err);
                return cb(err, null);
            }

            //Save the initial db reference
            initialDB = db;
            //logger.debug('initial database set');

            //Cache the initial connection and return the db.
            dbClients[accountName] = {
                touched: Date.now(),
                database: db
            };

            //logger.debug('initial db client added to cache');
            stats.dbClientCreated += 1;

            return cb(null, db);
        });

    } else {

        //If there is a cached db client for the account, update the time it was requested and return it.
        var dbClient = dbClients[accountName];
        if (dbClient) {
            dbClient.touched = Date.now();

            //logger.debug('realm client retrieved from cache ' + accountName);
            stats.cacheHits += 1;

            return cb(null, dbClient.database);
        }

        //logger.debug('[' + accountName + '] not cached in ' + JSON.stringify(us.keys(dbClients), null, 4));

        //If there is nothing cached, create a new one using the initial one.
        var dbWithSharedConnections = initialDB.db(accountName);

        //Cache the newly created client
        dbClients[accountName] = {
            touched: Date.now(),
            database: dbWithSharedConnections
        };

        //logger.debug('shared db client added to cache');
        stats.dbClientCreated += 1;

        return cb(null, dbWithSharedConnections);
    }
}
module.exports.getClientShared = getClientShared;

function getCollection(accountName, realmName, collectionName, cb) {

    //var logger = require('./loggers').getLogger();
    stats.collectionRequests += 1;

    return getClientStrategy(accountName, function (dbErr, db) {

        if (dbErr) {
            //logger.error('problem getting client', dbErr);
            return cb(dbErr, null);
        }

        var fullCollectionName = realmName + '.' + collectionName;
        //logger.debug('getting client for: ' + accountName + ' -> ' + fullCollectionName);

        db.collection(fullCollectionName, {strict: false}, function (collErr, coll) {

            if (collErr) {
                //logger.error('problem getting reference to collection', collErr);
                return cb(collErr, null);
            }

            if (coll) {
                //logger.debug('found existing collection', coll.collectionName);
                stats.collectionFound += 1;
                return cb(null, coll);
            }

            //We've can set 'strict' so collections are not automatically created when asked for
            //but we should add some logic here to ensure that the collection we are planning
            //to create manually is legal and authorized.  This might require an call to the
            //Auth service or maybe just a direct database call into the Account information.

            //Also, we should consider setting any provided index(es) when the collection is
            //created.
            db.createCollection(fullCollectionName, {w: 1}, function (createErr, newCollection) {
                if (createErr) {
                    //logger.error('could not create collection', createErr);
                    return cb(createErr, null);
                }
                //logger.debug('created new collection', newCollection.collectionName);
                stats.collectionCreated += 1;
                return cb(null, newCollection);
            });

        });
    });
}
module.exports.getCollection = getCollection;

/**
 * Get a plain connection to an account database using the currently configured strategy
 */
function getConnectionCurrentStrategy(accountName, callback) {
     getClientStrategy(accountName, callback);
}
module.exports.getConnectionCurrentStrategy = getConnectionCurrentStrategy;

//This should probably only be used for the purposes of testing as you may
//not want to cut connections drastically during production service shutdown.
function shutdown() {

    //var logger = require('./loggers').getLogger();
    //logger.debug('shutting down database clients...');

    us.each(dbClients, function (dbClient, cacheKey, dbClients) {
        //logger.debug('[' + cacheKey + ']  closing');
        dbClient.database.close();
        delete dbClients[cacheKey];
    });

    if (cacheCleaner) {
        clearInterval(cacheCleaner);
    }

}
module.exports.shutdown = shutdown;
<commit_msg>NTFY-242 Added function to pre-check db collection existence<commit_after>var config = require('./config');
var MongoClient = require('mongodb').MongoClient;
var util = require('util');
var us = require('underscore');

//The prefix of the URL used to connect to the database.  The name of the database (account) is appended to the end
var dbURLPrefix = config.database.scheme + '://' + config.database.host + ':' + config.database.port + '/';

//An object used to cache the database information for realms and accounts. It should look something like:
var dbClients = {};
//var dbClientSample = {
//    "realmNameOrAccountName": {
//        "touched": 1409243420271,
//        "database": "theDatabaseConnectionReference",
//        "accountName": "linkFromRealmToAccount"
//    }
//}

//An interval function that sweeps through the list of dbClients and removes any inactive ones.
var cacheCleaner;

//If using the shared connection approach where all databases use a single pool, then you need
//to have an intiial database reference.
var initialDB;

//The database collection that holds the realm and account information.  This should only be kept
//around until we have a valid service call although it may be valid for testing purposes.
var accountsCollection;

//Determine which connection strategy to use and set the appropriate function.
var getClientStrategy = config.database.sharedConnections ? getClientShared : getClient;

//These are the default options for a db instance (rather than server, replicate set, and mongos)
//var dbOptions = {
//    db: {
//        bufferMaxEntries: -1,
//        retryMiliSeconds: 5000,
//        numberOfRetries: 5
//    }
//};
var dbOptions = {
    db: {
        bufferMaxEntries: -1,
        retryMiliSeconds: 5000,
        numberOfRetries: 5
    }
};

//
var delayedProcessingTimeout = 20;

// Map of accountName keyed booleans indicating a connection attempt is in progress.
var currentlyProcessing = { };

//Debugging counters
var stats = {
    strategy: (config.database.sharedConnections ? 'shared' : 'not shared'),
    collectionRequests: 0,
    cacheHits: 0,
    cacheDelays: 0,
    dbClientCreated: 0,
    dbClientDestroyed: 0,
    collectionCreated: 0,
    collectionFound: 0
};

/**
 * Perform a native mongo check to see if a given database and realm exists.
 * takes callback with pattern function(boolean) that returns true if collection found, false if
 * either database or collection is not found. Collection names are lowercased as per the model creation
 * code
 */
function testCollectionExistence(accountName, collectionName, cb) {

    var logger = require('./loggers').getLogger();
    var safeDB = getSafeDatabaseName(accountName);

    getClient('test', function(err, testDb) {
        // Use the admin database for the operation
        var adminDb = testDb.admin();

        // List all the available databases
        adminDb.listDatabases(function(err, dbs) {
            if (err) {
                logger.error('Exception getting list of databases: ' + JSON.stringify(err));
                return cb(false);
            }
            var found = false;
            for (var i in dbs.databases) {
                if (dbs.databases[i].name === safeDB) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                logger.debug('No existing database found: ' + safeDB );
                return cb(false);
            }
            getClient(accountName, function(err, db) {
                if (err) {
                    logger.error('Exception getting database connection: ' + JSON.stringify(err));
                    return cb(false);
                }
                db.listCollections( {} ).toArray(function(err, items) {
                    if (err) {
                        logger.error('Exception getting list of collections: ' + JSON.stringify(err));
                        return cb(false);
                    }
                    for (var i in items) {
                        if (items[i].name === collectionName.toLowerCase()) {
                            return cb(true);
                        }
                    }
                    logger.debug('DB ' + safeDB + ' has no matching collection: ' + collectionName + ' in: ');
                    logger.debug(items);
                    return cb(false);
                });
            });
        });
    });
}
module.exports.testCollectionExistence = testCollectionExistence;

//The callback returns a MongoDB client connection to the database associated with the account.  This
//approach creates a new client per database and opens a connection pool for each (default 5).  So:
//   total connections used = active databases * 5
//This creates a greater number of connections than the shared strategy below - which creates a single
//pool and shares them with all databases.  Because of this, this function also kicks off an interval
//cache cleaning function to close client connections that haven't been used for a configurable duration.
function getClient(accountName, cb) {

    //var logger = require('./loggers').getLogger();

    //Start a cleaner for db connections that haven't been used in awhile
    if (!cacheCleaner) {
        cacheCleaner = setInterval(cleanDBClientCache, config.database.clientExpiry);
    }

    if (currentlyProcessing[accountName]) {

        stats.cacheDelays ++;
        setTimeout(getClient.bind(this, accountName, cb), delayedProcessingTimeout);

    } else {

        currentlyProcessing[accountName] = true;

        //If there is a cached dbClient using the account name,
        //update the time it was requested and return it.
        var dbClient = dbClients[accountName];
        if (dbClient) {
            dbClient.touched = Date.now();
            //logger.debug('db client retrieved from cache');
            stats.cacheHits += 1;
            currentlyProcessing[accountName] = false;
            return cb(null, dbClient.database);
        }

        //Looks like we don't currently have a database connection so make a new one.
        var dbURL = dbURLPrefix + accountName;
        //logger.debug('connecting to ' + dbURL);

        //Make the actual connection.  We may want to check with Auth service as to
        //whether this is a real account and only create it it should be created.
        MongoClient.connect(dbURL, dbOptions, function (err, db) {

            if (err) {
                //logger.error('problem connecting to MongoDB @ ' + dbURL, err);
                currentlyProcessing[accountName] = false;
                return cb(err, null);
            }

            //Cache the new connection and return it.  We cache it under both the realm
            //name and the account name as all realms will use the same connection pool.
            dbClients[accountName] = {
                touched: Date.now(),
                database: db
            };
            //logger.debug('new db client added to cache: ' + accountName);
            stats.dbClientCreated += 1;
            currentlyProcessing[accountName] = false;
            return cb(null, db);
        });
    }
}
module.exports.getClient = getClient;

//As per the rules for MongoDB database naming:
//
//   http://docs.mongodb.org/manual/reference/limits/#naming-restrictions
//
//This function:
//  - converts spaces to underscores
//  - removes any other illegal characters
//  - ensures the string length is less than 64 characters
//  - converts everything to lowercase
function getSafeDatabaseName(accountName) {
    return accountName.split(' ').join('_').replace(/[\\\/\.\"]/g, '').substring(0, 63).toLowerCase();
}
module.exports.getSafeDatabaseName = getSafeDatabaseName;

function dumpStats() {
    //var logger = require('./loggers').getLogger();
    console.log(JSON.stringify(stats, null, 4));
}
module.exports.dumpStats = dumpStats;

function cleanDBClientCache() {

    //var logger = require('./loggers').getLogger();

    us.each(dbClients, function (dbClient, dbClientKey, dbClients) {

        var touched = dbClient.touched;

        //See if the database connection hasn't been used over the configured expiry duration.
        var timeLeft = (touched + config.database.clientExpiry) - Date.now();
        //logger.debug('[' + dbClientKey + ']  expired: ' + (timeLeft < 0) + '  left: ' + timeLeft);

        if (timeLeft <= 0) {

            //Need to remove the client from the cache and close the connection.
            dbClient.database.close();

            delete dbClients[dbClientKey];
//            dbClients[dbClientKey] = null;

            stats.dbClientDestroyed += 1;
        }
    });
    //dumpStats();
}

function getClientShared(accountName, cb) {

    //var logger = require('./loggers').getLogger();

    //If we have not yet established a single database connection, then do it now.
    if (!initialDB) {

        var dbURL = dbURLPrefix + accountName;
        //logger.debug('connecting to initial database ' + dbURL);

        //Make the actual connection.  We may want to check with Auth service as to
        //whether this is a real account and only create it it should be created.
        MongoClient.connect(dbURL, dbOptions, function (err, db) {

            if (err) {
                //logger.error('problem connecting to MongoDB @ ' + dbURL, err);
                return cb(err, null);
            }

            //Save the initial db reference
            initialDB = db;
            //logger.debug('initial database set');

            //Cache the initial connection and return the db.
            dbClients[accountName] = {
                touched: Date.now(),
                database: db
            };

            //logger.debug('initial db client added to cache');
            stats.dbClientCreated += 1;

            return cb(null, db);
        });

    } else {

        //If there is a cached db client for the account, update the time it was requested and return it.
        var dbClient = dbClients[accountName];
        if (dbClient) {
            dbClient.touched = Date.now();

            //logger.debug('realm client retrieved from cache ' + accountName);
            stats.cacheHits += 1;

            return cb(null, dbClient.database);
        }

        //logger.debug('[' + accountName + '] not cached in ' + JSON.stringify(us.keys(dbClients), null, 4));

        //If there is nothing cached, create a new one using the initial one.
        var dbWithSharedConnections = initialDB.db(accountName);

        //Cache the newly created client
        dbClients[accountName] = {
            touched: Date.now(),
            database: dbWithSharedConnections
        };

        //logger.debug('shared db client added to cache');
        stats.dbClientCreated += 1;

        return cb(null, dbWithSharedConnections);
    }
}
module.exports.getClientShared = getClientShared;

function getCollection(accountName, realmName, collectionName, cb) {

    //var logger = require('./loggers').getLogger();
    stats.collectionRequests += 1;

    return getClientStrategy(accountName, function (dbErr, db) {

        if (dbErr) {
            //logger.error('problem getting client', dbErr);
            return cb(dbErr, null);
        }

        var fullCollectionName = realmName + '.' + collectionName;
        //logger.debug('getting client for: ' + accountName + ' -> ' + fullCollectionName);

        db.collection(fullCollectionName, {strict: false}, function (collErr, coll) {

            if (collErr) {
                //logger.error('problem getting reference to collection', collErr);
                return cb(collErr, null);
            }

            if (coll) {
                //logger.debug('found existing collection', coll.collectionName);
                stats.collectionFound += 1;
                return cb(null, coll);
            }

            //We've can set 'strict' so collections are not automatically created when asked for
            //but we should add some logic here to ensure that the collection we are planning
            //to create manually is legal and authorized.  This might require an call to the
            //Auth service or maybe just a direct database call into the Account information.

            //Also, we should consider setting any provided index(es) when the collection is
            //created.
            db.createCollection(fullCollectionName, {w: 1}, function (createErr, newCollection) {
                if (createErr) {
                    //logger.error('could not create collection', createErr);
                    return cb(createErr, null);
                }
                //logger.debug('created new collection', newCollection.collectionName);
                stats.collectionCreated += 1;
                return cb(null, newCollection);
            });

        });
    });
}
module.exports.getCollection = getCollection;

/**
 * Get a plain connection to an account database using the currently configured strategy
 */
function getConnectionCurrentStrategy(accountName, callback) {
     getClientStrategy(accountName, callback);
}
module.exports.getConnectionCurrentStrategy = getConnectionCurrentStrategy;

//This should probably only be used for the purposes of testing as you may
//not want to cut connections drastically during production service shutdown.
function shutdown() {

    //var logger = require('./loggers').getLogger();
    //logger.debug('shutting down database clients...');

    us.each(dbClients, function (dbClient, cacheKey, dbClients) {
        //logger.debug('[' + cacheKey + ']  closing');
        dbClient.database.close();
        delete dbClients[cacheKey];
    });

    if (cacheCleaner) {
        clearInterval(cacheCleaner);
    }

}
module.exports.shutdown = shutdown;
<|endoftext|>


<commit_before>/*
 * Copyright 2014, Gregg Tavares.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Gregg Tavares. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

"use strict";

var url = require('url');

/**
 * @typedef {sendJSON~Options}
 * @property {string?} method Default = 'POST'
 * @property {object?} headers key/value headers
 * @property {number?} timeout timeout
 */

/**
 * @callback {sendJSON~Callback}
 * @param {??} error
 * @param {object} result
 */

/**
 * make a json based request.
 *
 * @param {string} apiurl
 * @param {object} obj Object to send
 * @param {sendJSON~Options} options
 * @param {sendJSON~Callback} callback
 */
var sendJSON = function(apiurl, obj, options, callback) {
  options = JSON.parse(JSON.stringify(options));
  var parsedUrl = url.parse(apiurl);

  options.hostname = parsedUrl.hostname;
  options.port     = parsedUrl.port;
  options.method   = options.method || 'POST';
  options.headers  = options.headers || {};
  options.path     = parsedUrl.path;

  var body = JSON.stringify(obj);
  var headers = options.headers;

  headers["content-length"] = Buffer.byteLength(body, "utf8");
  headers["content-type"]   = "application/json; charset=utf-8";

  var callCallback = function(err, res) {
    if (callback) {
      var cb = callback;
      callback = undefined;
      cb(err, res);
    }
  };

  var protocol = parsedUrl.protocol.substring(0, parsedUrl.protocol.length - 1);
  var req = require(protocol).request(options, function(res) {
    res.setEncoding("utf8");
    var data = "";
    res.on("data", function(chunk) {
      data += chunk;
    });
    res.on("error", function(err) {
      callCallback(err);
    });
    res.on("end", function() {
      if (res.statusCode === 301 ||
          res.statusCode === 302 ||
          res.statusCode === 307 ||
          res.statusCode === 308) {
        sendJSON(res.headers["location"].toString(), obj, options, callCallback);
      } else if (res.statusCode >= 400 && res.statusCode < 600 || res.statusCode < 10) {
        callCallback(new Error("StatusCode: " + res.statusCode + "\n" + JSON.stringify(data, undefined, "  ")));
      } else {
        try {
          // TODO: based on content-type parse differently?
          callCallback(null, JSON.parse(data));
        } catch (e) {
          e.content = data;
          callCallback(e);
        }
      }
    });
  });

  if (options.timeout) {
    req.setTimeout(options.timeout);
  }

  req.on("error", function(e) {
    callCallback(e.message);
  });

  req.on("timeout", function() {
    callCallback(new error.GatewayTimeout());
  });

  req.write(body);
  req.end();
};

exports.sendJSON = sendJSON;

<commit_msg>harden io.js<commit_after>/*
 * Copyright 2014, Gregg Tavares.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Gregg Tavares. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

"use strict";

var url = require('url');

/**
 * @typedef {sendJSON~Options}
 * @property {string?} method Default = 'POST'
 * @property {object?} headers key/value headers
 * @property {number?} timeout timeout
 */

/**
 * @callback {sendJSON~Callback}
 * @param {??} error
 * @param {object} result
 */

/**
 * make a json based request.
 *
 * @param {string} apiurl
 * @param {object} obj Object to send
 * @param {sendJSON~Options} options
 * @param {sendJSON~Callback} callback
 */
var validProtocols = {
  http: true,
  https: true,
};

var sendJSON = function(apiurl, obj, options, callback) {
  options = JSON.parse(JSON.stringify(options));
  var parsedUrl = url.parse(apiurl);

  options.hostname = parsedUrl.hostname || "";
  options.port     = parsedUrl.port || "";
  options.method   = options.method || 'POST';
  options.headers  = options.headers || {};
  options.path     = parsedUrl.path || "";
  var protocol     = parsedUrl.protocol || "";

  protocol = protocol.substring(0, protocol.length - 1);
  if (!validProtocols[protocol]) {
    setTimeout(function() {
      callCallback(new Error("unknown protocol for url: " + apiurl));
    }, 0);
    return;
  }

  var body = JSON.stringify(obj);
  var headers = options.headers;

  headers["content-length"] = Buffer.byteLength(body, "utf8");
  headers["content-type"]   = "application/json; charset=utf-8";

  var callCallback = function(err, res) {
    if (callback) {
      var cb = callback;
      callback = undefined;
      cb(err, res);
    }
  };

  var req = require(protocol).request(options, function(res) {
    res.setEncoding("utf8");
    var data = "";
    res.on("data", function(chunk) {
      data += chunk;
    });
    res.on("error", function(err) {
      callCallback(err);
    });
    res.on("end", function() {
      if (res.statusCode === 301 ||
          res.statusCode === 302 ||
          res.statusCode === 307 ||
          res.statusCode === 308) {
        sendJSON(res.headers["location"].toString(), obj, options, callCallback);
      } else if (res.statusCode >= 400 && res.statusCode < 600 || res.statusCode < 10) {
        callCallback(new Error("StatusCode: " + res.statusCode + "\n" + JSON.stringify(data, undefined, "  ")));
      } else {
        try {
          // TODO: based on content-type parse differently?
          callCallback(null, JSON.parse(data));
        } catch (e) {
          e.content = data;
          callCallback(e);
        }
      }
    });
  });

  if (options.timeout) {
    req.setTimeout(options.timeout);
  }

  req.on("error", function(e) {
    callCallback(e.message);
  });

  req.on("timeout", function() {
    callCallback(new error.GatewayTimeout());
  });

  req.write(body);
  req.end();
};

exports.sendJSON = sendJSON;

<|endoftext|>


<commit_before>/**
 * Socket.IO client
 *
 * @author Guillermo Rauch <guillermo@learnboost.com>
 * @license The MIT license.
 * @copyright Copyright (c) 2010 LearnBoost <dev@learnboost.com>
 */

this.io = {
	version: '0.6',

	setPath: function(path){
		if (window.console && console.error) console.error('io.setPath will be removed. Please set the variable WEB_SOCKET_SWF_LOCATION pointing to WebSocketMain.swf');
		this.path = /\/$/.test(path) ? path : path + '/';
		WEB_SOCKET_SWF_LOCATION = path + 'lib/vendor/web-socket-js/WebSocketMain.swf';
	}
};

if ('jQuery' in this) jQuery.io = this.io;

if (typeof window != 'undefined'){
  // WEB_SOCKET_SWF_LOCATION = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//cdn.socket.io/' + this.io.version + '/WebSocketMain.swf';
  WEB_SOCKET_SWF_LOCATION = '/socket.io/lib/vendor/web-socket-js/WebSocketMain.swf';
}<commit_msg>Allow to specify WEB_SOCKET_SWF_LOCATION before socket.io is loaded. Thanks @saikat<commit_after>/**
 * Socket.IO client
 *
 * @author Guillermo Rauch <guillermo@learnboost.com>
 * @license The MIT license.
 * @copyright Copyright (c) 2010 LearnBoost <dev@learnboost.com>
 */

this.io = {
	version: '0.6',

	setPath: function(path){
		if (window.console && console.error) console.error('io.setPath will be removed. Please set the variable WEB_SOCKET_SWF_LOCATION pointing to WebSocketMain.swf');
		this.path = /\/$/.test(path) ? path : path + '/';
    if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined')
      WEB_SOCKET_SWF_LOCATION = path + 'lib/vendor/web-socket-js/WebSocketMain.swf';
	}
};

if ('jQuery' in this) jQuery.io = this.io;

if (typeof window != 'undefined'){
  // WEB_SOCKET_SWF_LOCATION = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//cdn.socket.io/' + this.io.version + '/WebSocketMain.swf';
  WEB_SOCKET_SWF_LOCATION = '/socket.io/lib/vendor/web-socket-js/WebSocketMain.swf';
}
<|endoftext|>


<commit_before>'use strict';
const execa = require('execa');
const sudo = require('./sudo');

async function getDefaultInterface() {
  const result = await execa.shell(
    "sudo route | grep '^default' | grep -o '[^ ]*$'"
  );
  return result.stdout;
}

async function modProbe() {
  try {
    await sudo('modprobe', 'ifb');
  } catch (e) {
    // we are probably in a Docker env
    // let us hope that the host is Linux
    await sudo('ip', 'link', 'add', 'ifb0', 'type', 'ifb');
  }
}

async function setup(defaultInterface) {
  await sudo('ip', 'link', 'set', 'dev', 'ifb0', 'up');
  await sudo('tc', 'qdisc', 'add', 'dev', defaultInterface, 'ingress');
  await sudo(
    'tc',
    'filter',
    'add',
    'dev',
    defaultInterface,
    'parent',
    'ffff:',
    'protocol',
    'ip',
    'u32',
    'match',
    'u32',
    '0',
    '0',
    'flowid',
    '1:1',
    'action',
    'mirred',
    'egress',
    'redirect',
    'dev',
    'ifb0'
  );
}

async function setLimits(up, down, halfWayRTT, iFace) {
  await sudo(
    'tc',
    'qdisc',
    'add',
    'dev',
    'ifb0',
    'root',
    'handle',
    '1:0',
    'netem',
    'delay',
    `${halfWayRTT}ms`,
    'rate',
    `${down}kbit`
  );
  await sudo(
    'tc',
    'qdisc',
    'add',
    'dev',
    iFace,
    'root',
    'handle',
    '1:0',
    'netem',
    'delay',
    `${halfWayRTT}ms`,
    'rate',
    `${up}kbit`
  );
}

module.exports = {
  async start(up, down, rtt) {
    const halfWayRTT = rtt / 2;

    try {
      await this.stop();
    } catch (e) {
      // ignore
    }

    const iFace = await getDefaultInterface();
    await modProbe();
    await setup(iFace);
    await setLimits(up, down, halfWayRTT, iFace);
  },
  async stop() {
    const iFace = await getDefaultInterface();

    try {
      try {
        await sudo('tc', 'qdisc', 'del', 'dev', iFace, 'root');
        await sudo('tc', 'qdisc', 'del', 'dev', iFace, 'ingress');
      } catch (e) {
        // make sure we try to remove the ingress
        sudo('tc', 'qdisc', 'del', 'dev', iFace, 'ingress');
      }
    } catch (e) {
      // ignore
    }

    try {
      await sudo('tc', 'qdisc', 'del', 'dev', 'ifb0', 'root');
    } catch (e) {
      // do nada
    }
  }
};
<commit_msg>catch setting up ifb twice<commit_after>'use strict';
const execa = require('execa');
const sudo = require('./sudo');

async function getDefaultInterface() {
  const result = await execa.shell(
    "sudo route | grep '^default' | grep -o '[^ ]*$'"
  );
  return result.stdout;
}

async function modProbe() {
  try {
    await sudo('modprobe', 'ifb');
  } catch (e) {
    // we are probably in a Docker env
    // let us hope that the host is Linux
    try {
      await sudo('ip', 'link', 'add', 'ifb0', 'type', 'ifb');
    } catch (e) {
      // If we already setup ifb in a previous run, this will fail
    }
  }
}

async function setup(defaultInterface) {
  await sudo('ip', 'link', 'set', 'dev', 'ifb0', 'up');
  await sudo('tc', 'qdisc', 'add', 'dev', defaultInterface, 'ingress');
  await sudo(
    'tc',
    'filter',
    'add',
    'dev',
    defaultInterface,
    'parent',
    'ffff:',
    'protocol',
    'ip',
    'u32',
    'match',
    'u32',
    '0',
    '0',
    'flowid',
    '1:1',
    'action',
    'mirred',
    'egress',
    'redirect',
    'dev',
    'ifb0'
  );
}

async function setLimits(up, down, halfWayRTT, iFace) {
  await sudo(
    'tc',
    'qdisc',
    'add',
    'dev',
    'ifb0',
    'root',
    'handle',
    '1:0',
    'netem',
    'delay',
    `${halfWayRTT}ms`,
    'rate',
    `${down}kbit`
  );
  await sudo(
    'tc',
    'qdisc',
    'add',
    'dev',
    iFace,
    'root',
    'handle',
    '1:0',
    'netem',
    'delay',
    `${halfWayRTT}ms`,
    'rate',
    `${up}kbit`
  );
}

module.exports = {
  async start(up, down, rtt) {
    const halfWayRTT = rtt / 2;

    try {
      await this.stop();
    } catch (e) {
      // ignore
    }

    const iFace = await getDefaultInterface();
    await modProbe();
    await setup(iFace);
    await setLimits(up, down, halfWayRTT, iFace);
  },
  async stop() {
    const iFace = await getDefaultInterface();

    try {
      try {
        await sudo('tc', 'qdisc', 'del', 'dev', iFace, 'root');
        await sudo('tc', 'qdisc', 'del', 'dev', iFace, 'ingress');
      } catch (e) {
        // make sure we try to remove the ingress
        sudo('tc', 'qdisc', 'del', 'dev', iFace, 'ingress');
      }
    } catch (e) {
      // ignore
    }

    try {
      await sudo('tc', 'qdisc', 'del', 'dev', 'ifb0', 'root');
    } catch (e) {
      // do nada
    }
  }
};
<|endoftext|>


<commit_before>"use strict";

var logger = require('winston');
var pluralize = require('pluralize');

class Model {

	// Standard constructor
	constructor(properties, noDefaults) {

		if (!this.constructor.connection) {
			return this._newNoConnectionPromise();
		} else {
			this.connection = this.constructor.connection;
		}

		// On first model init, add all required keys to an option
		var schema = this.constructor.schema;

		if (this.constructor.required === undefined) {
			var required = new Set();
			schema.forEach((value, key) => {
				if (value.required === true || value.primaryKey === true) {
					required.add(key);
				}
			});

			this.constructor.required = required;
		}

		// Properties stores the actual data for the schema
		this.properties = properties || new Map();

		// Initialize the various properties
		schema.forEach((value, key) => {

			if (!noDefaults) {
				// Insert any defaults if necessary
				var defaultValue = schema.get(key).defaultValue;

				// If a function is provided as a default, call the function
				if (defaultValue && !this.properties.has("key")) {
					if (typeof defaultValue === "function") {
						this.properties.set(key, defaultValue());
					} else {
						this.properties.set(key, defaultValue);
					}
				}
			}

			// Create accessors for the properties on the main class
			if (this[key] !== undefined) {
				logger.warn(`${key} is already defined in model ${this.constructor.name}. Be sure to access it through ${this.constructor.name}.properties.${key}.`);
			} else {
				Object.defineProperty(this, key, {
					get: () => this.properties[key],
					set: (newValue) => { this.properties[key] = newValue; }
				});
			}
		});
	}

	/**
	 * Find a particular record by id, if an id is specified
	 * @param id	a unique id for the object
	 * @return a Promise
	 */
	static findById(id) {
		if (!this.connection) return this._newNoConnectionPromise();

		var query = this.connection.select().from(this.tableName).where({ id: id });

		if (!this.connection.production) this._logQuery(query);

		return query;
	}

	/**
	 * Find all records in a specific table
	 * @return a Query object
	 */
	static findAll() {
		if (!this.connection) return this._newNoConnectionPromise();

		var query = this.connection.select().from(this.tableName);

		if (!this.connection.production) this._logQuery(query);

		return query;
	}

	/**
	 * Creation class methods
	 */

	/**
	 * Create a new record in the database
	 *
	 * @param properties	a JS Map of properties for the object
	 * @return a Promise returning an instance of the model
	 */
	static create(properties) {
		if (!this.connection) return this._newNoConnectionPromise();

		var model = new this();
		return model.create(properties).then(function() {
			return model;
		});
	}

	/**
	 * If a record exists in the database, update it.
	 * If the record does not exist, create a new one.
	 * @param properties	a JS object of properties for the object
	 * @return an Operation object
	 */
	static createOrUpdate(properties) {
		if (!this.connection) return this._newNoConnectionPromise();

		var model = new this();
		return model.createOrUpdate(properties).then(function() {
			return model;
		});
	}

	/**
	 * Static Utility methods
	 */

	/**
	 * Get the name that should represent this model in the database
	 * @return the name of the table as a string
	 */
	static get tableName() {
		// Lazy-load the table name
		delete this.tableName;

		if (typeof this.options.tableName === "string") {
			return this.tableName = this.options.tableName;
		}

		return this.tableName = pluralize(this.name).toLowerCase();
	}

	/**
	 * Create a Promise that immediately provides an error for no
	 * available connection
	 */
	static _newNoConnectionPromise() {
		return new Promise(
			(resolve, reject) => {
				var err = new Error("No connection available");
				err.type = "estelle.no-connection";
				logger.error("Model objects must have Model.connection defined.");
				reject(err);
			}
		);
	}

	static _newErrorPromise(err) {
		return new Promise(
			(resolve, reject) => {
				logger.error(err.message);
				reject(err);
			}
		);
	}

	/**
	 * Writes a query to the log
	 */
	static _logQuery(query) {
		logger.info(query.toString());
	}

	/**
	 * Instance methods
	 */

	/**
	 * Creation instance methods
	 */

	/**
	 * Create a new record in the database.
	 * @return a Knex promise
	 */
	create() {
		// Validate the object
		var validationError = new Error();

		if (!this.validate(validationError)) {
			return this.constructor._newErrorPromise(validationError);
		}

		// Serialize for use in the database
		var properties = this.serialize();

		// Add timestamps
		if (this.constructor.options.timestamp) {
			properties.created = Date.now();
			properties.updated = Date.now();
		}

		// Insert into the database
		return this.connection(this.constructor.tableName).insert(properties);
	}

	/**
	 * If a record exists in the database, update it.
	 * If the record does not exist, create a new one.
	 * @return an Operation object
	 */
	createOrUpdate() {
		// TODO: implement createOrUpdate
	}

	/**
	 * Update methods
	 */

	/**
	 * Push any updates made to the object to the database
	 * @return an Operation object
	 */
	update() {
		// Validate the object
		var validationError = new Error();

		if (!this.validate(validationError)) {
			return this.constructor._newErrorPromise(validationError);
		}

		// Serialize for use in the database
		var properties = this.serialize();

		// Add timestamp
		if (this.constructor.options.timestamp) {
			properties.updated = Date.now();
		}

		// Insert into the database
		return this.connection(this.constructor.tableName).where('id', this.id).update(properties);
	}

	/**
	 * Delete an object from the database
	 * @return an Operation object
	 */
	delete() {
		if (this.constructor.options.safeDelete) {
			var properties = {};

			// Add timestamp
			if (this.constructor.options.timestamp) {
				properties.updated = Date.now();
			}

			properties.deleted = true;

			return this.connection(this.constructor.tableName).where('id', this.id).update(properties);
		} else {
			return this.connection(this.constructor.tableName).where('id', this.id).del();
		}
	}

	/**
	 * Instance Utilties
	 */

	/**
	 * Validates the current instance based on the parent schema
	 * @param err	An empty error object for storing possible errors
	 * @return boolean - false if the schema is invalid, true if the schema is valid
	 */
	validate(err) {
		var model = this.constructor;
		var schema = model.schema;
		var prop = this.properties;

		for (let [key, value] of prop) {

			var type = schema.get(key);

			// Check if the key is in the schema
			if (!type) {
				err.type = "estelle.validation.unrecognizedKey";
				err.message = `${key} is not in the schema for ${model.name}.`;
				return false;
			}

			// Check that the key returns a supported type
			var supportedTypes;
			if (type.supportedTypes) {
				supportedTypes = type.supportedTypes;
			} else if (type.dataType) {
				supportedTypes = type.dataType.supportedTypes;
			}

			if (supportedTypes) {
				if (!supportedTypes.has(typeof value)) {
					err.type = "estelle.validation.unacceptedType";
					err.message = `${typeof value} is not an accepted type for ${key} (type: ${type.name}) in ${model.name}.`;
					return false;
				}
			}

			// Check in with the validation function
			var validate;
			if (type.validator) {
				validate = type.validator;
			} else if (type.dataType) {
				validate = type.dataType.validator;
			}
			if (typeof validate !== 'function') {
				err.type = "estelle.validation.noValidator";
				err.message = `${key} in ${model.name} does not have a valid validation function.`;
				return false;
			}

			if (!validate(value)) {
				err.type = "estelle.validation.validationFailed";
				err.message = `${value} is not a valid value for ${model.name}.${key}.`;
				return false;
			}
		}

		// Check to see if any required values are missing
		for (let item of model.required) {
			if (!prop.has(item)) {
				err.type = "estelle.validation.missingRequired";
				err.message = `${item} is required in ${model.name}.`;
				return false;
			}
		}

		// If we reach this point, then validation passed
		return true;
	}

	/**
	 * Converts the internal property scheme into something
	 * the database layer can understand
	 *
	 * Serialize generally operates under the assumption that
	 * validate() has already been executed
	 *
	 * @return a JS object with all of the necessary properties
	 */
	serialize() {
		var prop = this.properties;
		var schema = this.constructor.schema;

		var out = {};

		for (let [key, value] of prop) {
			var type = schema.get(key);

			var serializer;
			if (type.serialize) {
				serializer = type.serialize;
			} else if (type.dataType) {
				serializer = type.dataType.serialize;
			}

			if (typeof serializer !== 'function') {
				out[key] = value;
			} else {
				out[key] = serializer(value);
			}
		}

		return out;
	}
}

/**
 * A set of global options for the model.
 * Should NOT be modified at runtime
 */
Model.options = {
	// If true, the rows will only be marked deleted instead of actually removed
	safeDelete:	true,

	// Automatically add timestamps for update and creation times
	timestamp: true,

	// The name of the table in the database
	// If not set, the name will be automatically generated
	tableName: undefined,
};

/**
 * The schema for the data model
 * Represented as a JS Map where the property name is the key,
 * and the value is the type from DataTypes
 *
 * Alternatively, the value can be an object with the following options:
 * dataType: the type from DataTypes
 * defaultValue: the default value of the field
 * validator: a special validation function for this field (returns false if bad)
 * required: whether or not this is necessary
 * primaryKey: true if this is the primaryKey
 */
Model.schema = new Map();

module.exports = Model;<commit_msg>Added initialize function<commit_after>"use strict";

var logger = require('winston');
var pluralize = require('pluralize');

class Model {

	// Standard constructor
	constructor(properties, noDefaults) {

		if (!this.constructor.connection) {
			return this.constructor._newNoConnectionPromise();
		} else {
			this.connection = this.constructor.connection;
		}

		// On first model init, add all required keys to an option
		var schema = this.constructor.schema;

		if (this.constructor.required === undefined) {
			var required = new Set();
			schema.forEach((value, key) => {
				if (value.required === true || value.primaryKey === true) {
					required.add(key);
				}
			});

			this.constructor.required = required;
		}

		// Properties stores the actual data for the schema
		this.properties = properties || new Map();

		// Initialize the various properties
		schema.forEach((value, key) => {

			if (!noDefaults) {
				// Insert any defaults if necessary
				var defaultValue = schema.get(key).defaultValue;

				// If a function is provided as a default, call the function
				if (defaultValue && !this.properties.has("key")) {
					if (typeof defaultValue === "function") {
						this.properties.set(key, defaultValue());
					} else {
						this.properties.set(key, defaultValue);
					}
				}
			}

			// Create accessors for the properties on the main class
			if (this[key] !== undefined) {
				logger.warn(`${key} is already defined in model ${this.constructor.name}. Be sure to access it through ${this.constructor.name}.properties.${key}.`);
			} else {
				Object.defineProperty(this, key, {
					get: () => this.properties[key],
					set: (newValue) => { this.properties[key] = newValue; }
				});
			}
		});
	}

	/**
	 * Find a particular record by id, if an id is specified
	 * @param id	a unique id for the object
	 * @return a Promise
	 */
	static findById(id) {
		if (!this.connection) return this._newNoConnectionPromise();

		var query = this.connection.select().from(this.tableName).where({ id: id });

		if (!this.connection.production) this._logQuery(query);

		return query;
	}

	/**
	 * Find all records in a specific table
	 * @return a Query object
	 */
	static findAll() {
		if (!this.connection) return this._newNoConnectionPromise();

		var query = this.connection.select().from(this.tableName);

		if (!this.connection.production) this._logQuery(query);

		return query;
	}

	/**
	 * Creation class methods
	 */

	/**
	 * Create a new record in the database
	 *
	 * @param properties	a JS Map of properties for the object
	 * @return a Promise returning an instance of the model
	 */
	static create(properties) {
		if (!this.connection) return this._newNoConnectionPromise();

		var model = new this();
		return model.create(properties).then(function() {
			return model;
		});
	}

	/**
	 * If a record exists in the database, update it.
	 * If the record does not exist, create a new one.
	 * @param properties	a JS object of properties for the object
	 * @return an Operation object
	 */
	static createOrUpdate(properties) {
		if (!this.connection) return this._newNoConnectionPromise();

		var model = new this();
		return model.createOrUpdate(properties).then(function() {
			return model;
		});
	}

	/**
	 * Static Utility methods
	 */

	/**
	 * Get the name that should represent this model in the database
	 * @return the name of the table as a string
	 */
	static get tableName() {
		// Lazy-load the table name
		delete this.tableName;

		if (typeof this.options.tableName === "string") {
			return this.tableName = this.options.tableName;
		}

		return this.tableName = pluralize(this.name).toLowerCase();
	}

	/**
	 * Create a Promise that immediately provides an error for no
	 * available connection
	 */
	static _newNoConnectionPromise() {
		return new Promise(
			(resolve, reject) => {
				var err = new Error("No connection available");
				err.type = "estelle.no-connection";
				logger.error("Model objects must have Model.connection defined.");
				reject(err);
			}
		);
	}

	static _newErrorPromise(err) {
		return new Promise(
			(resolve, reject) => {
				logger.error(err.message);
				reject(err);
			}
		);
	}

	/**
	 * Writes a query to the log
	 */
	static _logQuery(query) {
		logger.info(query.toString());
	}

	/**
	 * Initializes the table in the database
	 */
	static initialize() {
		if (!this.connection) return this._newNoConnectionPromise();

		// TODO: validate schema

		this.connection.schema.createTableIfNotExists(this.tableName, function(table) {
			for (let [key, type] of this.schema) {
				var typeVal;
				if (type.types) {
					typeVal = type.types[this.connection.clientName];
				} else if (type.dataType) {
					typeVal = type.dataType.types[this.connection.clientName];
				}

				if (!typeVal) {
					var err = new Error();
					err.type = "estelle.schema.noColumnType";
					err.message = `Column type is not defined for ${this.name}.${key}.`;
					throw err;
				}

				table.specificType(key, typeVal);
			}
		});
	}

	/**
	 * Instance methods
	 */

	/**
	 * Creation instance methods
	 */

	/**
	 * Create a new record in the database.
	 * @return a Knex promise
	 */
	create() {
		// Validate the object
		var validationError = new Error();

		if (!this.validate(validationError)) {
			return this.constructor._newErrorPromise(validationError);
		}

		// Serialize for use in the database
		var properties = this.serialize();

		// Add timestamps
		if (this.constructor.options.timestamp) {
			properties.created = Date.now();
			properties.updated = Date.now();
		}

		// Insert into the database
		return this.connection(this.constructor.tableName).insert(properties);
	}

	/**
	 * If a record exists in the database, update it.
	 * If the record does not exist, create a new one.
	 * @return an Operation object
	 */
	createOrUpdate() {
		// TODO: implement createOrUpdate
	}

	/**
	 * Update methods
	 */

	/**
	 * Push any updates made to the object to the database
	 * @return an Operation object
	 */
	update() {
		// Validate the object
		var validationError = new Error();

		if (!this.validate(validationError)) {
			return this.constructor._newErrorPromise(validationError);
		}

		// Serialize for use in the database
		var properties = this.serialize();

		// Add timestamp
		if (this.constructor.options.timestamp) {
			properties.updated = Date.now();
		}

		// Insert into the database
		return this.connection(this.constructor.tableName).where('id', this.id).update(properties);
	}

	/**
	 * Delete an object from the database
	 * @return an Operation object
	 */
	delete() {
		if (this.constructor.options.safeDelete) {
			var properties = {};

			// Add timestamp
			if (this.constructor.options.timestamp) {
				properties.updated = Date.now();
			}

			properties.deleted = true;

			return this.connection(this.constructor.tableName).where('id', this.id).update(properties);
		} else {
			return this.connection(this.constructor.tableName).where('id', this.id).del();
		}
	}

	/**
	 * Instance Utilties
	 */

	/**
	 * Validates the current instance based on the parent schema
	 * @param err	An empty error object for storing possible errors
	 * @return boolean - false if the schema is invalid, true if the schema is valid
	 */
	validate(err) {
		var model = this.constructor;
		var schema = model.schema;
		var prop = this.properties;

		for (let [key, value] of prop) {

			var type = schema.get(key);

			// Check if the key is in the schema
			if (!type) {
				err.type = "estelle.validation.unrecognizedKey";
				err.message = `${key} is not in the schema for ${model.name}.`;
				return false;
			}

			// Check that the key returns a supported type
			var supportedTypes;
			if (type.supportedTypes) {
				supportedTypes = type.supportedTypes;
			} else if (type.dataType) {
				supportedTypes = type.dataType.supportedTypes;
			}

			if (supportedTypes) {
				if (!supportedTypes.has(typeof value)) {
					err.type = "estelle.validation.unacceptedType";
					err.message = `${typeof value} is not an accepted type for ${key} (type: ${type.name}) in ${model.name}.`;
					return false;
				}
			}

			// Check in with the validation function
			var validate;
			if (type.validator) {
				validate = type.validator;
			} else if (type.dataType) {
				validate = type.dataType.validator;
			}
			if (typeof validate !== 'function') {
				err.type = "estelle.validation.noValidator";
				err.message = `${key} in ${model.name} does not have a valid validation function.`;
				return false;
			}

			if (!validate(value)) {
				err.type = "estelle.validation.validationFailed";
				err.message = `${value} is not a valid value for ${model.name}.${key}.`;
				return false;
			}
		}

		// Check to see if any required values are missing
		for (let item of model.required) {
			if (!prop.has(item)) {
				err.type = "estelle.validation.missingRequired";
				err.message = `${item} is required in ${model.name}.`;
				return false;
			}
		}

		// If we reach this point, then validation passed
		return true;
	}

	/**
	 * Converts the internal property scheme into something
	 * the database layer can understand
	 *
	 * Serialize generally operates under the assumption that
	 * validate() has already been executed
	 *
	 * @return a JS object with all of the necessary properties
	 */
	serialize() {
		var prop = this.properties;
		var schema = this.constructor.schema;

		var out = {};

		for (let [key, value] of prop) {
			var type = schema.get(key);

			var serializer;
			if (type.serialize) {
				serializer = type.serialize;
			} else if (type.dataType) {
				serializer = type.dataType.serialize;
			}

			if (typeof serializer !== 'function') {
				out[key] = value;
			} else {
				out[key] = serializer(value);
			}
		}

		return out;
	}
}

/**
 * A set of global options for the model.
 * Should NOT be modified at runtime
 */
Model.options = {
	// If true, the rows will only be marked deleted instead of actually removed
	safeDelete:	true,

	// Automatically add timestamps for update and creation times
	timestamp: true,

	// The name of the table in the database
	// If not set, the name will be automatically generated
	tableName: undefined,
};

/**
 * The schema for the data model
 * Represented as a JS Map where the property name is the key,
 * and the value is the type from DataTypes
 *
 * Alternatively, the value can be an object with the following options:
 * dataType: the type from DataTypes
 * defaultValue: the default value of the field
 * validator: a special validation function for this field (returns false if bad)
 * required: whether or not this is necessary
 * primaryKey: true if this is the primaryKey
 */
Model.schema = new Map();

module.exports = Model;<|endoftext|>