<commit_before>/*
 * Copyright 2017 Scott Bender <scott@scottbender.net>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

const debug = require('debug')('signalk:interfaces:appstore')
const _ = require('lodash')
const compareVersions = require('compare-versions')
const installModule = require('../modules').installModule
const { findModulesWithKeyword, getLatestServerVersion } = require('../modules')

module.exports = function (app) {
  let moduleInstalling = undefined
  const modulesInstalledSinceStartup = {}
  const moduleInstallQueue = []

  return {
    start: function () {
      app.post(
        [
          '/appstore/install/:name/:version',
          '/appstore/install/:org/:name/:version'
        ],
        (req, res) => {
          let name = req.params.name
          const version = req.params.version

          if (req.params.org) {
            name = req.params.org + '/' + name
          }

          findPluginsAndWebapps()
            .then(([plugins, webapps]) => {
              if (
                name !== app.config.name &&
                !plugins.find(packageNameIs(name)) &&
                !webapps.find(packageNameIs(name))
              ) {
                res.status(404)
                res.send('No such webapp or plugin available:' + name)
              } else {
                if (moduleInstalling) {
                  moduleInstallQueue.push({ name: name, version: version })
                  sendAppStoreChangedEvent()
                } else {
                  installSKModule(name, version)
                }
                res.send(`Installing ${name}...`)
              }
            })
            .catch(error => {
              console.error(error.message)
              console.error(error.stack)
              res.status(500)
              res.send('
' + error.message + '
')
            })
        }
      )

      app.get('/appstore/available/', (req, res) => {
        findPluginsAndWebapps()
          .then(([plugins, webapps]) => {
            getLatestServerVersion().then(serverVersion => {
              const result = getAllModuleInfo(plugins, webapps, serverVersion)
              res.send(JSON.stringify(result))
            })
          })
          .catch(error => {
            if (error.code === 'ENOTFOUND') {
              res.send({
                available: [],
                installed: [],
                updates: [],
                installing: [],
                storeAvailable: false
              })
            }
            console.log(error)
            console.error(error.message)
            console.error(error.stack)
            res.status(500)
            res.send(error.message)
          })
      })
    },
    stop: function () {}
  }

  function findPluginsAndWebapps () {
    return Promise.all([
      findModulesWithKeyword('signalk-node-server-plugin'),
      findModulesWithKeyword('signalk-webapp')
    ])
  }

  function getPlugin (id) {
    return app.plugins.find(plugin => plugin.packageName === id)
  }

  function getWebApp (id) {
    return app.webapps.find(webapp => webapp.name === id)
  }

  function getAllModuleInfo (plugins, webapps, serverVersion) {
    const all = {
      available: [],
      installed: [],
      updates: [],
      installing: [],
      storeAvailable: true
    }

    if (
      process.argv.length > 0 &&
      (process.argv[1] === '/usr/bin/signalk-server' ||
        process.argv[1] ===
          '/usr/lib/node_modules/signalk-server/bin/signalk-server') &&
      !process.env.SIGNALK_DISABLE_SERVER_UPDATES
    ) {
      all.canUpdateServer = true
      if (compareVersions(serverVersion, app.config.version) > 0) {
        all.serverUpdate = serverVersion

        const info = {
          name: app.config.name,
          version: serverVersion,
          description: app.config.description,
          author: getAuthor(app.config),
          npmUrl:
            'https://github.com/SignalK/signalk-server-node/blob/master/CHANGELOG.md',
          isPlugin: false,
          isWebapp: false
        }

        if (moduleInstallQueue.find(p => p.name === info.name)) {
          info.isWaiting = true
          all.installing.push(info)
        } else if (modulesInstalledSinceStartup[info.name]) {
          if (moduleInstalling && moduleInstalling.name === info.name) {
            info.isInstalling = true
          } else if (modulesInstalledSinceStartup[info.name].code !== 0) {
            info.installFailed = true
          }
          all.installing.push(info)
        }
      }
    } else {
      all.canUpdateServer = false
    }

    getModulesInfo(plugins, getPlugin, all)
    getModulesInfo(webapps, getWebApp, all)
    return all
  }

  function getModulesInfo (modules, existing, result) {
    modules.forEach(plugin => {
      const name = plugin.package.name
      const version = plugin.package.version

      const pluginInfo = {
        name: name,
        version: version,
        description: plugin.package.description,
        author: getAuthor(plugin.package),
        npmUrl: getNpmUrl(plugin),
        isPlugin: plugin.package.keywords.some(
          v => v === 'signalk-node-server-plugin'
        ),
        isWebapp: plugin.package.keywords.some(v => v === 'signalk-webapp')
      }

      const installedModule = existing(name)

      if (installedModule) {
        pluginInfo.installedVersion = installedModule.version
      }

      if (moduleInstallQueue.find(p => p.name === name)) {
        pluginInfo.isWaiting = true
        addIfNotDuplicate(result.installing, pluginInfo)
      } else if (modulesInstalledSinceStartup[name]) {
        if (moduleInstalling && moduleInstalling.name === name) {
          pluginInfo.isInstalling = true
        } else if (modulesInstalledSinceStartup[name].code !== 0) {
          pluginInfo.installFailed = true
        }
        addIfNotDuplicate(result.installing, pluginInfo)
      } else if (installedModule) {
        if (compareVersions(version, installedModule.version) > 0) {
          addIfNotDuplicate(result.updates, pluginInfo)
        } else {
          addIfNotDuplicate(result.installed, pluginInfo)
        }
      } else {
        addIfNotDuplicate(result.available, pluginInfo)
      }

      return result
    })
  }

  function addIfNotDuplicate (theArray, moduleInfo) {
    if (!theArray.find(p => p.name === moduleInfo.name)) {
      theArray.push(moduleInfo)
    }
  }

  function getAuthor (_package) {
    debug(_package.name + ' author: ' + _package.author)
    return (
      (_package.author && (_package.author.name || _package.author.email)) +
      '' +
      (_package.contributors || [])
        .map(contributor => contributor.name || contributor.email)
        .join(',') +
      '' +
      (_package.name.startsWith('@signalk/') ? ' (Signal K team)' : '')
    )
  }

  function getNpmUrl (moduleInfo) {
    const npm = _.get(moduleInfo.package, 'links.npm')
    return npm || null
  }

  function sendAppStoreChangedEvent () {
    findPluginsAndWebapps().then(([plugins, webapps]) => {
      getLatestServerVersion().then(serverVersion => {
        const result = getAllModuleInfo(plugins, webapps, serverVersion)
        app.emit('serverevent', {
          type: 'APP_STORE_CHANGED',
          from: 'signalk-server',
          data: result
        })
      })
    })
  }

  function installSKModule (module, version) {
    moduleInstalling = {
      name: module,
      output: [],
      version: version
    }
    modulesInstalledSinceStartup[module] = moduleInstalling

    sendAppStoreChangedEvent()

    installModule(
      app,
      module,
      version,
      output => {
        modulesInstalledSinceStartup[module].output.push(output)
        console.log(`stdout: ${output}`)
      },
      output => {
        modulesInstalledSinceStartup[module].output.push(output)
        console.error(`stderr: ${output}`)
      },
      code => {
        debug('close: ' + module)
        modulesInstalledSinceStartup[module]['code'] = code
        moduleInstalling = undefined
        debug(`child process exited with code ${code}`)

        if (moduleInstallQueue.length) {
          const next = moduleInstallQueue.splice(0, 1)[0]
          installSKModule(next.name, next.version)
        }

        sendAppStoreChangedEvent()
      }
    )
  }
}

function packageNameIs (name) {
  return x => x.package.name === name
}
<commit_msg>fix: allow server updates when installed in /usr/local (#859)<commit_after>/*
 * Copyright 2017 Scott Bender <scott@scottbender.net>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

const debug = require('debug')('signalk:interfaces:appstore')
const _ = require('lodash')
const compareVersions = require('compare-versions')
const installModule = require('../modules').installModule
const { findModulesWithKeyword, getLatestServerVersion } = require('../modules')

const npmServerInstallLocations = [
  '/usr/bin/signalk-server',
  '/usr/lib/node_modules/signalk-server/bin/signalk-server',
  '/usr/local/bin/signalk-server',
  '/usr/local/lib/node_modules/signalk-server/bin/signalk-server'
]

module.exports = function (app) {
  let moduleInstalling
  const modulesInstalledSinceStartup = {}
  const moduleInstallQueue = []

  return {
    start: function () {
      app.post(
        [
          '/appstore/install/:name/:version',
          '/appstore/install/:org/:name/:version'
        ],
        (req, res) => {
          let name = req.params.name
          const version = req.params.version

          if (req.params.org) {
            name = req.params.org + '/' + name
          }

          findPluginsAndWebapps()
            .then(([plugins, webapps]) => {
              if (
                name !== app.config.name &&
                !plugins.find(packageNameIs(name)) &&
                !webapps.find(packageNameIs(name))
              ) {
                res.status(404)
                res.send('No such webapp or plugin available:' + name)
              } else {
                if (moduleInstalling) {
                  moduleInstallQueue.push({ name: name, version: version })
                  sendAppStoreChangedEvent()
                } else {
                  installSKModule(name, version)
                }
                res.send(`Installing ${name}...`)
              }
            })
            .catch(error => {
              console.error(error.message)
              console.error(error.stack)
              res.status(500)
              res.send('
' + error.message + '
')
            })
        }
      )

      app.get('/appstore/available/', (req, res) => {
        findPluginsAndWebapps()
          .then(([plugins, webapps]) => {
            getLatestServerVersion().then(serverVersion => {
              const result = getAllModuleInfo(plugins, webapps, serverVersion)
              res.send(JSON.stringify(result))
            })
          })
          .catch(error => {
            if (error.code === 'ENOTFOUND') {
              res.send({
                available: [],
                installed: [],
                updates: [],
                installing: [],
                storeAvailable: false
              })
            }
            console.log(error)
            console.error(error.message)
            console.error(error.stack)
            res.status(500)
            res.send(error.message)
          })
      })
    },
    stop: function () {}
  }

  function findPluginsAndWebapps () {
    return Promise.all([
      findModulesWithKeyword('signalk-node-server-plugin'),
      findModulesWithKeyword('signalk-webapp')
    ])
  }

  function getPlugin (id) {
    return app.plugins.find(plugin => plugin.packageName === id)
  }

  function getWebApp (id) {
    return app.webapps.find(webapp => webapp.name === id)
  }

  function getAllModuleInfo (plugins, webapps, serverVersion) {
    const all = {
      available: [],
      installed: [],
      updates: [],
      installing: [],
      storeAvailable: true
    }

    if (
      process.argv.length > 1 &&
      npmServerInstallLocations.includes(process.argv[1]) &&
      !process.env.SIGNALK_DISABLE_SERVER_UPDATES
    ) {
      all.canUpdateServer = true
      if (compareVersions(serverVersion, app.config.version) > 0) {
        all.serverUpdate = serverVersion

        const info = {
          name: app.config.name,
          version: serverVersion,
          description: app.config.description,
          author: getAuthor(app.config),
          npmUrl:
            'https://github.com/SignalK/signalk-server-node/blob/master/CHANGELOG.md',
          isPlugin: false,
          isWebapp: false
        }

        if (moduleInstallQueue.find(p => p.name === info.name)) {
          info.isWaiting = true
          all.installing.push(info)
        } else if (modulesInstalledSinceStartup[info.name]) {
          if (moduleInstalling && moduleInstalling.name === info.name) {
            info.isInstalling = true
          } else if (modulesInstalledSinceStartup[info.name].code !== 0) {
            info.installFailed = true
          }
          all.installing.push(info)
        }
      }
    } else {
      all.canUpdateServer = false
    }

    getModulesInfo(plugins, getPlugin, all)
    getModulesInfo(webapps, getWebApp, all)
    return all
  }

  function getModulesInfo (modules, existing, result) {
    modules.forEach(plugin => {
      const name = plugin.package.name
      const version = plugin.package.version

      const pluginInfo = {
        name: name,
        version: version,
        description: plugin.package.description,
        author: getAuthor(plugin.package),
        npmUrl: getNpmUrl(plugin),
        isPlugin: plugin.package.keywords.some(
          v => v === 'signalk-node-server-plugin'
        ),
        isWebapp: plugin.package.keywords.some(v => v === 'signalk-webapp')
      }

      const installedModule = existing(name)

      if (installedModule) {
        pluginInfo.installedVersion = installedModule.version
      }

      if (moduleInstallQueue.find(p => p.name === name)) {
        pluginInfo.isWaiting = true
        addIfNotDuplicate(result.installing, pluginInfo)
      } else if (modulesInstalledSinceStartup[name]) {
        if (moduleInstalling && moduleInstalling.name === name) {
          pluginInfo.isInstalling = true
        } else if (modulesInstalledSinceStartup[name].code !== 0) {
          pluginInfo.installFailed = true
        }
        addIfNotDuplicate(result.installing, pluginInfo)
      } else if (installedModule) {
        if (compareVersions(version, installedModule.version) > 0) {
          addIfNotDuplicate(result.updates, pluginInfo)
        } else {
          addIfNotDuplicate(result.installed, pluginInfo)
        }
      } else {
        addIfNotDuplicate(result.available, pluginInfo)
      }

      return result
    })
  }

  function addIfNotDuplicate (theArray, moduleInfo) {
    if (!theArray.find(p => p.name === moduleInfo.name)) {
      theArray.push(moduleInfo)
    }
  }

  function getAuthor (_package) {
    debug(_package.name + ' author: ' + _package.author)
    return (
      (_package.author && (_package.author.name || _package.author.email)) +
      '' +
      (_package.contributors || [])
        .map(contributor => contributor.name || contributor.email)
        .join(',') +
      '' +
      (_package.name.startsWith('@signalk/') ? ' (Signal K team)' : '')
    )
  }

  function getNpmUrl (moduleInfo) {
    const npm = _.get(moduleInfo.package, 'links.npm')
    return npm || null
  }

  function sendAppStoreChangedEvent () {
    findPluginsAndWebapps().then(([plugins, webapps]) => {
      getLatestServerVersion().then(serverVersion => {
        const result = getAllModuleInfo(plugins, webapps, serverVersion)
        app.emit('serverevent', {
          type: 'APP_STORE_CHANGED',
          from: 'signalk-server',
          data: result
        })
      })
    })
  }

  function installSKModule (module, version) {
    moduleInstalling = {
      name: module,
      output: [],
      version: version
    }
    modulesInstalledSinceStartup[module] = moduleInstalling

    sendAppStoreChangedEvent()

    installModule(
      app,
      module,
      version,
      output => {
        modulesInstalledSinceStartup[module].output.push(output)
        console.log(`stdout: ${output}`)
      },
      output => {
        modulesInstalledSinceStartup[module].output.push(output)
        console.error(`stderr: ${output}`)
      },
      code => {
        debug('close: ' + module)
        modulesInstalledSinceStartup[module]['code'] = code
        moduleInstalling = undefined
        debug(`child process exited with code ${code}`)

        if (moduleInstallQueue.length) {
          const next = moduleInstallQueue.splice(0, 1)[0]
          installSKModule(next.name, next.version)
        }

        sendAppStoreChangedEvent()
      }
    )
  }
}

function packageNameIs (name) {
  return x => x.package.name === name
}
<|endoftext|>


<commit_before>var _ = require('underscore'),
  async = require('async'),
  nano = require('nano'),
  url = require('url');

/**
 * class Db
 * - _url (String): CouchDB URL in format http(s)://user:pass@host:port/db
 **/
