<commit_before>
/**
 * Module dependencies.
 */

var Base = require('./base')
  , color = Base.color;

/**
 * Expose `List`.
 */

exports = module.exports = List;

/**
 * Initialize a new `List` test reporter.
 *
 * @param {Runner} runner
 * @api public
 */

function List(runner) {
  Base.call(this, runner);

  var self = this
    , stats = this.stats
    , n = 0;

  // tests started
  runner.on('start', function(){
    console.log();
  });

  // test started
  runner.on('test', function(test){
    process.stdout.write(color('pass', '    ' + test.fullTitle() + ': '));
  });

  // test passed
  runner.on('pass', function(test){
    var fmt = color('checkmark', '  ✔')
      + color('pass', ' %s: ')
      + color(test.speed, '%dms');
    console.log('\r' + fmt, test.fullTitle(), test.duration);
  });

  // test failed
  runner.on('fail', function(test, err){
    console.log(color('fail', '  %d) %s'), n++, test.fullTitle());
  });

  runner.on('end', self.epilogue.bind(self));
}

/**
 * Inherit from `Base.prototype`.
 */

List.prototype.__proto__ = Base.prototype;<commit_msg>fixed list reporter error styling to match prev commit<commit_after>
/**
 * Module dependencies.
 */

var Base = require('./base')
  , color = Base.color;

/**
 * Expose `List`.
 */

exports = module.exports = List;

/**
 * Initialize a new `List` test reporter.
 *
 * @param {Runner} runner
 * @api public
 */

function List(runner) {
  Base.call(this, runner);

  var self = this
    , stats = this.stats
    , n = 0;

  // tests started
  runner.on('start', function(){
    console.log();
  });

  // test started
  runner.on('test', function(test){
    process.stdout.write(color('pass', '    ' + test.fullTitle() + ': '));
  });

  // test passed
  runner.on('pass', function(test){
    var fmt = color('checkmark', '  ✔')
      + color('pass', ' %s: ')
      + color(test.speed, '%dms');
    console.log('\r' + fmt, test.fullTitle(), test.duration);
  });

  // test failed
  runner.on('fail', function(test, err){
    console.log('\r' + color('fail', '  %d) %s'), n++, test.fullTitle());
  });

  runner.on('end', self.epilogue.bind(self));
}

/**
 * Inherit from `Base.prototype`.
 */

List.prototype.__proto__ = Base.prototype;<|endoftext|>


<commit_before>/*
* That's the implementation of require-loader
*/<commit_msg>code require-loader.js<commit_after>/*
* That's the implementation of require-loader
*/

var fs = require('fs');
var util = require('util');

function ReqManager(options) {
    this.path = options.path || "./";
    this.watch = options.watch || true;
}

ReqManager.prototype.setPath = function(path) {
    this.path = path;
}

// not necessary
ReqManager.prototype.require = function(moduleName) {
    return require(moduleName);
}

ReqManager.prototype.remove = function(moduleName) {

}

ReqManager.prototype.autoStart = function() {

}

module.exports.create = function(options) {
    var requireManager = new ReqManager(options);
    if (options.autoStart) {
        requireManager.start();
    }
    return requireManager;
}

<|endoftext|>


<commit_before>// Load modules

var Stream = require('stream');
var Boom = require('boom');
var Shot = require('shot');
var Utils = require('../utils');

// Declare internals

var internals = {
    maxNestedPreparations: 5
};

/*
            /-- Raw        /-- Stream -----|--- File
    Base --|              |
            \-- Generic --|--- Buffer       /-- Text ----|-- Redirection
                          |                |
                           \-- Cacheable --|--- Empty
                                           |
                                           |-- Directory
                                           |
                                           |--- Object --|-- Error
                                           |
                                           |--- Cached
                                           |
                                            \-- View
*/

// Prototype response types

exports.Base = internals.Base = require('./base');
exports.Generic = internals.Generic = require('./generic');
exports.Cacheable = internals.Cacheable = require('./cacheable');

// Basic response types

exports.Empty = internals.Empty = require('./empty');
exports.Obj = internals.Obj = require('./obj');
exports.Text = internals.Text = require('./text');
exports.Stream = internals.Stream = require('./stream');
exports.File = internals.File = require('./file');
exports.Directory = internals.Directory = require('./directory');
exports.Raw = internals.Raw = require('./raw');
exports.Redirection = internals.Redirection = require('./redirection');
exports.View = internals.View = require('./view');
exports.Buffer = internals.Buffer = require('./buffer');

// Internal response types

internals.Error = require('./error');
internals.Cached = require('./cached');

// Utilities

exports.generate = function (result, onSend) {

    var response = null;

    if (result === null ||
        result === undefined ||
        result === '') {

        response = new internals.Empty();
    }
    else if (typeof result === 'string') {
        response = new internals.Text(result);
    }
    else if (typeof result === 'object') {
        if (Buffer.isBuffer(result)) {
            response = new internals.Buffer(result);
        }
        else if (result.variety ||
                result.isBoom) {

            response = result;
        }
        else if (result instanceof Error) {
            response = new Boom(result);
        }
        else if (result instanceof Stream) {
            response = new internals.Stream(result);
        }
    }

    if (!response) {
        response = new internals.Obj(result);
    }

    if (onSend) {
        response.send = function () {

            delete response.send;
            onSend();
        };
    }

    return response;
};

exports._respond = function (item, request, callback) {

    var errorPrepared = false;
    var prepareCount = 0;

    var prepare = function (response) {

        if (!response ||
            (!response.variety && !response.isBoom)) {

            response = Boom.internal('Unexpected response item', response);
        }

        if (response.isBoom) {
            response = new internals.Error(response);
            request.response = response;                        // Error occurs late and should update request.response object
        }

        if (!response._prepare) {
            return etag(response);
        }

        response._prepare(request, function (result) {

            if (!result._wasPrepared) {
                ++prepareCount;
                if (prepareCount > internals.maxNestedPreparations) {    // Prevent prepare loops
                    result = new internals.Error(Boom.internal('Response prepare count exceeded maximum allowed', item));
                    return send(result);
                }

                if (result.isBoom) {
                    if (errorPrepared) {
                        return send(new internals.Error(result));
                    }

                    errorPrepared = true;                                   // Prevent error loops
                }

                return prepare(result);
            }

            return etag(result);
        });
    };

    var etag = function (response) {

        if (request.method !== 'get' &&
            request.method !== 'head') {

            return send(response);
        }

        // Process ETag

        var etag = response._headers && response._headers.etag;
        if (etag &&
            request.raw.req.headers['if-none-match'] === etag) {

            var unchanged = new internals.Empty();
            unchanged._code = 304;
            return prepare(unchanged);
        }

        // Process If-Modified-Since headers

        var ifModifiedSinceHeader = request.raw.req.headers['if-modified-since'];
        var lastModifiedHeader = response._headers && response._headers['Last-Modified'];

        if (ifModifiedSinceHeader &&
            lastModifiedHeader) {

            var ifModifiedSince = Date.parse(ifModifiedSinceHeader);
            var lastModified = Date.parse(lastModifiedHeader);

            if (ifModifiedSince &&
                lastModified &&
                ifModifiedSince >= lastModified) {

                var unchanged = new internals.Empty();
                unchanged._code = 304;
                return prepare(unchanged);
            }
        }

        return send(response);
    };

    var send = function (response) {

        // Injection

        if (response._payload &&
            response._payload.length) {

            if (Shot.isInjection(request.raw.req) &&
                response.hasOwnProperty('raw')) {

                request.raw.res.hapi = { result: response.raw };
            }
        }

        response._transmit(request, function () {

            request.log(['http', 'response', response.variety]);
            return callback();
        });
    };

    prepare(item);
};

<commit_msg>Minor refactor of setting request.response<commit_after>// Load modules

var Stream = require('stream');
var Boom = require('boom');
var Shot = require('shot');
var Utils = require('../utils');

// Declare internals

var internals = {
    maxNestedPreparations: 5
};

/*
            /-- Raw        /-- Stream -----|--- File
    Base --|              |
            \-- Generic --|--- Buffer       /-- Text ----|-- Redirection
                          |                |
                           \-- Cacheable --|--- Empty
                                           |
                                           |-- Directory
                                           |
                                           |--- Object --|-- Error
                                           |
                                           |--- Cached
                                           |
                                            \-- View
*/

// Prototype response types

exports.Base = internals.Base = require('./base');
exports.Generic = internals.Generic = require('./generic');
exports.Cacheable = internals.Cacheable = require('./cacheable');

// Basic response types

exports.Empty = internals.Empty = require('./empty');
exports.Obj = internals.Obj = require('./obj');
exports.Text = internals.Text = require('./text');
exports.Stream = internals.Stream = require('./stream');
exports.File = internals.File = require('./file');
exports.Directory = internals.Directory = require('./directory');
exports.Raw = internals.Raw = require('./raw');
exports.Redirection = internals.Redirection = require('./redirection');
exports.View = internals.View = require('./view');
exports.Buffer = internals.Buffer = require('./buffer');

// Internal response types

internals.Error = require('./error');
internals.Cached = require('./cached');

// Utilities

exports.generate = function (result, onSend) {

    var response = null;

    if (result === null ||
        result === undefined ||
        result === '') {

        response = new internals.Empty();
    }
    else if (typeof result === 'string') {
        response = new internals.Text(result);
    }
    else if (typeof result === 'object') {
        if (Buffer.isBuffer(result)) {
            response = new internals.Buffer(result);
        }
        else if (result.variety ||
                result.isBoom) {

            response = result;
        }
        else if (result instanceof Error) {
            response = new Boom(result);
        }
        else if (result instanceof Stream) {
            response = new internals.Stream(result);
        }
    }

    if (!response) {
        response = new internals.Obj(result);
    }

    if (onSend) {
        response.send = function () {

            delete response.send;
            onSend();
        };
    }

    return response;
};

