diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d48edb27..00000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -tests/integration/deps/*.js -tests/integration/cordova/**/*.js -tests/performance-bundle.js -tests/fuzzy/seedrandom.min.js -tests/integration/utils-bundle.js \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index c03794ec..17bf1ddb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,18 +1,14 @@ { - "extends": "eslint:recommended", - "parserOptions": { "ecmaVersion": 6, "sourceType": "module" }, - "env": { "browser": true, "node": true, "mocha": true }, - "globals": { "Map": true, "Set": true, @@ -29,17 +25,47 @@ "testUtils": true, "importScripts": true }, - "rules": { "no-empty": 0, "no-console": 0, - "semi": ["error", "always"], - "curly": ["error", "all"], - "space-unary-ops": ["error", {"words": true}], - "space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}], - "max-len": ["error", 100, {"ignoreComments": true, "ignoreStrings": true, "ignoreRegExpLiterals": true}], - "curly": ["error"], - "keyword-spacing": ["error"], - "space-before-blocks": ["error"] + "semi": [ + "error", + "always" + ], + "curly": [ + "error", + "all" + ], + "space-unary-ops": [ + "error", + { + "words": true + } + ], + "space-before-function-paren": [ + "error", + { + "anonymous": "always", + "named": "never" + } + ], + "max-len": [ + "error", + 100, + { + "ignoreComments": true, + "ignoreStrings": true, + "ignoreRegExpLiterals": true + } + ], + "curly": [ + "error" + ], + "keyword-spacing": [ + "error" + ], + "space-before-blocks": [ + "error" + ] } } diff --git a/.gitignore b/.gitignore index 5948bc06..0d258827 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -node_modules/ +/node_modules testdb_* test_* _pouch_* @@ -12,3 +12,4 @@ config.json log.txt /pouchdb /tmp +pouchdb-tests diff --git a/.travis.yml b/.travis.yml index 71dc711e..dcecf787 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,8 @@ env: - secure: "MiufQQKR/EBoS7kcau/I7oYenVilysEqwx37zdgLEKlEUe3SxVOe31uLZv/bhfLNZiRuLAfmIOZmhLGnhMf0LaBzR2yC5qhBxrVHcAiTuS3q6zxpzEf02jnu+hACvj1kJJEPjpOLpEVx7ghWL4McEO0qLbdtSbQlm2IkOX1ONg0=" matrix: + - COMMAND=unit-tests + - COMMAND=test-express-minimum - CLIENT=node COMMAND=test-pouchdb - CLIENT=node SERVER_ARGS=--in-memory COMMAND=test-pouchdb - CLIENT=node SERVER_ARGS=--sqlite COMMAND=test-pouchdb @@ -40,6 +42,10 @@ env: - CLIENT=selenium:firefox COMMAND=test-pouchdb - CLIENT=selenium:phantomjs COMMAND=test-pouchdb + # Run the mapreduce tests + - COMMAND=test-pouchdb TYPE=mapreduce CLIENT=node + - COMMAND=test-pouchdb TYPE=mapreduce CLIENT=selenium:firefox + # Test against the couchdb harness - COMMAND=test-couchdb diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1795749..07bb584b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,71 @@ +Contributing +=== + +Want to contribute to PouchDB Server? Great, you've come to the right place! +This document contains everything you need to know to get started building, +testing, and publishing the code. + +Monorepo +--- + +PouchDB Server is a monorepo, containing the packages `pouchdb-server` and `express-pouchdb`. It follows the same format as the [PouchDB monorepo](https://github.com/pouchdb/pouchdb), using the [alle](https://github.com/boennemann/alle) monorepo setup. You can read more about monorepos [here](https://github.com/babel/babel/blob/master/doc/design/monorepo.md) and [here](https://gist.github.com/nolanlawson/457cdb309c9ec5b39f0d420266a9faa4). + +Conceptually `pouchdb-server` is a standalone CouchDB-esque server, whereas +`express-pouchdb` is designed to be injectable into an existing Express app. +`pouchdb-server` uses `express-pouchdb` under the hood. + +Testing +--- + +One of the primary benefits of **pouchdb-server** is the ability to run PouchDB's Node test suite against itself. To do that, you can simply, + +```bash +$ npm run test-pouchdb +``` + +Whatever args you provide as `SERVER_ARGS` will be passed to `pouchdb-server` itself: + +```bash +$ SERVER_ARGS='--in-memory' npm run test-pouchdb +``` + +Or to test in Firefox (IndexedDB): + +```bash +$ CLIENT=selenium:firefox npm run test-pouchdb +``` + +Or to test in PhantomJS (WebSQL): + +```bash +$ CLIENT=selenium:phantomjs ES5_SHIM=true npm run test-pouchdb +``` + +Additionally, we've started porting CouchDB's JavaScript test harness to +[a simple Node module](https://github.com/nick-thompson/couchdb-harness), which can be run against PouchDB via **pouchdb-server**. + +```bash +$ npm run test-couchdb +``` + +## Submitting a pull request + Want to help me make this thing awesome? Great! Here's how you should get started. -1. First, make sure that the bugfix or feature you're looking to implement isn't better fit for [express-pouchdb](https://github.com/nick-thompson/express-pouchdb). -2. PouchDB is still developing rapidly. If you need bleeding egde versions, you should first read how to [set up express-pouchdb for local development](https://github.com/nick-thompson/express-pouchdb#contributing). (Make sure that, afterwards, you `npm link` express-pouchdb). -3. Go ahead and fork **pouchdb-server**, clone it to your machine. -4. Now you'll want to, from the root of **pouchdb-server**, `npm link express-pouchdb`. -5. `npm install` the rest of the dependencies. +1. First, check whether your bugfix must be in `express-pouchdb` or `pouchdb-server`. +2. Make your changes on a separate branch whose name reflects your changes, +3. Run all tests to make sure your changes do not break anything +4. To create a PR, push your changes to your fork, and open a pull request! +5. For a PR, follow the commit message style guidelines in[PouchDB CONTRIBUTING.md](https://github.com/pouchdb/pouchdb/blob/master/CONTRIBUTING.md). + +## Release process + +`pouchdb-server` is a monorepo, meaning that when you publish, you need to publish all packages simultaneously. Versions are kept in sync across packages, for simplicity's sake. + +Release process: -Please make your changes on a separate branch whose name reflects your changes, push them to your fork, and open a pull request! +1. `npm version patch | minor | major` to change the version in the top-level `package.json`, which will apply to all packages in the release script +2. `git push origin master --tags` +3. `npm run release` -For commit message style guidelines, please refer to [PouchDB CONTRIBUTING.md](https://github.com/pouchdb/pouchdb/blob/master/CONTRIBUTING.md). +This will search through all the sub-packages and automatically figure out the correct `dependencies` and `optionalDependencies`. To add a new dependency, just add it to the top-level `package.json` and it will be identified at release time. \ No newline at end of file diff --git a/README.md b/README.md index 1223f16d..434c7ebb 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,54 @@ -# pouchdb-server -[![Build Status](https://travis-ci.org/pouchdb/pouchdb-server.svg)](https://travis-ci.org/pouchdb/pouchdb-server) +PouchDB Server [![Build Status](https://travis-ci.org/pouchdb/pouchdb-server.svg)](https://travis-ci.org/pouchdb/pouchdb-server) +===== -> A standalone REST interface server for PouchDB. +PouchDB Server is a drop-in replacement for CouchDB, using PouchDB and +Node.js. It is modeled after the single-node design of CouchDB 1.x, +although it contains some CouchDB 2.x features such as +[Mango queries](http://github.com/nolanlawson/pouchdb-find). -## Introduction +PouchDB Server is not designed for production environments, as it is much less +battle-tested than CouchDB. However, it can be useful for development, +testing, and prototypes. It passes the full [PouchDB test suite](https://github.com/pouchdb/pouchdb/tree/master/tests). -**pouchdb-server** is a simple Node.js server that presents a simple REST API, which mimics that of [CouchDB](http://couchdb.apache.org), -on top of [PouchDB](http://pouchdb.com). Among other things, this allows users to replicate IndexedDB stores to LevelDB, or to -spin up a quick and dirty drop-in replacement for CouchDB to get things moving quickly. +_For the `express-pouchdb` sub-package, skip to [express-pouchdb](#express-pouchdb)._ -## Installation +Usage +--- -```bash -$ npm install -g pouchdb-server -``` +Install: -## Usage + npm install -g pouchdb-server -``` +Then run: + + pouchdb-server + +Now you can view the [Fauxton](https://github.com/apache/couchdb-fauxton) web +interface by opening `http://localhost:5984/_utils` in a browser. + +### Basic options + +PouchDB Server's default port is 5984. To change it: + + pouchdb-server --port 3000 + +By default, all files are written in the current directory. To use another one: + pouchdb-server --dir path/to/my/directory + +PouchDB Server can run fully in-memory (no changes are saved): + + pouchdb-server --in-memory + +Or it can run using SQLite rather than LevelDB: + + pouchdb-server --sqlite + +### Full options + +Most PouchDB Server options are available via the command line: + +``` Usage: pouchdb-server [options] Options: @@ -73,59 +103,43 @@ Examples: Requires: npm install sqldown sqlite3 ``` -A simple example might be, +### Configuration -```bash -$ pouchdb-server -p 15984 -pouchdb-server listening on port 15984. -``` - -Alternatively, **pouchdb-server**'s functionality can be mounted into other Express web apps. For more information -on that, check out [express-pouchdb](https://github.com/nick-thompson/express-pouchdb). - -## Fauxton - -**pouchdb-server** currently supports an experimental version of CouchDB's [Fauxton](http://docs.couchdb.org/en/latest/fauxton/index.html). Fauxton, the successor to CouchDB's original Futon, is a simple web UI for interacting with your databases. With your server running, navigate to `/_utils` to check it out! - -## Configuration - -By default, you can configure pouchdb-server using a `config.json` file, which is -typically expected at the root of wherever you run pouchdb-server, but may be specified with the `--config` option. +By default, you can configure PouchDB Server using a `config.json` file, which is +typically expected at the root of wherever you run it, but may be specified with the `--config` option. Below are some examples of `config.json` options: -### log.level +#### log.level To change the log output level, you can create a `config.json` file containing e.g.: -```js +```json { "log": { "level": "none" } } - ``` The available values are `debug`, `info`, `warning`, `error`, and `none`. The default is `info`. -### log.file +#### log.file To choose the file where logs are written, you can create a `config.json` file containing e.g.: -```js +```json { "log": { "file": "/path/to/log.txt" } } - ``` By default, logs are written to `./log.txt`. -## Automatic port configuration +### Automatic port configuration Due to conventions set by Heroku and others, if you have a `PORT` environment variable, `pouchdb-server` will pick up on that and use it instead of `5984` as the default. @@ -135,61 +149,230 @@ export PORT=3000 pouchdb-server # will run on port 3000 ``` -## Testing +express-pouchdb +----- -One of the primary benefits of **pouchdb-server** is the ability to run PouchDB's Node test suite against itself. To do that, you can simply, +The `express-pouchdb` module is a fully qualified [Express](http://expressjs.com/) application with routing defined to +mimic most of the [CouchDB](http://couchdb.apache.org/) REST API, and whose behavior is handled by +[PouchDB](http://pouchdb.com/). -```bash -$ npm run test-pouchdb +The intention is for `express-pouchdb` to be mounted into Express apps for +extended usability. A simple example of this is +[pouchdb-server](https://github.com/pouchdb/pouchdb-server) itself. + +### Usage + +Install the necessary packages: + + npm install express-pouchdb pouchdb express + +Now here's a sample Express app, which we'll name `app.js`. + +```js +var PouchDB = require('pouchdb'); +var express = require('express'); +var app = express(); + +app.use('/db', require('express-pouchdb')(PouchDB)); + +app.listen(3000); ``` -Whatever args you provide as `SERVER_ARGS` will be passed to `pouchdb-server` itself: +Now we can run this script and find each of `express-pouchdb`'s routes +at the `/db` path: -```bash -$ SERVER_ARGS='--in-memory' npm run test-pouchdb + node app.js & + curl localhost:3000/db + +You should see: + +```json +{ + "express-pouchdb": "Welcome!", + "uuid": "c0da32be-957f-4934-861f-d1e3ed10e544", + "vendor": { + "name": "PouchDB authors", + "version": "2.2.6" + }, + "version": "2.2.6" +} ``` -Or to test in Firefox (IndexedDB): +*Note:* `express-pouchdb` conflicts with some middleware. You can work +around this by only enabling affected middleware for routes not handled +by `express-pouchdb`. [body-parser](https://www.npmjs.com/package/body-parser) +is the most important middleware known to be problematic. + +#### API + +`express-pouchdb` exports a single function that builds an express [application object](http://expressjs.com/4x/api.html#application). Its function signature is: + +``require('express-pouchdb')([PouchDB[, options]])`` +- ``PouchDB``: the PouchDB object used to access databases. Optional. +- ``options``: Optional. These options are supported: + - ``configPath``: a path to the configuration file to use. Defaults to './config.json'. + - ``logPath``: a path to the log file to use. Defaults to './log.txt'. + - ``inMemoryConfig``: `true` if all configuration should be in-memory. Defaults to `false`. + - ``mode``: determines which parts of the HTTP API express-pouchdb offers are enabled. There are three values: + - ``'fullCouchDB'``: enables every part of the HTTP API, which makes express-pouchdb very close to a full CouchDB replacement. This is the default. + - ``'minimumForPouchDB'``: just exposes parts of the HTTP API that map 1-1 to the PouchDB api. This is the minimum required to make the PouchDB test suite run, and a nice start when you just need an HTTP API to replicate with. + - ``'custom'``: no parts of the HTTP API are enabled. You can add parts yourself using the ``opts.overrideMode`` discussed below. + - ``overrideMode``: Sometimes the preprogrammed modes are insufficient for your needs, or you chose the ``'custom'`` mode. In that case, you can set this to an object. This object can have the following properties: + - ``'include'``: a javascript array that specifies parts to include on top of the ones specified by ``opts.mode``. Optional. + - ``'exclude'``: a javascript array that specifies parts to exclude from the ones specified by ``opts.mode``. Optional. + +The application object returned contains some extra properties that +offer additional functionality compared to an ordinary express +application: + +- ``setPouchDB``: a function that allows changing the ``PouchDB`` object `express-pouchdb` uses on the fly. Takes one argument: the new ``PouchDB`` object to use. +- ``couchConfig``: an object that provides programmatic access to the configuration file and HTTP API express-pouchdb offers. For an overview of available configuration options, take a look at Fauxton's configuration page. (``/_utils#_config``) +- ``couchLogger``: an object that provides programmatic access to the log file and HTTP API `express-pouchdb` offers. + +#### Examples + +##### Example 1 + +Builds an HTTP API that exposes a minimal HTTP interface, but adds +Fauxton as a debugging tool. + +```javascript +var app = require('express-pouchdb')({ + mode: 'minimumForPouchDB', + overrideMode: { + include: ['routes/fauxton'] + } +}); +// when not specifying PouchDB as an argument to the main function, you +// need to specify it like this before requests are routed to ``app`` +app.setPouchDB(require('pouchdb')); +``` -```bash -$ CLIENT=selenium:firefox npm run test-pouchdb +##### Example 2 + +builds a full HTTP API but excludes express-pouchdb's authentication +logic (say, because it interferes with custom authentication logic used +in our own express app): + +```javascript +var app2 = require('express-pouchdb')(require('pouchdb'), { + mode: 'fullCouchDB', // specified for clarity. It's the default so not necessary. + overrideMode: { + exclude: [ + 'routes/authentication', + // disabling the above, gives error messages which require you to disable the + // following parts too. Which makes sense since they depend on it. + 'routes/authorization', + 'routes/session' + ] + } +}); ``` -Or to test in PhantomJS (WebSQL): +#### Using your own PouchDB -```bash -$ CLIENT=selenium:phantomjs ES5_SHIM=true npm run test-pouchdb +Since you pass in the `PouchDB` that you would like to use with +`express-pouchb`, you can drop `express-pouchdb` into an existing Node-based +PouchDB application and get all the benefits of the HTTP interface +without having to change your code. + +```js +var express = require('express') + , app = express() + , PouchDB = require('pouchdb'); + +app.use('/db', require('express-pouchdb')(PouchDB)); + +var myPouch = new PouchDB('foo'); + +// myPouch is now modifiable in your own code, and it's also +// available via HTTP at /db/foo ``` -Additionally, we've started porting CouchDB's JavaScript test harness to -[a simple Node module](https://github.com/nick-thompson/couchdb-harness), which can be run against PouchDB via **pouchdb-server**. +#### PouchDB defaults -```bash -$ npm run test-couchdb +When you use your own PouchDB code in tandem with `express-pouchdb`, the `PouchDB.defaults()` API can be very convenient for specifying some default settings for how PouchDB databases are created. + +For instance, if you want to use an in-memory [MemDOWN](https://github.com/rvagg/memdown)-backed pouch, you can simply do: + +```js +var InMemPouchDB = PouchDB.defaults({db: require('memdown')}); + +app.use('/db', require('express-pouchdb')(InMemPouchDB)); + +var myPouch = new InMemPouchDB('foo'); ``` -## Contributing +Similarly, if you want to place all database files in a folder other than the `pwd`, you can do: -Want to help me make this thing awesome? Great! Here's how you should get started. +```js +var TempPouchDB = PouchDB.defaults({prefix: '/tmp/my-temp-pouch/'}); + +app.use('/db', require('express-pouchdb')(TempPouchDB)); + +var myPouch = new TempPouchDB('foo'); +``` -1. First, make sure that the bugfix or feature you're looking to implement isn't better fit for [express-pouchdb](https://github.com/nick-thompson/express-pouchdb). -2. PouchDB is still developing rapidly. If you need bleeding egde versions, you should first read how to [set up express-pouchdb for local development](https://github.com/nick-thompson/express-pouchdb#contributing). (Make sure that, afterwards, you `npm link` express-pouchdb). -3. Go ahead and fork **pouchdb-server**, clone it to your machine. -4. Now you'll want to, from the root of **pouchdb-server**, `npm link express-pouchdb`. -5. `npm install` the rest of the dependencies. +If you want express-pouchdb to proxy requests to another CouchDB-style +HTTP API, you can use [http-pouchdb](https://www.npmjs.com/package/http-pouchdb): -Please make your changes on a separate branch whose name reflects your changes, push them to your fork, and open a pull request! +```javascript +var TempPouchDB = require('http-pouchdb')(PouchDB, 'http://localhost:5984'); +app.use('/db', require('express-pouchdb')(TempPouchDB)); +``` + +#### Functionality + +On top of the exposing everything PouchDB offers through a CouchDB-like +interface, `express-pouchdb` also offers the following extra +functionality found in CouchDB but not in PouchDB by default (depending +on the mode used, of course): + +- [Fauxton][], a web interface for the HTTP API. +- [Authentication][] and [authorisation][] support. HTTP basic + authentication and cookie authentication are available. Authorisation + is handled by [validation functions][] and [security documents][]. +- [Configuration][] support. You can modify configuration values + manually in the `config.json` file, or use the HTTP or Fauxton + interface. +- [Replicator database][] support. This allows your replications to + persist past a restart of your application. +- Support for [show][], [list][] and [update][] functions. These allow + you to serve non-json content straight from your database. +- [Rewrite][] and [Virtual Host][] support, for nicer urls. + +[fauxton]: https://www.npmjs.com/package/fauxton +[authentication]: http://docs.couchdb.org/en/latest/intro/security.html +[authorisation]: http://docs.couchdb.org/en/latest/intro/overview.html#security-and-validation +[validation functions]: http://docs.couchdb.org/en/latest/couchapp/ddocs.html#vdufun +[security documents]: http://docs.couchdb.org/en/latest/api/database/security.html +[configuration]: http://docs.couchdb.org/en/latest/config/intro.html#setting-parameters-via-the-http-api +[replicator database]: http://docs.couchdb.org/en/latest/replication/replicator.html +[show]: http://guide.couchdb.org/editions/1/en/show.html +[list]: http://guide.couchdb.org/editions/1/en/transforming.html +[update]: http://docs.couchdb.org/en/latest/couchapp/ddocs.html#update-functions +[rewrite]: http://docs.couchdb.org/en/latest/api/ddoc/rewrites.html +[virtual host]: http://docs.couchdb.org/en/latest/config/http.html#vhosts + +Getting Help +------------ + +The PouchDB community is active [on Slack](http://slack.pouchdb.com/), [on Freenode IRC](https://www.irccloud.com/invite?channel=pouchdb&hostname=irc.freenode.net&port=6697&ssl=1), [Slack](http://slack.pouchdb.com),in [the Google Groups mailing list](https://groups.google.com/forum/#!forum/pouchdb), and [on StackOverflow](http://stackoverflow.com/questions/tagged/pouchdb). Or you can [tweet @pouchdb](http://twitter.com/pouchdb)! + +If you think you've found a bug in PouchDB, please write a reproducible test case and file [a Github issue](https://github.com/pouchdb/pouchdb/issues). We recommend [bl.ocks.org](http://bl.ocks.org/) for code snippets, because some iframe-based services like JSFiddle and JSBin do not support IndexedDB in all browsers. You can start with [this template](https://gist.github.com/nolanlawson/816f138a51b86785d3e6). + +## Contributing -For commit message style guidelines, please refer to [PouchDB CONTRIBUTING.md](https://github.com/pouchdb/pouchdb/blob/master/CONTRIBUTING.md). +See the [CONTRIBUTING.md](https://github.com/pouchdb/pouchdb-server/blob/master/CONTRIBUTING.md) file for how to get involved. ## Contributors -[These people](https://github.com/pouchdb/express-pouchdb/graphs/contributors) made **pouchdb-server** into what it is today! +[These people](https://github.com/pouchdb/pouchdb-server/graphs/contributors) made PouchDB Server into what it is today! ## Changelog -`pouchdb-server` follows [semantic versioning](http://semver.org/). To see a changelog with all releases since v2.0.0, check out the [Github releases page](https://github.com/pouchdb/pouchdb-server/releases). +`pouchdb-server` and `express-pouchdb` follow [semantic versioning](http://semver.org/). To see a changelog with all releases since v2.0.0, check out the [Github releases page](https://github.com/pouchdb/pouchdb-server/releases). ## License -Apache License Version 2.0. See [the LICENSE file](https://github.com/pouchdb/pouchdb-server/blob/master/LICENSE) for more information. +The Apache 2 License. See [the LICENSE file](https://github.com/pouchdb/pouchdb-server/blob/master/LICENSE) for more information. diff --git a/bin/express-pouchdb-minimum-for-pouchdb.js b/bin/express-pouchdb-minimum-for-pouchdb.js new file mode 100755 index 00000000..808a111e --- /dev/null +++ b/bin/express-pouchdb-minimum-for-pouchdb.js @@ -0,0 +1,8 @@ +'use strict'; + +var PouchDB = require('pouchdb'); + +var app = require('../packages/node_modules/express-pouchdb')(PouchDB, { + mode: 'minimumForPouchDB' +}); +app.listen(6984); diff --git a/bin/pouchdb-server b/bin/pouchdb-server deleted file mode 100755 index 18f15738..00000000 --- a/bin/pouchdb-server +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -require('../lib/index.js'); \ No newline at end of file diff --git a/bin/prerelease.js b/bin/prerelease.js new file mode 100644 index 00000000..070e49cf --- /dev/null +++ b/bin/prerelease.js @@ -0,0 +1,72 @@ +'use strict'; + +// Update all the dependencies inside packages/node_modules/*/package.json +// to reflect the true dependencies (automatically determined by require()) +// and update the version numbers to reflect the version from the top-level +// package.json. + +var fs = require('fs'); +var path = require('path'); +var glob = require('glob'); +var findRequires = require('find-requires'); +var builtinModules = require('builtin-modules'); +var uniq = require('lodash.uniq'); +var flatten = require('lodash.flatten'); + +var topPkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +var modules = fs.readdirSync('./packages/node_modules'); +var version = topPkg.version; + +modules.forEach(function (mod) { + var pkgDir = path.join('./packages/node_modules', mod); + var pkgPath = path.join(pkgDir, 'package.json'); + var pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + + // all packages get the same version as the top package.json + pkg.version = version; + + // for the dependencies, find all require() calls + var srcFiles = glob.sync(path.join(pkgDir, 'lib/**/*.js')); + var uniqDeps = uniq(flatten(srcFiles.map(function (srcFile) { + var code = fs.readFileSync(srcFile, 'utf8'); + try { + return findRequires(code); + } catch (e) { + return []; // happens if this is an es6 module, parsing fails + } + }))).filter(function (dep) { + // some modules require() themselves, e.g. for plugins + return dep !== pkg.name && + // exclude built-ins like 'inherits', 'fs', etc. + builtinModules.indexOf(dep) === -1; + }).sort(); + + //find dependencies and igonore if we referencing a local file + const dependencies = uniqDeps.reduce(function (deps, dep) { + if (/^\.\//.test(dep) || /^\.\.\//.test(dep)) { + return deps; // do nothing its a local file + } + + dep = dep.split('/')[0]; // split colors/safe to be colors + + if (topPkg.dependencies[dep]) { + deps.dependencies[dep] = topPkg.dependencies[dep]; + } else if (topPkg.optionalDependencies[dep]) { + deps.optionalDependencies[dep] = topPkg.optionalDependencies[dep]; + } else if (modules.indexOf(dep) !== -1) { // core pouchdb-* module + deps.dependencies[dep] = topPkg.version; + } else { + throw new Error('Unknown dependency ' + dep); + } + + return deps; + }, { + dependencies: {}, + optionalDependencies: {} + }); + + Object.assign(pkg, dependencies); + + var jsonString = JSON.stringify(pkg, null, ' ') + '\n'; + fs.writeFileSync(pkgPath, jsonString, 'utf8'); +}); diff --git a/bin/release.sh b/bin/release.sh new file mode 100755 index 00000000..b591ba26 --- /dev/null +++ b/bin/release.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +for pkg in $(ls packages/node_modules); do + if [ ! -d "packages/node_modules/$pkg" ]; then + continue + elif [ "true" = $(node --eval "console.log(require('./packages/node_modules/$pkg/package.json').private);") ]; then + continue + fi + cd packages/node_modules/$pkg + echo "Publishing $pkg..." + npm publish + cd - +done + +git checkout -- packages/node_modules/*/package.json \ No newline at end of file diff --git a/bin/set-version.js b/bin/set-version.js new file mode 100755 index 00000000..4fd1bfad --- /dev/null +++ b/bin/set-version.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node + +'use strict'; + +// Set the version of all modules, since they're versioned together. +// Usage: npm run set-version -- [version] +// e.g. npm run set-version -- 1.2.3 + +var version = process.argv[process.argv.length - 1]; +var fs = require('fs'); +var path = require('path'); + +var packages = fs.readdirSync('packages/node_modules'); + +packages.map(function (pkg) { + return path.resolve(__dirname, '../packages/node_modules', pkg, 'package.json'); +}).concat([ + path.resolve(__dirname, '../package.json') +]).forEach(function (jsonFile) { + var json = JSON.parse(fs.readFileSync(jsonFile), 'utf-8'); + json.version = version; + fs.writeFileSync(jsonFile, JSON.stringify(json, null, ' ') + '\n', 'utf-8'); +}); + +console.log('done'); diff --git a/bin/test-couchdb.sh b/bin/test-couchdb.sh index 9e14c697..7c3babff 100755 --- a/bin/test-couchdb.sh +++ b/bin/test-couchdb.sh @@ -4,7 +4,7 @@ # couchdb-harness depends on the fact that /_config/httpd/port equals # the port on which pouchdb-server is running. echo '{"httpd": {"port": 6984}}' > config.json -node bin/pouchdb-server & +./packages/node_modules/pouchdb-server/bin/pouchdb-server & export POUCHDB_SERVER_PID=$! ./node_modules/couchdb-harness/bin/couchdb-harness -p 6984 diff --git a/bin/test-express-minimum.sh b/bin/test-express-minimum.sh new file mode 100755 index 00000000..39341d69 --- /dev/null +++ b/bin/test-express-minimum.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +node ./bin/express-pouchdb-minimum-for-pouchdb.js & +POUCHDB_SERVER_PID=$! + +cd ./pouchdb-tests + +COUCH_HOST=http://127.0.0.1:6984 TIMEOUT=120000 npm run test-node + +EXIT_STATUS=$? +if [[ ! -z $POUCHDB_SERVER_PID ]]; then + kill $POUCHDB_SERVER_PID +fi +exit $EXIT_STATUS diff --git a/bin/test-pouchdb.sh b/bin/test-pouchdb.sh index eb724802..9b4fdc5e 100755 --- a/bin/test-pouchdb.sh +++ b/bin/test-pouchdb.sh @@ -1,18 +1,14 @@ #!/bin/bash -rm -fr tmp -mkdir -p tmp -cd tmp - -../bin/pouchdb-server -n -p 6984 $SERVER_ARGS & +./packages/node_modules/pouchdb-server/bin/pouchdb-server -n -p 6984 $SERVER_ARGS & POUCHDB_SERVER_PID=$! -cd ../pouchdb +cd ./pouchdb-tests -COUCH_HOST=http://127.0.0.1:6984 TIMEOUT=120000 npm run test-node +COUCH_HOST=http://localhost:6984 TIMEOUT=120000 npm run test-node EXIT_STATUS=$? if [[ ! -z $POUCHDB_SERVER_PID ]]; then kill $POUCHDB_SERVER_PID fi -exit $EXIT_STATUS +exit $EXIT_STATUS \ No newline at end of file diff --git a/bin/test-setup.sh b/bin/test-setup.sh index f576c581..b6f70272 100755 --- a/bin/test-setup.sh +++ b/bin/test-setup.sh @@ -1,18 +1,14 @@ #!/bin/bash -# install pouchdb from git rather than npm, -# so we can run its own tests -# also this branch may change as we update pouchdb -# versions because we can't necessarily test against -# the master tests since we use the version of pouchdb -# from npm -rm -rf pouchdb -git clone --single-branch \ - --branch master \ - --depth 300 \ - https://github.com/pouchdb/pouchdb.git pouchdb -cd pouchdb -# using a specific commit for now rather than master -git reset --hard 587ba8f4df4b6fcf32a8fd015f8c7ebadd8f280d +DIRECTORY='pouchdb-tests' + +if [ ! -d "$DIRECTORY" ]; then + # Control will enter here if $DIRECTORY exists. + git clone --single-branch --branch master \ + https://github.com/pouchdb/pouchdb.git ${DIRECTORY} +fi + +cd pouchdb-tests npm install -cd - + +cd .. diff --git a/examples/todos/app.js b/examples/todos/app.js new file mode 100644 index 00000000..60fb7b05 --- /dev/null +++ b/examples/todos/app.js @@ -0,0 +1,19 @@ + +/** + * Module dependencies. + */ + +var express = require('express') + , path = require('path') + , morgan = require('morgan'); + +var app = express(); + + +app.use(morgan('dev')); +app.use(express.static(path.join(__dirname, 'public'))); +app.use('/db', require('../../')); + + +app.listen(3000); +console.log("Express server listening on port " + 3000); diff --git a/examples/todos/public/images/bg.png b/examples/todos/public/images/bg.png new file mode 100644 index 00000000..b2a76008 Binary files /dev/null and b/examples/todos/public/images/bg.png differ diff --git a/examples/todos/public/index.html b/examples/todos/public/index.html new file mode 100644 index 00000000..17115ee5 --- /dev/null +++ b/examples/todos/public/index.html @@ -0,0 +1,35 @@ + + + + + + VanillaJS • TodoMVC + + + +
+ +
+ +
+ +
+ + + + + diff --git a/examples/todos/public/scripts/app.js b/examples/todos/public/scripts/app.js new file mode 100644 index 00000000..ba202611 --- /dev/null +++ b/examples/todos/public/scripts/app.js @@ -0,0 +1,168 @@ +(function() { + + 'use strict'; + + var ENTER_KEY = 13; + var newTodoDom = document.getElementById('new-todo'); + var syncDom = document.getElementById('sync-wrapper'); + + // EDITING STARTS HERE (you dont need to edit anything above this line) + + var db = new PouchDB('todos'); + var remoteCouch = window.location.href + 'db/todos'; + + db.info(function(err, info) { + db.changes({ + since: info.update_seq, + continuous: true, + onChange: showTodos + }); + }); + + // We have to create a new todo document and enter it in the database + function addTodo(text) { + var todo = { + _id: new Date().toISOString(), + title: text, + completed: false + }; + db.put(todo, function callback (err, result) { + if (!err) { + console.log('Successfully posted a todo!'); + } + }); + } + + // Show the current list of todos by reading them from the database + function showTodos() { + db.allDocs({ + include_docs: true, + descending: true + }, function(err, doc) { + redrawTodosUI(doc.rows); + }); + } + + function checkboxChanged(todo, event) { + todo.completed = event.target.checked; + db.put(todo); + } + + // User pressed the delete button for a todo, delete it + function deleteButtonPressed(todo) { + db.remove(todo); + } + + // The input box when editing a todo has blurred, we should save + // the new title or delete the todo if the title is empty + function todoBlurred(todo, event) { + var trimmedText = event.target.value.trim(); + if (!trimmedText) { + db.remove(todo); + } else { + todo.title = trimmedText; + db.put(todo); + } + } + + // Initialise a sync with the remote server + function sync() { + syncDom.setAttribute('data-sync-state', 'syncing'); + var opts = {continuous: true, complete: syncError}; + db.replicate.to(remoteCouch, opts); + db.replicate.from(remoteCouch, opts); + } + + // EDITING STARTS HERE (you dont need to edit anything below this line) + + // There was some form or error syncing + function syncError() { + syncDom.setAttribute('data-sync-state', 'error'); + } + + // User has double clicked a todo, display an input so they can edit the title + function todoDblClicked(todo) { + var div = document.getElementById('li_' + todo._id); + var inputEditTodo = document.getElementById('input_' + todo._id); + div.className = 'editing'; + inputEditTodo.focus(); + } + + // If they press enter while editing an entry, blur it to trigger save + // (or delete) + function todoKeyPressed(todo, event) { + if (event.keyCode === ENTER_KEY) { + var inputEditTodo = document.getElementById('input_' + todo._id); + inputEditTodo.blur(); + } + } + + // Given an object representing a todo, this will create a list item + // to display it. + function createTodoListItem(todo) { + var checkbox = document.createElement('input'); + checkbox.className = 'toggle'; + checkbox.type = 'checkbox'; + checkbox.addEventListener('change', checkboxChanged.bind(this, todo)); + + var label = document.createElement('label'); + label.appendChild( document.createTextNode(todo.title)); + label.addEventListener('dblclick', todoDblClicked.bind(this, todo)); + + var deleteLink = document.createElement('button'); + deleteLink.className = 'destroy'; + deleteLink.addEventListener( 'click', deleteButtonPressed.bind(this, todo)); + + var divDisplay = document.createElement('div'); + divDisplay.className = 'view'; + divDisplay.appendChild(checkbox); + divDisplay.appendChild(label); + divDisplay.appendChild(deleteLink); + + var inputEditTodo = document.createElement('input'); + inputEditTodo.id = 'input_' + todo._id; + inputEditTodo.className = 'edit'; + inputEditTodo.value = todo.title; + inputEditTodo.addEventListener('keypress', todoKeyPressed.bind(this, todo)); + inputEditTodo.addEventListener('blur', todoBlurred.bind(this, todo)); + + var li = document.createElement('li'); + li.id = 'li_' + todo._id; + li.appendChild(divDisplay); + li.appendChild(inputEditTodo); + + if (todo.completed) { + li.className += 'complete'; + checkbox.checked = true; + } + + return li; + } + + function redrawTodosUI(todos) { + var ul = document.getElementById('todo-list'); + ul.innerHTML = ''; + todos.forEach(function(todo) { + ul.appendChild(createTodoListItem(todo.doc)); + }); + } + + function newTodoKeyPressHandler( event ) { + if (event.keyCode === ENTER_KEY) { + addTodo(newTodoDom.value); + newTodoDom.value = ''; + } + } + + function addEventListeners() { + newTodoDom.addEventListener('keypress', newTodoKeyPressHandler, false); + } + + addEventListeners(); + showTodos(); + + if (remoteCouch) { + sync(); + } + +})(); diff --git a/examples/todos/public/scripts/pouchdb-nightly.min.js b/examples/todos/public/scripts/pouchdb-nightly.min.js new file mode 100644 index 00000000..ef9a7e95 --- /dev/null +++ b/examples/todos/public/scripts/pouchdb-nightly.min.js @@ -0,0 +1,3 @@ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.PouchDB=e():"undefined"!=typeof global?global.PouchDB=e():"undefined"!=typeof self&&(self.PouchDB=e())}(function(){var define,module,exports;return function e(t,n,r){function o(a,s){if(!n[a]){if(!t[a]){var u="function"==typeof require&&require;if(!s&&u)return u(a,!0);if(i)return i(a,!0);throw new Error("Cannot find module '"+a+"'")}var c=n[a]={exports:{}};t[a][0].call(c.exports,function(e){var n=t[a][1][e];return o(n?n:e)},c,c.exports,e,t,n,r)}return n[a].exports}for(var i="function"==typeof require&&require,a=0;at&&a.push(e)}),merge.traverseRevTree(o,function(e,t,n,r,o){var i=t+"-"+n;"available"===o.status&&-1!==a.indexOf(i)&&(o.status="missing",s.push(i))}),customApi._doCompaction(e,o,s,n)})}var api={},customApi=Pouch.adapters[opts.adapter](opts,function(e,t){if(e)return callback&&callback(e),void 0;for(var n in api)t.hasOwnProperty(n)||(t[n]=api[n]);opts.name===Pouch.prefix+Pouch.ALL_DBS?callback(e,t):Pouch.open(opts,function(e){callback(e,t)})}),auto_compaction=opts.auto_compaction===!0;api.post=function(e,t,n){return"function"==typeof t&&(n=t,t={}),"object"!=typeof e||Array.isArray(e)?call(n,errors.NOT_AN_OBJECT):customApi.bulkDocs({docs:[e]},t,autoCompact(yankError(n)))},api.put=function(e,t,n){return"function"==typeof t&&(n=t,t={}),"object"!=typeof e?call(n,errors.NOT_AN_OBJECT):"_id"in e?customApi.bulkDocs({docs:[e]},t,autoCompact(yankError(n))):call(n,errors.MISSING_ID)},api.putAttachment=function(e,t,n,r,o,i){function a(e){e._attachments=e._attachments||{},e._attachments[t]={content_type:o,data:r},api.put(e,i)}return api.taskqueue.ready()?("function"==typeof o&&(i=o,o=r,r=n,n=null),"undefined"==typeof o&&(o=r,r=n,n=null),api.get(e,function(t,r){return t&&t.error===errors.MISSING_DOC.error?(a({_id:e}),void 0):t?(call(i,t),void 0):r._rev!==n?(call(i,errors.REV_CONFLICT),void 0):(a(r),void 0)}),void 0):(api.taskqueue.addTask("putAttachment",arguments),void 0)},api.removeAttachment=function(e,t,n,r){api.get(e,function(e,o){return e?(call(r,e),void 0):o._rev!==n?(call(r,errors.REV_CONFLICT),void 0):o._attachments?(delete o._attachments[t],0===Object.keys(o._attachments).length&&delete o._attachments,api.put(o,r),void 0):call(r,null)})},api.remove=function(e,t,n){"function"==typeof t&&(n=t,t={}),void 0===t&&(t={}),t.was_delete=!0;var r={_id:e._id,_rev:e._rev};return r._deleted=!0,customApi.bulkDocs({docs:[r]},t,yankError(n))},api.revsDiff=function(e,t,n){function r(e,t){s[e]||(s[e]={missing:[]}),s[e].missing.push(t)}function o(t,n){var o=e[t].slice(0);merge.traverseRevTree(n,function(e,n,i,a,s){var u=n+"-"+i,c=o.indexOf(u);-1!==c&&(o.splice(c,1),"available"!==s.status&&r(t,u))}),o.forEach(function(e){r(t,e)})}"function"==typeof t&&(n=t,t={});var i=Object.keys(e),a=0,s={};i.map(function(t){customApi._getRevisionTree(t,function(r,u){if(r&&"not_found"===r.error&&"missing"===r.reason)s[t]={missing:e[t]};else{if(r)return call(n,r);o(t,u)}return++a===i.length?call(n,null,s):void 0})})},api.compact=function(e,t){"function"==typeof e&&(t=e,e={}),api.changes({complete:function(e,n){if(e)return call(t),void 0;var r=n.results.length;return r?(n.results.forEach(function(e){compactDocument(e.id,0,function(){r--,r||call(t)})}),void 0):(call(t),void 0)}})},api.get=function(e,t,n){function r(){var r=[],i=o.length;return i?(o.forEach(function(o){api.get(e,{rev:o,revs:t.revs},function(e,t){e?r.push({missing:o}):r.push({ok:t}),i--,i||call(n,null,r)})}),void 0):call(n,null,r)}if(!api.taskqueue.ready())return api.taskqueue.addTask("get",arguments),void 0;"function"==typeof t&&(n=t,t={});var o=[];{if(!t.open_revs)return customApi._get(e,t,function(e,r){if(e)return call(n,e);var o=r.doc,i=r.metadata,a=r.ctx;if(t.conflicts){var s=merge.collectConflicts(i);s.length&&(o._conflicts=s)}if(t.revs||t.revs_info){var u=merge.rootToLeaf(i.rev_tree),c=arrayFirst(u,function(e){return-1!==e.ids.map(function(e){return e.id}).indexOf(o._rev.split("-")[1])});if(c.ids.splice(c.ids.map(function(e){return e.id}).indexOf(o._rev.split("-")[1])+1),c.ids.reverse(),t.revs&&(o._revisions={start:c.pos+c.ids.length-1,ids:c.ids.map(function(e){return e.id})}),t.revs_info){var d=c.pos+c.ids.length;o._revs_info=c.ids.map(function(e){return d--,{rev:d+"-"+e.id,status:e.opts.status}})}}if(t.local_seq&&(o._local_seq=r.metadata.seq),t.attachments&&o._attachments){var l=o._attachments,f=Object.keys(l).length;if(0===f)return call(n,null,o);Object.keys(l).forEach(function(e){customApi._getAttachment(l[e],{encode:!0,ctx:a},function(t,r){o._attachments[e].data=r,--f||call(n,null,o)})})}else{if(o._attachments)for(var p in o._attachments)o._attachments[p].stub=!0;call(n,null,o)}});if("all"===t.open_revs)customApi._getRevisionTree(e,function(e,t){e&&(t=[]),o=merge.collectLeaves(t).map(function(e){return e.rev}),r()});else{if(!Array.isArray(t.open_revs))return call(n,utils.error(errors.UNKNOWN_ERROR,"function_clause"));o=t.open_revs;for(var i=0;i1&&"_design"!==d[0]&&"_local"!==d[0]||d.length>2&&"_design"===d[0]&&"_local"!==d[0])&&(c.binary=!0),n(c,function(e,t,n){return e?u.call(o,e):(u.call(o,null,t,n),void 0)})},l.remove=function(e,t,o){return l.taskqueue.ready()?("function"==typeof t&&(o=t,t={}),n({headers:s.headers,method:"DELETE",url:i(s,r(e._id))+"?rev="+e._rev},o),void 0):(l.taskqueue.addTask("remove",arguments),void 0)},l.getAttachment=function(e,t,n,o){"function"==typeof n&&(o=n,n={}),void 0===n.auto_encode&&(n.auto_encode=!0),n.auto_encode&&(e=r(e)),n.auto_encode=!1,l.get(e+"/"+t,n,o)},l.removeAttachment=function(e,t,o,a){return l.taskqueue.ready()?(n({headers:s.headers,method:"DELETE",url:i(s,r(e)+"/"+t)+"?rev="+o},a),void 0):(l.taskqueue.addTask("removeAttachment",arguments),void 0)},l.putAttachment=function(e,t,o,a,u,c){if(!l.taskqueue.ready())return l.taskqueue.addTask("putAttachment",arguments),void 0;"function"==typeof u&&(c=u,u=a,a=o,o=null),"undefined"==typeof u&&(u=a,a=o,o=null);var d=r(e)+"/"+t,f=i(s,d);o&&(f+="?rev="+o);var p={headers:s.headers,method:"PUT",url:f,processData:!1,body:a,timeout:6e4};p.headers["Content-Type"]=u,n(p,c)},l.put=function(e,t,o){if(!l.taskqueue.ready())return l.taskqueue.addTask("put",arguments),void 0;if("function"==typeof t&&(o=t,t={}),"object"!=typeof e)return u.call(o,c.NOT_AN_OBJECT);if(!("_id"in e))return u.call(o,c.MISSING_ID);var a=[];t&&"undefined"!=typeof t.new_edits&&a.push("new_edits="+t.new_edits),a=a.join("&"),""!==a&&(a="?"+a),n({headers:s.headers,method:"PUT",url:i(s,r(e._id))+a,body:e},o)},l.post=function(e,t,n){return l.taskqueue.ready()?("function"==typeof t&&(n=t,t={}),"object"!=typeof e?u.call(n,c.NOT_AN_OBJECT):("_id"in e?l.put(e,t,n):p.list.length>0?(e._id=p.list.pop(),l.put(e,t,n)):p.get(function(r){return r?u.call(n,c.UNKNOWN_ERROR):(e._id=p.list.pop(),l.put(e,t,n),void 0)}),void 0)):(l.taskqueue.addTask("post",arguments),void 0)},l.bulkDocs=function(e,t,r){return l.taskqueue.ready()?("function"==typeof t&&(r=t,t={}),t||(t={}),"undefined"!=typeof t.new_edits&&(e.new_edits=t.new_edits),n({headers:s.headers,method:"POST",url:i(s,"_bulk_docs"),body:e},r),void 0):(l.taskqueue.addTask("bulkDocs",arguments),void 0)},l.allDocs=function(e,t){if(!l.taskqueue.ready())return l.taskqueue.addTask("allDocs",arguments),void 0;"function"==typeof e&&(t=e,e={});var r,o=[],a="GET";e.conflicts&&o.push("conflicts=true"),e.descending&&o.push("descending=true"),e.include_docs&&o.push("include_docs=true"),e.startkey&&o.push("startkey="+encodeURIComponent(JSON.stringify(e.startkey))),e.endkey&&o.push("endkey="+encodeURIComponent(JSON.stringify(e.endkey))),e.limit&&o.push("limit="+e.limit),"undefined"!=typeof e.skip&&o.push("skip="+e.skip),o=o.join("&"),""!==o&&(o="?"+o),"undefined"!=typeof e.keys&&(a="POST",r=JSON.stringify({keys:e.keys})),n({headers:s.headers,method:a,url:i(s,"_all_docs"+o),body:r},t)},l.changes=function(e){var t=25;if(!l.taskqueue.ready()){var r=l.taskqueue.addTask("changes",arguments);return{cancel:function(){return r.task?r.task.cancel():(r.parameters[0].aborted=!0,void 0)}}}if("latest"===e.since){var o;return l.info(function(t,n){e.aborted||(e.since=n.update_seq,o=l.changes(e))}),{cancel:function(){return o?o.cancel():(e.aborted=!0,void 0)}}}var a={},d="undefined"!=typeof e.limit?e.limit:!1;0===d&&(d=1);var f=d;if(e.style&&(a.style=e.style),(e.include_docs||e.filter&&"function"==typeof e.filter)&&(a.include_docs=!0),e.continuous&&(a.feed="longpoll"),e.conflicts&&(a.conflicts=!0),e.descending&&(a.descending=!0),e.filter&&"string"==typeof e.filter&&(a.filter=e.filter,"_view"===e.filter&&e.view&&"string"==typeof e.view&&(a.view=e.view)),e.query_params&&"object"==typeof e.query_params)for(var p in e.query_params)e.query_params.hasOwnProperty(p)&&(a[p]=e.query_params[p]);var v,h,m,_,g=function(r,o){a.since=r,e.continuous||_||(_=m),a.limit=!d||f>t?t:f;var u="?"+Object.keys(a).map(function(e){return e+"="+a[e]}).join("&"),c={headers:s.headers,method:"GET",url:i(s,"_changes"+u),timeout:null};h=r,e.aborted||(v=n(c,o))},y=10,b=0,k={results:[]},w=function(n,r){if(r&&r.results){k.last_seq=r.last_seq;var o={};o.query=e.query_params,r.results=r.results.filter(function(t){f--;var n=u.filterChange(e)(t);return n&&(k.results.push(t),u.call(e.onChange,t)),n})}else n&&(e.aborted=!0,u.call(e.complete,n,null));r&&r.last_seq&&(h=r.last_seq);var i=r&&r.results.length||0;_-=t;var a=d&&0>=f||r&&!i&&0>=_||i&&r.last_seq===m||e.descending&&0!==h;if(e.continuous||!a){n?b+=1:b=0;var s=1<p&&u.call(e.complete,n||c.UNKNOWN_ERROR,null),setTimeout(function(){g(h,w)},l)}else u.call(e.complete,null,k)};return e.continuous?g(e.since||0,w):l.info(function(t,n){return t?u.call(e.complete,t):(m=n.update_seq,g(e.since||0,w),void 0)}),{cancel:function(){e.aborted=!0,v.abort()}}},l.revsDiff=function(e,t,r){return l.taskqueue.ready()?("function"==typeof t&&(r=t,t={}),n({headers:s.headers,method:"POST",url:i(s,"_revs_diff"),body:e},function(e,t){u.call(r,e,t)}),void 0):(l.taskqueue.addTask("revsDiff",arguments),void 0)},l.close=function(e){return l.taskqueue.ready()?(u.call(e,null),void 0):(l.taskqueue.addTask("close",arguments),void 0)},l.replicateOnServer=function(e,r,i){if(!l.taskqueue.ready())return l.taskqueue.addTask("replicateOnServer",arguments),i;var a=o(e.id()),c={source:s.db,target:a.protocol===s.protocol&&a.authority===s.authority?a.db:a.source};r.continuous&&(c.continuous=!0),r.create_target&&(c.create_target=!0),r.doc_ids&&(c.doc_ids=r.doc_ids),r.filter&&"string"==typeof r.filter&&(c.filter=r.filter),r.query_params&&(c.query_params=r.query_params);var d,f={},p={headers:s.headers,method:"POST",url:s.protocol+"://"+s.host+(80===s.port?"":":"+s.port)+"/_replicate",body:c};i.cancel=function(){this.cancelled=!0,d&&!f.ok&&d.abort(),f._local_id&&(p.body={replication_id:f._local_id}),p.body.cancel=!0,n(p,function(e,n,o){return e?u.call(t,e):(u.call(r.complete,null,f,o),void 0)})},i.cancelled||(d=n(p,function(e,n,o){return e?u.call(t,e):(f.ok=!0,n._local_id&&(f._local_id=n._local_id),u.call(r.complete,null,n,o),void 0)}))},l}var u=e("../utils"),c=e("../deps/errors");n.options={strictMode:!1,key:["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],q:{name:"queryKey",parser:/(?:^|&)([^&=]*)=?([^&]*)/g},parser:{strict:/^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,loose:/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/}},s.destroy=function(e,t,n){var r=o(e,t);t=t||{},"function"==typeof t&&(n=t,t={}),t.headers=r.headers,t.method="DELETE",t.url=i(r,""),u.ajax(t,n)},s.valid=function(){return!0},t.exports=s},{"../deps/errors":7,"../utils":14}],3:[function(e,t){"use strict";function n(e){return function(t){o.call(e,{status:500,error:t.type,reason:t.target})}}function r(e,t){function s(e){e.createObjectStore(l,{keyPath:"id"}).createIndex("seq","seq",{unique:!0}),e.createObjectStore(f,{autoIncrement:!0}).createIndex("_doc_id_rev","_doc_id_rev",{unique:!0}),e.createObjectStore(p,{keyPath:"digest"}),e.createObjectStore(v,{keyPath:"id",autoIncrement:!1}),e.createObjectStore(h)}function u(e){for(var t=e.length,n=new ArrayBuffer(t),r=new Uint8Array(n),o=0;t>o;o++)r[o]=e.charCodeAt(o);return n}function c(e,t){return e._bulk_seq-t._bulk_seq}var d=1,l="document-store",f="by-sequence",p="attach-store",v="meta-store",h="detect-blob-support",m=e.name,_=window.indexedDB.open(m,d);"openReqList"in r||(r.openReqList={}),r.openReqList[m]=_;var g=null,y=null,b={},k=null;return _.onupgradeneeded=function(e){for(var t=e.target.result,n=e.oldVersion;n!==e.newVersion;)0===n&&s(t),n++},_.onsuccess=function(e){k=e.target.result,k.onversionchange=function(){k.close()};var n=k.transaction([v,h],"readwrite"),r=n.objectStore(v).get(v);r.onsuccess=function(e){var r=e.target.result||{id:v};m+"_id"in r?y=r[m+"_id"]:(y=o.uuid(),r[m+"_id"]=y,n.objectStore(v).put(r));try{n.objectStore(h).put(o.createBlob(),"key"),g=!0}catch(i){g=!1}finally{o.call(t,null,b)}}},_.onerror=n(t),b.type=function(){return"idb"},b.id=function(){return y},b._bulkDocs=function(e,t,s){function d(e){var t=e.target.result;t.updateSeq=(t.updateSeq||0)+x,N.objectStore(v).put(t)}function h(){if(!C.length)return N.objectStore(v).get(v).onsuccess=d,void 0;var e=C.shift(),t=N.objectStore(l).get(e.metadata.id);t.onsuccess=function(t){var n=t.target.result;n?q(n,e):S(e)}}function _(){var e=[];A.sort(c),A.forEach(function(t){if(delete t._bulk_seq,t.error)return e.push(t),void 0;var n=t.metadata,a=i.winningRev(n);e.push({ok:!0,id:n.id,rev:a}),o.isLocalId(n.id)||(r.Changes.notify(m),r.Changes.notifyLocalWindows(m))}),o.call(s,null,e)}function y(e,t){if(e.stub)return t();if("string"==typeof e.data){var n;try{n=atob(e.data)}catch(r){var i=o.error(a.BAD_ARG,"Attachments need to be base64 encoded");return o.call(s,i)}if(e.digest="md5-"+o.Crypto.MD5(n),g){var c=e.content_type;n=u(n),e.data=o.createBlob([n],{type:c})}return t()}var d=new FileReader;d.onloadend=function(){e.digest="md5-"+o.Crypto.MD5(this.result),g||(e.data=btoa(this.result)),t()},d.readAsBinaryString(e.data)}function b(e){function t(){n++,C.length===n&&e()}if(!C.length)return e();var n=0;C.forEach(function(e){function n(){o++,o===r.length&&t()}var r=e.data&&e.data._attachments?Object.keys(e.data._attachments):[];if(!r.length)return t();var o=0;for(var i in e.data._attachments)y(e.data._attachments[i],n)})}function w(e,t){function n(e){a||(e?(a=e,o.call(t,a)):s===u.length&&i())}function r(e){s++,n(e)}function i(){e.data._doc_id_rev=e.data._id+"::"+e.data._rev;var n=N.objectStore(f).put(e.data);n.onsuccess=function(n){e.metadata.seq=n.target.result,delete e.metadata.rev;var r=N.objectStore(l).put(e.metadata);r.onsuccess=function(){A.push(e),o.call(t)}}}var a=null,s=0;e.data._id=e.metadata.id,e.data._rev=e.metadata.rev,x++,o.isDeleted(e.metadata,e.metadata.rev)&&(e.data._deleted=!0);var u=e.data._attachments?Object.keys(e.data._attachments):[];for(var c in e.data._attachments)if(e.data._attachments[c].stub)s++,n();else{var d=e.data._attachments[c].data;delete e.data._attachments[c].data;var p=e.data._attachments[c].digest;O(e,p,d,r)}u.length||i()}function q(e,t){var n=i.merge(e.rev_tree,t.metadata.rev_tree[0],1e3),r=o.isDeleted(e),s=r&&o.isDeleted(t.metadata)||!r&&R&&"new_leaf"!==n.conflicts;return s?(A.push(E(a.REV_CONFLICT,t._bulk_seq)),h()):(t.metadata.rev_tree=n.tree,w(t,h),void 0)}function S(e){return"was_delete"in t&&o.isDeleted(e.metadata)?(A.push(a.MISSING_DOC),h()):(w(e,h),void 0)}function E(e,t){return e._bulk_seq=t,e}function O(e,t,n,r){{var i=N.objectStore(p);i.get(t).onsuccess=function(a){var s=a.target.result&&a.target.result.refs||{},u=[e.metadata.id,e.metadata.rev].join("@"),c={digest:t,body:n,refs:s};c.refs[u]=!0;i.put(c).onsuccess=function(){o.call(r)}}}}var R=t.new_edits,T=e.docs,C=T.map(function(e,t){var n=o.parseDoc(e,R);return n._bulk_seq=t,n}),D=C.filter(function(e){return e.error});if(D.length)return o.call(s,D[0]);var N,A=[],x=0;b(function(){N=k.transaction([l,f,p,v],"readwrite"),N.onerror=n(s),N.ontimeout=n(s),N.oncomplete=_,h()})},b._get=function(e,t,n){function r(){o.call(n,c,{doc:s,metadata:u,ctx:d})}var s,u,c,d;d=t.ctx?t.ctx:k.transaction([l,f,p],"readonly"),d.objectStore(l).get(e).onsuccess=function(e){if(u=e.target.result,!u)return c=a.MISSING_DOC,r();if(o.isDeleted(u)&&!t.rev)return c=o.error(a.MISSING_DOC,"deleted"),r();var n=i.winningRev(u),l=u.id+"::"+(t.rev?t.rev:n),p=d.objectStore(f).index("_doc_id_rev");p.get(l).onsuccess=function(e){return s=e.target.result,s&&s._doc_id_rev&&delete s._doc_id_rev,s?(r(),void 0):(c=a.MISSING_DOC,r())}}},b._getAttachment=function(e,t,n){var r,i;i=t.ctx?t.ctx:k.transaction([l,f,p],"readonly");var a=e.digest,s=e.content_type;i.objectStore(p).get(a).onsuccess=function(e){var i=e.target.result.body;if(t.encode)if(g){var a=new FileReader;a.onloadend=function(){r=btoa(this.result),o.call(n,null,r)},a.readAsBinaryString(i)}else r=i,o.call(n,null,r);else g?r=i:(i=u(atob(i)),r=o.createBlob([i],{type:s})),o.call(n,null,r)}},b._allDocs=function(e,t){var n="startkey"in e?e.startkey:!1,r="endkey"in e?e.endkey:!1,a="descending"in e?e.descending:!1;a=a?"prev":null;var s=n&&r?window.IDBKeyRange.bound(n,r):n?window.IDBKeyRange.lowerBound(n):r?window.IDBKeyRange.upperBound(r):null,u=k.transaction([l,f],"readonly");u.oncomplete=function(){"keys"in e&&(e.keys.forEach(function(e){e in v?p.push(v[e]):p.push({key:e,error:"not_found"})}),e.descending&&p.reverse()),o.call(t,null,{total_rows:p.length,offset:e.skip,rows:"limit"in e?p.slice(e.skip,e.limit+e.skip):e.skip>0?p.slice(e.skip):p})};var c=u.objectStore(l),d=a?c.openCursor(s,a):c.openCursor(s),p=[],v={};d.onsuccess=function(t){function n(t,n){if(o.isLocalId(t.id))return r["continue"]();var a={id:t.id,key:t.id,value:{rev:i.winningRev(t)}};if(e.include_docs){a.doc=n,a.doc._rev=i.winningRev(t),a.doc._doc_id_rev&&delete a.doc._doc_id_rev,e.conflicts&&(a.doc._conflicts=i.collectConflicts(t));for(var s in a.doc._attachments)a.doc._attachments[s].stub=!0}"keys"in e?e.keys.indexOf(t.id)>-1&&(o.isDeleted(t)&&(a.value.deleted=!0,a.doc=null),v[a.id]=a):o.isDeleted(t)||p.push(a),r["continue"]()}if(t.target.result){var r=t.target.result,a=r.value;if(e.include_docs){var s=u.objectStore(f).index("_doc_id_rev"),c=i.winningRev(a),d=a.id+"::"+c;s.get(d).onsuccess=function(e){n(r.value,e.target.result)}}else n(a)}}},b._info=function(e){function t(e){o=e.target.result&&e.target.result.updateSeq||0}function n(e){var n=e.target.result;return n?(n.value.deleted!==!0&&r++,n["continue"](),void 0):(i.objectStore(v).get(v).onsuccess=t,void 0)}var r=0,o=0,i=k.transaction([l,v],"readonly");i.oncomplete=function(){e(null,{db_name:m,doc_count:r,update_seq:o})},i.objectStore(l).openCursor().onsuccess=n},b._changes=function(e){function t(){p=k.transaction([l,f]),p.oncomplete=a;var t;t=c?p.objectStore(f).openCursor(window.IDBKeyRange.lowerBound(e.since,!0),c):p.objectStore(f).openCursor(window.IDBKeyRange.lowerBound(e.since,!0)),t.onsuccess=n,t.onerror=s}function n(t){if(!t.target.result){for(var n=0,r=v.length;r>n;n++){var a=v[n];a&&_.push(a)}return!1}var s=t.target.result,u=s.value._id,c=h[u];if(void 0!==c)return v[c].seq=s.key,v.push(v[c]),v[c]=null,h[u]=v.length-1,s["continue"]();var m=p.objectStore(l);m.get(s.value._id).onsuccess=function(t){var n=t.target.result;if(o.isLocalId(n.id))return s["continue"]();d= "'+u+'"'),c&&(p+=(u?" AND ":" WHERE ")+d+'.id <= "'+c+'"'),p+=" ORDER BY "+d+".id "+(f?"DESC":"ASC")),b.transaction(function(t){t.executeSql(p,[],function(t,n){for(var r=0,u=n.rows.length;u>r;r++){var c=n.rows.item(r),d=JSON.parse(c.metadata),l=JSON.parse(c.data);if(!i.isLocalId(d.id)){if(c={id:d.id,key:d.id,value:{rev:a.winningRev(d)}},e.include_docs){c.doc=l,c.doc._rev=a.winningRev(d),e.conflicts&&(c.doc._conflicts=a.collectConflicts(d));for(var f in c.doc._attachments)c.doc._attachments[f].stub=!0}"keys"in e?e.keys.indexOf(d.id)>-1&&(i.isDeleted(d)&&(c.value.deleted=!0,c.doc=null),s[c.id]=c):i.isDeleted(d)||o.push(c)}}})},r(t),function(){"keys"in e&&(e.keys.forEach(function(e){e in s?o.push(s[e]):o.push({key:e,error:"not_found"})}),e.descending&&o.reverse()),i.call(t,null,{total_rows:o.length,offset:e.skip,rows:"limit"in e?o.slice(e.skip,e.limit+e.skip):e.skip>0?o.slice(e.skip):o})})},_._changes=function(e){function t(){var t="SELECT "+d+".id, "+l+".seq, "+l+".json AS data, "+d+".json AS metadata FROM "+l+" JOIN "+d+" ON "+l+".seq = "+d+".winningseq WHERE "+d+".seq > "+e.since+" ORDER BY "+d+".seq "+(r?"DESC":"ASC");b.transaction(function(n){n.executeSql(t,[],function(t,n){for(var r=0,o=0,u=n.rows.length;u>o;o++){var c=n.rows.item(o),d=JSON.parse(c.metadata);if(!i.isLocalId(d.id)){r=200&&p.status<300){var r;r=e.binary?i([p.response||""],{type:p.getResponseHeader("Content-Type")}):p.responseText,n(a,r,p,t)}else n(s,p,t)},e.timeout>0&&(l=setTimeout(c,e.timeout)),p.send(e.body),{abort:c}}return e.json&&(e.binary||(e.headers.Accept="application/json"),e.headers["Content-Type"]=e.headers["Content-Type"]||"application/json"),e.binary&&(e.encoding=null,e.json=!1),e.processData||(e.json=!1),r(e,function(r,o,i){if(r)return r.status=o?o.statusCode:400,n(s,r,t);var u=o.headers["content-type"],c=i||"";e.binary||!e.json&&e.processData||"object"==typeof c||!(/json/.test(u)||/^[\s]*\{/.test(c)&&/\}[\s]*$/.test(c))||(c=JSON.parse(c)),o.statusCode>=200&&o.statusCode<300?n(a,c,o,t):(e.binary&&(c=JSON.parse(c.toString())),c.status=o.statusCode,n(t,c))})}var r=e("request"),o=e("./extend.js"),i=e("./blob.js");t.exports=n},{"./blob.js":6,"./extend.js":8,request:16}],6:[function(e,t){function n(e,t){e=e||[],t=t||{};try{return new Blob(e,t)}catch(n){if("TypeError"!==n.name)throw n;for(var r=window.BlobBuilder||window.MSBlobBuilder||window.MozBlobBuilder||window.WebKitBlobBuilder,o=new r,i=0;id;d++)if(null!=(e=arguments[d]))for(t in e)n=c[t],r=e[t],c!==r&&(f&&r&&(o(r)||(s=p(r)))?(s?(s=!1,u=n&&p(n)?n:[]):u=n&&o(n)?n:{},c[t]=a(f,u,r)):void 0!==r&&(p(e)&&i(r)||(c[t]=r)));return c}for(var s={},u=["Boolean","Number","String","Function","Array","Date","RegExp","Object","Error"],c=0;c>>32-t}function n(e,t){var n,r,o,i,a;return o=2147483648&e,i=2147483648&t,n=1073741824&e,r=1073741824&t,a=(1073741823&e)+(1073741823&t),n&r?2147483648^a^o^i:n|r?1073741824&a?3221225472^a^o^i:1073741824^a^o^i:a^o^i}function i(e,t,n){return e&t|~e&n}function a(e,t,n){return e&n|t&~n}function s(e,t,n){return e^t^n}function u(e,t,n){return t^(e|~n)}function c(e,r,o,a,s,u,c){return e=n(e,n(n(i(r,o,a),s),c)),n(t(e,u),r)}function d(e,r,o,i,s,u,c){return e=n(e,n(n(a(r,o,i),s),c)),n(t(e,u),r)}function l(e,r,o,i,a,u,c){return e=n(e,n(n(s(r,o,i),a),c)),n(t(e,u),r)}function f(e,r,o,i,a,s,c){return e=n(e,n(n(u(r,o,i),a),c)),n(t(e,s),r)}function p(e){for(var t,n=e.length,r=n+8,o=(r-r%64)/64,i=16*(o+1),a=Array(i-1),s=0,u=0;n>u;)t=(u-u%4)/4,s=u%4*8,a[t]=a[t]|e.charCodeAt(u)<>>29,a}function v(e){var t,n,r="",o="";for(n=0;3>=n;n++)t=e>>>8*n&255,o="0"+t.toString(16),r+=o.substr(o.length-2,2);return r}if(!r.browser)return o.createHash("md5").update(e).digest("hex");var h,m,_,g,y,b,k,w,q,S=Array(),E=7,O=12,R=17,T=22,C=5,D=9,N=14,A=20,x=4,j=11,I=16,L=23,B=6,M=10,P=15,U=21;for(S=p(e),b=1732584193,k=4023233417,w=2562383102,q=271733878,h=0;hr;r++)i[r]=o[0|Math.random()*t];else{var a;for(i[8]=i[13]=i[18]=i[23]="-",i[14]="4",r=0;36>r;r++)i[r]||(a=0|16*Math.random(),i[r]=o[19==r?3&a|8:a])}return i.join("")}n.CHARS="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""),t.exports=n},{}],11:[function(e,t){function n(e,t,r){if(!(this instanceof n))return new n(e,t,r);("function"==typeof t||"undefined"==typeof t)&&(r=t,t={}),"object"==typeof e&&(t=e,e=void 0),"undefined"==typeof r&&(r=function(){});var o=n.parseAdapter(t.name||e);if(t.originalName=e,t.name=t.name||o.name,t.adapter=t.adapter||o.adapter,!n.adapters[t.adapter])throw"Adapter is missing";if(!n.adapters[t.adapter].valid())throw"Invalid Adapter";var a=new i(t,function(e,t){if(e)return r&&r(e),void 0;for(var o in n.plugins){var i=n.plugins[o](t);for(var a in i)a in t||(t[a]=i[a])}t.taskqueue.ready(!0),t.taskqueue.execute(t),r(null,t)});for(var s in a)this[s]=a[s];for(var u in n.plugins){var c=n.plugins[u](this);for(var d in c)d in this||(this[d]=c[d])}}var r=e("__browserify_process"),o=e("./utils"),i=e("./adapter")(n);n.adapters={},n.plugins={},n.prefix="_pouch_",n.parseAdapter=function(e){var t,r=e.match(/([a-z\-]*):\/\/(.*)/);if(r){if(e=/http(s?)/.test(r[1])?r[1]+"://"+r[2]:r[2],t=r[1],!n.adapters[t].valid())throw"Invalid adapter";return{name:e,adapter:r[1]}}for(var o=["idb","leveldb","websql"],i=0;i0;){var o=n.pop(),i=o.tree1,a=o.tree2;(i[1].status||a[1].status)&&(i[1].status="available"===i[1].status||"available"===a[1].status?"available":"missing");for(var s=0;s0;){var p=f.pop();0!==p.diff?p.ids&&p.ids[2].forEach(function(e,t){f.push({ids:e,diff:p.diff-1,parent:p.ids,parentIdx:t})}):p.ids[0]===c.ids[0]&&l.push(p)}var v=l[0];v?(o=r(v.ids,c.ids),v.parent[2][v.parentIdx]=o.tree,i.push({pos:u.pos,ids:u.ids}),a=a||o.conflicts,s=!0):i.push(e)}else i.push(e)}),s||i.push(t),i.sort(function(e,t){return e.pos-t.pos}),{tree:i,conflicts:a||"internal_node"}):{tree:[t],conflicts:"new_leaf"}}function i(e,t){var r=s.rootToLeaf(e).map(function(e){var r=e.ids.slice(-t);return{pos:e.pos+(e.ids.length-r.length),ids:n(r)}});return r.reduce(function(e,t){return o(e,t,!0).tree},[r.shift()])}var a=e("./deps/extend"),s={};s.merge=function(e,t,n){e=a(!0,[],e),t=a(!0,{},t);var r=o(e,t);return{tree:i(r.tree,n),conflicts:r.conflicts}},s.winningRev=function(e){var t=[];return s.traverseRevTree(e.rev_tree,function(e,n,r,o,i){e&&t.push({pos:n,id:r,deleted:!!i.deleted})}),t.sort(function(e,t){return e.deleted!==t.deleted?e.deleted>t.deleted?1:-1:e.pos!==t.pos?t.pos-e.pos:e.id0;){var r=n.pop(),o=r.pos,i=r.ids,a=t(0===i[2].length,o,i[0],r.ctx,i[1]);i[2].forEach(function(e){n.push({pos:o+1,ids:e,ctx:a})})}},s.collectLeaves=function(e){var t=[];return s.traverseRevTree(e,function(e,n,r,o,i){e&&t.unshift({rev:n+"-"+r,pos:n,opts:i})}),t.sort(function(e,t){return t.pos-e.pos}),t.map(function(e){delete e.pos}),t},s.collectConflicts=function(e){var t=s.winningRev(e),n=s.collectLeaves(e.rev_tree),r=[];return n.forEach(function(e){e.rev===t||e.opts.deleted||r.push(e.rev)}),r},s.rootToLeaf=function(e){var t=[];return s.traverseRevTree(e,function(e,n,r,o,i){if(o=o?o.slice(0):[],o.push({id:r,opts:i}),e){var a=n+1-o.length;t.unshift({pos:a,ids:o})}return o}),t},t.exports=s},{"./deps/extend":8}],13:[function(e,t,n){"use strict";function r(){var e=this;this.cancelled=!1,this.cancel=function(){e.cancelled=!0}}function o(e){var t=[],n={},r=!1;return n.enqueue=function(e,o){t.push({fun:e,args:o}),r||n.process()},n.process=function(){if(!r&&t.length&&!e.cancelled){r=!0;var n=t.shift();n.fun.apply(null,n.args)}},n.notifyRequestComplete=function(){r=!1,n.process()},n}function i(e,t,n){var r=n.filter?n.filter.toString():"";return"_local/"+d.Crypto.MD5(e.id()+t.id()+r)}function a(e,t,n,r){t.get(n,function(t,o){t&&404===t.status?r(null,0):e.get(n,function(e,t){e&&404===e.status||o.last_seq!==t.last_seq?r(null,0):r(null,t.last_seq)})})}function s(e,t,n,r,o){function i(e,t){e.get(n,function(o,i){o&&404===o.status&&(i={_id:n}),i.last_seq=r,e.put(i,t)})}i(t,function(){i(e,function(){o()})})}function u(e,t,n,r){function u(r,o,i){if(n.onChange)for(var a=0;i>a;a++)n.onChange.apply(this,[O]);w-=i,O.docs_written+=i,s(e,t,y,q,function(){_.notifyRequestComplete(),m()})}function c(){if(!g.length)return _.notifyRequestComplete();var e=g.length;t.bulkDocs({docs:g},{new_edits:!1},function(t,n){u(t,n,e)}),g=[]}function l(t,n){e.get(t,{revs:!0,rev:n,attachments:!0},function(e,t){O.docs_read++,_.notifyRequestComplete(),g.push(t),_.enqueue(c)})}function f(e){return function(t,o){if(_.notifyRequestComplete(),t)return S&&r.cancel(),d.call(n.complete,t,null),void 0;if(0===Object.keys(o).length){for(var i in e)w-=e[i];return m(),void 0}var a=function(e){_.enqueue(l,[s,e])};for(var s in o){var u=e[s]-o[s].missing.length;w-=u,o[s].missing.forEach(a)}}}function p(e,n){t.revsDiff(e,f(n))}function v(e){q=e.seq,b.push(e);var t={};t[e.id]=e.changes.map(function(e){return e.rev});var n={};n[e.id]=e.changes.length,w+=e.changes.length,_.enqueue(p,[t,n])}function h(){k=!0,m()}function m(){k&&0===w&&(O.end_time=new Date,d.call(n.complete,null,O))}var _=new o(r),g=[],y=i(e,t,n),b=[],k=!1,w=0,q=0,S=n.continuous||!1,E=n.doc_ids,O={ok:!0,start_time:new Date,docs_read:0,docs_written:0};a(e,t,y,function(t,o){if(t)return d.call(n.complete,t);if(q=o,!r.cancelled){var i={continuous:S,since:q,style:"all_docs",onChange:v,complete:h,doc_ids:E};n.filter&&(i.filter=n.filter),n.query_params&&(i.query_params=n.query_params);var a=e.changes(i);if(n.continuous){var s=r.cancel;r.cancel=function(){s(),a.cancel()}}}})}function c(e,t){return"string"==typeof e?new l(e,t):(t(null,e),void 0)}var d=e("./utils"),l=e("./index");n.replicate=function(e,t,n,o){n instanceof Function&&(o=n,n={}),void 0===n&&(n={}),n.complete||(n.complete=o);var i=new r;return c(e,function(e,r){return e?d.call(o,e):(c(t,function(e,t){if(e)return d.call(o,e);if(n.server){if("function"!=typeof r.replicateOnServer)return d.call(o,{error:"Server replication not supported for "+r.type()+" adapter"});if(r.type()!==t.type())return d.call(o,{error:"Server replication for different adapter types ("+r.type()+" and "+t.type()+") is not supported"});r.replicateOnServer(t,n,i)}else u(r,t,n,i)}),void 0)}),i}},{"./index":11,"./utils":14}],14:[function(e,t,n){function r(e){return/^_/.test(e)?/^_(design|local)/.test(e):!0}function o(){return"undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage&&"undefined"!=typeof chrome.storage.local}var i=e("./merge");n.extend=e("./deps/extend"),n.ajax=e("./deps/ajax"),n.createBlob=e("./deps/blob");var a=e("./deps/uuid");n.Crypto=e("./deps/md5.js");var s=e("./deps/buffer"),u=e("./deps/errors");n.error=function(e,t){return n.extend({},e,{reason:t})};var c=["_id","_rev","_attachments","_deleted","_revisions","_revs_info","_conflicts","_deleted_conflicts","_local_seq","_rev_tree"];n.uuids=function(e,t){"object"!=typeof t&&(t={});for(var n=t.length,r=t.radix,o=[];o.push(a(n,r))=0&&(t=t.split("-")[1]);var n=!1;return i.traverseRevTree(e.rev_tree,function(e,r,o,i,a){o===t&&(n=!!a.deleted)}),n},n.filterChange=function(e){return function(t){var n={},r=e.filter&&"function"==typeof e.filter;if(n.query=e.query_params,e.filter&&r&&!e.filter.call(this,t.doc,n))return!1;if(e.doc_ids&&-1===e.doc_ids.indexOf(t.id))return!1;if(e.include_docs)for(var o in t.doc._attachments)t.doc._attachments[o].stub=!0;else delete t.doc;return!0}},n.processChanges=function(e,t,r){t=t.filter(n.filterChange(e)),e.limit&&e.limito.since&&!o.cancelled&&(o.since=e.seq,n.call(o.onChange,e))}})})},e},n.atob="undefined"!=typeof window&&"atob"in window?function(e){return atob(e)}:function(e){var t=new s(e,"base64");if(t.toString("base64")!==e)throw"Cannot base64 encode full string";return t.toString("binary")},n.btoa="undefined"!=typeof window&&"btoa"in window?function(e){return btoa(e)}:function(e){return new s(e,"binary").toString("base64")},t.exports=n},{"./deps/ajax":5,"./deps/blob":6,"./deps/buffer":16,"./deps/errors":7,"./deps/extend":8,"./deps/md5.js":9,"./deps/uuid":10,"./merge":12}],15:[function(e,t){t.exports="1.0.0"},{}],16:[function(){},{}],17:[function(e,t){var n=t.exports={};n.nextTick=function(){var e="undefined"!=typeof window&&window.setImmediate,t="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(e)return function(e){return window.setImmediate(e)};if(t){var n=[];return window.addEventListener("message",function(e){if(e.source===window&&"process-tick"===e.data&&(e.stopPropagation(),n.length>0)){var t=n.shift();t()}},!0),function(e){n.push(e),window.postMessage("process-tick","*")}}return function(e){setTimeout(e,0)}}(),n.title="browser",n.browser=!0,n.env={},n.argv=[],n.binding=function(){throw new Error("process.binding is not supported")},n.cwd=function(){return"/"},n.chdir=function(){throw new Error("process.chdir is not supported")}},{}],18:[function(require,module,exports){"use strict";function MapReduce(db){function viewQuery(fun,options){function sum(e){return e.reduce(function(e,t){return e+t +},0)}function emit(e,t){var n={id:current.doc._id,key:e,value:t};if(!(options.startkey&&pouchCollate(e,options.startkey)<0||options.endkey&&pouchCollate(e,options.endkey)>0||options.key&&0!==pouchCollate(e,options.key))){if(num_started++,options.include_docs){if(t&&"object"==typeof t&&t._id)return db.get(t._id,function(e,t){t&&(n.doc=t),results.push(n),checkComplete()}),void 0;n.doc=current.doc}results.push(n)}}function checkComplete(){if(completed&&results.length==num_started){if(results.sort(function(e,t){return pouchCollate(e.key,t.key)}),options.descending&&results.reverse(),options.reduce===!1)return options.complete(null,{total_rows:results.length,offset:options.skip,rows:"limit"in options?results.slice(options.skip,options.limit+options.skip):options.skip>0?results.slice(options.skip):results});var e=[];results.forEach(function(t){var n=e[e.length-1]||null;return n&&0===pouchCollate(n.key[0][0],t.key)?(n.key.push([t.key,t.id]),n.value.push(t.value),void 0):(e.push({key:[[t.key,t.id]],value:[t.value]}),void 0)}),e.forEach(function(e){e.value=fun.reduce(e.key,e.value),e.value="undefined"==typeof e.value?null:e.value,e.key=e.key[0][0]}),options.complete(null,{total_rows:e.length,offset:options.skip,rows:"limit"in options?e.slice(options.skip,options.limit+options.skip):options.skip>0?e.slice(options.skip):e})}}if(options.complete){options.skip||(options.skip=0),fun.reduce||(options.reduce=!1);var builtInReduce={_sum:function(e,t){return sum(t)},_count:function(e,t,n){return n?sum(t):t.length},_stats:function(e,t){return{sum:sum(t),min:Math.min.apply(null,t),max:Math.max.apply(null,t),count:t.length,sumsqr:function(){var e=0;for(var n in t)"number"==typeof t[n]&&(e+=t[n]*t[n]);return e}()}}},results=[],current=null,num_started=0,completed=!1;eval("fun.map = "+fun.map.toString()+";"),fun.reduce&&(builtInReduce[fun.reduce]&&(fun.reduce=builtInReduce[fun.reduce]),eval("fun.reduce = "+fun.reduce.toString()+";")),db.changes({conflicts:!0,include_docs:!0,onChange:function(e){"deleted"in e||(current={doc:e.doc},fun.map.call(this,e.doc))},complete:function(){completed=!0,checkComplete()}})}}function httpQuery(e,t,n){var r=[],o=void 0,i="GET";if("undefined"!=typeof t.reduce&&r.push("reduce="+t.reduce),"undefined"!=typeof t.include_docs&&r.push("include_docs="+t.include_docs),"undefined"!=typeof t.limit&&r.push("limit="+t.limit),"undefined"!=typeof t.descending&&r.push("descending="+t.descending),"undefined"!=typeof t.startkey&&r.push("startkey="+encodeURIComponent(JSON.stringify(t.startkey))),"undefined"!=typeof t.endkey&&r.push("endkey="+encodeURIComponent(JSON.stringify(t.endkey))),"undefined"!=typeof t.key&&r.push("key="+encodeURIComponent(JSON.stringify(t.key))),"undefined"!=typeof t.group&&r.push("group="+t.group),"undefined"!=typeof t.group_level&&r.push("group_level="+t.group_level),"undefined"!=typeof t.skip&&r.push("skip="+t.skip),"undefined"!=typeof t.keys&&(i="POST",o=JSON.stringify({keys:t.keys})),r=r.join("&"),r=""===r?"":"?"+r,"string"==typeof e){var a=e.split("/");return db.request({method:i,url:"_design/"+a[0]+"/_view/"+a[1]+r,body:o},n),void 0}var s=JSON.parse(JSON.stringify(e,function(e,t){return"function"==typeof t?t+"":t}));db.request({method:"POST",url:"_temp_view"+r,body:s},n)}return this instanceof MapReduce?(this.query=function(e,t,n){if("function"==typeof t&&(n=t,t={}),n&&(t.complete=n),"http"===db.type())return"function"==typeof e?httpQuery({map:e},t,n):httpQuery(e,t,n);if("object"==typeof e)return viewQuery(e,t);if("function"==typeof e)return viewQuery({map:e},t);var r=e.split("/");db.get("_design/"+r[0],function(e,o){return e?(n&&n(e),void 0):o.views[r[1]]?(viewQuery({map:o.views[r[1]].map,reduce:o.views[r[1]].reduce},t),void 0):(n&&n({error:"not_found",reason:"missing_named_view"}),void 0)})},void 0):new MapReduce(db)}var pouchCollate=require("pouchdb-collate");MapReduce._delete=function(){},module.exports=MapReduce},{"pouchdb-collate":19}],19:[function(e,t){"use strict";function n(e,t){for(var n=Math.min(e.length,t.length),r=0;n>r;r++){var o=a(e[r],t[r]);if(0!==o)return o}return e.length===t.length?0:e.length>t.length?1:-1}function r(e,t){return e===t?0:e>t?1:-1}function o(e,t){for(var n=Object.keys(e),r=Object.keys(t),o=Math.min(n.length,r.length),i=0;o>i;i++){var s=a(n[i],r[i]);if(0!==s)return s;if(s=a(e[n[i]],t[r[i]]),0!==s)return s}return n.length===r.length?0:n.length>r.length?1:-1}function i(e){var t=["boolean","number","string","object"];return-1!==t.indexOf(typeof e)?null===e?1:t.indexOf(typeof e)+2:Array.isArray(e)?4.5:void 0}function a(e,t){var a=i(e),s=i(t);return a-s!==0?a-s:null===e?0:"number"==typeof e?e-t:"boolean"==typeof e?t>e?-1:1:"string"==typeof e?r(e,t):Array.isArray(e)?n(e,t):"object"==typeof e?o(e,t):void 0}t.exports=a},{}]},{},[11])(11)}); \ No newline at end of file diff --git a/examples/todos/public/styles/base.css b/examples/todos/public/styles/base.css new file mode 100644 index 00000000..4a05fc23 --- /dev/null +++ b/examples/todos/public/styles/base.css @@ -0,0 +1,424 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + color: inherit; + -webkit-appearance: none; + /*-moz-appearance: none;*/ + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #eaeaea url('../images/bg.png'); + color: #4d4d4d; + width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + -ms-font-smoothing: antialiased; + -o-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +#todoapp { + background: #fff; + background: rgba(255, 255, 255, 0.9); + margin: 130px 0 40px 0; + border: 1px solid #ccc; + position: relative; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.15); +} + +#todoapp:before { + content: ''; + border-left: 1px solid #f5d6d6; + border-right: 1px solid #f5d6d6; + width: 2px; + position: absolute; + top: 0; + left: 40px; + height: 100%; +} + +#todoapp input::-webkit-input-placeholder { + font-style: italic; +} + +#todoapp input:-moz-placeholder { + font-style: italic; + color: #a9a9a9; +} + +#todoapp h1 { + position: absolute; + top: -120px; + width: 100%; + font-size: 70px; + font-weight: bold; + text-align: center; + color: #b3b3b3; + color: rgba(255, 255, 255, 0.3); + text-shadow: -1px -1px rgba(0, 0, 0, 0.2); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + -ms-text-rendering: optimizeLegibility; + -o-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +#header { + padding-top: 15px; + border-radius: inherit; +} + +#header:before { + content: ''; + position: absolute; + top: 0; + right: 0; + left: 0; + height: 15px; + z-index: 2; + border-bottom: 1px solid #6c615c; + background: #8d7d77; + background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); + background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); + border-top-left-radius: 1px; + border-top-right-radius: 1px; +} + +#new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + -ms-font-smoothing: antialiased; + -o-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +#new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.02); + z-index: 2; + box-shadow: none; +} + +#main { + position: relative; + z-index: 2; + border-top: 1px dotted #adadad; +} + +label[for='toggle-all'] { + display: none; +} + +#toggle-all { + position: absolute; + top: -42px; + left: -4px; + width: 40px; + text-align: center; + border: none; /* Mobile Safari */ +} + +#toggle-all:before { + content: '»'; + font-size: 28px; + color: #d9d9d9; + padding: 0 25px 7px; +} + +#toggle-all:checked:before { + color: #737373; +} + +#todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +#todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px dotted #ccc; +} + +#todo-list li:last-child { + border-bottom: none; +} + +#todo-list li.editing { + border-bottom: none; + padding: 0; +} + +#todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +#todo-list li.editing .view { + display: none; +} + +#todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + /*-moz-appearance: none;*/ + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +#todo-list li .toggle:after { + content: '✔'; + line-height: 43px; /* 40 + a couple of pixels visual adjustment */ + font-size: 20px; + color: #d9d9d9; + text-shadow: 0 -1px 0 #bfbfbf; +} + +#todo-list li .toggle:checked:after { + color: #85ada7; + text-shadow: 0 1px 0 #669991; + bottom: 1px; + position: relative; +} + +#todo-list li label { + word-break: break-word; + padding: 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + -webkit-transition: color 0.4s; + -moz-transition: color 0.4s; + -ms-transition: color 0.4s; + -o-transition: color 0.4s; + transition: color 0.4s; +} + +#todo-list li.completed label { + color: #a9a9a9; + text-decoration: line-through; +} + +#todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 22px; + color: #a88a8a; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; +} + +#todo-list li .destroy:hover { + text-shadow: 0 0 1px #000, 0 0 10px rgba(199, 107, 107, 0.8); + -webkit-transform: scale(1.3); + -moz-transform: scale(1.3); + -ms-transform: scale(1.3); + -o-transform: scale(1.3); + transform: scale(1.3); +} + +#todo-list li .destroy:after { + content: '✖'; +} + +#todo-list li:hover .destroy { + display: block; +} + +#todo-list li .edit { + display: none; +} + +#todo-list li.editing:last-child { + margin-bottom: -1px; +} + +#footer { + color: #777; + padding: 0 15px; + position: absolute; + right: 0; + bottom: -31px; + left: 0; + height: 20px; + z-index: 1; +} + +#footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 31px; + left: 0; + height: 50px; + z-index: -1; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), + 0 6px 0 -3px rgba(255, 255, 255, 0.8), + 0 7px 1px -3px rgba(0, 0, 0, 0.3), + 0 43px 0 -6px rgba(255, 255, 255, 0.8), + 0 44px 2px -6px rgba(0, 0, 0, 0.2); +} + +#todo-count { + float: left; + text-align: left; +} + +#filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +#filters li { + display: inline; +} + +#filters li a { + color: #83756f; + margin: 2px; + text-decoration: none; +} + +#filters li a.selected { + font-weight: bold; +} + +#clear-completed { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + background: rgba(0, 0, 0, 0.1); + font-size: 11px; + padding: 0 10px; + border-radius: 3px; + box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); +} + +#clear-completed:hover { + background: rgba(0, 0, 0, 0.15); + box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); +} + +#info { + margin: 65px auto 0; + color: #a6a6a6; + font-size: 12px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); + text-align: center; +} + +#info a { + color: inherit; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox and Opera +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + #toggle-all, + #todo-list li .toggle { + background: none; + } + + #todo-list li .toggle { + height: 40px; + } + + #toggle-all { + top: -56px; + left: -15px; + width: 65px; + height: 41px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +.hidden{ + display:none; +} + +#sync-error, #sync-success { + display: none; +} + +[data-sync-state=syncing] #sync-success { + display: block; +} + +[data-sync-state=error] #sync-error { + display: block; +} diff --git a/package.json b/package.json index ae277816..83f01c48 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,84 @@ { - "name": "pouchdb-server", - "description": "A standalone REST interface server for PouchDB.", - "version": "2.0.2", - "homepage": "https://github.com/pouchdb/pouchdb-server", - "license": "Apache-2.0", - "author": { - "name": "Nick Thompson", - "email": "ncthom91@gmail.com" - }, - "repository": { - "type": "git", - "url": "git://github.com/pouchdb/pouchdb-server.git" - }, - "bin": { - "pouchdb-server": "./bin/pouchdb-server" - }, + "name": "pouchdb-server-monorepo", + "private": true, + "version": "2.3.0", "scripts": { - "start": "node ./bin/pouchdb-server", + "unit-tests": "mocha tests/**/*", "test-pouchdb": "./bin/test-setup.sh && ./bin/test-pouchdb.sh", "test-couchdb": "./bin/test-setup.sh && ./bin/test-couchdb.sh", - "eslint": "eslint bin/ lib/" - }, - "engines": { - "node": ">= 0.8.0" + "eslint": "eslint bin/ packages/node_modules/**/src tests/", + "test-express-minimum": "./bin/test-setup.sh && ./bin/test-express-minimum.sh", + "release": "node ./bin/prerelease.js && ./bin/release.sh" }, "dependencies": { + "basic-auth": "^1.1.0", + "body-parser": "^1.16.1", + "builtin-modules": "^1.1.1", "colors": "^1.0.3", + "compression": "^1.6.2", + "cookie-parser": "^1.4.3", "corser": "~2.0.0", "couchdb-harness": "*", "couchdb-log-parse": "^0.0.4", + "denodeify": "^1.2.1", "express": "^4.10.4", - "express-pouchdb": "^2.1.3", + "extend": "^3.0.0", + "find-requires": "^0.2.2", + "glob": "^7.1.1", + "header-case-normalizer": "^1.0.3", "http-pouchdb": "^2.1.2", "killable": "^1.0.0", + "lodash.flatten": "^4.4.0", + "lodash.uniq": "^4.5.0", "mkdirp": "^0.5.0", + "multiparty": "^4.1.3", "nomnom": "^1.8.1", "object-assign": "^4.1.0", + "on-finished": "^2.3.0", "pouchdb-adapter-leveldb-core": "^6.1.0", "pouchdb-adapter-memory": "^6.1.0", + "pouchdb-all-dbs": "^1.0.2", + "pouchdb-auth": "^2.1.1", + "pouchdb-collections": "^6.1.2", "pouchdb-core": "^6.1.0", + "pouchdb-fauxton": "0.0.2", "pouchdb-find": "^0.10.4", + "pouchdb-list": "^1.1.0", "pouchdb-mapreduce": "^6.1.0", "pouchdb-promise": "6.1.2", "pouchdb-replication": "^6.1.0", + "pouchdb-replicator": "^2.1.3", + "pouchdb-rewrite": "^1.0.7", + "pouchdb-security": "^1.2.6", + "pouchdb-show": "^1.0.8", + "pouchdb-size": "^1.2.2", + "pouchdb-update": "^1.0.8", + "pouchdb-validation": "^1.2.1", + "pouchdb-vhost": "^1.0.2", + "pouchdb-wrappers": "^1.3.6", + "raw-body": "^2.2.0", + "sanitize-filename": "^1.6.1", "serve-favicon": "~2.3.2", "tail": "^1.2.1", + "uuid": "^3.0.1", "wordwrap": "1.0.0" }, "devDependencies": { + "bluebird": "^3.4.7", + "builtin-modules": "^1.1.1", "eslint": "3.15.0", - "memdown": "^1.2.4" + "express": "^4.14.1", + "find-requires": "^0.2.2", + "fs-extra": "^2.0.0", + "glob": "^7.1.1", + "lodash.flatten": "^4.4.0", + "lodash.uniq": "^4.5.0", + "memdown": "^1.2.4", + "mocha": "^3.2.0", + "supertest": "^3.0.0" }, "optionalDependencies": { "pouchdb-adapter-node-websql": "^6.1.0", "pouchdb-adapter-leveldb": "^6.1.0" - }, - "files": [ - "lib", - "bin/pouchdb-server", - "favicon.ico" - ] + } } diff --git a/packages/node_modules/express-pouchdb/CONTRIBUTING.md b/packages/node_modules/express-pouchdb/CONTRIBUTING.md new file mode 100644 index 00000000..ed42c984 --- /dev/null +++ b/packages/node_modules/express-pouchdb/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing + +Want to help me make this thing awesome? Great! Here's how you should get started. + +1. Because PouchDB is still developing so rapidly, you'll want to clone `git@github.com:pouchdb/pouchdb.git`, and run `npm link` from the root folder of your clone. +2. Fork **express-pouchdb**, and clone it to your local machine. +3. From the root folder of your clone run `npm link pouchdb` to install PouchDB from your local repository from Step 1. +4. `npm install` + +Please make your changes on a separate branch whose name reflects your changes, push them to your fork, and open a pull request! + +For commit message style guidelines, please refer to [PouchDB CONTRIBUTING.md](https://github.com/pouchdb/pouchdb/blob/master/CONTRIBUTING.md). + +## Fauxton + +The custom Fauxton theme, with the PouchDB Server name and logo, are kept [in a Fauxton fork](https://github.com/nolanlawson/couchdb-fauxton) for the time being. + +## Testing + +To test for regressions, the following comes in handy: +- the PouchDB test suite: ``npm run test-pouchdb`` +- the jshint command: ``npm run jshint`` +- the express-pouchdb test suite (for express-pouchdb specific things like its API only!): ``npm run test-express-pouchdb`` + +``npm test`` combines these three. + +There is also the possibility to run express-pouchdb against a part of +the CouchDB test suite. For that, try: ``npm run test-couchdb``. If it +doesn't work, try using [couchdb-harness](https://github.com/nick-thompson/couchdb-harness), +which that command is based on, directly. diff --git a/packages/node_modules/express-pouchdb/LICENSE b/packages/node_modules/express-pouchdb/LICENSE new file mode 100644 index 00000000..f6cd2bc8 --- /dev/null +++ b/packages/node_modules/express-pouchdb/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/packages/node_modules/express-pouchdb/README.md b/packages/node_modules/express-pouchdb/README.md new file mode 100644 index 00000000..3e5f2a89 --- /dev/null +++ b/packages/node_modules/express-pouchdb/README.md @@ -0,0 +1,15 @@ +# express-pouchdb + +[![Build Status](https://travis-ci.org/pouchdb/pouchdb-server.svg)](https://travis-ci.org/pouchdb/pouchdb-server) + +An Express submodule with a CouchDB-style REST interface to PouchDB. + +### Source + +PouchDB Server and its sub-packages are distributed as a [monorepo](https://github.com/babel/babel/blob/master/doc/design/monorepo.md). + +For a full list of packages, see [the GitHub source](https://github.com/pouchdb/pouchdb-server/tree/master/packages/node_modules). + +## License + +The Apache 2 License. See [the LICENSE file](https://github.com/pouchdb/pouchdb-server/blob/master/LICENSE) for more information. \ No newline at end of file diff --git a/packages/node_modules/express-pouchdb/lib/clean-filename.js b/packages/node_modules/express-pouchdb/lib/clean-filename.js new file mode 100644 index 00000000..8a216a85 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/clean-filename.js @@ -0,0 +1,12 @@ +'use strict'; + +var sanitize = require('sanitize-filename'); + +function cleanFilename(name) { + // some windows reserved names like 'con' and 'prn' + // return an empty string here, so just wrap them in + // double underscores so it's at least something + return sanitize(name) || sanitize('__' + name + '__'); +} + +module.exports = cleanFilename; \ No newline at end of file diff --git a/packages/node_modules/express-pouchdb/lib/compression.js b/packages/node_modules/express-pouchdb/lib/compression.js new file mode 100644 index 00000000..8b0cb351 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/compression.js @@ -0,0 +1,4 @@ +"use strict"; +module.exports = function (app) { + app.use(require('compression')()); +}; diff --git a/packages/node_modules/express-pouchdb/lib/config-infrastructure.js b/packages/node_modules/express-pouchdb/lib/config-infrastructure.js new file mode 100644 index 00000000..9e08dd85 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/config-infrastructure.js @@ -0,0 +1,163 @@ +"use strict"; + +var fs = require('fs'), + path = require('path'), + util = require('util'), + events = require('events'), + extend = require('extend'), + Auth = require('pouchdb-auth'); + +function CouchConfig(file) { + events.EventEmitter.call(this); + + this._file = file; + this._tempFile = this._file != null ? + path.dirname(this._file) + '/.' + path.basename(this._file) : + null; + this._config = readConfig(this._file); + this._defaults = {}; + + // Do not create an empty config file + if (this._file != null && fs.existsSync(this._file)) { + // Hashes admin passwords in 'file' (if necessary) + this._save(); + } +} + +util.inherits(CouchConfig, events.EventEmitter); + +function readConfig(file) { + if (file != null && fs.existsSync(file)) { + return JSON.parse(fs.readFileSync(file)); + } + return {}; +} + +CouchConfig.prototype._save = function (callback) { + var self = this; + + if (typeof callback !== "function") { + callback = function () {}; + } + + function write() { + if (self._file == null) { + callback(); + return; + } + + // Pretty print + var json = JSON.stringify(self._config, null, 2) + '\n'; + fs.writeFile(self._tempFile, json, function () { + fs.rename(self._tempFile, self._file, callback); + }); + } + if (self._config.admins) { + Auth.hashAdminPasswords(self._config.admins).then(function (admins) { + self._config.admins = admins; + + write(); + }); + } else { + write(); + } +}; + +CouchConfig.prototype.get = function (section, key) { + if (exists(this._config[section]) && exists(this._config[section][key])) { + return this._config[section][key]; + } else { + // fall back on defaults + var sectionExists = exists(this._defaults[section]); + if (sectionExists && exists(this._defaults[section][key])) { + return this._defaults[section][key]; + } else { + return undefined; + } + } +}; + +function exists(val) { + return typeof val !== 'undefined'; +} + +CouchConfig.prototype.getAll = function () { + return stringify(extend(true, {}, this._defaults, this._config)); +}; + +function stringify(config) { + for (var sectionName in config) { + if (config.hasOwnProperty(sectionName)) { + stringifySection(config[sectionName]); + } + } + return config; +} + +function stringifySection(section) { + for (var key in section) { + if (section.hasOwnProperty(key)) { + var value = section[key]; + if (typeof value !== 'string') { + section[key] = JSON.stringify(value); + } + } + } + return section; +} + +CouchConfig.prototype.getSection = function (section) { + var data = extend(true, {}, this._defaults[section], this._config[section]); + return stringifySection(data); +}; + +CouchConfig.prototype.set = function (section, key, value, callback) { + var previousValue; + if (!this._config[section]) { + this._config[section] = {}; + } else { + previousValue = this._config[section][key]; + } + this._config[section][key] = value; + + this._changed(section, key, previousValue, callback); +}; + +CouchConfig.prototype._changed = function (section, key, prevVal, callback) { + var self = this; + + self._save(function (err) { + if (err) { + return callback(err); + } + + // run event handlers + self.emit(section + "." + key); + self.emit(section); + + callback(null, prevVal); + }); +}; + +CouchConfig.prototype.delete = function (section, key, callback) { + var previousValue = (this._config[section] || {})[key]; + if (exists(previousValue)) { + delete this._config[section][key]; + if (!Object.keys(this._config[section]).length) { + delete this._config[section]; + } + } + + this._changed(section, key, previousValue, callback); +}; + +CouchConfig.prototype.registerDefault = function (section, key, value) { + this._defaults[section] = this._defaults[section] || {}; + this._defaults[section][key] = value; +}; + +module.exports = function (app) { + var path = app.opts.configPath || './config.json'; + var inMemory = app.opts.inMemoryConfig || false; + app.couchConfig = new CouchConfig(inMemory ? null : path); +}; diff --git a/packages/node_modules/express-pouchdb/lib/create-or-delete-dbs.js b/packages/node_modules/express-pouchdb/lib/create-or-delete-dbs.js new file mode 100644 index 00000000..3b034e8f --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/create-or-delete-dbs.js @@ -0,0 +1,52 @@ +'use strict'; + +// unified access to creating/deleting PouchDB databases, +// to re-use PouchDB objects and avoid race conditions + +var Promise = require('pouchdb-promise'); +var PouchMap = require('pouchdb-collections').Map; + +var promiseChain = Promise.resolve(); + +// do all creations/deletions in a single global lock +// obviously this makes this operation slow... but hopefully you aren't +// creating and destroying a lot of databases all the time +function doSequentially(fun) { + promiseChain = promiseChain.then(fun); + return promiseChain; +} + +function getOrCreateDB(PouchDB, dbName) { + var map = PouchDB.__dbCacheMap; + if (!map) { + // cache DBs to avoid costly re-allocations + map = PouchDB.__dbCacheMap = new PouchMap(); + } + return doSequentially(function () { + var db = map.get(dbName); + if (db) { + return db; + } + db = new PouchDB(dbName); + map.set(dbName, db); + return db; + }); +} + +function deleteDB(PouchDB, dbName, opts) { + return doSequentially(function () { + var dbCache = PouchDB.__dbCacheMap; + var db = dbCache && dbCache.get(dbName); + if (!db) { + // if the db wasn't cached, we may still need to destroy it + // if it's saved to disk + return PouchDB.destroy(dbName, opts); + } + // if this db was cached, we need to remove it from the cache immediately + dbCache.delete(dbName); + return db.destroy(opts); + }); +} + +exports.getOrCreateDB = getOrCreateDB; +exports.deleteDB = deleteDB; \ No newline at end of file diff --git a/packages/node_modules/express-pouchdb/lib/daemon-manager.js b/packages/node_modules/express-pouchdb/lib/daemon-manager.js new file mode 100644 index 00000000..76de44db --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/daemon-manager.js @@ -0,0 +1,34 @@ +"use strict"; + +var Promise = require('pouchdb-promise'); +var utils = require('./utils'); + +var resolved = Promise.resolve(); + +function DaemonManager() { + // There are a few things express-pouchdb needs to do outside of + // requests. These things can't get a PouchDB object from the request + // like other code does. Instead, they register themselves here and + // get an object passed in. By providing both a start and stop + // function, it is possible to switch PouchDB objects on the fly. + this._daemons = []; +} + +DaemonManager.prototype.registerDaemon = function (daemon) { + this._daemons.push(daemon); +}; + +['start', 'stop'].forEach(function (name) { + DaemonManager.prototype[name] = function (PouchDB) { + var funcs = this._daemons.map(function (daemon) { + return daemon[name]; + }); + return utils.callAsyncRecursive(funcs, function (func, next) { + return resolved.then(function () { + return func(PouchDB); + }).then(next); + }); + }; +}); + +module.exports = DaemonManager; diff --git a/packages/node_modules/express-pouchdb/lib/db-wrapper.js b/packages/node_modules/express-pouchdb/lib/db-wrapper.js new file mode 100644 index 00000000..5506fde3 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/db-wrapper.js @@ -0,0 +1,38 @@ +"use strict"; + +var Promise = require('pouchdb-promise'); +var utils = require('./utils'); + +function DatabaseWrapper() { + // Databases can be wrapped to let them provide extra functionality. + // Examples of this include document validation, authorisation, and + // keeping track of the documents in _replicator. + // + // The logic of that is spread out through different parts of + // express-pouchdb, all of which register their functionality on this + // object. This way, the actual wrapping process is abstracted away. + this._wrappers = []; +} +module.exports = DatabaseWrapper; + +DatabaseWrapper.prototype.wrap = function (name, db) { + if (typeof db === 'undefined') { + throw new Error("no db defined!"); + } + if (db.__wrappedByExpressPouch) { + // dbs are cached, so it might already be wrapped + return Promise.resolve(db); + } + db.__wrappedByExpressPouch = true; + return utils.callAsyncRecursive(this._wrappers, function (wrapper, next) { + return Promise.resolve(wrapper(name, db, next)); + }).then(function () { + // https://github.com/pouchdb/pouchdb/issues/1940 + delete db.then; + return db; + }); +}; + +DatabaseWrapper.prototype.registerWrapper = function (wrapper) { + this._wrappers.push(wrapper); +}; diff --git a/packages/node_modules/express-pouchdb/lib/disk-size.js b/packages/node_modules/express-pouchdb/lib/disk-size.js new file mode 100644 index 00000000..1e14cd00 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/disk-size.js @@ -0,0 +1,13 @@ +"use strict"; + +module.exports = function enableDiskSize(app) { + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(require('pouchdb-size')); + } + }); + app.dbWrapper.registerWrapper(function (name, db, next) { + db.installSizeWrapper(); + return next(); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/eval-safely.js b/packages/node_modules/express-pouchdb/lib/eval-safely.js new file mode 100644 index 00000000..5d5ea347 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/eval-safely.js @@ -0,0 +1,11 @@ +'use strict'; + +var vm = require('vm'); + +function evalSafely(code) { + return vm.runInNewContext('(function() {"use strict"; return ' + + code + + ';})()'); +} + +module.exports = evalSafely; \ No newline at end of file diff --git a/packages/node_modules/express-pouchdb/lib/index.js b/packages/node_modules/express-pouchdb/lib/index.js new file mode 100644 index 00000000..551027ae --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/index.js @@ -0,0 +1,255 @@ +"use strict"; + +var wrappers = require('pouchdb-wrappers'), + express = require('express'), + DatabaseWrapper = require('./db-wrapper'), + DaemonManager = require('./daemon-manager'), + Promise = require('pouchdb-promise'), + utils = require('./utils'); + +var modes = {}; +modes.custom = []; +modes.minimumForPouchDB = [ + // sorted alphabetically + 'compression', + 'routes/404', + 'routes/all-dbs', + 'routes/all-docs', + 'routes/attachments', + 'routes/bulk-docs', + 'routes/bulk-get', + 'routes/changes', + 'routes/compact', + 'routes/db', + 'routes/documents', + 'routes/revs-diff', + 'routes/root', + 'routes/session-stub', + 'routes/temp-views', + 'routes/view-cleanup', + 'routes/views' +]; + +modes.fullCouchDB = [ + // sorted alphabetically + 'config-infrastructure', + 'disk-size', + 'logging-infrastructure', + 'replicator', + 'routes/active-tasks', + 'routes/authentication', + 'routes/authorization', + 'routes/cluster', + 'routes/cluster-rewrite', + 'routes/config', + 'routes/db-updates', + 'routes/ddoc-info', + 'routes/fauxton', + 'routes/find', + 'routes/http-log', + 'routes/list', + 'routes/log', + 'routes/replicate', + 'routes/rewrite', + 'routes/security', + 'routes/session', + 'routes/show', + 'routes/special-test-auth', + 'routes/stats', + 'routes/update', + 'routes/uuids', + 'routes/vhosts', + 'validation' +].concat(modes.minimumForPouchDB); + +function toObject(array) { + var result = {}; + array.forEach(function (item) { + result[item] = true; + }); + return result; +} + +module.exports = function (startPouchDB, opts) { + var currentPouchDB; + + // both PouchDB and opts are optional + if (startPouchDB && !startPouchDB.defaults) { + opts = startPouchDB; + startPouchDB = null; + } + opts = opts || {}; + + var app = express(); + app.enable('case sensitive routing'); + app.opts = opts; + + // determine which parts of express-pouchdb to activate + opts.overrideMode = opts.overrideMode || {}; + opts.overrideMode.include = opts.overrideMode.include || []; + opts.overrideMode.exclude = opts.overrideMode.exclude || []; + opts.overrideMode.include.forEach(function (part) { + if (modes.fullCouchDB.indexOf(part) === -1) { + throw new Error( + "opts.overrideMode.include contains the unknown part '" + + part + "'." + ); + } + }); + + var modeIncludes = modes[app.opts.mode || 'fullCouchDB']; + if (!modeIncludes) { + throw new Error('Unknown mode: ' + app.opts.mode); + } + var allIncludes = modeIncludes.concat(app.opts.overrideMode.include); + app.includes = toObject(allIncludes); + app.opts.overrideMode.exclude.forEach(function (part) { + if (!app.includes[part]) { + throw new Error( + "opts.overrideMode.exclude contains the not included part '" + + part + "'." + ); + } + delete app.includes[part]; + }); + + // the daemon manager is a non-negotiable part of express-pouchdb, + // it's needed for static method wrappers & installing + // pouchdb-all-dbs. Both are required for nearly everything. + app.daemonManager = new DaemonManager(); + app.setPouchDB = function (newPouchDB) { + var oldPouchDB = currentPouchDB; + currentPouchDB = newPouchDB; + + var stoppingDone = Promise.resolve(); + if (oldPouchDB) { + stoppingDone = app.daemonManager.stop(oldPouchDB); + } + return stoppingDone.then(function () { + return app.daemonManager.start(newPouchDB); + }); + }; + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + // add PouchDB.new() - by default it just returns 'new PouchDB()' + // also re-adds PouchDB.destroy(), see for reasoning: + // https://github.com/pouchdb/express-pouchdb/pull/231#issuecomment-136095649 + wrappers.installStaticWrapperMethods(PouchDB, {}); + + return Promise.resolve().then(function () { + return PouchDB.allDbs(); + }).catch(function () { + require('pouchdb-all-dbs')(PouchDB); + }); + } + }); + + // the dbWrapper is also a vital part of express-pouchdb which can't + // be disabled. Some methods of dbs need to be wrapped at the very + // least with a noop, to work around incompatible api. + app.dbWrapper = new DatabaseWrapper(); + app.dbWrapper.registerWrapper(function (name, db, next) { + //'fix' the PouchDB api (support opts arg everywhere) + function noop(orig) { + return orig(); + } + var wrapperMethods = {}; + ['info', 'removeAttachment'].forEach(function (name) { + wrapperMethods[name] = noop; + }); + wrappers.installWrapperMethods(db, wrapperMethods); + return next(); + }); + + app.use(function (req, res, next) { + var prop; + + // Normalize query string parameters for direct passing + // into PouchDB queries. + for (prop in req.query) { + if (Object.prototype.hasOwnProperty.call(req.query, prop)) { + try { + req.query[prop] = JSON.parse(req.query[prop]); + } catch (e) {} + } + } + + // Provide the request access to the current PouchDB object. + if (!currentPouchDB) { + var msg = "express-pouchdb needs a PouchDB object to route a request!"; + throw new Error(msg); + } + req.PouchDB = currentPouchDB; + + next(); + }); + + // load all modular files + [ + // order matters in this list! + 'config-infrastructure', + 'logging-infrastructure', + 'compression', + 'disk-size', + 'replicator', + 'routes/http-log', + 'routes/authentication', + 'routes/special-test-auth', + 'routes/authorization', + 'routes/vhosts', + 'routes/cluster-rewrite', + 'routes/rewrite', + 'routes/root', + 'routes/log', + 'routes/session', + 'routes/session-stub', + 'routes/fauxton', + 'routes/cluster', + 'routes/config', + 'routes/uuids', + 'routes/all-dbs', + 'routes/replicate', + 'routes/active-tasks', + 'routes/db-updates', + 'routes/stats', + 'routes/db', + 'routes/bulk-docs', + 'routes/bulk-get', + 'routes/all-docs', + 'routes/changes', + 'routes/compact', + 'routes/revs-diff', + 'routes/security', + 'routes/view-cleanup', + 'routes/temp-views', + 'routes/find', + 'routes/views', + 'routes/ddoc-info', + 'routes/show', + 'routes/list', + 'routes/update', + 'routes/attachments', + 'routes/documents', + 'validation', + 'routes/404' + ].forEach(function (file) { + if (app.includes[file]) { + require('./' + file)(app); + } + }); + + if (app.couchConfig) { + app.couchConfig.registerDefault( + 'couchdb', + 'max_document_size', + utils.maxDocumentSizeDefault + ); + } + + if (startPouchDB) { + app.setPouchDB(startPouchDB); + } + + return app; +}; diff --git a/packages/node_modules/express-pouchdb/lib/logging-infrastructure.js b/packages/node_modules/express-pouchdb/lib/logging-infrastructure.js new file mode 100644 index 00000000..a19c6ed9 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/logging-infrastructure.js @@ -0,0 +1,95 @@ +"use strict"; + +var fs = require('fs'); +var utils = require('./utils'); + +// sorted from a lot of output to no output +var LOG_LEVELS = ['debug', 'info', 'warning', 'error', 'none']; + +function CouchLogger(file, level) { + // set start values + this.setFile(file); + this.setLevel(level); +} + +CouchLogger.prototype.setFile = function (file) { + if (this._stream) { + this._stream.end(); + } + this._file = file; + this._stream = fs.createWriteStream(file, { + flags: 'a', + encoding: 'UTF-8' + }); +}; + +CouchLogger.prototype.setLevel = function (level) { + var newLevel = LOG_LEVELS.indexOf(level); + if (newLevel === -1) { + newLevel = LOG_LEVELS.indexOf("info"); // the default + } + this._level = newLevel; +}; + +CouchLogger.prototype._write = function (level, message) { + if (LOG_LEVELS.indexOf(level) < this._level) { + //don't log + return; + } + var date = new Date().toUTCString(); + var pid = '0.000.0'; + + var str = '[{0}] [{1}] [<{2}>] {3}\n' + .replace('{0}', date) + .replace('{1}', level) + .replace('{2}', pid) + .replace('{3}', message); + + this._stream.write(str); +}; + +// defines CouchLogger.debug(); CouchLogger.info(), +// CouchLogger.warning() & CouchLogger.error() + +// slice so the 'none' level doesn't get a function. +LOG_LEVELS.slice(0, -1).forEach(function (logLevel) { + CouchLogger.prototype[logLevel] = function (message) { + this._write(logLevel, message); + }; +}); + +CouchLogger.prototype.getLog = function (bytes, offset, callback) { + var self = this; + fs.stat(self._file, function (err, info) { + if (err) { + return callback(err); + } + var opts = { + start: Math.max(info.size - bytes - offset - 1, 0), + end: Math.max(info.size - offset - 1, 0), + encoding: 'UTF-8' + }; + callback(null, { + stream: fs.createReadStream(self._file, opts), + length: opts.end - opts.start + }); + }); +}; + +module.exports = function (app) { + utils.requires(app, 'config-infrastructure'); + + // set up log file + var logPath = app.opts.logPath || './log.txt'; + app.couchConfig.registerDefault('log', 'file', logPath); + app.couchConfig.registerDefault('log', 'level', 'info'); + var getFile = app.couchConfig.get.bind(app.couchConfig, 'log', 'file'); + var getLevel = app.couchConfig.get.bind(app.couchConfig, 'log', 'level'); + app.couchLogger = new CouchLogger(getFile(), getLevel()); + app.couchConfig.on('log.file', function () { + app.couchLogger.setFile(getFile()); + }); + app.couchConfig.on('log.level', function () { + app.couchLogger.setLevel(getLevel()); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/replicator.js b/packages/node_modules/express-pouchdb/lib/replicator.js new file mode 100644 index 00000000..d44eefd0 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/replicator.js @@ -0,0 +1,83 @@ +"use strict"; + +var Promise = require('pouchdb-promise'); +var utils = require('./utils'); +var getOrCreateDB = require('./create-or-delete-dbs').getOrCreateDB; + +module.exports = function enableReplicator(app) { + var db, PouchDB; + + utils.requires(app, 'config-infrastructure'); + + // explain how to activate the replicator db logic. + app.dbWrapper.registerWrapper(function (name, db, next) { + if (name === getReplicatorDBName()) { + return db.useAsReplicatorDB(); + } + return next(); + }); + + app.couchConfig.registerDefault('replicator', 'db', '_replicator'); + function getReplicatorDBName() { + return app.couchConfig.get('replicator', 'db'); + } + + app.couchConfig.on('replicator.db', restartDaemon); + + function onDestroy(dbName) { + // if the replicator db was removed, it should re-appear. + if (dbName === getReplicatorDBName()) { + restartDaemon(); + } + } + + function restartDaemon() { + return daemon.stop().then(function () { + return daemon.start(); + }); + } + + var currentActions = Promise.resolve(); + function serialExecution(func) { + currentActions = currentActions.then(func); + return currentActions; + } + + var daemon = { + start: function (thePouchDB) { + if (thePouchDB) { + PouchDB = thePouchDB; + + PouchDB.plugin(require('pouchdb-replicator')); + } + if (PouchDB.isHTTPPouchDB) { + return; + } + + return serialExecution(function () { + var name = getReplicatorDBName(); + return getOrCreateDB(PouchDB, name).then(function (db) { + PouchDB.on('destroyed', onDestroy); + return db.startReplicatorDaemon(); + }); + }); + }, + stop: function () { + if (PouchDB.isHTTPPouchDB) { + return; + } + return serialExecution(function () { + PouchDB.removeListener('destroyed', onDestroy); + // stop old replicator daemon + return Promise.resolve().then(function () { + return db.stopReplicatorDaemon(); + }).catch(function () { + // no worries if stopping failed, the database is probably + // just not there (or deleted). + }); + }); + } + }; + + app.daemonManager.registerDaemon(daemon); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/404.js b/packages/node_modules/express-pouchdb/lib/routes/404.js new file mode 100644 index 00000000..f174c514 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/404.js @@ -0,0 +1,13 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + // 404 handler + app.use(function (req, res) { + utils.sendJSON(res, 404, { + error: "not_found", + reason: "missing" + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/active-tasks.js b/packages/node_modules/express-pouchdb/lib/routes/active-tasks.js new file mode 100644 index 00000000..852292b7 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/active-tasks.js @@ -0,0 +1,11 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + // Active tasks (stub for now) + app.get('/_active_tasks', function (req, res) { + // TODO: implement + utils.sendJSON(res, 200, []); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/all-dbs.js b/packages/node_modules/express-pouchdb/lib/routes/all-dbs.js new file mode 100644 index 00000000..5402844d --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/all-dbs.js @@ -0,0 +1,19 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + // List all databases. + app.get('/_all_dbs', function (req, res) { + req.PouchDB.allDbs(function (err, response) { + if (err) { + return utils.sendError(res, err); + } + + response = response.filter(function (name) { + return name !== 'pouch__auth_sessions__'; + }); + utils.sendJSON(res, 200, response); + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/all-docs.js b/packages/node_modules/express-pouchdb/lib/routes/all-docs.js new file mode 100644 index 00000000..c131cbe2 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/all-docs.js @@ -0,0 +1,30 @@ +"use strict"; + +var utils = require('../utils'), + extend = require('extend'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + var isAllowedMethod = function (method, allowedMethods) { + return allowedMethods.indexOf(method) !== -1; + }; + + // All docs operations + app.all('/:db/_all_docs', utils.jsonParser, function (req, res, next) { + if (!isAllowedMethod(req.method, ['GET', 'HEAD', 'POST'])) { + return next(); + } + + // Check that the request body, if present, is an object. + if (req.body && (typeof req.body !== 'object' || Array.isArray(req.body))) { + return utils.sendJSON(res, 400, { + reason: "Request body must be a JSON object", + error: 'bad_request' + }); + } + + var opts = utils.makeOpts(req, extend({}, req.body, req.query)); + req.db.allDocs(opts, utils.sendCallback(res, 400)); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/attachments.js b/packages/node_modules/express-pouchdb/lib/routes/attachments.js new file mode 100644 index 00000000..5c19e041 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/attachments.js @@ -0,0 +1,113 @@ +"use strict"; + +var utils = require('../utils'); +var Promise = require('pouchdb-promise'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + // Put a document attachment + function putAttachment(db, name, req, res) { + utils.parseRawBody(req, res, function () { + var attachment = req.params.attachment, + rev = req.query.rev, + type = req.get('Content-Type') || 'application/octet-stream', + body = new Buffer(req.rawBody || '', 'binary'), + opts = utils.makeOpts(req); + + function cb(err, response) { + if (err) { + return utils.sendError(res, err); + } + res.set('ETag', JSON.stringify(response.rev)); + var attachmentURI = encodeURIComponent(attachment); + utils.setLocation(res, db + '/' + name + '/' + attachmentURI); + utils.sendJSON(res, 201, response); + } + req.db.putAttachment(name, attachment, rev, body, type, opts, cb); + }); + } + + app.put('/:db/_design/:id/:attachment(*)', function (req, res) { + putAttachment(req.params.db, '_design/' + req.params.id, req, res); + }); + + app.put('/:db/:id/:attachment(*)', function (req, res, next) { + // Be careful not to catch normal design docs or local docs + if (req.params.id === '_design' || req.params.id === '_local') { + return next(); + } + putAttachment(req.params.db, req.params.id, req, res); + }); + + // Retrieve a document attachment + function getAttachment(name, req, res) { + var attachment = req.params.attachment; + var opts = utils.makeOpts(req, req.query); + + return Promise.all([ + req.db.get(name, opts), + req.db.getAttachment(name, attachment, opts) + ]).then(function (results) { + var doc = results[0]; + var att = results[1]; + + var attInfo = doc._attachments && doc._attachments[attachment]; + + if (!attInfo) { + return utils.sendJSON(res, 404, { + error: 'not_found', + reason: 'missing' + }); + } + + var type = attInfo.content_type; + var md5 = attInfo.digest.slice(4); + + res.set('ETag', JSON.stringify(md5)); + res.setHeader('Content-Type', type); + res.status(200).send(att); + }).catch(function (err) { + utils.sendError(res, err); + }); + } + + app.get('/:db/_design/:id/:attachment(*)', function (req, res) { + getAttachment('_design/' + req.params.id, req, res); + }); + + app.get('/:db/:id/:attachment(*)', function (req, res, next) { + // Be careful not to catch normal design docs or local docs + if (req.params.id === '_design' || req.params.id === '_local') { + return next(); + } + getAttachment(req.params.id, req, res); + }); + + // Delete a document attachment + function deleteAttachment(name, req, res) { + var attachment = req.params.attachment, + rev = req.query.rev, + opts = utils.makeOpts(req); + + function cb(err, response) { + if (err) { + return utils.sendError(res, err); + } + utils.sendJSON(res, 200, response); + } + req.db.removeAttachment(name, attachment, rev, opts, cb); + } + + app.delete('/:db/_design/:id/:attachment(*)', function (req, res) { + deleteAttachment('_design/' + req.params.id, req, res); + }); + + app.delete('/:db/:id/:attachment(*)', function (req, res, next) { + // Be careful not to catch normal design docs or local docs + if (req.params.id === '_design' || req.params.id === '_local') { + return next(); + } + deleteAttachment(req.params.id, req, res); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/authentication.js b/packages/node_modules/express-pouchdb/lib/routes/authentication.js new file mode 100644 index 00000000..444d1561 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/authentication.js @@ -0,0 +1,148 @@ +"use strict"; + +var cookieParser = require('cookie-parser'), + basicAuth = require('basic-auth'), + utils = require('../utils'), + Promise = require('pouchdb-promise'), + Auth = require('pouchdb-auth'); + +var SECTION = 'couch_httpd_auth'; + +module.exports = function (app) { + var usersDBPromise, refreshUsersDBImpl; + + utils.requires(app, 'config-infrastructure'); + utils.requires(app, 'logging-infrastructure'); + + app.couchConfig.registerDefault(SECTION, 'authentication_db', '_users'); + app.couchConfig.registerDefault(SECTION, 'timeout', 600); + app.couchConfig.registerDefault(SECTION, 'secret', Auth.generateSecret()); + app.couchConfig.registerDefault(SECTION, 'iterations', 10); + + // explain how to activate the auth db logic. + app.dbWrapper.registerWrapper(function (name, db, next) { + if (name === getUsersDBName()) { + return db.useAsAuthenticationDB({ + isOnlineAuthDB: false, + timeout: app.couchConfig.get(SECTION, 'timeout'), + secret: app.couchConfig.get(SECTION, 'secret'), + iterations: app.couchConfig.get(SECTION, 'iterations'), + admins: app.couchConfig.getSection('admins') + }); + } + return next(); + }); + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(Auth); + + refreshUsersDBImpl = function () { + usersDBPromise = utils.getUsersDB(app, PouchDB); + }; + refreshUsersDB(); + PouchDB.on('destroyed', onDestroyed); + }, + stop: function (PouchDB) { + PouchDB.removeListener('destroyed', onDestroyed); + } + }); + + // utils + var getUsersDBName = utils.getUsersDBName.bind(null, app); + + function getUsersDB() { + // calls itself until usersDBPromise is a available + if (!usersDBPromise) { + return new Promise(function (resolve) { + setImmediate(function () { + resolve(getUsersDB()); + }); + }); + } + return usersDBPromise; + } + + function onDestroyed(dbName) { + // if the users db was removed, it should re-appear. + if (dbName === getUsersDBName()) { + refreshUsersDB(); + } + } + + function refreshUsersDB() { + return refreshUsersDBImpl(); + } + + // ensure there's always a users db + app.couchConfig.on(SECTION + '.authentication_db', refreshUsersDB); + app.couchConfig.on(SECTION + '.timeout', refreshUsersDB); + app.couchConfig.on(SECTION + '.secret', refreshUsersDB); + app.couchConfig.on(SECTION + '.iterations', refreshUsersDB); + app.couchConfig.on('admins', refreshUsersDB); + + // routing + app.use(cookieParser()); + + app.use(function (req, res, next) { + // TODO: TIMING ATTACK + Promise.resolve().then(function () { + return buildCookieSession(req, res); + }).catch(function () { + return buildBasicAuthSession(req); + }).then(function (result) { + req.couchSession = result; + req.couchSession.info.authentication_handlers = ['cookie', 'default']; + next(); + }).catch(function (err) { + utils.sendError(res, err); + }); + }); + + function buildCookieSession(req, res) { + var sessionID = (req.cookies || {}).AuthSession; + if (!sessionID) { + throw new Error("No cookie, so no cookie auth."); + } + return getUsersDB().then(function (db) { + return db.multiUserSession(sessionID); + }).then(function (session) { + if (session.info.authenticated) { + res.cookie('AuthSession', session.sessionID, {httpOnly: true}); + delete session.sessionID; + session.info.authenticated = 'cookie'; + logSuccess('cookie', session); + } + return session; + }); + } + + function logSuccess(type, session) { + var msg = 'Successful ' + type + ' auth as: "' + session.userCtx.name + '"'; + app.couchLogger.debug(msg); + } + + function buildBasicAuthSession(req) { + var userInfo = basicAuth(req); + var db; + var initializingDone = getUsersDB().then(function (theDB) { + db = theDB; + }); + if (userInfo) { + initializingDone = initializingDone.then(function () { + return db.multiUserLogIn(userInfo.name, userInfo.pass); + }); + } + return initializingDone.then(function (info) { + return db.multiUserSession((info || {}).sessionID); + }).then(function (session) { + delete session.sessionID; + + if (session.info.authenticated) { + session.info.authenticated = 'default'; + logSuccess('http basic', session); + } + return session; + }); + } +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/authorization.js b/packages/node_modules/express-pouchdb/lib/routes/authorization.js new file mode 100644 index 00000000..2963e200 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/authorization.js @@ -0,0 +1,29 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/authentication'); + + // routes that need server admin protection + app.get('/_config', requiresServerAdmin); + app.get('/_config/:section', requiresServerAdmin); + app.get('/_config/:section/:key', requiresServerAdmin); + app.put('/_config/:section/:key', requiresServerAdmin); + app.delete('/_config/:section/:key', requiresServerAdmin); + + app.get('/_log', requiresServerAdmin); + app.get('/_active_tasks', requiresServerAdmin); + app.get('/_db_updates', requiresServerAdmin); + app.post('/_restart', requiresServerAdmin); + + function requiresServerAdmin(req, res, next) { + if (req.couchSession.userCtx.roles.indexOf('_admin') !== -1) { + return next(); + } + utils.sendJSON(res, 401, { + error: 'unauthorized', + reason: "You are not a server admin." + }); + } +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/bulk-docs.js b/packages/node_modules/express-pouchdb/lib/routes/bulk-docs.js new file mode 100644 index 00000000..b31fbc79 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/bulk-docs.js @@ -0,0 +1,47 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + // Bulk docs operations + app.post('/:db/_bulk_docs', utils.jsonParser, function (req, res) { + + // Maybe this should be moved into the leveldb adapter itself? Not sure + // how uncommon it is for important options to come through in the body + // https://github.com/daleharvey/pouchdb/issues/435 + var opts = 'new_edits' in req.body ? + { new_edits: req.body.new_edits } : {}; + opts = utils.makeOpts(req, opts); + + if (Array.isArray(req.body)) { + return utils.sendJSON(res, 400, { + error: "bad_request", + reason: "Request body must be a JSON object" + }); + } + if (!req.body.docs) { + return utils.sendJSON(res, 400, { + error: "bad_request", + reason: "Missing JSON list of 'docs'" + }); + } + + req.db.bulkDocs(req.body, opts, function (err, response) { + if (err) { + return utils.sendError(res, err); + } + utils.sendJSON(res, 201, response.map(function (info) { + if (info.error) { + return { + id: info.id, + error: info.name, + reason: info.message + }; + } + return info; + })); + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/bulk-get.js b/packages/node_modules/express-pouchdb/lib/routes/bulk-get.js new file mode 100644 index 00000000..09c14f08 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/bulk-get.js @@ -0,0 +1,59 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + // Bulk docs operations + app.post('/:db/_bulk_get', utils.jsonParser, function (req, res) { + + var opts = req.body || {}; + + if (Array.isArray(req.body)) { + return utils.sendJSON(res, 400, { + error: "bad_request", + reason: "Request body must be a JSON object" + }); + } + if (!req.body.docs) { + return utils.sendJSON(res, 400, { + error: "bad_request", + reason: "Missing JSON list of 'docs'" + }); + } + + if (typeof req.db.bulkGet !== 'function') { + return utils.sendJSON(res, 400, { + error: "bad_request", + reason: "bulkGet not supported by this version of PouchDB" + }); + } + + // some options can be specified via query, e.g. ?revs=true + Object.keys(req.query).forEach(function (param) { + opts[param] = req.query[param]; + }); + + req.db.bulkGet(opts, function (err, response) { + if (err) { + return utils.sendError(res, err); + } + var httpResults = response.results.map(function (docInfo) { + return { + id: docInfo.id, + docs: docInfo.docs.map(function (info) { + if (info.error) { + return { + error: info.name, + reason: info.message + }; + } + return info; + }) + }; + }); + utils.sendJSON(res, 200, { results: httpResults }); + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/changes.js b/packages/node_modules/express-pouchdb/lib/routes/changes.js new file mode 100644 index 00000000..db24a59e --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/changes.js @@ -0,0 +1,103 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + // Monitor database changes + function changes(req, res) { + + utils.setJsonOrPlaintext(res); + + // api.changes expects a property `query_params` + // This is a pretty inefficient way to do it.. Revisit? + req.query.query_params = JSON.parse(JSON.stringify(req.query)); + + req.query = utils.makeOpts(req, req.query); + + if (req.body && req.body.doc_ids) { + req.query.doc_ids = req.body.doc_ids; + } + + if (req.query.feed === 'continuous' || req.query.feed === 'longpoll') { + var heartbeatInterval; + // 60000 is the CouchDB default + // TODO: figure out if we can make this default less aggressive + var heartbeat = (typeof req.query.heartbeat === 'number') ? + req.query.heartbeat : 6000; + var written = false; + heartbeatInterval = setInterval(function () { + written = true; + res.write('\n'); + }, heartbeat); + + var cleanup = function () { + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + } + }; + + if (req.query.feed === 'continuous') { + req.query.live = req.query.continuous = true; + req.db.changes(req.query).on('change', function (change) { + written = true; + utils.writeJSON(res, change); + }).on('error', function (err) { + if (!written) { + utils.sendError(res, err); + } else { + res.end(); + } + cleanup(); + }); + } else { // longpoll + // first check if there are >0. if so, return them immediately + req.query.live = req.query.continuous = false; + req.db.changes(req.query).then(function (complete) { + if (complete.results.length) { + utils.writeJSON(res, complete); + res.end(); + cleanup(); + } else { // do the longpolling + // mimicking CouchDB, start sending the JSON immediately + res.write('{"results":[\n'); + req.query.live = req.query.continuous = true; + var changes = req.db.changes(req.query) + .on('change', function (change) { + utils.writeJSON(res, change); + res.write('],\n"last_seq":' + change.seq + '}\n'); + res.end(); + changes.cancel(); + cleanup(); + }).on('error', function (err) { + // shouldn't happen + console.log(err); + res.end(); + cleanup(); + }).on('complete', function () { + cleanup(); + }); + + req.connection.on('close', function () { + changes.cancel(); + }); + } + }, function (err) { + if (!written) { + utils.sendError(res, err); + } + cleanup(); + }); + } + } else { // straight shot, not continuous + req.db.changes(req.query).then(function (response) { + utils.sendJSON(res, 200, response); + }).catch(function (err) { + utils.sendError(res, err); + }); + } + } + app.get('/:db/_changes', changes); + app.post('/:db/_changes', utils.jsonParser, changes); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/cluster-rewrite.js b/packages/node_modules/express-pouchdb/lib/routes/cluster-rewrite.js new file mode 100644 index 00000000..7badba55 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/cluster-rewrite.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = function (app) { + app.use(function (req, res, next) { + // magical route for "clustering" to support new Fauxton UI + var regex = /^\/_node\/node1@127.0.0.1/; + req.url = req.url.replace(regex, ''); + next(); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/cluster.js b/packages/node_modules/express-pouchdb/lib/routes/cluster.js new file mode 100644 index 00000000..7d042015 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/cluster.js @@ -0,0 +1,21 @@ +'use strict'; + +var utils = require('../utils'); + +module.exports = function (app) { + + // there is only one node, but these APIs + // are required for fauxton + app.get('/_membership', function (req, res) { + utils.sendJSON(res, 200, { + all_nodes: ['node1@127.0.0.1'], + cluster_nodes: ['node1@127.0.0.1'] + }); + }); + + app.get('/_cluster_setup', function (req, res) { + utils.sendJSON(res, 201, { + state: 'cluster_disabled' + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/compact.js b/packages/node_modules/express-pouchdb/lib/routes/compact.js new file mode 100644 index 00000000..9b01df9d --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/compact.js @@ -0,0 +1,17 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + // DB Compaction + app.post('/:db/_compact', utils.jsonParser, function (req, res) { + req.db.compact(utils.makeOpts(req), function (err) { + if (err) { + return utils.sendError(res, err); + } + utils.sendJSON(res, 200, {ok: true}); + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/config.js b/packages/node_modules/express-pouchdb/lib/routes/config.js new file mode 100644 index 00000000..df8b7663 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/config.js @@ -0,0 +1,155 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'config-infrastructure'); + + // Config + app.get('/_config', function (req, res) { + utils.sendJSON(res, 200, app.couchConfig.getAll()); + }); + + app.get('/_config/:section', function (req, res) { + utils.sendJSON(res, 200, app.couchConfig.getSection(req.params.section)); + }); + + app.get('/_config/:section/:key', function (req, res) { + var value = app.couchConfig.get(req.params.section, req.params.key); + sendConfigValue(res, value); + }); + + function sendConfigValue(res, value) { + if (typeof value === "undefined") { + return utils.sendJSON(res, 404, { + error: "not_found", + reason: "unknown_config_value" + }); + } + if (typeof value !== 'string') { + value = JSON.stringify(value); + } + utils.sendJSON(res, 200, value); + } + + function putHandler(req, res) { + // Custom JSON parsing, because the default JSON body parser + // middleware only supports JSON lists and objects. (Not numbers etc.) + var value; + try { + value = JSON.parse(req.rawBody.toString('utf-8')); + } catch (err) { + return utils.sendJSON(res, 400, { + error: "bad_request", + reason: "invalid_json" + }); + } + if (typeof value !== "string") { + return utils.sendJSON(res, 500, { + error: "unknown_error", + reason: "badarg" + }); + } + var section = req.params.section; + var key = req.params.key; + if (!whitelisted(section, key)) { + return sendModificationNotAllowed(res); + } + try { + value = JSON.parse(value); + } catch (err) {} + + function cb(err, oldValue) { + utils.sendJSON(res, 200, oldValue || ""); + } + app.couchConfig.set(section, key, value, cb); + } + app.put('/_config/:section/:key', utils.parseRawBody, putHandler); + + app.delete('/_config/:section/:key', function (req, res) { + var section = req.params.section; + var key = req.params.key; + if (!whitelisted(section, key)) { + return sendModificationNotAllowed(res); + } + app.couchConfig.delete(section, key, function (err, oldValue) { + sendConfigValue(res, oldValue); + }); + }); + + function getWhitelist() { + // parses an erlang term like: + // [{httpd,config_whitelist}, {<<"test">>,<<"foo">>}] + // into a JS array like: + // [['httpd', 'config_whitelist'], ['test', 'foo']] + // + // returns undefined when a parse error occurs, or when there is + // no whitelist + var val = app.couchConfig.get('httpd', 'config_whitelist'); + val = (val || '').trim(); + if (!val) { + return; + } + if (val[0] + val[val.length - 1] !== '[]') { + // invalid syntax + return; + } + val = val.slice(1, -1).trim(); + var whitelist = []; + if (!val) { + return whitelist; + } + + var foundAtLeastOneMatch = false; + var re = /{([^}]*)}/g; + var match; + while ((match = re.exec(val))) { + foundAtLeastOneMatch = true; + + var part = match[1]; + var subParts = parseToJSStrings(part.split(',')); + if (subParts.length !== 2) { + // invalid syntax + return; + } + whitelist.push(subParts); + } + if (foundAtLeastOneMatch) { + return whitelist; + } + // invalid syntax, return undefined + } + + function whitelisted(section, key) { + var wl = getWhitelist(); + if (!wl) { + // when the whitelist doesn't exist or can't be parsed every + // change is allowed + return true; + } + // only section/key pairs on the whitelist are allowed when there is + // one + return wl.some(function (item) { + return item[0] === section && item[1] === key; + }); + } +}; + +function parseToJSStrings(erlangStrings) { + return erlangStrings.map(function (str) { + return str.trim(); + }).map(function (str) { + // "string" -> string + return (/"([^"]*)"/.exec(str) || {})[1] || str; + }).map(function (str) { + // <<"string">> -> string + return (/<<"([^"]*)">>/.exec(str) || {})[1] || str; + }); +} + +function sendModificationNotAllowed(res) { + utils.sendJSON(res, 400, { + name: "modification_not_allowed", + reason: "This config variable is read-only" + }); +} diff --git a/packages/node_modules/express-pouchdb/lib/routes/db-updates.js b/packages/node_modules/express-pouchdb/lib/routes/db-updates.js new file mode 100644 index 00000000..5e9a32f4 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/db-updates.js @@ -0,0 +1,33 @@ +"use strict"; + +var events = require('events'); + +module.exports = function (app) { + // init DbUpdates + var couchDbUpdates = new events.EventEmitter(); + + function onDBCreated(dbName) { + couchDbUpdates.emit('update', {db_name: dbName, type: 'created'}); + } + function onDBDestroyed(dbName) { + couchDbUpdates.emit('update', {db_name: dbName, type: 'deleted'}); + } + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.on('created', onDBCreated); + PouchDB.on('destroyed', onDBDestroyed); + }, + stop: function (PouchDB) { + PouchDB.removeListener('created', onDBCreated); + PouchDB.removeListener('destroyed', onDBDestroyed); + } + }); + + app.all('/_db_updates', function (req, res) { + // TODO: implement + res.status(400).end(); + // app.couch_db_updates.on('update', function(update) { + // utils.sendJSON(res, 200, update); + // }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/db.js b/packages/node_modules/express-pouchdb/lib/routes/db.js new file mode 100644 index 00000000..60a9eb6b --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/db.js @@ -0,0 +1,142 @@ +"use strict"; + +var startTime = new Date().getTime(), + utils = require('../utils'), + wrappers = require('pouchdb-wrappers'), + mkdirp = require('mkdirp'), + pathResolve = require('path').resolve, + cleanFilename = require('../clean-filename'), + deleteDB = require('../create-or-delete-dbs').deleteDB; + +module.exports = function (app) { + // Create a database. + app.put('/:db', utils.jsonParser, function (req, res) { + var name = cleanFilename(req.params.db); + + req.PouchDB.allDbs(function (err, dbs) { + if (err) { + return utils.sendError(res, err); + } + + if (dbs.indexOf(name) !== -1) { + return utils.sendJSON(res, 412, { + 'error': 'file_exists', + 'reason': 'The database could not be created.' + }); + } + + // PouchDB.new() instead of new PouchDB() because that adds + // authorisation logic + var db = req.PouchDB.new(name, utils.makeOpts(req)); + db.info(function (err) { + if (err) { + // temoperary workaround for leveldb adapter, see + // https://github.com/pouchdb/pouchdb/issues/5668 + // when removing this code, also remove mkdir + // from dependencies in package.json + if (err.name === 'OpenError') { + var path = db.__opts.prefix ? db.__opts.prefix + name : name; + + mkdirp(pathResolve(path), function (err) { + if (err) { + return utils.sendError(res, err, 412); + } + + db = req.PouchDB.new(name, utils.makeOpts(req)); + db.info(function (err) { + + if (err) { + return utils.sendError(res, err, 412); + } + utils.setLocation(res, name); + utils.sendJSON(res, 201, {ok: true}); + }); + }); + return; + } + + return utils.sendError(res, err, 412); + } + utils.setLocation(res, name); + utils.sendJSON(res, 201, {ok: true}); + }); + }); + }); + + // Delete a database + app.delete('/:db', function (req, res) { + if (req.query.rev) { + return utils.sendJSON(res, 400, { + error: 'bad_request', + reason: ( + "You tried to DELETE a database with a ?rev= parameter. " + + "Did you mean to DELETE a document instead?" + ) + }); + } + + var dbName = cleanFilename(req.params.db); + deleteDB(req.PouchDB, dbName, utils.makeOpts(req)).then(function () { + utils.sendJSON(res, 200, {ok: true}); + }, function (err) { + utils.sendError(res, err); + }); + }); + + // At this point, some route middleware can take care of identifying the + // correct PouchDB instance. + ['/:db/*', '/:db'].forEach(function (route) { + app.all(route, function (req, res, next) { + utils.setDBOnReq(req.params.db, app.dbWrapper, req, res, next); + }); + }); + + // Get database information + app.get('/:db', function (req, res) { + req.db.info(utils.makeOpts(req), function (err, info) { + if (err) { + return utils.sendError(res, err); + } + info.instance_start_time = startTime.toString(); + // TODO: data_size? + utils.sendJSON(res, 200, info); + }); + }); + + // Ensure all commits are written to disk + app.post('/:db/_ensure_full_commit', function (req, res) { + // TODO: implement. Also check security then: who is allowed to + // access this? (db & server admins?) + utils.sendJSON(res, 201, { + ok: true, + instance_start_time: startTime.toString() + }); + }); + + app.dbWrapper.registerWrapper(function (name, db, next) { + // db.info() should just return the non-uri encoded db name + wrappers.installWrapperMethods(db, { + info: function (orig) { + return orig().then(function (info) { + info.db_name = decodeURIComponent(info.db_name); + return info; + }); + } + }); + + return next(); + }); + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + // PouchDB.allDbs() should return the non-uri encoded db name + wrappers.installStaticWrapperMethods(PouchDB, { + allDbs: function (orig) { + return orig().then(function (dbs) { + return dbs.map(decodeURIComponent); + }); + } + }); + } + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/ddoc-info.js b/packages/node_modules/express-pouchdb/lib/routes/ddoc-info.js new file mode 100644 index 00000000..25dcf7cd --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/ddoc-info.js @@ -0,0 +1,20 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + // Query design document info + app.get('/:db/_design/:id/_info', function (req, res) { + // Dummy data for Fauxton - when implementing fully also take into + // account req.couchSessionObj - this needs at least db view rights it + // seems. + // + // Also, if the implementation uses req.db, don't forget to add + // utils.requires(app, 'routes/db') at the top of the main function + // of this file. + utils.sendJSON(res, 200, { + 'name': req.query.id, + 'view_index': 'Not implemented.' + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/documents.js b/packages/node_modules/express-pouchdb/lib/routes/documents.js new file mode 100644 index 00000000..ccf24a82 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/documents.js @@ -0,0 +1,269 @@ +"use strict"; + +var fs = require('fs'), + multiparty = require('multiparty'), + utils = require('../utils'), + uuids = require('../uuids'), + extend = require('extend'), + Promise = require('pouchdb-promise'), + denodeify = require('denodeify'), + readFile = denodeify(fs.readFile); + +function onPutOrPostResponse(req, res) { + return function (response) { + res.set('ETag', '"' + response.rev + '"'); + utils.setLocation(res, req.params.db + '/' + response.id); + utils.sendJSON(res, 201, response); + }; +} + +function mergeAttachments(doc, attachments) { + if (!doc._attachments) { + doc._attachments = {}; + } + + // don't store the "follows" key + Object.keys(doc._attachments).forEach(function (filename) { + delete doc._attachments[filename].follows; + }); + // merge, since it could be a mix of stubs and non-stubs + doc._attachments = extend(true, doc._attachments, attachments); +} + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + if (app.couchConfig) { + app.use('/' + + app.couchConfig.get('couch_httpd_auth', 'authentication_db') + '/:id', + function (req, res, next) { + var userCtx = (req.couchSession || {}).userCtx; + if (userCtx && userCtx.name && !~userCtx.roles.indexOf('_admin')) { + req.db.get(req.params.id, function (err, response) { + if (!err && response.name === userCtx.name) { + userCtx.roles.push('_admin'); + } + next(); + }); + return; + } + next(); + } + ); + } + + // Slightly unusual endpoint where you can POST an attachment to a doc. + // Used by the Fauxton UI for uploading attachments. + app.post('/:db/:id(*)', utils.jsonParser, function (req, res) { + if (!/^multipart\/form-data/.test(req.headers['content-type'])) { + return utils.sendJSON(res, 400, { + error: "bad_request", + reason: "only_multipart_accepted" + }); + } + + var opts = utils.makeOpts(req, req.query); + + var promise = Promise.resolve(); + var attachments = {}; + var form = new multiparty.Form(); + var doc; + form.on('error', function (err) { + promise = promise.then(function () { + throw err; + }); + }).on('field', function (name, field) { + if (name !== '_rev') { + return; + } + promise = promise.then(function () { + return req.db.get(req.params.id, {rev: field}); + }).then(function (theDoc) { + doc = theDoc; + }); + }).on('file', function (_, file) { + var type = file.headers['content-type']; + var filename = file.originalFilename; + promise = promise.then(function () { + return readFile(file.path); + }).then(function (body) { + attachments[filename] = { + content_type: type, + data: body + }; + }); + }).on('close', function () { + promise.then(function () { + if (!doc) { + var err = new Error('bad_request'); + err.reason = 'no_doc_provided'; + err.status = 400; + throw err; + } + mergeAttachments(doc, attachments); + return req.db.put(doc, opts); + }).then( + onPutOrPostResponse(req, res) + ).catch(function (err) { + utils.sendError(res, err); + }); + }); + form.parse(req); + }); + + // Create or update document that has an ID + app.put('/:db/:id(*)', utils.jsonParser, function (req, res) { + + var opts = utils.makeOpts(req, req.query); + + if (/^multipart\/related/.test(req.headers['content-type'])) { + var doc; + var promise = Promise.resolve(); + var form = new multiparty.Form(); + var attachments = {}; + form.on('error', function (err) { + promise = promise.then(function () { + throw err; + }); + }).on('field', function (_, field) { + doc = JSON.parse(field); + }).on('file', function (_, file) { + var type = file.headers['content-type']; + var filename = file.originalFilename; + promise = promise.then(function () { + return readFile(file.path); + }).then(function (body) { + attachments[filename] = { + content_type: type, + data: body + }; + }); + }).on('close', function () { + promise.then(function () { + mergeAttachments(doc, attachments); + return req.db.put(doc, opts); + }).then( + onPutOrPostResponse(req, res) + ).catch(function (err) { + utils.sendError(res, err); + }); + }); + form.parse(req); + } else { + // normal PUT + req.body._id = req.body._id || req.query.id; + if (!req.body._id) { + req.body._id = (!!req.params.id && req.params.id !== 'null') ? + req.params.id : null; + } + req.body._rev = getRev(req, req.body); + req.db.put(req.body, opts).then( + onPutOrPostResponse(req, res) + ).catch(function (err) { + utils.sendError(res, err); + }); + } + }); + + function getRev(req, doc) { + return doc._rev || req.query.rev; + } + + // Create a document + app.post('/:db', utils.jsonParser, function (req, res) { + var opts = utils.makeOpts(req, req.query); + + req.body._id = req.body._id || uuids(1)[0]; + req.db.put(req.body, opts, function (err, response) { + if (err) { + return utils.sendError(res, err); + } + utils.setLocation(res, req.params.db + '/' + response.id); + utils.sendJSON(res, 201, response); + }); + }); + + // Retrieve a document + app.get('/:db/:id(*)', function (req, res) { + var opts = utils.makeOpts(req, req.query); + + req.db.get(req.params.id, opts, function (err, doc) { + if (err) { + return utils.sendError(res, err); + } + + utils.sendJSON(res, 200, doc); + }); + }); + + // Delete a document + app.delete('/:db/:id(*)', function (req, res) { + var opts = utils.makeOpts(req, req.query); + opts.rev = getRev(req, {}); + + req.db.get(req.params.id, opts, function (err, doc) { + if (err) { + return utils.sendError(res, err); + } + req.db.remove(doc, opts, function (err, response) { + if (err) { + return utils.sendError(res, err); + } + utils.sendJSON(res, 200, response); + }); + }); + }); + + // Copy a document + app.copy('/:db/:id', function (req, res) { + var dest = req.get('Destination'); + var rev, match; + + if (!dest) { + return utils.sendJSON(res, 400, { + 'error': 'bad_request', + 'reason': 'Destination header is mandatory for COPY.' + }); + } + + if (isHTTP(dest) || isHTTPS(dest)) { + return utils.sendJSON(res, 400, { + 'error': 'bad_request', + 'reason': 'Destination URL must be relative.' + }); + } + + if ((match = /(.+?)\?rev=(.+)/.exec(dest))) { + dest = match[1]; + rev = match[2]; + } + + var opts = utils.makeOpts(req, req.query); + + req.db.get(req.params.id, opts, function (err, doc) { + if (err) { + return utils.sendError(res, err); + } + doc._id = dest; + doc._rev = rev; + req.db.put(doc, opts, function (err) { + if (err) { + return utils.sendError(res, err, 409); + } + utils.sendJSON(res, 201, {ok: true}); + }); + }); + }); +}; + +function isHTTP(url) { + return hasPrefix(url, 'http://'); +} + +function isHTTPS(url) { + return hasPrefix(url, 'https://'); +} + +function hasPrefix(haystack, needle) { + return haystack.substr(0, needle.length) === needle; +} diff --git a/packages/node_modules/express-pouchdb/lib/routes/fauxton.js b/packages/node_modules/express-pouchdb/lib/routes/fauxton.js new file mode 100644 index 00000000..cb263266 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/fauxton.js @@ -0,0 +1,22 @@ +"use strict"; + +var express = require('express'); +var path = require('path'); + +// "main" for pouchdb-fauxton is "www/index.html", so this returns "www" +var FAUXTON_PATH = path.dirname(require.resolve('pouchdb-fauxton')); + +module.exports = function (app) { + app.get('/_utils', function (req, res) { + // We want to force /_utils to /_utils/ as this is the CouchDB behavior + // https://git.io/vDMOD#L78 + // https://git.io/vDMOQ#L974 + if (req.originalUrl === '/_utils') { + res.redirect(301, '/_utils/'); + } else { + res.sendFile(path.normalize(FAUXTON_PATH + '/index.html')); + } + }); + + app.use('/_utils', express.static(FAUXTON_PATH)); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/find.js b/packages/node_modules/express-pouchdb/lib/routes/find.js new file mode 100644 index 00000000..776d30ad --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/find.js @@ -0,0 +1,35 @@ +"use strict"; + +var utils = require('../utils'); + +// TODO: /_security considerations? (What's CouchDB going to do there?) +// Should be possible to add that on top by just upgrading +// pouchdb-security though. + +module.exports = function (app) { + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(require('pouchdb-find')); + } + }); + + app.get('/:db/_index', function (req, res) { + req.db.getIndexes(utils.sendCallback(res)); + }); + + app.post('/:db/_index', utils.jsonParser, function (req, res) { + req.db.createIndex(req.body, utils.sendCallback(res, 400)); + }); + + app.delete('/:db/_index/:ddoc/:type/:name', function (req, res) { + req.db.deleteIndex({ + ddoc: req.params.ddoc, + type: req.params.type, + name: req.params.name + }, utils.sendCallback(res)); + }); + + app.post('/:db/_find', utils.jsonParser, function (req, res) { + req.db.find(req.body, utils.sendCallback(res, 400)); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/http-log.js b/packages/node_modules/express-pouchdb/lib/routes/http-log.js new file mode 100644 index 00000000..fff94390 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/http-log.js @@ -0,0 +1,53 @@ +"use strict"; + +var onFinished = require('on-finished'), + utils = require('../utils'), + normalizeHeader = require('header-case-normalizer'); + +module.exports = function (app) { + utils.requires(app, 'logging-infrastructure'); + + app.use(function (req, res, next) { + var method = req.method; + var rawPath = utils.rawPath(req); + var ip = req.ip; + + var str = "'{0}' {1} {1,1} from \"{2}\"\n" + .replace('{0}', method) + .replace('{1}', rawPath) + .replace('{2}', ip); + + str += 'Headers: ' + formatHeaders(req.headers); + app.couchLogger.debug(str); + + onFinished(res, function () { + var msg = '{0} - - {1} {2} {3}' + .replace('{0}', ip) + .replace('{1}', method) + .replace('{2}', rawPath) + .replace('{3}', res.statusCode); + app.couchLogger.info(msg); + }); + next(); + }); +}; + +function formatHeaders(headers) { + var result = "["; + var first = true; + var keys = Object.keys(headers); + keys.sort(); + keys.forEach(function (key) { + if (first) { + first = false; + } else { + result += ',\n '; + } + var value = headers[key]; + + result += "{'{0}', {1}}" + .replace('{0}', normalizeHeader(key)) + .replace('{1}', value); + }); + return result; +} diff --git a/packages/node_modules/express-pouchdb/lib/routes/list.js b/packages/node_modules/express-pouchdb/lib/routes/list.js new file mode 100644 index 00000000..46ed9725 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/list.js @@ -0,0 +1,31 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(require('pouchdb-list')); + } + }); + + // Query design document list handler + function handler(req, res) { + var queryParts = [req.params.id, req.params.func]; + if ('id2' in req.params) { + queryParts.push(req.params.id2); + } + queryParts.push(req.params.view); + var query = queryParts.join("/"); + var cb = utils.sendCouchDBResp.bind(null, res); + req.db.list(query, req.couchDBReq, cb); + } + app.all('/:db/_design/:id/_list/:func/:view', + utils.couchDBReqMiddleware, handler + ); + app.all('/:db/_design/:id/_list/:func/:id2/:view', + utils.couchDBReqMiddleware, handler + ); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/log.js b/packages/node_modules/express-pouchdb/lib/routes/log.js new file mode 100644 index 00000000..25e05f24 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/log.js @@ -0,0 +1,22 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'logging-infrastructure'); + + // Log + app.get('/_log', function (req, res) { + var bytes = req.query.bytes || 1000; + var offset = req.query.offset || 0; + + app.couchLogger.getLog(bytes, offset, function (err, log) { + if (err) { + return utils.sendError(res, err); + } + res.set('Content-Type', 'text/plain; charset=utf-8'); + res.set('Content-Length', log.length); + log.stream.pipe(res); + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/replicate.js b/packages/node_modules/express-pouchdb/lib/routes/replicate.js new file mode 100644 index 00000000..aa1a790f --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/replicate.js @@ -0,0 +1,71 @@ +"use strict"; + +var utils = require('../utils'), + extend = require('extend'); + +module.exports = function (app) { + var histories = {}; + + // Replicate a database + app.post('/_replicate', utils.jsonParser, function (req, res) { + + var source = req.body.source, + target = req.body.target, + opts = utils.makeOpts(req, {continuous: !!req.body.continuous}); + + if (req.body.filter) { + opts.filter = req.body.filter; + } + if (req.body.query_params) { + opts.query_params = req.body.query_params; + } + + var startDate = new Date(); + // TODO: actually use the _replicator database + req.PouchDB.replicate(source, target, opts).then(function (response) { + + var historyObj = extend(true, { + start_time: startDate.toJSON(), + end_time: new Date().toJSON() + }, response); + + var currentHistories = []; + + if (!/^https?:\/\//.test(source)) { + histories[source] = histories[source] || []; + currentHistories.push(histories[source]); + + } + if (!/^https?:\/\//.test(target)) { + histories[target] = histories[target] || []; + currentHistories.push(histories[target]); + } + + currentHistories.forEach(function (history) { + // CouchDB caps history at 50 according to + // http://guide.couchdb.org/draft/replication.html + history.push(historyObj); + if (history.length > 50) { + history.splice(0, 1); // TODO: this is slow, use a stack instead + } + }); + + response.history = histories[source] || histories[target] || []; + if (!opts.continuous) { + utils.sendJSON(res, 200, response); + } + }, function (err) { + console.log(err); + console.log(err.stack); + if (!opts.continuous) { + utils.sendError(res, err); + } + }); + + // if continuous pull replication return 'ok' since we cannot wait + // for callback + if (opts.continuous) { + utils.sendJSON(res, 200, {ok: true}); + } + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/revs-diff.js b/packages/node_modules/express-pouchdb/lib/routes/revs-diff.js new file mode 100644 index 00000000..8e7e7aef --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/revs-diff.js @@ -0,0 +1,13 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + // Revs Diff + app.post('/:db/_revs_diff', utils.jsonParser, function (req, res) { + var cb = utils.sendCallback(res); + req.db.revsDiff(req.body || {}, utils.makeOpts(req), cb); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/rewrite.js b/packages/node_modules/express-pouchdb/lib/routes/rewrite.js new file mode 100644 index 00000000..c39bbe03 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/rewrite.js @@ -0,0 +1,64 @@ +"use strict"; + +var utils = require('../utils'); +var REGEX = /\/([^\/]*)\/_design\/([^\/]*)\/_rewrite\/([^?]*)/; +var cleanFilename = require('../clean-filename'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + utils.requires(app, 'config-infrastructure'); + utils.requires(app, 'logging-infrastructure'); + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(require('pouchdb-rewrite')); + } + }); + app.couchConfig.registerDefault('httpd', 'rewrite_limit', 100); + + function rewriteMiddleware(req, res, next, rewritesSoFar) { + // Prefers regex over setting the first argument of app.use(), because + // the last makes req.url relative, which in turn makes most rewrites + // impossible. + + var match = REGEX.exec(req.url); + if (!match) { + return next(); + } + if (rewritesSoFar >= app.couchConfig.get('httpd', 'rewrite_limit')) { + return utils.sendJSON(res, 400, { + error: 'bad_request', + reason: "Exceeded rewrite recursion limit" + }); + } + + var dbName = cleanFilename(decodeURIComponent(match[1])); + utils.setDBOnReq(dbName, app.dbWrapper, req, res, function () { + var query = match[2] + "/" + match[3]; + var opts = utils.expressReqToCouchDBReq(req); + // We don't know opts.path yet - that's the point. + delete opts.path; + req.db.rewriteResultRequestObject(query, opts, function (err, resp) { + if (err) { + return utils.sendError(res, err); + } + + req.rawBody = resp.body; + req.cookies = resp.cookie; + req.headers = resp.headers; + req.method = resp.method; + req.url = "/" + resp.path.join("/"); + req.query = resp.query; + + app.couchLogger.debug('rewrite to "' + req.url + '"'); + // Handle the newly generated request. + rewriteMiddleware(req, res, next, rewritesSoFar + 1); + }); + }); + } + + // Query design document rewrite handler + app.use(function (req, res, next) { + rewriteMiddleware(req, res, next, 0); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/root.js b/packages/node_modules/express-pouchdb/lib/routes/root.js new file mode 100644 index 00000000..8bb04812 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/root.js @@ -0,0 +1,42 @@ +"use strict"; + +var pkg = require('../../package'), + utils = require('../utils'), + uuids = require('../uuids'); + +module.exports = function (app) { + if (app.couchConfig) { + app.couchConfig.registerDefault('vendor', 'name', 'PouchDB authors'); + app.couchConfig.registerDefault('vendor', 'version', pkg.version); + } + // Root route, return welcome message + app.get('/', function (req, res) { + var json = { + 'express-pouchdb': 'Welcome!', + 'version': pkg.version + }; + function sendResp() { + utils.sendJSON(res, 200, json); + } + if (app.couchConfig) { + json.vendor = app.couchConfig.getSection('vendor'); + getServerUUID(app.couchConfig, function (uuid) { + json.uuid = uuid; + sendResp(); + }); + } else { + sendResp(); + } + }); +}; + +function getServerUUID(config, cb) { + var uuid = config.get('couchdb', 'uuid'); + if (uuid) { + return cb(uuid); + } + uuid = uuids(1)[0]; + config.set('couchdb', 'uuid', uuid, function () { + cb(uuid); + }); +} diff --git a/packages/node_modules/express-pouchdb/lib/routes/security.js b/packages/node_modules/express-pouchdb/lib/routes/security.js new file mode 100644 index 00000000..6fe99846 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/security.js @@ -0,0 +1,40 @@ +"use strict"; + +var Security = require('pouchdb-security'); +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(Security); + Security.installStaticSecurityMethods(PouchDB); + } + }); + + app.dbWrapper.registerWrapper(function (name, db, next) { + db.installSecurityMethods(); + return next(); + }); + + // Routing + ['/:db/*', '/:db'].forEach(function (url) { + app.use(url, function (req, res, next) { + req.db.getSecurity().then(function (secObj) { + req.couchSecurityObj = secObj; + + next(); + }); + }); + }); + + app.get('/:db/_security', function (req, res) { + req.db.getSecurity(utils.makeOpts(req), utils.sendCallback(res)); + }); + + app.put('/:db/_security', utils.jsonParser, function (req, res) { + var cb = utils.sendCallback(res); + req.db.putSecurity(req.body || {}, utils.makeOpts(req), cb); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/session-stub.js b/packages/node_modules/express-pouchdb/lib/routes/session-stub.js new file mode 100644 index 00000000..c4e821a8 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/session-stub.js @@ -0,0 +1,23 @@ +"use strict"; + +var utils = require('../utils'); + +// The PouchDB test suite needs some valid response at /_session. The +// full session support is quite heavy (in terms of amount of code), so +// this is a fallback that's enabled in the minimumForPouchDB profile. +module.exports = function (app) { + if (app.includes.session) { + // no need for the stub if the real thing is going to be used. + return; + } + + app.get('/_session', function (req, res) { + utils.sendJSON(res, 200, { + ok: true, + userCtx: { + name: null, + roles: ['_admin'] + } + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/session.js b/packages/node_modules/express-pouchdb/lib/routes/session.js new file mode 100644 index 00000000..5fb72f3d --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/session.js @@ -0,0 +1,40 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/authentication'); + utils.requires(app, 'config-infrastructure'); + + function getUsersDB(req) { + return utils.getUsersDB(app, req.PouchDB); + } + + app.get('/_session', function (req, res) { + utils.sendJSON(res, 200, req.couchSession); + }); + + function postHandler(req, res) { + var name = req.body.name; + var password = req.body.password; + getUsersDB(req).then(function (db) { + return db.multiUserLogIn(name, password); + }).then(function (resp) { + res.cookie('AuthSession', resp.sessionID, {httpOnly: true}); + delete resp.sessionID; + if (req.query.next) { + utils.setLocation(res, req.query.next); + return res.status(302).end(); + } + utils.sendJSON(res, 200, resp); + }).catch(function (err) { + utils.sendError(res, err); + }); + } + app.post('/_session', utils.jsonParser, utils.urlencodedParser, postHandler); + + app.delete('/_session', function (req, res) { + res.clearCookie('AuthSession'); + utils.sendJSON(res, 200, {ok: true}); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/show.js b/packages/node_modules/express-pouchdb/lib/routes/show.js new file mode 100644 index 00000000..c66a694d --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/show.js @@ -0,0 +1,24 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(require('pouchdb-show')); + } + }); + + // Query design document show handler + function handler(req, res) { + var queryBase = [req.params.id, req.params.func].join("/"); + var query = queryBase + req.params[0]; + var cb = utils.sendCouchDBResp.bind(null, res); + req.db.show(query, req.couchDBReq, cb); + } + app.all('/:db/_design/:id/_show/:func*', + utils.couchDBReqMiddleware, handler + ); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/special-test-auth.js b/packages/node_modules/express-pouchdb/lib/routes/special-test-auth.js new file mode 100644 index 00000000..cf763778 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/special-test-auth.js @@ -0,0 +1,57 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + // inspired by special_test_authentication_handler/1 in: + // couchdb-couch/src/couch_httpd_auth.erl + utils.requires(app, 'config-infrastructure'); + + app.use(function (req, res, next) { + var value = app.couchConfig.get('httpd', 'authentication_handlers') || ""; + if (value.indexOf('special_test_authentication_handler') === -1) { + return next(); + } + + var header = req.get('WWW-Authenticate') || ""; + if (header.indexOf('X-Couch-Test-Auth ') !== 0) { + // No X-Couch-Test-Auth credentials sent, give admin access so the + // previous authentication can be restored after the test + return setSession(req, { + roles: ['_admin'] + }, next); + } + var namePass = header.slice('X-Couch-Test-Auth '.length); + var name = namePass.split(':')[0]; + var pass = namePass.split(':')[1]; + + var expectedPw = { + "Jan Lehnardt": "apple", + "Christopher Lenz": "dog food", + "Noah Slater": "biggiesmalls endian", + "Chris Anderson": "mp3", + "Damien Katz": "pecan pie" + }[name]; + if (expectedPw !== pass) { + return utils.sendJSON(res, 401, { + error: 'unauthorized', + reason: "Name or password is incorrect." + }); + } + setSession(req, { + name: name + }, next); + }); +}; + +function setSession(req, userCtx, next) { + if (!userCtx.roles) { + userCtx.roles = []; + } + req.couchSession = { + ok: true, + userCtx: userCtx, + authenticated: 'special', + }; + next(); +} diff --git a/packages/node_modules/express-pouchdb/lib/routes/stats.js b/packages/node_modules/express-pouchdb/lib/routes/stats.js new file mode 100644 index 00000000..4e36edc5 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/stats.js @@ -0,0 +1,13 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + // Stats (stub for now) + app.get('/_stats', function (req, res) { + // TODO: implement + utils.sendJSON(res, 200, { + 'pouchdb-server': 'has not impemented _stats yet. PRs welcome!' + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/temp-views.js b/packages/node_modules/express-pouchdb/lib/routes/temp-views.js new file mode 100644 index 00000000..e482b85d --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/temp-views.js @@ -0,0 +1,29 @@ +"use strict"; + +var utils = require('../utils'), + extend = require('extend'), + evalSafely = require('../eval-safely'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + // Temp Views + app.all('/:db/_temp_view', utils.jsonParser, function (req, res, next) { + if (req.method !== 'GET' && req.method !== 'POST' && + req.method !== 'HEAD') { + return next(); + } + // Check that the request body, if present, is an object. + if (req.body && (typeof req.body !== 'object' || Array.isArray(req.body))) { + return utils.sendJSON(res, 400, { + reason: "Request body must be a JSON object", + error: 'bad_request' + }); + } + if (req.body.map) { + req.body.map = evalSafely(req.body.map); + } + var opts = utils.makeOpts(req, extend({}, req.body, req.query)); + req.db.query(req.body, opts, utils.sendCallback(res)); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/update.js b/packages/node_modules/express-pouchdb/lib/routes/update.js new file mode 100644 index 00000000..b701d54b --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/update.js @@ -0,0 +1,24 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(require('pouchdb-update')); + } + }); + + // Query design document update handler + function handler(req, res) { + var baseQuery = [req.params.id, req.params.func].join("/"); + var query = baseQuery + req.params[0]; + var cb = utils.sendCouchDBResp.bind(null, res); + req.db.update(query, req.couchDBReq, cb); + } + app.all('/:db/_design/:id/_update/:func*', + utils.couchDBReqMiddleware, handler + ); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/uuids.js b/packages/node_modules/express-pouchdb/lib/routes/uuids.js new file mode 100644 index 00000000..573ccd85 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/uuids.js @@ -0,0 +1,28 @@ +"use strict"; + +var utils = require('../utils'), + uuids = require('../uuids'); + +module.exports = function (app) { + utils.requires(app, 'config-infrastructure'); + + app.couchConfig.registerDefault('uuids', 'max_count', 1000); + + // Generate UUIDs + app.all('/_uuids', utils.restrictMethods(["GET"]), function (req, res) { + res.set({ + "Cache-Control": "must-revalidate, no-cache", + "Pragma": "no-cache" + }); + var count = typeof req.query.count === 'number' ? req.query.count : 1; + if (count > app.couchConfig.get('uuids', 'max_count')) { + return utils.sendJSON(res, 403, { + error: "forbidden", + reason: "count parameter too large" + }); + } + utils.sendJSON(res, 200, { + uuids: uuids(count) + }); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/vhosts.js b/packages/node_modules/express-pouchdb/lib/routes/vhosts.js new file mode 100644 index 00000000..b4dfdf14 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/vhosts.js @@ -0,0 +1,27 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'config-infrastructure'); + utils.requires(app, 'logging-infrastructure'); + + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + require('pouchdb-vhost')(PouchDB); + } + }); + + // Query design document rewrite handler + app.use(function (req, res, next) { + var couchReq = utils.expressReqToCouchDBReq(req); + var vhosts = app.couchConfig.getSection('vhosts'); + var newUrl = req.PouchDB.resolveVirtualHost(couchReq, vhosts); + + if (newUrl !== req.url) { + req.url = newUrl; + app.couchLogger.debug("Vhost Target: '\"" + newUrl + "\"'"); + } + next(); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/view-cleanup.js b/packages/node_modules/express-pouchdb/lib/routes/view-cleanup.js new file mode 100644 index 00000000..08fa2f45 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/view-cleanup.js @@ -0,0 +1,12 @@ +"use strict"; + +var utils = require('../utils'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + // View Cleanup + app.post('/:db/_view_cleanup', utils.jsonParser, function (req, res) { + req.db.viewCleanup(utils.makeOpts(req), utils.sendCallback(res)); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/routes/views.js b/packages/node_modules/express-pouchdb/lib/routes/views.js new file mode 100644 index 00000000..08bdaf6e --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/routes/views.js @@ -0,0 +1,28 @@ +"use strict"; + +var utils = require('../utils'), + extend = require('extend'); + +module.exports = function (app) { + utils.requires(app, 'routes/db'); + + app.all('/:db/_design/:id/_view/:view', utils.jsonParser, + function (req, res, next) { + if (req.method !== 'GET' && req.method !== 'POST' && + req.method !== 'HEAD') { + return next(); + } + + // Check that the request body, if present, is an object. + if (req.body && (typeof req.body !== 'object' || Array.isArray(req.body))) { + return utils.sendJSON(res, 400, { + reason: "Request body must be a JSON object", + error: 'bad_request' + }); + } + + var query = req.params.id + '/' + req.params.view; + var opts = utils.makeOpts(req, extend({}, req.body, req.query)); + req.db.query(query, opts, utils.sendCallback(res)); + }); +}; diff --git a/packages/node_modules/express-pouchdb/lib/utils.js b/packages/node_modules/express-pouchdb/lib/utils.js new file mode 100644 index 00000000..e46b3265 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/utils.js @@ -0,0 +1,308 @@ +"use strict"; + +var pathResolve = require('path').resolve; +var rawBody = require('raw-body'); +var Promise = require('pouchdb-promise'); +var mkdirp = require('mkdirp'); +var cleanFilename = require('./clean-filename'); +var getOrCreateDB = require('./create-or-delete-dbs').getOrCreateDB; + +//shared middleware + +exports.maxDocumentSizeDefault = 4 * 1024 * 1024 * 1024; + +function buildJSONParser(tolerateInvalid) { + return function (req, res, next) { + var limit; + if (req.app.couchConfig) { + limit = req.app.couchConfig.get('couchdb', 'max_document_size'); + } else { + limit = exports.maxDocumentSizeDefault; + } + var jsonParser = require('body-parser').json({limit: limit}); + jsonParser(req, res, function (err) { + if (err) { + if (err.status === 400) { + if (!tolerateInvalid) { + return exports.sendJSON(res, 400, { + error: "bad_request", + reason: "invalid_json" + }); + } + } else if (err.status === 413) { + return exports.sendJSON(res, 413, { + error: "too_large", + reason: "the request entity is too large" + }); + } else { + return exports.sendError(res, err, 500); + } + } + next(); + }); + }; +} + +exports.jsonParser = buildJSONParser(false); +exports.urlencodedParser = require('body-parser').urlencoded({extended: false}); + +exports.makeOpts = function (req, startOpts) { + // fill in opts so it can be used by authorisation logic + var opts = startOpts || {}; + opts.userCtx = (req.couchSession || {}).userCtx; + opts.secObj = req.couchSecurityObj; + + if (opts.userCtx) { + // add db name to userCtx (#218) + var dbname = req.db && req.db._db_name; + if (dbname) { + opts.userCtx.db = decodeURIComponent(dbname); + } + } + + return opts; +}; + +exports.setDBOnReq = function (dbName, dbWrapper, req, res, next) { + dbName = cleanFilename(dbName); + req.PouchDB.allDbs(function (err, dbs) { + if (err) { + return exports.sendError(res, err); + } + + if (dbs.indexOf(dbName) === -1) { + return exports.sendJSON(res, 404, { + error: 'not_found', + reason: 'no_db_file' + }); + } + getOrCreateDB(req.PouchDB, dbName).then(function (db) { + // temporary workaround for https://github.com/pouchdb/pouchdb/issues/5668 + // see also https://github.com/pouchdb/express-pouchdb/issues/274 + if (/\//.test(dbName)) { + var path = db.__opts.prefix ? db.__opts.prefix + dbName : dbName; + mkdirp.sync(pathResolve(path)); + } + + dbWrapper.wrap(dbName, db).then(function () { + req.db = db; + next(); + }); + }); + }); +}; + +exports.rawPath = function (req) { + var rawPath = req.originalUrl.slice(req.baseUrl.length); + if (rawPath[0] !== '/') { + rawPath = '/' + rawPath; + } + return rawPath; +}; + +exports.expressReqToCouchDBReq = function (req) { + var rawPath = exports.rawPath(req); + return exports.makeOpts(req, { + body: (req.rawBody ? req.rawBody.toString() : "") || "undefined", + cookie: req.cookies || {}, + form: req.couchDBForm || {}, + json: req.couchDBJSON || {}, + headers: req.headers, + method: req.method, + path: splitPath(req.url.split("?")[0]), + peer: req.ip, + query: req.query, + requested_path: splitPath(rawPath), + raw_path: rawPath, + }); +}; + +var sloppyJSONParser = buildJSONParser(true); + +exports.couchDBReqMiddleware = function (req, res, next) { + var i = 0; + function cb() { + i++; + if (i === 2) { + req.couchDBReq = exports.expressReqToCouchDBReq(req); + next(); + } + } + exports.parseRawBody(req, res, cb); + + sloppyJSONParser(req, res, function () { + req.couchDBJSON = req.body; + delete req.body; + exports.urlencodedParser(req, res, function () { + req.couchDBForm = req.body; + delete req.body; + + cb(); + }); + }, true); +}; + +function splitPath(path) { + return path.split("/").filter(function (part) { + return part; + }); +} + +exports.sendCouchDBResp = function (res, err, couchResp) { + if (err) { + return exports.sendError(res, err); + } + + for (var header in couchResp.headers) { + if (couchResp.headers.hasOwnProperty(header)) { + // use setHeader instead of res.set to prevent modification of + // headers by express. + res.setHeader(header, couchResp.headers[header]); + } + } + var body; + if (couchResp.base64) { + body = new Buffer(couchResp.base64, 'base64'); + } else { + //convert to buffer so express doesn't add the ; charset=utf-8 if it + //isn't already there by now. No performance problem: express does + //this internally anyway. + body = new Buffer(couchResp.body, 'utf-8'); + } + res.status(couchResp.code).send(body); +}; + +exports.sendError = function (res, err, baseStatus) { + var status = err.status || baseStatus || 500; + + // last argument is optional + if (err.name && err.message) { + if (err.name === 'Error' || err.name === 'TypeError') { + if (err.message.indexOf("Bad special document member") !== -1) { + err.name = 'doc_validation'; + // add more clauses here if the error name is too general + } else { + err.name = 'bad_request'; + } + } + err = { + error: err.name, + reason: err.message + }; + } + exports.sendJSON(res, status, err); +}; + +function setJsonOrPlaintext(res) { + // Send the client application/json if they asked for it, + // else send text/plain; charset=utf-8. This mimics CouchDB. + var type = res.req.accepts(['text', 'json']); + if (type === "json") { + res.setHeader('Content-Type', 'application/json'); + } else { + //adds ; charset=utf-8 + res.type('text/plain'); + } +} + +function jsonToBuffer(body) { + //convert to buffer so express doesn't add the ; charset=utf-8 if it + //isn't already there by now. No performance problem: express does + //this internally anyway. + return new Buffer(JSON.stringify(body) + "\n", 'utf8'); +} + +exports.setJsonOrPlaintext = setJsonOrPlaintext; + +exports.writeJSON = function (res, body) { + res.write(jsonToBuffer(body)); +}; + +exports.sendJSON = function (res, status, body) { + res.status(status); + setJsonOrPlaintext(res); + res.send(jsonToBuffer(body)); +}; + +exports.sendCallback = function (res, errCode, successCode) { + return function (err, response) { + if (err) { + return exports.sendError(res, err, errCode); + } + exports.sendJSON(res, successCode || 200, response); + }; +}; + +exports.setLocation = function (res, path) { + //CouchDB location headers are always non-relative. + var loc = ( + res.req.protocol + + '://' + + ((res.req.hostname === '127.0.0.1') ? + '' : res.req.subdomains.join('.') + '.') + + res.req.hostname + + ':' + res.req.socket.localPort + + '/' + path + ); + res.location(loc); +}; + +exports.restrictMethods = function (methods) { + return function (req, res, next) { + if (methods.indexOf(req.method) === -1) { + res.set("Allow", methods.join(", ")); + return exports.sendJSON(res, 405, { + error: 'method_not_allowed', + reason: "Only " + methods.join(",") + " allowed" + }); + } + next(); + }; +}; + +exports.parseRawBody = function (req, res, next) { + // Custom bodyParsing because bodyParser chokes + // on 'malformed' requests, and also because we need the + // rawBody for attachments + rawBody(req, { + length: req.headers['content-length'] + }, function (err, string) { + if (err) { + return next(err); + } + req.rawBody = string; + next(); + }); +}; + +exports.getUsersDBName = function (app) { + return app.couchConfig.get('couch_httpd_auth', 'authentication_db'); +}; + +exports.getUsersDB = function (app, PouchDB) { + var name = exports.getUsersDBName(app); + return app.dbWrapper.wrap(name, new PouchDB(name)); +}; + +exports.requires = function (app, part) { + if (!app.includes[part]) { + var msg = part + ( + " is required, but won't be active. Please adjust your " + + "opts.profile/opts.profileDiff accordingly." + ); + throw new Error(msg); + } +}; + +exports.callAsyncRecursive = function (funcs, handleCall) { + var i = 0; + function next() { + var func = funcs[i]; + if (typeof func === 'undefined') { + return Promise.resolve(); + } + i++; + return handleCall(func, next); + } + return next(); +}; diff --git a/packages/node_modules/express-pouchdb/lib/uuids.js b/packages/node_modules/express-pouchdb/lib/uuids.js new file mode 100644 index 00000000..457b3460 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/uuids.js @@ -0,0 +1,12 @@ +"use strict"; + +var uuid = require('uuid/v4'); + +module.exports = function generate(limit) { + var output = []; + var i = -1; + while (++i < limit) { + output.push(uuid()); + } + return output; +}; diff --git a/packages/node_modules/express-pouchdb/lib/validation.js b/packages/node_modules/express-pouchdb/lib/validation.js new file mode 100644 index 00000000..4fd621e9 --- /dev/null +++ b/packages/node_modules/express-pouchdb/lib/validation.js @@ -0,0 +1,15 @@ +"use strict"; + +module.exports = function enableValidation(app) { + app.daemonManager.registerDaemon({ + start: function (PouchDB) { + PouchDB.plugin(require('pouchdb-validation')); + } + }); + + app.dbWrapper.registerWrapper(function (name, db, next) { + db.installValidationMethods(); + + next(); + }); +}; diff --git a/packages/node_modules/express-pouchdb/package.json b/packages/node_modules/express-pouchdb/package.json new file mode 100644 index 00000000..df1d8bf6 --- /dev/null +++ b/packages/node_modules/express-pouchdb/package.json @@ -0,0 +1,14 @@ +{ + "name": "express-pouchdb", + "description": "Express submodule with a CouchDB-style REST interface to PouchDB.", + "homepage": "https://github.com/pouchdb/pouchdb-server", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git://github.com/pouchdb/pouchdb-server.git" + }, + "main": "./lib/index.js", + "files": [ + "lib" + ] +} diff --git a/packages/node_modules/pouchdb-server/LICENSE b/packages/node_modules/pouchdb-server/LICENSE new file mode 100644 index 00000000..f6cd2bc8 --- /dev/null +++ b/packages/node_modules/pouchdb-server/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/packages/node_modules/pouchdb-server/README.md b/packages/node_modules/pouchdb-server/README.md new file mode 100644 index 00000000..7445b7ee --- /dev/null +++ b/packages/node_modules/pouchdb-server/README.md @@ -0,0 +1,15 @@ +# express-pouchdb + +[![Build Status](https://travis-ci.org/pouchdb/pouchdb-server.svg)](https://travis-ci.org/pouchdb/pouchdb-server) + +A drop-in replacement for CouchDB, built on Node.js and PouchDB. + +### Source + +PouchDB Server and its sub-packages are distributed as a [monorepo](https://github.com/babel/babel/blob/master/doc/design/monorepo.md). + +For a full list of packages, see [the GitHub source](https://github.com/pouchdb/pouchdb-server/tree/master/packages/node_modules). + +## License + +The Apache 2 License. See [the LICENSE file](https://github.com/pouchdb/pouchdb-server/blob/master/LICENSE) for more information. \ No newline at end of file diff --git a/packages/node_modules/pouchdb-server/bin/pouchdb-server b/packages/node_modules/pouchdb-server/bin/pouchdb-server new file mode 100755 index 00000000..cec1dd28 --- /dev/null +++ b/packages/node_modules/pouchdb-server/bin/pouchdb-server @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +// The only reason this file exists is for backwards compatibility, because +// otherwise we break folks doing: +// `./node_modules/pouchdb-server/bin/pouchdb-server`. + +require('../lib/index.js'); \ No newline at end of file diff --git a/packages/node_modules/pouchdb-server/favicon.ico b/packages/node_modules/pouchdb-server/favicon.ico new file mode 100644 index 00000000..99c00b83 Binary files /dev/null and b/packages/node_modules/pouchdb-server/favicon.ico differ diff --git a/lib/cors.js b/packages/node_modules/pouchdb-server/lib/cors.js similarity index 100% rename from lib/cors.js rename to packages/node_modules/pouchdb-server/lib/cors.js diff --git a/lib/customLevelAdapter.js b/packages/node_modules/pouchdb-server/lib/customLevelAdapter.js similarity index 100% rename from lib/customLevelAdapter.js rename to packages/node_modules/pouchdb-server/lib/customLevelAdapter.js diff --git a/lib/index.js b/packages/node_modules/pouchdb-server/lib/index.js similarity index 100% rename from lib/index.js rename to packages/node_modules/pouchdb-server/lib/index.js diff --git a/lib/logging.js b/packages/node_modules/pouchdb-server/lib/logging.js similarity index 100% rename from lib/logging.js rename to packages/node_modules/pouchdb-server/lib/logging.js diff --git a/packages/node_modules/pouchdb-server/package.json b/packages/node_modules/pouchdb-server/package.json new file mode 100644 index 00000000..76309eef --- /dev/null +++ b/packages/node_modules/pouchdb-server/package.json @@ -0,0 +1,19 @@ +{ + "name": "pouchdb-server", + "description": "A drop-in replacement for CouchDB, built on Node.js and PouchDB", + "homepage": "https://github.com/pouchdb/pouchdb-server", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git://github.com/pouchdb/pouchdb-server.git" + }, + "main": "lib/index.js", + "files": [ + "lib", + "bin/pouchdb-server", + "favicon.ico" + ], + "bin": { + "pouchdb-server": "./bin/pouchdb-server" + } +} diff --git a/tests/express-pouchdb/test.js b/tests/express-pouchdb/test.js new file mode 100644 index 00000000..0d4594a4 --- /dev/null +++ b/tests/express-pouchdb/test.js @@ -0,0 +1,262 @@ +"use strict"; + +/*globals before */ + +var buildApp = require('../../packages/node_modules/express-pouchdb'), + PouchDB = require('pouchdb'), + express = require('express'), + request = require('supertest'), + Promise = require('bluebird'), + fse = Promise.promisifyAll(require('fs-extra')), + memdown = require('memdown'); + +var TEST_DATA = __dirname + '/testdata/'; +var LARGE_TIMEOUT = 5000; + +var expressApp, expressApp2; + +var customApp = buildApp(PouchDB.defaults({ + db: memdown, + prefix: 'c' +}), { + mode: 'custom', + overrideMode: { + include: ['routes/404'] + } +}); + +var coreApp = buildApp(PouchDB.defaults({ + db: memdown, + prefix: 'd' +}), { + mode: 'minimumForPouchDB', + overrideMode: { + include: ['routes/fauxton'] + } +}); + +var inMemoryConfigApp = buildApp(PouchDB.defaults({ + db: memdown, + prefix: 'e' +}), { + inMemoryConfig: true +}); + +before(function (done) { + this.timeout(LARGE_TIMEOUT); + cleanUp().then(function () { + return fse.mkdirsAsync(TEST_DATA + 'a'); + }).then(function () { + return fse.mkdirsAsync(TEST_DATA + 'b'); + }).then(function () { + expressApp = buildApp(PouchDB.defaults({ + prefix: TEST_DATA + 'a/' + })); + expressApp2 = buildApp(PouchDB.defaults({ + prefix: TEST_DATA + 'b/', + }), { + configPath: TEST_DATA + 'b-config.json', + logPath: TEST_DATA + 'b-log.txt' + }); + done(); + }).catch(done); +}); + +after(function (done) { + cleanUp().then(function () { + done(); + }).catch(done); +}); + +function cleanUp() { + return Promise.all([ + fse.removeAsync(TEST_DATA), + fse.removeAsync('./config.json'), + fse.removeAsync('./log.txt') + ]); +} + +describe('config', function () { + it('should not create empty config file', function (done) { + fse.exists('./config.json', function (exists) { + if (exists) { + return done(new Error("config.json should not have been created!")); + } + done(); + }); + }); + it('should support in memory config', function (done) { + // make sure the file is written to disk. + inMemoryConfigApp.couchConfig.set('demo', 'demo', true, function () { + fse.exists('./config.json', function (exists) { + if (exists) { + return done(new Error("config.json exists!")); + } + done(); + }); + }); + }); + it('should have ./config.json as default config path', function (done) { + expressApp.couchConfig.set('demo', 'demo', true, function () { + fse.exists('./config.json', function (exists) { + if (!exists) { + return done(new Error("config.json doesn't exist!")); + } + done(); + }); + }); + }); + it('should support setting a different config path', function (done) { + // make sure the file is written to disk. + expressApp2.couchConfig.set('demo', 'demo', true, function () { + fse.exists(TEST_DATA + 'b-config.json', function (exists) { + if (!exists) { + return done(new Error("b-config.json doesn't exist!")); + } + done(); + }); + }); + }); + it('should support setting a different log path', function (done) { + // make sure the file is written to disk. + expressApp2.couchConfig.set('demo', 'demo', true, function () { + fse.exists(TEST_DATA + 'b-log.txt', function (exists) { + if (!exists) { + return done(new Error("b-log.txt doesn't exist!")); + } + done(); + }); + }); + }); + it('should support externally adding a default', function (done) { + expressApp.couchConfig.registerDefault('a', 'b', 'c'); + request(expressApp) + .get('/_config') + .expect(200) + .expect(function (res) { + var a = JSON.parse(res.text).a; + if (!(typeof a === "object" && a.b === "c")) { + return "Default not shown"; + } + }) + .end(done); + }); + it('should support externally getting a config value', function (done) { + request(expressApp) + .put('/_config/test/a') + .send('"b"') + .expect(200) + .end(function () { + if (expressApp.couchConfig.get('test', 'a') !== 'b') { + return done(new Error("Can't read setting that's just been set")); + } + done(); + }); + }); + it('should support external listeners to a config change', function (done) { + var changed = false; + expressApp.couchConfig.on('test2.a', function () { + changed = true; + }); + request(expressApp) + .put('/_config/test2/a') + .send('"b"') + .expect(200) + .expect(function () { + if (!changed) { + return "Didn't get notice of the setting change"; + } + }) + .end(done); + }); +}); + +var prefixes = ['/', '/db/']; + +prefixes.forEach(function (prefix) { + describe('basics for ' + prefix, function () { + it('GET / should respond with a welcome page', function (done) { + var app = express(); + app.use(prefix, expressApp); + + testWelcome(app, done, prefix); + }); + }); +}); + +function testWelcome(app, done, path) { + request(app) + .get(path) + .expect(200) + .expect(function (res) { + if (!/Welcome!/.test(res.text)) { + return "No 'Welcome!' in response"; + } + }) + .end(done); +} + +describe('modes', function () { + it('should always return a 404 in our custom configuration', function (done) { + request(customApp) + .get('/') + .expect(404) + .expect(function (res) { + if (JSON.parse(res.text).error !== 'not_found') { + return "Wrong response body"; + } + }) + .end(done); + }); + it('should generate a functioning core app', function (done) { + testWelcome(coreApp, done, '/'); + }); + it('should throw an error when given an invalid mode', function () { + assertException(function () { + buildApp(PouchDB, {mode: 'unexisting-mode'}); + }, /Unknown mode: unexisting-mode/); + }); + it('should throw an error when a not included part is excluded', function () { + assertException(function () { + buildApp(PouchDB, {overrideMode: {exclude: ['abc']}}); + }, /exclude contains the not included part 'abc'/); + }); + it('should throw an error when an unknown part is included', function () { + assertException(function () { + buildApp(PouchDB, {overrideMode: {include: ['abc']}}); + }, /include contains the unknown part 'abc'/); + }); +}); + +describe('redirects', function () { + it('GET /_utils should redirect to /_utils/', function (done) { + request(coreApp) + .get('/_utils') + .expect(301) + .end(done); + }); + it('GET /_utils/ should return fauxton', function (done) { + request(coreApp) + .get('/_utils/') + .expect(200) + .expect(function (res) { + if (!/PouchDB Server<\/title>/.test(res.text)) { + return "No '<title>PouchDB Server' in response"; + } + }) + .end(done); + }); +}); + +function assertException(func, re) { + var e; + try { + func(); + } catch (err) { + if (re.test(err.toString())) { + return; + } + e = err; + } + throw (e || new Error('no error was thrown')); +} diff --git a/tests/mocha.opts b/tests/mocha.opts new file mode 100644 index 00000000..d52764dd --- /dev/null +++ b/tests/mocha.opts @@ -0,0 +1,2 @@ +--colors +-R spec