<commit_before>'use strict';

var http = require('http');
var tape = require('../lib/thaliTape');
var Promise = require('lie');
var EE = require('events').EventEmitter;
var ThaliMobileNativeWrapper =
  require('thali/NextGeneration/thaliMobileNativeWrapper');
var ThaliMobile = require('thali/NextGeneration/thaliMobile');
var platform = require('thali/NextGeneration/utils/platform');

var router = function () {};

var test = tape({
  setup: function (t) {
    t.end();
  },
  teardown: function (t) {
    ThaliMobileNativeWrapper.stop().then(function () {
      t.end();
    }).catch(t.end);
  }
});

function listen(server, port) {
  return new Promise(function (resolve, reject) {
    server.once('error', reject);
    server.listen(port, function () {
      server.removeListener('error', reject);
      resolve(server);
    });
  });
}

function makeRequest(hostname, port, method, path, data) {
  return new Promise(function (resolve, reject) {
    var options = {
      hostname: hostname,
      port: port,
      method: method,
      path: path
    };
    var request = http.request(options, function (response) {
      request.removeListener('error', reject);
      response.once('error', reject);

      var received = [];
      response.on('data', function (chunk) {
        received.push(chunk);
      });
      response.on('end', function () {
        response.removeListener('error', reject);
        var data = Buffer.concat(received);
        resolve(data);
      });
    });

    request.once('error', reject);
    if (data) {
      request.write(data);
    }
    request.end();
  });
}

test('Single local http request', function (t) {
  var server = http.createServer(function (request, response) {
    var reply = request.method + ' ' + request.url;
    response.end(reply);
  });

  listen(server, 0).then(function () {
    var port = server.address().port;
    console.log('listening on %d', port);
    return makeRequest('127.0.0.1', port, 'GET', '/path');
  }).then(function (data) {
    t.equal(data.toString(), 'GET /path');
    return null;
  }).catch(t.fail).then(t.end);
});

test('Multiple local http requests', function (t) {
  var server = http.createServer(function (request, response) {
    var reply = request.method + ' ' + request.url;
    response.end(reply);
  });

  listen(server, 0).then( function () {
    var port = server.address().port;
    console.log('listening on %d', port);
    return Promise.all([
      makeRequest('127.0.0.1', port, 'GET', '/path0'),
      makeRequest('127.0.0.1', port, 'GET', '/path1'),
      makeRequest('127.0.0.1', port, 'GET', '/path2'),
    ]);
  }).then(function (data) {
    t.equal(data[0].toString(), 'GET /path0');
    t.equal(data[1].toString(), 'GET /path1');
    t.equal(data[2].toString(), 'GET /path2');
    return null;
  }).catch(t.fail).then(t.end);
});

var peerFinder = function () {
  var peers = {};
  var ee = new EE();
  var handler = function (peer) {
    if (peer.peerAvailable) {
      peers[peer.peerIdentifier] = peer;
    } else {
      delete peers[peer.peerIdentifier];
    }
    ee.emit('peerschanged');
  };

  function values(obj) {
    return Object.keys(obj).map(function (k) { return obj[k]; });
  }

  function find(numberOfPeers) {
    var keys = Object.keys(peers);
    if (keys.length >= numberOfPeers) {
      return Promise.resolve(values(peers));
    }
    return new Promise(function (resolve) {
      ee.on('peerschanged', function fn () {
        var keys = Object.keys(peers);
        if (keys.length >= numberOfPeers) {
          ee.removeListener('peerschanged', fn);
          resolve(values(peers));
        }
      });
    });
  }

  ThaliMobileNativeWrapper.emitter.on(
    'nonTCPPeerAvailabilityChangedEvent', handler
  );
  find.cleanup = function () {
    ee.removeAllListeners('peerschanged');
    ThaliMobileNativeWrapper.emitter.removeListener(
      'nonTCPPeerAvailabilityChangedEvent', handler
    );
  };

  return find;
};

test('Single coordinated request ios native',
  function () {
    return platform.isAndroid ||
    global.NETWORK_TYPE === ThaliMobile.networkTypes.WIFI;
  },
  function (t) {
    var total = t.participants.length - 1;
    var find = peerFinder();

    var server = http.createServer(function (request, response) {
      var reply = request.method + ' ' + request.url;
      response.end(reply);
    });

    var listenNative = ThaliMobileNativeWrapper.start(router).then(function () {
      return ThaliMobileNativeWrapper.startListeningForAdvertisements();
    });
    var listenServer = listen(server, 0);

    var findPeers = find(total);

    Promise.all([ listenNative, listenServer ])
      .then(function (r) {
        var server = r[1];
        var port = server.address().port;
        console.log('listening on %d', port);
        return ThaliMobileNativeWrapper
          .startUpdateAdvertisingAndListening(port);
      })
      .then(function () {
        return findPeers;
      })
      .then(function (peers) {
        return Promise.all(peers.map(function (peer) {
          return ThaliMobileNativeWrapper._multiConnect(peer.peerIdentifier);
        }));
      })
      .then(function (ports) {
        return Promise.all(ports.map(function (port) {
          return makeRequest('127.0.0.1', port, 'GET', '/path');
        }));
      })
      .then(function (responses) {
        responses.forEach(function (r) {
          t.equal(r.toString(), 'GET /path');
        });
      })
      .catch(t.fail)
      .then(function () {
        find.cleanup();
        t.end();
      });
  });

test('Multiple coordinated request ios native',
  function () {
    return platform.isAndroid ||
    global.NETWORK_TYPE === ThaliMobile.networkTypes.WIFI;
  },
  function (t) {
    var total = t.participants.length - 1;
    var find = peerFinder();

    var server = http.createServer(function (request, response) {
      var reply = request.method + ' ' + request.url;
      response.end(reply);
    });

    var listenNative = ThaliMobileNativeWrapper.start(router).then(function () {
      return ThaliMobileNativeWrapper.startListeningForAdvertisements();
    });
    var listenServer = listen(server, 0);

    var findPeers = find(total);

    Promise.all([ listenNative, listenServer ])
      .then(function (r) {
        var server = r[1];
        var port = server.address().port;
        console.log('listening on %d', port);
        return ThaliMobileNativeWrapper
          .startUpdateAdvertisingAndListening(port);
      })
      .then(function () {
        return findPeers;
      })
      .then(function (peers) {
        return Promise.all(peers.map(function (peer) {
          return ThaliMobileNativeWrapper._multiConnect(peer.peerIdentifier);
        }));
      })
      .then(function (ports) {
        return Promise.all(ports.map(function (port) {
          return Promise.all([
            makeRequest('127.0.0.1', port, 'GET', '/path0'),
            makeRequest('127.0.0.1', port, 'GET', '/path1'),
            makeRequest('127.0.0.1', port, 'GET', '/path2'),
          ]);
        }));
      })
      .then(function (responses) {
        responses.forEach(function (r) {
          t.equal(r[0].toString(), 'GET /path0');
          t.equal(r[1].toString(), 'GET /path1');
          t.equal(r[2].toString(), 'GET /path2');
        });
      })
      .catch(t.fail)
      .then(function () {
        find.cleanup();
        t.end();
      });
  });
<commit_msg>Update tests according to the pr #1855<commit_after>'use strict';

var http = require('http');
var tape = require('../lib/thaliTape');
var Promise = require('lie');
var EventEmitter = require('events').EventEmitter;
var ThaliMobileNativeWrapper =
  require('thali/NextGeneration/thaliMobileNativeWrapper');
var ThaliMobile = require('thali/NextGeneration/thaliMobile');
var platform = require('thali/NextGeneration/utils/platform');

var router = function () {};

var test = tape({
  setup: function (t) {
    t.end();
  },
  teardown: function (t) {
    ThaliMobileNativeWrapper.stop().then(function () {
      t.end();
    }).catch(t.end);
  }
});

function serverIsListening(server, port) {
  return new Promise(function (resolve, reject) {
    server.once('error', reject);
    server.listen(port, function () {
      server.removeListener('error', reject);
      resolve(server);
    });
  });
}

function makeRequest(hostname, port, method, path, requestData) {
  return new Promise(function (resolve, reject) {
    var options = {
      hostname: hostname,
      port: port,
      method: method,
      path: path
    };
    var request = http.request(options, function (response) {
      request.removeListener('error', reject);
      response.once('error', reject);

      var received = [];
      response.on('data', function (chunk) {
        received.push(chunk);
      });
      response.on('end', function () {
        response.removeListener('error', reject);
        var responseData = Buffer.concat(received);
        resolve(responseData);
      });
    });

    request.once('error', reject);
    if (requestData) {
      request.write(requestData);
    }
    request.end();
  });
}

test('Single local http request', function (t) {
  var server = http.createServer(function (request, response) {
    var reply = request.method + ' ' + request.url;
    response.end(reply);
  });

  serverIsListening(server, 0).then(function () {
    var port = server.address().port;
    console.log('listening on %d', port);
    return makeRequest('127.0.0.1', port, 'GET', '/path');
  }).then(function (data) {
    t.equal(data.toString(), 'GET /path');
    return null;
  }).catch(t.fail).then(t.end);
});

test('Multiple local http requests', function (t) {
  var server = http.createServer(function (request, response) {
    var reply = request.method + ' ' + request.url;
    response.end(reply);
  });

  serverIsListening(server, 0).then( function () {
    var port = server.address().port;
    console.log('listening on %d', port);
    return Promise.all([
      makeRequest('127.0.0.1', port, 'GET', '/path0'),
      makeRequest('127.0.0.1', port, 'GET', '/path1'),
      makeRequest('127.0.0.1', port, 'GET', '/path2'),
    ]);
  }).then(function (data) {
    t.equal(data[0].toString(), 'GET /path0');
    t.equal(data[1].toString(), 'GET /path1');
    t.equal(data[2].toString(), 'GET /path2');
    return null;
  }).catch(t.fail).then(t.end);
});

var peerFinder = function () {
  var peers = {};
  var eventEmitter = new EventEmitter();
  var handler = function (peer) {
    if (peer.peerAvailable) {
      peers[peer.peerIdentifier] = peer;
    } else {
      delete peers[peer.peerIdentifier];
    }
    eventEmitter.emit('peerschanged');
  };

  function values(obj) {
    return Object.keys(obj).map(function (k) { return obj[k]; });
  }

  function find(numberOfPeers) {
    var keys = Object.keys(peers);
    if (keys.length >= numberOfPeers) {
      return Promise.resolve(values(peers));
    }
    return new Promise(function (resolve) {
      eventEmitter.on('peerschanged', function fn () {
        var keys = Object.keys(peers);
        if (keys.length >= numberOfPeers) {
          eventEmitter.removeListener('peerschanged', fn);
          resolve(values(peers));
        }
      });
    });
  }

  ThaliMobileNativeWrapper.emitter.on(
    'nonTCPPeerAvailabilityChangedEvent', handler
  );
  find.cleanup = function () {
    eventEmitter.removeAllListeners('peerschanged');
    ThaliMobileNativeWrapper.emitter.removeListener(
      'nonTCPPeerAvailabilityChangedEvent', handler
    );
  };

  return find;
};