exports._respond = function (item, request, callback) {

    var errorPrepared = false;
    var prepareCount = 0;

    var prepare = function (response) {

        if (!response ||
            (!response.variety && !response.isBoom)) {

            response = Boom.internal('Unexpected response item', response);
        }

        if (response.isBoom) {
            response = new internals.Error(response);
        }

        if (!response._prepare) {
            return etag(response);
        }

        response._prepare(request, function (result) {

            if (!result._wasPrepared) {
                ++prepareCount;
                if (prepareCount > internals.maxNestedPreparations) {    // Prevent prepare loops
                    result = new internals.Error(Boom.internal('Response prepare count exceeded maximum allowed', item));
                    return send(result);
                }

                if (result.isBoom) {
                    if (errorPrepared) {
                        return send(new internals.Error(result));
                    }

                    errorPrepared = true;                                   // Prevent error loops
                }

                return prepare(result);
            }

            return etag(result);
        });
    };

    var etag = function (response) {

        if (request.method !== 'get' &&
            request.method !== 'head') {

            return send(response);
        }

        // Process ETag

        var etag = response._headers && response._headers.etag;
        if (etag &&
            request.raw.req.headers['if-none-match'] === etag) {

            var unchanged = new internals.Empty();
            unchanged._code = 304;
            return prepare(unchanged);
        }

        // Process If-Modified-Since headers

        var ifModifiedSinceHeader = request.raw.req.headers['if-modified-since'];
        var lastModifiedHeader = response._headers && response._headers['Last-Modified'];

        if (ifModifiedSinceHeader &&
            lastModifiedHeader) {

            var ifModifiedSince = Date.parse(ifModifiedSinceHeader);
            var lastModified = Date.parse(lastModifiedHeader);

            if (ifModifiedSince &&
                lastModified &&
                ifModifiedSince >= lastModified) {

                var unchanged = new internals.Empty();
                unchanged._code = 304;
                return prepare(unchanged);
            }
        }

        return send(response);
    };

    var send = function (response) {

        request.response = response;                        // Error occurs late and should update request.response object

        // Injection

        if (response._payload &&
            response._payload.length) {

            if (Shot.isInjection(request.raw.req) &&
                response.hasOwnProperty('raw')) {

                request.raw.res.hapi = { result: response.raw };
            }
        }

        response._transmit(request, function () {

            request.log(['http', 'response', response.variety]);
            return callback();
        });
    };

    prepare(item);
};

<|endoftext|>


<commit_before>/**
 * @module result-builder
 */
'use strict';

var _ = require('lodash');
var ImplementationError = require('flora-errors').ImplementationError;
var DataError = require('flora-errors').DataError;
var NotFoundError = require('flora-errors').NotFoundError;

var keySeparator = '-';

/**
 * Builds the result object from output of {@link module:datasource-executor|datasource-executor}.
 *
 * @param {Api}     api
 * @param {Object}  request
 * @param {Array}   rawResults      - Flat list of results from {@link module:datasource-executor|datasource-executor}
 * @param {Object}  resolvedConfig  - Request-specific config (selected attributes flagged)
 * @return {Object} Result object with "data" node and optional "cursor" node
 */
module.exports = function resultBuilder(api, request, rawResults, resolvedConfig) {
    var rawResult, result, context;

    preprocessRawResults(rawResults, resolvedConfig);

    context = {
        api: api,
        request: request,
        rawResults: rawResults,
        resourceAttrNode: resolvedConfig, // attribute node of current (sub-)resource
        attrPath: [], // current path from root
        primaryKey: null, // current primary key
        secondaryRows: null // current row of all DataSources (by primary key)
    };

    // determine primary result of main resource to iterate over at root level:
    rawResult = getResult(resolvedConfig, resolvedConfig.primaryDataSource, context);

    if (!resolvedConfig.many) {
        if (rawResult.data.length <= 0) {
            throw new NotFoundError('Requested item not found');
        }
        // TODO: DataError when more than 1 result?

        result = {
            data: buildItem(resolvedConfig, rawResult.data[0], context)
        };
    } else {
        result = {
            cursor: {
                totalCount: rawResult.totalCount
            },
            data: rawResult.data.map(function (row) {
                return buildItem(resolvedConfig, row, context);
            })
        };
    }

    return result;
};

/**
 * Link resultIds into attribute tree and index rows by key.
 *
 * @private
 */
function preprocessRawResults(rawResults, resolvedConfig) {
    rawResults.forEach(function (rawResult, resultId) {
        // link resultIds into attribute tree (per DataSource):
        var attrNode = getAttribute(rawResult.attributePath, resolvedConfig);
        attrNode.dataSources[rawResult.dataSourceName].resultId = resultId;

        // index rows by childKey if available (top-level result has no childKey and does not need to be indexed):
        if (rawResult.childKey) {
            rawResult.indexedData = {};
            rawResult.data.forEach(function (row, i) {
                var key = rawResult.childKey.map(function (keyAttr) {
                    if (typeof row[keyAttr] === 'undefined') {
                        throw new DataError('Result-row ' + i + ' of ' +
                            '"' + (rawResult.attributePath.length > 0 ?
                                rawResult.attributePath.join('.') : '{root}') + '" ' +
                            '(DataSource "' + rawResult.dataSourceName + '") ' +
                            'misses child key attribute "' + keyAttr + '"');
                    }
                    return row[keyAttr];
                }).join(keySeparator);

                if (!rawResult.uniqueChildKey) {
                    if (!rawResult.indexedData[key]) rawResult.indexedData[key] = [];
                    rawResult.indexedData[key].push(row);
                } else {
                    if (rawResult.indexedData[key]) {
                        throw new DataError('Result-row ' + i + ' of ' +
                            '"' + (rawResult.attributePath.length > 0 ?
                                rawResult.attributePath.join('.') : '{root}') + '" ' +
                            '(DataSource "' + rawResult.dataSourceName + '") ' +
                            'has duplicate child key "' + key + '"');
                    }
                    rawResult.indexedData[key] = row;
                }
            });
        }
    });
}

/**
 * Resolve attribute path relative to attrNode and return child attrNode
 *
 * @param {Array} path Array of attribute-names representing the path
 * @param {Object} attrNode Root node where to start resolving
 * @private
 */
function getAttribute(path, attrNode) {
    path.forEach(function (attrName) {
        if (!(attrNode.attributes && attrNode.attributes[attrName])) {
            throw new ImplementationError('Result-Builder: Unknown attribute "' + path.join('.') + '"');
        }
        attrNode = attrNode.attributes[attrName];
    });
    return attrNode;
}

/**
 * Recursively builds one item. This is where most of the magic happens.
 *
 * @private
 */
function buildItem(parentAttrNode, row, context) {
    var item = {}, attrName, attrNode;

    if (context.primaryKey === null) {
        // determine current primary key (from primary DataSource):
        var resAttrNode = context.resourceAttrNode;
        var indexKey = resAttrNode.resolvedPrimaryKey[resAttrNode.primaryDataSource];
        context.primaryKey = indexKey.map(function (keyAttr) {
            if (typeof row[keyAttr] === 'undefined') {
                throw new DataError('Result-row of ' +
                    '"' + (context.attrPath.length > 0 ? context.attrPath.join('.') : '{root}') + '" ' +
                    '(DataSource "' + resAttrNode.primaryDataSource + '") ' +
                    'misses primary key attribute "' + keyAttr + '"');
            }
            return row[keyAttr];
        }).join(keySeparator);

        // link rows from secondary DataSources by primary key:
        context.secondaryRows = context.secondaryRows || {}; // row from joinVia-DataSource is passed via context here

        for (var dataSourceName in resAttrNode.dataSources) {
            if (dataSourceName === resAttrNode.primaryDataSource) continue;
            if (! ('resultId' in resAttrNode.dataSources[dataSourceName])) continue;

            var secondaryResult = context.rawResults[resAttrNode.dataSources[dataSourceName].resultId];
            if (typeof secondaryResult.indexedData[context.primaryKey] === 'undefined') {
                context.api.log.debug(new DataError('Secondary DataSource "' + dataSourceName + '" of ' +
                    '"' + (context.attrPath.length > 0 ? context.attrPath.join('.') : '{root}') + '": ' +
                    'row with key "' + context.primaryKey + '" not found'));
                context.secondaryRows[dataSourceName] = null;
            } else {
                context.secondaryRows[dataSourceName] = secondaryResult.indexedData[context.primaryKey];
            }
        }
    }

    for (attrName in parentAttrNode.attributes) {
        attrNode = parentAttrNode.attributes[attrName];

        if (!attrNode.selected) continue;

        if (attrNode.attributes) {
            var subContext = _.clone(context);
            subContext.attrPath = context.attrPath.concat([attrName]);

            if (attrNode.dataSources) {
                // resource context:
                subContext.resourceAttrNode = attrNode;
                subContext.primaryKey = null;
                subContext.secondaryRows = null;

                var rawResult = getResult(attrNode, attrNode.joinVia || attrNode.primaryDataSource, subContext);
                var keyIsNull = true;
                var keys = rawResult.parentKey.map(function (keyAttr) {
                    if (typeof row[keyAttr] === 'undefined') {
                        /*
                        // TODO: Strict mode?
                        throw new DataError('Sub-resource "' + subContext.attrPath.join('.') + '" ' +
                            'misses key attribute "' + keyAttr + '" in parent result ' +
                            '(DataSource "' + context.resourceAttrNode.primaryDataSource + '")');
                        */
                    } else if (row[keyAttr] !== null) {
                        keyIsNull = false;
                    }
                    return row[keyAttr];
                });

                if (keyIsNull) {
                    keys = [];
                } else if (!rawResult.multiValuedParentKey) {
                    // normal and composite parentKey: "keys" is a string (as array) afterwards:
                    keys = [keys.join(keySeparator)];
                } else {
                    // dereference parentKey array (key length must be 1 when multiValued)
                    // but "keys" is still an array (multiValued) afterwards:
                    keys = keys[0];

                    if (!Array.isArray(keys)) {
                        throw new DataError('Sub-resource "' + subContext.attrPath.join('.') + '" ' +
                            'multiValued key attribute "' + rawResult.parentKey[0] + '" ' +
                            'in parent result is not an array ' +
                            '(DataSource "' + context.resourceAttrNode.primaryDataSource + '")');
                    }
                }

                var secondaryRows = null;
                if (attrNode.joinVia) {
                    var primaryRawResult = getResult(attrNode, attrNode.primaryDataSource, subContext);
                    var joinViaKeys = [];
                    secondaryRows = {};

                    keys.forEach(function (key) {
                        var joinViaRows = rawResult.indexedData[key];
                        if (!joinViaRows) return;

                        if (rawResult.uniqueChildKey) joinViaRows = [joinViaRows];

                        joinViaRows.forEach(function (joinViaRow) {
                            var childKeyIsNull = true;
                            var childKey = primaryRawResult.parentKey.map(function (keyAttr) {
                                if (typeof joinViaRow[keyAttr] === 'undefined') {
                                    throw new DataError('Sub-resource "' + subContext.attrPath.join('.') + '" ' +
                                        'misses key attribute "' + keyAttr + '" in joinVia result ' +
                                        '(DataSource "' + attrNode.joinVia + '")');
                                } else if (joinViaRow[keyAttr] !== null) {
                                    childKeyIsNull = false;
                                }
                                return joinViaRow[keyAttr];
                            }).join(keySeparator);

                            if (!childKeyIsNull) {
                                joinViaKeys.push(childKey);

                                secondaryRows[childKey] = {};
                                secondaryRows[childKey][attrNode.joinVia] = joinViaRow;
                            }
                        });
                    });

                    rawResult = primaryRawResult;
                    keys = joinViaKeys;
                }

                var subItem = [];
                keys.forEach(function (key) {
                    var subRow = rawResult.indexedData[key];
                    if (!subRow) {
                        context.api.log.debug(new DataError(
                            'Foreign key ' + rawResult.parentKey.join(',') + ' = "' + key + '" ' +
                            'not found in sub-resource "' + subContext.attrPath.join('.') + '" ' +
                            '(DataSource "' + attrNode.primaryDataSource + '")'));
                    } else {
                        subContext.secondaryRows = secondaryRows ? secondaryRows[key] : null;

                        if (!rawResult.uniqueChildKey) {
                            subRow.forEach(function (singleSubRow) {
                                subItem.push(buildItem(attrNode, singleSubRow, subContext));
                            });
                        } else {
                            subItem.push(buildItem(attrNode, subRow, subContext));
                        }
                    }
                });

                if (attrNode.many) {
                    item[attrName] = subItem;
                } else {
                    item[attrName] = subItem[0] || null;
                }
            } else {
                // nested-attribute context:
                item[attrName] = buildItem(attrNode, row, subContext);
            }
        } else {
            item[attrName] = buildAttribute(attrNode, row, context);
        }
    }

    // extension: "item"
    var resource = context.api.getResource(parentAttrNode.resourceName);
    if (resource && resource.extensions && resource.extensions.item) {
        context.api.log.trace('handle: "item" extension (%s)', parentAttrNode.resourceName);
        resource.extensions.item({
            request: context.request,
            item: item
        });
    }

    context.primaryKey = null; // clear primaryKey cache for next row

    return item;
}

