<commit_before>const {INIT, OPEN_TAG, OPEN_COMMENT, COMMENT, CLOSE_TAG, END_TAG, TAGNAME, TAG, EXPR, TEXT, ATTR, EQUAL, VALUE, EOF} = require('./StateTypes');
//
function isSpace(letter) {
    const code = letter.charCodeAt(0);
    return code == 32;
}

// a-z
function isLowerCase(letter) {
    const code = letter.charCodeAt(0);
    return code >= 97 && code <= 122;
}

// A-Z
function isUpperCase(letter) {
    const code = letter.charCodeAt(0);
    return code >= 65 && code <= 90;
}

function isWord(letter) {
    return isLowerCase(letter) || isUpperCase(letter);
}

// 0-9
function isNumber(letter) {
    const code = letter.charCodeAt(0);
    return code >= 48 && code <= 57;
}

// _
function isUnderscore(letter) {
    const code = letter.charCodeAt(0);
    return code === 95;
}

// $
function isDollar(letter) {
    const code = letter.charCodeAt(0);
    return code === 36;
}

// !
function isExclamationMark (letter) {
    const code = letter.charCodeAt(0);
    return code === 33;
}

// !
function isDash (letter) {
    const code = letter.charCodeAt(0);
    return code === 45;
}

// <
function isOpenTag(letter) {
    const code = letter.charCodeAt(0);
    return code === 60;
}

// >
function isCloseTag(letter) {
    const code = letter.charCodeAt(0);
    return code === 62;
}

// {
function isOpenExpr(letter) {
    const code = letter.charCodeAt(0);
    return code === 123;
}

// }
function isCloseExpr(letter) {
    const code = letter.charCodeAt(0);
    return code === 125;
}

// /
function isSlash(letter) {
    const code = letter.charCodeAt(0);
    return code === 47;
}

// =
function isEqual(letter) {
    const code = letter.charCodeAt(0);
    return code === 61;
}

// '||"
function isQuote(letter) {
    const code = letter.charCodeAt(0);
    return code === 34 || code === 39;
}

function mergeTextTokens(tokens) {
    let index = 0;
    return tokens.reduce((list, token) => {
        const prevToken = list[index - 1];

        if ((token.type === TEXT) && prevToken && (prevToken.type === TEXT)) {
            prevToken.content += token.content;
        } else {
            list.push(token);
            index++;
        }
        return list;
    }, []);
}

function Lexer(source) {
    let states = [];
    let pos = 0;
    let tokens = [];
    let temp = [];
    let letter;

    pushState(INIT);

    /* eslint-disable */
    while (letter = source[pos]) {
    /* eslint-enable */
        const state = getState();
        if (state === INIT) {
            if (isOpenTag(letter)) {
                if (isSlash(getNextLetter())) {
                    if (isSpace(getNextLetter(2))) {
                        pushToTemp(letter);
                        next();
                        pushState(TEXT);
                    } else {
                        pushState(END_TAG);
                        next(2);
                    }
                } else if (isExclamationMark(getNextLetter())
                    && isDash(getNextLetter(2))
                    && isDash(getNextLetter(3))
                ) {
                    next(4);
                    pushState(OPEN_COMMENT);
                } else {
                    if (isSpace(getNextLetter())) {
                        pushToTemp(letter);
                        next();
                        pushState(TEXT);
                    } else {
                        pushState(OPEN_TAG);
                        next();
                    }
                }
            } else if (isOpenExpr(letter)) {
                pushState(EXPR);
                next();
            } else {
                pushState(TEXT);
            }
            resetTemp();
        } else if (state === OPEN_TAG) {
            if (isSpace(letter)) {
                next();
            } else {
                pushState(TAGNAME);
                resetTemp();
            }
        } else if (state === TAGNAME) {
            if (isSpace(letter)) {
                pushToken(TAGNAME);
                pushState(TAG);
                resetTemp();
            } else if ((isSlash(letter) && isCloseTag(getNextLetter()))) {
                setTokenSelfClose(pushToken(TAGNAME));
                next();
                pushState(TAG);
                resetTemp();
            } else if (isCloseTag(letter)) {
                pushToken(TAGNAME);
                pushState(TAG);
            } else {
                next();
                pushToTemp(letter);
            }
        } else if (state === TAG) {
            if (isCloseTag(letter)) {
                pushState(CLOSE_TAG);
            } else if (isSpace(letter)) {
                next();
            } else {
                pushState(ATTR);
                resetTemp();
            }
        } else if (state === ATTR) {
            if (isEqual(letter)) {
                pushToken(ATTR);
                next();
                pushState(EQUAL);
                resetTemp();
            } else if (isSpace(letter)) {
                next();
                pushToken(ATTR);
                resetTemp();
            } else if (isCloseTag(letter)) {
                pushToken(ATTR);
                pushState(TAG);
            } else {
                next();
                pushToTemp(letter);
            }
        } else if (state === EQUAL) {
            if (isQuote(letter)) {
                pushToTemp(letter);
                next();
                pushState(VALUE);
            } else if (isSpace(letter)) {
                next();
            } else if (isCloseTag(letter)) {
                pushState(TAG);
            } else {
                pushState(VALUE);
            }
        } else if (state === CLOSE_TAG) {
            popState(CLOSE_TAG);

            if (isAttrState()) {
                while (isAttrState()) {
                    popState();
                }
            }

            popState(TAGNAME);
            popState(OPEN_TAG);

            next();
            resetTemp();
        } else if (state === VALUE) {
            if (
                isQuote(letter) && letter === temp[0] && getPrevLetter() !== '//'
            ) {
                pushToTemp(letter);
                pushToken(VALUE);
                next();
                pushState(TAG);
            }  else if (isSpace(letter)) {
                if (isQuote(temp[0])) {
                    next();
                    pushToTemp(letter);
                } else {
                    pushToken(VALUE);
                    next();
                    pushState(TAG);
                }
            } else if (isCloseTag(letter)) {
                pushToken(VALUE);
                pushState(TAG);
            } else {
                next();
                pushToTemp(letter);
            }
        } else if (state === END_TAG) {
            if (isSpace(letter)) {
                next();
            } else if (isCloseTag(letter)) {
                pushToken(END_TAG);
                popState(END_TAG);
                next();
            } else {
                pushToTemp(letter);
                next();
            }
        } else if (state === EXPR) {
            if (isOpenExpr(letter)) {
                pushToTemp(letter);
                pushToken(TEXT);
                resetTemp();
                next();
            } else if (isCloseExpr(letter)) {
                pushToken(EXPR);
                next();
                popState(EXPR);
            } else {
                pushToTemp(letter);
                next();
            }
        } else if (state === TEXT) {
            if (isOpenTag(letter) || isOpenExpr(letter)) {
                pushToken(TEXT);
                popState();
            } else {
                pushToTemp(letter);
                next();
            }
        } else if (state === OPEN_COMMENT) {
            if (isDash(letter) && isDash(getNextLetter()) && isCloseTag(getNextLetter(2))) {
                pushToken(COMMENT);
                next(3);
                popState();
            } else {
                next();
                pushToTemp(letter);
            }
        }
    }

    let rollBackPos = -1;
    while (states.length !== 1){
        switch (getState()) {
        case OPEN_COMMENT:
            pushToken(COMMENT);
            popState();
            break;
        case TEXT:
            pushToken(TEXT);
            popState();
            break;
        case TAG:
        case TAGNAME:
        case OPEN_TAG:
            rollBackPos = states[states.length - 1].pos;
            popState();
            break;
        case EXPR:
            rollBackPos = states[states.length - 1].pos;
            popState();
            break;
        }
    }

    if (states.length !== 1) {
        throw `unparsable codes: ${getContentFromTemp()}`;
    }

    resetTemp();
    pushToTemp(source.substr(rollBackPos));
    pushToken(TEXT);

    tokens = mergeTextTokens(tokens);

    tokens = [...tokens, {
        type: EOF
    }];

    return tokens;

    function next(i = 1) {
        pos += i;
    }

    function pushState(nextState) {
        states.push({
            state: nextState,
            pos
        });
    }

    function pushToken(type) {
        const sourceFragment = source.substr(0, pos);
        const matchLines = sourceFragment.match(/\n/g);
        const matchColumns = sourceFragment.match(/[^\n]*$/g);
        const line = (matchLines && matchLines.length) + 1;
        const column = (matchColumns && matchColumns.length);

        const token = {
            type,
            content: getContentFromTemp(),
            pos: pos,
            line,
            column
        };

        tokens.push(token);

        return token;
    }

    function pushToTemp(item) {
        temp.push(item);
    }

    function resetTemp() {
        temp.length = 0;
    }

    function getContentFromTemp() {
        return temp.join('');
    }

    function getPrevLetter(i = 1) {
        return source[pos - i];
    }

    function getNextLetter(i = 1) {
        return source[pos + i];
    }

    function setTokenSelfClose(token) {
        token.isSelfClose = true;
    }

    function getState() {
        return (states[states.length - 1] || {}).state;
    }

    function popState(expectState) {
        if (!expectState) {
            return states.pop();
        }

        if (getState() === expectState) {
            return states.pop();
        }
        throw `Check your syntax - expect state ${expectState}, but actual state is ${getState()}`;
    }

    function isAttrState() {
        return ~[TAG, ATTR, EQUAL, VALUE].indexOf(getState());
    }
}

exports.Lexer = Lexer;<commit_msg>chore: uppercase<commit_after>const {INIT, OPEN_TAG, OPEN_COMMENT, COMMENT, CLOSE_TAG, END_TAG, TAGNAME, TAG, EXPR, TEXT, ATTR, EQUAL, VALUE, EOF} = require('./StateTypes');
//
function isSpace(letter) {
    const code = letter.charCodeAt(0);
    return code == 32;
}

// a-z
function isLowerCase(letter) {
    const code = letter.charCodeAt(0);
    return code >= 97 && code <= 122;
}

// A-Z
function isUpperCase(letter) {
    const code = letter.charCodeAt(0);
    return code >= 65 && code <= 90;
}

function isWord(letter) {
    return isLowerCase(letter) || isUpperCase(letter);
}

// 0-9
function isNumber(letter) {
    const code = letter.charCodeAt(0);
    return code >= 48 && code <= 57;
}

// _
function isUnderscore(letter) {
    const code = letter.charCodeAt(0);
    return code === 95;
}

// $
function isDollar(letter) {
    const code = letter.charCodeAt(0);
    return code === 36;
}

// !
function isExclamationMark (letter) {
    const code = letter.charCodeAt(0);
    return code === 33;
}

// !
function isDash (letter) {
    const code = letter.charCodeAt(0);
    return code === 45;
}

// <
function isOpenTag(letter) {
    const code = letter.charCodeAt(0);
    return code === 60;
}

// >
function isCloseTag(letter) {
    const code = letter.charCodeAt(0);
    return code === 62;
}

// {
function isOpenExpr(letter) {
    const code = letter.charCodeAt(0);
    return code === 123;
}

// }
function isCloseExpr(letter) {
    const code = letter.charCodeAt(0);
    return code === 125;
}

// /
function isSlash(letter) {
    const code = letter.charCodeAt(0);
    return code === 47;
}

// =
function isEqual(letter) {
    const code = letter.charCodeAt(0);
    return code === 61;
}

// '||"
function isQuote(letter) {
    const code = letter.charCodeAt(0);
    return code === 34 || code === 39;
}