function Db(_url) {

  var dbName = url.parse(_url).pathname,
    dbUrl = _url.replace(new RegExp(dbName + '$', 'g'), '');

  this.db = nano(dbUrl).use(dbName);
  this.inProgress = 0;
}

/**
 * Db#paginate
 * - interval (Number): interval between page retrievals, in milliseconds
 * - startKey (String): ID of the first document to retrieve, null indicates the first document in the database
 * - endKey (String): ID of the last document to retrieve
 * - pageCb (Function): a callback function to process documents per page retrieval
 * - cb (Function): standard cb(err, result) callback
 *
 * Retrieve documents in CouchDB database, page by page.
 * This implements linked list pagination - http://guide.couchdb.org/draft/recipes.html#fast .
 **/
Db.prototype.paginate = function (interval, startKey, endKey, pageSize, pageCb, cb) {

  var done = false,
    key = startKey,
    self = this;

  function _action(cb) {

    var opts = {
      startkey_docid: key,
      limit: pageSize + 1,
      include_docs: true
    };
    // unlike startkey_docid, null endkey_docid does not mean last document in the database
    // don't set endkey_docid option unless endKey argument is set
    if (endKey) {
      opts.endkey_docid = endKey;
    }

    self.db.list(opts, function (err, result) {

      if (err) {
        cb(err);

      } else {
        pageCb(_.pluck(result.rows, 'doc'));

        // when the number of retrieved documents is less than the limit, that means it's the last page
        if (result.rows.length < opts.limit) {
          done = true;
          interval = 0;
        // if not last page, then use the last document's key as the startkey of the first document on the next page
        } else {
          key = result.rows[result.rows.length - 1].key;
        }
        setTimeout(cb, interval);
      }
    });
  }

  function _check() {
    return done === true;
  }

  function _end(err) {
    cb(err);
  }

  async.until(_check, _action, _end);
};

/**
 * Db#update
 * - docs (Array): documents to be bulk updated
 *
 * Bulk update an array of documents.
 **/
Db.prototype.update = function (docs, cb) {

  this.inProgress += 1;

  var self = this;
  this.db.bulk({ docs: docs }, function (err, result) {
    self.inProgress -= 1;
    cb(err, result);
  });
};

/**
 * Db#done -> Boolean
 *
 * True if there's no more document in queue waiting to be updated. False otherwise.
 **/
Db.prototype.done = function () {
  return this.inProgress === 0;
};

module.exports = Db;
<commit_msg>Update db#update doco.<commit_after>var _ = require('underscore'),
  async = require('async'),
  nano = require('nano'),
  url = require('url');

/**
 * class Db
 * - _url (String): CouchDB URL in format http(s)://user:pass@host:port/db
 **/
function Db(_url) {

  var dbName = url.parse(_url).pathname,
    dbUrl = _url.replace(new RegExp(dbName + '$', 'g'), '');

  this.db = nano(dbUrl).use(dbName);
  this.inProgress = 0;
}

/**
 * Db#paginate
 * - interval (Number): interval between page retrievals, in milliseconds
 * - startKey (String): ID of the first document to retrieve, null indicates the first document in the database
 * - endKey (String): ID of the last document to retrieve
 * - pageCb (Function): a callback function to process documents per page retrieval
 * - cb (Function): standard cb(err, result) callback
 *
 * Retrieve documents in CouchDB database, page by page.
 * This implements linked list pagination - http://guide.couchdb.org/draft/recipes.html#fast .
 **/
Db.prototype.paginate = function (interval, startKey, endKey, pageSize, pageCb, cb) {

  var done = false,
    key = startKey,
    self = this;

  function _action(cb) {

    var opts = {
      startkey_docid: key,
      limit: pageSize + 1,
      include_docs: true
    };
    // unlike startkey_docid, null endkey_docid does not mean last document in the database
    // don't set endkey_docid option unless endKey argument is set
    if (endKey) {
      opts.endkey_docid = endKey;
    }

    self.db.list(opts, function (err, result) {

      if (err) {
        cb(err);

      } else {
        pageCb(_.pluck(result.rows, 'doc'));

        // when the number of retrieved documents is less than the limit, that means it's the last page
        if (result.rows.length < opts.limit) {
          done = true;
          interval = 0;
        // if not last page, then use the last document's key as the startkey of the first document on the next page
        } else {
          key = result.rows[result.rows.length - 1].key;
        }
        setTimeout(cb, interval);
      }
    });
  }

  function _check() {
    return done === true;
  }

  function _end(err) {
    cb(err);
  }

  async.until(_check, _action, _end);
};

/**
 * Db#update
 * - docs (Array): documents to be bulk updated
 * - cb (Function): standard cb(err, result) callback
 *
 * Bulk update an array of documents.
 **/
Db.prototype.update = function (docs, cb) {

  this.inProgress += 1;

  var self = this;
  this.db.bulk({ docs: docs }, function (err, result) {
    self.inProgress -= 1;
    cb(err, result);
  });
};

/**
 * Db#done -> Boolean
 *
 * True if there's no more document in queue waiting to be updated. False otherwise.
 **/
Db.prototype.done = function () {
  return this.inProgress === 0;
};

module.exports = Db;
<|endoftext|>


<commit_before>const debug = require('debug')('cosa:db');
const defaults = require('defaults');
const EJSON = require('mongodb-extended-json');
const EventEmitter = require('events').EventEmitter;
const MongoClient = require('mongodb').MongoClient;
const {
  curry, split, last, pipe, isEmpty
} = require('omnibelt');
const RESULT_FIELDS = [ 'insertedCount', 'insertedIds', 'matchedCount', 'modifiedCount', 'upsertedCount', 'deletedCount', 'upsertedIds' ];
const getDbName = pipe(split('/'), last);

const normalizeResult = (r) => {
  const result = {
    result: r.result,
    ops: r.ops
  };
  RESULT_FIELDS.forEach((type) => {
    if ('undefined' !== typeof r[type]) {
      result[type] = r[type];
    }
  });
  return result;
};

const actionOneOrMany = curry(async (action, collection, isMultiple, ...params) => {
  const type = isMultiple ? `${action}Many` : `${action}One`;
  debug(`db.${collection.collectionName}.${type}`, ...params);
  const r = await collection[type](...params);
  return normalizeResult(r);
});
const insertOneOrMany = actionOneOrMany('insert');
const updateOneOrMany = actionOneOrMany('update');
const removeOneOrMany = actionOneOrMany('remove');