function buildAttribute(attrNode, row, context) {
    var value = null;

    // value from mapped DataSource or static value:
    if (attrNode.map) {
        var mappedAttrName = attrNode.map.default[attrNode.selectedDataSource];

        if (attrNode.selectedDataSource === context.resourceAttrNode.primaryDataSource) {
            value = row[mappedAttrName];
        } else {
            if (!context.secondaryRows[attrNode.selectedDataSource]) {
                if (typeof context.secondaryRows[attrNode.selectedDataSource] === 'undefined') {
                    throw new ImplementationError('Secondary-Result for ' +
                        '"' + (context.attrPath.length > 0 ? context.attrPath.join('.') : '{root}') + '" ' +
                        '(DataSource "' + attrNode.selectedDataSource + '") missing');
                }
                value = null;
            } else {
                value = context.secondaryRows[attrNode.selectedDataSource][mappedAttrName];
            }
        }

        if (typeof value === 'undefined') {
            throw new DataError('Result-row ID "' + context.primaryKey + '" ' +
                (context.attrPath.length > 0 ? 'of "' + context.attrPath.join('.') + '" ' : '') +
                '(DataSource "' + attrNode.selectedDataSource + '") ' +
                'misses attribute "' + mappedAttrName + '"');
        }
    } else if ('value' in attrNode) {
        value = attrNode.value;
    }

    return value;
}

/**
 * Get raw result for given DataSource for current (sub-)resource.
 *
 * @private
 */
function getResult(attrNode, dataSourceName, context) {
    var resultId = attrNode.dataSources[dataSourceName].resultId;
    if (typeof resultId === 'undefined') {
        throw new ImplementationError('Result for ' +
            '"' + (context.attrPath.length > 0 ? context.attrPath.join('.') : '{root}') + '" ' +
            '(DataSource "' + dataSourceName + '") missing');
    }
    return context.rawResults[resultId];
}
<commit_msg>result-builder: do not collect joinVia-DataSources as secondaryRows by primaryKey<commit_after>/**
 * @module result-builder
 */
'use strict';

var _ = require('lodash');
var ImplementationError = require('flora-errors').ImplementationError;
var DataError = require('flora-errors').DataError;
var NotFoundError = require('flora-errors').NotFoundError;

var keySeparator = '-';

/**
 * Builds the result object from output of {@link module:datasource-executor|datasource-executor}.
 *
 * @param {Api}     api
 * @param {Object}  request
 * @param {Array}   rawResults      - Flat list of results from {@link module:datasource-executor|datasource-executor}
 * @param {Object}  resolvedConfig  - Request-specific config (selected attributes flagged)
 * @return {Object} Result object with "data" node and optional "cursor" node
 */
module.exports = function resultBuilder(api, request, rawResults, resolvedConfig) {
    var rawResult, result, context;

    preprocessRawResults(rawResults, resolvedConfig);

    context = {
        api: api,
        request: request,
        rawResults: rawResults,
        resourceAttrNode: resolvedConfig, // attribute node of current (sub-)resource
        attrPath: [], // current path from root
        primaryKey: null, // current primary key
        secondaryRows: null // current row of all DataSources (by primary key)
    };

    // determine primary result of main resource to iterate over at root level:
    rawResult = getResult(resolvedConfig, resolvedConfig.primaryDataSource, context);

    if (!resolvedConfig.many) {
        if (rawResult.data.length <= 0) {
            throw new NotFoundError('Requested item not found');
        }
        // TODO: DataError when more than 1 result?

        result = {
            data: buildItem(resolvedConfig, rawResult.data[0], context)
        };
    } else {
        result = {
            cursor: {
                totalCount: rawResult.totalCount
            },
            data: rawResult.data.map(function (row) {
                return buildItem(resolvedConfig, row, context);
            })
        };
    }

    return result;
};

/**
 * Link resultIds into attribute tree and index rows by key.
 *
 * @private
 */
function preprocessRawResults(rawResults, resolvedConfig) {
    rawResults.forEach(function (rawResult, resultId) {
        // link resultIds into attribute tree (per DataSource):
        var attrNode = getAttribute(rawResult.attributePath, resolvedConfig);
        attrNode.dataSources[rawResult.dataSourceName].resultId = resultId;

        // index rows by childKey if available (top-level result has no childKey and does not need to be indexed):
        if (rawResult.childKey) {
            rawResult.indexedData = {};
            rawResult.data.forEach(function (row, i) {
                var key = rawResult.childKey.map(function (keyAttr) {
                    if (typeof row[keyAttr] === 'undefined') {
                        throw new DataError('Result-row ' + i + ' of ' +
                            '"' + (rawResult.attributePath.length > 0 ?
                                rawResult.attributePath.join('.') : '{root}') + '" ' +
                            '(DataSource "' + rawResult.dataSourceName + '") ' +
                            'misses child key attribute "' + keyAttr + '"');
                    }
                    return row[keyAttr];
                }).join(keySeparator);

                if (!rawResult.uniqueChildKey) {
                    if (!rawResult.indexedData[key]) rawResult.indexedData[key] = [];
                    rawResult.indexedData[key].push(row);
                } else {
                    if (rawResult.indexedData[key]) {
                        throw new DataError('Result-row ' + i + ' of ' +
                            '"' + (rawResult.attributePath.length > 0 ?
                                rawResult.attributePath.join('.') : '{root}') + '" ' +
                            '(DataSource "' + rawResult.dataSourceName + '") ' +
                            'has duplicate child key "' + key + '"');
                    }
                    rawResult.indexedData[key] = row;
                }
            });
        }
    });
}

/**
 * Resolve attribute path relative to attrNode and return child attrNode
 *
 * @param {Array} path Array of attribute-names representing the path
 * @param {Object} attrNode Root node where to start resolving
 * @private
 */
function getAttribute(path, attrNode) {
    path.forEach(function (attrName) {
        if (!(attrNode.attributes && attrNode.attributes[attrName])) {
            throw new ImplementationError('Result-Builder: Unknown attribute "' + path.join('.') + '"');
        }
        attrNode = attrNode.attributes[attrName];
    });
    return attrNode;
}

/**
 * Recursively builds one item. This is where most of the magic happens.
 *
 * @private
 */