function mergeTextTokens(tokens) {
    let index = 0;
    return tokens.reduce((list, token) => {
        const prevToken = list[index - 1];

        if ((token.type === TEXT) && prevToken && (prevToken.type === TEXT)) {
            prevToken.content += token.content;
        } else {
            list.push(token);
            index++;
        }
        return list;
    }, []);
}

function Lexer(source) {
    let states = [];
    let pos = 0;
    let tokens = [];
    let temp = [];
    let letter;

    pushState(INIT);

    /* eslint-disable */
    while (letter = source[pos]) {
    /* eslint-enable */
        const state = getState();
        if (state === INIT) {
            if (isOpenTag(letter)) {
                if (isSlash(getNextLetter())) {
                    if (isSpace(getNextLetter(2))) {
                        pushToTemp(letter);
                        next();
                        pushState(TEXT);
                    } else {
                        pushState(END_TAG);
                        next(2);
                    }
                } else if (isExclamationMark(getNextLetter())
                    && isDash(getNextLetter(2))
                    && isDash(getNextLetter(3))
                ) {
                    next(4);
                    pushState(OPEN_COMMENT);
                } else {
                    if (isSpace(getNextLetter())) {
                        pushToTemp(letter);
                        next();
                        pushState(TEXT);
                    } else {
                        pushState(OPEN_TAG);
                        next();
                    }
                }
            } else if (isOpenExpr(letter)) {
                pushState(EXPR);
                next();
            } else {
                pushState(TEXT);
            }
            resetTemp();
        } else if (state === OPEN_TAG) {
            if (isSpace(letter)) {
                next();
            } else {
                pushState(TAGNAME);
                resetTemp();
            }
        } else if (state === TAGNAME) {
            if (isSpace(letter)) {
                pushToken(TAGNAME);
                pushState(TAG);
                resetTemp();
            } else if ((isSlash(letter) && isCloseTag(getNextLetter()))) {
                setTokenSelfClose(pushToken(TAGNAME));
                next();
                pushState(TAG);
                resetTemp();
            } else if (isCloseTag(letter)) {
                pushToken(TAGNAME);
                pushState(TAG);
            } else {
                next();
                pushToTemp(letter);
            }
        } else if (state === TAG) {
            if (isCloseTag(letter)) {
                pushState(CLOSE_TAG);
            } else if (isSpace(letter)) {
                next();
            } else {
                pushState(ATTR);
                resetTemp();
            }
        } else if (state === ATTR) {
            if (isEqual(letter)) {
                pushToken(ATTR);
                next();
                pushState(EQUAL);
                resetTemp();
            } else if (isSpace(letter)) {
                next();
                pushToken(ATTR);
                resetTemp();
            } else if (isCloseTag(letter)) {
                pushToken(ATTR);
                pushState(TAG);
            } else {
                next();
                pushToTemp(letter);
            }
        } else if (state === EQUAL) {
            if (isQuote(letter)) {
                pushToTemp(letter);
                next();
                pushState(VALUE);
            } else if (isSpace(letter)) {
                next();
            } else if (isCloseTag(letter)) {
                pushState(TAG);
            } else {
                pushState(VALUE);
            }
        } else if (state === CLOSE_TAG) {
            popState(CLOSE_TAG);

            if (isAttrState()) {
                while (isAttrState()) {
                    popState();
                }
            }

            popState(TAGNAME);
            popState(OPEN_TAG);

            next();
            resetTemp();
        } else if (state === VALUE) {
            if (
                isQuote(letter) && letter === temp[0] && getPrevLetter() !== '//'
            ) {
                pushToTemp(letter);
                pushToken(VALUE);
                next();
                pushState(TAG);
            }  else if (isSpace(letter)) {
                if (isQuote(temp[0])) {
                    next();
                    pushToTemp(letter);
                } else {
                    pushToken(VALUE);
                    next();
                    pushState(TAG);
                }
            } else if (isCloseTag(letter)) {
                pushToken(VALUE);
                pushState(TAG);
            } else {
                next();
                pushToTemp(letter);
            }
        } else if (state === END_TAG) {
            if (isSpace(letter)) {
                next();
            } else if (isCloseTag(letter)) {
                pushToken(END_TAG);
                popState(END_TAG);
                next();
            } else {
                pushToTemp(letter);
                next();
            }
        } else if (state === EXPR) {
            if (isOpenExpr(letter)) {
                pushToTemp(letter);
                pushToken(TEXT);
                resetTemp();
                next();
            } else if (isCloseExpr(letter)) {
                pushToken(EXPR);
                next();
                popState(EXPR);
            } else {
                pushToTemp(letter);
                next();
            }
        } else if (state === TEXT) {
            if (isOpenTag(letter) || isOpenExpr(letter)) {
                pushToken(TEXT);
                popState();
            } else {
                pushToTemp(letter);
                next();
            }
        } else if (state === OPEN_COMMENT) {
            if (isDash(letter) && isDash(getNextLetter()) && isCloseTag(getNextLetter(2))) {
                pushToken(COMMENT);
                next(3);
                popState();
            } else {
                next();
                pushToTemp(letter);
            }
        }
    }

    let rollBackPos = -1;
    while (states.length !== 1){
        switch (getState()) {
        case OPEN_COMMENT:
            pushToken(COMMENT);
            popState();
            break;
        case TEXT:
            pushToken(TEXT);
            popState();
            break;
        case TAG:
        case TAGNAME:
        case OPEN_TAG:
            rollBackPos = states[states.length - 1].pos;
            popState();
            break;
        case EXPR:
            rollBackPos = states[states.length - 1].pos;
            popState();
            break;
        }
    }

    if (states.length !== 1) {
        throw `Unparsable codes: ${getContentFromTemp()}`;
    }

    resetTemp();
    pushToTemp(source.substr(rollBackPos));
    pushToken(TEXT);

    tokens = mergeTextTokens(tokens);

    tokens = [...tokens, {
        type: EOF
    }];

    return tokens;

    function next(i = 1) {
        pos += i;
    }

    function pushState(nextState) {
        states.push({
            state: nextState,
            pos
        });
    }

    function pushToken(type) {
        const sourceFragment = source.substr(0, pos);
        const matchLines = sourceFragment.match(/\n/g);
        const matchColumns = sourceFragment.match(/[^\n]*$/g);
        const line = (matchLines && matchLines.length) + 1;
        const column = (matchColumns && matchColumns.length);

        const token = {
            type,
            content: getContentFromTemp(),
            pos: pos,
            line,
            column
        };

        tokens.push(token);

        return token;
    }

    function pushToTemp(item) {
        temp.push(item);
    }

    function resetTemp() {
        temp.length = 0;
    }

    function getContentFromTemp() {
        return temp.join('');
    }

    function getPrevLetter(i = 1) {
        return source[pos - i];
    }

    function getNextLetter(i = 1) {
        return source[pos + i];
    }

    function setTokenSelfClose(token) {
        token.isSelfClose = true;
    }

    function getState() {
        return (states[states.length - 1] || {}).state;
    }

    function popState(expectState) {
        if (!expectState) {
            return states.pop();
        }

        if (getState() === expectState) {
            return states.pop();
        }
        throw `Check your syntax - expect state ${expectState}, but actual state is ${getState()}`;
    }

    function isAttrState() {
        return ~[TAG, ATTR, EQUAL, VALUE].indexOf(getState());
    }
}

exports.Lexer = Lexer;<|endoftext|>


<commit_before>const path = require('path')
const _ = require('lodash')
const koa = require('koa')
const body = require('koa-body')
const serve = require('koa-static')
const mount = require('koa-mount')
const routes = require('./routes')
const stringUtil = require('../common/util/string')
const clientEntry = require('./middleware/client-entry')
const errorHandler = require('./middleware/error-handler')
const methods = require('./middleware/methods')
const logger = require('./middleware/logger')
const cookies = require('./middleware/cookies')
const defaults = require('../config/default.json')

/**
 * Server entry module
 *
 * Configures and spins up Express server
 */

module.exports = class Nimda {

  /**
   * Class constructor
   *
   * @param  {Object} config
   * @param  {Object} schema
   * @constructor
   */
  constructor(config, schema = {}) {
    // Set config
    this.config = _.extend({}, defaults, config)
    this.config.basePath = stringUtil.ensureTrailingSlash(this.config.basePath)

    // Instanciate utils and schema loader
    this.util = require('./util')(this)
    this.schemaLoader = require('./access/schema-loader')(this)

    // Load (resolve and store) schema
    this.schemaLoader.load(schema)

    // Instanciate modules
    this.db = require('./access/db')(this)
    this.fileTransports = require('./access/file-transport')(this)
    this.file = require('./access/file')(this)
    this.orm = require('./orm')(this)
    this.setup = require('./setup')(this)

    // Instanciate Koa server
    this.app = koa()
  }

  /**
   * Validate configuration Object
   *
   * @retur {Error|void}
   */
  validateConfig(config) {
    if (!config) { return new Error('Configuration Object is required') }

    if (!config.mongo) {
      return new Error('Configuration Object requires a `mongo` Object')
    }

    if (!config.mongo.url) {
      return new Error('Configuration Object requires `mongo.url` String')
    }

    return null
  }

  /**
   * Initialise server
   *
   * @return {void}
   */
  init() {
    let uploadsDir

    // Resolve uploads dir
    if (this.config.uploadsDir) {
      uploadsDir = this.config.uploadsDir
    } else {
      uploadsDir = path.resolve(__dirname, '../uploads')
    }

    console.log(uploadsDir)

    if (this._initialised) {
      throw new Error('This Nimda instance was already initialised')
    }

    this._initialised = true

    if (this.config) {
      let error = this.validateConfig(this.config)
      if (error) { throw error }
    }

    // Attach middleware
    this.app.use(cookies(this))
    this.app.use(logger(this))
    this.app.use(errorHandler(this))
    this.app.use(body({ multipart: true, formidable: { uploadDir: './tmp' } }))
    this.app.use(methods(this))
    this.app.use(routes(this))
    this.app.use(serve('www'))
    this.app.use(mount('/uploads', serve(uploadsDir)))
    this.app.use(clientEntry(this))

    // Log initialisation
    this.util.log.task('Initialising nimda', -1)

    // Connect to databse
    return this.db.connect().then(() => this.setup.run())
  }

  /**
   * Generate server middleware function
   *
   * @return {Function}
   */
  middleware() {
    let fn

    this.init().then(() => {
      let app = this.app

      if (this.config.basePath && this.config.basePath.length > 1) {
        let basePath = stringUtil.removeTrailingSlash(this.config.basePath)
        app = koa()
        app.use(mount(basePath, this.app))
      }

      fn = app.callback()
    })

    return function (req, res) {
      if (fn) { return fn.apply(this, arguments) }
      res.write('Starting Nimda - please refresh in a second..')
      res.end()
    }
  }

  /**
   * Start server and listen on given port
   *
   * @return {Promise}
   */
  listen(port = null) {
    this.init().then(() => {
      this.util.log.task('Starting server', -1)

      return this.app.listen(port || this.config.port, () => {
        return this.util.log.task(`Listening on port ${ this.config.port }`, 1)
      })
    })
  }

}<commit_msg>Remove log, fix static path<commit_after>const path = require('path')
const _ = require('lodash')
const koa = require('koa')
const body = require('koa-body')
const serve = require('koa-static')
const mount = require('koa-mount')
const routes = require('./routes')
const stringUtil = require('../common/util/string')
const clientEntry = require('./middleware/client-entry')
const errorHandler = require('./middleware/error-handler')
const methods = require('./middleware/methods')
const logger = require('./middleware/logger')
const cookies = require('./middleware/cookies')
const defaults = require('../config/default.json')