test('Single coordinated request ios native',
  function () {
    return platform.isAndroid ||
    global.NETWORK_TYPE === ThaliMobile.networkTypes.WIFI;
  },
  function (t) {
    var total = t.participants.length - 1;
    var find = peerFinder();

    var server = http.createServer(function (request, response) {
      var reply = request.method + ' ' + request.url;
      response.end(reply);
    });

    var listenNative = ThaliMobileNativeWrapper.start(router).then(function () {
      return ThaliMobileNativeWrapper.startListeningForAdvertisements();
    });
    var listenServer = serverIsListening(server, 0);

    var findPeers = find(total);

    Promise.all([ listenNative, listenServer ])
      .then(function (allResponses) {
        var server = allResponses[1];
        var port = server.address().port;
        console.log('listening on %d', port);
        return ThaliMobileNativeWrapper
          .startUpdateAdvertisingAndListening(port);
      })
      .then(function () {
        return findPeers;
      })
      .then(function (peers) {
        return Promise.all(peers.map(function (peer) {
          return ThaliMobileNativeWrapper._multiConnect(peer.peerIdentifier);
        }));
      })
      .then(function (ports) {
        return Promise.all(ports.map(function (port) {
          return makeRequest('127.0.0.1', port, 'GET', '/path');
        }));
      })
      .then(function (responses) {
        responses.forEach(function (r) {
          t.equal(r.toString(), 'GET /path');
        });
      })
      .catch(t.fail)
      .then(function () {
        find.cleanup();
        t.end();
      });
  });

test('Multiple coordinated request ios native',
  function () {
    return platform.isAndroid ||
    global.NETWORK_TYPE === ThaliMobile.networkTypes.WIFI;
  },
  function (t) {
    var total = t.participants.length - 1;
    var find = peerFinder();

    var server = http.createServer(function (request, response) {
      var reply = request.method + ' ' + request.url;
      response.end(reply);
    });

    var listenNative = ThaliMobileNativeWrapper.start(router).then(function () {
      return ThaliMobileNativeWrapper.startListeningForAdvertisements();
    });
    var listenServer = serverIsListening(server, 0);

    var findPeers = find(total);

    Promise.all([ listenNative, listenServer ])
      .then(function (allResponses) {
        var server = allResponses[1];
        var port = server.address().port;
        console.log('listening on %d', port);
        return ThaliMobileNativeWrapper
          .startUpdateAdvertisingAndListening(port);
      })
      .then(function () {
        return findPeers;
      })
      .then(function (peers) {
        return Promise.all(peers.map(function (peer) {
          return ThaliMobileNativeWrapper._multiConnect(peer.peerIdentifier);
        }));
      })
      .then(function (ports) {
        return Promise.all(ports.map(function (port) {
          return Promise.all([
            makeRequest('127.0.0.1', port, 'GET', '/path0'),
            makeRequest('127.0.0.1', port, 'GET', '/path1'),
            makeRequest('127.0.0.1', port, 'GET', '/path2'),
          ]);
        }));
      })
      .then(function (responses) {
        responses.forEach(function (r) {
          t.equal(r[0].toString(), 'GET /path0');
          t.equal(r[1].toString(), 'GET /path1');
          t.equal(r[2].toString(), 'GET /path2');
        });
      })
      .catch(t.fail)
      .then(function () {
        find.cleanup();
        t.end();
      });
  });
<|endoftext|>


<commit_before>import http from 'http';
import axios from 'axios';
import isString from 'lodash.isstring';
import isPlainObject from 'lodash.isplainobject';
import isRegExp from 'lodash.isregexp';
import isFunction from 'lodash.isfunction';
import debugFactory from 'debug';
import { createExpressMiddleware } from './express-middleware';
import { packageIdentifier } from './util';

const debug = debugFactory('@slack/interactive-messages:adapter');

/**
 * Transforms various forms of matching constraints to a single standard object shape
 * @param {string|RegExp|Object} matchingConstraints - the various forms of matching constraints
 * accepted
 * @returns {Object} - an object where each matching constraint is a property
 */
function formatMatchingConstraints(matchingConstraints) {
  let ret = {};
  if (!isPlainObject(matchingConstraints)) {
    ret.callbackId = matchingConstraints;
  } else {
    ret = Object.assign({}, matchingConstraints);
  }
  return ret;
}

/**
 * Validates general properties of a matching constraints object
 * @param {Object} matchingConstraints - object describing the constraints on a callback
 * @return {Error|false} - a false value represents successful validation, otherwise an error to
 * describe why validation failed.
 */
function validateConstraints(matchingConstraints) {
  if (matchingConstraints.callbackId &&
      !(isString(matchingConstraints.callbackId) || isRegExp(matchingConstraints.callbackId))) {
    return new TypeError('Callback ID must be a string or RegExp');
  }

  return false;
}

/**
 * Validates properties of a matching constraints object specific to registering an action
 * @param {Object} matchingConstraints - object describing the constraints on a callback
 * @return {Error|false} - a false value represents successful validation, otherwise an error to
 * describe why validation failed.
 */
function validateActionConstraints(actionConstraints) {
  if (actionConstraints.type &&
      !(actionConstraints.type === 'select' || actionConstraints.type === 'button')) {
    return new TypeError('Type must be \'select\' or \'button\'');
  }

  // We don't need to validate unfurl, we'll just cooerce it to a boolean
  return false;
}

export default class SlackMessageAdapter {
  /**
   * Create a message adapter.
   *
   * @param {string} verificationToken - Slack app verification token used to authenticate request
   */
  constructor(verificationToken) {
    if (!isString(verificationToken)) {
      throw new TypeError('SlackMessageAdapter needs a verification token');
    }

    this.verificationToken = verificationToken;
    this.callbacks = [];
    this.axios = axios.create({
      headers: {
        'User-Agent': packageIdentifier(),
      },
    });

    debug('instantiated');
  }

  /**
   * Create a server that's ready to serve requests from Slack's interactive messages.
   *
   * @param {string} [path=/slack/actions] - The path portion of the URL where the server will
   * listen for requests from Slack's interactive messages.
   */
  createServer(path = '/slack/actions') {
    // TODO: more options (like https)
    return Promise.resolve().then(() => Promise.all([
      import('express'),
      import('body-parser'),
    ]))
    .then(([express, bodyParser]) => {
      const app = express();
      app.use(bodyParser.urlencoded({ extended: false }));
      app.post(path, this.expressMiddleware());

      debug('server created - path: %s', path);

      return http.createServer(app);
    });
  }

  start(port) {
    return this.createServer()
      .then(server => new Promise((resolve, reject) => {
        this.server = server;
        server.on('error', reject);
        server.listen(port, () => resolve(server));
        debug('server started - port: %s', port);
      }));
  }

  stop() {
    return new Promise((resolve, reject) => {
      if (this.server) {
        this.server.close((error) => {
          delete this.server;
          if (error) {
            reject(error);
          } else {
            resolve();
          }
        });
      } else {
        reject(new Error('SlackMessageAdapter cannot stop when it did not start a server'));
      }
    });
  }

  expressMiddleware() {
    return createExpressMiddleware(this);
  }

  action(matchingConstraints, callback) {
    const actionConstraints = formatMatchingConstraints(matchingConstraints);

    const error = validateConstraints(actionConstraints) ||
      validateActionConstraints(actionConstraints);
    if (error) {
      debug('action could not be registered: %s', error.message);
      throw error;
    }

    return this.registerCallback(actionConstraints, callback);
  }

  options(matchingConstraints, callback) {
    const optionsConstraints = formatMatchingConstraints(matchingConstraints);

    const error = validateConstraints(optionsConstraints);
    if (error) {
      debug('options could not be registered: %s', error.message);
      throw error;
    }

    return this.registerCallback(optionsConstraints, callback);
  }

  /* @private */

  registerCallback(constraints, callback) {
    // Validation
    if (!isFunction(callback)) {
      debug('did not register callback because its not a function');
      throw new TypeError('callback must be a function');
    }

    this.callbacks.push([constraints, callback]);

    return this;
  }

  dispatch(payload) {
    const action = payload.actions && payload.actions[0];
    let result = { status: 200 };
    const respond = (message) => {
      debug('sending async response');
      return this.axios.post(payload.response_url, message);
    };

    this.callbacks.some(([constraints, fn]) => {
      // Returning false in this function continues the iteration, and returning true ends it.
      // The pattern is that we assign a value to `result` and then return true. We only desire one
      // result for the response.
      let callbackResult;

      if (constraints.callbackId) {
        if (isString(constraints.callbackId) && payload.callback_id !== constraints.callbackId) {
          return false;
        }
        if (isRegExp(constraints.callbackId) && !constraints.callbackId.text(payload.callback_id)) {
          return false;
        }
      }

      if (action && constraints.type && constraints.type !== action.type) {
        return false;
      }

      if ('unfurl' in constraints &&
           (
             (constraints.unfurl && !payload.is_app_unfurl) ||
             (!constraints.unfurl && payload.is_app_unfurl)
           )
         ) {
        return false;
      }

      try {
        callbackResult = fn.call(this, payload, respond);
      } catch (error) {
        debug('callback error: %o', error);
        result = { status: 500 };
        return true;
      }

      if (callbackResult) {
        // Checking for Promise type
        if (typeof callbackResult.then === 'function') {
          callbackResult.then(respond).catch((error) => {
            debug('async error for callback. callback_id: %s, error: %s',
                  payload.callback_id, error.message);
          });
          return true;
        }
        result = { status: 200, content: callbackResult };
        return true;
      }
      return true;
    });

    return result;
  }
}
<commit_msg>change 1 letter, fix 1 regex<commit_after>import http from 'http';
import axios from 'axios';
import isString from 'lodash.isstring';
import isPlainObject from 'lodash.isplainobject';
import isRegExp from 'lodash.isregexp';
import isFunction from 'lodash.isfunction';
import debugFactory from 'debug';
import { createExpressMiddleware } from './express-middleware';
import { packageIdentifier } from './util';

const debug = debugFactory('@slack/interactive-messages:adapter');

/**
 * Transforms various forms of matching constraints to a single standard object shape
 * @param {string|RegExp|Object} matchingConstraints - the various forms of matching constraints
 * accepted
 * @returns {Object} - an object where each matching constraint is a property
 */
function formatMatchingConstraints(matchingConstraints) {
  let ret = {};
  if (!isPlainObject(matchingConstraints)) {
    ret.callbackId = matchingConstraints;
  } else {
    ret = Object.assign({}, matchingConstraints);
  }
  return ret;
}

/**
 * Validates general properties of a matching constraints object
 * @param {Object} matchingConstraints - object describing the constraints on a callback
 * @return {Error|false} - a false value represents successful validation, otherwise an error to
 * describe why validation failed.
 */
function validateConstraints(matchingConstraints) {
  if (matchingConstraints.callbackId &&
      !(isString(matchingConstraints.callbackId) || isRegExp(matchingConstraints.callbackId))) {
    return new TypeError('Callback ID must be a string or RegExp');
  }

  return false;
}

/**
 * Validates properties of a matching constraints object specific to registering an action
 * @param {Object} matchingConstraints - object describing the constraints on a callback
 * @return {Error|false} - a false value represents successful validation, otherwise an error to
 * describe why validation failed.
 */
function validateActionConstraints(actionConstraints) {
  if (actionConstraints.type &&
      !(actionConstraints.type === 'select' || actionConstraints.type === 'button')) {
    return new TypeError('Type must be \'select\' or \'button\'');
  }

  // We don't need to validate unfurl, we'll just cooerce it to a boolean
  return false;
}

export default class SlackMessageAdapter {
  /**
   * Create a message adapter.
   *
   * @param {string} verificationToken - Slack app verification token used to authenticate request
   */
  constructor(verificationToken) {
    if (!isString(verificationToken)) {
      throw new TypeError('SlackMessageAdapter needs a verification token');
    }

    this.verificationToken = verificationToken;
    this.callbacks = [];
    this.axios = axios.create({
      headers: {
        'User-Agent': packageIdentifier(),
      },
    });

    debug('instantiated');
  }