function buildItem(parentAttrNode, row, context) {
    var item = {}, attrName, attrNode;

    if (context.primaryKey === null) {
        // determine current primary key (from primary DataSource):
        var resAttrNode = context.resourceAttrNode;
        var indexKey = resAttrNode.resolvedPrimaryKey[resAttrNode.primaryDataSource];
        context.primaryKey = indexKey.map(function (keyAttr) {
            if (typeof row[keyAttr] === 'undefined') {
                throw new DataError('Result-row of ' +
                    '"' + (context.attrPath.length > 0 ? context.attrPath.join('.') : '{root}') + '" ' +
                    '(DataSource "' + resAttrNode.primaryDataSource + '") ' +
                    'misses primary key attribute "' + keyAttr + '"');
            }
            return row[keyAttr];
        }).join(keySeparator);

        // link rows from secondary DataSources by primary key:
        context.secondaryRows = context.secondaryRows || {}; // row from joinVia-DataSource is passed via context here

        for (var dataSourceName in resAttrNode.dataSources) {
            if (dataSourceName === resAttrNode.primaryDataSource) continue;
            if (resAttrNode.joinVia && dataSourceName === resAttrNode.joinVia) continue;
            if (! ('resultId' in resAttrNode.dataSources[dataSourceName])) continue;

            var secondaryResult = context.rawResults[resAttrNode.dataSources[dataSourceName].resultId];
            if (typeof secondaryResult.indexedData[context.primaryKey] === 'undefined') {
                context.api.log.debug(new DataError('Secondary DataSource "' + dataSourceName + '" of ' +
                    '"' + (context.attrPath.length > 0 ? context.attrPath.join('.') : '{root}') + '": ' +
                    'row with key "' + context.primaryKey + '" not found'));
                context.secondaryRows[dataSourceName] = null;
            } else {
                context.secondaryRows[dataSourceName] = secondaryResult.indexedData[context.primaryKey];
            }
        }
    }

    for (attrName in parentAttrNode.attributes) {
        attrNode = parentAttrNode.attributes[attrName];

        if (!attrNode.selected) continue;

        if (attrNode.attributes) {
            var subContext = _.clone(context);
            subContext.attrPath = context.attrPath.concat([attrName]);

            if (attrNode.dataSources) {
                // resource context:
                subContext.resourceAttrNode = attrNode;
                subContext.primaryKey = null;
                subContext.secondaryRows = null;

                var rawResult = getResult(attrNode, attrNode.joinVia || attrNode.primaryDataSource, subContext);
                var keyIsNull = true;
                var keys = rawResult.parentKey.map(function (keyAttr) {
                    if (typeof row[keyAttr] === 'undefined') {
                        /*
                        // TODO: Strict mode?
                        throw new DataError('Sub-resource "' + subContext.attrPath.join('.') + '" ' +
                            'misses key attribute "' + keyAttr + '" in parent result ' +
                            '(DataSource "' + context.resourceAttrNode.primaryDataSource + '")');
                        */
                    } else if (row[keyAttr] !== null) {
                        keyIsNull = false;
                    }
                    return row[keyAttr];
                });

                if (keyIsNull) {
                    keys = [];
                } else if (!rawResult.multiValuedParentKey) {
                    // normal and composite parentKey: "keys" is a string (as array) afterwards:
                    keys = [keys.join(keySeparator)];
                } else {
                    // dereference parentKey array (key length must be 1 when multiValued)
                    // but "keys" is still an array (multiValued) afterwards:
                    keys = keys[0];

                    if (!Array.isArray(keys)) {
                        throw new DataError('Sub-resource "' + subContext.attrPath.join('.') + '" ' +
                            'multiValued key attribute "' + rawResult.parentKey[0] + '" ' +
                            'in parent result is not an array ' +
                            '(DataSource "' + context.resourceAttrNode.primaryDataSource + '")');
                    }
                }

                var secondaryRows = null;
                if (attrNode.joinVia) {
                    var primaryRawResult = getResult(attrNode, attrNode.primaryDataSource, subContext);
                    var joinViaKeys = [];
                    secondaryRows = {};

                    keys.forEach(function (key) {
                        var joinViaRows = rawResult.indexedData[key];
                        if (!joinViaRows) return;

                        if (rawResult.uniqueChildKey) joinViaRows = [joinViaRows];

                        joinViaRows.forEach(function (joinViaRow) {
                            var childKeyIsNull = true;
                            var childKey = primaryRawResult.parentKey.map(function (keyAttr) {
                                if (typeof joinViaRow[keyAttr] === 'undefined') {
                                    throw new DataError('Sub-resource "' + subContext.attrPath.join('.') + '" ' +
                                        'misses key attribute "' + keyAttr + '" in joinVia result ' +
                                        '(DataSource "' + attrNode.joinVia + '")');
                                } else if (joinViaRow[keyAttr] !== null) {
                                    childKeyIsNull = false;
                                }
                                return joinViaRow[keyAttr];
                            }).join(keySeparator);

                            if (!childKeyIsNull) {
                                joinViaKeys.push(childKey);

                                secondaryRows[childKey] = {};
                                secondaryRows[childKey][attrNode.joinVia] = joinViaRow;
                            }
                        });
                    });

                    rawResult = primaryRawResult;
                    keys = joinViaKeys;
                }

                var subItem = [];
                keys.forEach(function (key) {
                    var subRow = rawResult.indexedData[key];
                    if (!subRow) {
                        context.api.log.debug(new DataError(
                            'Foreign key ' + rawResult.parentKey.join(',') + ' = "' + key + '" ' +
                            'not found in sub-resource "' + subContext.attrPath.join('.') + '" ' +
                            '(DataSource "' + attrNode.primaryDataSource + '")'));
                    } else {
                        subContext.secondaryRows = secondaryRows ? secondaryRows[key] : null;

                        if (!rawResult.uniqueChildKey) {
                            subRow.forEach(function (singleSubRow) {
                                subItem.push(buildItem(attrNode, singleSubRow, subContext));
                            });
                        } else {
                            subItem.push(buildItem(attrNode, subRow, subContext));
                        }
                    }
                });

                if (attrNode.many) {
                    item[attrName] = subItem;
                } else {
                    item[attrName] = subItem[0] || null;
                }
            } else {
                // nested-attribute context:
                item[attrName] = buildItem(attrNode, row, subContext);
            }
        } else {
            item[attrName] = buildAttribute(attrNode, row, context);
        }
    }

    // extension: "item"
    var resource = context.api.getResource(parentAttrNode.resourceName);
    if (resource && resource.extensions && resource.extensions.item) {
        context.api.log.trace('handle: "item" extension (%s)', parentAttrNode.resourceName);
        resource.extensions.item({
            request: context.request,
            item: item
        });
    }

    context.primaryKey = null; // clear primaryKey cache for next row

    return item;
}

function buildAttribute(attrNode, row, context) {
    var value = null;

    // value from mapped DataSource or static value:
    if (attrNode.map) {
        var mappedAttrName = attrNode.map.default[attrNode.selectedDataSource];

        if (attrNode.selectedDataSource === context.resourceAttrNode.primaryDataSource) {
            value = row[mappedAttrName];
        } else {
            if (!context.secondaryRows[attrNode.selectedDataSource]) {
                if (typeof context.secondaryRows[attrNode.selectedDataSource] === 'undefined') {
                    throw new ImplementationError('Secondary-Result for ' +
                        '"' + (context.attrPath.length > 0 ? context.attrPath.join('.') : '{root}') + '" ' +
                        '(DataSource "' + attrNode.selectedDataSource + '") missing');
                }
                value = null;
            } else {
                value = context.secondaryRows[attrNode.selectedDataSource][mappedAttrName];
            }
        }

        if (typeof value === 'undefined') {
            throw new DataError('Result-row ID "' + context.primaryKey + '" ' +
                (context.attrPath.length > 0 ? 'of "' + context.attrPath.join('.') + '" ' : '') +
                '(DataSource "' + attrNode.selectedDataSource + '") ' +
                'misses attribute "' + mappedAttrName + '"');
        }
    } else if ('value' in attrNode) {
        value = attrNode.value;
    }

    return value;
}

/**
 * Get raw result for given DataSource for current (sub-)resource.
 *
 * @private
 */
function getResult(attrNode, dataSourceName, context) {
    var resultId = attrNode.dataSources[dataSourceName].resultId;
    if (typeof resultId === 'undefined') {
        throw new ImplementationError('Result for ' +
            '"' + (context.attrPath.length > 0 ? context.attrPath.join('.') : '{root}') + '" ' +
            '(DataSource "' + dataSourceName + '") missing');
    }
    return context.rawResults[resultId];
}
<|endoftext|>


<commit_before>/**
 This is intended to let other SUPER level apps trigger events
 */
var async = require('async');
var _ = require('underscore');
 module.exports = function(app){
       app.all(app.njax.config.trigger_uri, function(req, res, next){

            if(!req.active_application){
                return next();
            }
            //People using the core app shouldn't need this as it has direct access
            if(req.active_application.level != 'SUPER' || req.active_application.namespace == app.njax.config.core_app){
                return next();
            }
            var event = req.body.event;

            if(!req.body.users){
                return next(new Error("You need to submit a list of users to broadcast to"));
            }
           var users = [];
            async.eachSeries(
                req.body.users,
                function(user, cb){
                    if(_.isObject(user) && user._id){
                        var user = user._id;
                    }
                    if(_.isString(user) && user.length > 1){
                        var query = {
                            $or:[
                                { email:user },
                                { namespace:user }
                            ]
                        }
                        var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");

                        if(checkForHexRegExp.test(user)){
                            query.$or.push({ _id:new ObjectId(user) });
                        }

                        return app.model.Account.findOne(query).exec(function(err, account){
                            if(err) return next(err);
                            if(!account){
                                return next(new Error("Cannot identify user" + user));
                            }
                            users.push(account);
                            return cb();
                        });
                    }
                    return next(new Error("Cannot identify user" + user))
                },
                function(errs){
                    app.njax.broadcast(
                        users,
                        event,
                        req.body.data,
                        req.active_application.namespace
                    );
                    return res.json({ message: 'Job qued up' });//TODO: Insert Event ID Here
                }
            )

       });
 }<commit_msg>Code flying from the fingertips<commit_after>/**
 This is intended to let other SUPER level apps trigger events
 */
var async = require('async');
var _ = require('underscore');
 module.exports = function(app){
       app.all(app.njax.config.trigger_uri, function(req, res, next){

            if(!req.active_application){
                return next();
            }
            //People using the core app shouldn't need this as it has direct access
            if(req.active_application.level != 'SUPER' || req.active_application.namespace == app.njax.config.core_app){
                return next();
            }
            var event = req.body.event;

            if(!req.body.users){
                return next(new Error("You need to submit a list of users to broadcast to"));
            }
           var users = [];
            async.eachSeries(
                req.body.users,
                function(user, cb){
                    console.log(user);
                    if(_.isObject(user) && user._id){
                        var user = user._id;
                    }
                    var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
                    if(checkForHexRegExp.test(user)){
                        var query = {
                            $or:[
                                { _id: new app.mongoose.Types.ObjectId(user) },
                                { _id: user }
                            ]
                        };

                    }else if(_.isString(user) && user.length > 1){
                        var query = {
                            $or:[
                                { email:user },
                                { namespace:user }
                            ]
                        }
                    }

                    if(!query){
                        return next(new Error("Cannot identify user" + user))
                    }

                    return app.model.Account.findOne(query).exec(function(err, account){
                        if(err) return next(err);
                        if(!account){
                            return next(new Error("Cannot identify user: " + user));
                        }
                        users.push(account);
                        return cb();
                    });

                },
                function(errs){

                    app.njax.broadcast(
                        users,
                        event,
                        req.body.data,
                        req.active_application.namespace
                    );
                    return res.json({ message: 'Job qued up' });//TODO: Insert Event ID Here
                }
            )

       });
 }<|endoftext|>


<commit_before>'use strict';

var debug = require('debug')('sphere:modelstore');
var when = require('when');
var errors = require('./errors');
var Joi = require('joi');

var VALID_MODELS = ['room', 'thing'];

var UserSchema = Joi.object().keys({id: Joi.string().guid()});

module.exports.calculateSyncItems = function(user, modelName, nodeManifest) {
  Joi.assert(user, UserSchema);
  Joi.assert(modelName, Joi.string().valid(VALID_MODELS));
  Joi.assert(nodeManifest, Joi.object());

  var query = {
    key: [user.id, modelName]
  };
  return this.couchdb.view('manifest/manifest', query).then(function(result) {
    var cloudManifest = _makeManifest(result);
    var cloudNewerItems = _findGreaterItems(cloudManifest, nodeManifest);
    var nodeNewerItems = _findGreaterItems(nodeManifest, cloudManifest);

    var response = {
      model: modelName,
      cloud_requires: nodeNewerItems,
      node_requires: cloudNewerItems
    };

    return response;
  });
};