/**
 * Server entry module
 *
 * Configures and spins up Express server
 */

module.exports = class Nimda {

  /**
   * Class constructor
   *
   * @param  {Object} config
   * @param  {Object} schema
   * @constructor
   */
  constructor(config, schema = {}) {
    // Set config
    this.config = _.extend({}, defaults, config)
    this.config.basePath = stringUtil.ensureTrailingSlash(this.config.basePath)

    // Instanciate utils and schema loader
    this.util = require('./util')(this)
    this.schemaLoader = require('./access/schema-loader')(this)

    // Load (resolve and store) schema
    this.schemaLoader.load(schema)

    // Instanciate modules
    this.db = require('./access/db')(this)
    this.fileTransports = require('./access/file-transport')(this)
    this.file = require('./access/file')(this)
    this.orm = require('./orm')(this)
    this.setup = require('./setup')(this)

    // Instanciate Koa server
    this.app = koa()
  }

  /**
   * Validate configuration Object
   *
   * @retur {Error|void}
   */
  validateConfig(config) {
    if (!config) { return new Error('Configuration Object is required') }

    if (!config.mongo) {
      return new Error('Configuration Object requires a `mongo` Object')
    }

    if (!config.mongo.url) {
      return new Error('Configuration Object requires `mongo.url` String')
    }

    return null
  }

  /**
   * Initialise server
   *
   * @return {void}
   */
  init() {
    let staticDir = path.resolve(__dirname, '../www')
    let uploadsDir

    // Resolve uploads dir
    if (this.config.uploadsDir) {
      uploadsDir = this.config.uploadsDir
    } else {
      uploadsDir = path.resolve(__dirname, '../uploads')
    }

    if (this._initialised) {
      throw new Error('This Nimda instance was already initialised')
    }

    this._initialised = true

    if (this.config) {
      let error = this.validateConfig(this.config)
      if (error) { throw error }
    }

    // Attach middleware
    this.app.use(cookies(this))
    this.app.use(logger(this))
    this.app.use(errorHandler(this))
    this.app.use(body({ multipart: true, formidable: { uploadDir: './tmp' } }))
    this.app.use(methods(this))
    this.app.use(routes(this))
    this.app.use(serve(staticDir))
    this.app.use(mount('/uploads', serve(uploadsDir)))
    this.app.use(clientEntry(this))

    // Log initialisation
    this.util.log.task('Initialising nimda', -1)

    // Connect to databse
    return this.db.connect().then(() => this.setup.run())
  }

  /**
   * Generate server middleware function
   *
   * @return {Function}
   */
  middleware() {
    let fn

    this.init().then(() => {
      let app = this.app

      if (this.config.basePath && this.config.basePath.length > 1) {
        let basePath = stringUtil.removeTrailingSlash(this.config.basePath)
        app = koa()
        app.use(mount(basePath, this.app))
      }

      fn = app.callback()
    })

    return function (req, res) {
      if (fn) { return fn.apply(this, arguments) }
      res.write('Starting Nimda - please refresh in a second..')
      res.end()
    }
  }

  /**
   * Start server and listen on given port
   *
   * @return {Promise}
   */
  listen(port = null) {
    this.init().then(() => {
      this.util.log.task('Starting server', -1)

      return this.app.listen(port || this.config.port, () => {
        return this.util.log.task(`Listening on port ${ this.config.port }`, 1)
      })
    })
  }

}<|endoftext|>


<commit_before>var childProcess = require('child_process'),
  fs = require('fs'),
  path = require('path'),
  running = require('is-running'),
  LocalBinary = require('./LocalBinary'),
  LocalError = require('./LocalError'),
  psTree = require('ps-tree');

function Local(){
  this.pid = undefined;
  this.isProcessRunning = false;
  this.retriesLeft = 5;
  this.key = process.env.BROWSERSTACK_ACCESS_KEY;
  this.logfile = path.join(process.cwd(), 'local.log');
  this.opcode = 'start';
  this.exitCallback;

  this.errorRegex = /\*\*\* Error: [^\r\n]*/i;
  this.doneRegex = /Press Ctrl-C to exit/i;

  this.start = function(options, callback){
    this.userArgs = [];
    var that = this;
    this.addArgs(options);

    if(typeof options['onlyCommand'] !== 'undefined')
      return callback();

    this.getBinaryPath(function(binaryPath){
      that.binaryPath = binaryPath;
      childProcess.exec('echo "" > ' + that.logfile);

      that.opcode = 'start';
      that.tunnel = childProcess.execFile(that.binaryPath, that.getBinaryArgs(), function(error, stdout, stderr){
        if(error) {
          console.error('Error while trying to execute binary', error);
          if(that.retriesLeft > 0) {
            console.log('Retrying Binary Download. Retries Left', that.retriesLeft);
            that.retriesLeft -= 1;
            fs.unlinkSync(that.binaryPath);
            delete(that.binaryPath);
            that.start(options, callback);
            return;
          } else {
            callback(new LocalError(error.toString()));
          }
        }

        var data = {};
        if(stdout)
          data = JSON.parse(stdout);
        else if(stderr)
          data = JSON.parse(stderr);
        else
          callback(new LocalError('No output received'));

        if(data['state'] != 'connected'){
          callback(new LocalError(data['message']['message']));
        } else {
          that.pid = data['pid'];
          that.isProcessRunning = true;
          callback();
        }
      });
    });
  };

  this.isRunning = function(){
    return this.pid && running(this.pid) && this.isProcessRunning;
  };

  this.stop = function (callback) {
    if(!this.pid) return callback();
    this.killAllProcesses(function(error){
      if(error) callback(new LocalError(error.toString()));
      callback();
    });
  };

  this.addArgs = function(options){
    for(var key in options){
      var value = options[key];

      switch(key){
      case 'key':
        this.key = value;
        break;

      case 'verbose':
        if(value.toString() !== 'true')
          this.verboseFlag = value;
        else {
          this.verboseFlag = '1';
        }
        break;

      case 'force':
        if(value)
          this.forceFlag = '--force';
        break;

      case 'only':
        if(value)
          this.onlyHosts = value;
        break;

      case 'onlyAutomate':
        if(value)
          this.onlyAutomateFlag = '--only-automate';
        break;

      case 'forcelocal':
      case 'forceLocal':
        if(value)
          this.forceLocalFlag = '--force-local';
        break;

      case 'localIdentifier':
        if(value)
          this.localIdentifierFlag = value;
        break;

      case 'f':
      case 'folder':
        if(value){
          this.folderFlag = '-f';
          this.folderPath = value;
        }
        break;

      case 'proxyHost':
        if(value)
          this.proxyHost = value;
        break;

      case 'proxyPort':
        if(value)
          this.proxyPort = value;
        break;

      case 'proxyUser':
        if(value)
          this.proxyUser = value;
        break;

      case 'proxyPass':
        if(value)
          this.proxyPass = value;
        break;

      case 'forceproxy':
      case 'forceProxy':
        if(value)
          this.forceProxyFlag = '--force-proxy';
        break;

      case 'logfile':
      case 'logFile':
        if(value)
          this.logfile = value;
        break;

      case 'parallelRuns':
        if(value)
          this.parallelRunsFlag = value;
        break;

      case 'binarypath':
        if(value)
          this.binaryPath = value;
        break;

      default:
        if(value.toString().toLowerCase() == 'true'){
          this.userArgs.push('--' + key);
        } else {
          this.userArgs.push('--' + key);
          this.userArgs.push(value);
        }
        break;
      }
    }
  };

  this.getBinaryPath = function(callback){
    if(typeof(this.binaryPath) == 'undefined'){
      this.binary = new LocalBinary();
      var conf = {};
      if(this.proxyHost && this.proxyPort){
        conf.proxyHost = this.proxyHost;
        conf.proxyPort = this.proxyPort;
      }
      this.binary.binaryPath(conf, callback);
    } else {
      console.log('BINARY PATH IS DEFINED');
      callback(this.binaryPath);
    }
  };

  this.getBinaryArgs = function(){
    var args = ['--daemon', this.opcode, '--log-file', this.logfile];
    if(this.folderFlag)
      args.push(this.folderFlag);
    if(this.key) {
      args.push('--key');
      args.push(this.key);
    }
    if(this.folderPath)
      args.push(this.folderPath);
    if(this.forceLocalFlag)
      args.push(this.forceLocalFlag);
    if(this.localIdentifierFlag){
      args.push('--local-identifier');
      args.push(this.localIdentifierFlag);
    }
    if(this.parallelRunsFlag){
      args.push('--parallel-runs');
      args.push(this.parallelRunsFlag.toString());
    }
    if(this.onlyHosts) {
      args.push('--only');
      args.push(this.onlyHosts);
    }
    if(this.onlyAutomateFlag)
      args.push(this.onlyAutomateFlag);
    if(this.proxyHost){
      args.push('--proxy-host');
      args.push(this.proxyHost);
    }
    if(this.proxyPort){
      args.push('--proxy-port');
      args.push(this.proxyPort);
    }
    if(this.proxyUser){
      args.push('--proxy-user');
      args.push(this.proxyUser);
    }
    if(this.proxyPass){
      args.push('--proxy-pass');
      args.push(this.proxyPass);
    }
    if(this.forceProxyFlag)
      args.push(this.forceProxyFlag);
    if(this.forceFlag)
      args.push(this.forceFlag);
    if(this.verboseFlag){
      args.push('--verbose');
      args.push(this.verboseFlag.toString());
    }
    for(var i in this.userArgs){
      args.push(this.userArgs[i]);
    }
    return args;
  };

  this.killAllProcesses = function(callback){
    psTree(this.pid, (err, children) => {
      var childPids = children.map(val => val.PID);
      var killChecker = setInterval(() => {
        if(childPids.length === 0) {
          clearInterval(killChecker);
          try {
            process.kill(this.pid);
            // This gives time to local binary to send kill signal to railsApp.
            setTimeout(() => {
              this.isProcessRunning = false;
              callback();
            }, 2000);
          } catch(err) {
            this.isProcessRunning = false;
            callback();
          }
        }
        for(var i in childPids) {
          try {
            process.kill(childPids[i]);
          } catch(err) {
            childPids.splice(i, 1);
          }
        }
      },500);
    });
  };
}

module.exports = Local;
<commit_msg>Fix argument order sent via command line to browserstack local binary. Previously, the folder flag and folder value were split by the key argument.<commit_after>var childProcess = require('child_process'),
  fs = require('fs'),
  path = require('path'),
  running = require('is-running'),
  LocalBinary = require('./LocalBinary'),
  LocalError = require('./LocalError'),
  psTree = require('ps-tree');