/**
 * Singleton that provides methods for connecting to a MongoDB collection.
 * @class Database
 */
class Database extends EventEmitter {
  constructor() {
    super();
    this._db = null;
    this._client = null;
    this._connectionStatus = 'disconnected';
    Object.defineProperty(this, 'connectionStatus', {
      enumerable: true,
      get: function() {
        return this._connectionStatus;
      }
    });
  }

  /**
   * Initialize the database connection.
   * @param {string} [uri] - URI to the database. If no URI is provided, the value
     *    of `process.env.COSA_DB_URI` is used. See
     *    https://docs.mongodb.com/manual/reference/connection-string/ for
     *    information on the URI format.
   * @param {object} [options] - Optional collection settings.
   * @see {@link init}
   * @returns {Promise} returns the db when connected
   */
  async init(uri, options) {
    options = options || {};
    this._uri = uri || process.env.COSA_DB_URI;
    if (!(/mongodb:\/\//).test(this._uri)) {
      throw new Error('invalid mongodb uri');
    }
    // so we dont mess up people environment variables.
    const dbName = options.dbName || getDbName(this._uri);
    debug(`connecting to database: ${this._uri}`);
    this._connectionStatus = 'connecting';
    this.emit('connecting');
    try {
      this._client = await MongoClient.connect(this._uri, { useNewUrlParser: true });
      this._db = await this._client.db(dbName);
    } catch (err) {
      debug('failed to connect', err);
      this._db = null;
      this._client = null;
      this._connectionStatus = 'disconnected';
      throw err;
    }
    this._connectionStatus = 'connected';
    this._client.on('close', () => {
      debug('database connection closed');
      this._db = null;
      this._client = null;
      this._connectionStatus = 'disconnected';
      this.emit('disconnect');
    });
    this.emit('connect', this);
    debug('connected to database');
    return this;
  }

  /**
   * Fetch a specific database collection.
   * @param {string} name - Collection name.
   * @param {object} [options] - Optional collection settings.
   * @returns {Promise} resolves with the connection
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Db.html#collection}
   */
  async collection(name, options) {
    if (this._connectionStatus === 'connected') {
      debug(`loading collection ${name}`);
      const collection = await this._db.collection(name, options);
      debug(`collection ${name} loaded`);
      return collection;
    } else if (this._connectionStatus === 'connecting') {
      debug(`waiting on connection before loading collection ${name}`);
      return new Promise((resolve) => {
        this.on('connect', async () => {
          const collection = await this._db.collection(name, options);
          debug(`collection ${name} loaded`);
          return resolve(collection);
        });
      });
    } else {
      await this.init();
      const collection = await this._db.collection(name, options);
      debug(`collection ${name} loaded`);
      return collection;
    }
  }

  /**
   * Fetches documents from a collection with the given query.
   * @param {string} collectionName - Name of the collection.
   * @param {object} query - MongoDB query object.
   * @param {object} [options] - MongoDB query options.
   * @param {object} [options.fields] - (deprecated, will be mapped to projection) The fields to return in the query. Object of fields to include or exclude (not both), {'a':1
   * @param {object} [options.projection] - The projection to return in the query. Object of projection to include or exclude (not both), {'a':1
   * @param {(object|Array)} [options.sort] - Set to sort the documents coming back from the query. Array of indexes, [['a', 1]] etc.
   * @param {number} [options.skip=0] - Set to skip N documents ahead in your query (useful for pagination).
   * @param {object} [options.limit=1000] - Sets the limit of documents returned in the query.
   * @param {object} [options.count=false] - get a count of the items, instead of the items themselves.
   * @param {object} [options.findOne=false] - Should a single item be returned.
   * @returns {Cursor} returns Cursor object
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Cursor.html}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#find}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#findOne}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#count}
   */
  async find(collectionName, query, options) {
    options = defaults(options, {
      projection: undefined,
      sort: undefined,
      skip: undefined,
      limit: options && options.count ? null : 1000,
      count: false,
      findOne: false
    });
    query = EJSON.deserialize(query);
    debug('getting db collection');
    const collection = await this.collection(collectionName);
    if (options.count) {
      const countOptions = { limit: options.limit, skip: options.skip };
      debug(`db.${collection.collectionName}.countDocuments`, query, countOptions);
      if ((!query || isEmpty(query))) {
        let count = collection.estimatedDocumentCount(options);
        if (countOptions.skip) {
          count = count - options.skip;
          if (count < 0) {
            return 0;
          }
        }
        if (countOptions.limit && countOptions.limit <= count) {
          return countOptions.limit;
        }
        return count;
      } else {
        return collection.countDocuments(query, options);
      }
    } else if (options.findOne) {
      const findOptions = { projection: options.projection || options.fields, limit: options.limit, skip: options.skip, sort: options.sort };
      debug(`db.${collection.collectionName}.findOne`, query, findOptions);
      return collection.findOne(query, options);
    } else {
      const findOptions = { projection: options.projection || options.fields, limit: options.limit, skip: options.skip, sort: options.sort };
      debug(`db.${collection.collectionName}.find (toArray)`, query, findOptions);
      return collection.find(query, options);
    }
  }

  /**
   * Inserts the given docs into a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {(object|Array)} docs - Documents objects to insert.
   * @returns {Promise} resolves with an object with results, and ops as keys
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#insertmany}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#insertOne}
   */
  async insert(collectionName, docs) {
    debug('getting db collection');
    docs = EJSON.deserialize(docs);
    const collection = await this.collection(collectionName);
    return insertOneOrMany(collection, Array.isArray(docs), docs);
  }

  /**
   * Updates docs in a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {object} query - Query to find which documents to update.
   * @param {object} update - Document properties to update.
   * @param {object} [options] - Optional settings see mongo documentation
   * @param {boolean} [options.multiple=false] - Should multiple documents be updated.
   * @param {boolean} [options.upsert=false] - Should documents be inserted if they don't already exist..
   * @returns {Promise} resolves with an object with results, and ops as keys
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#updateMany}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#updateOne}
   */
  async update(collectionName, query, update, options) {
    options = defaults(options, {
      multiple: false,
      upsert: false
    });
    debug('getting db collection');
    query = EJSON.deserialize(query);
    update = EJSON.deserialize(update);
    const collection = await this.collection(collectionName);
    const updateOptions = { upsert: options.upsert };
    return updateOneOrMany(collection, options.multiple, query, update, updateOptions);
  }

  /**
   * Removes docs from a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {object} query - Query to find which documents to remove.
   * @param {object} [options] - Optional settings see mongo documentation
   * @param {boolean} [options.multiple=false] - Should multiple documents be removed.
   * @returns {Promise} resolves with an object with results, and ops as keys
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#deleteMany}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#deleteOne}
   */
  async remove(collectionName, query, options) {
    options = defaults(options, {
      multiple: false
    });
    debug('getting db collection');
    query = EJSON.deserialize(query);
    const collection = await this.collection(collectionName);
    return removeOneOrMany(collection, options.multiple, query);
  }

  /**
   * Executes aggregation pipeline against a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {object} pipeline - Aggregation pipeline.
   * @param {object} [options] - Optional settings see mongo documentation
   * @param {boolean} [options.explain=false] - Should should the execution plan be returned.
   * @returns {Promise} resovles with the result of the aggregatation from mongo
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#aggregate}
   */
  async aggregate(collectionName, pipeline, options) {
    options = defaults(options, {
      explain: false
    });
    debug('getting db collection');
    const collection = await this.collection(collectionName);
    return new Promise((resolve, reject) => {
      collection.aggregate(pipeline, options, function(err, r) {
        if (err) { return reject(err); }
        return resolve(r);
      });
    });
  }

  /**
   * Returns list of unique values for the given key across a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {string} key - Document property.
   * @param {object} query - Query to find which documents evaluate.
   * @param {object} [options] - Optional settings see mongo documentation
   * @returns {Promise} resovles with the result of the distinct query from mongo
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#distinct}
   */
  async distinct(collectionName, key, query, options) {
    debug('getting db collection');
    query = EJSON.deserialize(query);
    const collection = await this.collection(collectionName);
    return collection.distinct(key, query, options);
  }

  async replace(collectionName, query, replace) {
    const collection = await this.collection(collectionName);
    query = EJSON.deserialize(query);
    replace = EJSON.deserialize(replace);
    debug(`db.${collection.collectionName}.replaceOne`, query, replace);
    const r = await collection.replaceOne(query, replace);
    return normalizeResult(r);
  }

}

module.exports = new Database();
<commit_msg>removing extra ()<commit_after>const debug = require('debug')('cosa:db');
const defaults = require('defaults');
const EJSON = require('mongodb-extended-json');
const EventEmitter = require('events').EventEmitter;
const MongoClient = require('mongodb').MongoClient;
const {
  curry, split, last, pipe, isEmpty
} = require('omnibelt');
const RESULT_FIELDS = [ 'insertedCount', 'insertedIds', 'matchedCount', 'modifiedCount', 'upsertedCount', 'deletedCount', 'upsertedIds' ];
const getDbName = pipe(split('/'), last);

const normalizeResult = (r) => {
  const result = {
    result: r.result,
    ops: r.ops
  };
  RESULT_FIELDS.forEach((type) => {
    if ('undefined' !== typeof r[type]) {
      result[type] = r[type];
    }
  });
  return result;
};

const actionOneOrMany = curry(async (action, collection, isMultiple, ...params) => {
  const type = isMultiple ? `${action}Many` : `${action}One`;
  debug(`db.${collection.collectionName}.${type}`, ...params);
  const r = await collection[type](...params);
  return normalizeResult(r);
});
const insertOneOrMany = actionOneOrMany('insert');
const updateOneOrMany = actionOneOrMany('update');
const removeOneOrMany = actionOneOrMany('remove');

/**
 * Singleton that provides methods for connecting to a MongoDB collection.
 * @class Database
 */
class Database extends EventEmitter {
  constructor() {
    super();
    this._db = null;
    this._client = null;
    this._connectionStatus = 'disconnected';
    Object.defineProperty(this, 'connectionStatus', {
      enumerable: true,
      get: function() {
        return this._connectionStatus;
      }
    });
  }