  /**
   * Create a server that's ready to serve requests from Slack's interactive messages.
   *
   * @param {string} [path=/slack/actions] - The path portion of the URL where the server will
   * listen for requests from Slack's interactive messages.
   */
  createServer(path = '/slack/actions') {
    // TODO: more options (like https)
    return Promise.resolve().then(() => Promise.all([
      import('express'),
      import('body-parser'),
    ]))
    .then(([express, bodyParser]) => {
      const app = express();
      app.use(bodyParser.urlencoded({ extended: false }));
      app.post(path, this.expressMiddleware());

      debug('server created - path: %s', path);

      return http.createServer(app);
    });
  }

  start(port) {
    return this.createServer()
      .then(server => new Promise((resolve, reject) => {
        this.server = server;
        server.on('error', reject);
        server.listen(port, () => resolve(server));
        debug('server started - port: %s', port);
      }));
  }

  stop() {
    return new Promise((resolve, reject) => {
      if (this.server) {
        this.server.close((error) => {
          delete this.server;
          if (error) {
            reject(error);
          } else {
            resolve();
          }
        });
      } else {
        reject(new Error('SlackMessageAdapter cannot stop when it did not start a server'));
      }
    });
  }

  expressMiddleware() {
    return createExpressMiddleware(this);
  }

  action(matchingConstraints, callback) {
    const actionConstraints = formatMatchingConstraints(matchingConstraints);

    const error = validateConstraints(actionConstraints) ||
      validateActionConstraints(actionConstraints);
    if (error) {
      debug('action could not be registered: %s', error.message);
      throw error;
    }

    return this.registerCallback(actionConstraints, callback);
  }

  options(matchingConstraints, callback) {
    const optionsConstraints = formatMatchingConstraints(matchingConstraints);

    const error = validateConstraints(optionsConstraints);
    if (error) {
      debug('options could not be registered: %s', error.message);
      throw error;
    }

    return this.registerCallback(optionsConstraints, callback);
  }

  /* @private */

  registerCallback(constraints, callback) {
    // Validation
    if (!isFunction(callback)) {
      debug('did not register callback because its not a function');
      throw new TypeError('callback must be a function');
    }

    this.callbacks.push([constraints, callback]);

    return this;
  }

  dispatch(payload) {
    const action = payload.actions && payload.actions[0];
    let result = { status: 200 };
    const respond = (message) => {
      debug('sending async response');
      return this.axios.post(payload.response_url, message);
    };

    this.callbacks.some(([constraints, fn]) => {
      // Returning false in this function continues the iteration, and returning true ends it.
      // The pattern is that we assign a value to `result` and then return true. We only desire one
      // result for the response.
      let callbackResult;

      if (constraints.callbackId) {
        if (isString(constraints.callbackId) && payload.callback_id !== constraints.callbackId) {
          return false;
        }
        if (isRegExp(constraints.callbackId) && !constraints.callbackId.test(payload.callback_id)) {
          return false;
        }
      }

      if (action && constraints.type && constraints.type !== action.type) {
        return false;
      }

      if ('unfurl' in constraints &&
           (
             (constraints.unfurl && !payload.is_app_unfurl) ||
             (!constraints.unfurl && payload.is_app_unfurl)
           )
         ) {
        return false;
      }

      try {
        callbackResult = fn.call(this, payload, respond);
      } catch (error) {
        debug('callback error: %o', error);
        result = { status: 500 };
        return true;
      }

      if (callbackResult) {
        // Checking for Promise type
        if (typeof callbackResult.then === 'function') {
          callbackResult.then(respond).catch((error) => {
            debug('async error for callback. callback_id: %s, error: %s',
                  payload.callback_id, error.message);
          });
          return true;
        }
        result = { status: 200, content: callbackResult };
        return true;
      }
      return true;
    });

    return result;
  }
}
<|endoftext|>


<commit_before>//
// DynamoDB wrapper
//
var _ = require('lodash');
var async = require('async');
var aws = require('aws-sdk');
var errors = require('./errors');

function keyName(obj) {
  return Object.getOwnPropertyNames(obj)[0];
}

function keyValue(obj) {
  return obj[keyName(obj)];
}

var Table = function(name) {
  this.name = name;
  this.dynamo = new aws.DynamoDB.DocumentClient();
};

/**
 * Lists items with a given hash key and optional range of range keys.
 *
 * Params:
 *   Key: the hash key for items to list (required)
 *   MinRangeKey: the minimum range key to return, inclusive
 *   MaxRangeKey: the maximum range key to return, inclusive
 *   Limit: maximum items to return
 *   ScanIndexForward: if false, will return results in descending order. If true (default),
 *     it will return results in ascending order
 *   ConsistentRead: if false, the operation will use eventually consistent reads. If true (default),
 *     the operation will use strongly consistent reads
 *   IndexName: The name of an index to query. This index can be any local secondary index or
 *     global secondary index on the table.
 */
Table.prototype.list = function(params, callback) {
  var expression = '#hashKey=:hashKey';
  var attributeNames = { '#hashKey': keyName(params.Key) };
  var attributeValues = { ':hashKey': keyValue(params.Key) };
  var scanIndexForward = true;
  var consistentRead = true;
  if (params.MinRangeKey && params.MaxRangeKey) {
    expression += ' AND #rangeKey BETWEEN :minRangeKey AND :maxRangeKey';
    attributeNames['#rangeKey'] = keyName(params.MinRangeKey);
    attributeValues[':minRangeKey'] = keyValue(params.MinRangeKey);
    attributeValues[':maxRangeKey'] = keyValue(params.MaxRangeKey);
  } else if (params.MinRangeKey) {
    expression += ' AND #rangeKey>=:minRangeKey';
    attributeNames['#rangeKey'] = keyName(params.MinRangeKey);
    attributeValues[':minRangeKey'] = keyValue(params.MinRangeKey);
  } else if (params.MaxRangeKey) {
    expression += ' AND #rangeKey<=:maxRangeKey';
    attributeNames['#rangeKey'] = keyName(params.MaxRangeKey);
    attributeValues[':maxRangeKey'] = keyValue(params.MaxRangeKey);
  }
  if (params.ScanIndexForward !== undefined) {
    scanIndexForward = params.ScanIndexForward;
  };
  if (params.ConsistentRead !== undefined) {
    consistentRead = params.ConsistentRead;
  };
  var queryParams = {
    TableName: this.name,
    KeyConditionExpression: expression,
    ExpressionAttributeNames: attributeNames,
    ExpressionAttributeValues: attributeValues,
    ConsistentRead: consistentRead,
    ScanIndexForward: scanIndexForward,
    Limit: params.Limit || 100,
  };
  if (params.IndexName) {
    queryParams.IndexName = params.IndexName;
    delete queryParams.ConsistentRead;
  };
  this.dynamo.query(queryParams, function(err, result) {
    callback(err, err ? undefined : result.Items || []);
  });
};

/**
 * Returns the item for a single key from the table.
 *
 * Params:
 *   Key: the key to use for the lookup (can contain both a hash key and range key)
 *   AllowMissing: if true, succeeds with null if the item is not found; if false (default),
 *     fails with a not found error.
 */
Table.prototype.get = function(params, callback) {
  this.dynamo.get({
    TableName: this.name,
    Key: params.Key
  }, function(err, result) {
    if (err) {
      callback(err);
    } else if (!result.Item && !params.AllowMissing) {
      callback(errors.notFound());
    } else {
      callback(null, result.Item ? result.Item : null);
    }
  });
};

/**
 * Returns items for multiple keys. This is limited to 100 keys at a time due to DynamoDB limits.
 *
 * Params:
 *   Keys: array of keys to fetch
 */
Table.prototype.batchGet = function(params, callback) {
  var keys = params.Keys || [];
  if (keys.length === 0) {
    callback(null, []);
    return;
  }
  var tableName = this.name;
  var getParams = { RequestItems: {} };
  getParams.RequestItems[tableName] = { Keys: keys };

  this.dynamo.batchGet(getParams, function(err, result) {
    if (err) {
      callback(err);
    } else if (result.UnprocessedKeys && result.UnprocessedKeys[tableName]) {
      // TODO(kito): we should retry these.
      callback(errors.serverError('Unprocessed Keys'));
    } else {
      callback(null, result.Responses[tableName] || []);
    }
  });
};

/**
 * Writes an item to the database, invoking the callback with the written item on success.
 *
 * Params:
 *   Item: the item to write.
 *   AllowOverwrite: if true, existing items can be overwritten; if false (default), attempting
 *     to overwrite an existing item will fail with a conflict error.
 */
Table.prototype.put = function(params, callback) {
  var item = _.cloneDeep(params.Item || {});
  item.createdAt = _.now();
  item.modifiedAt = item.createdAt;

  var putArgs = {
    TableName: this.name,
    Item: item,
  };
  if (!params.AllowOverwrite) {
    putArgs.ConditionExpression = 'attribute_not_exists(createdAt)';
  }
  this.dynamo.put(putArgs, function(err, result) {
    if (err && err.name == 'ConditionalCheckFailedException') {
      callback(errors.conflict());
    } else {
      callback(err, item);
    }
  });
};

/**
 * Atomically updates properties on an item. Fails with a not found error if the item does not
 * exist.
 *
 * Params:
 *   Key: key of the object to update
 *   Update: dictionary of fields to update and their new values
 */
Table.prototype.patch = function(params, callback) {
  var updates = params.Update || {};
  var names = Object.getOwnPropertyNames(updates);
  if (names.indexOf('createdAt') >= 0 || names.indexOf('modifiedAt') >= 0) {
    callback(errors.badRequest('Cannot patch createdAt or modifiedAt'));
    return;
  } else if (names.length === 0) {
    callback(errors.badRequest('No updates specified'));
    return;
  }

  // Always update the modified time.
  var expression = 'SET modifiedAt=:modifiedAt';
  var attributeNames = {};
  var attributeValues = { ':modifiedAt': new Date().getTime() };

  // Add to the expression for each of the updates:
  for (var i = 0; i < names.length; i++) {
    var placeholder = 'x' + i;
    expression += ', #' + placeholder + '=:' + placeholder;
    attributeNames['#' + placeholder] = names[i];
    attributeValues[':' + placeholder] = updates[names[i]];
  }

  this.dynamo.update({
    TableName: this.name,
    Key: params.Key,
    ConditionExpression: 'attribute_exists(createdAt)',
    UpdateExpression: expression,
    ExpressionAttributeNames: attributeNames,
    ExpressionAttributeValues: attributeValues,
    ReturnValues: 'ALL_NEW',
  }, function(err, result) {
    if (err && err.name == 'ConditionalCheckFailedException') {
      err = errors.notFound();
    }
    callback(err, result ? result.Attributes : undefined);
  });
};