function Local(){
  this.pid = undefined;
  this.isProcessRunning = false;
  this.retriesLeft = 5;
  this.key = process.env.BROWSERSTACK_ACCESS_KEY;
  this.logfile = path.join(process.cwd(), 'local.log');
  this.opcode = 'start';
  this.exitCallback;

  this.errorRegex = /\*\*\* Error: [^\r\n]*/i;
  this.doneRegex = /Press Ctrl-C to exit/i;

  this.start = function(options, callback){
    this.userArgs = [];
    var that = this;
    this.addArgs(options);

    if(typeof options['onlyCommand'] !== 'undefined')
      return callback();

    this.getBinaryPath(function(binaryPath){
      that.binaryPath = binaryPath;
      childProcess.exec('echo "" > ' + that.logfile);

      that.opcode = 'start';
      that.tunnel = childProcess.execFile(that.binaryPath, that.getBinaryArgs(), function(error, stdout, stderr){
        if(error) {
          console.error('Error while trying to execute binary', error);
          if(that.retriesLeft > 0) {
            console.log('Retrying Binary Download. Retries Left', that.retriesLeft);
            that.retriesLeft -= 1;
            fs.unlinkSync(that.binaryPath);
            delete(that.binaryPath);
            that.start(options, callback);
            return;
          } else {
            callback(new LocalError(error.toString()));
          }
        }

        var data = {};
        if(stdout)
          data = JSON.parse(stdout);
        else if(stderr)
          data = JSON.parse(stderr);
        else
          callback(new LocalError('No output received'));

        if(data['state'] != 'connected'){
          callback(new LocalError(data['message']['message']));
        } else {
          that.pid = data['pid'];
          that.isProcessRunning = true;
          callback();
        }
      });
    });
  };

  this.isRunning = function(){
    return this.pid && running(this.pid) && this.isProcessRunning;
  };

  this.stop = function (callback) {
    if(!this.pid) return callback();
    this.killAllProcesses(function(error){
      if(error) callback(new LocalError(error.toString()));
      callback();
    });
  };

  this.addArgs = function(options){
    for(var key in options){
      var value = options[key];

      switch(key){
      case 'key':
        this.key = value;
        break;

      case 'verbose':
        if(value.toString() !== 'true')
          this.verboseFlag = value;
        else {
          this.verboseFlag = '1';
        }
        break;

      case 'force':
        if(value)
          this.forceFlag = '--force';
        break;

      case 'only':
        if(value)
          this.onlyHosts = value;
        break;

      case 'onlyAutomate':
        if(value)
          this.onlyAutomateFlag = '--only-automate';
        break;

      case 'forcelocal':
      case 'forceLocal':
        if(value)
          this.forceLocalFlag = '--force-local';
        break;

      case 'localIdentifier':
        if(value)
          this.localIdentifierFlag = value;
        break;

      case 'f':
      case 'folder':
        if(value){
          this.folderFlag = '-f';
          this.folderPath = value;
        }
        break;

      case 'proxyHost':
        if(value)
          this.proxyHost = value;
        break;

      case 'proxyPort':
        if(value)
          this.proxyPort = value;
        break;

      case 'proxyUser':
        if(value)
          this.proxyUser = value;
        break;

      case 'proxyPass':
        if(value)
          this.proxyPass = value;
        break;

      case 'forceproxy':
      case 'forceProxy':
        if(value)
          this.forceProxyFlag = '--force-proxy';
        break;

      case 'logfile':
      case 'logFile':
        if(value)
          this.logfile = value;
        break;

      case 'parallelRuns':
        if(value)
          this.parallelRunsFlag = value;
        break;

      case 'binarypath':
        if(value)
          this.binaryPath = value;
        break;

      default:
        if(value.toString().toLowerCase() == 'true'){
          this.userArgs.push('--' + key);
        } else {
          this.userArgs.push('--' + key);
          this.userArgs.push(value);
        }
        break;
      }
    }
  };

  this.getBinaryPath = function(callback){
    if(typeof(this.binaryPath) == 'undefined'){
      this.binary = new LocalBinary();
      var conf = {};
      if(this.proxyHost && this.proxyPort){
        conf.proxyHost = this.proxyHost;
        conf.proxyPort = this.proxyPort;
      }
      this.binary.binaryPath(conf, callback);
    } else {
      console.log('BINARY PATH IS DEFINED');
      callback(this.binaryPath);
    }
  };

  this.getBinaryArgs = function(){
    var args = ['--daemon', this.opcode, '--log-file', this.logfile];
    if(this.key) {
      args.push('--key');
      args.push(this.key);
    }
    if(this.folderFlag)
      args.push(this.folderFlag);
    if(this.folderPath)
      args.push(this.folderPath);
    if(this.forceLocalFlag)
      args.push(this.forceLocalFlag);
    if(this.localIdentifierFlag){
      args.push('--local-identifier');
      args.push(this.localIdentifierFlag);
    }
    if(this.parallelRunsFlag){
      args.push('--parallel-runs');
      args.push(this.parallelRunsFlag.toString());
    }
    if(this.onlyHosts) {
      args.push('--only');
      args.push(this.onlyHosts);
    }
    if(this.onlyAutomateFlag)
      args.push(this.onlyAutomateFlag);
    if(this.proxyHost){
      args.push('--proxy-host');
      args.push(this.proxyHost);
    }
    if(this.proxyPort){
      args.push('--proxy-port');
      args.push(this.proxyPort);
    }
    if(this.proxyUser){
      args.push('--proxy-user');
      args.push(this.proxyUser);
    }
    if(this.proxyPass){
      args.push('--proxy-pass');
      args.push(this.proxyPass);
    }
    if(this.forceProxyFlag)
      args.push(this.forceProxyFlag);
    if(this.forceFlag)
      args.push(this.forceFlag);
    if(this.verboseFlag){
      args.push('--verbose');
      args.push(this.verboseFlag.toString());
    }
    for(var i in this.userArgs){
      args.push(this.userArgs[i]);
    }
    return args;
  };

  this.killAllProcesses = function(callback){
    psTree(this.pid, (err, children) => {
      var childPids = children.map(val => val.PID);
      var killChecker = setInterval(() => {
        if(childPids.length === 0) {
          clearInterval(killChecker);
          try {
            process.kill(this.pid);
            // This gives time to local binary to send kill signal to railsApp.
            setTimeout(() => {
              this.isProcessRunning = false;
              callback();
            }, 2000);
          } catch(err) {
            this.isProcessRunning = false;
            callback();
          }
        }
        for(var i in childPids) {
          try {
            process.kill(childPids[i]);
          } catch(err) {
            childPids.splice(i, 1);
          }
        }
      },500);
    });
  };
}

module.exports = Local;
<|endoftext|>


<commit_before>var BufferQueueReader = require('h5.buffers').BufferQueueReader;
var codeRegistry = require('./codeRegistry');
var optionRegistry = require('./optionRegistry');
var Message = require('./Message');

/**
 * @constructor
 * @param {Socket} socket
 */
function Proxy(socket)
{
  /**
   * @private
   * @type {Function}
   */
  this.onSocketData = this.onSocketData.bind(this);

  /**
   * @private
   * @type {Function}
   */
  this.onSocketClose = this.onSocketClose.bind(this);

  /**
   * @private
   * @type {Function}
   */
  this.handleReaderData = this.handleReaderData.bind(this);

  /**
   * @private
   * @type {Number}
   */
  this.packetId = 0;

  /**
   * @private
   * @type {Socket}
   */
  this.socket = socket;
  this.socket.on('data', this.onSocketData);

  /**
   * @private
   * @type {BufferQueueReader}
   */
  this.reader = new BufferQueueReader();

  /**
   * @private
   * @type {Number}
   */
  this.currentFrameLength = 0;

  /**
   * @private
   * @type {Object.<String, Message>}
   */
  this.requests = {};

  /**
   * @private
   * @type {Object.<String, String>}
   */
  this.observers = {};
}

Proxy.prototype.destroy = function()
{
  this.socket.removeListener('data', this.onSocketData);
  this.socket.removeListener('close', this.onSocketClose);
  this.socket = null;

  this.reader.skip(this.reader.length);
  this.reader = null;

  this.requests = null;
  this.observers = null;
};

/**
 * @param {Message|Object} message
 * @return {Message}
 */
Proxy.prototype.request = function(message)
{
  var request = Message.fromObject(message);
  var requestFrame = request.toBuffer();
  var requestId = this.getNextPacketId();
  var packet = new Buffer(4 + requestFrame.length);

  packet.writeUInt16BE(2 + requestFrame.length, 0);
  packet.writeUInt16BE(requestId, 2);
  requestFrame.copy(packet, 4);

  var proxy = this;
  var internal = arguments[1] === true;

  try
  {
    this.socket.write(packet, function()
    {
      proxy.requests[requestId] = request;

      if (!internal)
      {
        proxy.manageObserver(requestId, request);
      }
    });
  }
  catch (err)
  {
    request.emit('error', err);
  }

  return request;
};

/**
 * @private
 * @return {Number}
 */
Proxy.prototype.getNextPacketId = function()
{
  this.packetId += 1;

  if (this.packetId > 0xFFFF)
  {
    this.packetId = 1;
  }

  return this.packetId;
};

/**
 * @private
 * @param {Number} requestId
 * @param {Message} request
 */
Proxy.prototype.manageObserver = function(requestId, request)
{
  if (request.getCode() !== codeRegistry.get)
  {
    return;
  }

  if (request.hasOption(optionRegistry.observe))
  {
    this.addObserver(requestId, request);
  }
  else
  {
    this.removeObserver(request.getUri());
  }
};

/**
 * @private
 * @param {Number} requestId
 * @param {Message} observer
 */
Proxy.prototype.addObserver = function(requestId, observer)
{
  var uri = observer.getUri();

  this.observers[uri] = requestId;

  var proxy = this;

  observer.cancel(function()
  {
    var request = this;

    if (request.cancelledObserver || request.cancellingObserver)
    {
      return;
    }

    request.cancellingObserver = true;

    var cancelRequest = new Message(Message.NON, codeRegistry.get);

    cancelRequest.setUri(uri);
    cancelRequest.setOption(request.getOptions(optionRegistry.proxyUri));
    cancelRequest.on('response', function(response)
    {
      request.cancellingObserver = false;

      if (response.isResponse()
        && response.getCode() < codeRegistry.badRequest)
      {
        request.cancelledObserver = true;

        if (proxy.observers[uri] === requestId)
        {
          delete proxy.observers[uri];
        }

        delete proxy.requests[requestId];

        request.emit('cancelled');
      }
    });

    proxy.request(cancelRequest, true);
  });
};

/**
 * @private
 * @param {String} uri
 */
Proxy.prototype.removeObserver = function(uri)
{
  var observerId = this.observers[uri];

  if (typeof observerId !== 'undefined')
  {
    delete this.observers[uri];

    var observer = this.requests[observerId];

    if (typeof observer !== 'undefined')
    {
      delete this.requests[observerId];

      observer.emit('cancelled');
    }
  }
};

/**
 * @private
 */
Proxy.prototype.onSocketClose = function()
{
  this.reader.skip(this.reader.length);

  this.requests = {};
  this.observers = {};
};

/**
 * @private
 * @param {Buffer} data
 */
Proxy.prototype.onSocketData = function(data)
{
  this.reader.push(data);

  this.handleReaderData();
};

/**
 * @private
 */
Proxy.prototype.handleReaderData = function()
{
  if (this.reader === null)
  {
    return;
  }

  if (this.currentFrameLength === 0 && this.reader.length >= 2)
  {
    this.currentFrameLength = this.reader.shiftUInt16();
  }

  if (this.currentFrameLength < 6)
  {
    this.reader.skip(this.reader.length);

    this.currentFrameLength = 0;
  }
  else if (this.reader.length >= this.currentFrameLength)
  {
    var requestId = this.reader.shiftUInt16();

    if (requestId in this.requests)
    {
      var responseFrame = this.reader.shiftBuffer(this.currentFrameLength - 2);

      this.handleResponseFrame(requestId, responseFrame);
    }
    else
    {
      this.reader.skip(this.currentFrameLength - 2);
    }

    this.currentFrameLength = 0;

    if (this.reader.length > 0)
    {
      process.nextTick(this.handleReaderData);
    }
  }
};