  /**
   * Initialize the database connection.
   * @param {string} [uri] - URI to the database. If no URI is provided, the value
     *    of `process.env.COSA_DB_URI` is used. See
     *    https://docs.mongodb.com/manual/reference/connection-string/ for
     *    information on the URI format.
   * @param {object} [options] - Optional collection settings.
   * @see {@link init}
   * @returns {Promise} returns the db when connected
   */
  async init(uri, options) {
    options = options || {};
    this._uri = uri || process.env.COSA_DB_URI;
    if (!(/mongodb:\/\//).test(this._uri)) {
      throw new Error('invalid mongodb uri');
    }
    // so we dont mess up people environment variables.
    const dbName = options.dbName || getDbName(this._uri);
    debug(`connecting to database: ${this._uri}`);
    this._connectionStatus = 'connecting';
    this.emit('connecting');
    try {
      this._client = await MongoClient.connect(this._uri, { useNewUrlParser: true });
      this._db = await this._client.db(dbName);
    } catch (err) {
      debug('failed to connect', err);
      this._db = null;
      this._client = null;
      this._connectionStatus = 'disconnected';
      throw err;
    }
    this._connectionStatus = 'connected';
    this._client.on('close', () => {
      debug('database connection closed');
      this._db = null;
      this._client = null;
      this._connectionStatus = 'disconnected';
      this.emit('disconnect');
    });
    this.emit('connect', this);
    debug('connected to database');
    return this;
  }

  /**
   * Fetch a specific database collection.
   * @param {string} name - Collection name.
   * @param {object} [options] - Optional collection settings.
   * @returns {Promise} resolves with the connection
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Db.html#collection}
   */
  async collection(name, options) {
    if (this._connectionStatus === 'connected') {
      debug(`loading collection ${name}`);
      const collection = await this._db.collection(name, options);
      debug(`collection ${name} loaded`);
      return collection;
    } else if (this._connectionStatus === 'connecting') {
      debug(`waiting on connection before loading collection ${name}`);
      return new Promise((resolve) => {
        this.on('connect', async () => {
          const collection = await this._db.collection(name, options);
          debug(`collection ${name} loaded`);
          return resolve(collection);
        });
      });
    } else {
      await this.init();
      const collection = await this._db.collection(name, options);
      debug(`collection ${name} loaded`);
      return collection;
    }
  }

  /**
   * Fetches documents from a collection with the given query.
   * @param {string} collectionName - Name of the collection.
   * @param {object} query - MongoDB query object.
   * @param {object} [options] - MongoDB query options.
   * @param {object} [options.fields] - (deprecated, will be mapped to projection) The fields to return in the query. Object of fields to include or exclude (not both), {'a':1
   * @param {object} [options.projection] - The projection to return in the query. Object of projection to include or exclude (not both), {'a':1
   * @param {(object|Array)} [options.sort] - Set to sort the documents coming back from the query. Array of indexes, [['a', 1]] etc.
   * @param {number} [options.skip=0] - Set to skip N documents ahead in your query (useful for pagination).
   * @param {object} [options.limit=1000] - Sets the limit of documents returned in the query.
   * @param {object} [options.count=false] - get a count of the items, instead of the items themselves.
   * @param {object} [options.findOne=false] - Should a single item be returned.
   * @returns {Cursor} returns Cursor object
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Cursor.html}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#find}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#findOne}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#count}
   */
  async find(collectionName, query, options) {
    options = defaults(options, {
      projection: undefined,
      sort: undefined,
      skip: undefined,
      limit: options && options.count ? null : 1000,
      count: false,
      findOne: false
    });
    query = EJSON.deserialize(query);
    debug('getting db collection');
    const collection = await this.collection(collectionName);
    if (options.count) {
      const countOptions = { limit: options.limit, skip: options.skip };
      debug(`db.${collection.collectionName}.countDocuments`, query, countOptions);
      if (!query || isEmpty(query)) {
        let count = collection.estimatedDocumentCount(options);
        if (countOptions.skip) {
          count = count - options.skip;
          if (count < 0) {
            return 0;
          }
        }
        if (countOptions.limit && countOptions.limit <= count) {
          return countOptions.limit;
        }
        return count;
      } else {
        return collection.countDocuments(query, options);
      }
    } else if (options.findOne) {
      const findOptions = { projection: options.projection || options.fields, limit: options.limit, skip: options.skip, sort: options.sort };
      debug(`db.${collection.collectionName}.findOne`, query, findOptions);
      return collection.findOne(query, options);
    } else {
      const findOptions = { projection: options.projection || options.fields, limit: options.limit, skip: options.skip, sort: options.sort };
      debug(`db.${collection.collectionName}.find (toArray)`, query, findOptions);
      return collection.find(query, options);
    }
  }

  /**
   * Inserts the given docs into a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {(object|Array)} docs - Documents objects to insert.
   * @returns {Promise} resolves with an object with results, and ops as keys
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#insertmany}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#insertOne}
   */
  async insert(collectionName, docs) {
    debug('getting db collection');
    docs = EJSON.deserialize(docs);
    const collection = await this.collection(collectionName);
    return insertOneOrMany(collection, Array.isArray(docs), docs);
  }

  /**
   * Updates docs in a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {object} query - Query to find which documents to update.
   * @param {object} update - Document properties to update.
   * @param {object} [options] - Optional settings see mongo documentation
   * @param {boolean} [options.multiple=false] - Should multiple documents be updated.
   * @param {boolean} [options.upsert=false] - Should documents be inserted if they don't already exist..
   * @returns {Promise} resolves with an object with results, and ops as keys
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#updateMany}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#updateOne}
   */
  async update(collectionName, query, update, options) {
    options = defaults(options, {
      multiple: false,
      upsert: false
    });
    debug('getting db collection');
    query = EJSON.deserialize(query);
    update = EJSON.deserialize(update);
    const collection = await this.collection(collectionName);
    const updateOptions = { upsert: options.upsert };
    return updateOneOrMany(collection, options.multiple, query, update, updateOptions);
  }

  /**
   * Removes docs from a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {object} query - Query to find which documents to remove.
   * @param {object} [options] - Optional settings see mongo documentation
   * @param {boolean} [options.multiple=false] - Should multiple documents be removed.
   * @returns {Promise} resolves with an object with results, and ops as keys
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#deleteMany}
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#deleteOne}
   */
  async remove(collectionName, query, options) {
    options = defaults(options, {
      multiple: false
    });
    debug('getting db collection');
    query = EJSON.deserialize(query);
    const collection = await this.collection(collectionName);
    return removeOneOrMany(collection, options.multiple, query);
  }

  /**
   * Executes aggregation pipeline against a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {object} pipeline - Aggregation pipeline.
   * @param {object} [options] - Optional settings see mongo documentation
   * @param {boolean} [options.explain=false] - Should should the execution plan be returned.
   * @returns {Promise} resovles with the result of the aggregatation from mongo
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#aggregate}
   */
  async aggregate(collectionName, pipeline, options) {
    options = defaults(options, {
      explain: false
    });
    debug('getting db collection');
    const collection = await this.collection(collectionName);
    return new Promise((resolve, reject) => {
      collection.aggregate(pipeline, options, function(err, r) {
        if (err) { return reject(err); }
        return resolve(r);
      });
    });
  }

  /**
   * Returns list of unique values for the given key across a collection.
   * @param {string} collectionName - Name of the collection.
   * @param {string} key - Document property.
   * @param {object} query - Query to find which documents evaluate.
   * @param {object} [options] - Optional settings see mongo documentation
   * @returns {Promise} resovles with the result of the distinct query from mongo
   * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#distinct}
   */
  async distinct(collectionName, key, query, options) {
    debug('getting db collection');
    query = EJSON.deserialize(query);
    const collection = await this.collection(collectionName);
    return collection.distinct(key, query, options);
  }

  async replace(collectionName, query, replace) {
    const collection = await this.collection(collectionName);
    query = EJSON.deserialize(query);
    replace = EJSON.deserialize(replace);
    debug(`db.${collection.collectionName}.replaceOne`, query, replace);
    const r = await collection.replaceOne(query, replace);
    return normalizeResult(r);
  }

}

module.exports = new Database();
<|endoftext|>


<commit_before>// layerswitcher to different projections uses following functions not currently exported:
// source.getProjection(); vectorLayer.getFeatures();

var ol = require('ol');
require('./css/ol.css!');

// 1 view per projection
var map, views = {};
// create layer switcher div
var layersDiv = document.createElement('div');
layersDiv.id = 'layerswitch';

function addLayersDiv(div, layersDiv, name) {
  var label = document.createElement('div');
  label.innerHTML = name;
  layersDiv.appendChild(label);
  layersDiv.appendChild(div);
}

/**
 * params: options (center, zoom and rotation),
 *        projection code, extent, resolutions array from layer def
 * returns ol.View2D instance
 */
function createView(options, projCode, extent, resolutions) {
  // default center is center of raster extent; default zoom 0
  var defaultCenter = extent ? ol.extent.getCenter(extent)
      : [0, 0];
  var center = options.center ?
      ol.proj.transform([options.center.lon, options.center.lat], 'EPSG:4326', projCode)
      : defaultCenter;
  var viewOptions = {
    center: center,
    zoom: options.zoom || 0
  };
  if (projCode) {
    viewOptions.projection = projCode;
  }
  if (resolutions) {
    viewOptions.resolutions = resolutions;
  }
  if (options.rotation) {
    viewOptions.rotation = options.rotation;
  }
  return new ol.View2D(viewOptions);
}

// create a default div for the map
function createMapTarget(noKeyboardPan) {
  var mapDiv = document.createElement('div');
  mapDiv.id = 'map';
  mapDiv.style.height = '400px'; // make configurable
  if (!noKeyboardPan) {
    mapDiv.tabIndex=0; // so can use keyboard pan/zoom
  }
  document.body.appendChild(mapDiv);
  return mapDiv;
}

function switcherRasterHandler(evt) {
  // uses 'map' as closure
  // click may be on label or checkbox
  var target = document.getElementById(evt.target.htmlFor) || evt.target;
  var oldLayers = map.getLayers().getArray().filter(function(l) {
    return l.activeLayer === true;
  });
  var newLayers = map.getLayers().getArray().filter(function(l) {
    return l.get('id') == target.value;
  });
  for (var i = 0; i < oldLayers.length; i++) {
    oldLayers[i].setVisible(false);
    oldLayers[i].activeLayer = false;
  }
  for (i = 0; i < newLayers.length; i++) {
    newLayers[i].setVisible(true);
    newLayers[i].activeLayer = true;
  }
  // FIXME getProjection not currently exported
  var newProjCode = newLayers[0].getSource().getProjection().getCode();
  var oldProjCode = oldLayers[0].getSource().getProjection().getCode();
  if (newProjCode != oldProjCode) {
    // new projection, so change view and reproject vectors
    var oldExtent = map.getView().calculateExtent(map.getSize());
    var transformer = ol.proj.getTransform(oldProjCode, newProjCode);
    var newExtent = ol.extent.transform(oldExtent, transformer);
    var vectorLayers = map.getLayers().getArray().filter(function(l) {
      return l instanceof ol.layer.Vector;
    });
    for (i = 0; i < vectorLayers.length; i++) {
      // FIXME not in api
      var features = vectorLayers[i].getFeatures();
      for (var feature in features) {
        features[feature].getGeometry().transform(transformer);
      }
    }
    map.setView(views[newProjCode]);
    map.getView().fitExtent(newExtent, map.getSize());
  }
}

function switcherVectorHandler(evt) {
  // uses 'map' as closure
  // click may be on label or checkbox
  var target = document.getElementById(evt.target.htmlFor) || evt.target;
  var newLayer = map.getLayers().getArray().filter(function(l) {
    return l.get('id') == target.value;
  })[0]; // only one
  newLayer.setVisible(target.checked);
}

function addRaster(raster, options, rastersDiv, apikey, active) {
  var projCode = raster.projCode || 'EPSG:3857';
  if (!views[projCode]) {
    // assumes all layers for 1 projection have same extent/resolutions
    views[projCode] = createView(options, projCode, raster.extent, raster.resolutions);
  }
  var layers = raster.getLayers({
    zoom: options.zoom,
    apikey: apikey
  }), layer; // zoom may be needed to set param
  // a raster def may have >1 layer, but if so all should be treated as 1
  for (var j = 0; j < layers.length; j++) {
    layer = layers[j];
    // set layers invisible to start with
    layer.setVisible(false);
    if (active) {
      layer.activeLayer = true;
      // if no vectors, make 1st raster visible
      if (!options.vectors) {
        layer.setVisible(true);
      }
    } else {
      layer.activeLayer = false;
    }
    // add layer to map
    map.addLayer(layer);

  }
  // create layerswitcher element
  var inputElem = document.createElement('input');
  inputElem.name = 'rasters';
  inputElem.value = inputElem.id = layer.get('id');
  inputElem.type = 'radio';
  if (active) {
    inputElem.checked = true;
  }
  var label = document.createElement('label');
  label.htmlFor = label.innerHTML = inputElem.value;
  label.style.verticalAlign = 'bottom';
  var div = document.createElement('div');
  div.appendChild(inputElem);
  div.appendChild(label);
  div.onclick = switcherRasterHandler;
  rastersDiv.appendChild(div);

  if (raster.viewEvent) {
    views[projCode].on(raster.viewEvent.type, raster.viewEvent.func);
  }
}

module.exports = {
  createMap: function(target, noKeyboardPan) {
    map = new ol.Map({
      renderer: ol.RendererHint.CANVAS, // currently only canvas handles vectors
      // create default target div if not defined in options
      target: target || createMapTarget(noKeyboardPan)
    });
  },

  /**
   * param: options.rasters (array of ids)
   *        rasterDefs (registry defs)
   * returns array with layers array, projs array, layersDiv
   */
  createRasters: function(rasterDefs, options) {
    var defaultView;
    // layerswitcher layer list and click handler
    var rastersDiv = document.createElement('div');
    rastersDiv.id = 'rasters';

    for (var i = 0; i < options.rasters.length; i++) {
      var rasterName, apikey, key;
        if (typeof options.rasters[i] === 'object') {
          for (key in options.rasters[i]) {
            rasterName = key; // only 1
            apikey = options.rasters[i][key];
          }
        } else {
          rasterName = options.rasters[i];
        }
      // this assumes correct raster definition loaded
      var raster = rasterDefs[rasterName];
      addRaster(raster, options, rastersDiv, apikey, i === 0); // #0 is active
        // 1st one is default
        if (i === 0) {
          defaultView = raster.projCode;
        }
    }
    // use view for 1st layer/projection
    map.setView(views[defaultView]);
    addLayersDiv(rastersDiv, layersDiv, 'Rasters'); // FIXME English
  },

  addRaster: addRaster,

  create4326View: function(options) {
    // vectors only, so use 4326 view
    var projCode = 'EPSG:4326';
    views[projCode] = createView(options, projCode);
    // defaultView = projCode;
  },

  setDefaultView: function(defaultView) {
    map.setView(views[defaultView]);
  },

  /**
   * param: options.vectors (array of ids)
   * returns array of layers array and layersDiv
   */
  createVectors: function(vectors) {
    var vectorsDiv = document.createElement('div');
    vectorsDiv.id = 'vectors';
    // function for click handler

    for (var i = 0, s = vectors; i < s.length; i++) {
      var parser, url = s[i].url;
      if (!s[i].parser) {
        // define parser from file extension
        var parsers = {
          geojson: 'GeoJSON',
          gpx: 'GPX',
          kml: 'KML'
        };
        var ext = url.substring(url.lastIndexOf('.')+1);
        parser = parsers[ext];
      }
      parser = s[i].parser || parser;
      var vectOpts = {
        source: new ol.source.Vector({
          url: url,
          attributions: [new ol.Attribution({
            html: s[i].attribution
          })],
          parser: new ol.parser[parser]()
        }),
        id: s[i].id
      };
      if (s[i].styles) {
        if (s[i].styles[0].stroke) { // FIXME >1
          var stroke = s[i].styles[0].stroke;
          // change default style
          vectOpts.style = new ol.style.Style({
            symbolizers: [
              new ol.style.Stroke({
                color: stroke.color,
                width: stroke.width,
                opacity: stroke.opacity
              })
            ]
          });
        }
      }
      map.addLayer(new ol.layer.Vector(vectOpts));

      // add to layerswitcher
      var inputElem = document.createElement('input');
      inputElem.name = inputElem.id = inputElem.value = s[i].id;
      inputElem.type = 'checkbox';
      inputElem.checked = true;
      var label = document.createElement('label');
      label.htmlFor = label.innerHTML = inputElem.value;
      label.style.verticalAlign = 'baseline';
      var div = document.createElement('div');
      div.appendChild(inputElem);
      div.appendChild(label);
      div.onclick = switcherVectorHandler;
      vectorsDiv.appendChild(div);
    }
    addLayersDiv(vectorsDiv, layersDiv, 'Vectors'); // FIXME English
  },

  createView: createView,

  /**
   * Add event returns extent, so can use this to zoom to feature data extent.
   * Make first tile layer visible at this point, so tiles are only fetched after the
   * extent has been established and the appropriate zoom/resolution set.
   *
   * params: vectorLayers
   */
  addFeaturesListener: function () {
    var vectorLayers = map.getLayers().getArray().filter(function(l) {
      return l instanceof ol.layer.Vector;
    });
    var vectorsExtent = ol.extent.createEmpty();
    var sourcesRead = 0;
    // callback
    var callback = function(e) {
      // FIXME hack until https://github.com/openlayers/ol3/issues/1134 fixed
      ol.extent.extend(vectorsExtent, e.extents ? e.extents[0] : e.a[0]);
      sourcesRead++;
      if (sourcesRead == vectorLayers.length) {
        // we've loaded all the sources
        // so zoom to data extent and make 1st raster layer visible
        map.getView().fitExtent(vectorsExtent, map.getSize());
        var rasterLayer0 = map.getLayers().getArray().filter(function(l) {
          return l.activeLayer === true;
        })[0]; // only one
        if (rasterLayer0) {
          rasterLayer0.setVisible(true);
        }
        var status = document.getElementById('status');
        if (status) {
          status.style.display = 'none';
        }
      }
    };
    for (var i = 0; i < vectorLayers.length; i++) {
      vectorLayers[i].on('featureadd', callback);
    }
  },

  /**
   * param: options.widgets (object with each required widget set to true)
   * returns: array of widgets
   * TODO allow setting options for each widget/control
   */
  addWidgets: function(widgets, widgetDir) {
    for (var widget in widgets) {
      jspm.import(widgetDir + widget);
    }
  },

  make1stLayerVisible: function() {
    // may be >1
    map.getLayers().getArray().filter(function(l) {
      return l.activeLayer === true;
    }).forEach(function(e){e.setVisible(true)});
  },

  setFocus: function() {
    map.getTarget().focus();
  },

  getLayersDiv: function() {
    return layersDiv;
  },

  get: function() {
    return map;
  }
};<commit_msg>Fix layerswitcher evt handling bug<commit_after>// layerswitcher to different projections uses following functions not currently exported:
// source.getProjection(); vectorLayer.getFeatures();

var ol = require('ol');
require('./css/ol.css!');

// 1 view per projection
var map, views = {};
// create layer switcher div
var layersDiv = document.createElement('div');
layersDiv.id = 'layerswitch';

function addLayersDiv(div, layersDiv, name) {
  var label = document.createElement('div');
  label.innerHTML = name;
  layersDiv.appendChild(label);
  layersDiv.appendChild(div);
}

/**
 * params: options (center, zoom and rotation),
 *        projection code, extent, resolutions array from layer def
 * returns ol.View2D instance
 */
function createView(options, projCode, extent, resolutions) {
  // default center is center of raster extent; default zoom 0
  var defaultCenter = extent ? ol.extent.getCenter(extent)
      : [0, 0];
  var center = options.center ?
      ol.proj.transform([options.center.lon, options.center.lat], 'EPSG:4326', projCode)
      : defaultCenter;
  var viewOptions = {
    center: center,
    zoom: options.zoom || 0
  };
  if (projCode) {
    viewOptions.projection = projCode;
  }
  if (resolutions) {
    viewOptions.resolutions = resolutions;
  }
  if (options.rotation) {
    viewOptions.rotation = options.rotation;
  }
  return new ol.View2D(viewOptions);
}

// create a default div for the map
function createMapTarget(noKeyboardPan) {
  var mapDiv = document.createElement('div');
  mapDiv.id = 'map';
  mapDiv.style.height = '400px'; // make configurable
  if (!noKeyboardPan) {
    mapDiv.tabIndex=0; // so can use keyboard pan/zoom
  }
  document.body.appendChild(mapDiv);
  return mapDiv;
}

function switcherRasterHandler(evt) {
  // uses 'map' as closure
  // click may be on label or checkbox
  var target = document.getElementById(evt.target.htmlFor) || evt.target;
  if (!target.value) {
    return; // do nothing if not label or checkbox
  }
  var oldLayers = map.getLayers().getArray().filter(function(l) {
    return l.activeLayer === true;
  });
  var newLayers = map.getLayers().getArray().filter(function(l) {
    return l.get('id') == target.value;
  });
  for (var i = 0; i < oldLayers.length; i++) {
    oldLayers[i].setVisible(false);
    oldLayers[i].activeLayer = false;
  }
  for (i = 0; i < newLayers.length; i++) {
    newLayers[i].setVisible(true);
    newLayers[i].activeLayer = true;
  }
  // FIXME getProjection not currently exported
  var newProjCode = newLayers[0].getSource().getProjection().getCode();
  var oldProjCode = oldLayers[0].getSource().getProjection().getCode();
  if (newProjCode != oldProjCode) {
    // new projection, so change view and reproject vectors
    var oldExtent = map.getView().calculateExtent(map.getSize());
    var transformer = ol.proj.getTransform(oldProjCode, newProjCode);
    var newExtent = ol.extent.transform(oldExtent, transformer);
    var vectorLayers = map.getLayers().getArray().filter(function(l) {
      return l instanceof ol.layer.Vector;
    });
    for (i = 0; i < vectorLayers.length; i++) {
      // FIXME not in api
      var features = vectorLayers[i].getFeatures();
      for (var feature in features) {
        features[feature].getGeometry().transform(transformer);
      }
    }
    map.setView(views[newProjCode]);
    map.getView().fitExtent(newExtent, map.getSize());
  }
}

function switcherVectorHandler(evt) {
  // uses 'map' as closure
  // click may be on label or checkbox
  var target = document.getElementById(evt.target.htmlFor) || evt.target;
  var newLayer = map.getLayers().getArray().filter(function(l) {
    return l.get('id') == target.value;
  })[0]; // only one
  newLayer.setVisible(target.checked);
}

function addRaster(raster, options, rastersDiv, apikey, active) {
  var projCode = raster.projCode || 'EPSG:3857';
  if (!views[projCode]) {
    // assumes all layers for 1 projection have same extent/resolutions
    views[projCode] = createView(options, projCode, raster.extent, raster.resolutions);
  }
  var layers = raster.getLayers({
    zoom: options.zoom,
    apikey: apikey
  }), layer; // zoom may be needed to set param
  // a raster def may have >1 layer, but if so all should be treated as 1
  for (var j = 0; j < layers.length; j++) {
    layer = layers[j];
    // set layers invisible to start with
    layer.setVisible(false);
    if (active) {
      layer.activeLayer = true;
      // if no vectors, make 1st raster visible
      if (!options.vectors) {
        layer.setVisible(true);
      }
    } else {
      layer.activeLayer = false;
    }
    // add layer to map
    map.addLayer(layer);

  }
  // create layerswitcher element
  var inputElem = document.createElement('input');
  inputElem.name = 'rasters';
  inputElem.value = inputElem.id = layer.get('id');
  inputElem.type = 'radio';
  if (active) {
    inputElem.checked = true;
  }
  var label = document.createElement('label');
  label.htmlFor = label.innerHTML = inputElem.value;
  label.style.verticalAlign = 'bottom';
  var div = document.createElement('div');
  div.appendChild(inputElem);
  div.appendChild(label);
  div.onclick = switcherRasterHandler;
  rastersDiv.appendChild(div);

  if (raster.viewEvent) {
    views[projCode].on(raster.viewEvent.type, raster.viewEvent.func);
  }
}

module.exports = {
  createMap: function(target, noKeyboardPan) {
    map = new ol.Map({
      renderer: ol.RendererHint.CANVAS, // currently only canvas handles vectors
      // create default target div if not defined in options
      target: target || createMapTarget(noKeyboardPan)
    });
  },

  /**
   * param: options.rasters (array of ids)
   *        rasterDefs (registry defs)
   * returns array with layers array, projs array, layersDiv
   */
  createRasters: function(rasterDefs, options) {
    var defaultView;
    // layerswitcher layer list and click handler
    var rastersDiv = document.createElement('div');
    rastersDiv.id = 'rasters';

    for (var i = 0; i < options.rasters.length; i++) {
      var rasterName, apikey, key;
        if (typeof options.rasters[i] === 'object') {
          for (key in options.rasters[i]) {
            rasterName = key; // only 1
            apikey = options.rasters[i][key];
          }
        } else {
          rasterName = options.rasters[i];
        }
      // this assumes correct raster definition loaded
      var raster = rasterDefs[rasterName];
      addRaster(raster, options, rastersDiv, apikey, i === 0); // #0 is active
        // 1st one is default
        if (i === 0) {
          defaultView = raster.projCode;
        }
    }
    // use view for 1st layer/projection
    map.setView(views[defaultView]);
    addLayersDiv(rastersDiv, layersDiv, 'Rasters'); // FIXME English
  },

  addRaster: addRaster,

  create4326View: function(options) {
    // vectors only, so use 4326 view
    var projCode = 'EPSG:4326';
    views[projCode] = createView(options, projCode);
    // defaultView = projCode;
  },

  setDefaultView: function(defaultView) {
    map.setView(views[defaultView]);
  },

  /**
   * param: options.vectors (array of ids)
   * returns array of layers array and layersDiv
   */
  createVectors: function(vectors) {
    var vectorsDiv = document.createElement('div');
    vectorsDiv.id = 'vectors';
    // function for click handler

    for (var i = 0, s = vectors; i < s.length; i++) {
      var parser, url = s[i].url;
      if (!s[i].parser) {
        // define parser from file extension
        var parsers = {
          geojson: 'GeoJSON',
          gpx: 'GPX',
          kml: 'KML'
        };
        var ext = url.substring(url.lastIndexOf('.')+1);
        parser = parsers[ext];
      }
      parser = s[i].parser || parser;
      var vectOpts = {
        source: new ol.source.Vector({
          url: url,
          attributions: [new ol.Attribution({
            html: s[i].attribution
          })],
          parser: new ol.parser[parser]()
        }),
        id: s[i].id
      };
      if (s[i].styles) {
        if (s[i].styles[0].stroke) { // FIXME >1
          var stroke = s[i].styles[0].stroke;
          // change default style
          vectOpts.style = new ol.style.Style({
            symbolizers: [
              new ol.style.Stroke({
                color: stroke.color,
                width: stroke.width,
                opacity: stroke.opacity
              })
            ]
          });
        }
      }
      map.addLayer(new ol.layer.Vector(vectOpts));

      // add to layerswitcher
      var inputElem = document.createElement('input');
      inputElem.name = inputElem.id = inputElem.value = s[i].id;
      inputElem.type = 'checkbox';
      inputElem.checked = true;
      var label = document.createElement('label');
      label.htmlFor = label.innerHTML = inputElem.value;
      label.style.verticalAlign = 'baseline';
      var div = document.createElement('div');
      div.appendChild(inputElem);
      div.appendChild(label);
      div.onclick = switcherVectorHandler;
      vectorsDiv.appendChild(div);
    }
    addLayersDiv(vectorsDiv, layersDiv, 'Vectors'); // FIXME English
  },

  createView: createView,

  /**
   * Add event returns extent, so can use this to zoom to feature data extent.
   * Make first tile layer visible at this point, so tiles are only fetched after the
   * extent has been established and the appropriate zoom/resolution set.
   *
   * params: vectorLayers
   */
  addFeaturesListener: function () {
    var vectorLayers = map.getLayers().getArray().filter(function(l) {
      return l instanceof ol.layer.Vector;
    });
    var vectorsExtent = ol.extent.createEmpty();
    var sourcesRead = 0;
    // callback
    var callback = function(e) {
      // FIXME hack until https://github.com/openlayers/ol3/issues/1134 fixed
      ol.extent.extend(vectorsExtent, e.extents ? e.extents[0] : e.a[0]);
      sourcesRead++;
      if (sourcesRead == vectorLayers.length) {
        // we've loaded all the sources
        // so zoom to data extent and make 1st raster layer visible
        map.getView().fitExtent(vectorsExtent, map.getSize());
        var rasterLayer0 = map.getLayers().getArray().filter(function(l) {
          return l.activeLayer === true;
        })[0]; // only one
        if (rasterLayer0) {
          rasterLayer0.setVisible(true);
        }
        var status = document.getElementById('status');
        if (status) {
          status.style.display = 'none';
        }
      }
    };
    for (var i = 0; i < vectorLayers.length; i++) {
      vectorLayers[i].on('featureadd', callback);
    }
  },

  /**
   * param: options.widgets (object with each required widget set to true)
   * returns: array of widgets
   * TODO allow setting options for each widget/control
   */
  addWidgets: function(widgets, widgetDir) {
    for (var widget in widgets) {
      jspm.import(widgetDir + widget);
    }
  },

  make1stLayerVisible: function() {
    // may be >1
    map.getLayers().getArray().filter(function(l) {
      return l.activeLayer === true;
    }).forEach(function(e){e.setVisible(true)});
  },

  setFocus: function() {
    map.getTarget().focus();
  },

  getLayersDiv: function() {
    return layersDiv;
  },

  get: function() {
    return map;
  }
};<|endoftext|>


<commit_before>/*
 * apps.js: Commands related to app resources
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var analyzer = require('require-analyzer'),
    jitsu = require('../../jitsu');

var apps = exports;

apps.usage = [
  '`jitsu apps *` commands allow you to work with your',
  'Applications on Nodejitsu. Valid commands are:',
  '',
  'jitsu apps create',
  'jitsu apps list',
  'jitsu apps deploy',
  'jitsu apps view    [<name>]',
  'jitsu apps update  [<name>]',
  'jitsu apps destroy [<name>]',
  'jitsu apps start   [<name>]',
  'jitsu apps restart [<name>]',
  'jitsu apps stop    [<name>]',
  '',
  'For commands that take a <name> parameter, if no parameter',
  'is supplied, jitsu will attempt to read the package.json',
  'from the current directory.'
];

//
// ### function deploy (callback)
// #### @callback {function} Continuation to respond to when complete.
// Deploys an application through the following steps:
// 1. Creates the application (if necessary)
// 2. Creates or validates the package.json
// 3. Packages and creates a new snapshot
// 4. Stops the application (if necessary)
// 5. Starts the application
//
apps.deploy = function (callback) {
  var dir = process.cwd(), pkg;

  function startApp(err, existing) {
    if (err) {
      jitsu.log.error('Error creating snapshot for app ' + pkg.name.magenta);
      jitsu.log.error(err.message);
      return callback(new Error(), true);
    }

    //if (existing.state === 'started') {
    //
    // existing.state is thrown away in check app.
    // these closures would need to be rearanged so that this closure can see the result of view..
    //
    apps.stop(existing.name, function (err) {
      return err ? callback(err) : apps.start(existing.name, callback);
    });
    //}
    //else {
    //  apps.start(existing.name, callback);
    //}
  }

  function activateSnapshot(existing, snapshot) {
    jitsu.log.info('Activating snapshot ' + snapshot.grey + ' for ' + existing.name.magenta);
    jitsu.snapshots.activate(existing.name, snapshot, function (err) {
      jitsu.log.silly('Done activating snapshot ' + snapshot.grey);
      startApp(err, existing);
    });
  }

  function updateApp(existing, snapshot) {
    jitsu.log.info('Updating application ' + existing.name.magenta);
    jitsu.apps.update(existing.name, pkg, function (err) {
      if (err) {
        return callback(err);
      }

      activateSnapshot(existing, snapshot);
    });
  }

  function updateSnapshot(err, existing, firstSnapshot) {
    if (err) {
      return callback(err, true, true);
    }

    jitsu.package.updateTarball(null, pkg, existing, firstSnapshot, function (err, snapshot) {
      if (err) {
        return callback(err, snapshot);
      }

      updateApp(existing, snapshot);
    });
  }

  function checkApp(err, local) {
    if (err) {
      jitsu.log.error(err);
      return callback(new Error(), true);
    }

    pkg = local;
    jitsu.log.silly('Checking if application ' + local.name.magenta + ' exists.');
    jitsu.apps.view(local.name, function (err, app) {
      var pkg;

      if (err) {
        if (err.result.error === 'not_found') {

          jitsu.log.warn('No application exists for ' + local.name.magenta);
          //
          // If there is a before filter we should run before creating the app do so,
          // then create the application.
          //
          apps.create(local, function (err, existing) {
            updateSnapshot(err, existing, true);
          });
        }
        else if (err.result.statusCode === 403) {
          jitsu.commands.users.login(function (err) {
            if (err) {
              return callback(err, true);
            }

            pkg = analyzer.merge({}, local);
            jitsu.package.validate(pkg, dir, updateSnapshot);
          });
        }
        else {
          return callback(err, true);
        }
      }
      else {
        pkg = analyzer.merge({}, local);
        jitsu.package.validate(pkg, dir, updateSnapshot);
      }
    });
  }

  jitsu.package.read(dir, function (err, local) {
    if (err) {
      return jitsu.package.get(dir, checkApp);
    }
    else {

      return jitsu.package.validate(local, dir, {}, checkApp);
    }
  });
};

apps.deploy.usage = [
  '',
  'Deploys an application using the following steps:'.cyan.underline,
  '',
  '  1. Creates the application (if necessary)',
  '  2. Creates or validates the package.json',
  '  3. Packages and creates a new snapshot',
  '  4. Stops the application (if necessary)',
  '  5. Starts the application',
  '',
  'jitsu deploy',
  'jitsu apps deploy'
];

//
// ### function list (callback)
// #### @target {string|Object} **optional** Name of the application to create
// #### @callback {function} Continuation to pass control to when complete.
// Creates an application for the package.json in the current directory
// using `name` if supplied and falling back to `package.name`.
//
apps.create = function (target, callback) {
  var dir = process.cwd();

  function createApp(err, pkg) {
    if (err) {
      return callback(err);
    }
    jitsu.log.info('Creating app ' + pkg.name.magenta);
    jitsu.apps.create(pkg, function (err, res, result) {
      if (err) {
        jitsu.log.error('Error creating ' + pkg.name.magenta);
        jitsu.log.error(err.message);
        return callback(err);
      }

      jitsu.log.silly('Done creating app ' + pkg.name.magenta);
      return callback(null, pkg);
    });
  }

  function executeCreate(err, pkg) {
    if (err) {
      return callback(err);
    }

    pkg.state = 'stopped';
    jitsu.log.info('Checking app availability ' + pkg.name.magenta);
    jitsu.package.available(pkg, dir, createApp, executeCreate);
  }

  if (target && typeof target === 'object') {
    return executeCreate(null, target);
  }

  jitsu.package.get(process.cwd(), executeCreate);
};

//
// Usage for `jitsu apps create [<name>]`
//
apps.create.usage = [
  'Creates an application using the package.json file in the',
  'current directory, if <name> is supplied then this is used',
  'instead of the `name` property in the package.json file.',
  '',
  'jitsu create',
  'jitsu apps create',
  'jitsu apps create <name>'
];

//
// ### function list (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Lists the applications for the authenticated user.
//
apps.list = function (username, callback) {
  jitsu.log.info('Listing apps');
  username = username || jitsu.config.get('username');

  jitsu.apps.list(username, function (err, apps) {
    if (err) {
      console.log(err);
      return callback(err);
    }

    if (!apps || apps.length === 0) {
      jitsu.log.warn('No applications exist.');
      jitsu.log.help('Try creating one with ' + 'jitsu apps create'.magenta);
      return callback();
    }

    var rows = [['name', 'state', 'subdomain', 'start', 'latest']],
        colors = ['underline', 'underline', 'underline', 'underline', 'underline'];

    apps.forEach(function (app) {
      rows.push([
        app.name,
        app.state || 'stopped',
        app.subdomain,
        app.scripts.start || '---',
        app.snapshots && app.snapshots.length > 0 ? app.snapshots[app.snapshots.length - 1].filename : '---'
      ]);
    });

    jitsu.inspect.putRows('data', rows, colors);
    callback();
  });
};

//
// Usage for `jitsu apps list`
//
apps.list.usage = [
  'Lists all of the applications for the current user',
  '',
  'jitsu list',
  'jitsu apps list'
];

//
// ### function view (name, callback)
// #### @name {string} **optional** Name of the application to view
// #### @callback {function} Continuation to pass control to when complete.
// Views the application with the specfied `name` for the authenticated user.
// If no name is supplied this will view the application in the current directory.
//
apps.view = function (name, callback) {

  function executeView() {
    jitsu.apps.view(name, function (err, app) {
      if (err) {
        return callback(err);
      }

      app = jitsu.common.formatApp(app);
      jitsu.inspect.putObject(app);
      callback(null, app);
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeView();
    });
  }

  executeView();
};

//
// Usage for `jitsu apps view [<name>]`
//
apps.view.usage = [
  'Lists the information for the application in the current',
  'directory. If <name> is supplied then that application',
  'is listed instead.',
  '',
  'jitsu apps view',
  'jitsu apps view <name>'
];

//
// ### function list (callback)
// #### @name {string} **optional** Name of the application to create
// #### @callback {function} Continuation to pass control to when complete.
// Lists the applications for the authenticated user.
//
apps.update = function (name, callback) {
  jitsu.log.silly('Reading package.json in ' + process.cwd());
  jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
    name = name || pkg.name;

    jitsu.apps.view(name, function (err, app) {
      if (err) {
        return callback(err);
      }

      var diff = jitsu.common.objectDiff(app, pkg);
      if (!diff) {
        jitsu.log.warn('No changes found to your package.json for ' + name.magenta);
        return callback();
      }

      jitsu.log.info('Updating application ' + name.magenta + ' with:');
      jitsu.inspect.putObject(diff);

      jitsu.apps.update(name, diff, callback);
    });
  });
};

//
// Usage for `jitsu apps update [<name>]`
//
apps.update.usage = [
  'Updates the application in the current directory',
  'with the information in the package.json file. If',
  '<name> is supplied the application with <name> is updated.',
  '',
  'jitsu apps update',
  'jitsu apps update <name>'
];

//
// ### function destroy (callback)
// #### @name {string} **optional** Name of the application to destroy.
// #### @callback {function} Continuation to pass control to when complete.
// Destroys the application specified by `name`. If no name is supplied
// this will destroy the application in the current directory.
//
apps.destroy = function (name, callback) {

  function executeDestroy() {
    jitsu.log.info('Destroying app ' + name.magenta);
    jitsu.apps.destroy(name, function (err) {
      jitsu.log.silly('Done destroying app ' + name.magenta);
      return err ? callback(err) : callback();
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeDestroy();
    });
  }

  executeDestroy();
};

//
// Usage for `jitsu apps destroy [<name>]`
//
apps.destroy.destructive = true
apps.destroy.usage = [
  'Destroys the application in the current directory. If',
  '<name> is supplied then that application is destroyed instead',
  '',
  'jitsu apps destroy',
  'jitsu apps destroy <name>'
];

//
// ### function start (callback)
// #### @name {string} **optional** Name of the application to start.
// #### @callback {function} Continuation to pass control to when complete.
// Starts the application specified by `name`. If no name is supplied
// this will start the application in the current directory.
//
apps.start = function (name, callback) {

  function executeStart() {
    jitsu.log.info('Starting app ' + name.magenta);
    jitsu.apps.start(name, function (err) {
      if (err) {
        return callback(err, true);
      }

      jitsu.apps.view(name, function (err, app) {
        if (err) {
          return callback(err);
        }

        jitsu.log.info('App ' + name.magenta + ' is now started')
        jitsu.log.info(('http://' + app.subdomain + '.nodejitsu.com').magenta + ' on Port ' + '80'.magenta);
        callback();
      });
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeStart();
    });
  }

  executeStart();
};

//
// Usage for `jitsu apps start [<name>]`
//
apps.start.usage = [
  'Starts the application in the current directory. If <name>',
  'is supplied then that application is started instead.',
  '',
  'jitsu apps start',
  'jitsu apps start <name>'
];

//
// ### function restart (name, callback)
// #### @name {string} **optional** Name of the application to restart.
// #### @callback {function} Continuation to pass control to when complete.
// Restarts the application specified by `name`. If no name is supplied
// this will restart the application in the current directory.
//
apps.restart = function (name, callback) {

  function executeRestart() {
    jitsu.log.info('Restarting app ' + name.magenta);
    jitsu.apps.restart(name, function (err) {
      if (err) {
        return callback(err);
      }

      jitsu.log.info('App ' + name.magenta + ' has been restarted');
      callback();
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeRestart();
    });
  }

  executeRestart();
};

//
// Usage for `jitsu apps restart [<name>]`
//
apps.restart.usage = [
  'Restarts the application in the current directory. If <name>',
  'is supplied then that application is restarted instead.',
  '',
  'jitsu apps restart',
  'jitsu apps restart <name>'
];

//
// ### function stop (callback)
// #### @name {string} **optional** Name of the application to stop.
// #### @callback {function} Continuation to pass control to when complete.
// Stops the application specified by `name`. If no name is supplied
// this will stop the application in the current directory.
//
apps.stop = function (name, callback) {

  function executeStop() {
    jitsu.log.info('Stopping app ' + name.magenta);
    jitsu.apps.stop(name, function (err) {
      if (err) {
        return callback(err);
      }

      jitsu.log.info('App ' + name.magenta + ' is now stopped');
      callback();
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeStop();
    });
  }

  executeStop();
};

//
// Usage for `jitsu apps stop [<name>]`
//
apps.stop.usage = [
  'Stops the application in the current directory. If <name>',
  'is supplied then that application is stopped instead.',
  '',
  'jitsu apps stop',
  'jitsu apps stop <name>'
];
<commit_msg>[minor] Errant console.log<commit_after>/*
 * apps.js: Commands related to app resources
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var analyzer = require('require-analyzer'),
    jitsu = require('../../jitsu');

var apps = exports;

apps.usage = [
  '`jitsu apps *` commands allow you to work with your',
  'Applications on Nodejitsu. Valid commands are:',
  '',
  'jitsu apps create',
  'jitsu apps list',
  'jitsu apps deploy',
  'jitsu apps view    [<name>]',
  'jitsu apps update  [<name>]',
  'jitsu apps destroy [<name>]',
  'jitsu apps start   [<name>]',
  'jitsu apps restart [<name>]',
  'jitsu apps stop    [<name>]',
  '',
  'For commands that take a <name> parameter, if no parameter',
  'is supplied, jitsu will attempt to read the package.json',
  'from the current directory.'
];

//
// ### function deploy (callback)
// #### @callback {function} Continuation to respond to when complete.
// Deploys an application through the following steps:
// 1. Creates the application (if necessary)
// 2. Creates or validates the package.json
// 3. Packages and creates a new snapshot
// 4. Stops the application (if necessary)
// 5. Starts the application
//
apps.deploy = function (callback) {
  var dir = process.cwd(), pkg;

  function startApp(err, existing) {
    if (err) {
      jitsu.log.error('Error creating snapshot for app ' + pkg.name.magenta);
      jitsu.log.error(err.message);
      return callback(new Error(), true);
    }

    //if (existing.state === 'started') {
    //
    // existing.state is thrown away in check app.
    // these closures would need to be rearanged so that this closure can see the result of view..
    //
    apps.stop(existing.name, function (err) {
      return err ? callback(err) : apps.start(existing.name, callback);
    });
    //}
    //else {
    //  apps.start(existing.name, callback);
    //}
  }

  function activateSnapshot(existing, snapshot) {
    jitsu.log.info('Activating snapshot ' + snapshot.grey + ' for ' + existing.name.magenta);
    jitsu.snapshots.activate(existing.name, snapshot, function (err) {
      jitsu.log.silly('Done activating snapshot ' + snapshot.grey);
      startApp(err, existing);
    });
  }

  function updateApp(existing, snapshot) {
    jitsu.log.info('Updating application ' + existing.name.magenta);
    jitsu.apps.update(existing.name, pkg, function (err) {
      if (err) {
        return callback(err);
      }

      activateSnapshot(existing, snapshot);
    });
  }

  function updateSnapshot(err, existing, firstSnapshot) {
    if (err) {
      return callback(err, true, true);
    }

    jitsu.package.updateTarball(null, pkg, existing, firstSnapshot, function (err, snapshot) {
      if (err) {
        return callback(err, snapshot);
      }

      updateApp(existing, snapshot);
    });
  }

  function checkApp(err, local) {
    if (err) {
      jitsu.log.error(err);
      return callback(new Error(), true);
    }

    pkg = local;
    jitsu.log.silly('Checking if application ' + local.name.magenta + ' exists.');
    jitsu.apps.view(local.name, function (err, app) {
      var pkg;

      if (err) {
        if (err.result.error === 'not_found') {

          jitsu.log.warn('No application exists for ' + local.name.magenta);
          //
          // If there is a before filter we should run before creating the app do so,
          // then create the application.
          //
          apps.create(local, function (err, existing) {
            updateSnapshot(err, existing, true);
          });
        }
        else if (err.result.statusCode === 403) {
          jitsu.commands.users.login(function (err) {
            if (err) {
              return callback(err, true);
            }

            pkg = analyzer.merge({}, local);
            jitsu.package.validate(pkg, dir, updateSnapshot);
          });
        }
        else {
          return callback(err, true);
        }
      }
      else {
        pkg = analyzer.merge({}, local);
        jitsu.package.validate(pkg, dir, updateSnapshot);
      }
    });
  }

  jitsu.package.read(dir, function (err, local) {
    if (err) {
      return jitsu.package.get(dir, checkApp);
    }
    else {

      return jitsu.package.validate(local, dir, {}, checkApp);
    }
  });
};

apps.deploy.usage = [
  '',
  'Deploys an application using the following steps:'.cyan.underline,
  '',
  '  1. Creates the application (if necessary)',
  '  2. Creates or validates the package.json',
  '  3. Packages and creates a new snapshot',
  '  4. Stops the application (if necessary)',
  '  5. Starts the application',
  '',
  'jitsu deploy',
  'jitsu apps deploy'
];

//
// ### function list (callback)
// #### @target {string|Object} **optional** Name of the application to create
// #### @callback {function} Continuation to pass control to when complete.
// Creates an application for the package.json in the current directory
// using `name` if supplied and falling back to `package.name`.
//
apps.create = function (target, callback) {
  var dir = process.cwd();

  function createApp(err, pkg) {
    if (err) {
      return callback(err);
    }
    jitsu.log.info('Creating app ' + pkg.name.magenta);
    jitsu.apps.create(pkg, function (err, res, result) {
      if (err) {
        jitsu.log.error('Error creating ' + pkg.name.magenta);
        jitsu.log.error(err.message);
        return callback(err);
      }

      jitsu.log.silly('Done creating app ' + pkg.name.magenta);
      return callback(null, pkg);
    });
  }

  function executeCreate(err, pkg) {
    if (err) {
      return callback(err);
    }

    pkg.state = 'stopped';
    jitsu.log.info('Checking app availability ' + pkg.name.magenta);
    jitsu.package.available(pkg, dir, createApp, executeCreate);
  }

  if (target && typeof target === 'object') {
    return executeCreate(null, target);
  }

  jitsu.package.get(process.cwd(), executeCreate);
};

//
// Usage for `jitsu apps create [<name>]`
//
apps.create.usage = [
  'Creates an application using the package.json file in the',
  'current directory, if <name> is supplied then this is used',
  'instead of the `name` property in the package.json file.',
  '',
  'jitsu create',
  'jitsu apps create',
  'jitsu apps create <name>'
];

//
// ### function list (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Lists the applications for the authenticated user.
//
apps.list = function (username, callback) {
  jitsu.log.info('Listing apps');
  username = username || jitsu.config.get('username');

  jitsu.apps.list(username, function (err, apps) {
    if (err) {
      return callback(err);
    }

    if (!apps || apps.length === 0) {
      jitsu.log.warn('No applications exist.');
      jitsu.log.help('Try creating one with ' + 'jitsu apps create'.magenta);
      return callback();
    }

    var rows = [['name', 'state', 'subdomain', 'start', 'latest']],
        colors = ['underline', 'underline', 'underline', 'underline', 'underline'];

    apps.forEach(function (app) {
      rows.push([
        app.name,
        app.state || 'stopped',
        app.subdomain,
        app.scripts.start || '---',
        app.snapshots && app.snapshots.length > 0 ? app.snapshots[app.snapshots.length - 1].filename : '---'
      ]);
    });

    jitsu.inspect.putRows('data', rows, colors);
    callback();
  });
};

//
// Usage for `jitsu apps list`
//
apps.list.usage = [
  'Lists all of the applications for the current user',
  '',
  'jitsu list',
  'jitsu apps list'
];

//
// ### function view (name, callback)
// #### @name {string} **optional** Name of the application to view
// #### @callback {function} Continuation to pass control to when complete.
// Views the application with the specfied `name` for the authenticated user.
// If no name is supplied this will view the application in the current directory.
//
apps.view = function (name, callback) {

  function executeView() {
    jitsu.apps.view(name, function (err, app) {
      if (err) {
        return callback(err);
      }

      app = jitsu.common.formatApp(app);
      jitsu.inspect.putObject(app);
      callback(null, app);
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeView();
    });
  }

  executeView();
};

//
// Usage for `jitsu apps view [<name>]`
//
apps.view.usage = [
  'Lists the information for the application in the current',
  'directory. If <name> is supplied then that application',
  'is listed instead.',
  '',
  'jitsu apps view',
  'jitsu apps view <name>'
];

//
// ### function list (callback)
// #### @name {string} **optional** Name of the application to create
// #### @callback {function} Continuation to pass control to when complete.
// Lists the applications for the authenticated user.
//
apps.update = function (name, callback) {
  jitsu.log.silly('Reading package.json in ' + process.cwd());
  jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
    name = name || pkg.name;

    jitsu.apps.view(name, function (err, app) {
      if (err) {
        return callback(err);
      }

      var diff = jitsu.common.objectDiff(app, pkg);
      if (!diff) {
        jitsu.log.warn('No changes found to your package.json for ' + name.magenta);
        return callback();
      }

      jitsu.log.info('Updating application ' + name.magenta + ' with:');
      jitsu.inspect.putObject(diff);

      jitsu.apps.update(name, diff, callback);
    });
  });
};

//
// Usage for `jitsu apps update [<name>]`
//
apps.update.usage = [
  'Updates the application in the current directory',
  'with the information in the package.json file. If',
  '<name> is supplied the application with <name> is updated.',
  '',
  'jitsu apps update',
  'jitsu apps update <name>'
];

//
// ### function destroy (callback)
// #### @name {string} **optional** Name of the application to destroy.
// #### @callback {function} Continuation to pass control to when complete.
// Destroys the application specified by `name`. If no name is supplied
// this will destroy the application in the current directory.
//
apps.destroy = function (name, callback) {

  function executeDestroy() {
    jitsu.log.info('Destroying app ' + name.magenta);
    jitsu.apps.destroy(name, function (err) {
      jitsu.log.silly('Done destroying app ' + name.magenta);
      return err ? callback(err) : callback();
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeDestroy();
    });
  }

  executeDestroy();
};

//
// Usage for `jitsu apps destroy [<name>]`
//
apps.destroy.destructive = true
apps.destroy.usage = [
  'Destroys the application in the current directory. If',
  '<name> is supplied then that application is destroyed instead',
  '',
  'jitsu apps destroy',
  'jitsu apps destroy <name>'
];

//
// ### function start (callback)
// #### @name {string} **optional** Name of the application to start.
// #### @callback {function} Continuation to pass control to when complete.
// Starts the application specified by `name`. If no name is supplied
// this will start the application in the current directory.
//
apps.start = function (name, callback) {

  function executeStart() {
    jitsu.log.info('Starting app ' + name.magenta);
    jitsu.apps.start(name, function (err) {
      if (err) {
        return callback(err, true);
      }

      jitsu.apps.view(name, function (err, app) {
        if (err) {
          return callback(err);
        }

        jitsu.log.info('App ' + name.magenta + ' is now started')
        jitsu.log.info(('http://' + app.subdomain + '.nodejitsu.com').magenta + ' on Port ' + '80'.magenta);
        callback();
      });
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeStart();
    });
  }

  executeStart();
};

//
// Usage for `jitsu apps start [<name>]`
//
apps.start.usage = [
  'Starts the application in the current directory. If <name>',
  'is supplied then that application is started instead.',
  '',
  'jitsu apps start',
  'jitsu apps start <name>'
];

//
// ### function restart (name, callback)
// #### @name {string} **optional** Name of the application to restart.
// #### @callback {function} Continuation to pass control to when complete.
// Restarts the application specified by `name`. If no name is supplied
// this will restart the application in the current directory.
//
apps.restart = function (name, callback) {

  function executeRestart() {
    jitsu.log.info('Restarting app ' + name.magenta);
    jitsu.apps.restart(name, function (err) {
      if (err) {
        return callback(err);
      }

      jitsu.log.info('App ' + name.magenta + ' has been restarted');
      callback();
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeRestart();
    });
  }

  executeRestart();
};

//
// Usage for `jitsu apps restart [<name>]`
//
apps.restart.usage = [
  'Restarts the application in the current directory. If <name>',
  'is supplied then that application is restarted instead.',
  '',
  'jitsu apps restart',
  'jitsu apps restart <name>'
];

//
// ### function stop (callback)
// #### @name {string} **optional** Name of the application to stop.
// #### @callback {function} Continuation to pass control to when complete.
// Stops the application specified by `name`. If no name is supplied
// this will stop the application in the current directory.
//
apps.stop = function (name, callback) {

  function executeStop() {
    jitsu.log.info('Stopping app ' + name.magenta);
    jitsu.apps.stop(name, function (err) {
      if (err) {
        return callback(err);
      }

      jitsu.log.info('App ' + name.magenta + ' is now stopped');
      callback();
    });
  }

  if (!name) {
    return jitsu.package.tryRead(process.cwd(), callback, function (pkg) {
      name = pkg.name;
      executeStop();
    });
  }

  executeStop();
};

//
// Usage for `jitsu apps stop [<name>]`
//
apps.stop.usage = [
  'Stops the application in the current directory. If <name>',
  'is supplied then that application is stopped instead.',
  '',
  'jitsu apps stop',
  'jitsu apps stop <name>'
];
<|endoftext|>