/**
 * Atomically increments (or decrements) numeric fields on an item. Invokes callback with a
 * dictionary mapping each of the incremented fields to its new value. Fails with a not found
 * error if the item does not exist.
 *
 * Params:
 *   Key: key of the item to update
 *   Amount: dictionary of fields to updates and increment amounts (numeric)
 */
 Table.prototype.increment = function(params, callback) {
  var amounts = params.Amount || {};
  var names = Object.getOwnPropertyNames(amounts);
  if (names.indexOf('createdAt') >= 0 || names.indexOf('modifiedAt') >= 0) {
    callback(errors.badRequest('Cannot increment createdAt or modifiedAt'));
    return;
  } else if (names.length === 0) {
    callback(errors.badRequest('No amounts specified'));
    return;
  }

  // Always update the modified time.
  var expression = 'SET modifiedAt=:modifiedAt ADD ';
  var attributeNames = {};
  var attributeValues = { ':modifiedAt': new Date().getTime() };

  // Add to the expression for each of the amounts:
  for (var i = 0; i < names.length; i++) {
    var placeholder = 'x' + i;
    expression += ((i > 0) ? ', ' : '') + '#' + placeholder + ' :' + placeholder;
    attributeNames['#' + placeholder] = names[i];
    attributeValues[':' + placeholder] = amounts[names[i]];
  }

  this.dynamo.update({
    TableName: this.name,
    Key: params.Key,
    ConditionExpression: 'attribute_exists(createdAt)',
    UpdateExpression: expression,
    ExpressionAttributeNames: attributeNames,
    ExpressionAttributeValues: attributeValues,
    ReturnValues: 'ALL_NEW',
  }, function(err, result) {
    if (err && err.name == 'ConditionalCheckFailedException') {
      err = errors.notFound();
    }
    if (err) {
      callback(err);
    } else {
      var newValues = result.Attributes;
      delete(newValues.modifiedAt);
      callback(null, newValues);
    }
  });
};

/**
 * Deletes an item from the database. If the item is not found, it succeeds anyway.
 *
 * Params:
 *   Key: key of the item to delete.
 */
Table.prototype.delete = function(params, callback) {
  this.dynamo.delete({
    TableName: this.name,
    Key: params.Key
  }, function(err, result) {
    callback(err);
  });
};

var ACL = function(name) {
  this.name = name;
  this.dynamo = new aws.DynamoDB.DocumentClient();
};

/**
 * Returns the ACL entry for a given user and resource.
 *
 * Params:
 *   User: the user ID
 *   Resource: the resource identifier. This can be whatever your application wants.
 */
ACL.prototype.get = function(params, callback) {
  var key = { userID: params.User, res: params.Resource };
  this.dynamo.get({ TableName: this.name, Key: key }, function(err, result) {
    if (err) {
      callback(err);
    } else {
      callback(null, result.Item ? result.Item : key);
    }
  });
};

/**
 * Fails with a forbidden error if the user does not have all of the listed permissions on the
 * resource.
 *
 * Params:
 *   User: the user ID
 *   Resource: the resource identifier
 *   Permissions: array of permissions (strings).
 */
ACL.prototype.check = function(params, callback) {
  this.get(params, function(err, item) {
    if (!err) {
      for (var i = 0; i < params.Permissions.length; i++) {
        if (!item[params.Permissions[i]]) {
          err = errors.forbidden();
          break;
        }
      }
    }
    callback(err);
  });
};

/**
 * Grants the given list of permissions on the given resource to the given user.
 *
 * Params:
 *   User: the user ID
 *   Resource: the resource identifier
 *   Permissions: array of permissions (strings)
 */
ACL.prototype.grant = function(params, callback) {
  var expression = '';
  var attributeNames = {};
  var attributeValues = {};
  for (var i = 0; i < params.Permissions.length; i++) {
    var placeholder = 'p' + i;
    expression += (expression.length ? ', ' : 'SET ') + '#' + placeholder + '=:' + placeholder;
    attributeNames['#' + placeholder] = params.Permissions[i];
    attributeValues[':' + placeholder] = 1;
  }
  this.dynamo.update({
    TableName: this.name,
    Key: { userID: params.User, res: params.Resource },
    UpdateExpression: expression,
    ExpressionAttributeNames: attributeNames,
    ExpressionAttributeValues: attributeValues,
  }, function(err, result) {
    callback(err);
  });
};

/**
 * Revokes all permissions on the given resource from the given user.
 *
 * Params:
 *   User: the user ID
 *   Resource: the resource identifier
 */
ACL.prototype.revoke = function(params, callback) {
  var key = { userID: params.User, res: params.Resource };
  this.dynamo.delete({ TableName: this.name, Key: key }, function(err, result) {
    callback(err);
  });
};

function deleteGrants(dynamo, table, grants, callback) {
  // Form batches of 25 grants:
  var batches = [];
  while (grants.length) {
    batches.push(grants.splice(0, 25));
  }

  // Generate tasks that perform a dynamodb batch write for each batch:
  var tasks = batches.map(function(batch) {
    return function(callback) {
      var items = {};
      items[table] = batch.map(function(grant) {
        var key = { userID: grant.userID, res: grant.res };
        return { DeleteRequest: { Key: key } };
      });
      dynamo.batchWrite({ RequestItems: items }, function(err, result) {
        if (result && result.UnprocessedItems && result.UnprocessedItems[table]) {
          // TODO(kito): we should retry these
          err = errors.serverError('Unprocessed Items');
        }
        callback(err);
      });
    };
  });

  // Run all tasks in parallel:
  async.parallel(tasks, function(err) { callback(err); });
}

/**
 * Revokes all permissions for all users on the given resource.
 *
 * Params:
 *   Resource: the resource identifier
 */
ACL.prototype.revokeAll = function(params, callback) {
  var self = this;
  async.waterfall([
    function(next) { self.grantsForResource(params, next); },
    function(grants, next) { deleteGrants(self.dynamo, self.name, grants, next); }
  ], callback);
};

/**
 * Returns all grants for a given user, in descending order of resource ID with optional arguments.
 *
 * Params:
 *   User: the user ID
 *   MinResource: minimum resource to return (inclusive)
 *   MaxResource: maximum resource to return (inclusive)
 *   Limit: maximum number of grants to return
 */
ACL.prototype.grantsForUser = function(params, callback) {
  var expression = 'userID=:userID';
  var attributeValues = { ':userID': params.User };
  if (params.MinResource && params.MaxResource) {
    expression += ' AND res BETWEEN :minResource AND :maxResource';
    attributeValues[':minResource'] = params.MinResource;
    attributeValues[':maxResource'] = params.MaxResource;
  } else if (params.MinResource) {
    expression += ' AND res>=:minResource';
    attributeValues[':minResource'] = params.MinResource;
  } else if (params.MaxResource) {
    expression += ' AND res<=:maxResource';
    attributeValues[':maxResource'] = params.MaxResource;
  }
  this.dynamo.query({
    TableName: this.name,
    KeyConditionExpression: expression,
    ExpressionAttributeValues: attributeValues,
    ConsistentRead: true,
    ScanIndexForward: false,
    Limit: params.Limit || 10
  }, function(err, result) {
    callback(err, result ? result.Items : []);
  });
};

/**
 * Returns all grants for a given resource.
 *
 * Params:
 *   Resource: the resource ID
 */
ACL.prototype.grantsForResource = function(params, callback) {
  this.dynamo.query({
    TableName: this.name,
    IndexName: 'by-resource',
    KeyConditionExpression: 'res=:res',
    ExpressionAttributeValues: { ':res': params.Resource },
    ConsistentRead: false,
    ScanIndexForward: true,
  }, function(err, result) {
    callback(err, result ? result.Items : []);
  });
};

var Database = {
  /** Returns an object providing access to a DynamoDB table with given name. */
  table: function(name) {
    return new Table(name);
  },

  /** Returns an object providing access to the ACL with given name. */
  acl: function(name) {
    return new ACL(name);
  },
};

module.exports = Database;
<commit_msg>fixed tests<commit_after>//
// DynamoDB wrapper
//
var _ = require('lodash');
var async = require('async');
var aws = require('aws-sdk');
var errors = require('./errors');

function keyName(obj) {
  return Object.getOwnPropertyNames(obj)[0];
}

function keyValue(obj) {
  return obj[keyName(obj)];
}

var Table = function(name) {
  this.name = name;
  this.dynamo = new aws.DynamoDB.DocumentClient();
};

/**
 * Lists items with a given hash key and optional range of range keys.
 *
 * Params:
 *   Key: the hash key for items to list (required)
 *   MinRangeKey: the minimum range key to return, inclusive
 *   MaxRangeKey: the maximum range key to return, inclusive
 *   Limit: maximum items to return
 *   ScanIndexForward: if false, will return results in descending order. If true (default),
 *     it will return results in ascending order
 *   ConsistentRead: if false, the operation will use eventually consistent reads. If true (default),
 *     the operation will use strongly consistent reads
 *   IndexName: The name of an index to query. This index can be any local secondary index or
 *     global secondary index on the table.
 */
Table.prototype.list = function(params, callback) {
  var expression = '#hashKey=:hashKey';
  var attributeNames = { '#hashKey': keyName(params.Key) };
  var attributeValues = { ':hashKey': keyValue(params.Key) };
  var scanIndexForward = true;
  var consistentRead = true;
  if (params.MinRangeKey && params.MaxRangeKey) {
    expression += ' AND #rangeKey BETWEEN :minRangeKey AND :maxRangeKey';
    attributeNames['#rangeKey'] = keyName(params.MinRangeKey);
    attributeValues[':minRangeKey'] = keyValue(params.MinRangeKey);
    attributeValues[':maxRangeKey'] = keyValue(params.MaxRangeKey);
  } else if (params.MinRangeKey) {
    expression += ' AND #rangeKey>=:minRangeKey';
    attributeNames['#rangeKey'] = keyName(params.MinRangeKey);
    attributeValues[':minRangeKey'] = keyValue(params.MinRangeKey);
  } else if (params.MaxRangeKey) {
    expression += ' AND #rangeKey<=:maxRangeKey';
    attributeNames['#rangeKey'] = keyName(params.MaxRangeKey);
    attributeValues[':maxRangeKey'] = keyValue(params.MaxRangeKey);
  }
  if (params.ScanIndexForward !== undefined) {
    scanIndexForward = params.ScanIndexForward;
  }
  if (params.ConsistentRead !== undefined) {
    consistentRead = params.ConsistentRead;
  }
  var queryParams = {
    TableName: this.name,
    KeyConditionExpression: expression,
    ExpressionAttributeNames: attributeNames,
    ExpressionAttributeValues: attributeValues,
    ConsistentRead: consistentRead,
    ScanIndexForward: scanIndexForward,
    Limit: params.Limit || 100,
  };
  if (params.IndexName) {
    queryParams.IndexName = params.IndexName;
    delete queryParams.ConsistentRead;
  }
  this.dynamo.query(queryParams, function(err, result) {
    callback(err, err ? undefined : result.Items || []);
  });
};

/**
 * Returns the item for a single key from the table.
 *
 * Params:
 *   Key: the key to use for the lookup (can contain both a hash key and range key)
 *   AllowMissing: if true, succeeds with null if the item is not found; if false (default),
 *     fails with a not found error.
 */
Table.prototype.get = function(params, callback) {
  this.dynamo.get({
    TableName: this.name,
    Key: params.Key
  }, function(err, result) {
    if (err) {
      callback(err);
    } else if (!result.Item && !params.AllowMissing) {
      callback(errors.notFound());
    } else {
      callback(null, result.Item ? result.Item : null);
    }
  });
};

/**
 * Returns items for multiple keys. This is limited to 100 keys at a time due to DynamoDB limits.
 *
 * Params:
 *   Keys: array of keys to fetch
 */