/**
 * @private
 * @param {Number} requestId
 * @param {Buffer} responseFrame
 */
Proxy.prototype.handleResponseFrame = function(requestId, responseFrame)
{
  var request = this.requests[requestId];

  if (!request.hasOption(optionRegistry.observe))
  {
    delete this.requests[requestId];
  }

  try
  {
    var response = Message.fromBuffer(responseFrame);

    request.emit('response', response);
  }
  catch (err)
  {
    request.emit('error', err);
  }
};

module.exports = Proxy;
<commit_msg>Changed the packet length field to 32-bit to reflect the changes in the californium-proxy-java.<commit_after>var BufferQueueReader = require('h5.buffers').BufferQueueReader;
var codeRegistry = require('./codeRegistry');
var optionRegistry = require('./optionRegistry');
var Message = require('./Message');

/**
 * @constructor
 * @param {Socket} socket
 */
function Proxy(socket)
{
  /**
   * @private
   * @type {Function}
   */
  this.onSocketData = this.onSocketData.bind(this);

  /**
   * @private
   * @type {Function}
   */
  this.onSocketClose = this.onSocketClose.bind(this);

  /**
   * @private
   * @type {Function}
   */
  this.handleReaderData = this.handleReaderData.bind(this);

  /**
   * @private
   * @type {Number}
   */
  this.packetId = 0;

  /**
   * @private
   * @type {Socket}
   */
  this.socket = socket;
  this.socket.on('data', this.onSocketData);

  /**
   * @private
   * @type {BufferQueueReader}
   */
  this.reader = new BufferQueueReader();

  /**
   * @private
   * @type {Number}
   */
  this.currentFrameLength = 0;

  /**
   * @private
   * @type {Object.<String, Message>}
   */
  this.requests = {};

  /**
   * @private
   * @type {Object.<String, String>}
   */
  this.observers = {};
}

Proxy.prototype.destroy = function()
{
  this.socket.removeListener('data', this.onSocketData);
  this.socket.removeListener('close', this.onSocketClose);
  this.socket = null;

  this.reader.skip(this.reader.length);
  this.reader = null;

  this.requests = null;
  this.observers = null;
};

/**
 * @param {Message|Object} message
 * @return {Message}
 */
Proxy.prototype.request = function(message)
{
  var request = Message.fromObject(message);
  var requestFrame = request.toBuffer();
  var requestId = this.getNextPacketId();
  var packet = new Buffer(6 + requestFrame.length);

  packet.writeUInt32BE(2 + requestFrame.length, 0);
  packet.writeUInt16BE(requestId, 4);
  requestFrame.copy(packet, 6);

  var proxy = this;
  var internal = arguments[1] === true;

  try
  {
    this.socket.write(packet, function()
    {
      proxy.requests[requestId] = request;

      if (!internal)
      {
        proxy.manageObserver(requestId, request);
      }
    });
  }
  catch (err)
  {
    request.emit('error', err);
  }

  return request;
};

/**
 * @private
 * @return {Number}
 */
Proxy.prototype.getNextPacketId = function()
{
  this.packetId += 1;

  if (this.packetId > 0xFFFF)
  {
    this.packetId = 1;
  }

  return this.packetId;
};

/**
 * @private
 * @param {Number} requestId
 * @param {Message} request
 */
Proxy.prototype.manageObserver = function(requestId, request)
{
  if (request.getCode() !== codeRegistry.get)
  {
    return;
  }

  if (request.hasOption(optionRegistry.observe))
  {
    this.addObserver(requestId, request);
  }
  else
  {
    this.removeObserver(request.getUri());
  }
};

/**
 * @private
 * @param {Number} requestId
 * @param {Message} observer
 */
Proxy.prototype.addObserver = function(requestId, observer)
{
  var uri = observer.getUri();

  this.observers[uri] = requestId;

  var proxy = this;

  observer.cancel(function()
  {
    var request = this;

    if (request.cancelledObserver || request.cancellingObserver)
    {
      return;
    }

    request.cancellingObserver = true;

    var cancelRequest = new Message(Message.NON, codeRegistry.get);

    cancelRequest.setUri(uri);
    cancelRequest.setOption(request.getOptions(optionRegistry.proxyUri));
    cancelRequest.on('response', function(response)
    {
      request.cancellingObserver = false;

      if (response.isResponse()
        && response.getCode() < codeRegistry.badRequest)
      {
        request.cancelledObserver = true;

        if (proxy.observers[uri] === requestId)
        {
          delete proxy.observers[uri];
        }

        delete proxy.requests[requestId];

        request.emit('cancelled');
      }
    });

    proxy.request(cancelRequest, true);
  });
};

/**
 * @private
 * @param {String} uri
 */
Proxy.prototype.removeObserver = function(uri)
{
  var observerId = this.observers[uri];

  if (typeof observerId !== 'undefined')
  {
    delete this.observers[uri];

    var observer = this.requests[observerId];

    if (typeof observer !== 'undefined')
    {
      delete this.requests[observerId];

      observer.emit('cancelled');
    }
  }
};

/**
 * @private
 */
Proxy.prototype.onSocketClose = function()
{
  this.reader.skip(this.reader.length);

  this.requests = {};
  this.observers = {};
};

/**
 * @private
 * @param {Buffer} data
 */
Proxy.prototype.onSocketData = function(data)
{
  this.reader.push(data);

  this.handleReaderData();
};

/**
 * @private
 */
Proxy.prototype.handleReaderData = function()
{
  if (this.reader === null)
  {
    return;
  }

  if (this.currentFrameLength === 0 && this.reader.length >= 4)
  {
    this.currentFrameLength = this.reader.shiftUInt32();
  }

  if (this.currentFrameLength < 6)
  {
    this.reader.skip(this.reader.length);

    this.currentFrameLength = 0;
  }
  else if (this.reader.length >= this.currentFrameLength)
  {
    var requestId = this.reader.shiftUInt16();

    if (requestId in this.requests)
    {
      var responseFrame = this.reader.shiftBuffer(this.currentFrameLength - 2);

      this.handleResponseFrame(requestId, responseFrame);
    }
    else
    {
      this.reader.skip(this.currentFrameLength - 2);
    }

    this.currentFrameLength = 0;

    if (this.reader.length > 0)
    {
      process.nextTick(this.handleReaderData);
    }
  }
};

/**
 * @private
 * @param {Number} requestId
 * @param {Buffer} responseFrame
 */
Proxy.prototype.handleResponseFrame = function(requestId, responseFrame)
{
  var request = this.requests[requestId];

  if (!request.hasOption(optionRegistry.observe))
  {
    delete this.requests[requestId];
  }

  try
  {
    var response = Message.fromBuffer(responseFrame);

    request.emit('response', response);
  }
  catch (err)
  {
    request.emit('error', err);
  }
};

module.exports = Proxy;
<|endoftext|>


<commit_before>'use strict';
var Q = require('q');
var errors = require('./errors');
var debug = require('debug')('dynamoose:query');

function Query (Model, query, options) {
  this.Model = Model;
  this.options = options || {};

  // {
  //   hashKey: {
  //     name: 'name',
  //     value: 'value'
  //   },
  //   rangeKey: {
  //     name: 'name',
  //     value: 'value',
  //     comparison: 'string'
  //   }
  // }
  this.query = {hashKey: {}};

  this.filters = {};
  this.buildState = false;

  var hashKeyName, hashKeyVal;
  if(typeof query === 'string') {
    this.buildState = 'hashKey';
    this.query.hashKey.name = query;
  } else if (query.hash) {
    hashKeyName = Object.keys(query.hash)[0];
    hashKeyVal = query.hash[hashKeyName];
    if(hashKeyVal.eq !== null && hashKeyVal.eq !== undefined) {
      hashKeyVal = hashKeyVal.eq;
    }
    this.query.hashKey.name = hashKeyName;
    this.query.hashKey.value = hashKeyVal;

    if(query.range) {
      var rangeKeyName = Object.keys(query.range)[0];
      var rangeKeyVal = query.range[rangeKeyName];
      var rangeKeyComp = Object.keys(rangeKeyVal)[0];
      rangeKeyVal = rangeKeyVal[rangeKeyComp];
      this.query.rangeKey = {
        name: rangeKeyName,
        value: rangeKeyVal,
        comparison: rangeKeyComp
      };
    }
  } else {
    hashKeyName = Object.keys(query)[0];
    hashKeyVal = query[hashKeyName];
    if(hashKeyVal.eq !== null && hashKeyVal.eq !== undefined) {
      hashKeyVal = hashKeyVal.eq;
    }
    this.query.hashKey.name = hashKeyName;
    this.query.hashKey.value = hashKeyVal;  }
}

Query.prototype.exec = function (next) {
  debug('exec query for ', this.query);
  var Model = this.Model;
  var Model$ = Model.$__;
  var schema = Model$.schema;
  var options = this.options;

  debug('Query with schema', schema);

  var queryReq = {
    TableName: Model.$__.name,
    KeyConditions: {}
  };

  var indexName, index;
  if(schema.hashKey.name !== this.query.hashKey.name) {
    debug('query is on global secondary index');
    for(indexName in schema.indexes.global) {
      index = schema.indexes.global[indexName];
      if(index.name === this.query.hashKey.name) {
        debug('using index', indexName);
        queryReq.IndexName = indexName;
        break;
      }
    }

  }

  var hashAttr = schema.attributes[this.query.hashKey.name];

  queryReq.KeyConditions[this.query.hashKey.name] = {
    AttributeValueList: [hashAttr.toDynamo(this.query.hashKey.value)],
    ComparisonOperator: 'EQ'
  };

  var i, val;

  if(this.query.rangeKey) {
    var rangeKey = this.query.rangeKey;
    var rangeAttr = schema.attributes[rangeKey.name];

    if(!queryReq.IndexName && schema.rangeKey.name !== rangeKey.name) {
      debug('query is on local secondary index');
      for(indexName in schema.indexes.local) {
        index = schema.indexes.local[indexName];
        if(index.name === rangeKey.name) {
          debug('using local index', indexName);
          queryReq.IndexName = indexName;
          break;
        }
      }
    }

    if(!rangeKey || rangeKey.values === undefined) {
      debug('No range key value (i.e. get all)');
    } else {
      debug('Range key: %s', rangeKey.name);
      var keyConditions = queryReq.KeyConditions[rangeKey.name] = {
        AttributeValueList: [],
        ComparisonOperator: rangeKey.comparison.toUpperCase()
      };
      for (i = 0; i < rangeKey.values.length; i++) {
        val = rangeKey.values [i];
        keyConditions.AttributeValueList.push(
          rangeAttr.toDynamo(val, true)
        );
      }
    }
  }

  if(this.filters && Object.keys(this.filters).length > 0) {
    queryReq.QueryFilter = {};
    for(var name in this.filters) {
      debug('Filter on: %s', name);
      var filter = this.filters[name];
      var filterAttr = schema.attributes[name];
      queryReq.QueryFilter[name] = {
        AttributeValueList: [],
        ComparisonOperator: filter.comparison.toUpperCase()
      };

      if(filter.values) {
        for (i = 0; i < filter.values.length; i++) {
          val = filter.values[i];
          queryReq.QueryFilter[name].AttributeValueList.push(
            filterAttr.toDynamo(val, true)
          );
        }
      }
    }
  }

  if(options.or) {
    queryReq.ConditionalOperator = 'OR'; // defualts to AND
  }

  if(options.attributes) {
    queryReq.AttributesToGet = options.attributes;
  }

  if(options.consistent) {
    queryReq.ConsistentRead = true;
  }

  if(options.limit) {
    queryReq.Limit = options.limit;
  }

  if(options.one) {
    queryReq.Limit = 1;
  }

  if(options.descending) {
    queryReq.ScanIndexForward = false;
  }

  if(options.ExclusiveStartKey) {
    queryReq.ExclusiveStartKey = options.ExclusiveStartKey;
  }

  function query () {
    var deferred = Q.defer();

    debug('DynamoDB Query: %j', queryReq);
    Model$.base.ddb().query(queryReq, function(err, data) {
      if(err) {
        debug('Error returned by query', err);
        return deferred.reject(err);
      }
      debug('DynamoDB Query Response: %j', data);

      if(!Object.keys(data).length) {
        return deferred.resolve();
      }

      function toModel (item) {
        var model = new Model();
        model.$__.isNew = false;
        schema.parseDynamo(model, item);

        debug('query parsed model', model);

        return model;
      }

      try {

        var models = data.Items.map(toModel);

        if(options.one) {
          if (!models || models.length === 0) {
            return deferred.resolve();
          }
          return deferred.resolve(models[0]);
        }

        models.lastKey = data.LastEvaluatedKey;
        deferred.resolve(models);

      } catch (err) {
        deferred.reject(err);
      }
    });

    return deferred.promise.nodeify(next);
  }

  if(Model$.options.waitForActive) {
    return Model$.table.waitForActive().then(query);
  }

  return query();
};