module.exports.doSyncItems = function(user, modelName, pushedItems, requestedObjectIds) {
  Joi.assert(user, UserSchema);
  Joi.assert(modelName, Joi.string().valid(VALID_MODELS));
  Joi.assert(requestedObjectIds, Joi.array().includes(Joi.string().guid()));
  Joi.assert(pushedItems, Joi.object());

  // retrieve the requested objects

  var query = {
    keys: [],
    include_docs: true
  };

  requestedObjectIds.forEach(function(v) {
    Joi.assert(v, Joi.string().guid());
    query.keys.push([user.id, modelName, v]);
  });

  var responseItems = this.couchdb.view('manifest/manifest', query).then(function(result) {
    var resultMap = {};

    result.rows.map(function(row) {
      resultMap[row.doc.model.id] = {
        data: row.doc.data,
        last_modified: row.doc.last_modified
      };
    });

    return resultMap;
  });

  var existingIds = Object.keys(pushedItems).map(function(objectId) {
    return [user.id, modelName, objectId].join(':');
  });

  var savePromise = this.couchdb.getAll(existingIds).then(function(result) {
    // update the objects provided by the node
    var toSave = [];

    var oldDocLookup = {};
    result.forEach(function(doc) {
      if (doc !== null) {
        oldDocLookup[doc._id] = doc;
      }
    });

    for (var objectId in pushedItems) {
      var object = pushedItems[objectId];

      Joi.assert(objectId, Joi.string().guid());
      Joi.assert(object, Joi.object().keys({ last_modified: Joi.number(), data: Joi.object().allow(null) }));

      // object exists
      var newDocument = {
        _id: [user.id, modelName, objectId].join(':'),
        user: user,
        model: {
          type: modelName,
          id: objectId,
        },
        data: object.data,
        last_modified: object.last_modified,
      };

      if (object.data === null) {
        newDocument._deleted = true; // mark as couchdb-deleted
      }

      if (oldDocLookup.hasOwnProperty(newDocument._id)) {
        var oldDoc = oldDocLookup[newDocument._id];
        if (oldDoc.last_modified < newDocument.last_modified) {
          newDocument._rev = oldDoc._rev;
        } else {
          continue; // skip this document, it's been updated in cloud in the meantime
        }
      }

      toSave.push(newDocument);
    }

    if (toSave.length > 0) {
      return this.couchdb.saveAll(toSave);
    }
  }.bind(this));

  // merge for response

  return when.all([
    responseItems,
    savePromise,
  ]).spread(function(requestedObjects, saveResponse) {
    var pushedObjects = {};

    if (saveResponse) {
      saveResponse.forEach(function(doc) {
        pushedObjects[doc.data.id] = (typeof doc._rev === 'string')
      });
    }

    return {
      model: modelName,
      requestedObjects: requestedObjects,
      pushedObjects: pushedObjects
    };
  });
};

module.exports.pushItemChange = function(user, modelName, pushedItem) {
  Joi.assert(user, UserSchema);
  Joi.assert(modelName, Joi.string().valid(VALID_MODELS));
  Joi.assert(pushedItem, Joi.object().keys({
    id: Joi.string().guid(),
    last_modified: Joi.number(),
    data: Joi.object().keys({
      id: Joi.string().guid()
    }).unknown(true).allow(null)
  }));

  var newDocument = {
    _id: [user.id, modelName, pushedItem.id].join(':'),
    user: user,
    model: {
      type: modelName,
      id: pushedItem.id,
    },
    data: pushedItem.data,
    last_modified: pushedItem.last_modified,
  };

  if (pushedItem.data === null) {
    newDocument._deleted = true; // mark as couchdb-deleted
  }

  return this.couchdb.get(newDocument._id).then(function(oldDoc) {
    debug('pushItemChange', 'oldDoc', oldDoc);
    if (oldDoc) {
      if (oldDoc.last_modified < newDocument.last_modified) {
        newDocument._rev = oldDoc._rev;
      } else {
        return; // timestamp mismatch
      }
    }

    return this.couchdb.save(newDocument);
  }.bind(this)).then(function(result) {
    var success = !!(result && result._rev);
    var pushedObjects = {};
    pushedObjects[newDocument.model.id] = success;
    return {
      model: modelName,
      pushedObjects: pushedObjects
    };
  });
};

function _makeManifest(result) {
  var manifest = {};

  result.rows.forEach(function(row) {
    manifest[row.value.id] = row.value.last_modified;
  });

  return manifest;
}

function _findGreaterItems(src, dst) {
  var greater = {};

  for (var k in src) {
    var sv = src[k];
    var dv = dst[k];

    if (!dv || dv < sv) {
      greater[k] = sv;
    }
  }

  return greater;
}<commit_msg>Allow device model.<commit_after>'use strict';

var debug = require('debug')('sphere:modelstore');
var when = require('when');
var errors = require('./errors');
var Joi = require('joi');

var VALID_MODELS = ['room', 'thing', 'device'];

var UserSchema = Joi.object().keys({id: Joi.string().guid()});

module.exports.calculateSyncItems = function(user, modelName, nodeManifest) {
  Joi.assert(user, UserSchema);
  Joi.assert(modelName, Joi.string().valid(VALID_MODELS));
  Joi.assert(nodeManifest, Joi.object());

  var query = {
    key: [user.id, modelName]
  };
  return this.couchdb.view('manifest/manifest', query).then(function(result) {
    var cloudManifest = _makeManifest(result);
    var cloudNewerItems = _findGreaterItems(cloudManifest, nodeManifest);
    var nodeNewerItems = _findGreaterItems(nodeManifest, cloudManifest);

    var response = {
      model: modelName,
      cloud_requires: nodeNewerItems,
      node_requires: cloudNewerItems
    };

    return response;
  });
};

module.exports.doSyncItems = function(user, modelName, pushedItems, requestedObjectIds) {
  Joi.assert(user, UserSchema);
  Joi.assert(modelName, Joi.string().valid(VALID_MODELS));
  Joi.assert(requestedObjectIds, Joi.array().includes(Joi.string().guid()));
  Joi.assert(pushedItems, Joi.object());

  // retrieve the requested objects

  var query = {
    keys: [],
    include_docs: true
  };

  requestedObjectIds.forEach(function(v) {
    Joi.assert(v, Joi.string().guid());
    query.keys.push([user.id, modelName, v]);
  });

  var responseItems = this.couchdb.view('manifest/manifest', query).then(function(result) {
    var resultMap = {};

    result.rows.map(function(row) {
      resultMap[row.doc.model.id] = {
        data: row.doc.data,
        last_modified: row.doc.last_modified
      };
    });

    return resultMap;
  });

  var existingIds = Object.keys(pushedItems).map(function(objectId) {
    return [user.id, modelName, objectId].join(':');
  });

  var savePromise = this.couchdb.getAll(existingIds).then(function(result) {
    // update the objects provided by the node
    var toSave = [];

    var oldDocLookup = {};
    result.forEach(function(doc) {
      if (doc !== null) {
        oldDocLookup[doc._id] = doc;
      }
    });

    for (var objectId in pushedItems) {
      var object = pushedItems[objectId];

      Joi.assert(objectId, Joi.string().guid());
      Joi.assert(object, Joi.object().keys({ last_modified: Joi.number(), data: Joi.object().allow(null) }));

      // object exists
      var newDocument = {
        _id: [user.id, modelName, objectId].join(':'),
        user: user,
        model: {
          type: modelName,
          id: objectId,
        },
        data: object.data,
        last_modified: object.last_modified,
      };

      if (object.data === null) {
        newDocument._deleted = true; // mark as couchdb-deleted
      }

      if (oldDocLookup.hasOwnProperty(newDocument._id)) {
        var oldDoc = oldDocLookup[newDocument._id];
        if (oldDoc.last_modified < newDocument.last_modified) {
          newDocument._rev = oldDoc._rev;
        } else {
          continue; // skip this document, it's been updated in cloud in the meantime
        }
      }

      toSave.push(newDocument);
    }

    if (toSave.length > 0) {
      return this.couchdb.saveAll(toSave);
    }
  }.bind(this));

  // merge for response

  return when.all([
    responseItems,
    savePromise,
  ]).spread(function(requestedObjects, saveResponse) {
    var pushedObjects = {};

    if (saveResponse) {
      saveResponse.forEach(function(doc) {
        pushedObjects[doc.data.id] = (typeof doc._rev === 'string')
      });
    }

    return {
      model: modelName,
      requestedObjects: requestedObjects,
      pushedObjects: pushedObjects
    };
  });
};

module.exports.pushItemChange = function(user, modelName, pushedItem) {
  Joi.assert(user, UserSchema);
  Joi.assert(modelName, Joi.string().valid(VALID_MODELS));
  Joi.assert(pushedItem, Joi.object().keys({
    id: Joi.string().guid(),
    last_modified: Joi.number(),
    data: Joi.object().keys({
      id: Joi.string().guid()
    }).unknown(true).allow(null)
  }));

  var newDocument = {
    _id: [user.id, modelName, pushedItem.id].join(':'),
    user: user,
    model: {
      type: modelName,
      id: pushedItem.id,
    },
    data: pushedItem.data,
    last_modified: pushedItem.last_modified,
  };

  if (pushedItem.data === null) {
    newDocument._deleted = true; // mark as couchdb-deleted
  }

  return this.couchdb.get(newDocument._id).then(function(oldDoc) {
    debug('pushItemChange', 'oldDoc', oldDoc);
    if (oldDoc) {
      if (oldDoc.last_modified < newDocument.last_modified) {
        newDocument._rev = oldDoc._rev;
      } else {
        return; // timestamp mismatch
      }
    }

    return this.couchdb.save(newDocument);
  }.bind(this)).then(function(result) {
    var success = !!(result && result._rev);
    var pushedObjects = {};
    pushedObjects[newDocument.model.id] = success;
    return {
      model: modelName,
      pushedObjects: pushedObjects
    };
  });
};

function _makeManifest(result) {
  var manifest = {};

  result.rows.forEach(function(row) {
    manifest[row.value.id] = row.value.last_modified;
  });

  return manifest;
}

function _findGreaterItems(src, dst) {
  var greater = {};

  for (var k in src) {
    var sv = src[k];
    var dv = dst[k];

    if (!dv || dv < sv) {
      greater[k] = sv;
    }
  }

  return greater;
}<|endoftext|>


<commit_before>var async = require("async");
var AWS = require("aws-sdk");
var underscore = require("underscore");

var S3_API_VERION = "2006-03-01";

var _createS3 = function(accessKeyId, secretAccessKey, region) {
    var options = {
        "accessKeyId": accessKeyId,
        "secretAccessKey": secretAccessKey,
        "region": region,
        "apiVersion": S3_API_VERION,
    };

    var s3 = new AWS.S3(options);

    return s3;
};

var _listBucket = function(s3, bucket, prefix, items, callback) {
    var params = {
        "Bucket": bucket,
        "Prefix": prefix,
    };

    if (items.length > 0) {
        var lastKey = underscore.last(items).Key;
        params.Marker = lastKey;
    }

    s3.listObjects(params, function(err, data) {
        if (err) {
            console.error("Unable to list objects for bucket: " + bucket);
            callback(err);
        }
        else {
            items = items.concat(data.Contents);
            if (data.IsTruncated) {
                _listBucket(s3, bucket, prefix, items, callback);
            }
            else {
                var contents = underscore.indexBy(items, "Key");
                callback(null, contents);
            }
        }
    });
};

exports.listBucket = function(s3, bucket, prefix, callback) {
    _listBucket(s3, bucket, prefix, [], callback);
};