Table.prototype.batchGet = function(params, callback) {
  var keys = params.Keys || [];
  if (keys.length === 0) {
    callback(null, []);
    return;
  }
  var tableName = this.name;
  var getParams = { RequestItems: {} };
  getParams.RequestItems[tableName] = { Keys: keys };

  this.dynamo.batchGet(getParams, function(err, result) {
    if (err) {
      callback(err);
    } else if (result.UnprocessedKeys && result.UnprocessedKeys[tableName]) {
      // TODO(kito): we should retry these.
      callback(errors.serverError('Unprocessed Keys'));
    } else {
      callback(null, result.Responses[tableName] || []);
    }
  });
};

/**
 * Writes an item to the database, invoking the callback with the written item on success.
 *
 * Params:
 *   Item: the item to write.
 *   AllowOverwrite: if true, existing items can be overwritten; if false (default), attempting
 *     to overwrite an existing item will fail with a conflict error.
 */
Table.prototype.put = function(params, callback) {
  var item = _.cloneDeep(params.Item || {});
  item.createdAt = _.now();
  item.modifiedAt = item.createdAt;

  var putArgs = {
    TableName: this.name,
    Item: item,
  };
  if (!params.AllowOverwrite) {
    putArgs.ConditionExpression = 'attribute_not_exists(createdAt)';
  }
  this.dynamo.put(putArgs, function(err, result) {
    if (err && err.name == 'ConditionalCheckFailedException') {
      callback(errors.conflict());
    } else {
      callback(err, item);
    }
  });
};

/**
 * Atomically updates properties on an item. Fails with a not found error if the item does not
 * exist.
 *
 * Params:
 *   Key: key of the object to update
 *   Update: dictionary of fields to update and their new values
 */
Table.prototype.patch = function(params, callback) {
  var updates = params.Update || {};
  var names = Object.getOwnPropertyNames(updates);
  if (names.indexOf('createdAt') >= 0 || names.indexOf('modifiedAt') >= 0) {
    callback(errors.badRequest('Cannot patch createdAt or modifiedAt'));
    return;
  } else if (names.length === 0) {
    callback(errors.badRequest('No updates specified'));
    return;
  }

  // Always update the modified time.
  var expression = 'SET modifiedAt=:modifiedAt';
  var attributeNames = {};
  var attributeValues = { ':modifiedAt': new Date().getTime() };

  // Add to the expression for each of the updates:
  for (var i = 0; i < names.length; i++) {
    var placeholder = 'x' + i;
    expression += ', #' + placeholder + '=:' + placeholder;
    attributeNames['#' + placeholder] = names[i];
    attributeValues[':' + placeholder] = updates[names[i]];
  }

  this.dynamo.update({
    TableName: this.name,
    Key: params.Key,
    ConditionExpression: 'attribute_exists(createdAt)',
    UpdateExpression: expression,
    ExpressionAttributeNames: attributeNames,
    ExpressionAttributeValues: attributeValues,
    ReturnValues: 'ALL_NEW',
  }, function(err, result) {
    if (err && err.name == 'ConditionalCheckFailedException') {
      err = errors.notFound();
    }
    callback(err, result ? result.Attributes : undefined);
  });
};

/**
 * Atomically increments (or decrements) numeric fields on an item. Invokes callback with a
 * dictionary mapping each of the incremented fields to its new value. Fails with a not found
 * error if the item does not exist.
 *
 * Params:
 *   Key: key of the item to update
 *   Amount: dictionary of fields to updates and increment amounts (numeric)
 */
 Table.prototype.increment = function(params, callback) {
  var amounts = params.Amount || {};
  var names = Object.getOwnPropertyNames(amounts);
  if (names.indexOf('createdAt') >= 0 || names.indexOf('modifiedAt') >= 0) {
    callback(errors.badRequest('Cannot increment createdAt or modifiedAt'));
    return;
  } else if (names.length === 0) {
    callback(errors.badRequest('No amounts specified'));
    return;
  }

  // Always update the modified time.
  var expression = 'SET modifiedAt=:modifiedAt ADD ';
  var attributeNames = {};
  var attributeValues = { ':modifiedAt': new Date().getTime() };

  // Add to the expression for each of the amounts:
  for (var i = 0; i < names.length; i++) {
    var placeholder = 'x' + i;
    expression += ((i > 0) ? ', ' : '') + '#' + placeholder + ' :' + placeholder;
    attributeNames['#' + placeholder] = names[i];
    attributeValues[':' + placeholder] = amounts[names[i]];
  }

  this.dynamo.update({
    TableName: this.name,
    Key: params.Key,
    ConditionExpression: 'attribute_exists(createdAt)',
    UpdateExpression: expression,
    ExpressionAttributeNames: attributeNames,
    ExpressionAttributeValues: attributeValues,
    ReturnValues: 'ALL_NEW',
  }, function(err, result) {
    if (err && err.name == 'ConditionalCheckFailedException') {
      err = errors.notFound();
    }
    if (err) {
      callback(err);
    } else {
      var newValues = result.Attributes;
      delete(newValues.modifiedAt);
      callback(null, newValues);
    }
  });
};

/**
 * Deletes an item from the database. If the item is not found, it succeeds anyway.
 *
 * Params:
 *   Key: key of the item to delete.
 */
Table.prototype.delete = function(params, callback) {
  this.dynamo.delete({
    TableName: this.name,
    Key: params.Key
  }, function(err, result) {
    callback(err);
  });
};

var ACL = function(name) {
  this.name = name;
  this.dynamo = new aws.DynamoDB.DocumentClient();
};

/**
 * Returns the ACL entry for a given user and resource.
 *
 * Params:
 *   User: the user ID
 *   Resource: the resource identifier. This can be whatever your application wants.
 */
ACL.prototype.get = function(params, callback) {
  var key = { userID: params.User, res: params.Resource };
  this.dynamo.get({ TableName: this.name, Key: key }, function(err, result) {
    if (err) {
      callback(err);
    } else {
      callback(null, result.Item ? result.Item : key);
    }
  });
};

/**
 * Fails with a forbidden error if the user does not have all of the listed permissions on the
 * resource.
 *
 * Params:
 *   User: the user ID
 *   Resource: the resource identifier
 *   Permissions: array of permissions (strings).
 */
ACL.prototype.check = function(params, callback) {
  this.get(params, function(err, item) {
    if (!err) {
      for (var i = 0; i < params.Permissions.length; i++) {
        if (!item[params.Permissions[i]]) {
          err = errors.forbidden();
          break;
        }
      }
    }
    callback(err);
  });
};

/**
 * Grants the given list of permissions on the given resource to the given user.
 *
 * Params:
 *   User: the user ID
 *   Resource: the resource identifier
 *   Permissions: array of permissions (strings)
 */
ACL.prototype.grant = function(params, callback) {
  var expression = '';
  var attributeNames = {};
  var attributeValues = {};
  for (var i = 0; i < params.Permissions.length; i++) {
    var placeholder = 'p' + i;
    expression += (expression.length ? ', ' : 'SET ') + '#' + placeholder + '=:' + placeholder;
    attributeNames['#' + placeholder] = params.Permissions[i];
    attributeValues[':' + placeholder] = 1;
  }
  this.dynamo.update({
    TableName: this.name,
    Key: { userID: params.User, res: params.Resource },
    UpdateExpression: expression,
    ExpressionAttributeNames: attributeNames,
    ExpressionAttributeValues: attributeValues,
  }, function(err, result) {
    callback(err);
  });
};

/**
 * Revokes all permissions on the given resource from the given user.
 *
 * Params:
 *   User: the user ID
 *   Resource: the resource identifier
 */
ACL.prototype.revoke = function(params, callback) {
  var key = { userID: params.User, res: params.Resource };
  this.dynamo.delete({ TableName: this.name, Key: key }, function(err, result) {
    callback(err);
  });
};

function deleteGrants(dynamo, table, grants, callback) {
  // Form batches of 25 grants:
  var batches = [];
  while (grants.length) {
    batches.push(grants.splice(0, 25));
  }

  // Generate tasks that perform a dynamodb batch write for each batch:
  var tasks = batches.map(function(batch) {
    return function(callback) {
      var items = {};
      items[table] = batch.map(function(grant) {
        var key = { userID: grant.userID, res: grant.res };
        return { DeleteRequest: { Key: key } };
      });
      dynamo.batchWrite({ RequestItems: items }, function(err, result) {
        if (result && result.UnprocessedItems && result.UnprocessedItems[table]) {
          // TODO(kito): we should retry these
          err = errors.serverError('Unprocessed Items');
        }
        callback(err);
      });
    };
  });

  // Run all tasks in parallel:
  async.parallel(tasks, function(err) { callback(err); });
}

/**
 * Revokes all permissions for all users on the given resource.
 *
 * Params:
 *   Resource: the resource identifier
 */
ACL.prototype.revokeAll = function(params, callback) {
  var self = this;
  async.waterfall([
    function(next) { self.grantsForResource(params, next); },
    function(grants, next) { deleteGrants(self.dynamo, self.name, grants, next); }
  ], callback);
};

/**
 * Returns all grants for a given user, in descending order of resource ID with optional arguments.
 *
 * Params:
 *   User: the user ID
 *   MinResource: minimum resource to return (inclusive)
 *   MaxResource: maximum resource to return (inclusive)
 *   Limit: maximum number of grants to return
 */
ACL.prototype.grantsForUser = function(params, callback) {
  var expression = 'userID=:userID';
  var attributeValues = { ':userID': params.User };
  if (params.MinResource && params.MaxResource) {
    expression += ' AND res BETWEEN :minResource AND :maxResource';
    attributeValues[':minResource'] = params.MinResource;
    attributeValues[':maxResource'] = params.MaxResource;
  } else if (params.MinResource) {
    expression += ' AND res>=:minResource';
    attributeValues[':minResource'] = params.MinResource;
  } else if (params.MaxResource) {
    expression += ' AND res<=:maxResource';
    attributeValues[':maxResource'] = params.MaxResource;
  }
  this.dynamo.query({
    TableName: this.name,
    KeyConditionExpression: expression,
    ExpressionAttributeValues: attributeValues,
    ConsistentRead: true,
    ScanIndexForward: false,
    Limit: params.Limit || 10
  }, function(err, result) {
    callback(err, result ? result.Items : []);
  });
};

/**
 * Returns all grants for a given resource.
 *
 * Params:
 *   Resource: the resource ID
 */
ACL.prototype.grantsForResource = function(params, callback) {
  this.dynamo.query({
    TableName: this.name,
    IndexName: 'by-resource',
    KeyConditionExpression: 'res=:res',
    ExpressionAttributeValues: { ':res': params.Resource },
    ConsistentRead: false,
    ScanIndexForward: true,
  }, function(err, result) {
    callback(err, result ? result.Items : []);
  });
};

var Database = {
  /** Returns an object providing access to a DynamoDB table with given name. */
  table: function(name) {
    return new Table(name);
  },

  /** Returns an object providing access to the ACL with given name. */
  acl: function(name) {
    return new ACL(name);
  },
};

module.exports = Database;
<|endoftext|>


<commit_before>
/**
 * Module Dependencies
 */
var _ = require('lodash'),
    async = require('async'),
    _runJoins = require('waterline-cursor'),
    utils = require('./utils'),
    log = require('./logger')('waterline-orientdb:associations');

/**
 * Associations
 *
 * Associations contains methods used in implementing Waterline's Associations
 *
 * @param {Object} connection
 * @api private
 */
var Associations = module.exports = function Associations(connection) {

  // A connection to the DB used for queries
  this.connection = connection;
  this.fetchPlanLevel = connection.config.options.fetchPlanLevel;

  return this;
};

/**
 * Create Nested Associations
 *
 * @param {Object} values
 * @return {Object}
 * @api public
 */
