diff --git a/.gitignore b/.gitignore index f06235c..0e75fe5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4a63e93 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +sudo: false +language: node_js + +cache: + directories: + - node_modules + +node_js: + - "0.10" + +services: + - couchdb + +before_install: + - npm i -g npm@^2.0.0 + +before_script: + - npm prune + +script: npm run $COMMAND + +env: + matrix: + - COMMAND='helper -- lint' + - COMMAND='helper -- js-test' + - COMMAND='build' + +#after_success: +# - npm run helper -- semantic-release + diff --git a/README.md b/README.md index 80d2dc3..8bd0566 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,38 @@ pouchdb-show ============ +[![Build Status](https://travis-ci.org/pouchdb/pouchdb-show.svg?branch=master)](https://travis-ci.org/pouchdb/pouchdb-show) +[![Dependency Status](https://david-dm.org/pouchdb/pouchdb-show.svg)](https://david-dm.org/pouchdb/pouchdb-show) +[![devDependency Status](https://david-dm.org/pouchdb/pouchdb-show/dev-status.svg)](https://david-dm.org/pouchdb/pouchdb-show#info=devDependencies) + A PouchDB plug-in that allows you to re-use your CouchDB show functions on the client side. A browser version is available. -See also [pouchdb-show's documentation](http://pythonhosted.org/Python-PouchDB/js-plugins.html#pouchdb-show-plug-in) +TODO: rst -> md, update & restructure +```rst +.. _pouchdb-show-plug-in: + +PouchDB Show plug-in +==================== ++----------------------+-----------------+ +| NodeJS package name: | `pouchdb-show`_ | ++----------------------+-----------------+ +| Browser object name: | ``window.Show`` | ++----------------------+-----------------+ + +First, make sure you understand how show functions work in CouchDB. A +good start is `the CouchDB guide entry on shows`_. + +.. _pouchdb-show: https://www.npmjs.org/package/pouchdb-show +.. _the CouchDB guide entry on shows: http://guide.couchdb.org/draft/formats.html + +.. js:function:: Show.show(showPath[, options[, callback]]) + + Similar to the :js:func:`List.list` function, but then for show + functions. Only differences are documented. + + :param string showPath: specifies the show (and optionally the + document) to use. Has the following form: + ``designDocName/showName[/docId]`` -[Website of this plug-in and a few others](http://python-pouchdb.marten-de-vries.nl/plugins.html) +``` diff --git a/index.js b/index.js index 63e62b7..3806bca 100644 --- a/index.js +++ b/index.js @@ -68,7 +68,7 @@ function offlineQuery(db, designDocName, showName, docId, req, options) { } return designDoc; }); - var docPromise = db.get(docId, options).catch(function (err) { + var docPromise = db.get(docId, options).catch(function () { //doc might not exist - that's ok and expected. return null; }); diff --git a/package.json b/package.json index ca6e500..b0a8354 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,37 @@ { - "name": "pouchdb-show", - "version": "1.0.7", - "main": "index.js", - "description": "A PouchDB plug-in that allows you to re-use your CouchDB show functions on the client side.", - "repository": "pouchdb/pouchdb-show", - "homepage": "http://python-pouchdb.marten-de-vries.nl/plugins.html", - "keywords": [ - "pouch", - "pouchdb", - "couch", - "couchdb", - "show", - "design" - ], - "license": "Apache-2.0", - "author": "Marten de Vries", - "dependencies": { - "couchdb-objects": "^1.0.0", - "couchdb-render": "^1.0.0", - "pouchdb-req-http-query": "^1.0.0", - "promise-nodify": "^1.0.0", - "pouchdb-promise": "^0.0.0", - "pouchdb-plugin-error": "^1.0.0" - }, - "devDependencies": { - "browserify": "^4.1.8", - "uglify-js": "^2.4.13", - "es3ify": "^0.1.3" - }, - "scripts": { - "build-js": "mkdir -p dist && browserify index.js -s Show -g es3ify -o dist/pouchdb-show.js", - "build": "npm run build-js; cd dist; uglifyjs pouchdb-show.js -mc > pouchdb-show.min.js" - } + "name": "pouchdb-show", + "version": "1.0.7", + "main": "index.js", + "description": "A PouchDB plug-in that allows you to re-use your CouchDB show functions on the client side.", + "repository": { + "type": "git", + "url": "https://github.com/pouchdb/pouchdb-show.git" + }, + "keywords": [ + "pouch", + "pouchdb", + "couch", + "couchdb", + "show", + "design" + ], + "license": "Apache-2.0", + "author": "Marten de Vries", + "dependencies": { + "couchdb-objects": "^1.0.0", + "couchdb-render": "^1.0.0", + "pouchdb-req-http-query": "^1.0.0", + "promise-nodify": "^1.0.0", + "pouchdb-promise": "^0.0.0", + "pouchdb-plugin-error": "^1.0.0" + }, + "devDependencies": { + "navigator": "^1.0.1", + "pouchdb-plugin-helper": "^2.0.0" + }, + "scripts": { + "helper": "./node_modules/.bin/pouchdb-plugin-helper", + "test": "npm run helper -- test", + "build": "npm run helper -- build Show" + } } diff --git a/test/features.js b/test/features.js new file mode 100644 index 0000000..fc69a19 --- /dev/null +++ b/test/features.js @@ -0,0 +1,302 @@ +import {setup, teardown, showDocument, shouldThrowError, should, checkUserAgent, checkUuid} from './utils'; + +let db; + +describe('Sync show tests', () => { + beforeEach(async () => { + db = setup(); + await db.put(showDocument); + }); + afterEach(teardown); + + it('should fail when given an invalid response object', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/invalidRespObject'); + }); + err.status.should.equal(500); + err.name.should.equal('external_response_error'); + err.message.should.equal('Invalid data from external server: {<<"abc">>,<<"test">>}'); + }); + + it('show without doc with a browser-like env', async () => { + global.window = {navigator: require('navigator')}; + + const result = await db.show('test/myshow'); + result.code.should.equal(200); + + delete global.window; + }); + + it('invalid return type and provides', async () => { + const result = await db.show('test/invalidReturnTypeAndProvides'); + result.code.should.equal(200); + result.body.should.equal(''); + }); + + it('throwing error', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/throwingError'); + }); + err.status.should.equal(500); + }); + + it('throwing error in provides', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/throwingErrorInProvides'); + }); + err.status.should.equal(500); + }); + + it('show without doc', async () => { + const result = await db.show('test/myshow'); + result.code.should.equal(200); + result.headers['Content-Type'].should.equal('text/html; charset=utf-8'); + result.headers.Vary.should.equal('Accept'); + result.body.should.equal('no doc'); + }); + + it('show with doc', async () => { + await db.post({_id: 'mytest', description: 'Hello World!'}); + + const result = await db.show('test/myshow/mytest'); + result.body.should.equal('Hello World!'); + }); + + it('overwrite args', async () => { + const resp = await db.show('test/args', {method: 'POST'}); + const req = JSON.parse(resp.body).args[1]; + req.method.should.equal('POST'); + }); + + it('overwrite header', async () => { + const resp = await db.show('test/args', {headers: {Host: 'example.com'}}); + const req = JSON.parse(resp.body).args[1]; + // check if the header update was succesful. + req.headers.Host.should.equal('example.com'); + // check if other headers (test subject is Accept) are still set. + req.headers.Accept.should.equal('text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'); + }); + + it('show args', async () => { + const [doc, req] = JSON.parse((await db.show('test/args')).body).args; + + // test doc - well, the unavailability of it... + should.equal(doc, null); + + // test request object + req.body.should.equal('undefined'); + req.cookie.should.eql({}); + req.form.should.eql({}); + + req.headers.Host.should.equal('localhost:5984'); + req.headers.Accept.should.equal('text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'); + req.headers['Accept-Language'].should.contain('en'); + req.headers['Accept-Language'].should.contain('en-us'); + checkUserAgent(req.headers['User-Agent']); + + should.equal(req.id, null); + req.info.db_name.should.equal('test'); + req.info.should.have.property('update_seq'); + req.method.should.equal('GET'); + req.path.should.eql(['test', '_design', 'test', '_show', 'args']); + req.peer.should.equal('127.0.0.1'); + req.query.should.eql({}); + req.raw_path.should.equal('/test/_design/test/_show/args'); + req.requested_path.should.eql(['test', '_design', 'test', '_show', 'args']); + req.secObj.should.eql({}); + req.userCtx.should.eql({ + db: 'test', + name: null, + roles: [ + '_admin' + ] + }); + checkUuid(req.uuid); + }); + + it('unexisting doc', async () => { + const [doc, req] = JSON.parse((await db.show('test/args/abc')).body).args; + should.equal(doc, null); + req.id.should.equal('abc'); + req.path.should.contain('abc'); + }); + + it('with design doc as arg', async () => { + const resp = await db.show('test/args/_design/test'); + const [doc, req] = JSON.parse(resp.body).args; + req.id.should.equal('_design/test'); + req.raw_path.should.equal('/test/_design/test/_show/args/_design/test'); + doc.should.have.property('shows'); + }); + + it('with fake design doc as arg', async () => { + const resp = await db.show('test/args/_design'); + const [doc, req] = JSON.parse(resp.body).args; + should.equal(doc, null); + req.id.should.equal('_design'); + req.raw_path.should.equal('/test/_design/test/_show/args/_design'); + }); + + it('setting query', async () => { + const resp = await db.show('test/args', {query: {a: 1}}); + const [doc, req] = JSON.parse(resp.body).args; + should.equal(doc, null); + req.raw_path.slice(-4).should.equal('?a=1'); + req.requested_path.should.eql(['test', '_design', 'test', '_show', 'args?a=1']); + req.path.should.eql(['test', '_design', 'test', '_show', 'args']); + }); + + it('setting form', async () => { + const resp = await db.show('test/args', {form: {a: 1}}); + const [doc, req] = JSON.parse(resp.body).args; + should.equal(doc, null); + req.body.should.equal('a=1'); + req.headers['Content-Type'].should.equal('application/x-www-form-urlencoded'); + req.headers['Content-Length'].should.equal('3'); + req.method.should.equal('POST'); + }); + + it('unexisting design doc', async () => { + const err = await shouldThrowError(async () => { + await db.show('abc/args'); + }); + err.name.should.equal('not_found'); + err.message.should.equal('missing'); + }); + + it('unexisting show function', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/unexisting-show'); + }); + err.status.should.equal(404); + err.name.should.equal('not_found'); + err.message.should.equal("missing show function unexisting-show on design doc _design/test"); + }); + + it('providers default', async () => { + const resp = await db.show('test/usingProviders'); + resp.code.should.equal(200); + resp.body.should.equal('