Query.prototype.where = function (rangeKey) {
  if(this.buildState) {
    throw new errors.QueryError('Invalid query state; where() must follow eq()');
  }
  if(typeof rangeKey === 'string') {
    this.buildState = 'rangeKey';
    this.query.rangeKey = {name: rangeKey};
  } else {
    var rangeKeyName = Object.keys(rangeKey)[0];
    var rangeKeyVal = rangeKey[rangeKeyName];
    var rangeKeyComp = Object.keys(rangeKeyVal)[0];
    rangeKeyVal = rangeKeyVal[rangeKeyComp];
    this.query.rangeKey = {
      name: rangeKeyName,
      values: [rangeKeyVal],
      comparison: rangeKeyComp
    };
  }

  return this;
};

Query.prototype.filter = function (filter) {
  if(this.buildState) {
    throw new errors.QueryError('Invalid query state; filter() must follow comparison');
  }
  if(typeof filter === 'string') {
    this.buildState = 'filter';
    this.currentFilter = filter;
    if(this.filters[filter]) {
      throw new errors.QueryError('Invalid query state; %s filter can only be used once', filter);
    }
    this.filters[filter] = {name: filter};
  }

  return this;
};

var VALID_RANGE_KEYS = ['EQ', 'LE', 'LT', 'GE', 'GT', 'BEGINS_WITH', 'BETWEEN'];
Query.prototype.compVal = function (vals, comp) {
  if(this.buildState === 'hashKey') {
    if(comp !== 'EQ') {
      throw new errors.QueryError('Invalid query state; eq must follow query()');
    }
    this.query.hashKey.value = vals[0];
  } else if (this.buildState === 'rangeKey'){
    if(VALID_RANGE_KEYS.indexOf(comp) < 0) {
      throw new errors.QueryError('Invalid query state; %s must follow filter()', comp);
    }
    this.query.rangeKey.values = vals;
    this.query.rangeKey.comparison = comp;
  } else if (this.buildState === 'filter') {
    this.filters[this.currentFilter].values = vals;
    this.filters[this.currentFilter].comparison = comp;
  } else {
    throw new errors.QueryError('Invalid query state; %s must follow query(), where() or filter()', comp);
  }

  this.buildState = false;
  this.notState = false;

  return this;
};

Query.prototype.and = function() {
  this.options.or = false;

  return this;
};

Query.prototype.or = function() {
  this.options.or = true;

  return this;
};

Query.prototype.not = function() {
  this.notState = true;
  return this;
};

Query.prototype.null = function() {
  if(this.notState) {
    return this.compVal(null, 'NOT_NULL');
  } else {
    return this.compVal(null, 'NULL');
  }
};

Query.prototype.eq = function (val) {
  if(this.notState) {
    return this.compVal([val], 'NE');
  } else {
    return this.compVal([val], 'EQ');
  }
};

Query.prototype.lt = function (val) {
  if(this.notState) {
    return this.compVal([val], 'GE');
  } else {
    return this.compVal([val], 'LT');
  }
};

Query.prototype.le = function (val) {
  if(this.notState) {
    return this.compVal([val], 'GT');
  } else {
    return this.compVal([val], 'LE');
  }
};

Query.prototype.ge = function (val) {
  if(this.notState) {
    return this.compVal([val], 'LT');
  } else {
    return this.compVal([val], 'GE');
  }
};

Query.prototype.gt = function (val) {
  if(this.notState) {
    return this.compVal([val], 'LE');
  } else {
    return this.compVal([val], 'GT');
  }
};

Query.prototype.contains = function (val) {
  if(this.notState) {
    return this.compVal([val], 'NOT_CONTAINS');
  } else {
    return this.compVal([val], 'CONTAINS');
  }
};

Query.prototype.beginsWith = function (val) {
  if(this.notState) {
    throw new errors.QueryError('Invalid Query state: beginsWith() cannot follow not()');
  }
  return this.compVal([val], 'BEGINS_WITH');
};

Query.prototype.in = function (vals) {
  if(this.notState) {
    throw new errors.QueryError('Invalid Query state: in() cannot follow not()');
  }

  return this.compVal(vals, 'IN');
};

Query.prototype.between = function (a, b) {
  if(this.notState) {
    throw new errors.QueryError('Invalid Query state: between() cannot follow not()');
  }
  return this.compVal([a, b], 'BETWEEN');
};

Query.prototype.limit = function (limit) {
  this.options.limit = limit;
  return this;
};

Query.prototype.one = function () {
  this.options.one = true;
  return this;
};

Query.prototype.consistent = function () {
  this.options.consistent = true;
  return this;
};

Query.prototype.descending = function () {
  this.options.descending = true;
  return this;
};

Query.prototype.ascending = function () {
  this.options.descending = false;
  return this;
};

Query.prototype.startAt = function (key) {
  this.options.ExclusiveStartKey = key;
  return this;
};

Query.prototype.attributes = function (attributes) {
  this.options.attributes = attributes;
  return this;
};

module.exports = Query;
<commit_msg>Pass error along when query building so it can sent to callback and promise<commit_after>'use strict';
var Q = require('q');
var errors = require('./errors');
var debug = require('debug')('dynamoose:query');

var validationError = null;

function Query (Model, query, options) {
  this.Model = Model;
  this.options = options || {};

  // {
  //   hashKey: {
  //     name: 'name',
  //     value: 'value'
  //   },
  //   rangeKey: {
  //     name: 'name',
  //     value: 'value',
  //     comparison: 'string'
  //   }
  // }
  this.query = {hashKey: {}};

  this.filters = {};
  this.buildState = false;

  var hashKeyName, hashKeyVal;
  if(typeof query === 'string') {
    this.buildState = 'hashKey';
    this.query.hashKey.name = query;
  } else if (query.hash) {
    hashKeyName = Object.keys(query.hash)[0];
    hashKeyVal = query.hash[hashKeyName];
    if(hashKeyVal.eq !== null && hashKeyVal.eq !== undefined) {
      hashKeyVal = hashKeyVal.eq;
    }
    this.query.hashKey.name = hashKeyName;
    this.query.hashKey.value = hashKeyVal;

    if(query.range) {
      var rangeKeyName = Object.keys(query.range)[0];
      var rangeKeyVal = query.range[rangeKeyName];
      var rangeKeyComp = Object.keys(rangeKeyVal)[0];
      rangeKeyVal = rangeKeyVal[rangeKeyComp];
      this.query.rangeKey = {
        name: rangeKeyName,
        value: rangeKeyVal,
        comparison: rangeKeyComp
      };
    }
  } else {
    hashKeyName = Object.keys(query)[0];
    hashKeyVal = query[hashKeyName];
    if(hashKeyVal.eq !== null && hashKeyVal.eq !== undefined) {
      hashKeyVal = hashKeyVal.eq;
    }
    this.query.hashKey.name = hashKeyName;
    this.query.hashKey.value = hashKeyVal;  }
}

Query.prototype.exec = function (next) {
  if (this.validationError) {
    if (next) {
      next(this.validationError);
    }
    return Q.reject(this.validationError);
  }
  debug('exec query for ', this.query);
  var Model = this.Model;
  var Model$ = Model.$__;
  var schema = Model$.schema;
  var options = this.options;

  debug('Query with schema', schema);

  var queryReq = {
    TableName: Model.$__.name,
    KeyConditions: {}
  };

  var indexName, index;
  if(schema.hashKey.name !== this.query.hashKey.name) {
    debug('query is on global secondary index');
    for(indexName in schema.indexes.global) {
      index = schema.indexes.global[indexName];
      if(index.name === this.query.hashKey.name) {
        debug('using index', indexName);
        queryReq.IndexName = indexName;
        break;
      }
    }

  }

  var hashAttr = schema.attributes[this.query.hashKey.name];

  queryReq.KeyConditions[this.query.hashKey.name] = {
    AttributeValueList: [hashAttr.toDynamo(this.query.hashKey.value)],
    ComparisonOperator: 'EQ'
  };

  var i, val;

  if(this.query.rangeKey) {
    var rangeKey = this.query.rangeKey;
    var rangeAttr = schema.attributes[rangeKey.name];

    if(!queryReq.IndexName && schema.rangeKey.name !== rangeKey.name) {
      debug('query is on local secondary index');
      for(indexName in schema.indexes.local) {
        index = schema.indexes.local[indexName];
        if(index.name === rangeKey.name) {
          debug('using local index', indexName);
          queryReq.IndexName = indexName;
          break;
        }
      }
    }

    if(!rangeKey || rangeKey.values === undefined) {
      debug('No range key value (i.e. get all)');
    } else {
      debug('Range key: %s', rangeKey.name);
      var keyConditions = queryReq.KeyConditions[rangeKey.name] = {
        AttributeValueList: [],
        ComparisonOperator: rangeKey.comparison.toUpperCase()
      };
      for (i = 0; i < rangeKey.values.length; i++) {
        val = rangeKey.values [i];
        keyConditions.AttributeValueList.push(
          rangeAttr.toDynamo(val, true)
        );
      }
    }
  }

  if(this.filters && Object.keys(this.filters).length > 0) {
    queryReq.QueryFilter = {};
    for(var name in this.filters) {
      debug('Filter on: %s', name);
      var filter = this.filters[name];
      var filterAttr = schema.attributes[name];
      queryReq.QueryFilter[name] = {
        AttributeValueList: [],
        ComparisonOperator: filter.comparison.toUpperCase()
      };

      if(filter.values) {
        for (i = 0; i < filter.values.length; i++) {
          val = filter.values[i];
          queryReq.QueryFilter[name].AttributeValueList.push(
            filterAttr.toDynamo(val, true)
          );
        }
      }
    }
  }

  if(options.or) {
    queryReq.ConditionalOperator = 'OR'; // defualts to AND
  }

  if(options.attributes) {
    queryReq.AttributesToGet = options.attributes;
  }

  if(options.consistent) {
    queryReq.ConsistentRead = true;
  }

  if(options.limit) {
    queryReq.Limit = options.limit;
  }

  if(options.one) {
    queryReq.Limit = 1;
  }

  if(options.descending) {
    queryReq.ScanIndexForward = false;
  }

  if(options.ExclusiveStartKey) {
    queryReq.ExclusiveStartKey = options.ExclusiveStartKey;
  }

  function query () {
    var deferred = Q.defer();

    debug('DynamoDB Query: %j', queryReq);
    Model$.base.ddb().query(queryReq, function(err, data) {
      if(err) {
        debug('Error returned by query', err);
        return deferred.reject(err);
      }
      debug('DynamoDB Query Response: %j', data);

      if(!Object.keys(data).length) {
        return deferred.resolve();
      }

      function toModel (item) {
        var model = new Model();
        model.$__.isNew = false;
        schema.parseDynamo(model, item);

        debug('query parsed model', model);

        return model;
      }

      try {

        var models = data.Items.map(toModel);

        if(options.one) {
          if (!models || models.length === 0) {
            return deferred.resolve();
          }
          return deferred.resolve(models[0]);
        }

        models.lastKey = data.LastEvaluatedKey;
        deferred.resolve(models);

      } catch (err) {
        deferred.reject(err);
      }
    });

    return deferred.promise.nodeify(next);
  }

  if(Model$.options.waitForActive) {
    return Model$.table.waitForActive().then(query);
  }

  return query();
};