Associations.prototype.createNestedAssociations = function createNestedAssociations(nestedAssociations, callback) {
  var self = this;

  async.reduce(Object.keys(nestedAssociations), {}, function(accumulator, key, next) {
    var value = nestedAssociations[key];
    var data = value.values;
    var collection = self.connection.collectionsByIdentity[value.collection];
    var tableName = collection.tableName || collection.identity;

    if ( !_.isArray(data) ) {
      self.connection.create(tableName, data, function(err, klass) {
        accumulator[key] = klass && klass.id;
        next(err, accumulator);
      });
    } else {
      async.map(data, function(item, complete) {
        self.connection.create(tableName, item, function(err, klass) {
          complete(err, klass && klass.id);
        });
      }, function(err, results) {
        accumulator[key] = results;
        next(err, accumulator);
      });
    }
  }, function(err, result) {
    callback(err, result);
  });
};

/**
 * Join call
 *
 * @param {String} collectionName
 * @param {Object} criteria
 * @param {Function} cb
 * @return {Object}
 * @api public
 */
Associations.prototype.join = function join(collectionName, criteria, cb) {
  //TODO: for now we only use fetch plan for many-to-many through associations. Use it for all associations
  if(this.isThroughJoin(criteria))
    return this.fetchPlanJoin(collectionName, criteria, cb);

  return this.genericJoin(collectionName, criteria, cb);
};

/**
 * Get Fetch Plan
 *
 * Infers the fetch plan query
 *
 * @param {String}  collectionName
 * @param {Object}  criteria
 * @return {String} fetchPlan query string
 * @api private
 */
Associations.prototype.getFetchPlan = function getFetchPlan(collectionName, criteria, fetchPlanLevel) {
  var self = this;
  fetchPlanLevel = fetchPlanLevel || 1;
  var parentSchema = self.connection.collections[collectionName].attributes;
  var fetchPlan = '';
  var associations = [];

  // Populate joined collections by fetching related vertices
  criteria.joins.forEach(function(join){
    if(!join.junctionTable && !join.select) { return; }

    // One-to-* associations such as model (links and linksets)
    if(!join.junctionTable && (parentSchema[join.alias].model || parentSchema[join.alias].collection)){
      fetchPlan += join.parentKey + ':1 ';
      return;
    }

    if(!join.junctionTable){
      log.warn("Unexpected behaviour. Can't process join: ", join);
      return;
    }

    // Many-to-many through associations (edges)
    associations.push(join.parent);
    var edgeSides = self.getEdgeSides(join.parent);

    if(edgeSides.out.referencedCollectionTableName === join.child
      && edgeSides.in.referencedAttributeColumnName === join.alias) {
      fetchPlan += 'in_' + join.parent + ':1 in_' + join.parent + '.out:' + fetchPlanLevel + ' ';
    }
    else if(edgeSides.in.referencedCollectionTableName === join.child
      && edgeSides.out.referencedAttributeColumnName === join.alias) {
      fetchPlan += 'out_' + join.parent + ':1 out_' + join.parent + '.in:' + fetchPlanLevel + ' ';
    }
  });

  // For remaining associated collections, expand the edges to get the vertices' foreign keys
  Object.keys(parentSchema).forEach(function(attributeKey){
    var joinTable = parentSchema[attributeKey].through;
    if(!joinTable) { return; };

    var joinTableName = self.connection.collectionsByIdentity[joinTable].tableName || joinTable;
    if(associations.indexOf(joinTableName) !== -1) { return; } // already processed

    var edgeSides = self.getEdgeSides(joinTableName);
    var columnName = parentSchema[attributeKey].columnName || attributeKey;

    var associatedCollection = parentSchema[attributeKey].collection;
    var associatedCollectionTablename = self.connection.collections[associatedCollection].tableName || associatedCollection;

    if(edgeSides.out.referencedCollectionTableName === associatedCollectionTablename
      && edgeSides.in.referencedAttributeColumnName === columnName) {
      fetchPlan += 'in_' + joinTableName + ':1 ';
    }
    else if(edgeSides.in.referencedCollectionTableName === associatedCollectionTablename
      && edgeSides.out.referencedAttributeColumnName === columnName) {
      fetchPlan += 'out_' + joinTableName + ':1 ';
    }
  });

  return fetchPlan.slice(-1) === ' ' ? fetchPlan.slice(0, -1) : fetchPlan;
};

/**
 * FetchPlan Join call
 *
 * Makes only one call
 *
 * @param {String} collectionName
 * @param {Object} criteria
 * @param {Function} cb
 * @return {Object}
 * @api private
 */
Associations.prototype.fetchPlanJoin = function fetchPlanJoin(collectionName, criteria, cb) {
  var self = this;

  var options = _.clone(criteria);
  options.fetchPlan = self.getFetchPlan(collectionName, criteria, self.fetchPlanLevel);
  var parentSchema = self.connection.collections[collectionName].attributes;

  self.connection.find(collectionName, options, function(err, results){
    if(err)
      return cb(err);
    else if(!results || results.length === 0)
      return cb(null, results);

    var normalisedResults = [];
    var keysToDelete = [];

    var expandedResults = self.expandResults(results);

    expandedResults.forEach(function(record){

      criteria.joins.forEach(function(join){
        var childTableSchema = self.connection.collections[join.child].attributes;

        if(join.parent === collectionName){
          if(join.removeParentKey)
            keysToDelete.push(join.parentKey);

          if (record[join.alias] && _.isObject(record[join.alias]))
            record[join.alias] = utils.rewriteIds(record[join.alias], childTableSchema);
          return; //nothing more to do
        }

        if(!join.junctionTable){
          return; //TODO: we should also handle many-to-many joins
        }

        var edgeSides = self.getEdgeSides(join.parent);
        var parentSide;
        if(edgeSides.out.referencedCollectionTableName === join.child
          && edgeSides.in.referencedAttributeColumnName === join.alias) {
          parentSide = edgeSides.in;
        }
        else if(edgeSides.in.referencedCollectionTableName === join.child
          && edgeSides.out.referencedAttributeColumnName === join.alias) {
          parentSide = edgeSides.out;
        }

        //Process record
        //TODO: record may be array

        if(!parentSide || !record[parentSide.referencedAttributeEdge]){
          return; //it probably has been processed already
        }

        record[join.alias] = self.getVerticesFromEdges(record[parentSide.referencedAttributeEdge], parentSide.edgeOppositeEnd);
        delete record[parentSide.referencedAttributeEdge];

        record[join.alias] = utils.rewriteIds(record[join.alias], childTableSchema);

        // record[join.alias].forEach(function(associatedRecord){
          // utils.cleanOrientAttributes(associatedRecord, childTableSchema);
        // });
      });

      // utils.cleanOrientAttributes(record, parentSchema);  // experimental

      normalisedResults.push(record);
  });

  cb(err, normalisedResults);
  });
};

/**
 * Expand Results
 *
 * Takes result set from fetch plan and replace RIDs with proper object
 *
 * @param {Object} resultset
 * @return {Object}
 * @api private
 */
Associations.prototype.expandResults = function expandResults(resultset) {
  var self = this;
  var clonedResults = _.cloneDeep(resultset);

  var recordMap = utils.reduceNestedObjects(clonedResults, function(accumulator, obj, key){
      if(obj.id)
        accumulator[obj.id] = _.cloneDeep(obj); // ugly but avoids circular references issues when converting to json
      if(obj['@rid'])
        log.warn('fetchPlanJoin: object ' + obj['@rid'] + ' still has @rid!');
      return accumulator;
    }, {});

  utils.forEachNested(clonedResults, function(value, key, parent){
    if(key !== 'id' && /*utils.matchRecordId(value) &&*/ recordMap[value]){
      parent[key] = recordMap[value];
    }
  });

  return clonedResults;
};

/**
 * Converts edges into an array of objects representing vertices. Edges can be a single value or a
 * mixed array of strings and objects
 *
 * @param {Object} result
 * @return {Array}
 * @api private
 */
Associations.prototype.getVerticesFromEdges = function getVerticesFromEdges(edges, side) {
  if(!_.isArray(edges)){
    if(_.isString(edges)) { return []; }; // only carries ID of edge
    var vertex = _.isString(edges[side]) ? { id: edges[side] } : edges[side];
    return [ vertex ];
  }

  var filteredEdges = _.filter(edges, function(edge) { return !_.isString(edge); });

  var vertices = _.map(filteredEdges, function(edge){
    return _.isString(edge[side]) ? { id: edge[side] } : edge[side];
  });

  return vertices;
};

/**
 * Generic Join call
 *
 * Makes multiple calls over the network
 *
 * @param {String} collectionName
 * @param {Object} criteria
 * @param {Function} cb
 * @return {Object}
 * @api private
 */
Associations.prototype.genericJoin = function genericJoin(collectionName, criteria, cb) {
  var self = this;

  // Ignore `select` from waterline core
  if ( typeof criteria === 'object') {
    delete criteria.select;
  }

  var connectionObject = self.connection;

  // Populate associated records for each parent result
  // (or do them all at once as an optimization, if possible)
  _runJoins({

    instructions: criteria,
    parentCollection: collectionName,

    /**
     * Find some records directly (using only this adapter)
     * from the specified collection.
     *
     * @param  {String}   collectionIdentity
     * @param  {Object}   criteria
     * @param  {Function} cb
     */
    $find: function (collectionIdentity, criteria, cb) {
      return connectionObject.find(collectionIdentity, criteria, cb);
    },

    /**
     * Look up the name of the primary key field
     * for the collection with the specified identity.
     *
     * @param  {String}   collectionIdentity
     * @return {String}
     */
    $getPK: function (collectionIdentity) {
      if (!collectionIdentity) return;
      var schema = connectionObject.collections[collectionIdentity].attributes;

      if(!schema)
       return 'id';

      for(key in schema){
        if(schema[key].primaryKey)
          return key;
      }
      return 'id';
    }
  }, cb);

};

/**
 * Get edge sides
 *
 * Returns and object describing the out and the in sides of the edge
 *
 * @param {Object} collectionName
 * @return {Object}
 * @api private
 */