Hello World!

'); + resp.headers['Content-Type'].should.equal('text/html; charset=utf-8'); + resp.headers.Vary.should.equal('Accept'); + }); + + it('providers format', async () => { + const resp = await db.show('test/usingProviders', {query: {format: 'json'}}); + resp.code.should.equal(200); + JSON.parse(resp.body).should.eql({message: 'Hello World!'}); + resp.headers['Content-Type'].should.equal('application/json'); + resp.headers.Vary.should.equal('Accept'); + }); + + it('providers accept header', async () => { + const resp = await db.show('test/usingProviders', { + headers: {Accept: 'text/css,*/*;q=0.1'} + }); + resp.body.should.equal("body {content: 'Hello World!'}"); + resp.headers['Content-Type'].should.equal('text/css'); + }); + + it('custom provider', async () => { + const resp = await db.show('test/usingProviders', { + headers: {Accept: 'application/octet-stream'} + }); + resp.code.should.equal(200); + resp.should.not.have.property('body'); + new Buffer(resp.base64, 'base64').toString('ascii').should.equal('Hello World!'); + resp.headers['Content-Type'].should.equal('application/octet-stream; charset=ascii'); + }); + + it('unexisting format', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/usingProviders', {query: {format: 'text'}}); + }); + err.status.should.equal(500); + err.name.should.equal('render_error'); + err.message.should.equal("the format option is set to 'text', but there's no provider registered for that format."); + }); + + it('no matching provider', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/usingProviders', {headers: {Accept: 'text/plain'}}); + }); + err.status.should.equal(406); + err.name.should.equal('not_acceptable'); + err.message.indexOf("Content-Type(s) text/plain not supported, try one of: ").should.equal(0); + err.message.should.contain('application/json'); + }); + + it('old style json', async () => { + const resp = await db.show('test/oldStyleJson'); + resp.headers['Content-Type'].should.equal('application/json'); + JSON.parse(resp.body).should.eql({old_style: 'json'}); + }); + + it('format when empty show function', async () => { + const resp = await db.show('test/empty'); + resp.code.should.equal(200); + resp.headers['Content-Type'].should.equal('text/html; charset=utf-8'); + resp.body.should.equal(''); + }); + + it('no function', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/nofunc'); + }); + err.status.should.equal(500); + err.name.should.equal('compilation_error'); + }); + + it('invalid syntax', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/invalidsyntax'); + }); + err.status.should.equal(500); + err.name.should.equal('compilation_error'); + }); + + it('no doc with error response', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/throwingError/some-doc'); + }); + err.status.should.equal(404); + err.name.should.equal('not_found'); + err.message.should.equal('document not found'); + }); +}); + +describe('Sync show tests with empty design doc', () => { + beforeEach(async () => { + db = setup(); + await db.put({_id: '_design/test'}); + }); + afterEach(teardown); + it('test', async () => { + const err = await shouldThrowError(async () => { + await db.show('test/test/test'); + }); + err.status.should.equal(404); + }); +}); + +describe('async show tests', () => { + beforeEach(done => { + db = setup(); + db.put(showDocument, done); + }); + afterEach(teardown); + + it('should work without doc', done => { + db.show('test/myshow', (err, result) => { + result.body.should.equal('no doc'); + done(err); + }); + }); + it('should fail with an unexisting ddoc', done => { + db.show('abc/args', err => { + err.name.should.equal('not_found'); + err.message.should.equal('missing'); + done(); + }); + }); +}); diff --git a/test/http.js b/test/http.js new file mode 100644 index 0000000..f8f33dc --- /dev/null +++ b/test/http.js @@ -0,0 +1,48 @@ +import {setupHTTP, teardown, showDocument, should, BASE_URL, checkUserAgent, checkUuid} from './utils'; + +let db; + +describe('http', () => { + beforeEach(async () => { + db = setupHTTP(); + await db.put(showDocument); + }); + afterEach(teardown); + + it('show', async () => { + const resp = await db.show('test/args', {body: 'Hello World!', headers: {'Content-Type': 'text/plain'}}); + resp.code.should.equal(200); + resp.headers['Content-Type'].should.equal('text/html; charset=utf-8'); + + const [doc, req] = JSON.parse(resp.body).args; + + // test doc - well, the unavailability of it... + should.equal(doc, null); + + // test request object + req.body.should.equal('Hello World!'); + req.cookie.should.eql({}); + req.form.should.eql({}); + + BASE_URL.should.contain(req.headers.Host); + req.headers.Accept.should.equal('*/*, text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8') + req.headers['Content-Type'].should.equal('text/plain'); + req.headers['Accept-Language'].should.contain('en'); + req.headers['Accept-Language'].should.contain('en-us'); + checkUserAgent(req.headers['User-Agent']); + + should.equal(req.id, null); + req.info.db_name.should.equal('pouchdb-plugin-helper-db'); + req.info.should.have.property('update_seq'); + req.method.should.equal('POST'); + req.path.should.eql(['pouchdb-plugin-helper-db', '_design', 'test', '_show', 'args']) + req.peer.should.equal('127.0.0.1'); + req.query.should.eql({}); + req.raw_path.should.equal('/pouchdb-plugin-helper-db/_design/test/_show/args') + req.requested_path.should.eql(['pouchdb-plugin-helper-db', '_design', 'test', '_show', 'args']) + req.secObj.should.eql({}); + req.userCtx.db.should.equal('pouchdb-plugin-helper-db'); + req.userCtx.should.have.property('name'); + checkUuid(req.uuid); + }); +}); diff --git a/test/signatures.js b/test/signatures.js new file mode 100644 index 0000000..057169c --- /dev/null +++ b/test/signatures.js @@ -0,0 +1,15 @@ +import {setup, teardown} from './utils'; + +let db; + +describe('signatures', () => { + beforeEach(() => { + db = setup(); + }); + afterEach(teardown); + it('show', () => { + const promise = db.show('test/test/test', () => {}); + promise.then.should.be.ok; + promise.catch.should.be.ok; + }); +}); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..50e8152 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,76 @@ +import stuff from 'pouchdb-plugin-helper/testutils'; +import Show from '../'; + +stuff.PouchDB.plugin(Show); + +stuff.showDocument = { + _id: '_design/test', + shows: { + myshow: `function (doc, req) { + if (!doc) { + return {body: 'no doc'} + } else { + return {body: doc.description} + } + };\n`, + args: `function (doc, req) { + return toJSON({args: [doc, req]}); + }`, + usingProviders: `function (doc, req) { + provides('json', function () { + return toJSON({message: 'Hello World!'}); + }); + provides('html', function () { + log({'type': 'is', 'html': 'for', 'this': 'func'}) + return '

Hello World!

'; + }); + provides('css', function () { + return "body {content: 'Hello World!'}"; + }); + registerType('ascii-binary', 'application/octet-stream; charset=ascii'); + provides('ascii-binary', function () { + return { + 'base64': 'SGVsbG8gV29ybGQh' + }; + }); + }`, + oldStyleJson: `function (doc, req) { + return { + json: { + old_style: 'json' + } + }; + }`, + empty: `function (doc, req) {}`, + nofunc: `'Hello World!'`, + invalidsyntax: `function (doc, req)) {}`, + invalidReturnTypeAndProvides: `function (doc, req) { + provides('html', function () { + return 42; + }); + }`, + throwingError: `function (doc, req) { + throw new Error('Hello World!') + }`, + throwingErrorInProvides: `function (doc, req) { + provides('text', function () { + throw new Error('Hello World!'); + }); + }`, + invalidRespObject: `function (doc, req) { + return {body: 'test', abc: 'test'}; + }` + } +}; + +stuff.checkUserAgent = ua => { + ua.should.contain('Mozilla'); + ua.should.contain('Gecko'); +} + +stuff.checkUuid = uuid => { + uuid.should.be.a('string'); + uuid.length.should.be.greaterThan(30) +} + +module.exports = stuff;