├── 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 |

QUnit Test Suite

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 |
    64 |

    Learn More »

    65 |
    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 |
    14 | 15 |

    PouchDB

    16 | 17 |
    18 | Download 19 |
    20 | 21 | 29 | 30 |
    31 | 32 |
    {{ content }}
    33 | 34 | 73 | 74 | Fork me on GitHub 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(''); 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('\n'); 243 | } else { 244 | data.push('\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 | --------------------------------------------------------------------------------