Associations.prototype.getEdgeSides = function getEdgeSides(collectionName) {
  var self = this,
      collection = this.connection.collections[collectionName],
      schema = collection.attributes,
      identity = collection.identity || collection.tableName,
      vertexA,
      vertexB;

  Object.keys(schema).forEach(function(key) {
    var reference = schema[key].model || schema[key].references;
    if(!reference)
      return;

    var referencedCollection = self.connection.collectionsByIdentity[reference];
    var referencedSchema = referencedCollection.attributes;

    var referencedAttributeKey;
    Object.keys(referencedSchema).forEach(function(referencedSchemaKey) {
      var attribute = referencedSchema[referencedSchemaKey];
      if(attribute.through === identity && attribute.via === key)
        referencedAttributeKey = referencedSchemaKey;
    });
    if(!referencedAttributeKey){
        Object.keys(referencedSchema).forEach(function(referencedSchemaKey) {
        var attribute = referencedSchema[referencedSchemaKey];
        // Optimistic attribute assignment...
        if(attribute.through === identity)
          referencedAttributeKey = referencedSchemaKey;
      });
    }
    if(!referencedAttributeKey)
      return;

    var referencedAttribute = referencedSchema[referencedAttributeKey];

    var vertex = {
      referencedCollectionName: reference,
      referencedCollectionTableName: referencedCollection.tableName || reference,
      referencedAttributeKey: referencedAttributeKey,
      referencedAttributeColumnName: referencedAttribute.columnName || referencedAttributeKey,
      dominant: referencedAttribute.dominant,
      junctionTableKey: key,
      junctionTableColumnName: schema[key].columnName || key
    };

    if(!vertexA)
      vertexA = vertex;
    else if(!vertexB)
      vertexB = vertex;
    else
      log.error('Too many associations! Unable to process model [' + identity + '] attribute [' + key + '].');
  });

  if(!vertexA.dominant && !vertexB.dominant){
    var dominantVertex = (vertexA.junctionTableKey < vertexB.junctionTableKey) ? vertexA : vertexB;
    dominantVertex.dominant = true;

    log.warn(collectionName + ' junction table associations [' + vertexA.referencedCollectionName
      + ', ' + vertexB.referencedCollectionName + '] have no dominant through association. ' + dominantVertex.junctionTableKey
      + ' was chosen as dominant.');
  }

  if(vertexA.dominant){
    vertexA.referencedAttributeEdge = 'out_' + collectionName;
    vertexA.edgeOppositeEnd = 'in';
    vertexB.referencedAttributeEdge = 'in_' + collectionName;
    vertexB.edgeOppositeEnd = 'out';
    return { out: vertexA, in: vertexB };
  }

  if(vertexB.dominant){
    vertexA.referencedAttributeEdge = 'in_' + collectionName;
    vertexA.edgeOppositeEnd = 'out';
    vertexB.referencedAttributeEdge = 'out_' + collectionName;
    vertexB.edgeOppositeEnd = 'in';
    return { out: vertexB, in: vertexA };
  }
};

/**
 * Get edge
 *
 * Normalizes data for edge creation
 *
 * @param {Object} values
 * @return {Object}
 * @api private
 */
Associations.prototype.getEdge = function getEdge(collectionName, values) {
  var edgeSides = this.getEdgeSides(collectionName);
  return {
    from : values[edgeSides.out.junctionTableColumnName],
    to : values[edgeSides.in.junctionTableColumnName],
    keys: [edgeSides.out.junctionTableColumnName, edgeSides.in.junctionTableColumnName]
  };
};

/**
 * Is many-to-many through join
 *
 * @param {Object} criteria
 * @return {Boolean}
 * @api public
 */
Associations.prototype.isThroughJoin = function isThroughJoin(criteria) {
  var self = this;
  if(!criteria.joins)
    return false;

  for(i=0; i < criteria.joins.length; i++){
    var join = criteria.joins[i];
    var collectionInstance = self.connection.collections[join.parent];
    if(utils.isJunctionTableThrough(collectionInstance))
      return true;
  }

  return false;
};<commit_msg>Associations bug where collection was being retrieved from wrong list<commit_after>
/**
 * Module Dependencies
 */
var _ = require('lodash'),
    async = require('async'),
    _runJoins = require('waterline-cursor'),
    utils = require('./utils'),
    log = require('./logger')('waterline-orientdb:associations');

/**
 * Associations
 *
 * Associations contains methods used in implementing Waterline's Associations
 *
 * @param {Object} connection
 * @api private
 */
var Associations = module.exports = function Associations(connection) {

  // A connection to the DB used for queries
  this.connection = connection;
  this.fetchPlanLevel = connection.config.options.fetchPlanLevel;

  return this;
};

/**
 * Create Nested Associations
 *
 * @param {Object} values
 * @return {Object}
 * @api public
 */
Associations.prototype.createNestedAssociations = function createNestedAssociations(nestedAssociations, callback) {
  var self = this;

  async.reduce(Object.keys(nestedAssociations), {}, function(accumulator, key, next) {
    var value = nestedAssociations[key];
    var data = value.values;
    var collection = self.connection.collectionsByIdentity[value.collection];
    var tableName = collection.tableName || collection.identity;

    if ( !_.isArray(data) ) {
      self.connection.create(tableName, data, function(err, klass) {
        accumulator[key] = klass && klass.id;
        next(err, accumulator);
      });
    } else {
      async.map(data, function(item, complete) {
        self.connection.create(tableName, item, function(err, klass) {
          complete(err, klass && klass.id);
        });
      }, function(err, results) {
        accumulator[key] = results;
        next(err, accumulator);
      });
    }
  }, function(err, result) {
    callback(err, result);
  });
};

/**
 * Join call
 *
 * @param {String} collectionName
 * @param {Object} criteria
 * @param {Function} cb
 * @return {Object}
 * @api public
 */
Associations.prototype.join = function join(collectionName, criteria, cb) {
  //TODO: for now we only use fetch plan for many-to-many through associations. Use it for all associations
  if(this.isThroughJoin(criteria))
    return this.fetchPlanJoin(collectionName, criteria, cb);

  return this.genericJoin(collectionName, criteria, cb);
};

/**
 * Get Fetch Plan
 *
 * Infers the fetch plan query
 *
 * @param {String}  collectionName
 * @param {Object}  criteria
 * @return {String} fetchPlan query string
 * @api private
 */
Associations.prototype.getFetchPlan = function getFetchPlan(collectionName, criteria, fetchPlanLevel) {
  var self = this;
  fetchPlanLevel = fetchPlanLevel || 1;
  var parentSchema = self.connection.collections[collectionName].attributes;
  var fetchPlan = '';
  var associations = [];

  // Populate joined collections by fetching related vertices
  criteria.joins.forEach(function(join){
    if(!join.junctionTable && !join.select) { return; }

    // One-to-* associations such as model (links and linksets)
    if(!join.junctionTable && (parentSchema[join.alias].model || parentSchema[join.alias].collection)){
      fetchPlan += join.parentKey + ':1 ';
      return;
    }

    if(!join.junctionTable){
      log.warn("Unexpected behaviour. Can't process join: ", join);
      return;
    }

    // Many-to-many through associations (edges)
    associations.push(join.parent);
    var edgeSides = self.getEdgeSides(join.parent);

    if(edgeSides.out.referencedCollectionTableName === join.child
      && edgeSides.in.referencedAttributeColumnName === join.alias) {
      fetchPlan += 'in_' + join.parent + ':1 in_' + join.parent + '.out:' + fetchPlanLevel + ' ';
    }
    else if(edgeSides.in.referencedCollectionTableName === join.child
      && edgeSides.out.referencedAttributeColumnName === join.alias) {
      fetchPlan += 'out_' + join.parent + ':1 out_' + join.parent + '.in:' + fetchPlanLevel + ' ';
    }
  });

  // For remaining associated collections, expand the edges to get the vertices' foreign keys
  Object.keys(parentSchema).forEach(function(attributeKey){
    var joinTable = parentSchema[attributeKey].through;
    if(!joinTable) { return; };

    var joinTableName = self.connection.collectionsByIdentity[joinTable].tableName || joinTable;
    if(associations.indexOf(joinTableName) !== -1) { return; } // already processed

    var edgeSides = self.getEdgeSides(joinTableName);
    var columnName = parentSchema[attributeKey].columnName || attributeKey;

    var associatedCollection = parentSchema[attributeKey].collection;
    var associatedCollectionTablename = self.connection.collectionsByIdentity[associatedCollection].tableName || associatedCollection;

    if(edgeSides.out.referencedCollectionTableName === associatedCollectionTablename
      && edgeSides.in.referencedAttributeColumnName === columnName) {
      fetchPlan += 'in_' + joinTableName + ':1 ';
    }
    else if(edgeSides.in.referencedCollectionTableName === associatedCollectionTablename
      && edgeSides.out.referencedAttributeColumnName === columnName) {
      fetchPlan += 'out_' + joinTableName + ':1 ';
    }
  });

  return fetchPlan.slice(-1) === ' ' ? fetchPlan.slice(0, -1) : fetchPlan;
};

/**
 * FetchPlan Join call
 *
 * Makes only one call
 *
 * @param {String} collectionName
 * @param {Object} criteria
 * @param {Function} cb
 * @return {Object}
 * @api private
 */
Associations.prototype.fetchPlanJoin = function fetchPlanJoin(collectionName, criteria, cb) {
  var self = this;

  var options = _.clone(criteria);
  options.fetchPlan = self.getFetchPlan(collectionName, criteria, self.fetchPlanLevel);
  var parentSchema = self.connection.collections[collectionName].attributes;

  self.connection.find(collectionName, options, function(err, results){
    if(err)
      return cb(err);
    else if(!results || results.length === 0)
      return cb(null, results);

    var normalisedResults = [];
    var keysToDelete = [];

    var expandedResults = self.expandResults(results);

    expandedResults.forEach(function(record){

      criteria.joins.forEach(function(join){
        var childTableSchema = self.connection.collections[join.child].attributes;

        if(join.parent === collectionName){
          if(join.removeParentKey)
            keysToDelete.push(join.parentKey);

          if (record[join.alias] && _.isObject(record[join.alias]))
            record[join.alias] = utils.rewriteIds(record[join.alias], childTableSchema);
          return; //nothing more to do
        }

        if(!join.junctionTable){
          return; //TODO: we should also handle many-to-many joins
        }

        var edgeSides = self.getEdgeSides(join.parent);
        var parentSide;
        if(edgeSides.out.referencedCollectionTableName === join.child
          && edgeSides.in.referencedAttributeColumnName === join.alias) {
          parentSide = edgeSides.in;
        }
        else if(edgeSides.in.referencedCollectionTableName === join.child
          && edgeSides.out.referencedAttributeColumnName === join.alias) {
          parentSide = edgeSides.out;
        }

        //Process record
        //TODO: record may be array

        if(!parentSide || !record[parentSide.referencedAttributeEdge]){
          return; //it probably has been processed already
        }

        record[join.alias] = self.getVerticesFromEdges(record[parentSide.referencedAttributeEdge], parentSide.edgeOppositeEnd);
        delete record[parentSide.referencedAttributeEdge];

        record[join.alias] = utils.rewriteIds(record[join.alias], childTableSchema);

        // record[join.alias].forEach(function(associatedRecord){
          // utils.cleanOrientAttributes(associatedRecord, childTableSchema);
        // });
      });

      // utils.cleanOrientAttributes(record, parentSchema);  // experimental

      normalisedResults.push(record);
  });

  cb(err, normalisedResults);
  });
};

/**
 * Expand Results
 *
 * Takes result set from fetch plan and replace RIDs with proper object
 *
 * @param {Object} resultset
 * @return {Object}
 * @api private
 */
Associations.prototype.expandResults = function expandResults(resultset) {
  var self = this;
  var clonedResults = _.cloneDeep(resultset);

  var recordMap = utils.reduceNestedObjects(clonedResults, function(accumulator, obj, key){
      if(obj.id)
        accumulator[obj.id] = _.cloneDeep(obj); // ugly but avoids circular references issues when converting to json
      if(obj['@rid'])
        log.warn('fetchPlanJoin: object ' + obj['@rid'] + ' still has @rid!');
      return accumulator;
    }, {});

  utils.forEachNested(clonedResults, function(value, key, parent){
    if(key !== 'id' && /*utils.matchRecordId(value) &&*/ recordMap[value]){
      parent[key] = recordMap[value];
    }
  });

  return clonedResults;
};

/**
 * Converts edges into an array of objects representing vertices. Edges can be a single value or a
 * mixed array of strings and objects
 *
 * @param {Object} result
 * @return {Array}
 * @api private
 */