Query.prototype.where = function (rangeKey) {
  if (this.validationError) {
    return this;
  }
  if(this.buildState) {
    this.validationError = new errors.QueryError('Invalid query state; where() must follow eq()');
    return this;
  }
  if(typeof rangeKey === 'string') {
    this.buildState = 'rangeKey';
    this.query.rangeKey = {name: rangeKey};
  } else {
    var rangeKeyName = Object.keys(rangeKey)[0];
    var rangeKeyVal = rangeKey[rangeKeyName];
    var rangeKeyComp = Object.keys(rangeKeyVal)[0];
    rangeKeyVal = rangeKeyVal[rangeKeyComp];
    this.query.rangeKey = {
      name: rangeKeyName,
      values: [rangeKeyVal],
      comparison: rangeKeyComp
    };
  }

  return this;
};

Query.prototype.filter = function (filter) {
  if (this.validationError) {
    return this;
  }
  if(this.buildState) {
    this.validationError = new errors.QueryError('Invalid query state; filter() must follow comparison');
    return this;
  }
  if(typeof filter === 'string') {
    this.buildState = 'filter';
    this.currentFilter = filter;
    if(this.filters[filter]) {
      this.validationError =  new errors.QueryError('Invalid query state; %s filter can only be used once', filter);
      return this;
    }
    this.filters[filter] = {name: filter};
  }

  return this;
};

var VALID_RANGE_KEYS = ['EQ', 'LE', 'LT', 'GE', 'GT', 'BEGINS_WITH', 'BETWEEN'];
Query.prototype.compVal = function (vals, comp) {
  if (this.validationError) {
    return this;
  }
  if(this.buildState === 'hashKey') {
    if(comp !== 'EQ') {
      this.validationError = new errors.QueryError('Invalid query state; eq must follow query()');
      return this;
    }
    this.query.hashKey.value = vals[0];
  } else if (this.buildState === 'rangeKey'){
    if(VALID_RANGE_KEYS.indexOf(comp) < 0) {
      this.validationError =  new errors.QueryError('Invalid query state; %s must follow filter()', comp);
      return this;
    }
    this.query.rangeKey.values = vals;
    this.query.rangeKey.comparison = comp;
  } else if (this.buildState === 'filter') {
    this.filters[this.currentFilter].values = vals;
    this.filters[this.currentFilter].comparison = comp;
  } else {
    this.validationError =  new errors.QueryError('Invalid query state; %s must follow query(), where() or filter()', comp);
    return this;
  }

  this.buildState = false;
  this.notState = false;

  return this;
};

Query.prototype.and = function() {
  this.options.or = false;

  return this;
};

Query.prototype.or = function() {
  this.options.or = true;

  return this;
};

Query.prototype.not = function() {
  this.notState = true;
  return this;
};

Query.prototype.null = function() {
  if(this.notState) {
    return this.compVal(null, 'NOT_NULL');
  } else {
    return this.compVal(null, 'NULL');
  }
};

Query.prototype.eq = function (val) {
  if(this.notState) {
    return this.compVal([val], 'NE');
  } else {
    return this.compVal([val], 'EQ');
  }
};

Query.prototype.lt = function (val) {
  if(this.notState) {
    return this.compVal([val], 'GE');
  } else {
    return this.compVal([val], 'LT');
  }
};

Query.prototype.le = function (val) {
  if(this.notState) {
    return this.compVal([val], 'GT');
  } else {
    return this.compVal([val], 'LE');
  }
};

Query.prototype.ge = function (val) {
  if(this.notState) {
    return this.compVal([val], 'LT');
  } else {
    return this.compVal([val], 'GE');
  }
};

Query.prototype.gt = function (val) {
  if(this.notState) {
    return this.compVal([val], 'LE');
  } else {
    return this.compVal([val], 'GT');
  }
};

Query.prototype.contains = function (val) {
  if(this.notState) {
    return this.compVal([val], 'NOT_CONTAINS');
  } else {
    return this.compVal([val], 'CONTAINS');
  }
};

Query.prototype.beginsWith = function (val) {
  if (this.validationError) {
    return this;
  }
  if(this.notState) {
    this.validationError =  new errors.QueryError('Invalid Query state: beginsWith() cannot follow not()');
    return this;
  }
  return this.compVal([val], 'BEGINS_WITH');
};

Query.prototype.in = function (vals) {
  if (this.validationError) {
    return this;
  }
  if(this.notState) {
    this.validationError = new errors.QueryError('Invalid Query state: in() cannot follow not()');
    return this;
  }

  return this.compVal(vals, 'IN');
};

Query.prototype.between = function (a, b) {
  if (this.validationError) {
    return this;
  }
  if(this.notState) {
    this.validationError = new errors.QueryError('Invalid Query state: between() cannot follow not()');
    return this;
  }
  return this.compVal([a, b], 'BETWEEN');
};

Query.prototype.limit = function (limit) {
  this.options.limit = limit;
  return this;
};

Query.prototype.one = function () {
  this.options.one = true;
  return this;
};

Query.prototype.consistent = function () {
  this.options.consistent = true;
  return this;
};

Query.prototype.descending = function () {
  this.options.descending = true;
  return this;
};

Query.prototype.ascending = function () {
  this.options.descending = false;
  return this;
};

Query.prototype.startAt = function (key) {
  this.options.ExclusiveStartKey = key;
  return this;
};

Query.prototype.attributes = function (attributes) {
  this.options.attributes = attributes;
  return this;
};

module.exports = Query;
<|endoftext|>


<commit_before>var uuid = require('uuid');
var runCallback = require('./common.js').runCallback;

/**
 * Conventions:
 *  - The "id" property is always used as the unique ID for classes.
 *  - Classes should be defined then extended as:
 *      function Cls(..) { .. }; // Makes class name available.
 *      Model(Cls);
 *  - Classes have a "self" property that references the static class.
 */

/**
 * Mix model functions into a class.
 *
 * Static methods mixed in to classes are documented under {@link Model.statics}.
 * Instance methods are documented under {@link Model.instance}.
 *
 * @param {Object} cls A class to imbue with Model functionality.
 * @param {string[]} properties A list of properties to be stored in a redis hash.
 * @param {Redis} client A redis client to be used for this model.
 * @param {Object} options An object containing options used for this model.
 *
 * @example
 * var redis = require('redis').createClient();
 * function MyModelClass() {}
 * Model(MyModelClass, ['name'], redis);
 *
 * // Create
 * var mdl = new MyModelClass();
 * mdl.name = "John Smith";
 * mdl.save();
 * var id = mdl.id;
 *
 * // Retrieve
 * mdl = MyModelClass.get(id);
 *
 * // Update
 * mdl.name = "John Smythe";
 * mdl.save();
 *
 * // Delete
 * mdl.delete();
 */
function Model(cls, properties, client, options) {
  options = options || {};

  var override = options.override === undefined || options.override === true;

  /* Mix in static methods and properties */
  for (s in Model.statics) {
    if ((cls[s] === undefined) || override) {
      cls[s] = Model.statics[s];
    }
  }

  /* Mix in instance methods and properties */
  for (i in Model.instance) {
    if ((cls.prototype[i] === undefined) || override) {
      cls.prototype[i] = Model.instance[i];
    }
  }

  /* Pre-determine this class' prefix. */
  cls.prefix = options.prefix || cls.name;
  if (cls.prefix != "") {
    if (cls.prefix.charAt(cls.prefix.length - 1) != ':') {
      cls.prefix += ':';
    }
  }

  cls.client = client;
  cls.properties = properties || [];
  cls.prototype.self = cls;
}

/**
 * A list of static properties and functions that are mixed in to classes.
 *
 * The {@link Model} function should be used to perform the mixing.
 *
 * @mixin
 */
Model.statics = {
  /**
   * Get the key for an object id.
   *
   * @param {string} id The ID. If not provided, a new ID is generated.
   * @return {string} The key for the given (possibly new) id.
   */
  getKey: function(id) {
    if (id === undefined) {
      id = this.idgen();
    }

    return this.prefix + id;
  },

  /**
   * Retrieve one or more models by key.
   *
   * @param {string} id The identifier for the object to fetch. An array of ids is also accepted.
   * @param {Model.statics.getCallback} callback The callback receiving the get result(s).
   */
  get: function(id, callback) {
    if (typeof(id) === 'undefined') {
      runCallback(callback, "No ID provided", undefined);
      return;
    }

    var self = this;

    var cb = function(err, result) {
      var mdl;
      if (!err) {
        mdl = self.hydrate(id, result);
      }

      runCallback(callback, err, mdl);
    };

    if (id instanceof Array) {
      var multi = this.client.multi();
      for (var i = 0; i < id.length; i++) {
        multi.hgetall(this.getKey(id[i]));
      }
      multi.exec(cb);
    } else {
      this.client.hgetall(this.getKey(id), cb);
    }
  },
  /**
   * @callback Model.statics.getCallback
   * @param {string} error An error string, or null if no error occurred.
   * @param {Model} result The retrieved model object, or null on error.
   */

  /**
   * Get a list of all object identifiers for this model type.
   *
   * @param {function} callback
   */
  all: function(callback) {
    this.client.smembers(this.getKey('all'), function(err, res) {
        runCallback(callback, err, res);
    })
  },

  /**
   * Build one or more objects from an hgetall or a multi-wrapped hgetall.
   *
   * @param {string} id The unique identifier for the object.
   * @param {Object} result Typically an anonymous hgetall result, or an array of hgetall results.
   */
  hydrate: function(id, result) {
    if (result instanceof Array) {
      var results = [];
      for (var i = 0; i < result.length; i++) {
        results.push(result[i] ? this.hydrate(id[i], result[i]) : result[i]);
      }
      return results;
    } else {
      var mdl = new this();
      mdl.id = id;
      mdl.fromObject(result);
      return mdl;
    }
  },

  /**
   * Purge all objects of this type from redis
   *
   * @param {function} callback
   */
  purge: function(callback) {
    var self = this;
    this.all(function(err, res) {
      if (res) {
        var client = self.client.multi();
        client.del(self.getKey('all'));
        for (var i = 0; i < res.length; i++) {
          client.del(self.getKey(res[i]));
        }
        client.exec(function(err, res) {
          runCallback(callback, err, err ? null : true);
        });
      }
    });
  },

  /**
   * Unique ID generator. Defaults to using UUIDs.
   *
   * @kind function
   * @returns {string} A unique identifier.
   */
  idgen: uuid.v4
};