exports.synchronizeS3Buckets = function(sourceBucket, destinationBucket, options, callback) {

    var sourceAccessKeyId = options.sourceAccessKeyId || process.env.AWS_ACCESS_KEY_ID;
    var sourceSecretAccessKey = options.sourceSecretAccessKey || process.env.AWS_SECRET_ACCESS_KEY;
    var sourceRegion = options.sourceRegion || "us-east-1";
    var sourcePrefix = options.sourcePrefix || "";

    var destinationAccessKeyId = options.destinationAccessKeyId || sourceAccessKeyId;
    var destinationSecretAccessKey = options.destinationSecretAccessKey || sourceSecretAccessKey;
    var destinationRegion = options.destinationRegion || sourceRegion;
    var destinationPrefix = options.sourcePrefix || sourcePrefix;

    // TODO: validate

    var sourceS3 = _createS3(sourceAccessKeyId, sourceSecretAccessKey, sourceRegion);
    var destinationS3 = _createS3(destinationAccessKeyId, destinationSecretAccessKey, destinationRegion);

    var listSourceItems = function(next) {
        exports.listBucket(sourceS3, sourceBucket, sourcePrefix, next);
    };

    var listDestinationItems = function(next) {
        exports.listBucket(destinationS3, destinationBucket, destinationPrefix, next);
    };

    var uploadMissingItems = function(next, results) {
        var files = underscore.
        _uploadFiles(sourceS3, destinationS3, files, next);
    };

    async.auto({
        "sourceItems": listSourceItems,
        "destinationItems": listDestinationItems,
        "uploadMissingItems": ["sourceItems", "destinationItems", uploadMissingItems],
        // "deleteExtraItems": ["uploadMissingItems", deleteExtraItems],
        // "syncChangedItems": ["deleteExtraItems", overwriteChangedItems],
    }, callback);

    // Create source S3 object
    // Create destination S3 object
    // List keys in sourceBucket
    // List keys in destinationBucket
    // Compare keys
    // Download and upload missing keys
    // Overwrite non matching keys
    // Remove extra keys
};<commit_msg>Implement uploading missing files<commit_after>var async = require("async");
var AWS = require("aws-sdk");
var underscore = require("underscore");
var util = require("util");

var S3_API_VERION = "2006-03-01";
var CONCURRENT_UPLOADS = 1;

var _createS3 = function(accessKeyId, secretAccessKey, region) {
    var options = {
        "accessKeyId": accessKeyId,
        "secretAccessKey": secretAccessKey,
        "region": region,
        "apiVersion": S3_API_VERION,
    };

    var s3 = new AWS.S3(options);

    return s3;
};

var _uploadFiles = function(sourceS3, destinationS3, sourceBucket, destinationBucket, files, callback) {
    async.eachLimit(files, CONCURRENT_UPLOADS, function(file, next) {

        console.error("Uploading object '" + file.Key + "' (" + file.Size + " bytes) to bucket '" + destinationBucket + "'.");

        var sourceParams = {
            "Bucket": sourceBucket,
            "Key": file.Key
        };

        var sourceFileBody = sourceS3.getObject(sourceParams).createReadStream();

        var destinationParams = {
            "Bucket": destinationBucket,
            "Key": file.Key,
            "ACL": "public-read",
            "Body": sourceFileBody,
            "ServerSideEncryption": "AES256",
            "ContentLength": file.Size,
            "StorageClass": file.StorageClass,
        };

        destinationS3.putObject(destinationParams, function(err, data) {
            if (err) {
                console.error("Unable to put object " + file.Key + " to bucket " + destinationBucket);
                next(err);
                return void(0);
            }

            console.error("Uploaded object '" + file.Key + "' to bucket '" + destinationBucket + "'.");

            next();
        });

        // });

    }, callback);
};

var _listBucket = function(s3, bucket, prefix, items, callback) {
    var params = {
        "Bucket": bucket,
        "Prefix": prefix,
    };

    if (items.length > 0) {
        var lastKey = underscore.last(items).Key;
        params.Marker = lastKey;
    }

    s3.listObjects(params, function(err, data) {
        if (err) {
            console.error("Unable to list objects for bucket: " + bucket);
            callback(err);
        }
        else {
            items = items.concat(data.Contents);
            if (data.IsTruncated) {
                _listBucket(s3, bucket, prefix, items, callback);
            }
            else {
                var contents = underscore.indexBy(items, "Key");
                callback(null, contents);
            }
        }
    });
};

exports.listBucket = function(s3, bucket, prefix, callback) {
    _listBucket(s3, bucket, prefix, [], callback);
};

exports.synchronizeS3Buckets = function(sourceBucket, destinationBucket, options, callback) {

    var sourceAccessKeyId = options.sourceAccessKeyId || process.env.AWS_ACCESS_KEY_ID;
    var sourceSecretAccessKey = options.sourceSecretAccessKey || process.env.AWS_SECRET_ACCESS_KEY;
    var sourceRegion = options.sourceRegion || "us-east-1";
    var sourcePrefix = options.sourcePrefix || "";

    var destinationAccessKeyId = options.destinationAccessKeyId || sourceAccessKeyId;
    var destinationSecretAccessKey = options.destinationSecretAccessKey || sourceSecretAccessKey;
    var destinationRegion = options.destinationRegion || sourceRegion;
    var destinationPrefix = options.sourcePrefix || sourcePrefix;

    // TODO: validate

    var sourceS3 = _createS3(sourceAccessKeyId, sourceSecretAccessKey, sourceRegion);
    var destinationS3 = _createS3(destinationAccessKeyId, destinationSecretAccessKey, destinationRegion);

    var listSourceItems = function(next) {
        exports.listBucket(sourceS3, sourceBucket, sourcePrefix, next);
    };

    var listDestinationItems = function(next) {
        exports.listBucket(destinationS3, destinationBucket, destinationPrefix, next);
    };

    var uploadMissingItems = function(next, results) {
        var files = underscore.chain(results.sourceItems)
            .filter(function(value, key) {
                if (results.destinationItems[key]) {
                    return false;
                }
                else {
                    return true;
                }
            })
            .value();

        console.log("Uploading " + files.length + " new file(s)");

        _uploadFiles(sourceS3, destinationS3, sourceBucket, destinationBucket, files, next);
    };

    async.auto({
        "sourceItems": listSourceItems,
        "destinationItems": listDestinationItems,
        "uploadMissingItems": ["sourceItems", "destinationItems", uploadMissingItems],
        // "deleteExtraItems": ["uploadMissingItems", deleteExtraItems],
        // "syncChangedItems": ["deleteExtraItems", overwriteChangedItems],
    }, callback);

    // Create source S3 object
    // Create destination S3 object
    // List keys in sourceBucket
    // List keys in destinationBucket
    // Compare keys
    // Download and upload missing keys
    // Overwrite non matching keys
    // Remove extra keys
};<|endoftext|>


<commit_before>"use strict";

var Park = require("../park");
var Moment = require("moment-timezone");

var s_parkID = Symbol();
var s_authToken = Symbol();
var s_apiBase = Symbol();
var s_rideTypes = Symbol();

/**
 * Implements the Seaworld API framework.
 * @class
 * @extends Park
 */
class SeaworldPark extends Park {
    /**
     * Create new SeaworldPark Object.
     * This object should not be called directly, but rather extended for each of the individual SeaWorld parks
     * @param {Object} options
     * @param {String} options.park_id ID of the park to access the API for
     * @param {String} [options.auth_token] Auth token to use to connect to the API
     * @param {String} [options.api_base] Base URL to access the API
     * @param {String[]} [options.ride_types] Array of types that denote rides at the park (to avoid listing restaurants/toilets etc. as rides)
     */
    constructor(options = {}) {
        options.name = options.name || "SeaWorld Park";

        // inherit from base class
        super(options);

        // assign park configurations
        if (!options.park_id) throw new Error("Missing park's API ID");
        this[s_parkID] = options.park_id;

        // accept API options to override defaults if needed
        this[s_authToken] = options.auth_token || "***REMOVED***";
        this[s_apiBase] = options.api_base || "https://seas.te2.biz/v1/rest/";

        // array of valid ride types. Some implementations of the API use various types to declare rides (eg. Family/Kid/Thrill etc.)
        this[s_rideTypes] = options.ride_types || ["Ride"];
    }

    FetchWaitTimes() {
        return new Promise(function(resolve, reject) {
            // first make sure we have our ride names
            this.GetRideNames().then(function(rideNames) {
                this.GetAPIUrl({
                    url: `${this[s_apiBase]}venue/${this[s_parkID]}/poi/all/status`
                }).then(function(waitTimeData) {
                    for (var i = 0, ride; ride = waitTimeData[i++];) {
                        // find/create this ride object (only if we have a name for it)
                        if (rideNames[ride.id]) {
                            var rideObject = this.GetRideObject({
                                id: ride.id,
                                name: rideNames[ride.id]
                            });

                            if (rideObject) {
                                // update ride wait time
                                rideObject.WaitTime = ride.status && ride.status.waitTime ? ride.status.waitTime : -1;
                            }
                        }
                    }

                    resolve();
                }.bind(this), reject);
            }.bind(this), reject);
        }.bind(this));
    }

    FetchOpeningTimes() {
        return new Promise(function(resolve, reject) {
            this.GetAPIUrl({
                url: `${this[s_apiBase]}venue/${this[s_parkID]}/hours/${Moment().tz(this.Timezone).format("YYYY-MM-DD")}`,
                data: {
                    days: 30
                }
            }).then(function(scheduleData) {
                for (var i = 0, day; day = scheduleData[i++];) {
                    var thisDay = Moment(day.date, "YYYY-MM-DD");
                    this.Schedule.SetDate({
                        date: thisDay,
                        openingTime: day.open ? Moment(day.open, "YYYY-MM-DDTHH:mm:ss.SSSZZ").tz(this.Timezone) : thisDay,
                        closingTime: day.close ? Moment(day.close, "YYYY-MM-DDTHH:mm:ss.SSSZZ").tz(this.Timezone) : thisDay,
                        type: day.isOpen ? "Operating" : "Closed",
                    });
                }

                resolve();
            }.bind(this), reject);
        }.bind(this));
    }

    /**
     * Get cached (or fresh fetch) of ride names
     * @returns {Promise<Object>} Object of RideID => Ride name in English
     */
    GetRideNames() {
        return new Promise(function(resolve, reject) {
            // wrap cache request (cache ride names for 24 hours)
            this.Cache.Wrap("ridenames", this.FetchRideNames.bind(this), 60 * 60 * 24).then(resolve, reject);
        }.bind(this));
    }

