├── lib
├── version.js
├── deps
│ ├── buffer.js
│ ├── blob.js
│ ├── uuid.js
│ ├── errors.js
│ ├── extend.js
│ ├── ajax.js
│ └── md5.js
├── index.js
├── constructor.js
├── setup.js
├── plugins
│ └── pouchdb.spatial.js
├── replicate.js
├── merge.js
└── utils.js
├── tests
├── pouch.shim.js
├── chromeappbackground.js
├── postTest.js
├── test.html
├── test.setup.js
├── worker.js
├── test.http.js
├── test.worker.js
├── test.issue915.js
├── perf.attachments.js
├── test.taskqueue.js
├── test.conflicts.js
├── test.uuids.js
├── webrunner.js
├── test.issue221.js
├── test.revs_diff.js
├── test.design_docs.js
├── test.slash_id.js
├── qunit
│ ├── qunit.css
│ └── junitlogger.js
├── test.auth_replication.js
├── test.bulk_docs.js
├── test.compaction.js
└── test.cors.js
├── docs
├── _config.yml
├── static
│ ├── screenshots
│ │ └── todo-1.png
│ ├── style
│ │ ├── GitHub-Mark-64px.png
│ │ ├── couchdb-icon-64px.png
│ │ ├── noun_project_5618.png
│ │ ├── twitter-bird-light-bgs.png
│ │ ├── solarized_dark.min.css
│ │ ├── pygments.css
│ │ ├── noun_project_5618.svg
│ │ └── pouchdb.css
│ └── assets
│ │ └── pouchdb-getting-started-todo.zip
├── _includes
│ └── navigation.md
├── errors.md
├── external.md
├── _layouts
│ ├── learn.html
│ └── default.html
├── index.md
├── faq.md
├── learn.md
└── getting-started.md
├── .gitignore
├── .travis.yml
├── scripts
├── bundle-browserify-test.sh
├── jenkins-deploy.sh
├── baldrick-test.sh
└── start_standalone_couch.sh
├── bin
├── publish-site.sh
├── dev-server.js
├── publish.sh
├── test-node.js
└── test-browser.js
├── component.json
├── manifest.json
├── test
├── README.md
├── test.utils.js
└── integration
│ ├── replication_test.js
│ └── basics_test.js
├── bower.json
├── .jshintrc
├── README.md
├── package.json
└── CONTRIBUTING.md
/lib/version.js:
--------------------------------------------------------------------------------
1 | module.exports = 'nightly';
--------------------------------------------------------------------------------
/tests/pouch.shim.js:
--------------------------------------------------------------------------------
1 | global.PouchDB = {};
2 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | pygments: true
2 | markdown: redcarpet
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | tmp
3 | .DS_Store
4 | node_modules
5 | docs/_site
6 | npm-debug.log
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "0.10"
5 |
6 | services:
7 | - couchdb
--------------------------------------------------------------------------------
/lib/deps/buffer.js:
--------------------------------------------------------------------------------
1 | //this soley exists so we can exclude it in browserify
2 | module.exports = Buffer;
--------------------------------------------------------------------------------
/docs/static/screenshots/todo-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/pouchdb/master/docs/static/screenshots/todo-1.png
--------------------------------------------------------------------------------
/docs/static/style/GitHub-Mark-64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/pouchdb/master/docs/static/style/GitHub-Mark-64px.png
--------------------------------------------------------------------------------
/docs/static/style/couchdb-icon-64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/pouchdb/master/docs/static/style/couchdb-icon-64px.png
--------------------------------------------------------------------------------
/docs/static/style/noun_project_5618.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/pouchdb/master/docs/static/style/noun_project_5618.png
--------------------------------------------------------------------------------
/docs/static/style/twitter-bird-light-bgs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/pouchdb/master/docs/static/style/twitter-bird-light-bgs.png
--------------------------------------------------------------------------------
/docs/static/assets/pouchdb-getting-started-todo.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/pouchdb/master/docs/static/assets/pouchdb-getting-started-todo.zip
--------------------------------------------------------------------------------
/docs/_includes/navigation.md:
--------------------------------------------------------------------------------
1 | * What is PouchDB
2 | * Getting Started
3 | * [API Reference](/api.html)
4 | * Browser Details
5 | * External Projects
6 | * GQL
7 | * PouchDB Server
8 | * Puton
9 |
--------------------------------------------------------------------------------
/tests/chromeappbackground.js:
--------------------------------------------------------------------------------
1 | /*globals chrome */
2 |
3 | 'use strict';
4 |
5 | chrome.app.runtime.onLaunched.addListener(function(){
6 | chrome.app.window.create("./test.html", {
7 | "width": 1000,
8 | "height": 800
9 | });
10 | });
--------------------------------------------------------------------------------
/scripts/bundle-browserify-test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '' > test.html
4 | echo "
" >> test.html
5 | echo "" >> test.html
--------------------------------------------------------------------------------
/scripts/jenkins-deploy.sh:
--------------------------------------------------------------------------------
1 | ROOT=$(pwd)
2 |
3 | # Build the docs
4 | cd $ROOT/docs
5 | jekyll
6 |
7 | # Publish docs
8 | cp -R $ROOT/docs/_site/* /home/daleharvey/www/pouchdb.com
9 |
10 | # Build
11 | cd $ROOT
12 | npm install
13 |
14 | grunt
15 | grunt spatial
16 | grunt gql
17 |
18 | cp $ROOT/dist/* /home/daleharvey/www/download.pouchdb.com
--------------------------------------------------------------------------------
/bin/publish-site.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # publish-site requires jekyll
3 | # install with gem install jekyll
4 |
5 | npm run build
6 |
7 | # Build pouchdb.com
8 | cd docs
9 | jekyll build
10 | cd ..
11 |
12 | # Publish pouchdb.com + nightly
13 | scp -r docs/_site/* pouchdb.com:www/pouchdb.com
14 | scp dist/* pouchdb.com:www/download.pouchdb.com
15 |
--------------------------------------------------------------------------------
/docs/errors.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: learn
3 | title: PouchDB, the JavaScript Database that Syncs!
4 | ---
5 |
6 | # Common Errors
7 |
8 | ### PouchDB is throwing `InvalidStateError`
9 |
10 | Are you in private browsing mode? IndexedDB is [disabled in private browsing mode](https://developer.mozilla.org/en-US/docs/IndexedDB/Using_IndexedDB) of Firefox.
11 |
--------------------------------------------------------------------------------
/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pouchdb",
3 | "version": "0.0.0",
4 | "description": "PouchDB is a pocket-sized database.",
5 | "repo": "daleharvey/pouchdb",
6 | "keywords": [
7 | "db",
8 | "couchdb",
9 | "pouchdb"
10 | ],
11 | "dependencies": {},
12 | "development": {},
13 | "license": "Apache",
14 | "main": "dist/pouchdb-nightly.js",
15 | "scripts": [
16 | "dist/pouchdb-nightly.js"
17 | ]
18 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pouchdb Tests",
3 | "description": "Pouchdb chrome app tests",
4 | "version": "0.1",
5 | "manifest_version": 1,
6 | "app": {
7 | "background": {
8 | "scripts": ["./tests/chromeappbackground.js"]
9 | }
10 | },
11 | "content_security_policy": "default-src 'none' ; script-src 'self' 'http://localhost' 'unsafe-eval'; object-src 'self' 'http://localhost'",
12 | "permissions": [
13 | "storage"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | This is an experimental change to our test suite to more easily
2 | run tests in both browser + node, to run the current test suite
3 |
4 | $ node test/unit/merge_rev_tree_test.js
5 |
6 | for node, and
7 |
8 | $ browserify test/unit/merge_rev_tree_test.js | testling
9 |
10 | for the browser.
11 |
12 | Currently this requires manual shutdown of the browser in OSX, you must
13 | also:
14 |
15 | $ npm install tape
16 | $ npm install -g testling
17 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pouchdb",
3 | "version": "0.0.0",
4 | "description": "PouchDB is a pocket-sized database.",
5 | "homepage": "https://github.com/daleharvey/pouchdb",
6 | "author": "Dale Harvey",
7 | "main": "dist/pouchdb-nightly.js",
8 | "keywords": [
9 | "pouch",
10 | "couch",
11 | "db"
12 | ],
13 | "license": "Apache",
14 | "ignore": [
15 | "**/.*",
16 | "node_modules",
17 | "bower_components",
18 | "test",
19 | "tests"
20 | ]
21 | }
--------------------------------------------------------------------------------
/tests/postTest.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module("misc", {
4 | setup : function () {
5 | var dbname = location.search.match(/[?&]dbname=([^&]+)/);
6 | this.name = dbname && decodeURIComponent(dbname[1]);
7 | }
8 | });
9 |
10 | asyncTest("Add a doc", 2, function() {
11 | testUtils.openTestDB(this.name, function(err, db) {
12 | ok(!err, 'opened the pouch');
13 | db.post({test:"somestuff"}, function (err, info) {
14 | ok(!err, 'saved a doc with post');
15 | start();
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/tests/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | QUnit Test Suite
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/test.setup.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | if (typeof module !== undefined && module.exports) {
4 | var testUtils = require('./test.utils.js');
5 | var PouchDB = require('../lib');
6 | }
7 |
8 | QUnit.module("Test DB Setup");
9 |
10 | asyncTest("Test we can find CouchDB with admin credentials", 2, function() {
11 | PouchDB.ajax({
12 | url: testUtils.couchHost() + '/_session'
13 | }, function(err, res) {
14 | if (err) {
15 | ok(false, 'There was an error accessing your CouchDB instance');
16 | return start();
17 | }
18 | ok(res.ok, 'Found CouchDB');
19 | ok(res.userCtx.roles.indexOf('_admin') !== -1, 'Found admin permissions');
20 | start();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/test/test.utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var PouchDB = require('../');
4 |
5 | module.exports.setupDb = function() {
6 |
7 | var dbs = Array.prototype.slice.call(arguments);
8 |
9 | var deleteDatabases = function(t) {
10 | t.plan(dbs.length)
11 |
12 | var done = function(err) {
13 | t.ok(!(err && err.status !== 404), 'Deleting database');
14 | };
15 |
16 | dbs.forEach(function(db) {
17 | PouchDB.destroy(db, done);
18 | });
19 | };
20 |
21 | // We delete databases once the tests have completed, but we also
22 | // need to do it during setup in case of test crashes
23 | return {
24 | setup: deleteDatabases,
25 | teardown: deleteDatabases
26 | };
27 |
28 | };
29 |
30 |
--------------------------------------------------------------------------------
/bin/dev-server.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | var cors_proxy = require("corsproxy");
6 | var http_proxy = require("http-proxy");
7 | var http_server = require("http-server");
8 |
9 | var program = require('commander');
10 |
11 | var COUCH_HOST = process.env.COUCH_HOST || 'http://127.0.0.1:5984';
12 |
13 | var HTTP_PORT = 8000;
14 | var CORS_PORT = 2020;
15 |
16 | function startServers(couchHost) {
17 | http_server.createServer().listen(HTTP_PORT);
18 | cors_proxy.options = {target: couchHost || COUCH_HOST};
19 | http_proxy.createServer(cors_proxy).listen(CORS_PORT);
20 | }
21 |
22 |
23 | if (require.main === module) {
24 | startServers();
25 | } else {
26 | module.exports.start = startServers;
27 | }
28 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "eqeqeq": true,
4 | "immed": true,
5 | "newcap": true,
6 | "noarg": true,
7 | "sub": true,
8 | "undef": true,
9 | "eqnull": true,
10 | "browser": true,
11 | "node": true,
12 | "strict": true,
13 | "globalstrict": true,
14 | "globals": { "Pouch": true},
15 | "white": true,
16 | "indent": 2,
17 | "predef": [
18 | "QUnit",
19 | "asyncTest",
20 | "test",
21 | "DB",
22 | "deepEqual",
23 | "equal",
24 | "expect",
25 | "fail",
26 | "module",
27 | "nextTest",
28 | "notEqual",
29 | "ok",
30 | "sample",
31 | "start",
32 | "stop",
33 | "unescape",
34 | "process",
35 | "global",
36 | "require",
37 | "console"
38 | ]
39 | }
--------------------------------------------------------------------------------
/tests/worker.js:
--------------------------------------------------------------------------------
1 | importScripts('../dist/pouchdb-nightly.js');
2 | function bigTest(name){
3 | PouchDB(name,function(err,db){
4 | if(err){
5 | throw err;
6 | }
7 | db.post({_id:"blablah",key:'lala'},function(err){
8 | if(err){
9 | throw err;
10 | }
11 | db.get('blablah',function(err,doc){
12 | if(err){
13 | throw err;
14 | }
15 | self.postMessage(doc.key);
16 | PouchDB.destroy(name);
17 | });
18 | });
19 | });
20 | }
21 | self.addEventListener('message',function(e){
22 | if(e.data==='ping'){
23 | self.postMessage('pong');
24 | }
25 | if(e.data==='version'){
26 | self.postMessage(PouchDB.version);
27 | }
28 | if(Array.isArray(e.data)&&e.data[0]==='create'){
29 | bigTest(e.data[1]);
30 | }
31 | });
--------------------------------------------------------------------------------
/lib/deps/blob.js:
--------------------------------------------------------------------------------
1 | //Abstracts constructing a Blob object, so it also works in older
2 | //browsers that don't support the native Blob constructor. (i.e.
3 | //old QtWebKit versions, at least).
4 | function createBlob(parts, properties) {
5 | parts = parts || [];
6 | properties = properties || {};
7 | try {
8 | return new Blob(parts, properties);
9 | } catch (e) {
10 | if (e.name !== "TypeError") {
11 | throw(e);
12 | }
13 | var BlobBuilder = window.BlobBuilder || window.MSBlobBuilder || window.MozBlobBuilder || window.WebKitBlobBuilder;
14 | var builder = new BlobBuilder();
15 | for (var i = 0; i < parts.length; i += 1) {
16 | builder.append(parts[i]);
17 | }
18 | return builder.getBlob(properties.type);
19 | }
20 | };
21 |
22 |
23 | module.exports = createBlob;
24 |
25 |
--------------------------------------------------------------------------------
/scripts/baldrick-test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -x
2 |
3 | # Run PouchDB test suite, expect a global couchdb command to be installed
4 | #
5 | # Run as:
6 | #
7 | # ./scripts/baldrick-test.sh
8 |
9 | # tmp directory to store CouchDB data files
10 | TMP=./tmp
11 | COUCH_URI_FILE=$TMP/couch.uri
12 |
13 | # Install PouchDB dependancies
14 | npm install
15 |
16 | # Provision a CouchDB instance just for this test
17 | ./scripts/start_standalone_couch.sh $TMP > /dev/null 2>&1 &
18 | COUCH_PID=$!
19 |
20 | # Wait for CouchDB to start by polling for the uri file
21 | # Not nasty at all :)
22 | while [ ! -f $COUCH_URI_FILE ]
23 | do
24 | sleep 2
25 | done
26 | COUCH_HOST=$(cat $COUCH_URI_FILE)
27 |
28 | # Run tests
29 | grunt test --couch-host=$COUCH_HOST
30 | EXIT_STATUS=$?
31 |
32 | # Cleanup
33 | kill $COUCH_PID
34 |
35 | # Make sure we exit with the right status
36 | exit $EXIT_STATUS
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var PouchDB = require('./setup');
4 |
5 | module.exports = PouchDB;
6 |
7 | PouchDB.ajax = require('./deps/ajax');
8 | PouchDB.extend = require('./deps/extend');
9 | PouchDB.utils = require('./utils');
10 | PouchDB.Errors = require('./deps/errors');
11 | PouchDB.replicate = require('./replicate').replicate;
12 | PouchDB.version = require('./version');
13 | var httpAdapter = require('./adapters/http');
14 | PouchDB.adapter('http', httpAdapter);
15 | PouchDB.adapter('https', httpAdapter);
16 |
17 | PouchDB.adapter('idb', require('./adapters/idb'));
18 | PouchDB.adapter('websql', require('./adapters/websql'));
19 | PouchDB.plugin('mapreduce', require('pouchdb-mapreduce'));
20 |
21 | if (!process.browser) {
22 | var ldbAdapter = require('./adapters/leveldb');
23 | PouchDB.adapter('ldb', ldbAdapter);
24 | PouchDB.adapter('leveldb', ldbAdapter);
25 | }
26 |
--------------------------------------------------------------------------------
/tests/test.http.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var adapter = 'http-1';
4 |
5 | if (typeof module !== undefined && module.exports) {
6 | var PouchDB = require('../lib');
7 | var testUtils = require('./test.utils.js');
8 | }
9 |
10 | QUnit.module("http-adapter", {
11 | setup: function() {
12 | this.name = testUtils.generateAdapterUrl(adapter);
13 | },
14 | teardown: function() {
15 | if (!testUtils.PERSIST_DATABASES) {
16 | PouchDB.destroy(this.name);
17 | }
18 | }
19 | });
20 |
21 |
22 |
23 | asyncTest("Create a pouch without DB setup", function() {
24 | var instantDB;
25 | var name = this.name;
26 | PouchDB.destroy(name, function() {
27 | instantDB = new PouchDB(name, {skipSetup: true});
28 | instantDB.post({test:"abc"}, function(err, info) {
29 | ok(err && err.name === 'not_found', 'Skipped setup of database');
30 | start();
31 | });
32 | });
33 | });
34 |
35 |
36 |
--------------------------------------------------------------------------------
/test/integration/replication_test.js:
--------------------------------------------------------------------------------
1 | /*globals require */
2 |
3 | 'use strict';
4 |
5 | var PouchDB = require('../../');
6 | var utils = require('../test.utils.js');
7 | var opts = require('browserify-getopts');
8 |
9 | var db1 = opts.db1 || 'testdb1';
10 | var db2 = opts.db2 || 'testdb2';
11 | var db3 = opts.db2 || 'testdb3';
12 |
13 | var test = require('wrapping-tape')(utils.setupDb(db1, db2, db3));
14 |
15 | test('Replicate without creating src', function(t) {
16 | t.plan(2);
17 | var db = new PouchDB(db1);
18 | var docs = [{a: 'doc'}, {anew: 'doc'}];
19 | db.bulkDocs({docs: docs}, function() {
20 | PouchDB.replicate(db1, db2, {complete: function(err, changes) {
21 | t.equal(changes.docs_written, docs.length, 'Docs written');
22 | PouchDB.replicate(db1, db3, {complete: function(err, changes) {
23 | t.equal(changes.docs_written, docs.length, 'Docs written');
24 | }});
25 | }});
26 | });
27 | });
--------------------------------------------------------------------------------
/bin/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | VERSION=$1
4 |
5 | if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9]+)?)?$ ]]; then
6 | echo "Usage: ./bin/publish.sh 0.0.1(-version(.2))"
7 | exit 2
8 | fi
9 |
10 | # Build
11 | git checkout -b build
12 | ./node_modules/tin/bin/tin -v $VERSION
13 | echo "module.exports = '"$VERSION"';" > lib/version.js
14 | npm run build
15 | git add dist -f
16 | git add lib/version.js package.json bower.json component.json
17 | git commit -m "build $VERSION"
18 |
19 | # Tag and push
20 | git tag $VERSION
21 | git push --tags git@github.com:daleharvey/pouchdb.git $VERSION
22 |
23 | # Publish JS modules
24 | npm publish
25 |
26 | # Build pouchdb.com
27 | cd docs
28 | jekyll build
29 | cd ..
30 |
31 | # Publish pouchdb.com + nightly
32 | scp -r docs/_site/* pouchdb.com:www/pouchdb.com
33 | scp dist/* pouchdb.com:www/download.pouchdb.com
34 |
35 | # Cleanup
36 | git checkout master
37 | git branch -D build
38 |
--------------------------------------------------------------------------------
/tests/test.worker.js:
--------------------------------------------------------------------------------
1 | QUnit.module('worker');
2 |
3 | asyncTest('create it',1,function(){
4 | var worker = new Worker('worker.js');
5 | worker.addEventListener('message',function(e){
6 | ok('pong',e.data);
7 | worker.terminate();
8 | start();
9 | });
10 | worker.postMessage('ping');
11 | });
12 | asyncTest('check pouch version',1,function(){
13 | var worker = new Worker('worker.js');
14 | worker.addEventListener('message',function(e){
15 | ok(PouchDB.version,e.data);
16 | worker.terminate();
17 | start();
18 | });
19 | worker.postMessage('version');
20 | });
21 | asyncTest('create db',1,function(){
22 | var worker = new Worker('worker.js');
23 | worker.addEventListener('error',function(e){
24 | throw e;
25 | });
26 | worker.addEventListener('message',function(e){
27 | ok('lala',e.data);
28 | worker.terminate();
29 | start();
30 | });
31 | worker.postMessage(['create',testUtils.generateAdapterUrl(adapter)]);
32 | });
--------------------------------------------------------------------------------
/tests/test.issue915.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | if (typeof module !== undefined && module.exports) {
4 | var PouchDB = require('../lib');
5 | var utils = require('./test.utils.js');
6 | var fs = require('fs');
7 | }
8 |
9 | QUnit.module("Remove DB", {
10 | setup: function() {
11 | //Create a dir
12 | fs.mkdirSync('veryimportantfiles');
13 | },
14 | teardown: function() {
15 | PouchDB.destroy('name');
16 | fs.rmdirSync('veryimportantfiles');
17 | }
18 | });
19 |
20 |
21 |
22 | asyncTest("Create a pouch without DB setup", function() {
23 | var instantDB;
24 | instantDB = new PouchDB('name', {skipSetup: true}, function() {
25 | PouchDB.destroy('veryimportantfiles', function( error, response ) {
26 | equal(error.message, 'Database not found', 'should return Database not found error');
27 | equal(fs.existsSync('veryimportantfiles'), true, 'veryimportantfiles was not removed');
28 | start();
29 | });
30 | });
31 | });
32 |
33 |
34 |
--------------------------------------------------------------------------------
/docs/static/style/solarized_dark.min.css:
--------------------------------------------------------------------------------
1 | pre code{display:block;padding:.5em;background:#002b36;color:#839496}pre .comment,pre .template_comment,pre .diff .header,pre .doctype,pre .pi,pre .lisp .string,pre .javadoc{color:#586e75;font-style:italic}pre .keyword,pre .winutils,pre .method,pre .addition,pre .css .tag,pre .request,pre .status,pre .nginx .title{color:#859900}pre .number,pre .command,pre .string,pre .tag .value,pre .phpdoc,pre .tex .formula,pre .regexp,pre .hexcolor{color:#2aa198}pre .title,pre .localvars,pre .chunk,pre .decorator,pre .built_in,pre .identifier,pre .vhdl .literal,pre .id{color:#268bd2}pre .attribute,pre .variable,pre .lisp .body,pre .smalltalk .number,pre .constant,pre .class .title,pre .parent,pre .haskell .type{color:#b58900}pre .preprocessor,pre .preprocessor .keyword,pre .shebang,pre .symbol,pre .symbol .string,pre .diff .change,pre .special,pre .attr_selector,pre .important,pre .subst,pre .cdata,pre .clojure .title{color:#cb4b16}pre .deletion{color:#dc322f}pre .tex .formula{background:#073642}
--------------------------------------------------------------------------------
/scripts/start_standalone_couch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | # Start a standalone CouchDB, this is a wrapper around the $ couchdb
4 | # command that will create a standalone instance of CouchDB allowing
5 | # you to easily run serveral servers in parallel, each instance starts
6 | # on an ephemeral port and has a dedicated directory for its data and logs
7 | # use the couch.uri file to locate the host
8 | #
9 | # Run as:
10 | #
11 | # $ ./start_standalone_couch.sh ~/data/instanceId
12 |
13 | COUCH_DIR=$1
14 |
15 | # Make all the directories
16 | mkdir -p $COUCH_DIR/data/views
17 |
18 | # Create a standalone configuration based on the directory
19 | # we are passed in, CouchDB will start on a random port and couch.uri
20 | # will tell us where that is, data is stored within the directory
21 | echo "[httpd]
22 | bind_address = 127.0.0.1
23 | port = 0
24 |
25 | [log]
26 | level = debug
27 | file = $COUCH_DIR/couch.log
28 |
29 | [couchdb]
30 | database_dir = $COUCH_DIR/data
31 | view_index_dir = $COUCH_DIR/data/views
32 | uri_file = $COUCH_DIR/couch.uri" > $COUCH_DIR/couch.ini
33 |
34 | couchdb -a $COUCH_DIR/couch.ini
--------------------------------------------------------------------------------
/bin/test-node.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // specify dependency
4 | var testrunner = require("qunit");
5 | var fs = require('fs');
6 |
7 | var excludedTests = [
8 | // auth_replication and cors need admin access (#1030)
9 | 'test.auth_replication.js',
10 | 'test.cors.js',
11 | //no workers in node
12 | 'test.worker.js',
13 | // Plugins currnetly arent tested (#1031)
14 | 'test.gql.js',
15 | 'test.spatial.js'
16 | ];
17 |
18 | var testFiles;
19 |
20 | if (process.env.TEST_FILE) {
21 | testFiles = [process.env.TEST_FILE];
22 | } else {
23 | testFiles = fs.readdirSync("./tests").filter(function(name){
24 | return (/^test\.([a-z0-9_])*\.js$/).test(name) &&
25 | (excludedTests.indexOf(name) === -1);
26 | });
27 | }
28 |
29 | testrunner.setup({
30 | log: {
31 | errors: true,
32 | summary: true
33 | }
34 | });
35 |
36 | testrunner.run({
37 | deps: [
38 | './lib/deps/extend.js',
39 | './lib/deps/blob.js',
40 | './lib/deps/ajax.js',
41 | './tests/pouch.shim.js'
42 | ],
43 | code: "./lib/adapters/leveldb.js",
44 | tests: testFiles.map(function(n) {
45 | return "./tests/" + n;
46 | })
47 | }, function(err, result) {
48 | if (err) {
49 | console.error(err);
50 | process.exit(1);
51 | return;
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/docs/external.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: learn
3 | title: PouchDB, the JavaScript Database that Syncs!
4 | ---
5 |
6 | # External Projects
7 |
8 | A set of projects that either provide plugins or related tools for PouchDB.
9 |
10 | ### PouchDB Server
11 |
12 | A standalone CouchDB style REST interface server to PouchDB.
13 |
14 | [Github](https://github.com/nick-thompson/pouchdb-server)
15 |
16 | ### Express PouchDB
17 |
18 | An express submodule with a CouchDB style REST interface to PouchDB.
19 |
20 | [Github](https://github.com/nick-thompson/express-pouchdb)
21 |
22 | ### PouchDB Spatial
23 |
24 | Multidimensional and Spatial Queries with PouchDB.
25 |
26 | [Source](https://github.com/daleharvey/pouchdb/blob/master/src/plugins/pouchdb.spatial.js)
27 | [Build](http://download.pouchdb.com/pouchdb.spatial-nightly.js)
28 |
29 | ### GQL
30 |
31 | Google Query Language(GQL) queries with PouchDB.
32 |
33 | [Documentation](http://pouchdb.com/gql.html)
34 | [Build](http://download.pouchdb.com/pouchdb.gql-nightly.js)
35 |
36 | ### Backbone PouchDB
37 |
38 | Backbone PouchDB Sync Adapter.
39 |
40 | [Github](https://github.com/jo/backbone-pouch)
41 |
42 | ### Puton
43 |
44 | A bookmarklet for inspecting PouchDB databases within the browser.
45 |
46 | [Website](http://puton.jit.su/)
47 | [Github](http://github.com/ymichael/puton)
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [PouchDB](http://pouchdb.com/) - The Javascript Database that Syncs
2 | ==================================================
3 |
4 | PouchDB was written to help web developers build applications that work as well offline as well as they do online, applications save data locally so the user can use all the features of an app even while offline and synchronise the data between clients so they have up to date data wherever they go.
5 |
6 | PouchDB is a free open source project, written in Javascript by these [wonderful contributors](https://github.com/daleharvey/pouchdb/graphs/contributors) and inspired by Apache CouchDB .
7 |
8 | Using PouchDB
9 | -------------
10 |
11 | To get started using PouchDB check out our [Documentation](http://pouchdb.com/learn.html) and the [API Documentation](http://pouchdb.com/api.html).
12 |
13 |
14 | Contributors
15 | ------------
16 | If you want to get involved then check out the [contributing guide](https://github.com/daleharvey/pouchdb/blob/master/CONTRIBUTING.md)
17 |
18 | Example
19 | -------
20 |
21 | ```
22 | var db = new PouchDB('dbname');
23 |
24 | db.put({
25 | _id: 'dave@gmail.com',
26 | name: 'David',
27 | age: 66
28 | });
29 |
30 | db.changes({
31 | onChange: function() {
32 | console.log('Ch-Ch-Changes');
33 | }
34 | });
35 |
36 | db.replicate.to('http://example.com/mydb');
37 | ```
38 |
--------------------------------------------------------------------------------
/tests/perf.attachments.js:
--------------------------------------------------------------------------------
1 | ["idb-1"].map(function(adapter) {
2 |
3 | module('attachment performance: ' + adapter, {
4 | setup : function () {
5 | this.name = generateAdapterUrl(adapter)
6 | }
7 | });
8 |
9 | asyncTest("Test large attachments", function() {
10 |
11 | var fiveMB="";
12 | for (i=0;i<5000000;i++){ //not sure if this is exactly 5MB but it's big enough
13 | fiveMB+="a";
14 | }
15 |
16 | var fiveMB2="";
17 | for (i=0;i<5000000;i++){
18 | fiveMB2+="b";
19 | }
20 |
21 | initTestDB(this.name, function(err, db) {
22 |
23 | var queryStartTime1=new Date().getTime();
24 | function map(doc){
25 | {emit(null, doc);}
26 | }
27 |
28 | db.query({map: map},{reduce: false}, function(err, response) {
29 | var duration1 = new Date().getTime() - queryStartTime1;
30 | console.log("query 1 took " + duration1 + " ms");
31 | db.put({ _id: 'mydoc' }, function(err, resp) {
32 | db.putAttachment('mydoc/mytext', resp.rev, fiveMB, 'text/plain', function(err, res) {
33 | db.put({ _id: 'mydoc2' }, function(err, resp) {
34 | db.putAttachment('mydoc/mytext2', resp.rev, fiveMB2, 'text/plain', function(err, res) {
35 | var queryStartTime2 = new Date().getTime();
36 | function map(doc) { emit(null, doc); }
37 | db.query({map: map},{reduce: false}, function(err, response) {
38 | var duration2 = new Date().getTime()-queryStartTime2;
39 | console.log("query 2 took "+duration2+" ms");
40 | ok(duration2<=duration1*10, 'Query finished within order of magnitude');
41 | start();
42 | });
43 | });
44 | });
45 | });
46 | });
47 | });
48 | });
49 | });
50 |
51 | });
52 |
--------------------------------------------------------------------------------
/docs/_layouts/learn.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
14 |
15 |
API
16 |
35 |
36 |
37 |
38 |
39 | {{ content }}
40 |
41 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: PouchDB, the JavaScript Database that Syncs!
4 | ---
5 |
6 | The Database that Syncs!
7 |
8 |
9 |
10 |
11 |
12 | PouchDB is an Open Source JavaScript Database inspired by Apache CouchDB that is designed to run well within the browser.
13 |
14 | PouchDB was created to help web developers build applications that work equally as well offline as they do online. It enables applications to store data locally while offline, and synchronise it with CouchDB and compatible servers when the application is back online, keeping the user's data in sync no matter where they next login.
15 |
16 |
24 |
25 |
26 |
27 |
28 |
29 | {% highlight js linenos=table %}
30 | var db = new PouchDB('dbname');
31 |
32 | db.put({
33 | _id: 'dave@gmail.com',
34 | name: 'David',
35 | age: 66
36 | });
37 |
38 | db.changes({
39 | onChange: function() {
40 | console.log('Ch-Ch-Changes');
41 | }
42 | });
43 |
44 | db.replicate.to('http://example.com/mydb');
45 | {% endhighlight %}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Cross Browser
54 | Works in Firefox, Chrome, Opera, Safari, IE and Node.js
55 |
56 |
57 |
58 | Lightweight
59 | PouchDB is just a script tag and 25KB(gzipped) away in the browser, or $ npm install pouchdb away
60 | in node.
61 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/lib/deps/uuid.js:
--------------------------------------------------------------------------------
1 | // BEGIN Math.uuid.js
2 |
3 | /*!
4 | Math.uuid.js (v1.4)
5 | http://www.broofa.com
6 | mailto:robert@broofa.com
7 |
8 | Copyright (c) 2010 Robert Kieffer
9 | Dual licensed under the MIT and GPL licenses.
10 | */
11 |
12 | /*
13 | * Generate a random uuid.
14 | *
15 | * USAGE: Math.uuid(length, radix)
16 | * length - the desired number of characters
17 | * radix - the number of allowable values for each character.
18 | *
19 | * EXAMPLES:
20 | * // No arguments - returns RFC4122, version 4 ID
21 | * >>> Math.uuid()
22 | * "92329D39-6F5C-4520-ABFC-AAB64544E172"
23 | *
24 | * // One argument - returns ID of the specified length
25 | * >>> Math.uuid(15) // 15 character ID (default base=62)
26 | * "VcydxgltxrVZSTV"
27 | *
28 | * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62)
29 | * >>> Math.uuid(8, 2) // 8 character ID (base=2)
30 | * "01001010"
31 | * >>> Math.uuid(8, 10) // 8 character ID (base=10)
32 | * "47473046"
33 | * >>> Math.uuid(8, 16) // 8 character ID (base=16)
34 | * "098F4D35"
35 | */
36 |
37 |
38 | function uuid(len, radix) {
39 | var chars = uuid.CHARS
40 | var uuidInner = [];
41 | var i;
42 |
43 | radix = radix || chars.length;
44 |
45 | if (len) {
46 | // Compact form
47 | for (i = 0; i < len; i++) uuidInner[i] = chars[0 | Math.random()*radix];
48 | } else {
49 | // rfc4122, version 4 form
50 | var r;
51 |
52 | // rfc4122 requires these characters
53 | uuidInner[8] = uuidInner[13] = uuidInner[18] = uuidInner[23] = '-';
54 | uuidInner[14] = '4';
55 |
56 | // Fill in random data. At i==19 set the high bits of clock sequence as
57 | // per rfc4122, sec. 4.1.5
58 | for (i = 0; i < 36; i++) {
59 | if (!uuidInner[i]) {
60 | r = 0 | Math.random()*16;
61 | uuidInner[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
62 | }
63 | }
64 | }
65 |
66 | return uuidInner.join('');
67 | };
68 | uuid.CHARS = (
69 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
70 | 'abcdefghijklmnopqrstuvwxyz'
71 | ).split('');
72 |
73 | module.exports = uuid;
74 |
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pouchdb",
3 | "version": "0.0.0",
4 | "description": "PouchDB is a pocket-sized database.",
5 | "release": "nightly",
6 | "author": "Dale Harvey",
7 | "main": "./lib/index.js",
8 | "homepage": "https://github.com/daleharvey/pouchdb",
9 | "repository": "https://github.com/daleharvey/pouchdb",
10 | "keywords": [
11 | "db",
12 | "couchdb",
13 | "pouchdb"
14 | ],
15 | "tags": [
16 | "db",
17 | "couchdb",
18 | "pouchdb"
19 | ],
20 | "dependencies": {
21 | "level": "~0.18.0",
22 | "request": "~2.28.0",
23 | "pouchdb-mapreduce": "0.2.0"
24 | },
25 | "devDependencies": {
26 | "commander": "~2.1.0",
27 | "webdriverjs": "~0.7.14",
28 | "selenium-standalone": "~2.38.0",
29 | "watchify": "~0.4.1",
30 | "uglify-js": "~2.4.6",
31 | "jshint": "~2.3.0",
32 | "http-proxy": "~0.10.3",
33 | "corsproxy": "~0.2.13",
34 | "http-server": "~0.5.5",
35 | "qunit": "~0.5.17",
36 | "browserify": "~2.36.1",
37 | "tin": "~0.3.1"
38 | },
39 | "maintainers": [
40 | {
41 | "name": "Dale Harvey",
42 | "web": "https://github.com/daleharvey"
43 | },
44 | {
45 | "name": "Ryan Ramage",
46 | "web": "https://github.com/ryanramage"
47 | }
48 | ],
49 | "scripts": {
50 | "jshint": "jshint -c .jshintrc lib/*.js lib/adapters/*.js",
51 | "build-js": "mkdir -p dist && browserify lib/index.js -s PouchDB -o dist/pouchdb-nightly.js",
52 | "watch-js": "mkdir -p dist && watchify lib/index.js -s PouchDB -o dist/pouchdb-nightly.js",
53 | "uglify": "uglifyjs dist/pouchdb-nightly.js -mc > dist/pouchdb-nightly.min.js",
54 | "build": "npm run build-js && npm run uglify",
55 | "test-node": "./bin/test-node.js",
56 | "test-browser": "npm run build-js && ./bin/test-browser.js",
57 | "dev-server": "npm run watch-js & ./bin/dev-server.js",
58 | "test": "npm run jshint && npm run test-node && npm run test-browser"
59 | },
60 | "browser": {
61 | "./adapters/leveldb": false,
62 | "./deps/buffer": false,
63 | "request": false,
64 | "level": false,
65 | "path": false,
66 | "fs": false,
67 | "events": false,
68 | "crypto": false
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/constructor.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var Adapter = require('./adapter')(PouchDB);
4 | function PouchDB(name, opts, callback) {
5 |
6 | if (!(this instanceof PouchDB)) {
7 | return new PouchDB(name, opts, callback);
8 | }
9 |
10 | if (typeof opts === 'function' || typeof opts === 'undefined') {
11 | callback = opts;
12 | opts = {};
13 | }
14 |
15 | if (typeof name === 'object') {
16 | opts = name;
17 | name = undefined;
18 | }
19 |
20 | if (typeof callback === 'undefined') {
21 | callback = function () {};
22 | }
23 |
24 | var originalName = opts.name || name;
25 | var backend = PouchDB.parseAdapter(originalName);
26 |
27 | opts.originalName = originalName;
28 | opts.name = backend.name;
29 | opts.adapter = opts.adapter || backend.adapter;
30 |
31 | if (!PouchDB.adapters[opts.adapter]) {
32 | throw 'Adapter is missing';
33 | }
34 |
35 | if (!PouchDB.adapters[opts.adapter].valid()) {
36 | throw 'Invalid Adapter';
37 | }
38 |
39 | var adapter = new Adapter(opts, function (err, db) {
40 | if (err) {
41 | if (callback) {
42 | callback(err);
43 | }
44 | return;
45 | }
46 |
47 | for (var plugin in PouchDB.plugins) {
48 | // In future these will likely need to be async to allow the plugin
49 | // to initialise
50 | var pluginObj = PouchDB.plugins[plugin](db);
51 | for (var api in pluginObj) {
52 | // We let things like the http adapter use its own implementation
53 | // as it shares a lot of code
54 | if (!(api in db)) {
55 | db[api] = pluginObj[api];
56 | }
57 | }
58 | }
59 | db.taskqueue.ready(true);
60 | db.taskqueue.execute(db);
61 | callback(null, db);
62 | });
63 | for (var j in adapter) {
64 | this[j] = adapter[j];
65 | }
66 | for (var plugin in PouchDB.plugins) {
67 | // In future these will likely need to be async to allow the plugin
68 | // to initialise
69 | var pluginObj = PouchDB.plugins[plugin](this);
70 | for (var api in pluginObj) {
71 | // We let things like the http adapter use its own implementation
72 | // as it shares a lot of code
73 | if (!(api in this)) {
74 | this[api] = pluginObj[api];
75 | }
76 | }
77 | }
78 | }
79 |
80 | module.exports = PouchDB;
--------------------------------------------------------------------------------
/tests/test.taskqueue.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var adapters = ['http-1', 'local-1'];
4 |
5 | if (typeof module !== undefined && module.exports) {
6 | var PouchDB = require('../lib');
7 | var testUtils = require('./test.utils.js');
8 | }
9 |
10 | adapters.map(function(adapter) {
11 |
12 | QUnit.module("taskqueue: " + adapter, {
13 | setup: function() {
14 | this.name = testUtils.generateAdapterUrl(adapter);
15 | PouchDB.enableAllDbs = true;
16 | },
17 | teardown: testUtils.cleanupTestDatabases
18 | });
19 |
20 | asyncTest("Add a doc", 1, function() {
21 | var name = this.name;
22 | PouchDB.destroy(name, function() {
23 | var db = testUtils.openTestAsyncDB(name);
24 | db.post({test:"somestuff"}, function (err, info) {
25 | ok(!err, 'saved a doc with post');
26 | start();
27 | });
28 | });
29 | });
30 |
31 | asyncTest("Query", 1, function() {
32 | var name = this.name;
33 | PouchDB.destroy(name, function() {
34 | var db = testUtils.openTestAsyncDB(name);
35 | var queryFun = {
36 | map: function(doc) { }
37 | };
38 | db.query(queryFun, { reduce: false }, function (_, res) {
39 | equal(res.rows.length, 0);
40 | start();
41 | });
42 | });
43 | });
44 |
45 | asyncTest("Bulk docs", 2, function() {
46 | var name = this.name;
47 | PouchDB.destroy(name, function() {
48 | var db = testUtils.openTestAsyncDB(name);
49 |
50 | db.bulkDocs({docs: [{test:"somestuff"}, {test:"another"}]}, function(err, infos) {
51 | ok(!infos[0].error);
52 | ok(!infos[1].error);
53 | start();
54 | });
55 | });
56 | });
57 |
58 | asyncTest("Get", 1, function() {
59 | var name = this.name;
60 | PouchDB.destroy(name, function() {
61 | var db = testUtils.openTestAsyncDB(name);
62 |
63 | db.get('0', function(err, res) {
64 | ok(err);
65 | start();
66 | });
67 | });
68 | });
69 |
70 | asyncTest("Info", 2, function() {
71 | var name = this.name;
72 | PouchDB.destroy(name, function() {
73 | var db = testUtils.openTestAsyncDB(name);
74 |
75 | db.info(function(err, info) {
76 | ok(info.doc_count === 0);
77 | ok(info.update_seq === 0);
78 | start();
79 | });
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/tests/test.conflicts.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var adapters = ['http-1', 'local-1'];
4 |
5 | if (typeof module !== undefined && module.exports) {
6 | var PouchDB = require('../lib');
7 | var testUtils = require('./test.utils.js');
8 | }
9 |
10 | adapters.map(function(adapter) {
11 |
12 | QUnit.module('conflicts: ' + adapter, {
13 | setup: function () {
14 | this.name = testUtils.generateAdapterUrl(adapter);
15 | PouchDB.enableAllDbs = true;
16 | },
17 | teardown: testUtils.cleanupTestDatabases
18 | });
19 |
20 | asyncTest('Testing conflicts', function() {
21 | testUtils.initTestDB(this.name, function(err, db) {
22 | var doc = {_id: 'foo', a:1, b: 1};
23 | db.put(doc, function(err, res) {
24 | doc._rev = res.rev;
25 | ok(res.ok, 'Put first document');
26 | db.get('foo', function(err, doc2) {
27 | ok(doc._id === doc2._id && doc._rev && doc2._rev, 'Docs had correct id + rev');
28 | doc.a = 2;
29 | doc2.a = 3;
30 | db.put(doc, function(err, res) {
31 | ok(res.ok, 'Put second doc');
32 | db.put(doc2, function(err) {
33 | ok(err.name === 'conflict', 'Put got a conflicts');
34 | db.changes({
35 | complete: function(err, results) {
36 | ok(results.results.length === 1, 'We have one entry in changes');
37 | doc2._rev = undefined;
38 | db.put(doc2, function(err) {
39 | ok(err.name === 'conflict', 'Another conflict');
40 | start();
41 | });
42 | }
43 | });
44 | });
45 | });
46 | });
47 | });
48 | });
49 | });
50 |
51 | asyncTest('Testing conflicts', function() {
52 | var doc = {_id: 'fubar', a:1, b: 1};
53 | testUtils.initTestDB(this.name, function(err, db) {
54 | db.put(doc, function(err, ndoc) {
55 | doc._rev = ndoc.rev;
56 | db.remove(doc, function() {
57 | delete doc._rev;
58 | db.put(doc, function(err, ndoc) {
59 | if (err) {
60 | ok(false);
61 | start();
62 | return;
63 | }
64 | ok(ndoc.ok, 'written previously deleted doc without rev');
65 | start();
66 | });
67 | });
68 | });
69 | });
70 | });
71 |
72 | });
73 |
--------------------------------------------------------------------------------
/bin/test-browser.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require('path');
4 | var spawn = require('child_process').spawn;
5 |
6 | var webdriverjs = require('webdriverjs');
7 | var devserver = require('./dev-server.js');
8 |
9 | var SELENIUM_PATH = '../node_modules/.bin/start-selenium';
10 | var testUrl = 'http://127.0.0.1:8000/tests/test.html';
11 | var testTimeout = 2 * 60 * 1000;
12 | var testSelector = 'body.testsComplete';
13 |
14 | var currentTest = '';
15 | var results = {};
16 | var client = {};
17 |
18 | var browsers = [
19 | 'firefox',
20 | // Temporarily disable safari until it is fixed (#1068)
21 | // 'safari',
22 | 'chrome'
23 | ];
24 |
25 | // Travis only has firefox
26 | if (process.env.TRAVIS) {
27 | browsers = ['firefox'];
28 | }
29 |
30 | function startServers(callback) {
31 |
32 | // Starts the file and CORS proxy
33 | devserver.start();
34 |
35 | // Start selenium
36 | var selenium = spawn(path.resolve(__dirname, SELENIUM_PATH));
37 |
38 | selenium.stdout.on('data', function(data) {
39 | if (/Started org.openqa.jetty.jetty/.test(data)) {
40 | callback();
41 | }
42 | });
43 | }
44 |
45 | function testsComplete() {
46 | var passed = Object.keys(results).every(function(x) {
47 | return results[x].passed;
48 | });
49 |
50 | if (passed) {
51 | console.log('Woot, tests passed');
52 | process.exit(0);
53 | } else {
54 | console.error('Doh, tests failed');
55 | process.exit(1);
56 | }
57 | }
58 |
59 | function resultCollected(err, result) {
60 | console.log('[' + currentTest + '] ' +
61 | (result.value.passed ? 'passed' : 'failed'));
62 | results[currentTest] = result.value;
63 | client.end(startTest);
64 | }
65 |
66 | function testComplete(err, result) {
67 | if (err) {
68 | console.log('[' + currentTest + '] failed');
69 | results[currentTest] = {passed: false};
70 | return client.end(startTest);
71 | }
72 | client.execute('return window.testReport;', [], resultCollected);
73 | }
74 |
75 | function startTest() {
76 | if (!browsers.length) {
77 | return testsComplete();
78 | }
79 |
80 | currentTest = browsers.pop();
81 | console.log('[' + currentTest + '] starting');
82 |
83 | client = webdriverjs.remote({
84 | logLevel: 'silent',
85 | desiredCapabilities: {
86 | browserName: currentTest
87 | }
88 | });
89 |
90 | client.init();
91 | client.url(testUrl).waitFor(testSelector, testTimeout, testComplete);
92 | }
93 |
94 | startServers(function() {
95 | startTest();
96 | });
97 |
--------------------------------------------------------------------------------
/tests/test.uuids.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var qunit = module;
4 |
5 | if (typeof module !== undefined && module.exports) {
6 | var PouchDB = require('../lib');
7 | var testUtils = require('./test.utils.js');
8 | }
9 |
10 | var rfcRegexp = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
11 |
12 | test('UUID generation count', 1, function() {
13 | var count = 10;
14 |
15 | equal(PouchDB.utils.uuids(count).length, count, "Correct number of uuids generated.");
16 | });
17 |
18 | test('UUID RFC4122 test', 2, function() {
19 | var uuid = PouchDB.utils.uuids()[0];
20 | equal(rfcRegexp.test(PouchDB.utils.uuids()[0]), true,
21 | "Single UUID complies with RFC4122.");
22 | equal(rfcRegexp.test(PouchDB.utils.uuid()), true,
23 | "Single UUID through Pouch.utils.uuid complies with RFC4122.");
24 | });
25 |
26 | test('UUID generation uniqueness', 1, function() {
27 | var count = 1000;
28 | var uuids = PouchDB.utils.uuids(count);
29 |
30 | equal(testUtils.eliminateDuplicates(uuids).length, count,
31 | "Generated UUIDS are unique.");
32 | });
33 |
34 | test('Test small uuid uniqness', 1, function() {
35 | var length = 8;
36 | var count = 2000;
37 |
38 | var uuids = PouchDB.utils.uuids(count, {length: length});
39 | equal(testUtils.eliminateDuplicates(uuids).length, count,
40 | "Generated small UUIDS are unique.");
41 | });
42 |
43 | test('Test custom length', 11, function() {
44 | var length = 32;
45 | var count = 10;
46 | var options = {length: length};
47 |
48 | var uuids = PouchDB.utils.uuids(count, options);
49 | // Test single UUID wrapper
50 | uuids.push(PouchDB.utils.uuid(options));
51 |
52 | uuids.map(function (uuid) {
53 | equal(uuid.length, length, "UUID length is correct.");
54 | });
55 | });
56 |
57 | test('Test custom length, redix', 22, function() {
58 | var length = 32;
59 | var count = 10;
60 | var radix = 5;
61 | var options = {length: length, radix: radix};
62 |
63 | var uuids = PouchDB.utils.uuids(count, options);
64 | // Test single UUID wrapper
65 | uuids.push(PouchDB.utils.uuid(options));
66 |
67 | uuids.map(function (uuid) {
68 | var nums = uuid.split('').map(function(character) {
69 | return parseInt(character, radix);
70 | });
71 |
72 | var max = Math.max.apply(Math, nums);
73 | var min = Math.min.apply(Math, nums);
74 |
75 | equal(max < radix, true, "Maximum character is less than radix");
76 | equal(min >= 0, true, "Min character is greater than or equal to 0");
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: learn
3 | title: PouchDB, the JavaScript Database that Syncs!
4 | ---
5 |
6 | # FAQ
7 |
8 |
9 | ### Can PouchDB sync with MySQL / my current non CouchDB database?
10 |
11 | No, the data model of your application has a lot of impact on its ability to sync, relational data with the existence of transactions make this harder. It may be possible given some tradeoffs but right now we are focussing on making PouchDB <-> (PouchDB / CouchDB) sync as reliable and easy to use as possible.
12 |
13 | ### What can PouchDB sync with?
14 |
15 | There are a number of projects that implement a CouchDB like protocol and PouchDB should be able to replicate with them, they include:
16 |
17 | * [PouchDB-Server](https://github.com/nick-thompson/pouchdb-server) - a HTTP api written on top of PouchDB
18 | * [Cloudant](https://cloudant.com/) - A cluster aware fork of CouchDB
19 | * [Couchbase Sync Gateway](http://www.couchbase.com/communities/couchbase-sync-gateway) - A sync gateway for Couchbase
20 |
21 | ### The web is nice, but I want to build a native app?
22 |
23 | PouchDB is one of multiple projects that implement the CouchDB protocol and these can all be used to sync the same set of data. For desktop applications you may want to look into embedding CouchDB (or [rcouch](https://github.com/refuge/rcouch)), for mobile applications you can use PouchDB within [Apache Cordova](http://cordova.apache.org/) or you can look at [Couchbase lite for iOS](https://github.com/couchbase/couchbase-lite-ios) and [Android](https://github.com/couchbase/couchbase-lite-android).
24 |
25 | ### Browsers have storage limitations, how much data can PouchDB store?
26 |
27 | In Firefox PouchDB uses IndexedDB, this will ask the user if data can be stored the first it is attempted then every 50MB after, the amount that can be stored is not limited.
28 |
29 | Chrome calculates the amount of storage left available on the users hard drive and uses [that to calculate a limit](https://developers.google.com/chrome/whitepapers/storage#temporary).
30 |
31 | Mobile Safari on iOS has a hard 50MB limit, desktop Safari will prompt users wanting to store more than 5MB up to a limit of 500MB.
32 |
33 | Opera has no known limit.
34 |
35 | Internet Exporer 10 has a hard 250MB limit.
36 |
37 | ### CouchDB Differences
38 |
39 | PouchDB is also a CouchDB client and you should be able to switch between a local database or an online CouchDB instance changing any of your applications code, there are some minor differences to note:
40 |
41 | **View Collation** - CouchDB uses ICU to order keys in a view query, in PouchDB they are ASCII ordered.
42 |
43 | **View Offset** - CouchDB returns an `offset` property in the view results, PouchDB doesnt.
44 |
--------------------------------------------------------------------------------
/docs/learn.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: learn
3 | title: PouchDB, the JavaScript Database that Syncs!
4 | ---
5 |
6 | # About PouchDB
7 |
8 | PouchDB was written to help web developers build applications that work as well offline as well as they do online, applications save data locally so the user can use all the features of an app even while offline and synchronise the data between clients so they have up to date data wherever they go.
9 |
10 | PouchDB is a free open source project, written in Javascript by these [wonderful contributors](https://github.com/daleharvey/pouchdb/graphs/contributors) and inspired by Apache CouchDB . If you want to get involved then check out the [contributing guide](https://github.com/daleharvey/pouchdb/blob/master/CONTRIBUTING.md)
11 |
12 | # Browser Support
13 |
14 | PouchDB uses various backends so it can work across various browsers and in Node.js. It uses IndexedDB in Firefox and Chrome, WebSQL in Safari and Opera and LevelDB in Node.js. It is currently tested in:
15 |
16 | * Firefox 12+
17 | * Chrome 19+
18 | * Opera 12+
19 | * Safari 5+
20 | * [Node.js 0.10+](http://nodejs.org/)
21 | * [Apache Cordova](http://cordova.apache.org/)
22 | * Internet Explorer 10+
23 |
24 | If your application requires support for Internet Explorer below version 10, it is possible to use an online CouchDB as a fallback, however it will not work offline.
25 |
26 | # Current Status
27 |
28 | PouchDB in the browser currently beta release software, it is extensively tested and the functionality implemented is known to be stable however you may find bugs in lesser used parts of the API. The API is currently stable with no known changes and you will be able to upgrade PouchDB without losing data. We are currently working towards a stable release of PouchDB.
29 |
30 | PouchDB in Node.js is currently alpha and an upgrade to the library can break current databases. It is however possible to upgrade by replicating data across different versions to manually upgrade.
31 |
32 | # Installing
33 |
34 | PouchDB is designed to be a minimal library that is suitable for mobile devices, to start using PouchDB in your website you simply [Download](http://download.pouchdb.com) and include it in your webpage.
35 |
36 | {% highlight html %}{% endhighlight %}
37 |
38 | If you are using Node.js then
39 |
40 | {% highlight bash %}$ npm install pouchdb{% endhighlight %}
41 |
42 | For a HTTP API to PouchDB check out [PouchDB Server](https://github.com/nick-thompson/pouchdb-server)
43 |
44 | # Using PouchDB
45 |
46 | To get started using PouchDB check out our [Getting Started Tutorial](getting-started.html) and the [API Documentation](api.html).
--------------------------------------------------------------------------------
/tests/webrunner.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // use query parameter testFiles if present,
4 | // eg: test.html?testFiles=test.basics.js
5 | var testFiles = window.location.search.match(/[?&]testFiles=([^&]+)/);
6 | testFiles = testFiles && testFiles[1].split(',') || [];
7 | var started = new Date();
8 | if (!testFiles.length) {
9 | testFiles = [
10 | 'test.setup.js',
11 | 'test.basics.js', 'test.all_dbs.js', 'test.changes.js',
12 | 'test.bulk_docs.js', 'test.all_docs.js', 'test.conflicts.js',
13 | 'test.revs_diff.js',
14 | 'test.replication.js', 'test.views.js', 'test.taskqueue.js',
15 | 'test.design_docs.js', 'test.issue221.js', 'test.http.js',
16 | 'test.compaction.js', 'test.get.js',
17 | 'test.attachments.js', 'test.uuids.js', 'test.slash_id.js',
18 | 'test.worker.js'
19 | ];
20 | }
21 |
22 | testFiles.unshift('test.utils.js');
23 |
24 | // The tests use Pouch.extend and Pouch.ajax directly (for now)
25 | var sourceFiles = [
26 | '../dist/pouchdb-nightly.js'
27 | ];
28 |
29 | // Thanks to http://engineeredweb.com/blog/simple-async-javascript-loader/
30 | function asyncLoadScript(url, callback) {
31 |
32 | // Create a new script and setup the basics.
33 | var script = document.createElement("script"),
34 | firstScript = document.getElementsByTagName('script')[0];
35 |
36 | script.async = true;
37 | script.src = url;
38 |
39 | // Handle the case where an optional callback was passed in.
40 | if ( "function" === typeof(callback) ) {
41 | script.onload = function() {
42 | callback();
43 |
44 | // Clear it out to avoid getting called more than once or any memory leaks.
45 | script.onload = script.onreadystatechange = undefined;
46 | };
47 | script.onreadystatechange = function() {
48 | if ( "loaded" === script.readyState || "complete" === script.readyState ) {
49 | script.onload();
50 | }
51 | };
52 | }
53 |
54 | // Attach the script tag to the page (before the first script) so the
55 | //magic can happen.
56 | firstScript.parentNode.insertBefore(script, firstScript);
57 | }
58 |
59 | function startQUnit() {
60 | QUnit.config.reorder = false;
61 | }
62 |
63 | function asyncParForEach(array, fn, callback) {
64 | if (array.length === 0) {
65 | callback(); // done immediately
66 | return;
67 | }
68 | var toLoad = array.shift();
69 | fn(toLoad, function() {
70 | asyncParForEach(array, fn, callback);
71 | });
72 | }
73 |
74 | QUnit.config.testTimeout = 60000;
75 |
76 | QUnit.jUnitReport = function(report) {
77 | document.body.classList.add('testsComplete');
78 | report.started = started;
79 | report.completed = new Date();
80 | report.passed = (report.results.failed === 0);
81 | delete report.xml;
82 | window.testReport = report;
83 | };
84 |
85 | asyncParForEach(sourceFiles, asyncLoadScript, function() {
86 | asyncParForEach(testFiles, asyncLoadScript, function() {
87 | startQUnit();
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/docs/static/style/pygments.css:
--------------------------------------------------------------------------------
1 | .highlight{background-color:#073642;color:#93a1a1}.highlight .c{color:#586e75 !important;font-style:italic !important}.highlight .cm{color:#586e75 !important;font-style:italic !important}.highlight .cp{color:#586e75 !important;font-style:italic !important}.highlight .c1{color:#586e75 !important;font-style:italic !important}.highlight .cs{color:#586e75 !important;font-weight:bold !important;font-style:italic !important}.highlight .err{color:#dc322f !important;background:none !important}.highlight .k{color:#cb4b16 !important}.highlight .o{color:#93a1a1 !important;font-weight:bold !important}.highlight .p{color:#93a1a1 !important}.highlight .ow{color:#2aa198 !important;font-weight:bold !important}.highlight .gd{color:#93a1a1 !important;background-color:#372c34 !important;display:inline-block}.highlight .gd .x{color:#93a1a1 !important;background-color:#4d2d33 !important;display:inline-block}.highlight .ge{color:#93a1a1 !important;font-style:italic !important}.highlight .gr{color:#aa0000}.highlight .gh{color:#586e75 !important}.highlight .gi{color:#93a1a1 !important;background-color:#1a412b !important;display:inline-block}.highlight .gi .x{color:#93a1a1 !important;background-color:#355720 !important;display:inline-block}.highlight .go{color:#888888}.highlight .gp{color:#555555}.highlight .gs{color:#93a1a1 !important;font-weight:bold !important}.highlight .gu{color:#6c71c4 !important}.highlight .gt{color:#aa0000}.highlight .kc{color:#859900 !important;font-weight:bold !important}.highlight .kd{color:#268bd2 !important}.highlight .kp{color:#cb4b16 !important;font-weight:bold !important}.highlight .kr{color:#d33682 !important;font-weight:bold !important}.highlight .kt{color:#2aa198 !important}.highlight .n{color:#268bd2 !important}.highlight .na{color:#268bd2 !important}.highlight .nb{color:#859900 !important}.highlight .nc{color:#d33682 !important}.highlight .no{color:#b58900 !important}.highlight .ni{color:#800080}.highlight .nl{color:#859900 !important}.highlight .ne{color:#268bd2 !important;font-weight:bold !important}.highlight .nf{color:#268bd2 !important;font-weight:bold !important}.highlight .nn{color:#b58900 !important}.highlight .nt{color:#268bd2 !important;font-weight:bold !important}.highlight .nx{color:#b58900 !important}.highlight .bp{color:#999999}.highlight .vc{color:#008080}.highlight .vg{color:#268bd2 !important}.highlight .vi{color:#268bd2 !important}.highlight .nv{color:#268bd2 !important}.highlight .w{color:#bbbbbb}.highlight .mf{color:#2aa198 !important}.highlight .m{color:#2aa198 !important}.highlight .mh{color:#2aa198 !important}.highlight .mi{color:#2aa198 !important}.highlight .mo{color:#009999}.highlight .s{color:#2aa198 !important}.highlight .sb{color:#d14}.highlight .sc{color:#d14}.highlight .sd{color:#2aa198 !important}.highlight .s2{color:#2aa198 !important}.highlight .se{color:#dc322f !important}.highlight .sh{color:#d14}.highlight .si{color:#268bd2 !important}.highlight .sx{color:#d14}.highlight .sr{color:#2aa198 !important}.highlight .s1{color:#2aa198 !important}.highlight .ss{color:#990073}.highlight .il{color:#009999}.highlight div .gd,.highlight div .gd .x,.highlight div .gi,.highlight div .gi .x{display:inline-block;width:100%}
2 |
--------------------------------------------------------------------------------
/tests/test.issue221.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var adapters = [
4 | ['local-1', 'http-1'],
5 | ['http-1', 'http-2'],
6 | ['http-1', 'local-1'],
7 | ['local-1', 'local-2']
8 | ];
9 |
10 | if (typeof module !== undefined && module.exports) {
11 | var PouchDB = require('../lib');
12 | var testUtils = require('./test.utils.js');
13 | }
14 |
15 | adapters.map(function(adapters) {
16 |
17 | QUnit.module('replication + compaction: ' + adapters[0] + ':' + adapters[1], {
18 | setup: function() {
19 | this.local = testUtils.generateAdapterUrl(adapters[0]);
20 | this.remote = testUtils.generateAdapterUrl(adapters[1]);
21 | PouchDB.enableAllDbs = true;
22 | },
23 | teardown: testUtils.cleanupTestDatabases
24 | });
25 |
26 | var doc = { _id: '0', integer: 0 };
27 |
28 | asyncTest('Testing issue #221', function() {
29 | var self = this;
30 | // Create databases.
31 | testUtils.initDBPair(self.local, self.remote, function(local, remote) {
32 | // Write a doc in CouchDB.
33 | remote.put(doc, function(err, results) {
34 | // Update the doc.
35 | doc._rev = results.rev;
36 | doc.integer = 1;
37 | remote.put(doc, function(err, results) {
38 | // Compact the db.
39 | remote.compact(function() {
40 | remote.get(doc._id, {revs_info:true},function(err, data) {
41 | var correctRev = data._revs_info[0];
42 | local.replicate.from(remote, function(err, results) {
43 | // Check the Pouch doc.
44 | local.get(doc._id, function(err, results) {
45 | strictEqual(results._rev, correctRev.rev,
46 | 'correct rev stored after replication');
47 | strictEqual(results.integer, 1,
48 | 'correct content stored after replication');
49 | start();
50 | });
51 | });
52 | });
53 | });
54 | });
55 | });
56 | });
57 | });
58 |
59 | asyncTest('Testing issue #221 again', function() {
60 | var self = this;
61 | // Create databases.
62 | testUtils.initDBPair(self.local, self.remote, function(local, remote) {
63 | // Write a doc in CouchDB.
64 | remote.put(doc, function(err, results) {
65 | doc._rev = results.rev;
66 | // Second doc so we get 2 revisions from replicate.
67 | remote.put(doc, function(err, results) {
68 | doc._rev = results.rev;
69 | local.replicate.from(remote, function(err, results) {
70 | doc.integer = 1;
71 | // One more change
72 | remote.put(doc, function(err, results) {
73 | // Testing if second replications fails now
74 | local.replicate.from(remote, function(err, results) {
75 | local.get(doc._id, function(err, results) {
76 | strictEqual(results.integer, 1, 'correct content stored after replication');
77 | start();
78 | });
79 | });
80 | });
81 | });
82 | });
83 | });
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/lib/deps/errors.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function PouchError(opts) {
4 | this.status = opts.status;
5 | this.name = opts.error;
6 | this.message = opts.reason;
7 | this.error = true;
8 | }
9 | PouchError.prototype = new Error();
10 |
11 |
12 | exports.MISSING_BULK_DOCS = new PouchError({
13 | status: 400,
14 | error: 'bad_request',
15 | reason: "Missing JSON list of 'docs'"
16 | });
17 | exports.MISSING_DOC = new PouchError({
18 | status: 404,
19 | error: 'not_found',
20 | reason: 'missing'
21 | });
22 | exports.REV_CONFLICT = new PouchError({
23 | status: 409,
24 | error: 'conflict',
25 | reason: 'Document update conflict'
26 | });
27 | exports.INVALID_ID = new PouchError({
28 | status: 400,
29 | error: 'invalid_id',
30 | reason: '_id field must contain a string'
31 | });
32 | exports.MISSING_ID = new PouchError({
33 | status: 412,
34 | error: 'missing_id',
35 | reason: '_id is required for puts'
36 | });
37 | exports.RESERVED_ID = new PouchError({
38 | status: 400,
39 | error: 'bad_request',
40 | reason: 'Only reserved document ids may start with underscore.'
41 | });
42 | exports.NOT_OPEN = new PouchError({
43 | status: 412,
44 | error: 'precondition_failed',
45 | reason: 'Database not open so cannot close'
46 | });
47 | exports.UNKNOWN_ERROR = new PouchError({
48 | status: 500,
49 | error: 'unknown_error',
50 | reason: 'Database encountered an unknown error'
51 | });
52 | exports.BAD_ARG = new PouchError({
53 | status: 500,
54 | error: 'badarg',
55 | reason: 'Some query argument is invalid'
56 | });
57 | exports.INVALID_REQUEST = new PouchError({
58 | status: 400,
59 | error: 'invalid_request',
60 | reason: 'Request was invalid'
61 | });
62 | exports.QUERY_PARSE_ERROR = new PouchError({
63 | status: 400,
64 | error: 'query_parse_error',
65 | reason: 'Some query parameter is invalid'
66 | });
67 | exports.DOC_VALIDATION = new PouchError({
68 | status: 500,
69 | error: 'doc_validation',
70 | reason: 'Bad special document member'
71 | });
72 | exports.BAD_REQUEST = new PouchError({
73 | status: 400,
74 | error: 'bad_request',
75 | reason: 'Something wrong with the request'
76 | });
77 | exports.NOT_AN_OBJECT = new PouchError({
78 | status: 400,
79 | error: 'bad_request',
80 | reason: 'Document must be a JSON object'
81 | });
82 | exports.DB_MISSING = new PouchError({
83 | status: 404,
84 | error: 'not_found',
85 | reason: 'Database not found'
86 | });
87 | exports.IDB_ERROR = new PouchError({
88 | status: 500,
89 | error: 'indexed_db_went_bad',
90 | reason: 'unknown'
91 | });
92 | exports.WSQ_ERROR = new PouchError({
93 | status: 500,
94 | error: 'web_sql_went_bad',
95 | reason: 'unknown'
96 | });
97 | exports.LDB_ERROR = new PouchError({
98 | status: 500,
99 | error: 'levelDB_went_went_bad',
100 | reason: 'unknown'
101 | });
102 | exports.error = function (error, reason, name) {
103 | function CustomPouchError(msg) {
104 | this.message = reason;
105 | if (name) {
106 | this.name = name;
107 | }
108 | }
109 | CustomPouchError.prototype = error;
110 | return new CustomPouchError(reason);
111 | };
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ page.title }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
31 |
32 |
33 |
34 |
73 |
74 |
75 |
76 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/tests/test.revs_diff.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var adapters = ['http-1', 'local-1'];
4 |
5 | if (typeof module !== undefined && module.exports) {
6 | var PouchDB = require('../lib');
7 | var testUtils = require('./test.utils.js');
8 | }
9 |
10 | adapters.map(function(adapter) {
11 |
12 | QUnit.module("revs diff:" + adapter, {
13 | setup : function () {
14 | this.name = testUtils.generateAdapterUrl(adapter);
15 | PouchDB.enableAllDbs = true;
16 | },
17 | teardown: testUtils.cleanupTestDatabases
18 | });
19 |
20 | asyncTest("Test revs diff", function() {
21 | var revs = [];
22 | testUtils.initTestDB(this.name, function(err, db) {
23 | db.post({test: "somestuff", _id: 'somestuff'}, function (err, info) {
24 | revs.push(info.rev);
25 | db.put({_id: info.id, _rev: info.rev, another: 'test'}, function(err, info2) {
26 | revs.push(info2.rev);
27 | db.revsDiff({'somestuff': revs}, function(err, results) {
28 | ok(!('somestuff' in results), 'werent missing any revs');
29 | revs.push('2-randomid');
30 | db.revsDiff({'somestuff': revs}, function(err, results) {
31 | ok('somestuff' in results, 'listed missing revs');
32 | ok(results.somestuff.missing.length === 1, 'listed currect number of');
33 | start();
34 | });
35 | });
36 | });
37 | });
38 | });
39 | });
40 |
41 | asyncTest('Missing docs should be returned with all revisions being asked for',
42 | function() {
43 | testUtils.initTestDB(this.name, function(err, db) {
44 | // empty database
45 | var revs = ['1-a', '2-a', '2-b'];
46 | db.revsDiff({'foo': revs}, function(err, results) {
47 | ok('foo' in results, 'listed missing revs');
48 | deepEqual(results.foo.missing, revs, 'listed all revs');
49 | start();
50 | });
51 | });
52 | });
53 |
54 | asyncTest('Conflicting revisions that are available should not be marked as' +
55 | ' missing (#939)', function() {
56 | var doc = {_id: '939', _rev: '1-a'};
57 |
58 | function createConflicts(db, callback) {
59 | db.put(doc, {new_edits: false}, function(err, res) {
60 | testUtils.putAfter(db, {_id: '939', _rev: '2-a'}, '1-a', function(err, res) {
61 | testUtils.putAfter(db, {_id: '939', _rev: '2-b'}, '1-a', callback);
62 | });
63 | });
64 | }
65 |
66 | testUtils.initTestDB(this.name, function(err, db) {
67 | createConflicts(db, function() {
68 | db.revsDiff({'939': ['1-a', '2-a', '2-b']}, function(err, results) {
69 | ok(!('939' in results), 'no missing revs');
70 | start();
71 | });
72 | });
73 | });
74 | });
75 |
76 | asyncTest('Deleted revisions that are available should not be marked as' +
77 | ' missing (#935)', function() {
78 |
79 | function createDeletedRevision(db, callback) {
80 | db.put({_id: '935', _rev: '1-a'}, {new_edits: false}, function (err, info) {
81 | testUtils.putAfter(db, {_id: '935', _rev: '2-a', _deleted: true}, '1-a', callback);
82 | });
83 | }
84 |
85 | testUtils.initTestDB(this.name, function(err, db) {
86 | createDeletedRevision(db, function() {
87 | db.revsDiff({'935': ['1-a', '2-a']}, function(err, results) {
88 | ok(!('935' in results), 'should not return the deleted revs');
89 | start();
90 | });
91 | });
92 | });
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/docs/static/style/noun_project_5618.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/test.design_docs.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var adapters = ['local-1', 'http-1'];
4 |
5 | if (typeof module !== undefined && module.exports) {
6 | var PouchDB = require('../lib');
7 | var testUtils = require('./test.utils.js');
8 | }
9 |
10 | adapters.map(function(adapter) {
11 |
12 | QUnit.module("design_docs: " + adapter, {
13 | setup : function () {
14 | this.name = testUtils.generateAdapterUrl(adapter);
15 | PouchDB.enableAllDbs = true;
16 | },
17 | teardown: testUtils.cleanupTestDatabases
18 | });
19 |
20 | var doc = {
21 | _id: '_design/foo',
22 | views: {
23 | scores: {
24 | map: 'function(doc) { if (doc.score) { emit(null, doc.score); } }',
25 | reduce: 'function(keys, values, rereduce) { return sum(values); }'
26 | }
27 | },
28 | filters: {
29 | even: 'function(doc) { return doc.integer % 2 === 0; }'
30 | }
31 | };
32 |
33 | asyncTest("Test writing design doc", function () {
34 | testUtils.initTestDB(this.name, function(err, db) {
35 | db.post(doc, function (err, info) {
36 | ok(!err, 'Wrote design doc');
37 | db.get('_design/foo', function (err, info) {
38 | ok(!err, 'Read design doc');
39 | start();
40 | });
41 | });
42 | });
43 | });
44 |
45 | asyncTest("Changes filter", function() {
46 |
47 | var docs1 = [
48 | doc,
49 | {_id: "0", integer: 0},
50 | {_id: "1", integer: 1},
51 | {_id: "2", integer: 2},
52 | {_id: "3", integer: 3}
53 | ];
54 |
55 | var docs2 = [
56 | {_id: "4", integer: 4},
57 | {_id: "5", integer: 5},
58 | {_id: "6", integer: 6},
59 | {_id: "7", integer: 7}
60 | ];
61 |
62 | testUtils.initTestDB(this.name, function(err, db) {
63 | var count = 0;
64 | db.bulkDocs({docs: docs1}, function(err, info) {
65 | var changes = db.changes({
66 | filter: 'foo/even',
67 | onChange: function(change) {
68 | count += 1;
69 | if (count === 4) {
70 | ok(true, 'We got all the changes');
71 | changes.cancel();
72 | start();
73 | }
74 | },
75 | continuous: true
76 | });
77 | db.bulkDocs({docs: docs2}, {});
78 | });
79 | });
80 | });
81 |
82 | asyncTest("Basic views", function () {
83 |
84 | var docs1 = [
85 | doc,
86 | {_id: "dale", score: 3},
87 | {_id: "mikeal", score: 5},
88 | {_id: "max", score: 4},
89 | {_id: "nuno", score: 3}
90 | ];
91 |
92 | testUtils.initTestDB(this.name, function(err, db) {
93 | db.bulkDocs({docs: docs1}, function(err, info) {
94 | db.query('foo/scores', {reduce: false}, function(err, result) {
95 | equal(result.rows.length, 4, 'Correct # of results');
96 | db.query('foo/scores', function(err, result) {
97 | equal(result.rows[0].value, 15, 'Reduce gave correct result');
98 | start();
99 | });
100 | });
101 | });
102 | });
103 | });
104 |
105 | asyncTest("Concurrent queries", function() {
106 | testUtils.initTestDB(this.name, function(err, db) {
107 | db.bulkDocs({docs: [doc, {_id: "dale", score: 3}]}, function(err, info) {
108 | var cnt = 0;
109 | db.query('foo/scores', {reduce: false}, function(err, result) {
110 | equal(result.rows.length, 1, 'Correct # of results');
111 | if (cnt++ === 1) {
112 | start();
113 | }
114 | });
115 | db.query('foo/scores', {reduce: false}, function(err, result) {
116 | equal(result.rows.length, 1, 'Correct # of results');
117 | if (cnt++ === 1) {
118 | start();
119 | }
120 | });
121 | });
122 | });
123 | });
124 |
125 | });
126 |
--------------------------------------------------------------------------------
/lib/deps/extend.js:
--------------------------------------------------------------------------------
1 | // Extends method
2 | // (taken from http://code.jquery.com/jquery-1.9.0.js)
3 | // Populate the class2type map
4 | var class2type = {};
5 |
6 | var types = ["Boolean", "Number", "String", "Function", "Array", "Date", "RegExp", "Object", "Error"];
7 | for (var i = 0; i < types.length; i++) {
8 | var typename = types[i];
9 | class2type[ "[object " + typename + "]" ] = typename.toLowerCase();
10 | }
11 |
12 | var core_toString = class2type.toString;
13 | var core_hasOwn = class2type.hasOwnProperty;
14 |
15 | function type(obj) {
16 | if (obj === null) {
17 | return String( obj );
18 | }
19 | return typeof obj === "object" || typeof obj === "function" ?
20 | class2type[core_toString.call(obj)] || "object" :
21 | typeof obj;
22 | };
23 |
24 | function isWindow(obj) {
25 | return obj !== null && obj === obj.window;
26 | }
27 |
28 | function isPlainObject( obj ) {
29 | // Must be an Object.
30 | // Because of IE, we also have to check the presence of the constructor property.
31 | // Make sure that DOM nodes and window objects don't pass through, as well
32 | if ( !obj || type(obj) !== "object" || obj.nodeType || isWindow( obj ) ) {
33 | return false;
34 | }
35 |
36 | try {
37 | // Not own constructor property must be Object
38 | if ( obj.constructor &&
39 | !core_hasOwn.call(obj, "constructor") &&
40 | !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
41 | return false;
42 | }
43 | } catch ( e ) {
44 | // IE8,9 Will throw exceptions on certain host objects #9897
45 | return false;
46 | }
47 |
48 | // Own properties are enumerated firstly, so to speed up,
49 | // if last one is own, then all properties are own.
50 |
51 | var key;
52 | for ( key in obj ) {}
53 |
54 | return key === undefined || core_hasOwn.call( obj, key );
55 | };
56 |
57 |
58 | function isFunction(obj) {
59 | return type(obj) === "function";
60 | };
61 |
62 | var isArray = Array.isArray || function (obj) {
63 | return type(obj) === "array";
64 | };
65 |
66 | function extend() {
67 | var options, name, src, copy, copyIsArray, clone,
68 | target = arguments[0] || {},
69 | i = 1,
70 | length = arguments.length,
71 | deep = false;
72 |
73 | // Handle a deep copy situation
74 | if ( typeof target === "boolean" ) {
75 | deep = target;
76 | target = arguments[1] || {};
77 | // skip the boolean and the target
78 | i = 2;
79 | }
80 |
81 | // Handle case when target is a string or something (possible in deep copy)
82 | if ( typeof target !== "object" && !isFunction (target) ) {
83 | target = {};
84 | }
85 |
86 | // extend jQuery itself if only one argument is passed
87 | if ( length === i ) {
88 | target = this;
89 | --i;
90 | }
91 |
92 | for ( ; i < length; i++ ) {
93 | // Only deal with non-null/undefined values
94 | if ((options = arguments[ i ]) != null) {
95 | // Extend the base object
96 | for ( name in options ) {
97 | src = target[ name ];
98 | copy = options[ name ];
99 |
100 | // Prevent never-ending loop
101 | if ( target === copy ) {
102 | continue;
103 | }
104 |
105 | // Recurse if we're merging plain objects or arrays
106 | if ( deep && copy && ( isPlainObject(copy) || (copyIsArray = isArray(copy)) ) ) {
107 | if ( copyIsArray ) {
108 | copyIsArray = false;
109 | clone = src && isArray(src) ? src : [];
110 |
111 | } else {
112 | clone = src && isPlainObject(src) ? src : {};
113 | }
114 |
115 | // Never move original objects, clone them
116 | target[ name ] = extend( deep, clone, copy );
117 |
118 | // Don't bring in undefined values
119 | } else if ( copy !== undefined ) {
120 | if (!(isArray(options) && isFunction (copy))) {
121 | target[ name ] = copy;
122 | }
123 | }
124 | }
125 | }
126 | }
127 |
128 | // Return the modified object
129 | return target;
130 | };
131 |
132 |
133 | module.exports = extend;
134 |
135 |
--------------------------------------------------------------------------------
/tests/test.slash_id.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var adapters = ['local-1', 'http-1'];
4 | var repl_adapters = [['local-1', 'http-1'],
5 | ['http-1', 'http-2'],
6 | ['http-1', 'local-1'],
7 | ['local-1', 'local-2']];
8 |
9 | if (typeof module !== undefined && module.exports) {
10 | var PouchDB = require('../lib');
11 | var testUtils = require('./test.utils.js');
12 | }
13 |
14 | adapters.map(function(adapter) {
15 | QUnit.module('functions with / in _id: ' + adapter, {
16 | setup : function () {
17 | this.name = testUtils.generateAdapterUrl(adapter);
18 | PouchDB.enableAllDbs = true;
19 | },
20 | teardown: testUtils.cleanupTestDatabases
21 | });
22 |
23 | var binAttDoc = {
24 | _id: "bin_doc",
25 | _attachments:{
26 | "foo.txt": {
27 | content_type:"text/plain",
28 | data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
29 | }
30 | }
31 | };
32 |
33 | asyncTest('Insert a doc, putAttachment and allDocs', function() {
34 | testUtils.initTestDB(this.name, function(err, db) {
35 | ok(!err, 'opened the pouch');
36 | var docId = 'doc/with/slashes';
37 | var attachmentId = 'attachment/with/slashes';
38 | var blobData = 'attachment content';
39 | var blob = testUtils.makeBlob(blobData);
40 | var doc = {_id: docId, test: true};
41 | db.put(doc, function(err, info) {
42 | ok(!err, 'saved doc');
43 | strictEqual(info.id, 'doc/with/slashes', 'id is the same as inserted');
44 | db.putAttachment(docId, attachmentId, info.rev, blob, 'text/plain', function(err, res) {
45 | db.getAttachment(docId, attachmentId, function(err, res) {
46 | testUtils.readBlob(res, function(data) {
47 | db.get(docId, function(err, res){
48 | strictEqual(res._id, docId);
49 | ok(attachmentId in res._attachments, 'contains correct attachment');
50 | start();
51 | });
52 | });
53 | });
54 | });
55 | });
56 | });
57 | });
58 |
59 | asyncTest('BulkDocs and changes', function() {
60 | testUtils.initTestDB(this.name, function(err, db) {
61 | var docs = [
62 | {_id: 'part/doc1', int: 1},
63 | {_id: 'part/doc2', int: 2, _attachments: {
64 | 'attachment/with/slash': {
65 | content_type: 'text/plain',
66 | data: 'c29tZSBkYXRh'
67 | }
68 | }},
69 | {_id: 'part/doc3', int: 3}
70 | ];
71 | db.bulkDocs({docs: docs}, function(err, res) {
72 | for(var i = 0; i < 3; i++){
73 | strictEqual(res[i].ok, true, 'correctly inserted ' + docs[i]._id);
74 | }
75 | db.allDocs({include_docs: true, attachments: true}, function(err, res) {
76 | res.rows.sort(function(a, b){return a.doc.int - b.doc.int;});
77 | for(var i = 0; i < 3; i++){
78 | strictEqual(res.rows[i].doc._id, docs[i]._id, '(allDocs) correctly inserted ' + docs[i]._id);
79 | }
80 | strictEqual('attachment/with/slash' in res.rows[1].doc._attachments, true, 'doc2 has attachment');
81 | db.changes({
82 | complete: function(err, res) {
83 | res.results.sort(function(a, b){return a.id.localeCompare(b.id);});
84 | for(var i = 0; i < 3; i++){
85 | strictEqual(res.results[i].id, docs[i]._id, '(changes) correctly inserted ' + docs[i]._id);
86 | }
87 | start();
88 | }
89 | });
90 | });
91 | });
92 | });
93 | });
94 | });
95 |
96 |
97 | repl_adapters.map(function(adapters) {
98 |
99 | QUnit.module('replication with / in _id: ' + adapters[0] + ':' + adapters[1], {
100 | setup : function () {
101 | this.name = testUtils.generateAdapterUrl(adapters[0]);
102 | this.remote = testUtils.generateAdapterUrl(adapters[1]);
103 | }
104 | });
105 |
106 | asyncTest("Attachments replicate", function() {
107 | var binAttDoc = {
108 | _id: "bin_doc/with/slash",
109 | _attachments:{
110 | "foo/with/slash.txt": {
111 | content_type:"text/plain",
112 | data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
113 | }
114 | }
115 | };
116 |
117 | var docs1 = [
118 | binAttDoc,
119 | {_id: "0", integer: 0},
120 | {_id: "1", integer: 1},
121 | {_id: "2", integer: 2},
122 | {_id: "3", integer: 3}
123 | ];
124 |
125 | testUtils.initDBPair(this.name, this.remote, function(db, remote) {
126 | remote.bulkDocs({docs: docs1}, function(err, info) {
127 | var replicate = db.replicate.from(remote, function() {
128 | db.get('bin_doc/with/slash', {attachments: true}, function(err, doc) {
129 | equal(binAttDoc._attachments['foo/with/slash.txt'].data,
130 | doc._attachments['foo/with/slash.txt'].data);
131 | start();
132 | });
133 | });
134 | });
135 | });
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | [PouchDB](http://pouchdb.com/) - The Javascript Database that Syncs
2 | ==================================================
3 |
4 | Welcome, so you are thinking about contributing to PouchDB? awesome, this is a great place to start.
5 |
6 | Get in Touch
7 | ------------
8 |
9 | The following documentation should answer most of the common questions about how to get starting contributing, if you have any questions, please feel free to ask on the
10 | [PouchDB Mailing List](https://groups.google.com/forum/#!forum/pouchdb) or in #pouchdb on irc.freenode.net.
11 |
12 | Most project discussions should happen on the Mailing list / Bug Tracker and IRC, however if you are a first time contributor and want some help getting started feel free to send a private email to any of the following maintainers:
13 |
14 | * Dale Harvey (dale@arandomurl.com, daleharvey on IRC)
15 | * Calvin Metcalf (calvin.metcalf@gmail.com)
16 |
17 |
18 | Good First Patch
19 | ----------------
20 |
21 | If you are looking for something to work on, we try to maintain a list of issues that should be suitable for first time contributions, they can be found tagged [goodfirstpatch](https://github.com/daleharvey/pouchdb/issues?labels=goodfirstpatch&state=open).
22 |
23 |
24 | Guide to Contributions
25 | --------------------------------------
26 |
27 | * Almost all Pull Requests for features or bug fixes will need tests
28 | * We follow [Felix's Node.js Style Guide](http://nodeguide.com/style.html)
29 | * Almost all Pull Requests for features or bug fixes will need tests (seriously, its really important)
30 | * Before opening a pull request run `$ npm test` to lint test the changes and run node tests. Preferably run the browser tests as well.
31 | * Commit messages should follow the following style:
32 |
33 | ```
34 | (#99) - A brief one line description < 50 chars
35 |
36 | Followed by further explanation if needed, this should be wrapped at
37 | around 72 characters. Most commits should reference an existing
38 | issue
39 | ```
40 |
41 | Dependencies
42 | --------------------------------------
43 |
44 | PouchDB needs the following to be able to build and test your build, if you havent installed them then best to do do so now, we will wait.
45 |
46 | * [Node.js](http://nodejs.org/)
47 | * [CouchDB](http://couchdb.apache.org/)
48 |
49 | Building PouchDB
50 | --------------------------------------
51 |
52 | All dependancies installed? great, now building PouchDB itself is a breeze:
53 |
54 | $ cd pouchdb
55 | $ npm install
56 | $ npm run build
57 |
58 | You will now have various distributions of PouchDB in your `dist` folder, congratulations.
59 |
60 | Running PouchDB Tests
61 | --------------------------------------
62 |
63 | The PouchDB test suite expects an instance of CouchDB running in Admin Party on http://127.0.0.1:5984, you can configure this by sending the `COUCH_HOST` env var when running the Node tests or the `dev-server`
64 |
65 | ### Node Tests
66 |
67 | Run all tests with:
68 |
69 | $ npm run test-node
70 |
71 | Run an indivitual test:
72 |
73 | $ TEST_FILE=test.basics.js npm run test-node
74 |
75 | ### Browser Tests
76 |
77 | Browser tests require a running HTTP server and a CORS proxy:
78 |
79 | $ npm run dev-server
80 | # or
81 | $ COUCH_HOST=http://user:pass@myname.host.com npm run dev-server
82 |
83 | Now visit http://127.0.0.1:8000/tests/test.html in your browser add ?testFiles=test.basics.js to run single test file. You do not need to manually rebuild PouchDB when you run the `dev-server` target, any changes you make to the source will automatically be built.
84 |
85 | ### All Tests
86 |
87 | To run all tests:
88 |
89 | $ npm test
90 |
91 | Git Essentials
92 | --------------------------------------
93 |
94 | Workflows can vary, but here is a very simple workflow for contributing a bug fix:
95 |
96 | $ git clone git@github.com:myfork/pouchdb.git
97 | $ git remote add pouchdb https://github.com/daleharvey/pouchdb.git
98 |
99 | $ git checkout -b 121-issue-keyword master
100 | # Write tests + code
101 | $ git add src/afile.js
102 | $ git commit -m "(#121) - A brief description of what I changed"
103 | $ git push origin 121-issue-keyword
104 |
105 | Building PouchDB Documentation
106 | --------------------------------------
107 |
108 | The source for the website http://pouchdb.com is stored inside the `docs` directory of the PouchDB repository, you can make changes and submit pull requests as with any other patch. To build and view the website locally you will need to install [jekyll](http://jekyllrb.com/) then:
109 |
110 | $ cd docs
111 | $ jekyll -w serve
112 |
113 | You should now find the documentation at http://127.0.0.1:4000
114 |
115 | Committers!
116 | --------------
117 |
118 | With great power comes great responsibility yada yada yada:
119 |
120 | * Code is peer reviewed, you should (almost) never push your own code.
121 | * Please dont accidently force push to master.
122 | * Cherry Pick / Rebase commits, dont use the big green button.
123 | * Ensure reviewed code follows the above contribution guidelines, if it doesnt feel free to ammend and make note.
124 | * Please try to watch when Pull Requests are made and review and / or commit them in a timely manner.
125 | * Thanks, you are all awesome human beings.
126 |
--------------------------------------------------------------------------------
/tests/qunit/qunit.css:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.9.0pre - A JavaScript Unit Testing Framework
3 | *
4 | * http://docs.jquery.com/QUnit
5 | *
6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer
7 | * Dual licensed under the MIT (MIT-LICENSE.txt)
8 | * or GPL (GPL-LICENSE.txt) licenses.
9 | */
10 |
11 | /** Font Family and Sizes */
12 |
13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
15 | }
16 |
17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
18 | #qunit-tests { font-size: smaller; }
19 |
20 |
21 | /** Resets */
22 |
23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 |
29 | /** Header */
30 |
31 | #qunit-header {
32 | padding: 0.5em 0 0.5em 1em;
33 |
34 | color: #8699a4;
35 | background-color: #0d3349;
36 |
37 | font-size: 1.5em;
38 | line-height: 1em;
39 | font-weight: normal;
40 |
41 | border-radius: 15px 15px 0 0;
42 | -moz-border-radius: 15px 15px 0 0;
43 | -webkit-border-top-right-radius: 15px;
44 | -webkit-border-top-left-radius: 15px;
45 | }
46 |
47 | #qunit-header a {
48 | text-decoration: none;
49 | color: #c2ccd1;
50 | }
51 |
52 | #qunit-header a:hover,
53 | #qunit-header a:focus {
54 | color: #fff;
55 | }
56 |
57 | #qunit-header label {
58 | display: inline-block;
59 | padding-left: 0.5em;
60 | }
61 |
62 | #qunit-banner {
63 | height: 5px;
64 | }
65 |
66 | #qunit-testrunner-toolbar {
67 | padding: 0.5em 0 0.5em 2em;
68 | color: #5E740B;
69 | background-color: #eee;
70 | }
71 |
72 | #qunit-userAgent {
73 | padding: 0.5em 0 0.5em 2.5em;
74 | background-color: #2b81af;
75 | color: #fff;
76 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
77 | }
78 |
79 |
80 | /** Tests: Pass/Fail */
81 |
82 | #qunit-tests {
83 | list-style-position: inside;
84 | }
85 |
86 | #qunit-tests li {
87 | padding: 0.4em 0.5em 0.4em 2.5em;
88 | border-bottom: 1px solid #fff;
89 | list-style-position: inside;
90 | }
91 |
92 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
93 | display: none;
94 | }
95 |
96 | #qunit-tests li strong {
97 | cursor: pointer;
98 | }
99 |
100 | #qunit-tests li a {
101 | padding: 0.5em;
102 | color: #c2ccd1;
103 | text-decoration: none;
104 | }
105 | #qunit-tests li a:hover,
106 | #qunit-tests li a:focus {
107 | color: #000;
108 | }
109 |
110 | #qunit-tests ol {
111 | margin-top: 0.5em;
112 | padding: 0.5em;
113 |
114 | background-color: #fff;
115 |
116 | border-radius: 15px;
117 | -moz-border-radius: 15px;
118 | -webkit-border-radius: 15px;
119 |
120 | box-shadow: inset 0px 2px 13px #999;
121 | -moz-box-shadow: inset 0px 2px 13px #999;
122 | -webkit-box-shadow: inset 0px 2px 13px #999;
123 | }
124 |
125 | #qunit-tests table {
126 | border-collapse: collapse;
127 | margin-top: .2em;
128 | }
129 |
130 | #qunit-tests th {
131 | text-align: right;
132 | vertical-align: top;
133 | padding: 0 .5em 0 0;
134 | }
135 |
136 | #qunit-tests td {
137 | vertical-align: top;
138 | }
139 |
140 | #qunit-tests pre {
141 | margin: 0;
142 | white-space: pre-wrap;
143 | word-wrap: break-word;
144 | }
145 |
146 | #qunit-tests del {
147 | background-color: #e0f2be;
148 | color: #374e0c;
149 | text-decoration: none;
150 | }
151 |
152 | #qunit-tests ins {
153 | background-color: #ffcaca;
154 | color: #500;
155 | text-decoration: none;
156 | }
157 |
158 | /*** Test Counts */
159 |
160 | #qunit-tests b.counts { color: black; }
161 | #qunit-tests b.passed { color: #5E740B; }
162 | #qunit-tests b.failed { color: #710909; }
163 |
164 | #qunit-tests li li {
165 | margin: 0.5em;
166 | padding: 0.4em 0.5em 0.4em 0.5em;
167 | background-color: #fff;
168 | border-bottom: none;
169 | list-style-position: inside;
170 | }
171 |
172 | /*** Passing Styles */
173 |
174 | #qunit-tests li li.pass {
175 | color: #5E740B;
176 | background-color: #fff;
177 | border-left: 26px solid #C6E746;
178 | }
179 |
180 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
181 | #qunit-tests .pass .test-name { color: #366097; }
182 |
183 | #qunit-tests .pass .test-actual,
184 | #qunit-tests .pass .test-expected { color: #999999; }
185 |
186 | #qunit-banner.qunit-pass { background-color: #C6E746; }
187 |
188 | /*** Failing Styles */
189 |
190 | #qunit-tests li li.fail {
191 | color: #710909;
192 | background-color: #fff;
193 | border-left: 26px solid #EE5757;
194 | white-space: pre;
195 | }
196 |
197 | #qunit-tests > li:last-child {
198 | border-radius: 0 0 15px 15px;
199 | -moz-border-radius: 0 0 15px 15px;
200 | -webkit-border-bottom-right-radius: 15px;
201 | -webkit-border-bottom-left-radius: 15px;
202 | }
203 |
204 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
205 | #qunit-tests .fail .test-name,
206 | #qunit-tests .fail .module-name { color: #000000; }
207 |
208 | #qunit-tests .fail .test-actual { color: #EE5757; }
209 | #qunit-tests .fail .test-expected { color: green; }
210 |
211 | #qunit-banner.qunit-fail { background-color: #EE5757; }
212 |
213 |
214 | /** Result */
215 |
216 | #qunit-testresult {
217 | padding: 0.5em 0.5em 0.5em 2.5em;
218 |
219 | color: #2b81af;
220 | background-color: #D2E0E6;
221 |
222 | border-bottom: 1px solid white;
223 | }
224 | #qunit-testresult .module-name {
225 | font-weight: bold;
226 | }
227 |
228 | /** Fixture */
229 |
230 | #qunit-fixture {
231 | position: absolute;
232 | top: -10000px;
233 | left: -10000px;
234 | width: 1000px;
235 | height: 1000px;
236 | }
237 |
--------------------------------------------------------------------------------
/docs/static/style/pouchdb.css:
--------------------------------------------------------------------------------
1 | /*** GLOBAL TAG DEFINITIONS ***/
2 | * {
3 | box-sizing: border-box;
4 | }
5 |
6 | html {
7 | overflow-y: scroll;
8 | }
9 |
10 | body {
11 | margin: 0;
12 | font-family: 'Helvetica Neue', Helvetica, Arial, Sans-Serif;
13 | line-height: 1.4em;
14 | font-size: 15px;
15 | }
16 |
17 | a {
18 | text-decoration: none;
19 | }
20 |
21 | p {
22 | margin: 0 0 15px 0;
23 | }
24 |
25 | pre {
26 | line-height: 125%;
27 | padding: 10px;
28 | margin: 0;
29 | }
30 |
31 | table {
32 | padding: 0;
33 | border-spacing:0;
34 | border-collapse:collapse;
35 | }
36 |
37 | table td, table th {
38 | padding: 0;
39 | }
40 |
41 | /*** UTILITY CLASSES ***/
42 |
43 | .lineno {
44 | border-right: 1px solid #666;
45 | padding-right: 5px;
46 | font-size: 12px;
47 | }
48 |
49 | div.highlight, div.linenodiv {
50 | margin-bottom: 10px;
51 | }
52 |
53 | div.highlight {
54 | border-radius: 0 5px 5px 0;
55 | }
56 |
57 | div.linenodiv {
58 | border-radius: 5px 0 0 5px;
59 | background-color: rgb(7, 54, 66);
60 | color: #777;
61 | border-right: 1px solid rgba(0, 0, 0, 0.6);
62 | }
63 |
64 | p code {
65 | padding: 0px 3px;
66 | background: none repeat scroll 0% 0% rgb(255, 255, 255);
67 | border: 1px solid rgb(221, 221, 221);
68 | font-size: 12px;
69 | }
70 |
71 | /*** GLOBAL LAYOUT ***/
72 |
73 | #headerwrapper, #footerwrapper, #content {
74 | max-width: 960px;
75 | margin: 0 auto;
76 | }
77 |
78 | header {
79 | background: none repeat scroll 0% 0% rgb(51, 51, 51);
80 | overflow: auto;
81 | }
82 |
83 | header h1 {
84 | line-height: 30px;
85 | }
86 |
87 | header a {
88 | line-height: 30px;
89 | color: #FFF;
90 | }
91 |
92 | nav a {
93 | padding-top: 10px;
94 | padding-bottom: 10px;
95 | padding-left: 20px;
96 | padding-right: 20px;
97 | text-transform: uppercase;
98 | }
99 |
100 | nav li:first-child a {
101 | border-left: 0;
102 | }
103 |
104 | nav a:hover {
105 | background: #000;
106 | }
107 |
108 | header nav {
109 | float: right;
110 | margin-top: 4px;
111 | }
112 |
113 | header ul {
114 | list-style-type: none;
115 | }
116 |
117 | header li {
118 | float: left;
119 | }
120 |
121 | header h1 {
122 | float: left;
123 | }
124 |
125 | footer {
126 | margin-top: 50px;
127 | background: #333;
128 | overflow: auto;
129 | padding-bottom: 40px;
130 | }
131 |
132 | footer section {
133 | min-width: 240px;
134 | float: left;
135 | }
136 |
137 | footer ul {
138 | list-style-type: none;
139 | padding: 0;
140 | margin: 0;
141 | }
142 |
143 | footer a {
144 | color: #FFF;
145 | }
146 |
147 | footer h2 {
148 | color: #999;
149 | line-height: 35px;
150 | font-weight: normal;
151 | margin-bottom: 0px;
152 | }
153 |
154 | #content {
155 | overflow: auto;
156 | }
157 |
158 | #download {
159 | float: left;
160 | }
161 |
162 | #download a {
163 | display: block;
164 | background: #000;
165 | margin-top: 17px;
166 | margin-left: 20px;
167 | line-height: 30px;
168 | padding: 3px 15px;
169 | border-radius: 5px;
170 | }
171 |
172 | #learn-more {
173 | display: block;
174 | width: 100px;
175 | margin: 20px auto;
176 | padding: 10px;
177 | border-radius: 10px;
178 | color: white;
179 | }
180 |
181 | #logos a {
182 | transition: opacity 0.5s;
183 | opacity: 0.3;
184 | display: block;
185 | text-indent: -9999px;
186 | width: 64px;
187 | height: 64px;
188 | float: left;
189 | margin-top: 20px;
190 | margin-right: 10px;
191 | background-size: 100%;
192 | }
193 |
194 | #couchdb_logo {
195 | background: url(couchdb-icon-64px.png);
196 | }
197 |
198 | #github_logo {
199 | background: url(GitHub-Mark-64px.png);
200 | }
201 | #twitter_logo {
202 | background: url(twitter-bird-light-bgs.png);
203 | }
204 |
205 | #logos a:hover {
206 | opacity: 1;
207 | }
208 |
209 |
210 | /*** PAGE LAYOUT - HOME ***/
211 |
212 | #the_database_that_syncs {
213 | font-weight: normal;
214 | font-size: 36px;
215 | text-align: center;
216 | line-height: 60px;
217 | }
218 |
219 | #home1 {
220 | margin-top: 20px;
221 | overflow: auto;
222 | position: relative;
223 | }
224 |
225 | #home1 section {
226 | width: 50%;
227 | float: left;
228 | }
229 |
230 | #home1 section:first-child {
231 | width: calc(50% - 20px);
232 | padding-right: 20px;
233 | }
234 |
235 | #home2 {
236 | margin-top: 30px;
237 | overflow: auto;
238 | }
239 |
240 | #home2 section {
241 | box-sizing: border-box;
242 | width: 23%;
243 | padding: 5%;
244 | float: left;
245 | overflow: hidden;
246 | text-align: center;
247 | }
248 |
249 | #home2 section:last-child {
250 | border-right: 0;
251 | }
252 |
253 | #home2 section h2 {
254 | margin-top: 0;
255 | text-align: center;
256 | font-weight: normal;
257 | }
258 |
259 | #download {
260 | text-align: left;
261 | }
262 |
263 | #home1 ul {
264 | padding: 0px 20px 0px 0;
265 | list-style-type: none;
266 | width: 450px;
267 | }
268 | #home1 li:first-child {
269 | border-top: 0;
270 | }
271 | #home1 li {
272 | border-top: 1px solid #CCC;
273 | padding: 5px 0;
274 | }
275 | #home1 small {
276 | width: 100px;
277 | display: block;
278 | float: right;
279 | text-align: right;
280 | }
281 | #home1 strong {
282 | font-weight: normal;
283 | font-size: 16px;
284 | text-transform: uppercase;
285 | }
286 |
287 | #news {
288 | position: absolute;
289 | bottom: 0;
290 | }
291 |
292 | /*** PAGE LAYOUT - LEARN ***/
293 |
294 | #learn-nav {
295 | width: 200px;
296 | float: left;
297 | }
298 |
299 | #learn-content {
300 | width: 700px;
301 | float: right;
302 | }
303 |
304 | #learn-nav ul {
305 | padding: 0;
306 | list-style-type: none;
307 | border-top: 1px solid #CCC;
308 | }
309 |
310 | #learn-nav a {
311 | display: block;
312 | line-height: 30px;
313 | border-style: solid;
314 | border-width: 0 1px 1px 1px;
315 | border-color: #CCC;
316 | text-indent: 10px;
317 | color: #666;
318 | }
319 |
320 | #learn-nav a:hover {
321 | background: #EEE;
322 | }
--------------------------------------------------------------------------------
/tests/test.auth_replication.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var remote = {host: 'localhost:2020'};
4 | var local = 'test_suite_db';
5 |
6 | if (typeof module !== undefined && module.exports) {
7 | var PouchDB = require('../lib');
8 | var testUtils = require('./test.utils.js');
9 | }
10 |
11 | QUnit.module('auth_replication', {
12 | setup: function () {
13 | this.name = local;
14 | this.remote = 'http://' + remote.host + '/test_suite_db/';
15 | },
16 | teardown: function() {
17 | if (!testUtils.PERSIST_DATABASES) {
18 | Pouch.destroy(this.name);
19 | Pouch.destroy(this.remote);
20 | }
21 | }
22 | });
23 |
24 | function login(username, password, callback) {
25 | Pouch.ajax({
26 | type: 'POST',
27 | url: 'http://' + remote.host + '/_session',
28 | data: {name: username, password: password},
29 | beforeSend: function(xhr) {
30 | xhr.setRequestHeader('Accept', 'application/json');
31 | },
32 | success: function () {
33 | callback();
34 | },
35 | error: function (err) {
36 | callback(err);
37 | }
38 | });
39 | }
40 |
41 | function logout(callback) {
42 | Pouch.ajax({
43 | type: 'DELETE',
44 | url: 'http://' + remote.host + '/_session',
45 | success: function () {
46 | callback();
47 | },
48 | error: function (err) {
49 | callback(err);
50 | }
51 | });
52 | }
53 |
54 | function createAdminUser(callback) {
55 | // create admin user
56 | var adminuser = {
57 | _id: 'org.couchdb.user:adminuser',
58 | name: 'adminuser',
59 | type: 'user',
60 | password: 'password',
61 | roles: []
62 | };
63 |
64 | Pouch.ajax({
65 | url: 'http://' + remote.host + '/_config/admins/adminuser',
66 | type: 'PUT',
67 | data: JSON.stringify(adminuser.password),
68 | contentType: 'application/json',
69 | success: function () {
70 | setTimeout(function() {
71 | login('adminuser', 'password', function (err) {
72 | if (err) {
73 | return callback(err);
74 | }
75 | Pouch.ajax({
76 | url: 'http://' + remote.host + '/_users/' +
77 | 'org.couchdb.user%3Aadminuser',
78 | type: 'PUT',
79 | data: JSON.stringify(adminuser),
80 | contentType: 'application/json',
81 | dataType: 'json',
82 | success: function (data) {
83 | logout(function (err) {
84 | if (err) {
85 | return callback(err);
86 | }
87 | callback(null, adminuser);
88 | });
89 | },
90 | error: function (err) {
91 | callback(null, adminuser);
92 | }
93 | });
94 | });
95 | }, 500);
96 | },
97 | error: function (err) {
98 | callback(err);
99 | }
100 | });
101 | }
102 |
103 | function deleteAdminUser(adminuser, callback) {
104 | Pouch.ajax({
105 | type: 'DELETE',
106 | beforeSend: function (xhr) {
107 | var token = btoa('adminuser:password');
108 | xhr.setRequestHeader("Authorization", "Basic " + token);
109 | },
110 | url: 'http://' + remote.host + '/_config/admins/adminuser',
111 | contentType: 'application/json',
112 | success: function () {
113 | var adminUrl = 'http://' + remote.host + '/_users/' +
114 | 'org.couchdb.user%3Aadminuser';
115 | Pouch.ajax({
116 | type: 'GET',
117 | url: adminUrl,
118 | dataType: 'json',
119 | success: function(doc) {
120 | Pouch.ajax({
121 | type: 'DELETE',
122 | url: 'http://' + remote.host + '/_users/' +
123 | 'org.couchdb.user%3Aadminuser?rev=' + doc._rev,
124 | contentType: 'application/json',
125 | success: function () {
126 | callback();
127 | },
128 | error: function (err) {
129 | callback();
130 | }
131 | });
132 | },
133 | error: function() {
134 | callback();
135 | }
136 | });
137 | },
138 | error: function (err) {
139 | callback(err);
140 | }
141 | });
142 | }
143 |
144 | asyncTest("Replicate from DB as non-admin user", function() {
145 | // SEE: https://github.com/apache/couchdb/blob/master/share/www/script/couch_test_runner.js
146 | // - create new DB
147 | // - push docs to new DB
148 | // - add new admin user
149 | // - login as new admin user
150 | // - add new user (non admin)
151 | // - login as new user
152 | // - replicate from new DB
153 | // - login as admin user
154 | // - delete users and return to admin party
155 | // - delete original DB
156 |
157 | var self = this;
158 |
159 | var docs = [
160 | {_id: 'one', count: 1},
161 | {_id: 'two', count: 2}
162 | ];
163 |
164 | function cleanup() {
165 | deleteAdminUser(self.adminuser, function (err) {
166 | if (err) {
167 | console.error(err);
168 | }
169 | logout(function (err) {
170 | if (err) {
171 | console.error(err);
172 | }
173 | start();
174 | });
175 | });
176 | }
177 |
178 | initDBPair(self.name, self.remote, function(db, remote) {
179 |
180 | // add user
181 | createAdminUser(function (err, adminuser) {
182 | if (err) {
183 | ok(false, 'unable to create admin user');
184 | console.error(err);
185 | return cleanup();
186 | }
187 |
188 | self.adminuser = adminuser;
189 |
190 | login('adminuser', 'password', function (err) {
191 | if (err) {
192 | console.error(err);
193 | }
194 | remote.bulkDocs({docs: docs}, {}, function(err, results) {
195 | Pouch.replicate(self.remote, self.name, {}, function(err, result) {
196 | db.allDocs(function(err, result) {
197 | ok(result.rows.length === docs.length, 'correct # docs exist');
198 | cleanup();
199 | });
200 | });
201 | });
202 | });
203 | });
204 | });
205 |
206 | });
207 |
--------------------------------------------------------------------------------
/tests/qunit/junitlogger.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var count = 0, suiteCount = 0, currentSuite, currentTest, suites = [], assertCount, start, results = {failed:0, passed:0, total:0, time:0};
3 |
4 | QUnit.jUnitReport = function(data) {
5 | // Gets called when a report is generated
6 | };
7 |
8 | QUnit.moduleStart(function(data) {
9 | currentSuite = {
10 | name: data.name,
11 | tests: [],
12 | failures: 0,
13 | time: 0,
14 | stdout : '',
15 | stderr : ''
16 | };
17 |
18 | suites.push(currentSuite);
19 | });
20 |
21 | QUnit.moduleDone(function(data) {
22 | });
23 |
24 | QUnit.testStart(function(data) {
25 | if(!start){ start = new Date(); }
26 |
27 | assertCount = 0;
28 |
29 | currentTest = {
30 | name: data.name,
31 | failures: [],
32 | start: new Date()
33 | };
34 |
35 | // Setup default suite if no module was specified
36 | if (!currentSuite) {
37 | currentSuite = {
38 | name: "default",
39 | tests: [],
40 | failures: 0,
41 | time: 0,
42 | stdout : '',
43 | stderr : ''
44 | };
45 |
46 | suites.push(currentSuite);
47 | }
48 |
49 | currentSuite.tests.push(currentTest);
50 | });
51 |
52 | QUnit.testDone(function(data) {
53 | currentTest.failed = data.failed;
54 | currentTest.total = data.total;
55 | currentSuite.failures += data.failed;
56 |
57 | results.failed += data.failed;
58 | results.passed += data.passed;
59 | results.total += data.total;
60 | });
61 |
62 | QUnit.log(function(data) {
63 | assertCount++;
64 |
65 | if (!data.result) {
66 | currentTest.failures.push(data.message);
67 |
68 | // Add log message of failure to make it easier to find in jenkins UI
69 | currentSuite.stdout += '[' + currentSuite.name + ', ' + currentTest.name + ', ' + assertCount + '] ' + data.message + '\n';
70 | }
71 | });
72 |
73 | QUnit.done(function(data) {
74 | function ISODateString(d) {
75 | function pad(n) {
76 | return n < 10 ? '0' + n : n;
77 | }
78 |
79 | return d.getUTCFullYear() + '-' +
80 | pad(d.getUTCMonth() + 1)+'-' +
81 | pad(d.getUTCDate()) + 'T' +
82 | pad(d.getUTCHours()) + ':' +
83 | pad(d.getUTCMinutes()) + ':' +
84 | pad(d.getUTCSeconds()) + 'Z';
85 | }
86 |
87 | // Generate XML report
88 | var i, ti, fi, test, suite,
89 | xmlWriter = new XmlWriter({
90 | linebreak_at : "testsuites,testsuite,testcase,failure,system-out,system-err"
91 | }),
92 | now = new Date();
93 |
94 | xmlWriter.start('testsuites');
95 |
96 | for (i = 0; i < suites.length; i++) {
97 | suite = suites[i];
98 |
99 | // Calculate time
100 | for (ti = 0; ti < suite.tests.length; ti++) {
101 | test = suite.tests[ti];
102 |
103 | test.time = (now.getTime() - test.start.getTime()) / 1000;
104 | suite.time += test.time;
105 | }
106 |
107 | xmlWriter.start('testsuite', {
108 | id: "" + i,
109 | name: suite.name,
110 | errors: "0",
111 | failures: suite.failures,
112 | hostname: "localhost",
113 | tests: suite.tests.length,
114 | time: Math.round(suite.time * 1000) / 1000,
115 | timestamp: ISODateString(now)
116 | });
117 |
118 | for (ti = 0; ti < suite.tests.length; ti++) {
119 | test = suite.tests[ti];
120 |
121 | xmlWriter.start('testcase', {
122 | name: test.name,
123 | total: test.total,
124 | failed: test.failed,
125 | time: Math.round(test.time * 1000) / 1000
126 | });
127 |
128 | for (fi = 0; fi < test.failures.length; fi++) {
129 | xmlWriter.start('failure', {type: "AssertionFailedError", message: test.failures[fi]}, true);
130 | }
131 |
132 | xmlWriter.end('testcase');
133 | }
134 |
135 | if (suite.stdout) {
136 | xmlWriter.start('system-out');
137 | xmlWriter.cdata('\n' + suite.stdout);
138 | xmlWriter.end('system-out');
139 | }
140 |
141 | if (suite.stderr) {
142 | xmlWriter.start('system-err');
143 | xmlWriter.cdata('\n' + suite.stderr);
144 | xmlWriter.end('system-err');
145 | }
146 |
147 | xmlWriter.end('testsuite');
148 | }
149 |
150 | xmlWriter.end('testsuites');
151 |
152 | results.time = new Date() - start;
153 |
154 | QUnit.jUnitReport({
155 | suites : suites,
156 | results:results,
157 | xml: xmlWriter.getString()
158 | });
159 | });
160 |
161 | function XmlWriter(settings) {
162 | function addLineBreak(name) {
163 | if (lineBreakAt[name] && data[data.length - 1] !== '\n') {
164 | data.push('\n');
165 | }
166 | }
167 |
168 | function makeMap(items, delim, map) {
169 | var i;
170 |
171 | items = items || [];
172 |
173 | if (typeof(items) === "string") {
174 | items = items.split(',');
175 | }
176 |
177 | map = map || {};
178 |
179 | i = items.length;
180 | while (i--) {
181 | map[items[i]] = {};
182 | }
183 |
184 | return map;
185 | }
186 |
187 | function encode(text) {
188 | var baseEntities = {
189 | '"' : '"',
190 | "'" : ''',
191 | '<' : '<',
192 | '>' : '>',
193 | '&' : '&'
194 | };
195 |
196 | return ('' + text).replace(/[<>&\"\']/g, function(chr) {
197 | return baseEntities[chr] || chr;
198 | });
199 | }
200 |
201 | var data = [], stack = [], lineBreakAt;
202 |
203 | settings = settings || {};
204 | lineBreakAt = makeMap(settings.linebreak_at || 'mytag');
205 |
206 | this.start = function(name, attrs, empty) {
207 | if (!empty) {
208 | stack.push(name);
209 | }
210 |
211 | data.push('<', name);
212 |
213 | for (var aname in attrs) {
214 | data.push(" " + encode(aname), '="', encode(attrs[aname]), '"');
215 | }
216 |
217 | data.push(empty ? ' />' : '>');
218 | addLineBreak(name);
219 | };
220 |
221 | this.end = function(name) {
222 | stack.pop();
223 | addLineBreak(name);
224 | data.push('', name, '>');
225 | addLineBreak(name);
226 | };
227 |
228 | this.text = function(text) {
229 | data.push(encode(text));
230 | };
231 |
232 | this.cdata = function(text) {
233 | data.push('');
234 | };
235 |
236 | this.comment = function(text) {
237 | data.push('');
238 | };
239 |
240 | this.pi = function(name, text) {
241 | if (text) {
242 | data.push('', name, ' ', text, '?>\n');
243 | } else {
244 | data.push('', name, '?>\n');
245 | }
246 | };
247 |
248 | this.doctype = function(text) {
249 | data.push('\n');
250 | };
251 |
252 | this.getString = function() {
253 | for (var i = stack.length - 1; i >= 0; i--) {
254 | this.end(stack[i]);
255 | }
256 |
257 | stack = [];
258 |
259 | return data.join('').replace(/\n$/, '');
260 | };
261 |
262 | this.reset = function() {
263 | data = [];
264 | stack = [];
265 | };
266 |
267 | this.pi(settings.xmldecl || 'xml version="1.0" encoding="UTF-8"');
268 | }
269 | })();
--------------------------------------------------------------------------------
/lib/setup.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var PouchDB = require("./constructor");
4 |
5 | PouchDB.adapters = {};
6 | PouchDB.plugins = {};
7 |
8 | PouchDB.prefix = '_pouch_';
9 |
10 | PouchDB.parseAdapter = function (name) {
11 | var match = name.match(/([a-z\-]*):\/\/(.*)/);
12 | var adapter;
13 | if (match) {
14 | // the http adapter expects the fully qualified name
15 | name = /http(s?)/.test(match[1]) ? match[1] + '://' + match[2] : match[2];
16 | adapter = match[1];
17 | if (!PouchDB.adapters[adapter].valid()) {
18 | throw 'Invalid adapter';
19 | }
20 | return {name: name, adapter: match[1]};
21 | }
22 |
23 | var preferredAdapters = ['idb', 'leveldb', 'websql'];
24 | for (var i = 0; i < preferredAdapters.length; ++i) {
25 | if (preferredAdapters[i] in PouchDB.adapters) {
26 | adapter = PouchDB.adapters[preferredAdapters[i]];
27 | var use_prefix = 'use_prefix' in adapter ? adapter.use_prefix : true;
28 |
29 | return {
30 | name: use_prefix ? PouchDB.prefix + name : name,
31 | adapter: preferredAdapters[i]
32 | };
33 | }
34 | }
35 |
36 | throw 'No valid adapter found';
37 | };
38 |
39 | PouchDB.destroy = function (name, opts, callback) {
40 | if (typeof opts === 'function' || typeof opts === 'undefined') {
41 | callback = opts;
42 | opts = {};
43 | }
44 |
45 | if (typeof name === 'object') {
46 | opts = name;
47 | name = undefined;
48 | }
49 |
50 | if (typeof callback === 'undefined') {
51 | callback = function () {};
52 | }
53 | var backend = PouchDB.parseAdapter(opts.name || name);
54 | var dbName = backend.name;
55 |
56 | var cb = function (err, response) {
57 | if (err) {
58 | callback(err);
59 | return;
60 | }
61 |
62 | for (var plugin in PouchDB.plugins) {
63 | PouchDB.plugins[plugin]._delete(dbName);
64 | }
65 | //console.log(dbName + ': Delete Database');
66 |
67 | // call destroy method of the particular adaptor
68 | PouchDB.adapters[backend.adapter].destroy(dbName, opts, callback);
69 | };
70 |
71 | // remove PouchDB from allDBs
72 | PouchDB.removeFromAllDbs(backend, cb);
73 | };
74 |
75 | PouchDB.removeFromAllDbs = function (opts, callback) {
76 | // Only execute function if flag is enabled
77 | if (!PouchDB.enableAllDbs) {
78 | callback();
79 | return;
80 | }
81 |
82 | // skip http and https adaptors for allDbs
83 | var adapter = opts.adapter;
84 | if (adapter === "http" || adapter === "https") {
85 | callback();
86 | return;
87 | }
88 |
89 | // remove db from PouchDB.ALL_DBS
90 | new PouchDB(PouchDB.allDBName(opts.adapter), function (err, db) {
91 | if (err) {
92 | // don't fail when allDbs fail
93 | //console.error(err);
94 | callback();
95 | return;
96 | }
97 | // check if db has been registered in PouchDB.ALL_DBS
98 | var dbname = PouchDB.dbName(opts.adapter, opts.name);
99 | db.get(dbname, function (err, doc) {
100 | if (err) {
101 | callback();
102 | } else {
103 | db.remove(doc, function (err, response) {
104 | if (err) {
105 | //console.error(err);
106 | }
107 | callback();
108 | });
109 | }
110 | });
111 | });
112 |
113 | };
114 |
115 | PouchDB.adapter = function (id, obj) {
116 | if (obj.valid()) {
117 | PouchDB.adapters[id] = obj;
118 | }
119 | };
120 |
121 | PouchDB.plugin = function (id, obj) {
122 | PouchDB.plugins[id] = obj;
123 | };
124 |
125 | // flag to toggle allDbs (off by default)
126 | PouchDB.enableAllDbs = false;
127 |
128 | // name of database used to keep track of databases
129 | PouchDB.ALL_DBS = "_allDbs";
130 | PouchDB.dbName = function (adapter, name) {
131 | return [adapter, "-", name].join('');
132 | };
133 | PouchDB.realDBName = function (adapter, name) {
134 | return [adapter, "://", name].join('');
135 | };
136 | PouchDB.allDBName = function (adapter) {
137 | return [adapter, "://", PouchDB.prefix + PouchDB.ALL_DBS].join('');
138 | };
139 |
140 | PouchDB.open = function (opts, callback) {
141 | // Only register pouch with allDbs if flag is enabled
142 | if (!PouchDB.enableAllDbs) {
143 | callback();
144 | return;
145 | }
146 |
147 | var adapter = opts.adapter;
148 | // skip http and https adaptors for allDbs
149 | if (adapter === "http" || adapter === "https") {
150 | callback();
151 | return;
152 | }
153 |
154 | new PouchDB(PouchDB.allDBName(adapter), function (err, db) {
155 | if (err) {
156 | // don't fail when allDb registration fails
157 | //console.error(err);
158 | callback();
159 | return;
160 | }
161 |
162 | // check if db has been registered in PouchDB.ALL_DBS
163 | var dbname = PouchDB.dbName(adapter, opts.name);
164 | db.get(dbname, function (err, response) {
165 | if (err && err.status === 404) {
166 | db.put({
167 | _id: dbname,
168 | dbname: opts.originalName
169 | }, function (err) {
170 | if (err) {
171 | //console.error(err);
172 | }
173 |
174 | callback();
175 | });
176 | } else {
177 | callback();
178 | }
179 | });
180 | });
181 | };
182 |
183 | PouchDB.allDbs = function (callback) {
184 | var accumulate = function (adapters, all_dbs) {
185 | if (adapters.length === 0) {
186 | // remove duplicates
187 | var result = [];
188 | all_dbs.forEach(function (doc) {
189 | var exists = result.some(function (db) {
190 | return db.id === doc.id;
191 | });
192 |
193 | if (!exists) {
194 | result.push(doc);
195 | }
196 | });
197 |
198 | // return an array of dbname
199 | callback(null, result.map(function (row) {
200 | return row.doc.dbname;
201 | }));
202 | return;
203 | }
204 |
205 | var adapter = adapters.shift();
206 |
207 | // skip http and https adaptors for allDbs
208 | if (adapter === "http" || adapter === "https") {
209 | accumulate(adapters, all_dbs);
210 | return;
211 | }
212 |
213 | new PouchDB(PouchDB.allDBName(adapter), function (err, db) {
214 | if (err) {
215 | callback(err);
216 | return;
217 | }
218 | db.allDocs({include_docs: true}, function (err, response) {
219 | if (err) {
220 | callback(err);
221 | return;
222 | }
223 |
224 | // append from current adapter rows
225 | all_dbs.unshift.apply(all_dbs, response.rows);
226 |
227 | // code to clear allDbs.
228 | // response.rows.forEach(function (row) {
229 | // db.remove(row.doc, function () {
230 | // //console.log(arguments);
231 | // });
232 | // });
233 |
234 | // recurse
235 | accumulate(adapters, all_dbs);
236 | });
237 | });
238 | };
239 | var adapters = Object.keys(PouchDB.adapters);
240 | accumulate(adapters, []);
241 | };
242 |
243 | module.exports = PouchDB;
244 |
--------------------------------------------------------------------------------
/lib/deps/ajax.js:
--------------------------------------------------------------------------------
1 | var request = require('request');
2 | var extend = require('./extend.js');
3 | var createBlob = require('./blob.js');
4 | var errors = require('./errors');
5 | function ajax(options, callback) {
6 |
7 | if (typeof options === "function") {
8 | callback = options;
9 | options = {};
10 | }
11 |
12 | function call(fun) {
13 | var args = Array.prototype.slice.call(arguments, 1);
14 | if (typeof fun === typeof Function) {
15 | fun.apply(this, args);
16 | }
17 | };
18 |
19 | var defaultOptions = {
20 | method : "GET",
21 | headers: {},
22 | json: true,
23 | processData: true,
24 | timeout: 10000
25 | };
26 |
27 | options = extend(true, defaultOptions, options);
28 |
29 |
30 | function onSuccess(obj, resp, cb) {
31 | if (!options.binary && !options.json && options.processData &&
32 | typeof obj !== 'string') {
33 | obj = JSON.stringify(obj);
34 | } else if (!options.binary && options.json && typeof obj === 'string') {
35 | try {
36 | obj = JSON.parse(obj);
37 | } catch (e) {
38 | // Probably a malformed JSON from server
39 | call(cb, e);
40 | return;
41 | }
42 | }
43 | if (Array.isArray(obj)) {
44 | obj = obj.map(function(v) {
45 | var obj;
46 | if (v.ok) {
47 | return v;
48 | } else if (v.error&&v.error==='conflict') {
49 | obj = errors.REV_CONFLICT;
50 | obj.id = v.id;
51 | return obj;
52 | } else if (v.missing) {
53 | obj = errors.MISSING_DOC;
54 | obj.missing = v.missing;
55 | return obj;
56 | }
57 | });
58 | }
59 | call(cb, null, obj, resp);
60 | };
61 |
62 | function onError(err, cb){
63 | var errParsed, errObj, errType, key;
64 | try {
65 | errParsed = JSON.parse(err.responseText);
66 | //would prefer not to have a try/catch clause
67 | for(key in errors){
68 | if(errors[key].name === errParsed.error){
69 | errType = errors[key];
70 | break;
71 | }
72 | }
73 | errType = errType || errors.UNKNOWN_ERROR;
74 | errObj = errors.error(errType, errParsed.reason);
75 | } catch(e) {
76 | errObj = errors.UNKNOWN_ERROR;
77 | }
78 | call(cb, errObj);
79 | };
80 |
81 | if (process.browser && typeof XMLHttpRequest !== 'undefined') {
82 | var timer, timedout = false;
83 | var xhr = new XMLHttpRequest();
84 |
85 | xhr.open(options.method, options.url);
86 | xhr.withCredentials = true;
87 |
88 | if (options.json) {
89 | options.headers.Accept = 'application/json';
90 | options.headers['Content-Type'] = options.headers['Content-Type'] ||
91 | 'application/json';
92 | if (options.body && options.processData && typeof options.body !== "string") {
93 | options.body = JSON.stringify(options.body);
94 | }
95 | }
96 |
97 | if (options.binary) {
98 | xhr.responseType = 'arraybuffer';
99 | }
100 |
101 | function createCookie(name,value,days) {
102 | if (days) {
103 | var date = new Date();
104 | date.setTime(date.getTime()+(days*24*60*60*1000));
105 | var expires = "; expires="+date.toGMTString();
106 | } else {
107 | var expires = "";
108 | }
109 | document.cookie = name+"="+value+expires+"; path=/";
110 | }
111 |
112 | for (var key in options.headers) {
113 | if (key === 'Cookie') {
114 | var cookie = options.headers[key].split('=');
115 | createCookie(cookie[0], cookie[1], 10);
116 | } else {
117 | xhr.setRequestHeader(key, options.headers[key]);
118 | }
119 | }
120 |
121 | if (!("body" in options)) {
122 | options.body = null;
123 | }
124 |
125 | function abortReq() {
126 | timedout=true;
127 | xhr.abort();
128 | call(onError, xhr, callback);
129 | };
130 |
131 | xhr.onreadystatechange = function () {
132 | if (xhr.readyState !== 4 || timedout) {
133 | return;
134 | }
135 | clearTimeout(timer);
136 | if (xhr.status >= 200 && xhr.status < 300) {
137 | var data;
138 | if (options.binary) {
139 | data = createBlob([xhr.response || ''], {
140 | type: xhr.getResponseHeader('Content-Type')
141 | });
142 | } else {
143 | data = xhr.responseText;
144 | }
145 | call(onSuccess, data, xhr, callback);
146 | } else {
147 | call(onError, xhr, callback);
148 | }
149 | };
150 |
151 | if (options.timeout > 0) {
152 | timer = setTimeout(abortReq, options.timeout);
153 | xhr.upload.onprogress = xhr.onprogress = function() {
154 | clearTimeout(timer);
155 | timer = setTimeout(abortReq, options.timeout);
156 | };
157 | }
158 | xhr.send(options.body);
159 | return {abort:abortReq};
160 |
161 | } else {
162 |
163 | if (options.json) {
164 | if (!options.binary) {
165 | options.headers.Accept = 'application/json';
166 | }
167 | options.headers['Content-Type'] = options.headers['Content-Type'] ||
168 | 'application/json';
169 | }
170 |
171 | if (options.binary) {
172 | options.encoding = null;
173 | options.json = false;
174 | }
175 |
176 | if (!options.processData) {
177 | options.json = false;
178 | }
179 |
180 | return request(options, function (err, response, body) {
181 | if (err) {
182 | err.status = response ? response.statusCode : 400;
183 | return call(onError, err, callback);
184 | }
185 | var error;
186 | var content_type = response.headers['content-type'];
187 | var data = (body || '');
188 |
189 | // CouchDB doesn't always return the right content-type for JSON data, so
190 | // we check for ^{ and }$ (ignoring leading/trailing whitespace)
191 | if (!options.binary && (options.json || !options.processData) &&
192 | typeof data !== 'object' &&
193 | (/json/.test(content_type) ||
194 | (/^[\s]*\{/.test(data) && /\}[\s]*$/.test(data)))) {
195 | data = JSON.parse(data);
196 | }
197 |
198 | if (response.statusCode >= 200 && response.statusCode < 300) {
199 | call(onSuccess, data, response, callback);
200 | }
201 | else {
202 | if (options.binary) {
203 | data = JSON.parse(data.toString());
204 | }
205 | if (data.reason === 'missing') {
206 | error = errors.MISSING_DOC;
207 | } else if (data.reason === 'no_db_file') {
208 | error = errors.error(errors.DB_MISSING, data.reason);
209 | } else if (data.error === 'conflict'){
210 | error = errors.REV_CONFLICT;
211 | } else {
212 | error = errors.error(errors.UNKNOWN_ERROR, data.reason, data.error);
213 | }
214 | error.status = response.statusCode;
215 | call(callback, error);
216 | }
217 | });
218 | }
219 | };
220 |
221 | module.exports = ajax;
222 |
--------------------------------------------------------------------------------
/lib/deps/md5.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * MD5 (Message-Digest Algorithm)
4 | *
5 | * For original source see http://www.webtoolkit.info/
6 | * Download: 15.02.2009 from http://www.webtoolkit.info/javascript-md5.html
7 | *
8 | * Licensed under CC-BY 2.0 License
9 | * (http://creativecommons.org/licenses/by/2.0/uk/)
10 | *
11 | **/
12 | var crypto = require('crypto');
13 |
14 | exports.MD5 = function(string) {
15 | if (!process.browser) {
16 | return crypto.createHash('md5').update(string).digest('hex');
17 | }
18 | function RotateLeft(lValue, iShiftBits) {
19 | return (lValue<>>(32-iShiftBits));
20 | }
21 |
22 | function AddUnsigned(lX,lY) {
23 | var lX4,lY4,lX8,lY8,lResult;
24 | lX8 = (lX & 0x80000000);
25 | lY8 = (lY & 0x80000000);
26 | lX4 = (lX & 0x40000000);
27 | lY4 = (lY & 0x40000000);
28 | lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF);
29 | if (lX4 & lY4) {
30 | return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
31 | }
32 | if (lX4 | lY4) {
33 | if (lResult & 0x40000000) {
34 | return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
35 | } else {
36 | return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
37 | }
38 | } else {
39 | return (lResult ^ lX8 ^ lY8);
40 | }
41 | }
42 |
43 | function F(x,y,z) { return (x & y) | ((~x) & z); }
44 | function G(x,y,z) { return (x & z) | (y & (~z)); }
45 | function H(x,y,z) { return (x ^ y ^ z); }
46 | function I(x,y,z) { return (y ^ (x | (~z))); }
47 |
48 | function FF(a,b,c,d,x,s,ac) {
49 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
50 | return AddUnsigned(RotateLeft(a, s), b);
51 | };
52 |
53 | function GG(a,b,c,d,x,s,ac) {
54 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
55 | return AddUnsigned(RotateLeft(a, s), b);
56 | };
57 |
58 | function HH(a,b,c,d,x,s,ac) {
59 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
60 | return AddUnsigned(RotateLeft(a, s), b);
61 | };
62 |
63 | function II(a,b,c,d,x,s,ac) {
64 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
65 | return AddUnsigned(RotateLeft(a, s), b);
66 | };
67 |
68 | function ConvertToWordArray(string) {
69 | var lWordCount;
70 | var lMessageLength = string.length;
71 | var lNumberOfWords_temp1=lMessageLength + 8;
72 | var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64;
73 | var lNumberOfWords = (lNumberOfWords_temp2+1)*16;
74 | var lWordArray=Array(lNumberOfWords-1);
75 | var lBytePosition = 0;
76 | var lByteCount = 0;
77 | while ( lByteCount < lMessageLength ) {
78 | lWordCount = (lByteCount-(lByteCount % 4))/4;
79 | lBytePosition = (lByteCount % 4)*8;
80 | lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<>>29;
88 | return lWordArray;
89 | };
90 |
91 | function WordToHex(lValue) {
92 | var WordToHexValue="",WordToHexValue_temp="",lByte,lCount;
93 | for (lCount = 0;lCount<=3;lCount++) {
94 | lByte = (lValue>>>(lCount*8)) & 255;
95 | WordToHexValue_temp = "0" + lByte.toString(16);
96 | WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2);
97 | }
98 | return WordToHexValue;
99 | };
100 |
101 | //** function Utf8Encode(string) removed. Aready defined in pidcrypt_utils.js
102 |
103 | var x=Array();
104 | var k,AA,BB,CC,DD,a,b,c,d;
105 | var S11=7, S12=12, S13=17, S14=22;
106 | var S21=5, S22=9 , S23=14, S24=20;
107 | var S31=4, S32=11, S33=16, S34=23;
108 | var S41=6, S42=10, S43=15, S44=21;
109 |
110 | // string = Utf8Encode(string); #function call removed
111 |
112 | x = ConvertToWordArray(string);
113 |
114 | a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
115 |
116 | for (k=0;k -1) {
210 | asyncTest('Auto-compaction test', function() {
211 | testUtils.initTestDB(this.name, {auto_compaction: true}, function(err, db) {
212 | var doc = {_id: "doc", val: "1"};
213 | db.post(doc, function(err, res) {
214 | var rev1 = res.rev;
215 | doc._rev = rev1;
216 | doc.val = "2";
217 | db.post(doc, function(err, res) {
218 | var rev2 = res.rev;
219 | doc._rev = rev2;
220 | doc.val = "3";
221 | db.post(doc, function(err, res) {
222 | var rev3 = res.rev;
223 | db.get("doc", {rev: rev1}, function(err, doc) {
224 | strictEqual(err.status, 404, "compacted document is missing");
225 | strictEqual(err.name, "not_found", "compacted document is missing");
226 | db.get("doc", {rev: rev2}, function(err, doc) {
227 | ok(!err, "leaf's parent does not get compacted");
228 | db.get("doc", {rev: rev3}, function(err, doc) {
229 | ok(!err, "leaf revision does not get compacted");
230 | start();
231 | });
232 | });
233 | });
234 | });
235 | });
236 | });
237 | });
238 | });
239 | }
240 | });
241 |
--------------------------------------------------------------------------------
/test/integration/basics_test.js:
--------------------------------------------------------------------------------
1 | /*globals require */
2 |
3 | 'use strict';
4 |
5 | var PouchDB = require('../../');
6 | var utils = require('../test.utils.js');
7 | var opts = require('browserify-getopts');
8 |
9 | var db1 = opts.db1 || 'testdb';
10 | var test = require('wrapping-tape')(utils.setupDb(db1));
11 |
12 | test('Create a Pouch', 1, function(t) {
13 | new PouchDB(db1, function(err, db) {
14 | t.ok(!err, 'Created');
15 | });
16 | });
17 |
18 | test('Remove a Pouch', 1, function(t) {
19 | new PouchDB(db1, function(err, db) {
20 | PouchDB.destroy(db1, function(err, db) {
21 | t.ok(!err, 'Deleted database');
22 | });
23 | });
24 | });
25 |
26 | test('Post a document', 1, function(t) {
27 | var db = new PouchDB(db1);
28 | db.post({a: 'doc'}, function(err, res) {
29 | t.notOk(err, 'No error posting docs');
30 | });
31 | });
32 |
33 | test('Modify a doc', 1, function(t) {
34 | var db = new PouchDB(db1);
35 | db.post({test: 'somestuff'}, function(err, info) {
36 | db.put({_id: info.id, _rev: info.rev, another: 'test'}, function(err, info2) {
37 | t.ok(!err && info2.rev !== info._rev, 'updated a doc with put');
38 | });
39 | });
40 | });
41 |
42 | test('Read db id', 1, function(t) {
43 | new PouchDB(db1, function(err, db) {
44 | t.equal(typeof db.id(), 'string', 'got db id');
45 | });
46 | });
47 |
48 | test('Close db', 1, function(t) {
49 | new PouchDB(db1, function(err, db) {
50 | db.close(function(err) {
51 | t.ok(!err, 'Close database');
52 | });
53 | });
54 | });
55 |
56 | test('Read db after closing', 2, function(t) {
57 | new PouchDB(db1, function(err, db) {
58 | t.equal(typeof db.id(), 'string', 'got db id');
59 | db.close(function(err) {
60 | new PouchDB(db1, function(err, db) {
61 | t.equal(typeof db.id(), 'string', 'got db id');
62 | });
63 | });
64 | });
65 | });
66 |
67 | test('Modify doc with incorrect rev', 1, function(t) {
68 | var db = new PouchDB(db1);
69 | db.post({a: 'doc'}, function(err, info) {
70 | var nDoc = {_id: info.id, _rev: info.rev + 'broken', another: 'test'};
71 | db.put(nDoc, function(err, info2) {
72 | t.ok(err, 'put was denied');
73 | });
74 | });
75 | });
76 |
77 | test('Remove doc', 1, function(t) {
78 | var db = new PouchDB(db1);
79 | db.put({_id: 'foo', value: 'test'}, function(err, res) {
80 | db.get('foo', function(err, doc) {
81 | db.remove(doc, function(err, res) {
82 | db.get('foo', {rev: res.rev}, function(err, doc) {
83 | t.deepEqual(doc, {_id: res.id, _rev: res.rev, _deleted: true},
84 | 'removal left only stub');
85 | });
86 | });
87 | });
88 | });
89 | });
90 |
91 | test('remove doc twice', 4, function(t) {
92 | var db = new PouchDB(db1);
93 | db.put({_id:"specifiedId", test:"somestuff"}, function(err, info) {
94 | db.get("specifiedId", function(err, doc) {
95 | t.ok(doc.test, "Put and got doc");
96 | db.remove(doc, function(err, response) {
97 | t.ok(!err, "Removed doc");
98 | db.put({_id:"specifiedId", test:"somestuff2"}, function(err, info) {
99 | db.get("specifiedId", function(err, doc){
100 | t.ok(doc, "Put and got doc again");
101 | db.remove(doc, function(err, response) {
102 | t.ok(!err, "Removed doc again");
103 | });
104 | });
105 | });
106 | });
107 | });
108 | });
109 | });
110 |
111 | test('Remove doc no callback', 1, function(t) {
112 | new PouchDB(db1, function(err, db) {
113 | var changes = db.changes({
114 | continuous: true,
115 | include_docs: true,
116 | onChange: function(change) {
117 | if (change.doc._deleted) {
118 | changes.cancel();
119 | t.ok(true, 'doc deleted');
120 | }
121 | }
122 | });
123 |
124 | db.post({_id:"somestuff"}, function (err, res) {
125 | db.remove({_id: res.id, _rev: res.rev});
126 | });
127 | });
128 | });
129 |
130 | test('Delete document without id', 1, function(t) {
131 | var db = new PouchDB(db1);
132 | db.remove({test: 'ing'}, function(err) {
133 | t.ok(err, 'failed to deleted');
134 | });
135 | });
136 |
137 | test('Bulk docs', 2, function(t) {
138 | var db = new PouchDB(db1);
139 | db.bulkDocs({docs: [{test:"somestuff"}, {test:"another"}]}, function(err, infos) {
140 | t.ok(!infos[0].error);
141 | t.ok(!infos[1].error);
142 | });
143 | });
144 |
145 | test('Basic checks', 6, function(t) {
146 | var db = new PouchDB(db1);
147 | db.info(function(err, info) {
148 | var updateSeq = info.update_seq;
149 | var doc = {_id: '0', a: 1, b:1};
150 | t.equal(info.doc_count, 0, 'No docs');
151 | db.put(doc, function(err, res) {
152 | t.equal(res.ok, true, 'Put was ok');
153 | db.info(function(err, info) {
154 | t.equal(info.doc_count, 1);
155 | t.notEqual(info.update_seq, updateSeq , 'update seq changed');
156 | db.get(doc._id, function(err, doc) {
157 | t.ok(doc._id === res.id && doc._rev === res.rev, 'revs right');
158 | db.get(doc._id, {revs_info: true}, function(err, doc) {
159 | t.equal(doc._revs_info[0].status, 'available', 'rev status');
160 | });
161 | });
162 | });
163 | });
164 | });
165 | });
166 |
167 | test('doc validation', 2, function(t) {
168 | var bad_docs = [
169 | {"_zing": 4},
170 | {"_zoom": "hello"},
171 | {"zane": "goldfish", "_fan": "something smells delicious"},
172 | {"_bing": {"wha?": "soda can"}}
173 | ];
174 | var db = new PouchDB(db1);
175 | db.bulkDocs({docs: bad_docs}, function(err, res) {
176 | t.equal(err.status, 500);
177 | t.equal(err.error, 'doc_validation');
178 | });
179 | });
180 |
181 | test('testing issue #48', 1, function(t) {
182 | var docs = [{"id":"0"}, {"id":"1"}, {"id":"2"}, {"id":"3"}, {"id":"4"}, {"id":"5"}];
183 | var x = 0;
184 | var timer;
185 | var db = new PouchDB(db1);
186 | var save = function() {
187 | db.bulkDocs({docs: docs}, function(err, res) {
188 | if (++x === 10) {
189 | clearInterval(timer);
190 | t.ok(true, 'all updated succedded');
191 | }
192 | });
193 | };
194 | timer = setInterval(save, 50);
195 | });
196 |
197 | test('Testing valid id', 1, function(t) {
198 | new PouchDB(db1, function(err, db) {
199 | db.post({'_id': 123, test: "somestuff"}, function (err, info) {
200 | t.ok(err, 'id must be a string');
201 | });
202 | });
203 | });
204 |
205 | test('put without _id should fail', 1, function(t) {
206 | var db = new PouchDB(db1);
207 | db.put({test:"somestuff"}, function(err, info) {
208 | t.ok(err, '_id is required');
209 | });
210 | });
211 |
212 | test('update_seq persists', 2, function(t) {
213 | var db = new PouchDB(db1);
214 | db.post({test: 'sometuff'}, function(err, info) {
215 | var newDb = new PouchDB(db1);
216 | newDb.info(function(err, info) {
217 | t.notEqual(info.update, 0, 'update seq');
218 | t.equal(info.doc_count, 1, 'doc count persists');
219 | });
220 | });
221 | });
222 |
223 | test('deletions persist', 1, function(t) {
224 | var doc = {_id: 'staticId', contents: 'stuff'};
225 | function writeAndDelete(db, cb) {
226 | db.put(doc, function(err, info) {
227 | db.remove({_id:info.id, _rev:info.rev}, cb);
228 | });
229 | }
230 | var db = new PouchDB(db1);
231 | writeAndDelete(db, function() {
232 | writeAndDelete(db, function() {
233 | db.put(doc, function() {
234 | db.get(doc._id, {conflicts: true}, function(err, details) {
235 | t.equal(false, '_conflicts' in details, 'Should not have conflicts');
236 | });
237 | });
238 | });
239 | });
240 | });
241 |
242 | test('Error when document is not an object', 5, function(t) {
243 | var doc1 = [{_id: 'foo'}, {_id: 'bar'}];
244 | var doc2 = "this is not an object";
245 |
246 | var callback = function(err, resp) {
247 | t.ok(err, 'doc must be an object');
248 | };
249 |
250 | var db = new PouchDB(db1);
251 | db.post(doc1, callback);
252 | db.post(doc2, callback);
253 | db.put(doc1, callback);
254 | db.put(doc2, callback);
255 | db.bulkDocs({docs: [doc1, doc2]}, callback);
256 | });
257 |
258 | test('test instance update_seq updates correctly', 2, function(t) {
259 | new PouchDB(db1, function(err, a) {
260 | new PouchDB(db1, function(err, b) {
261 | a.post({a: 'doc'}, function(err, info) {
262 | a.info(function(err, db1Info) {
263 | b.info(function(err, db2Info) {
264 | t.notEqual(db1Info.update_seq, 0, 'Update seqs arent 0');
265 | t.notEqual(db2Info.update_seq, 0, 'Update seqs arent 0');
266 | });
267 | });
268 | });
269 | });
270 | });
271 | });
--------------------------------------------------------------------------------
/lib/plugins/pouchdb.spatial.js:
--------------------------------------------------------------------------------
1 | /*global Pouch: true */
2 |
3 | "use strict";
4 |
5 | // If we wanted to store incremental views we can do it here by listening
6 | // to the changes feed (keeping track of our last update_seq between page loads)
7 | // and storing the result of the map function (possibly using the upcoming
8 | // extracted adapter functions)
9 |
10 | var Spatial = function (db) {
11 |
12 | var isArray = Array.isArray || function (obj) {
13 | return type(obj) === "array";
14 | };
15 |
16 | function viewQuery(fun, options) {
17 | if (!options.complete) {
18 | return;
19 | }
20 |
21 | var results = [];
22 | var current = null;
23 | var num_started= 0;
24 | var completed= false;
25 |
26 | // Make the key a proper one. If a value is a single point, transform it
27 | // to a range. If the first element (or the whole key) is a geometry,
28 | // calculate its bounding box.
29 | // The geometry is also returned (`null` if there is none).
30 | var normalizeKey = function (key) {
31 | var newKey = [];
32 | var geometry = null;
33 |
34 | // Whole key is one geometry
35 | if (!isArray(key) && typeof key === "object") {
36 | return {
37 | key: Spatial.calculateBbox(key),
38 | geometry: key
39 | };
40 | }
41 |
42 | if (!isArray(key[0]) && typeof key[0] === "object") {
43 | newKey = Spatial.calculateBbox(key[0]);
44 | geometry = key[0];
45 | key = key.slice(1);
46 | }
47 |
48 | for(var i=0; i= start_range[i] || start_range[i] === null))
77 | // End is set
78 | || (end >= start_range[i] || start_range[i] === null))) {
79 | continue;
80 | } else {
81 | return false;
82 | }
83 | }
84 | return true;
85 | };
86 |
87 | var emit = function (key, val) {
88 | var keyGeom = normalizeKey(key);
89 | var viewRow = {
90 | id: current.doc._id,
91 | key: keyGeom.key,
92 | value: val,
93 | geometry: keyGeom.geometry
94 | };
95 |
96 | // If no range is given, return everything
97 | if (options.start_range !== undefined &&
98 | options.end_range !== undefined) {
99 | if (!within(keyGeom.key, options.start_range, options.end_range)) {
100 | return;
101 | }
102 | }
103 |
104 | num_started++;
105 | if (options.include_docs) {
106 | //in this special case, join on _id (issue #106)
107 | if (val && typeof val === 'object' && val._id){
108 | db.get(val._id,
109 | function (_, joined_doc){
110 | if (joined_doc) {
111 | viewRow.doc = joined_doc;
112 | }
113 | results.push(viewRow);
114 | checkComplete();
115 | });
116 | return;
117 | } else {
118 | viewRow.doc = current.doc;
119 | }
120 | }
121 | results.push(viewRow);
122 | };
123 |
124 | // ugly way to make sure references to 'emit' in map/reduce bind to the
125 | // above emit
126 | eval('fun = ' + fun.toString() + ';');
127 |
128 | // exclude _conflicts key by default
129 | // or to use options.conflicts if it's set when called by db.query
130 | var conflicts = ('conflicts' in options ? options.conflicts : false);
131 |
132 | // only proceed once all documents are mapped and joined
133 | var checkComplete= function () {
134 | if (completed && results.length == num_started){
135 | return options.complete(null, {rows: results});
136 | }
137 | }
138 |
139 | db.changes({
140 | conflicts: conflicts,
141 | include_docs: true,
142 | onChange: function (doc) {
143 | // Don't index deleted or design documents
144 | if (!('deleted' in doc) && doc.id.indexOf('_design/') !== 0) {
145 | current = {doc: doc.doc};
146 | fun.call(this, doc.doc);
147 | }
148 | },
149 | complete: function () {
150 | completed= true;
151 | checkComplete();
152 | }
153 | });
154 | }
155 |
156 | function httpQuery(location, opts, callback) {
157 |
158 | // List of parameters to add to the PUT request
159 | var params = [];
160 |
161 | // TODO vmx 2013-01-27: Support skip and limit
162 |
163 | if (typeof opts.start_range !== 'undefined') {
164 | params.push('start_range=' + encodeURIComponent(JSON.stringify(
165 | opts.start_range)));
166 | }
167 | if (typeof opts.end_range !== 'undefined') {
168 | params.push('end_range=' + encodeURIComponent(JSON.stringify(
169 | opts.end_range)));
170 | }
171 | if (typeof opts.key !== 'undefined') {
172 | params.push('key=' + encodeURIComponent(JSON.stringify(opts.key)));
173 | }
174 |
175 | // Format the list of parameters into a valid URI query string
176 | params = params.join('&');
177 | params = params === '' ? '' : '?' + params;
178 |
179 | // We are referencing a query defined in the design doc
180 | var parts = location.split('/');
181 | db.request({
182 | method: 'GET',
183 | url: '_design/' + parts[0] + '/_spatial/' + parts[1] + params
184 | }, callback);
185 | }
186 |
187 | function query(fun, opts, callback) {
188 | if (typeof opts === 'function') {
189 | callback = opts;
190 | opts = {};
191 | }
192 |
193 | if (callback) {
194 | opts.complete = callback;
195 | }
196 |
197 | if (typeof fun !== 'string') {
198 | var error = Pouch.error( Pouch.Errors.INVALID_REQUEST, 'Querying with a function is not supported for Spatial Views');
199 | return callback ? callback(error) : undefined;
200 | }
201 |
202 | if (db.type() === 'http') {
203 | return httpQuery(fun, opts, callback);
204 | }
205 |
206 | var parts = fun.split('/');
207 | db.get('_design/' + parts[0], function (err, doc) {
208 | if (err) {
209 | if (callback) callback(err);
210 | return;
211 | }
212 | viewQuery(doc.spatial[parts[1]], opts);
213 | });
214 | }
215 |
216 | return {spatial: query};
217 | };
218 |
219 | // Store it in the Spatial object, so we can test it
220 | Spatial.calculateBbox = function (geom) {
221 | var coords = geom.coordinates;
222 | if (geom.type === 'Point') {
223 | return [[coords[0], coords[0]], [coords[1], coords[1]]];
224 | }
225 | if (geom.type === 'GeometryCollection') {
226 | coords = geom.geometries.map(function (g) {
227 | return Spatial.calculateBbox(g);
228 | });
229 |
230 | // Merge all bounding boxes into one big one that encloses all
231 | return coords.reduce(function (acc, bbox) {
232 | var minX = Math.min(acc[0][0], bbox[0][0]);
233 | var minY = Math.min(acc[1][0], bbox[1][0]);
234 | var maxX = Math.max(acc[0][1], bbox[0][1]);
235 | var maxY = Math.max(acc[1][1], bbox[1][1]);
236 | return [[minX, maxX], [minY, maxY]];
237 | });
238 | }
239 |
240 | // Flatten coords as much as possible
241 | while (Array.isArray(coords[0][0])) {
242 | coords = coords.reduce(function (a, b) {
243 | return a.concat(b);
244 | });
245 | };
246 |
247 | // Calculate the enclosing bounding box of all coordinates
248 | return coords.reduce(function (acc, coord) {
249 | if (acc === null) {
250 | return [[coord[0], coord[0]], [coord[1], coord[1]]];
251 | }
252 | var minX = Math.min(acc[0][0], coord[0]);
253 | var minY = Math.min(acc[1][0], coord[1]);
254 | var maxX = Math.max(acc[0][1], coord[0]);
255 | var maxY = Math.max(acc[1][1], coord[1]);
256 | return [[minX, maxX], [minY, maxY]];
257 | }, null);
258 | };
259 |
260 | // Deletion is a noop since we dont store the results of the view
261 | Spatial._delete = function () { };
262 |
263 | Pouch.plugin('spatial', Spatial);
264 |
--------------------------------------------------------------------------------
/lib/replicate.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var PouchUtils = require('./utils');
4 | var Pouch = require('./index');
5 |
6 | // We create a basic promise so the caller can cancel the replication possibly
7 | // before we have actually started listening to changes etc
8 | function Promise() {
9 | var that = this;
10 | this.cancelled = false;
11 | this.cancel = function () {
12 | that.cancelled = true;
13 | };
14 | }
15 |
16 | // The RequestManager ensures that only one database request is active at
17 | // at time, it ensures we dont max out simultaneous HTTP requests and makes
18 | // the replication process easier to reason about
19 |
20 | function RequestManager(promise) {
21 | var queue = [];
22 | var api = {};
23 | var processing = false;
24 |
25 | // Add a new request to the queue, if we arent currently processing anything
26 | // then process it immediately
27 | api.enqueue = function (fun, args) {
28 | queue.push({fun: fun, args: args});
29 | if (!processing) {
30 | api.process();
31 | }
32 | };
33 |
34 | // Process the next request
35 | api.process = function () {
36 | if (processing || !queue.length || promise.cancelled) {
37 | return;
38 | }
39 | processing = true;
40 | var task = queue.shift();
41 | process.nextTick(function () {
42 | task.fun.apply(null, task.args);
43 | });
44 | };
45 |
46 | // We need to be notified whenever a request is complete to process
47 | // the next request
48 | api.notifyRequestComplete = function () {
49 | processing = false;
50 | api.process();
51 | };
52 |
53 | return api;
54 | }
55 |
56 | // TODO: check CouchDB's replication id generation, generate a unique id particular
57 | // to this replication
58 |
59 | function genReplicationId(src, target, opts) {
60 | var filterFun = opts.filter ? opts.filter.toString() : '';
61 | return '_local/' + PouchUtils.Crypto.MD5(src.id() + target.id() + filterFun);
62 | }
63 |
64 | // A checkpoint lets us restart replications from when they were last cancelled
65 |
66 | function fetchCheckpoint(src, target, id, callback) {
67 | target.get(id, function (err, targetDoc) {
68 | if (err && err.status === 404) {
69 | callback(null, 0);
70 | } else {
71 | src.get(id, function (err, sourceDoc) {
72 | if (err && err.status === 404 || targetDoc.last_seq !== sourceDoc.last_seq) {
73 | callback(null, 0);
74 | } else {
75 | callback(null, sourceDoc.last_seq);
76 | }
77 | });
78 | }
79 | });
80 | }
81 |
82 | function writeCheckpoint(src, target, id, checkpoint, callback) {
83 | function updateCheckpoint(db, callback) {
84 | db.get(id, function (err, doc) {
85 | if (err && err.status === 404) {
86 | doc = {_id: id};
87 | }
88 | doc.last_seq = checkpoint;
89 | db.put(doc, callback);
90 | });
91 | }
92 | updateCheckpoint(target, function (err, doc) {
93 | updateCheckpoint(src, function (err, doc) {
94 | callback();
95 | });
96 | });
97 | }
98 |
99 | function replicate(src, target, opts, promise) {
100 |
101 | var requests = new RequestManager(promise);
102 | var writeQueue = [];
103 | var repId = genReplicationId(src, target, opts);
104 | var results = [];
105 | var completed = false;
106 | var pendingRevs = 0;
107 | var last_seq = 0;
108 | var continuous = opts.continuous || false;
109 | var doc_ids = opts.doc_ids;
110 | var result = {
111 | ok: true,
112 | start_time: new Date(),
113 | docs_read: 0,
114 | docs_written: 0
115 | };
116 |
117 | function docsWritten(err, res, len) {
118 | if (opts.onChange) {
119 | for (var i = 0; i < len; i++) {
120 | /*jshint validthis:true */
121 | opts.onChange.apply(this, [result]);
122 | }
123 | }
124 | pendingRevs -= len;
125 | result.docs_written += len;
126 |
127 | writeCheckpoint(src, target, repId, last_seq, function (err, res) {
128 | requests.notifyRequestComplete();
129 | isCompleted();
130 | });
131 | }
132 |
133 | function writeDocs() {
134 | if (!writeQueue.length) {
135 | return requests.notifyRequestComplete();
136 | }
137 | var len = writeQueue.length;
138 | target.bulkDocs({docs: writeQueue}, {new_edits: false}, function (err, res) {
139 | docsWritten(err, res, len);
140 | });
141 | writeQueue = [];
142 | }
143 |
144 | function eachRev(id, rev) {
145 | src.get(id, {revs: true, rev: rev, attachments: true}, function (err, doc) {
146 | result.docs_read++;
147 | requests.notifyRequestComplete();
148 | writeQueue.push(doc);
149 | requests.enqueue(writeDocs);
150 | });
151 | }
152 |
153 | function onRevsDiff(diffCounts) {
154 | return function (err, diffs) {
155 | requests.notifyRequestComplete();
156 | if (err) {
157 | if (continuous) {
158 | promise.cancel();
159 | }
160 | PouchUtils.call(opts.complete, err, null);
161 | return;
162 | }
163 |
164 | // We already have all diffs passed in `diffCounts`
165 | if (Object.keys(diffs).length === 0) {
166 | for (var docid in diffCounts) {
167 | pendingRevs -= diffCounts[docid];
168 | }
169 | isCompleted();
170 | return;
171 | }
172 |
173 | var _enqueuer = function (rev) {
174 | requests.enqueue(eachRev, [id, rev]);
175 | };
176 |
177 | for (var id in diffs) {
178 | var diffsAlreadyHere = diffCounts[id] - diffs[id].missing.length;
179 | pendingRevs -= diffsAlreadyHere;
180 | diffs[id].missing.forEach(_enqueuer);
181 | }
182 | };
183 | }
184 |
185 | function fetchRevsDiff(diff, diffCounts) {
186 | target.revsDiff(diff, onRevsDiff(diffCounts));
187 | }
188 |
189 | function onChange(change) {
190 | last_seq = change.seq;
191 | results.push(change);
192 | var diff = {};
193 | diff[change.id] = change.changes.map(function (x) { return x.rev; });
194 | var counts = {};
195 | counts[change.id] = change.changes.length;
196 | pendingRevs += change.changes.length;
197 | requests.enqueue(fetchRevsDiff, [diff, counts]);
198 | }
199 |
200 | function complete() {
201 | completed = true;
202 | isCompleted();
203 | }
204 |
205 | function isCompleted() {
206 | if (completed && pendingRevs === 0) {
207 | result.end_time = new Date();
208 | PouchUtils.call(opts.complete, null, result);
209 | }
210 | }
211 |
212 | fetchCheckpoint(src, target, repId, function (err, checkpoint) {
213 |
214 | if (err) {
215 | return PouchUtils.call(opts.complete, err);
216 | }
217 |
218 | last_seq = checkpoint;
219 |
220 | // Was the replication cancelled by the caller before it had a chance
221 | // to start. Shouldnt we be calling complete?
222 | if (promise.cancelled) {
223 | return;
224 | }
225 |
226 | var repOpts = {
227 | continuous: continuous,
228 | since: last_seq,
229 | style: 'all_docs',
230 | onChange: onChange,
231 | complete: complete,
232 | doc_ids: doc_ids
233 | };
234 |
235 | if (opts.filter) {
236 | repOpts.filter = opts.filter;
237 | }
238 |
239 | if (opts.query_params) {
240 | repOpts.query_params = opts.query_params;
241 | }
242 |
243 | var changes = src.changes(repOpts);
244 |
245 | if (opts.continuous) {
246 | var cancel = promise.cancel;
247 | promise.cancel = function () {
248 | cancel();
249 | changes.cancel();
250 | };
251 | }
252 | });
253 |
254 | }
255 |
256 | function toPouch(db, callback) {
257 | if (typeof db === 'string') {
258 | return new Pouch(db, callback);
259 | }
260 | callback(null, db);
261 | }
262 |
263 | exports.replicate = function (src, target, opts, callback) {
264 | if (opts instanceof Function) {
265 | callback = opts;
266 | opts = {};
267 | }
268 | if (opts === undefined) {
269 | opts = {};
270 | }
271 | if (!opts.complete) {
272 | opts.complete = callback;
273 | }
274 | var replicateRet = new Promise();
275 | toPouch(src, function (err, src) {
276 | if (err) {
277 | return PouchUtils.call(callback, err);
278 | }
279 | toPouch(target, function (err, target) {
280 | if (err) {
281 | return PouchUtils.call(callback, err);
282 | }
283 | if (opts.server) {
284 | if (typeof src.replicateOnServer !== 'function') {
285 | return PouchUtils.call(callback, { error: 'Server replication not supported for ' + src.type() + ' adapter' });
286 | }
287 | if (src.type() !== target.type()) {
288 | return PouchUtils.call(callback, { error: 'Server replication for different adapter types (' + src.type() + ' and ' + target.type() + ') is not supported' });
289 | }
290 | src.replicateOnServer(target, opts, replicateRet);
291 | } else {
292 | replicate(src, target, opts, replicateRet);
293 | }
294 | });
295 | });
296 | return replicateRet;
297 | };
298 |
--------------------------------------------------------------------------------
/tests/test.cors.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var adapter = 'http-1';
4 |
5 | if (typeof module !== undefined && module.exports) {
6 | var PouchDB = require('../lib');
7 | var testUtils = require('./test.utils.js');
8 | }
9 |
10 | QUnit.module('cors-adapter:', {
11 | setup: function () {
12 | this.name = generateAdapterUrl(adapter);
13 | },
14 | teardown: function () {
15 | if (!PERSIST_DATABASES) {
16 | stop();
17 | var name = this.name;
18 | //get rid of cookie used for auth
19 | deleteCookieAuth(name, function (err, ret, res) {
20 | //get rid of admin and user
21 | if (typeof module !== undefined && module.exports) {
22 | tearDownAdminAndMemberConfig(name, function (err, info) {
23 | cleanUpCors(name, function () {
24 | cleanupTestDatabases(true);
25 | });
26 | });
27 | } else {
28 | tearDownAdminAndMemberConfig(name.replace('5984','2020'), function (err, info) {
29 | cleanUpCors(name, function () {
30 | cleanupTestDatabases(true);
31 | });
32 | });
33 | }
34 | });
35 | }
36 | }
37 | });
38 |
39 | //-------Cookie Auth Tests-----------//
40 | asyncTest('Cookie Authentication with Admin.', function () {
41 | var name = this.name;
42 |
43 | //--Do Test Prep
44 | //setup security for db
45 | var testDB = new Pouch(name);
46 | testDB.put({
47 | _id: '_security',
48 | 'admins': {
49 | 'names': ['TestAdmin'],
50 | 'roles': []
51 | },
52 | 'members': {
53 | 'names': ['TestUser'],
54 | 'roles': []
55 | }
56 | }, function (err, res) {
57 |
58 | //add an admin and user
59 | setupAdminAndMemberConfig(name, function (err, info) {
60 |
61 | //--Run tests (NOTE: because of how this is run, COR's credentials must be sent so that the server receives the auth cookie)
62 | var host = 'http://' + name.split('/')[2] + '/';
63 | Pouch.ajax({
64 | method: 'POST',
65 | url: host + '_session',
66 | json: false,
67 | headers: {
68 | 'Content-Type': 'application/x-www-form-urlencoded'
69 | },
70 | body: 'name=TestAdmin&password=admin',
71 | withCredentials: true
72 | }, function(err, ret, res) {
73 | var instantDB = new Pouch(name, function (err, db) {
74 | ok(err === null, 'Cookie authentication.');
75 | db.post({
76 | _id: '_design/testdesign',
77 | views: {
78 | test_view: {
79 | map: 'function(doc){emit(doc._id,doc._rev);}'
80 | }
81 | }
82 | }, function (err, info) { //add design doc (only admins can do this)
83 | ok(err === null, 'Design Doc inserted.');
84 | start();
85 | });
86 | });
87 | });
88 | });
89 | });
90 | });
91 |
92 | asyncTest('Cookie Authentication with User.', 3, function () {
93 | var name = this.name;
94 |
95 | //--Do Test Prep
96 | //setup security for db first
97 | var testDB = new Pouch(name);
98 | testDB.put({
99 | _id: '_security',
100 | 'admins': {
101 | 'names': ['TestAdmin'],
102 | 'roles': []
103 | },
104 | 'members': {
105 | 'names': ['TestUser'],
106 | 'roles': []
107 | }
108 | }, function (err, res) {
109 | //add an admin and user
110 | setupAdminAndMemberConfig(name, function (err, info) {
111 |
112 | //--Run tests (NOTE: because of how this is run, COR's credentials must be sent so that the server recieves the auth cookie)
113 | var host = 'http://' + name.split('/')[2] + '/';
114 | Pouch.ajax({
115 | method: 'POST',
116 | url: host + '_session',
117 | json: false,
118 | headers: {
119 | 'Content-Type': 'application/x-www-form-urlencoded'
120 | },
121 | body: 'name=TestUser&password=user',
122 | withCredentials: true
123 | }, function(err, ret, res) {
124 | var instantDB = new Pouch(name, function (err, db) {
125 | ok(err === null, 'Cookie authentication.');
126 | db.post({
127 | _id: '_design/testdesign',
128 | views: {
129 | test_view: {
130 | map: 'function(doc){emit(doc._id,doc._rev);}'
131 | }
132 | }
133 | }, function (err, info) { //add design doc (only admins can do this)
134 | ok(err && err.error === 'unauthorized', 'Design Doc failed to be inserted because we are not a db admin.');
135 | });
136 | db.post({
137 | test: 'abc'
138 | }, function (err, info) {
139 | ok(err === null, 'Doc inserted.');
140 | start();
141 | });
142 | });
143 | });
144 | });
145 | });
146 | });
147 |
148 | //-------CORS Enabled Tests----------//
149 | asyncTest('Create a pouchDB with CORS', 1, function () {
150 | var name = this.name;
151 |
152 | //--Run Tests
153 | var instantDB = new Pouch(this.name, function (err, info) {
154 | ok(err === null, 'DB created.');
155 | start();
156 | });
157 | });
158 |
159 | asyncTest('Add a doc using CORS', 2, function () {
160 | var name = this.name;
161 |
162 | //--Run Tests
163 | var instantDB = new Pouch(this.name, function (err, db) {
164 | ok(err === null, 'DB created.');
165 | db.post({
166 | test: 'abc'
167 | }, function (err, info) {
168 | ok(err === null, 'Doc inserted.');
169 | start();
170 | });
171 | });
172 | });
173 |
174 | asyncTest('Delete a DB using CORS', 2, function () {
175 | var name = this.name;
176 |
177 | //--Run Tests
178 | var instantDB = new Pouch(this.name, function (err, db) {
179 | ok(err === null, 'DB created.');
180 | Pouch.destroy(name, function (err, db) {
181 | ok(err === null, 'DB destroyed.');
182 | start();
183 | });
184 | });
185 | });
186 |
187 |
188 | //-------CORS Credentials Enabled Tests----------//
189 | asyncTest('Create DB as Admin with CORS Credentials.', 2, function () {
190 | var name = this.name; //saved for prep and cleanup
191 |
192 | setupAdminAndMemberConfig(this.name, function (err, info) {
193 | //--Run tests
194 | var host = 'http://' + name.split('/')[2] + '/';
195 | Pouch.ajax({
196 | method: 'POST',
197 | url: host + '_session',
198 | json: false,
199 | headers: {
200 | 'Content-Type': 'application/x-www-form-urlencoded'
201 | },
202 | body: 'name=TestAdmin&password=admin',
203 | withCredentials: true
204 | }, function(err, ret, res) {
205 | var instantDB = new Pouch(name, function (err, db) {
206 | ok(err === null, 'DB Created.');
207 | db.info(function (err, info) {
208 | ok(err === null, 'DB Get Info.');
209 | start();
210 | });
211 | });
212 | });
213 | });
214 | });
215 |
216 | asyncTest('Add Doc to DB as User with CORS Credentials.', 2, function () {
217 | var name = this.name; //saved for prep and cleanup
218 |
219 | //--Do Test Prep
220 | //add an admin and user
221 | setupAdminAndMemberConfig(name, function (err, info) {
222 |
223 | //--Run tests
224 | var host = 'http://' + name.split('/')[2] + '/';
225 | Pouch.ajax({
226 | method: 'POST',
227 | url: host + '_session',
228 | json: false,
229 | headers: {
230 | 'Content-Type': 'application/x-www-form-urlencoded'
231 | },
232 | body: 'name=TestAdmin&password=admin',
233 | withCredentials: true
234 | }, function(err, ret, res) {
235 | var instantDB = new Pouch(name, function (err, db) {
236 | ok(err === null, 'DB Created.');
237 | db.post({
238 | test: 'abc'
239 | }, function (err, info) {
240 | ok(err === null, 'Doc Inserted.');
241 | start();
242 | });
243 | });
244 | });
245 | });
246 | });
247 |
248 | asyncTest('Delete DB as Admin with CORS Credentials.', 3, function () {
249 | var name = this.name; //saved for prep and cleanup
250 |
251 | //--Do Test Prep
252 | //add an admin and user
253 | setupAdminAndMemberConfig(name, function (err, info) {
254 |
255 | //--Run tests
256 | var host = 'http://' + name.split('/')[2] + '/';
257 | Pouch.ajax({
258 | method: 'POST',
259 | url: host + '_session',
260 | json: false,
261 | headers: {
262 | 'Content-Type': 'application/x-www-form-urlencoded'
263 | },
264 | body: 'name=TestAdmin&password=admin',
265 | withCredentials: true
266 | }, function(err, ret, res) {
267 | var instantDB = new Pouch(name, function (err, db) {
268 | ok(err === null, 'DB Created.');
269 | db.post({
270 | test: 'abc'
271 | }, function (err, res) {
272 | ok(err === null, 'Doc Inserted.');
273 |
274 | Pouch.destroy(name, function (err, res) {
275 | ok(err === null, 'DB Deleted.');
276 | start();
277 | });
278 | });
279 | });
280 | });
281 | });
282 | });
283 |
--------------------------------------------------------------------------------
/lib/merge.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var extend = require('./deps/extend');
4 |
5 |
6 | // for a better overview of what this is doing, read:
7 | // https://github.com/apache/couchdb/blob/master/src/couchdb/couch_key_tree.erl
8 | //
9 | // But for a quick intro, CouchDB uses a revision tree to store a documents
10 | // history, A -> B -> C, when a document has conflicts, that is a branch in the
11 | // tree, A -> (B1 | B2 -> C), We store these as a nested array in the format
12 | //
13 | // KeyTree = [Path ... ]
14 | // Path = {pos: position_from_root, ids: Tree}
15 | // Tree = [Key, Opts, [Tree, ...]], in particular single node: [Key, []]
16 |
17 | // Turn a path as a flat array into a tree with a single branch
18 | function pathToTree(path) {
19 | var doc = path.shift();
20 | var root = [doc.id, doc.opts, []];
21 | var leaf = root;
22 | var nleaf;
23 |
24 | while (path.length) {
25 | doc = path.shift();
26 | nleaf = [doc.id, doc.opts, []];
27 | leaf[2].push(nleaf);
28 | leaf = nleaf;
29 | }
30 | return root;
31 | }
32 |
33 | // Merge two trees together
34 | // The roots of tree1 and tree2 must be the same revision
35 | function mergeTree(in_tree1, in_tree2) {
36 | var queue = [{tree1: in_tree1, tree2: in_tree2}];
37 | var conflicts = false;
38 | while (queue.length > 0) {
39 | var item = queue.pop();
40 | var tree1 = item.tree1;
41 | var tree2 = item.tree2;
42 |
43 | if (tree1[1].status || tree2[1].status) {
44 | tree1[1].status = (tree1[1].status === 'available' ||
45 | tree2[1].status === 'available') ? 'available' : 'missing';
46 | }
47 |
48 | for (var i = 0; i < tree2[2].length; i++) {
49 | if (!tree1[2][0]) {
50 | conflicts = 'new_leaf';
51 | tree1[2][0] = tree2[2][i];
52 | continue;
53 | }
54 |
55 | var merged = false;
56 | for (var j = 0; j < tree1[2].length; j++) {
57 | if (tree1[2][j][0] === tree2[2][i][0]) {
58 | queue.push({tree1: tree1[2][j], tree2: tree2[2][i]});
59 | merged = true;
60 | }
61 | }
62 | if (!merged) {
63 | conflicts = 'new_branch';
64 | tree1[2].push(tree2[2][i]);
65 | tree1[2].sort();
66 | }
67 | }
68 | }
69 | return {conflicts: conflicts, tree: in_tree1};
70 | }
71 |
72 | function doMerge(tree, path, dontExpand) {
73 | var restree = [];
74 | var conflicts = false;
75 | var merged = false;
76 | var res, branch;
77 |
78 | if (!tree.length) {
79 | return {tree: [path], conflicts: 'new_leaf'};
80 | }
81 |
82 | tree.forEach(function (branch) {
83 | if (branch.pos === path.pos && branch.ids[0] === path.ids[0]) {
84 | // Paths start at the same position and have the same root, so they need
85 | // merged
86 | res = mergeTree(branch.ids, path.ids);
87 | restree.push({pos: branch.pos, ids: res.tree});
88 | conflicts = conflicts || res.conflicts;
89 | merged = true;
90 | } else if (dontExpand !== true) {
91 | // The paths start at a different position, take the earliest path and
92 | // traverse up until it as at the same point from root as the path we want to
93 | // merge. If the keys match we return the longer path with the other merged
94 | // After stemming we dont want to expand the trees
95 |
96 | var t1 = branch.pos < path.pos ? branch : path;
97 | var t2 = branch.pos < path.pos ? path : branch;
98 | var diff = t2.pos - t1.pos;
99 |
100 | var candidateParents = [];
101 |
102 | var trees = [];
103 | trees.push({ids: t1.ids, diff: diff, parent: null, parentIdx: null});
104 | while (trees.length > 0) {
105 | var item = trees.pop();
106 | if (item.diff === 0) {
107 | if (item.ids[0] === t2.ids[0]) {
108 | candidateParents.push(item);
109 | }
110 | continue;
111 | }
112 | if (!item.ids) {
113 | continue;
114 | }
115 | /*jshint loopfunc:true */
116 | item.ids[2].forEach(function (el, idx) {
117 | trees.push({ids: el, diff: item.diff - 1, parent: item.ids, parentIdx: idx});
118 | });
119 | }
120 |
121 | var el = candidateParents[0];
122 |
123 | if (!el) {
124 | restree.push(branch);
125 | } else {
126 | res = mergeTree(el.ids, t2.ids);
127 | el.parent[2][el.parentIdx] = res.tree;
128 | restree.push({pos: t1.pos, ids: t1.ids});
129 | conflicts = conflicts || res.conflicts;
130 | merged = true;
131 | }
132 | } else {
133 | restree.push(branch);
134 | }
135 | });
136 |
137 | // We didnt find
138 | if (!merged) {
139 | restree.push(path);
140 | }
141 |
142 | restree.sort(function (a, b) {
143 | return a.pos - b.pos;
144 | });
145 |
146 | return {
147 | tree: restree,
148 | conflicts: conflicts || 'internal_node'
149 | };
150 | }
151 |
152 | // To ensure we dont grow the revision tree infinitely, we stem old revisions
153 | function stem(tree, depth) {
154 | // First we break out the tree into a complete list of root to leaf paths,
155 | // we cut off the start of the path and generate a new set of flat trees
156 | var stemmedPaths = PouchMerge.rootToLeaf(tree).map(function (path) {
157 | var stemmed = path.ids.slice(-depth);
158 | return {
159 | pos: path.pos + (path.ids.length - stemmed.length),
160 | ids: pathToTree(stemmed)
161 | };
162 | });
163 | // Then we remerge all those flat trees together, ensuring that we dont
164 | // connect trees that would go beyond the depth limit
165 | return stemmedPaths.reduce(function (prev, current, i, arr) {
166 | return doMerge(prev, current, true).tree;
167 | }, [stemmedPaths.shift()]);
168 | }
169 |
170 | var PouchMerge = {};
171 |
172 | PouchMerge.merge = function (tree, path, depth) {
173 | // Ugh, nicer way to not modify arguments in place?
174 | tree = extend(true, [], tree);
175 | path = extend(true, {}, path);
176 | var newTree = doMerge(tree, path);
177 | return {
178 | tree: stem(newTree.tree, depth),
179 | conflicts: newTree.conflicts
180 | };
181 | };
182 |
183 | // We fetch all leafs of the revision tree, and sort them based on tree length
184 | // and whether they were deleted, undeleted documents with the longest revision
185 | // tree (most edits) win
186 | // The final sort algorithm is slightly documented in a sidebar here:
187 | // http://guide.couchdb.org/draft/conflicts.html
188 | PouchMerge.winningRev = function (metadata) {
189 | var leafs = [];
190 | PouchMerge.traverseRevTree(metadata.rev_tree,
191 | function (isLeaf, pos, id, something, opts) {
192 | if (isLeaf) {
193 | leafs.push({pos: pos, id: id, deleted: !!opts.deleted});
194 | }
195 | });
196 | leafs.sort(function (a, b) {
197 | if (a.deleted !== b.deleted) {
198 | return a.deleted > b.deleted ? 1 : -1;
199 | }
200 | if (a.pos !== b.pos) {
201 | return b.pos - a.pos;
202 | }
203 | return a.id < b.id ? 1 : -1;
204 | });
205 |
206 | return leafs[0].pos + '-' + leafs[0].id;
207 | };
208 |
209 | // Pretty much all below can be combined into a higher order function to
210 | // traverse revisions
211 | // The return value from the callback will be passed as context to all
212 | // children of that node
213 | PouchMerge.traverseRevTree = function (revs, callback) {
214 | var toVisit = [];
215 |
216 | revs.forEach(function (tree) {
217 | toVisit.push({pos: tree.pos, ids: tree.ids});
218 | });
219 | while (toVisit.length > 0) {
220 | var node = toVisit.pop();
221 | var pos = node.pos;
222 | var tree = node.ids;
223 | var newCtx = callback(tree[2].length === 0, pos, tree[0], node.ctx, tree[1]);
224 | /*jshint loopfunc: true */
225 | tree[2].forEach(function (branch) {
226 | toVisit.push({pos: pos + 1, ids: branch, ctx: newCtx});
227 | });
228 | }
229 | };
230 |
231 | PouchMerge.collectLeaves = function (revs) {
232 | var leaves = [];
233 | PouchMerge.traverseRevTree(revs, function (isLeaf, pos, id, acc, opts) {
234 | if (isLeaf) {
235 | leaves.unshift({rev: pos + "-" + id, pos: pos, opts: opts});
236 | }
237 | });
238 | leaves.sort(function (a, b) {
239 | return b.pos - a.pos;
240 | });
241 | leaves.map(function (leaf) { delete leaf.pos; });
242 | return leaves;
243 | };
244 |
245 | // returns revs of all conflicts that is leaves such that
246 | // 1. are not deleted and
247 | // 2. are different than winning revision
248 | PouchMerge.collectConflicts = function (metadata) {
249 | var win = PouchMerge.winningRev(metadata);
250 | var leaves = PouchMerge.collectLeaves(metadata.rev_tree);
251 | var conflicts = [];
252 | leaves.forEach(function (leaf) {
253 | if (leaf.rev !== win && !leaf.opts.deleted) {
254 | conflicts.push(leaf.rev);
255 | }
256 | });
257 | return conflicts;
258 | };
259 |
260 | PouchMerge.rootToLeaf = function (tree) {
261 | var paths = [];
262 | PouchMerge.traverseRevTree(tree, function (isLeaf, pos, id, history, opts) {
263 | history = history ? history.slice(0) : [];
264 | history.push({id: id, opts: opts});
265 | if (isLeaf) {
266 | var rootPos = pos + 1 - history.length;
267 | paths.unshift({pos: rootPos, ids: history});
268 | }
269 | return history;
270 | });
271 | return paths;
272 | };
273 |
274 |
275 | module.exports = PouchMerge;
276 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | /*jshint strict: false */
2 | /*global chrome */
3 |
4 | var merge = require('./merge');
5 | exports.extend = require('./deps/extend');
6 | exports.ajax = require('./deps/ajax');
7 | exports.createBlob = require('./deps/blob');
8 | var uuid = require('./deps/uuid');
9 | exports.Crypto = require('./deps/md5.js');
10 | var buffer = require('./deps/buffer');
11 | var errors = require('./deps/errors');
12 |
13 | // List of top level reserved words for doc
14 | var reservedWords = [
15 | '_id',
16 | '_rev',
17 | '_attachments',
18 | '_deleted',
19 | '_revisions',
20 | '_revs_info',
21 | '_conflicts',
22 | '_deleted_conflicts',
23 | '_local_seq',
24 | '_rev_tree'
25 | ];
26 | exports.uuids = function (count, options) {
27 |
28 | if (typeof(options) !== 'object') {
29 | options = {};
30 | }
31 |
32 | var length = options.length;
33 | var radix = options.radix;
34 | var uuids = [];
35 |
36 | while (uuids.push(uuid(length, radix)) < count) { }
37 |
38 | return uuids;
39 | };
40 |
41 | // Give back one UUID
42 | exports.uuid = function (options) {
43 | return exports.uuids(1, options)[0];
44 | };
45 | // Determine id an ID is valid
46 | // - invalid IDs begin with an underescore that does not begin '_design' or '_local'
47 | // - any other string value is a valid id
48 | exports.isValidId = function (id) {
49 | if (!id || (typeof id !== 'string')) {
50 | return false;
51 | }
52 | if (/^_/.test(id)) {
53 | return (/^_(design|local)/).test(id);
54 | }
55 | return true;
56 | };
57 |
58 | function isChromeApp() {
59 | return (typeof chrome !== "undefined" &&
60 | typeof chrome.storage !== "undefined" &&
61 | typeof chrome.storage.local !== "undefined");
62 | }
63 |
64 | // Pretty dumb name for a function, just wraps callback calls so we dont
65 | // to if (callback) callback() everywhere
66 | exports.call = function (fun) {
67 | if (typeof fun === typeof Function) {
68 | var args = Array.prototype.slice.call(arguments, 1);
69 | fun.apply(this, args);
70 | }
71 | };
72 |
73 | exports.isLocalId = function (id) {
74 | return (/^_local/).test(id);
75 | };
76 |
77 | // check if a specific revision of a doc has been deleted
78 | // - metadata: the metadata object from the doc store
79 | // - rev: (optional) the revision to check. defaults to winning revision
80 | exports.isDeleted = function (metadata, rev) {
81 | if (!rev) {
82 | rev = merge.winningRev(metadata);
83 | }
84 | if (rev.indexOf('-') >= 0) {
85 | rev = rev.split('-')[1];
86 | }
87 | var deleted = false;
88 | merge.traverseRevTree(metadata.rev_tree, function (isLeaf, pos, id, acc, opts) {
89 | if (id === rev) {
90 | deleted = !!opts.deleted;
91 | }
92 | });
93 |
94 | return deleted;
95 | };
96 |
97 | exports.filterChange = function (opts) {
98 | return function (change) {
99 | var req = {};
100 | var hasFilter = opts.filter && typeof opts.filter === 'function';
101 |
102 | req.query = opts.query_params;
103 | if (opts.filter && hasFilter && !opts.filter.call(this, change.doc, req)) {
104 | return false;
105 | }
106 | if (opts.doc_ids && opts.doc_ids.indexOf(change.id) === -1) {
107 | return false;
108 | }
109 | if (!opts.include_docs) {
110 | delete change.doc;
111 | } else {
112 | for (var att in change.doc._attachments) {
113 | change.doc._attachments[att].stub = true;
114 | }
115 | }
116 | return true;
117 | };
118 | };
119 |
120 | exports.processChanges = function (opts, changes, last_seq) {
121 | // TODO: we should try to filter and limit as soon as possible
122 | changes = changes.filter(exports.filterChange(opts));
123 | if (opts.limit) {
124 | if (opts.limit < changes.length) {
125 | changes.length = opts.limit;
126 | }
127 | }
128 | changes.forEach(function (change) {
129 | exports.call(opts.onChange, change);
130 | });
131 | exports.call(opts.complete, null, {results: changes, last_seq: last_seq});
132 | };
133 |
134 | // Preprocess documents, parse their revisions, assign an id and a
135 | // revision for new writes that are missing them, etc
136 | exports.parseDoc = function (doc, newEdits) {
137 | var error = null;
138 | var nRevNum;
139 | var newRevId;
140 | var revInfo;
141 | var opts = {status: 'available'};
142 | if (doc._deleted) {
143 | opts.deleted = true;
144 | }
145 |
146 | if (newEdits) {
147 | if (!doc._id) {
148 | doc._id = exports.uuid();
149 | }
150 | newRevId = exports.uuid({length: 32, radix: 16}).toLowerCase();
151 | if (doc._rev) {
152 | revInfo = /^(\d+)-(.+)$/.exec(doc._rev);
153 | if (!revInfo) {
154 | throw "invalid value for property '_rev'";
155 | }
156 | doc._rev_tree = [{
157 | pos: parseInt(revInfo[1], 10),
158 | ids: [revInfo[2], {status: 'missing'}, [[newRevId, opts, []]]]
159 | }];
160 | nRevNum = parseInt(revInfo[1], 10) + 1;
161 | } else {
162 | doc._rev_tree = [{
163 | pos: 1,
164 | ids : [newRevId, opts, []]
165 | }];
166 | nRevNum = 1;
167 | }
168 | } else {
169 | if (doc._revisions) {
170 | doc._rev_tree = [{
171 | pos: doc._revisions.start - doc._revisions.ids.length + 1,
172 | ids: doc._revisions.ids.reduce(function (acc, x) {
173 | if (acc === null) {
174 | return [x, opts, []];
175 | } else {
176 | return [x, {status: 'missing'}, [acc]];
177 | }
178 | }, null)
179 | }];
180 | nRevNum = doc._revisions.start;
181 | newRevId = doc._revisions.ids[0];
182 | }
183 | if (!doc._rev_tree) {
184 | revInfo = /^(\d+)-(.+)$/.exec(doc._rev);
185 | if (!revInfo) {
186 | return errors.BAD_ARG;
187 | }
188 | nRevNum = parseInt(revInfo[1], 10);
189 | newRevId = revInfo[2];
190 | doc._rev_tree = [{
191 | pos: parseInt(revInfo[1], 10),
192 | ids: [revInfo[2], opts, []]
193 | }];
194 | }
195 | }
196 |
197 | if (typeof doc._id !== 'string') {
198 | error = errors.INVALID_ID;
199 | }
200 | else if (!exports.isValidId(doc._id)) {
201 | error = errors.RESERVED_ID;
202 | }
203 |
204 | for (var key in doc) {
205 | if (doc.hasOwnProperty(key) && key[0] === '_' && reservedWords.indexOf(key) === -1) {
206 | error = exports.extend({}, errors.DOC_VALIDATION);
207 | error.reason += ': ' + key;
208 | }
209 | }
210 |
211 | doc._id = decodeURIComponent(doc._id);
212 | doc._rev = [nRevNum, newRevId].join('-');
213 |
214 | if (error) {
215 | return error;
216 | }
217 |
218 | return Object.keys(doc).reduce(function (acc, key) {
219 | if (/^_/.test(key) && key !== '_attachments') {
220 | acc.metadata[key.slice(1)] = doc[key];
221 | } else {
222 | acc.data[key] = doc[key];
223 | }
224 | return acc;
225 | }, {metadata : {}, data : {}});
226 | };
227 |
228 | exports.isCordova = function () {
229 | return (typeof cordova !== "undefined" ||
230 | typeof PhoneGap !== "undefined" ||
231 | typeof phonegap !== "undefined");
232 | };
233 |
234 | exports.Changes = function () {
235 |
236 | var api = {};
237 | var listeners = {};
238 |
239 | if (isChromeApp()) {
240 | chrome.storage.onChanged.addListener(function (e) {
241 | // make sure it's event addressed to us
242 | if (e.db_name != null) {
243 | api.notify(e.db_name.newValue);//object only has oldValue, newValue members
244 | }
245 | });
246 | } else if (typeof window !== 'undefined') {
247 | window.addEventListener("storage", function (e) {
248 | api.notify(e.key);
249 | });
250 | }
251 |
252 | api.addListener = function (db_name, id, db, opts) {
253 | if (!listeners[db_name]) {
254 | listeners[db_name] = {};
255 | }
256 | listeners[db_name][id] = {
257 | db: db,
258 | opts: opts
259 | };
260 | };
261 |
262 | api.removeListener = function (db_name, id) {
263 | if (listeners[db_name]) {
264 | delete listeners[db_name][id];
265 | }
266 | };
267 |
268 | api.clearListeners = function (db_name) {
269 | delete listeners[db_name];
270 | };
271 |
272 | api.notifyLocalWindows = function (db_name) {
273 | //do a useless change on a storage thing
274 | //in order to get other windows's listeners to activate
275 | if (!isChromeApp()) {
276 | localStorage[db_name] = (localStorage[db_name] === "a") ? "b" : "a";
277 | } else {
278 | chrome.storage.local.set({db_name: db_name});
279 | }
280 | };
281 |
282 | api.notify = function (db_name) {
283 | if (!listeners[db_name]) { return; }
284 |
285 | Object.keys(listeners[db_name]).forEach(function (i) {
286 | var opts = listeners[db_name][i].opts;
287 | listeners[db_name][i].db.changes({
288 | include_docs: opts.include_docs,
289 | conflicts: opts.conflicts,
290 | continuous: false,
291 | descending: false,
292 | filter: opts.filter,
293 | view: opts.view,
294 | since: opts.since,
295 | query_params: opts.query_params,
296 | onChange: function (c) {
297 | if (c.seq > opts.since && !opts.cancelled) {
298 | opts.since = c.seq;
299 | exports.call(opts.onChange, c);
300 | }
301 | }
302 | });
303 | });
304 | };
305 |
306 | return api;
307 | };
308 |
309 | if (typeof window === 'undefined' || !('atob' in window)) {
310 | exports.atob = function (str) {
311 | var base64 = new buffer(str, 'base64');
312 | // Node.js will just skip the characters it can't encode instead of
313 | // throwing and exception
314 | if (base64.toString('base64') !== str) {
315 | throw ("Cannot base64 encode full string");
316 | }
317 | return base64.toString('binary');
318 | };
319 | } else {
320 | exports.atob = function (str) {
321 | return atob(str);
322 | };
323 | }
324 |
325 | if (typeof window === 'undefined' || !('btoa' in window)) {
326 | exports.btoa = function (str) {
327 | return new buffer(str, 'binary').toString('base64');
328 | };
329 | } else {
330 | exports.btoa = function (str) {
331 | return btoa(str);
332 | };
333 | }
334 |
335 |
336 | module.exports = exports;
337 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: learn
3 | title: PouchDB, the JavaScript Database that Syncs!
4 | ---
5 |
6 | # Getting Started Guide
7 |
8 | In this tutorial we will write a basic Todo web application based on [TodoMVC](http://todomvc.com/) that syncs to an online CouchDB server. It should take around 10 minutes.
9 |
10 | # Download Assets
11 |
12 | We will start with a template of the project where all the data related functions have been replaced with empty stubs. Download and unzip [pouchdb-getting-started-todo.zip](/static/assets/pouchdb-getting-started-todo.zip). When dealing with XHR and IndexedDB you are better off running web pages from a server as opposed to a filesystem. To do this you can run:
13 |
14 | {% highlight bash %}
15 | $ cd pouchdb-getting-started-todo
16 | $ python -m SimpleHTTPServer
17 | {% endhighlight %}
18 |
19 | Then visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/). If you see the following screenshot, you are good to go:
20 |
21 |
22 |
23 |
24 |
25 | It's also a good idea to open your browser's console so you can see any errors or confirmation messages.
26 |
27 | # Installing PouchDB
28 |
29 | Open `index.html` and include PouchDB in the app by adding a script tag:
30 |
31 | {% highlight html %}
32 |
33 |
34 |
35 | {% endhighlight %}
36 |
37 | PouchDB is now installed in your app and ready to use! (In production, you should use a local copy of the script.)
38 |
39 | # Creating a database
40 |
41 | The rest of the work will be done inside `app.js`. We will start by creating a database to enter your todos. To create a database simply instantiate a new PouchDB object with the name of the database:
42 |
43 | {% highlight js %}
44 | // EDITING STARTS HERE (you dont need to edit anything above this line)
45 |
46 | var db = new PouchDB('todos');
47 | var remoteCouch = false;
48 | {% endhighlight %}
49 |
50 | You don't need to create a schema for the database. After giving it a name, you can immediately start writing objects to it.
51 |
52 | # Write todos to the database
53 |
54 | The first thing we shall do is start writing items to the database. The main input will call `addTodo` with the current text when the user presses `Enter`. We can complete this function with the following code:
55 |
56 | {% highlight js %}
57 | function addTodo(text) {
58 | var todo = {
59 | _id: new Date().toISOString(),
60 | title: text,
61 | completed: false
62 | };
63 | db.put(todo, function callback(err, result) {
64 | if (!err) {
65 | console.log('Successfully posted a todo!');
66 | }
67 | });
68 | }
69 | {% endhighlight %}
70 |
71 | In PouchDB each document is required to have a unique `_id`. Any subsequent writes to a document with the same `_id` will be considered updates. Here we are using a date string as an `_id`. For our use case, it will be unique, and it can also be used to sort items in the database. You can use `PouchDB.uuids()` or `db.post()` if you want random ids. The `_id` is the only thing required when creating a new document. The rest of the object you can create as you like.
72 |
73 | The `callback` function will be called once the document has been written (or failed to write). If the `err` argument is not null, then it will have an object explaining the error, otherwise the `result` will hold the result.
74 |
75 | # Show items from the database
76 |
77 | We have included a helper function `redrawTodosUI` that takes an array of todos to display, so all we need to do is read the todos from the database. Here we will simply read all the documents using `db.allDocs`. The `include_docs` option tells PouchDB to give us the data within each document, and the `descending` option tells PouchDB how to order the results based on their `_id` field, giving us newest first.
78 |
79 | {% highlight js %}
80 | function showTodos() {
81 | db.allDocs({include_docs: true, descending: true}, function(err, doc) {
82 | redrawTodosUI(doc.rows);
83 | });
84 | }
85 | {% endhighlight %}
86 |
87 | Once you have included this code, you should be able to refresh the page to see any todos you have entered.
88 |
89 | # Update the UI
90 |
91 | We dont want to refresh the page to see new items. More typically you would update the UI manually when you write data to it, however, in PouchDB you may be syncing data remotely, so you want to make sure you update whenever the remote data changes. To do this we will call `db.changes` which subscribes to updates to the database, wherever they come from. You can enter this code between the `remoteCouch` and `addTodo` declaration:
92 |
93 | {% highlight js %}
94 | var remoteCouch = false;
95 |
96 | db.info(function(err, info) {
97 | db.changes({
98 | since: info.update_seq,
99 | continuous: true,
100 | onChange: showTodos
101 | });
102 | });
103 |
104 | // Show the current list of todos by reading them from the database
105 | function addTodo() {
106 | {% endhighlight %}
107 |
108 | So every time an update happens to the database, we redraw the UI to show the new data. The `continuous` flag means this function will continue to run indefinitely. Now try entering a new todo and it should appear immediately.
109 |
110 | # Edit a todo
111 |
112 | When the user checks a checkbox, the `checkboxChanged` function will be called, so we'll fill in the code to edit the object and call `db.put`:
113 |
114 | {% highlight js %}
115 | function checkboxChanged(todo, event) {
116 | todo.completed = event.target.checked;
117 | db.put(todo);
118 | }
119 | {% endhighlight %}
120 |
121 | This is similiar to creating a document, however the document must also contain a `_rev` field (in addition to `_id`), otherwise the write will be rejected. This ensures that you dont accidently overwrite changes to a document.
122 |
123 | You can test that this works by checking a todo item and refreshing the page. It should stay checked.
124 |
125 | # Delete an object
126 |
127 | To delete an object you can call db.remove with the object.
128 |
129 | {% highlight js %}
130 | function deleteButtonPressed(todo) {
131 | db.remove(todo);
132 | }
133 | {% endhighlight %}
134 |
135 | Similiar to editing a document, both the `_id` and `_rev` properties are required. You may notice that we are passing around the full object that we previously read from the database. You can of course manually construct the object, like: `{_id: todo._id, _rev: todo._rev}`, but passing around the existing object is usually more convenient and less error prone.
136 |
137 | # Complete rest of the Todo UI
138 |
139 | `todoBlurred` is called when the user edits a document. Here we'll delete the document if the user has entered a blank title, and we'll update it otherwise.
140 |
141 | {% highlight js %}
142 | function todoBlurred(todo, event) {
143 | var trimmedText = event.target.value.trim();
144 | if (!trimmedText) {
145 | db.remove(todo);
146 | } else {
147 | todo.title = trimmedText;
148 | db.put(todo);
149 | }
150 | }
151 | {% endhighlight %}
152 |
153 | # Installing CouchDB
154 |
155 | Now we'll implement the syncing. You need to have a CouchDB instance, which you can either install yourself [CouchDB(1.3+) locally](http://couchdb.apache.org/) or use with an online provider like [IrisCouch](http://iriscouch.com).
156 |
157 | # Enabling CORS
158 |
159 | To replicate directly with CouchDB, you need to make sure CORS is enabled. Only set the username and password if you have set them previously. By default, CouchDB will be installed in "Admin Party," where username and password are not needed. You will need to replace `myname.iriscouch.com` with your own host (`127.0.0.1:5984` if installed locally):
160 |
161 | {% highlight bash %}
162 | $ export HOST=http://username:password@myname.iriscouch.com
163 | $ curl -X PUT $HOST/_config/httpd/enable_cors -d '"true"'
164 | $ curl -X PUT $HOST/_config/cors/origins -d '"*"'
165 | $ curl -X PUT $HOST/_config/cors/credentials -d '"true"'
166 | $ curl -X PUT $HOST/_config/cors/methods -d '"GET, PUT, POST, HEAD, DELETE"'
167 | $ curl -X PUT $HOST/_config/cors/headers -d \
168 | '"accept, authorization, content-type, origin"'
169 | {% endhighlight %}
170 |
171 | # Implement basic two way sync
172 |
173 | Now we will have the todo list sync. Back in `app.js` we need to specify the address of the remote database. Remember to replace `user`, `pass` and `myname.iriscouch.com` with the credentials of your own CouchDB instance:
174 |
175 | {% highlight js %}
176 | // EDITING STARTS HERE (you dont need to edit anything above this line)
177 |
178 | var db = new PouchDB('todos');
179 | var remoteCouch = 'http://user:pass@mname.iriscouch.com/todos';
180 | {% endhighlight %}
181 |
182 | Then we can implement the sync function like so:
183 |
184 | {% highlight js %}
185 | function sync() {
186 | syncDom.setAttribute('data-sync-state', 'syncing');
187 | var opts = {continuous: true, complete: syncError};
188 | db.replicate.to(remoteCouch, opts);
189 | db.replicate.from(remoteCouch, opts);
190 | }
191 | {% endhighlight %}
192 |
193 | `db.replicate()` tells PouchDB to transfer all the documents `to` or `from` the `remoteCouch`. This can either be a string identifier or a PouchDB object. We call this twice: once to receive remote updates, and once to push local changes. Again, the `continuous` flag is used to tell PouchDB to carry on doing this indefinitely. The `complete` callback will be called whenever this finishes. For continuous replication, this will mean an error has occured, like losing your connection.
194 |
195 | You should be able to open [the todo app](http://127.0.0.1:8000) in another browser and see that the two lists stay in sync with any changes you make to them. You may also want to look at your CouchDB's Futon administration page and see the populated database.
196 |
197 | # Congratulations!
198 |
199 | You've completed your first PouchDB application. This is a basic example, and a real world application will need to integrate more error checking, user signup, etc. But you should now understand the basics you need to start working on your own PouchDB project. If you have any more questions, please get in touch on [IRC](irc://freenode.net#pouchdb) or the [mailing list](https://groups.google.com/forum/#!forum/pouchdb).
200 |
--------------------------------------------------------------------------------