/**
 * Methods and properties that get mixed into object prototypes.
 *
 * The {@link Model} function should be used to perform the mixing.
 *
 * @mixin
 */
Model.instance = {
  /**
   * Save an object to Redis.
   *
   * @param {function} callback
   * @instance
   */
  save: function(callback) {
    if (!this.id) {
      this.id = this.self.idgen();
    }

    var mdl = this,
      key = this.self.getKey(this.id),
      all = this.self.getKey('all'),
      client = this.self.client.multi();

    client.sadd(all, this.id);
    client.hmset(key, this.toObject());
    client.exec(function(err, result) {
      /* The hmset is the critical result, so err on hmset fail. */
      if (result[1] != 'OK') {
        runCallback(callback, result[1], null);
      } else {
        runCallback(callback, null, mdl);
      }
    });
  },

  /**
   * Remove an object.
   *
   * @param {function} callback Method to be called
   * @instance
   */
  delete: function(callback) {
    var key = this.self.getKey(this.id);
    var all = this.self.getKey('all');
    var client = this.self.client.multi();
    client.srem(all, this.id);
    client.del(key);
    client.exec(function(err, result) {
      /* The hash del is the critical result, so err on del fail. */
      if (result[1] !== 1 && result[1] !==0) {
        runCallback(callback, result[1], null);
      } else {
        runCallback(callback, null, result[1]);
      }
    });
  },

  /**
   * Copy properties from an anonymous object into this object.
   *
   * @param {Object} obj
   * @instance
   */
  fromObject: function(obj) {
    for (var p = 0; p < this.self.properties.length; p++) {
      var property = this.self.properties[p];
      if (typeof(obj[property]) !== "undefined") {
        this[property] = obj[property];
      }
    }
  },

  /**
   * Convert a model to an anonymous object.
   *
   * @returns {Object} An anonymous object with properties and values belonging to this class.
   * @instance
   *
   * @example
   * JSON.stringify(mymodel.toObject());
   */
  toObject: function() {
    dst = {};
    for (var p = 0; p < this.self.properties.length; p++) {
      var property = this.self.properties[p];
      dst[property] = this[property] || "";
    }
    return dst;
  }
};

/**
 * Create a new sequence-based unique ID generator.
 *
 * @param {integer} init The initial value. Defaults to 0.
 * @return {function} An ID generator function.
 * @alias sequence
 *
 * @example
 * var init = 123; // Get initial value somehow.
 * function MyModel() {};
 * Model(MyModel);
 * MyModel.idgen = Model.sequence(init);
 */
Model.sequence = function(init) {
  var sequence = (init !== undefined) ? init : 0;
  return function() { return new String(sequence++).valueOf(); };
}

module.exports = Model;
<commit_msg>Make sure an empty list get returns an empty list.<commit_after>var uuid = require('uuid');
var runCallback = require('./common.js').runCallback;

/**
 * Conventions:
 *  - The "id" property is always used as the unique ID for classes.
 *  - Classes should be defined then extended as:
 *      function Cls(..) { .. }; // Makes class name available.
 *      Model(Cls);
 *  - Classes have a "self" property that references the static class.
 */

/**
 * Mix model functions into a class.
 *
 * Static methods mixed in to classes are documented under {@link Model.statics}.
 * Instance methods are documented under {@link Model.instance}.
 *
 * @param {Object} cls A class to imbue with Model functionality.
 * @param {string[]} properties A list of properties to be stored in a redis hash.
 * @param {Redis} client A redis client to be used for this model.
 * @param {Object} options An object containing options used for this model.
 *
 * @example
 * var redis = require('redis').createClient();
 * function MyModelClass() {}
 * Model(MyModelClass, ['name'], redis);
 *
 * // Create
 * var mdl = new MyModelClass();
 * mdl.name = "John Smith";
 * mdl.save();
 * var id = mdl.id;
 *
 * // Retrieve
 * mdl = MyModelClass.get(id);
 *
 * // Update
 * mdl.name = "John Smythe";
 * mdl.save();
 *
 * // Delete
 * mdl.delete();
 */
function Model(cls, properties, client, options) {
  options = options || {};

  var override = options.override === undefined || options.override === true;

  /* Mix in static methods and properties */
  for (s in Model.statics) {
    if ((cls[s] === undefined) || override) {
      cls[s] = Model.statics[s];
    }
  }

  /* Mix in instance methods and properties */
  for (i in Model.instance) {
    if ((cls.prototype[i] === undefined) || override) {
      cls.prototype[i] = Model.instance[i];
    }
  }

  /* Pre-determine this class' prefix. */
  cls.prefix = options.prefix || cls.name;
  if (cls.prefix != "") {
    if (cls.prefix.charAt(cls.prefix.length - 1) != ':') {
      cls.prefix += ':';
    }
  }

  cls.client = client;
  cls.properties = properties || [];
  cls.prototype.self = cls;
}

/**
 * A list of static properties and functions that are mixed in to classes.
 *
 * The {@link Model} function should be used to perform the mixing.
 *
 * @mixin
 */
Model.statics = {
  /**
   * Get the key for an object id.
   *
   * @param {string} id The ID. If not provided, a new ID is generated.
   * @return {string} The key for the given (possibly new) id.
   */
  getKey: function(id) {
    if (id === undefined) {
      id = this.idgen();
    }

    return this.prefix + id;
  },

  /**
   * Retrieve one or more models by key.
   *
   * @param {string} id The identifier for the object to fetch. An array of ids is also accepted.
   * @param {Model.statics.getCallback} callback The callback receiving the get result(s).
   */
  get: function(id, callback) {
    if (typeof(id) === 'undefined') {
      runCallback(callback, "No ID provided", undefined);
      return;
    }

    var self = this;

    var cb = function(err, result) {
      var mdl;
      if (!err) {
        mdl = self.hydrate(id, result);
      }

      runCallback(callback, err, mdl);
    };

    if (id instanceof Array) {
      /* If for some reason we're not getting anything, this makes sure we return something coherent. */
      if (!id.length) runCallback(callback, null, []);

      var multi = this.client.multi();
      for (var i = 0; i < id.length; i++) {
        multi.hgetall(this.getKey(id[i]));
      }
      multi.exec(cb);
    } else {
      this.client.hgetall(this.getKey(id), cb);
    }
  },
  /**
   * @callback Model.statics.getCallback
   * @param {string} error An error string, or null if no error occurred.
   * @param {Model} result The retrieved model object, or null on error.
   */

  /**
   * Get a list of all object identifiers for this model type.
   *
   * @param {function} callback
   */
  all: function(callback) {
    this.client.smembers(this.getKey('all'), function(err, res) {
        runCallback(callback, err, res);
    })
  },

  /**
   * Build one or more objects from an hgetall or a multi-wrapped hgetall.
   *
   * @param {string} id The unique identifier for the object.
   * @param {Object} result Typically an anonymous hgetall result, or an array of hgetall results.
   */
  hydrate: function(id, result) {
    if (result instanceof Array) {
      var results = [];
      for (var i = 0; i < result.length; i++) {
        results.push(result[i] ? this.hydrate(id[i], result[i]) : result[i]);
      }
      return results;
    } else {
      var mdl = new this();
      mdl.id = id;
      mdl.fromObject(result);
      return mdl;
    }
  },

  /**
   * Purge all objects of this type from redis
   *
   * @param {function} callback
   */
  purge: function(callback) {
    var self = this;
    this.all(function(err, res) {
      if (res) {
        var client = self.client.multi();
        client.del(self.getKey('all'));
        for (var i = 0; i < res.length; i++) {
          client.del(self.getKey(res[i]));
        }
        client.exec(function(err, res) {
          runCallback(callback, err, err ? null : true);
        });
      }
    });
  },

  /**
   * Unique ID generator. Defaults to using UUIDs.
   *
   * @kind function
   * @returns {string} A unique identifier.
   */
  idgen: uuid.v4
};

/**
 * Methods and properties that get mixed into object prototypes.
 *
 * The {@link Model} function should be used to perform the mixing.
 *
 * @mixin
 */
Model.instance = {
  /**
   * Save an object to Redis.
   *
   * @param {function} callback
   * @instance
   */
  save: function(callback) {
    if (!this.id) {
      this.id = this.self.idgen();
    }

    var mdl = this,
      key = this.self.getKey(this.id),
      all = this.self.getKey('all'),
      client = this.self.client.multi();

    client.sadd(all, this.id);
    client.hmset(key, this.toObject());
    client.exec(function(err, result) {
      /* The hmset is the critical result, so err on hmset fail. */
      if (result[1] != 'OK') {
        runCallback(callback, result[1], null);
      } else {
        runCallback(callback, null, mdl);
      }
    });
  },

  /**
   * Remove an object.
   *
   * @param {function} callback Method to be called
   * @instance
   */
  delete: function(callback) {
    var key = this.self.getKey(this.id);
    var all = this.self.getKey('all');
    var client = this.self.client.multi();
    client.srem(all, this.id);
    client.del(key);
    client.exec(function(err, result) {
      /* The hash del is the critical result, so err on del fail. */
      if (result[1] !== 1 && result[1] !==0) {
        runCallback(callback, result[1], null);
      } else {
        runCallback(callback, null, result[1]);
      }
    });
  },

  /**
   * Copy properties from an anonymous object into this object.
   *
   * @param {Object} obj
   * @instance
   */
  fromObject: function(obj) {
    for (var p = 0; p < this.self.properties.length; p++) {
      var property = this.self.properties[p];
      if (typeof(obj[property]) !== "undefined") {
        this[property] = obj[property];
      }
    }
  },

  /**
   * Convert a model to an anonymous object.
   *
   * @returns {Object} An anonymous object with properties and values belonging to this class.
   * @instance
   *
   * @example
   * JSON.stringify(mymodel.toObject());
   */
  toObject: function() {
    dst = {};
    for (var p = 0; p < this.self.properties.length; p++) {
      var property = this.self.properties[p];
      dst[property] = this[property] || "";
    }
    return dst;
  }
};

/**
 * Create a new sequence-based unique ID generator.
 *
 * @param {integer} init The initial value. Defaults to 0.
 * @return {function} An ID generator function.
 * @alias sequence
 *
 * @example
 * var init = 123; // Get initial value somehow.
 * function MyModel() {};
 * Model(MyModel);
 * MyModel.idgen = Model.sequence(init);
 */
Model.sequence = function(init) {
  var sequence = (init !== undefined) ? init : 0;
  return function() { return new String(sequence++).valueOf(); };
}

module.exports = Model;
<|endoftext|>