Associations.prototype.getVerticesFromEdges = function getVerticesFromEdges(edges, side) {
  if(!_.isArray(edges)){
    if(_.isString(edges)) { return []; }; // only carries ID of edge
    var vertex = _.isString(edges[side]) ? { id: edges[side] } : edges[side];
    return [ vertex ];
  }

  var filteredEdges = _.filter(edges, function(edge) { return !_.isString(edge); });

  var vertices = _.map(filteredEdges, function(edge){
    return _.isString(edge[side]) ? { id: edge[side] } : edge[side];
  });

  return vertices;
};

/**
 * Generic Join call
 *
 * Makes multiple calls over the network
 *
 * @param {String} collectionName
 * @param {Object} criteria
 * @param {Function} cb
 * @return {Object}
 * @api private
 */
Associations.prototype.genericJoin = function genericJoin(collectionName, criteria, cb) {
  var self = this;

  // Ignore `select` from waterline core
  if ( typeof criteria === 'object') {
    delete criteria.select;
  }

  var connectionObject = self.connection;

  // Populate associated records for each parent result
  // (or do them all at once as an optimization, if possible)
  _runJoins({

    instructions: criteria,
    parentCollection: collectionName,

    /**
     * Find some records directly (using only this adapter)
     * from the specified collection.
     *
     * @param  {String}   collectionIdentity
     * @param  {Object}   criteria
     * @param  {Function} cb
     */
    $find: function (collectionIdentity, criteria, cb) {
      return connectionObject.find(collectionIdentity, criteria, cb);
    },

    /**
     * Look up the name of the primary key field
     * for the collection with the specified identity.
     *
     * @param  {String}   collectionIdentity
     * @return {String}
     */
    $getPK: function (collectionIdentity) {
      if (!collectionIdentity) return;
      var schema = connectionObject.collections[collectionIdentity].attributes;

      if(!schema)
       return 'id';

      for(key in schema){
        if(schema[key].primaryKey)
          return key;
      }
      return 'id';
    }
  }, cb);

};

/**
 * Get edge sides
 *
 * Returns and object describing the out and the in sides of the edge
 *
 * @param {Object} collectionName
 * @return {Object}
 * @api private
 */
Associations.prototype.getEdgeSides = function getEdgeSides(collectionName) {
  var self = this,
      collection = this.connection.collections[collectionName],
      schema = collection.attributes,
      identity = collection.identity || collection.tableName,
      vertexA,
      vertexB;

  Object.keys(schema).forEach(function(key) {
    var reference = schema[key].model || schema[key].references;
    if(!reference)
      return;

    var referencedCollection = self.connection.collectionsByIdentity[reference];
    var referencedSchema = referencedCollection.attributes;

    var referencedAttributeKey;
    Object.keys(referencedSchema).forEach(function(referencedSchemaKey) {
      var attribute = referencedSchema[referencedSchemaKey];
      if(attribute.through === identity && attribute.via === key)
        referencedAttributeKey = referencedSchemaKey;
    });
    if(!referencedAttributeKey){
        Object.keys(referencedSchema).forEach(function(referencedSchemaKey) {
        var attribute = referencedSchema[referencedSchemaKey];
        // Optimistic attribute assignment...
        if(attribute.through === identity)
          referencedAttributeKey = referencedSchemaKey;
      });
    }
    if(!referencedAttributeKey)
      return;

    var referencedAttribute = referencedSchema[referencedAttributeKey];

    var vertex = {
      referencedCollectionName: reference,
      referencedCollectionTableName: referencedCollection.tableName || reference,
      referencedAttributeKey: referencedAttributeKey,
      referencedAttributeColumnName: referencedAttribute.columnName || referencedAttributeKey,
      dominant: referencedAttribute.dominant,
      junctionTableKey: key,
      junctionTableColumnName: schema[key].columnName || key
    };

    if(!vertexA)
      vertexA = vertex;
    else if(!vertexB)
      vertexB = vertex;
    else
      log.error('Too many associations! Unable to process model [' + identity + '] attribute [' + key + '].');
  });

  if(!vertexA.dominant && !vertexB.dominant){
    var dominantVertex = (vertexA.junctionTableKey < vertexB.junctionTableKey) ? vertexA : vertexB;
    dominantVertex.dominant = true;

    log.warn(collectionName + ' junction table associations [' + vertexA.referencedCollectionName
      + ', ' + vertexB.referencedCollectionName + '] have no dominant through association. ' + dominantVertex.junctionTableKey
      + ' was chosen as dominant.');
  }

  if(vertexA.dominant){
    vertexA.referencedAttributeEdge = 'out_' + collectionName;
    vertexA.edgeOppositeEnd = 'in';
    vertexB.referencedAttributeEdge = 'in_' + collectionName;
    vertexB.edgeOppositeEnd = 'out';
    return { out: vertexA, in: vertexB };
  }

  if(vertexB.dominant){
    vertexA.referencedAttributeEdge = 'in_' + collectionName;
    vertexA.edgeOppositeEnd = 'out';
    vertexB.referencedAttributeEdge = 'out_' + collectionName;
    vertexB.edgeOppositeEnd = 'in';
    return { out: vertexB, in: vertexA };
  }
};

/**
 * Get edge
 *
 * Normalizes data for edge creation
 *
 * @param {Object} values
 * @return {Object}
 * @api private
 */
Associations.prototype.getEdge = function getEdge(collectionName, values) {
  var edgeSides = this.getEdgeSides(collectionName);
  return {
    from : values[edgeSides.out.junctionTableColumnName],
    to : values[edgeSides.in.junctionTableColumnName],
    keys: [edgeSides.out.junctionTableColumnName, edgeSides.in.junctionTableColumnName]
  };
};

/**
 * Is many-to-many through join
 *
 * @param {Object} criteria
 * @return {Boolean}
 * @api public
 */
Associations.prototype.isThroughJoin = function isThroughJoin(criteria) {
  var self = this;
  if(!criteria.joins)
    return false;

  for(i=0; i < criteria.joins.length; i++){
    var join = criteria.joins[i];
    var collectionInstance = self.connection.collections[join.parent];
    if(utils.isJunctionTableThrough(collectionInstance))
      return true;
  }

  return false;
};<|endoftext|>


<commit_before>/**
 * adapter.js
 *
 * A jQuery/MooTools adapter for various DOM operations.
 * -----------------------------------------------------------------------------
 *
 * Composer.js is an MVC framework for creating and organizing javascript
 * applications. For documentation, please visit:
 *
 *     http://lyonbros.github.com/composer.js/
 *
 * -----------------------------------------------------------------------------
 *
 * Copyright (c) 2011, Lyon Bros Enterprises, LLC. (http://www.lyonbros.com)
 *
 * Licensed under The MIT License.
 * Redistributions of files must retain the above copyright notice.
 */
(function() {
	"use strict";

	var global = this;

	var has_jquery = !!global.jQuery;
	var has_slick = !!global.Slick;

	var find = (function() {
		if(has_jquery)
		{
			return function(context, selector) {
				context || (context = document);
				return jQuery(context).find(selector)[0];
			};
		}
		else if(has_slick)
		{
			return function(context, selector) {
				context || (context = document);
				return Slick.find(context, selector)
			};
		}
		error('No selector engine present. Include jQuery or Slick.');
	})();

	var match = (function() {
		if(has_jquery)
		{
			return function(context, selector) {
				context || (context = document);
				return jQuery(context).is(selector);
			};
		}
		else if(has_slick)
		{
			return function(context, selector) {
				context || (context = document);
				return Slick.match(context, selector);
			};
		}
		error('No selector engine present. Include jQuery or Slick.');
	})();

	var add_event = (function() {
		if(has_jquery)
		{
			return function(el, ev, fn, selector) {
				if(selector) return jQuery(el).on(ev, selector, fn);
				else return jQuery(el).on(ev, fn);
			};
		}
		else if(document.body && document.body.addEvent)
		{
			return function(el, ev, fn, selector) {
				if(selector) return el.addEvent(ev+':relay('+selector+')', fn);
				else return el.addEvent(ev, fn);
			};
		}
		else
		{
			return function(el, ev, fn, selector) {
				if(selector)
				{
					el.addEventListener(ev, fn, false);
				}
				else
				{
					el.addEventListener(ev, function(event) {
						var target = event.target || event.srcElement;
						if(event.__composer_handled || !match(target, selector)) return false;
						event.__composer_handled = true;
						fn(event);
					});
				}
			};
		}
	})();

	var remove_event = (function() {
		if(has_jquery)
		{
		}
		else if(document.body && document.body.removeEvent)
		{
		}
		else
		{
			return function(el, ev, fn) {
				el.removeEventListener(ev, fn, false);
			};
		}
	})();

	Composer.export({
		find: find,
		match: match,
		add_event: add_event,
		remove_event: remove_event
	});
}).apply((typeof exports != 'undefined') ? exports : this);

<commit_msg>oops, finishing Composer.remove_event<commit_after>/**
 * adapter.js
 *
 * A jQuery/MooTools adapter for various DOM operations.
 * -----------------------------------------------------------------------------
 *
 * Composer.js is an MVC framework for creating and organizing javascript
 * applications. For documentation, please visit:
 *
 *     http://lyonbros.github.com/composer.js/
 *
 * -----------------------------------------------------------------------------
 *
 * Copyright (c) 2011, Lyon Bros Enterprises, LLC. (http://www.lyonbros.com)
 *
 * Licensed under The MIT License.
 * Redistributions of files must retain the above copyright notice.
 */
(function() {
	"use strict";

	var global = this;

	var has_jquery = !!global.jQuery;
	var has_slick = !!global.Slick;

	var find = (function() {
		if(has_jquery)
		{
			return function(context, selector) {
				context || (context = document);
				return jQuery(context).find(selector)[0];
			};
		}
		else if(has_slick)
		{
			return function(context, selector) {
				context || (context = document);
				return Slick.find(context, selector)
			};
		}
		error('No selector engine present. Include jQuery or Slick.');
	})();

	var match = (function() {
		if(has_jquery)
		{
			return function(context, selector) {
				context || (context = document);
				return jQuery(context).is(selector);
			};
		}
		else if(has_slick)
		{
			return function(context, selector) {
				context || (context = document);
				return Slick.match(context, selector);
			};
		}
		error('No selector engine present. Include jQuery or Slick.');
	})();

	var add_event = (function() {
		if(has_jquery)
		{
			return function(el, ev, fn, selector) {
				if(selector) return jQuery(el).on(ev, selector, fn);
				else return jQuery(el).on(ev, fn);
			};
		}
		else if(document.body && document.body.addEvent)
		{
			return function(el, ev, fn, selector) {
				if(selector) return el.addEvent(ev+':relay('+selector+')', fn);
				else return el.addEvent(ev, fn);
			};
		}
		else
		{
			return function(el, ev, fn, selector) {
				if(selector)
				{
					el.addEventListener(ev, fn, false);
				}
				else
				{
					el.addEventListener(ev, function(event) {
						var target = event.target || event.srcElement;
						if(event.__composer_handled || !match(target, selector)) return false;
						event.__composer_handled = true;
						fn(event);
					});
				}
			};
		}
	})();

	var remove_event = (function() {
		if(has_jquery)
		{
			return function(el, ev, fn) {
				jQuery(el).off(ev, fn);
			};
		}
		else if(document.body && document.body.removeEvent)
		{
			return function(el, ev, fn) {
				el.removeEvent(ev, fn);
			};
		}
		else
		{
			return function(el, ev, fn) {
				el.removeEventListener(ev, fn, false);
			};
		}
	})();

	Composer.export({
		find: find,
		match: match,
		add_event: add_event,
		remove_event: remove_event
	});
}).apply((typeof exports != 'undefined') ? exports : this);

<|endoftext|>