    /**
     * Fetch all the rides and ride names for this park from the API (skip the cache)
     * @returns {Promise<Object>} Object of RideID => Ride name in English
     */
    FetchRideNames() {
        return new Promise(function(resolve, reject) {
            this.Log(`Fetching ride names for ${this.Name}`);

            // fetch POI (points-of-interest) data from API
            this.GetAPIUrl({
                url: `${this[s_apiBase]}venue/${this[s_parkID]}/poi/all`
            }).then(function(rideData) {
                if (!rideData) return reject("No POI data returned from Seaworld API");

                var rideNames = {};
                for (var i = 0, poi; poi = rideData[i++];) {
                    // only include POI data for rides
                    if (this[s_rideTypes].indexOf(poi.type) >= 0) {
                        // grab "label", which is the English title for each POI
                        rideNames[poi.id] = poi.label;
                    }
                }

                resolve(rideNames);
            }.bind(this), reject);
        }.bind(this));
    }

    GetAPIUrl(requestObject) {
        return new Promise(function(resolve, reject) {
            // make sure headers exist if they weren't set already
            if (!requestObject.headers) requestObject.headers = [];
            requestObject.headers.Authorization = "Basic " + new Buffer(this[s_authToken]).toString("base64");

            // make sure we get JSON back
            requestObject.forceJSON = true;

            // send network request
            this.HTTP(requestObject).then(resolve, reject);
        }.bind(this));
    }

    /**
     * Get the park ID used by the SeaWorld/Cedar Fair API
     * @type {String}
     * */
    get ParkID() {
        return this[s_parkID];
    }

    /**
     * Get this park's API Base URL
     * @type {String}
     * */
    get APIBase() {
        return this[s_apiBase];
    }
}

module.exports = SeaworldPark;<commit_msg>Check the isOpen field for rides that just have no wait time<commit_after>"use strict";

var Park = require("../park");
var Moment = require("moment-timezone");

var s_parkID = Symbol();
var s_authToken = Symbol();
var s_apiBase = Symbol();
var s_rideTypes = Symbol();

/**
 * Implements the Seaworld API framework.
 * @class
 * @extends Park
 */
class SeaworldPark extends Park {
    /**
     * Create new SeaworldPark Object.
     * This object should not be called directly, but rather extended for each of the individual SeaWorld parks
     * @param {Object} options
     * @param {String} options.park_id ID of the park to access the API for
     * @param {String} [options.auth_token] Auth token to use to connect to the API
     * @param {String} [options.api_base] Base URL to access the API
     * @param {String[]} [options.ride_types] Array of types that denote rides at the park (to avoid listing restaurants/toilets etc. as rides)
     */
    constructor(options = {}) {
        options.name = options.name || "SeaWorld Park";

        // inherit from base class
        super(options);

        // assign park configurations
        if (!options.park_id) throw new Error("Missing park's API ID");
        this[s_parkID] = options.park_id;

        // accept API options to override defaults if needed
        this[s_authToken] = options.auth_token || "***REMOVED***";
        this[s_apiBase] = options.api_base || "https://seas.te2.biz/v1/rest/";

        // array of valid ride types. Some implementations of the API use various types to declare rides (eg. Family/Kid/Thrill etc.)
        this[s_rideTypes] = options.ride_types || ["Ride"];
    }

    FetchWaitTimes() {
        return new Promise(function(resolve, reject) {
            // first make sure we have our ride names
            this.GetRideNames().then(function(rideNames) {
                this.GetAPIUrl({
                    url: `${this[s_apiBase]}venue/${this[s_parkID]}/poi/all/status`
                }).then(function(waitTimeData) {
                    for (var i = 0, ride; ride = waitTimeData[i++];) {
                        // find/create this ride object (only if we have a name for it)
                        if (rideNames[ride.id]) {
                            var rideObject = this.GetRideObject({
                                id: ride.id,
                                name: rideNames[ride.id]
                            });

                            if (rideObject && ride.status) {
                                // update ride wait time
                                rideObject.WaitTime = ride.status.waitTime ? ride.status.waitTime : (ride.status.isOpen ? 0 : -1);
                            }
                        }
                    }

                    resolve();
                }.bind(this), reject);
            }.bind(this), reject);
        }.bind(this));
    }

    FetchOpeningTimes() {
        return new Promise(function(resolve, reject) {
            this.GetAPIUrl({
                url: `${this[s_apiBase]}venue/${this[s_parkID]}/hours/${Moment().tz(this.Timezone).format("YYYY-MM-DD")}`,
                data: {
                    days: 30
                }
            }).then(function(scheduleData) {
                for (var i = 0, day; day = scheduleData[i++];) {
                    var thisDay = Moment(day.date, "YYYY-MM-DD");
                    this.Schedule.SetDate({
                        date: thisDay,
                        openingTime: day.open ? Moment(day.open, "YYYY-MM-DDTHH:mm:ss.SSSZZ").tz(this.Timezone) : thisDay,
                        closingTime: day.close ? Moment(day.close, "YYYY-MM-DDTHH:mm:ss.SSSZZ").tz(this.Timezone) : thisDay,
                        type: day.isOpen ? "Operating" : "Closed",
                    });
                }

                resolve();
            }.bind(this), reject);
        }.bind(this));
    }

    /**
     * Get cached (or fresh fetch) of ride names
     * @returns {Promise<Object>} Object of RideID => Ride name in English
     */
    GetRideNames() {
        return new Promise(function(resolve, reject) {
            // wrap cache request (cache ride names for 24 hours)
            this.Cache.Wrap("ridenames", this.FetchRideNames.bind(this), 60 * 60 * 24).then(resolve, reject);
        }.bind(this));
    }

    /**
     * Fetch all the rides and ride names for this park from the API (skip the cache)
     * @returns {Promise<Object>} Object of RideID => Ride name in English
     */
    FetchRideNames() {
        return new Promise(function(resolve, reject) {
            this.Log(`Fetching ride names for ${this.Name}`);

            // fetch POI (points-of-interest) data from API
            this.GetAPIUrl({
                url: `${this[s_apiBase]}venue/${this[s_parkID]}/poi/all`
            }).then(function(rideData) {
                if (!rideData) return reject("No POI data returned from Seaworld API");

                var rideNames = {};
                for (var i = 0, poi; poi = rideData[i++];) {
                    // only include POI data for rides
                    if (this[s_rideTypes].indexOf(poi.type) >= 0) {
                        // grab "label", which is the English title for each POI
                        rideNames[poi.id] = poi.label;
                    }
                }

                resolve(rideNames);
            }.bind(this), reject);
        }.bind(this));
    }

    GetAPIUrl(requestObject) {
        return new Promise(function(resolve, reject) {
            // make sure headers exist if they weren't set already
            if (!requestObject.headers) requestObject.headers = [];
            requestObject.headers.Authorization = "Basic " + new Buffer(this[s_authToken]).toString("base64");

            // make sure we get JSON back
            requestObject.forceJSON = true;

            // send network request
            this.HTTP(requestObject).then(resolve, reject);
        }.bind(this));
    }

    /**
     * Get the park ID used by the SeaWorld/Cedar Fair API
     * @type {String}
     * */
    get ParkID() {
        return this[s_parkID];
    }

    /**
     * Get this park's API Base URL
     * @type {String}
     * */
    get APIBase() {
        return this[s_apiBase];
    }
}

module.exports = SeaworldPark;<|endoftext|>


<commit_before>var IMPLEMENTS = 'security';
var INJECT = ['logger', 'eventBus'];

function factory(services) {
  var eventBus = services.eventBus;
  var isSecure = false;

  var result = {
    secure: setSecure.bind(null, true),
    unsecure: setSecure.bind(null, false)
  };

  Object.defineProperty(result, 'isSecure' , function() {
    return isSecure;
  });

  function setSecure(secure) {
    isSecure = secure;
    eventBus.emit('security', 'changed', secure);
  }
}

module.exports = exports = factory;
exports.$implements = IMPLEMENTS;
exports.$inject = INJECT;<commit_msg>add security module<commit_after>var IMPLEMENTS = 'security';
var INJECT = ['logger', 'eventBus', 'presence'];

function factory(services) {
  var eventBus = services.eventBus;
  var isSecure = false;

  services.presence.on('person:any', function(present) {
    setSecure(!present);
  });

  var result = {
    secure: setSecure.bind(null, true),
    unsecure: setSecure.bind(null, false)
  };

  Object.defineProperty(result, 'isSecure' , function() {
    return isSecure;
  });

  function setSecure(secure) {
    isSecure = secure;
    eventBus.emit('security', 'changed', secure);
  }
}

module.exports = exports = factory;
exports.$implements = IMPLEMENTS;
exports.$inject = INJECT;<|endoftext|>


<commit_before>'use strict';

let
  debug = require('debug')('kuzzle-proxy:broker'),
  fs = require('fs'),
  http = require('http'),
  net = require('net'),
  WebSocketServer = require('ws').Server,
  Backend = require('./Backend'),
  async = require('async'),
  RequestContext = require('kuzzle-common-objects').models.RequestContext,
  ServiceUnavailableError = require('kuzzle-common-objects').errors.ServiceUnavailableError;

let
  _proxy;

/**
 * Broker constructor
 *
 * @constructor
 */
function Broker () {
  this.config = null;
  this.socketOptions = null;
  this.backendHandler = null;
}

/**
 * Initializes the Broker
 *
 * @param {KuzzleProxy} proxy
 * @param {Object} config
 */
Broker.prototype.init = function (proxy, config) {
  _proxy = proxy;

  debug('initializing proxy broker with config:\n%O', config);

  this.config = config;

  this.initiateServer();
};

/**
 * Initializes the server socket
 */
Broker.prototype.initiateServer = function () {
  let
    broker,
    server = http.createServer(),
    initCB = () => {
      broker = new WebSocketServer({ server }, {perMessageDeflate: false});

      broker.on('connection', this.onConnection.bind(this));
      broker.on('error', this.onError.bind(this));
    };

  if (this.config.socket) {
    server.on('error', error => {
      if (error.code !== 'EADDRINUSE') {
        debug('broker server received an error:\n%O', error);

        throw error;
      }

      net.connect(this.config.socket, () => {
        // really in use, re-throw
        throw error;
      })
        .on('error', e => {
          if (e.code !== 'ECONNREFUSED') {
            debug('broker server can\'t open requested socket, seem to be already used by another process');

            throw e;
          }

          debug('broker server can\'t open requested socket, seem to be unused, trying to recreate it');

          fs.unlinkSync(this.config.socket);
          if (broker) {
            broker.close(() => {
              server.listen(this.config.socket);
            });
          } else {
            server.listen(this.config.socket);
          }
        });
    });

    debug('initialize broker server through socket "%s"', this.config.socket);

    server.listen(this.config.socket, initCB);
  }
  else if (this.config.port) {
    if (this.config.host) {
      debug('initialize broker server through net port "%s" on host "%s"', this.config.port, this.config.host);

      server.listen(this.config.port, this.config.host, initCB);
    }
    else {
      debug('initialize broker server through net port "%s"', this.config.port);

      server.listen(this.config.port, initCB);
    }
  }
  else {
    _proxy.log.error('Invalid configuration provided. Either "socket" or "port" must be provided.');
    process.exit(1);
  }
};

/**
 * Triggered when a connection is made on the brokerSocket
 *
 * @param {WebSocket} backendSocket
 */
Broker.prototype.onConnection = function (backendSocket) {
  let
    backend = new Backend(backendSocket, _proxy, this.config.timeout),
    clientConnections = _proxy.clientConnectionStore.getAll();

  debug('[%s] broker server received a connection', backendSocket.upgradeReq && backendSocket.upgradeReq.headers ? backendSocket.upgradeReq.headers['sec-websocket-key'] : 'unknown identifier');

  if (this.config.socket) {
    _proxy.log.info('Connection established with a new backend');
  }
  else {
    _proxy.log.info(`A connection has been established with backend ${backendSocket.upgradeReq.connection.remoteAddress}.`);
  }

  async.each(
    clientConnections,
    (clientConnection, callback) => backend.sendRaw('connection', clientConnection, callback),
    error => this.handleBackendRegistration(error, backend)
  );
};

/**
 * Handles registration of backend, post-pone registration if http port is not received yet
 *
 * @param {Backend} backend
 * @param {Error|undefined} error
 */
Broker.prototype.handleBackendRegistration = function (error, backend) {
  debug('broker server handle backend registration of backend:\n%O', backend);

  if (error) {
    // We presume the error comes from the socket connection and close it
    debug('broker server handle backend registration error:\n%O', error);

    let backendIdentifier;

    if (this.config.socket) {
      backendIdentifier = this.config.socket;
    }
    else {
      if (this.config.host) {
        backendIdentifier = this.config.host + ':';
      }
      else {
        backendIdentifier = '0.0.0.0:';
      }
      backendIdentifier += this.config.port;
    }

    _proxy.log.error('Failed to init connection with backend %s:\n%s', backendIdentifier, error.stack);
    backend.socket.close();
  }
  else {
    _proxy.backendHandler.addBackend(backend);
  }
};

/**
 * Triggered when the brokerSocket is in error
 *
 * @param {Error} error
 */
Broker.prototype.onError = function (error) {
  _proxy.log.error('An error occurred with the broker socket, shutting down; Reason:\n%s', error.stack);
  process.exit(1);
};

/**
 * Sends the client's message to one of the backends
 *
 * @param {string} room - target Kuzzle server room
 * @param {string} id - request id
 * @param {string} connectionId - connection id
 * @param {Object} data - request data to deliver to Kuzzle
 * @param {Function} callback - Client's callback to resolve
 */
Broker.prototype.brokerCallback = function (room, id, connectionId, data, callback) {
  const backend = _proxy.backendHandler.getBackend();

  if (!backend) {
    let error = new ServiceUnavailableError('No Kuzzle instance found');
    delete error.stack;

    callback(error);
  }
  else {
    backend.send(room, id, data, (error, result) => {
      _proxy.logAccess(connectionId, data, error, result);
      callback(error, result);
    });
  }
};

/**
 * Adds a client connection and spreads the word to all backends
 *
 * @param {ClientConnection} connection
 */
Broker.prototype.addClientConnection = function (connection) {
  debug('broker server add client connection\n%O', connection);

  const context = new RequestContext({
    connectionId: connection.id,
    protocol: connection.protocol
  });
  this.broadcastMessage('connection', context);
  _proxy.clientConnectionStore.add(connection);
};

/**
 * Removes a client connection and spreads the word to all backends
 *
 * @param {ClientConnection} connection
 */
Broker.prototype.removeClientConnection = function (connection) {
  debug('broker server remove client connection\n%O', connection);

  const context = new RequestContext({
    connectionId: connection.id,
    protocol: connection.protocol
  });
  this.broadcastMessage('disconnect', context);
  _proxy.clientConnectionStore.remove(connection.id);
};

/**
 * Broadcasts the message to all connected backends
 *
 * @param {string} room
 * @param {object} data
 */
Broker.prototype.broadcastMessage = function (room, data) {
  debug('broker server broadcasting message on room "%s":\n%O', room, data);

  _proxy.backendHandler.getAllBackends().forEach(backend => backend.sendRaw(room, data));
};

module.exports = Broker;
<commit_msg>remove stack only if NODE_ENV is not development<commit_after>'use strict';

let
  debug = require('debug')('kuzzle-proxy:broker'),
  fs = require('fs'),
  http = require('http'),
  net = require('net'),
  WebSocketServer = require('ws').Server,
  Backend = require('./Backend'),
  async = require('async'),
  RequestContext = require('kuzzle-common-objects').models.RequestContext,
  ServiceUnavailableError = require('kuzzle-common-objects').errors.ServiceUnavailableError;

let
  _proxy;

/**
 * Broker constructor
 *
 * @constructor
 */
function Broker () {
  this.config = null;
  this.socketOptions = null;
  this.backendHandler = null;
}

/**
 * Initializes the Broker
 *
 * @param {KuzzleProxy} proxy
 * @param {Object} config
 */
Broker.prototype.init = function (proxy, config) {
  _proxy = proxy;

  debug('initializing proxy broker with config:\n%O', config);

  this.config = config;

  this.initiateServer();
};

/**
 * Initializes the server socket
 */
Broker.prototype.initiateServer = function () {
  let
    broker,
    server = http.createServer(),
    initCB = () => {
      broker = new WebSocketServer({ server }, {perMessageDeflate: false});

      broker.on('connection', this.onConnection.bind(this));
      broker.on('error', this.onError.bind(this));
    };

  if (this.config.socket) {
    server.on('error', error => {
      if (error.code !== 'EADDRINUSE') {
        debug('broker server received an error:\n%O', error);

        throw error;
      }

      net.connect(this.config.socket, () => {
        // really in use, re-throw
        throw error;
      })
        .on('error', e => {
          if (e.code !== 'ECONNREFUSED') {
            debug('broker server can\'t open requested socket, seem to be already used by another process');

            throw e;
          }

          debug('broker server can\'t open requested socket, seem to be unused, trying to recreate it');

          fs.unlinkSync(this.config.socket);
          if (broker) {
            broker.close(() => {
              server.listen(this.config.socket);
            });
          } else {
            server.listen(this.config.socket);
          }
        });
    });

    debug('initialize broker server through socket "%s"', this.config.socket);

    server.listen(this.config.socket, initCB);
  }
  else if (this.config.port) {
    if (this.config.host) {
      debug('initialize broker server through net port "%s" on host "%s"', this.config.port, this.config.host);

      server.listen(this.config.port, this.config.host, initCB);
    }
    else {
      debug('initialize broker server through net port "%s"', this.config.port);

      server.listen(this.config.port, initCB);
    }
  }
  else {
    _proxy.log.error('Invalid configuration provided. Either "socket" or "port" must be provided.');
    process.exit(1);
  }
};

/**
 * Triggered when a connection is made on the brokerSocket
 *
 * @param {WebSocket} backendSocket
 */
Broker.prototype.onConnection = function (backendSocket) {
  let
    backend = new Backend(backendSocket, _proxy, this.config.timeout),
    clientConnections = _proxy.clientConnectionStore.getAll();

  debug('[%s] broker server received a connection', backendSocket.upgradeReq && backendSocket.upgradeReq.headers ? backendSocket.upgradeReq.headers['sec-websocket-key'] : 'unknown identifier');

  if (this.config.socket) {
    _proxy.log.info('Connection established with a new backend');
  }
  else {
    _proxy.log.info(`A connection has been established with backend ${backendSocket.upgradeReq.connection.remoteAddress}.`);
  }

  async.each(
    clientConnections,
    (clientConnection, callback) => backend.sendRaw('connection', clientConnection, callback),
    error => this.handleBackendRegistration(error, backend)
  );
};

/**
 * Handles registration of backend, post-pone registration if http port is not received yet
 *
 * @param {Backend} backend
 * @param {Error|undefined} error
 */
Broker.prototype.handleBackendRegistration = function (error, backend) {
  debug('broker server handle backend registration of backend:\n%O', backend);

  if (error) {
    // We presume the error comes from the socket connection and close it
    debug('broker server handle backend registration error:\n%O', error);

    let backendIdentifier;

    if (this.config.socket) {
      backendIdentifier = this.config.socket;
    }
    else {
      if (this.config.host) {
        backendIdentifier = this.config.host + ':';
      }
      else {
        backendIdentifier = '0.0.0.0:';
      }
      backendIdentifier += this.config.port;
    }

    _proxy.log.error('Failed to init connection with backend %s:\n%s', backendIdentifier, error.stack);
    backend.socket.close();
  }
  else {
    _proxy.backendHandler.addBackend(backend);
  }
};

/**
 * Triggered when the brokerSocket is in error
 *
 * @param {Error} error
 */
Broker.prototype.onError = function (error) {
  _proxy.log.error('An error occurred with the broker socket, shutting down; Reason:\n%s', error.stack);
  process.exit(1);
};

/**
 * Sends the client's message to one of the backends
 *
 * @param {string} room - target Kuzzle server room
 * @param {string} id - request id
 * @param {string} connectionId - connection id
 * @param {Object} data - request data to deliver to Kuzzle
 * @param {Function} callback - Client's callback to resolve
 */
Broker.prototype.brokerCallback = function (room, id, connectionId, data, callback) {
  const backend = _proxy.backendHandler.getBackend();

  if (!backend) {
    let error = new ServiceUnavailableError('No Kuzzle instance found');

    if (process.env.NODE_ENV !== 'development') {
      delete error.stack;
    }

    callback(error);
  }
  else {
    backend.send(room, id, data, (error, result) => {
      _proxy.logAccess(connectionId, data, error, result);
      callback(error, result);
    });
  }
};

/**
 * Adds a client connection and spreads the word to all backends
 *
 * @param {ClientConnection} connection
 */
Broker.prototype.addClientConnection = function (connection) {
  debug('broker server add client connection\n%O', connection);

  const context = new RequestContext({
    connectionId: connection.id,
    protocol: connection.protocol
  });
  this.broadcastMessage('connection', context);
  _proxy.clientConnectionStore.add(connection);
};

/**
 * Removes a client connection and spreads the word to all backends
 *
 * @param {ClientConnection} connection
 */
Broker.prototype.removeClientConnection = function (connection) {
  debug('broker server remove client connection\n%O', connection);

  const context = new RequestContext({
    connectionId: connection.id,
    protocol: connection.protocol
  });
  this.broadcastMessage('disconnect', context);
  _proxy.clientConnectionStore.remove(connection.id);
};

/**
 * Broadcasts the message to all connected backends
 *
 * @param {string} room
 * @param {object} data
 */
Broker.prototype.broadcastMessage = function (room, data) {
  debug('broker server broadcasting message on room "%s":\n%O', room, data);

  _proxy.backendHandler.getAllBackends().forEach(backend => backend.sendRaw(room, data));
};

module.exports = Broker;
<|endoftext|>