You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by gl...@apache.org on 2020/02/12 11:25:55 UTC

[couchdb-nano] 07/15: design functions

This is an automated email from the ASF dual-hosted git repository.

glynnbird pushed a commit to branch jest
in repository https://gitbox.apache.org/repos/asf/couchdb-nano.git

commit 47c985b042fb80c4fd05ba318b99d794c2ba0edd
Author: Glynn Bird <gl...@gmail.com>
AuthorDate: Fri Feb 7 10:18:49 2020 +0000

    design functions
---
 lib/nano.js                        |  53 ++++++++++--
 test/design.atomic.test.js         |  94 +++++++++++++++++++++
 test/design.createIndex.test.js    |  82 +++++++++++++++++++
 test/design.find.test.js           |  83 +++++++++++++++++++
 test/design.findAsStream.test.js   |  56 +++++++++++++
 test/design.search.test.js         |  72 ++++++++++++++++
 test/design.searchAsStream.test.js |  46 +++++++++++
 test/design.show.test.js           |  66 +++++++++++++++
 test/design.view.test.js           | 164 +++++++++++++++++++++++++++++++++++++
 test/design.viewAsStream.test.js   |  43 ++++++++++
 10 files changed, 753 insertions(+), 6 deletions(-)

diff --git a/lib/nano.js b/lib/nano.js
index 7b63bc3..6af0e37 100644
--- a/lib/nano.js
+++ b/lib/nano.js
@@ -677,8 +677,8 @@ module.exports = exports = function dbScope (cfg) {
       const { opts, callback } = getCallback(qs0, callback0)
 
       if (!docNames || typeof docNames !== 'object' ||
-      !docNames.keys || !Array.isArray(docNames.keys) ||
-      docNames.keys.length === 0) {
+          !docNames.keys || !Array.isArray(docNames.keys) ||
+          docNames.keys.length === 0) {
         const e = new Error('Invalid keys')
         if (callback) {
           return callback(e, null)
@@ -698,6 +698,15 @@ module.exports = exports = function dbScope (cfg) {
     function view (ddoc, viewName, meta, qs0, callback0) {
       const { opts, callback } = getCallback(qs0, callback0)
 
+      if (!ddoc || !viewName) {
+        const e = new Error('Invalid view')
+        if (callback) {
+          return callback(e, null)
+        } else {
+          return Promise.reject(e)
+        }
+      }
+
       if (typeof meta.stream !== 'boolean') {
         meta.stream = false
       }
@@ -776,6 +785,14 @@ module.exports = exports = function dbScope (cfg) {
 
     // http://docs.couchdb.org/en/latest/api/ddoc/render.html#get--db-_design-ddoc-_show-func
     function showDoc (ddoc, viewName, docName, qs, callback) {
+      if (!ddoc || !viewName || !docName) {
+        const e = new Error('Invalid show')
+        if (callback) {
+          return callback(e, null)
+        } else {
+          return Promise.reject(e)
+        }
+      }
       return view(ddoc, viewName + '/' + docName, { type: 'show' }, qs, callback)
     }
 
@@ -785,6 +802,14 @@ module.exports = exports = function dbScope (cfg) {
         callback = body
         body = undefined
       }
+      if (!ddoc || !viewName || !docName) {
+        const e = new Error('Invalid update')
+        if (callback) {
+          return callback(e, null)
+        } else {
+          return Promise.reject(e)
+        }
+      }
       return view(ddoc, viewName + '/' + encodeURIComponent(docName), {
         type: 'update',
         method: 'PUT',
@@ -925,26 +950,42 @@ module.exports = exports = function dbScope (cfg) {
       }, callback)
     }
 
-    function find (selector, callback) {
+    function find (query, callback) {
+      if (!query || typeof query !== 'object') {
+        const e = new Error('Invalid query')
+        if (callback) {
+          return callback(e, null)
+        } else {
+          return Promise.reject(e)
+        }
+      }
       return relax({
         db: dbName,
         path: '_find',
         method: 'POST',
-        body: selector
+        body: query
       }, callback)
     }
 
-    function findAsStream (selector) {
+    function findAsStream (query) {
       return relax({
         db: dbName,
         path: '_find',
         method: 'POST',
-        body: selector,
+        body: query,
         stream: true
       })
     }
 
     function createIndex (indexDef, callback) {
+      if (!indexDef || typeof indexDef !== 'object') {
+        const e = new Error('Invalid index definition')
+        if (callback) {
+          return callback(e, null)
+        } else {
+          return Promise.reject(e)
+        }
+      }
       return relax({
         db: dbName,
         path: '_index',
diff --git a/test/design.atomic.test.js b/test/design.atomic.test.js
new file mode 100644
index 0000000..95c37fa
--- /dev/null
+++ b/test/design.atomic.test.js
@@ -0,0 +1,94 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to use an update function - PUT /db/_design/ddoc/_update/updatename/docid - db.atomic', async () => {
+  const updateFunction = function (doc, req) {
+    if (doc) {
+      doc.ts = new Date().getTime()
+    }
+    return [doc, { json: { status: 'ok' } }]
+  }
+  const response = updateFunction({})[1].json
+
+  // mocks
+  const scope = nock(COUCH_URL)
+    .put('/db/_design/ddoc/_update/updatename/docid')
+    .reply(200, response)
+
+  // test POST /db/_find
+  const db = nano.db.use('db')
+  const p = await db.atomic('ddoc', 'updatename', 'docid')
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should be able to use an update function with body - PUT /db/_design/ddoc/_update/updatename/docid - db.atomic', async () => {
+  const updateFunction = function (doc, req) {
+    if (doc) {
+      doc.ts = new Date().getTime()
+    }
+    return [doc, { json: { status: 'ok' } }]
+  }
+  const body = { a: 1, b: 2 }
+  const response = updateFunction({})[1].json
+
+  // mocks
+  const scope = nock(COUCH_URL)
+    .put('/db/_design/ddoc/_update/updatename/docid', body)
+    .reply(200, response)
+
+  // test POST /db/_find
+  const db = nano.db.use('db')
+  const p = await db.atomic('ddoc', 'updatename', 'docid', body)
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should be able to handle 404 - db.atomic', async () => {
+  // mocks
+  const response = {
+    error: 'not_found',
+    reason: 'missing'
+  }
+  const body = { a: 1, b: 2 }
+  const scope = nock(COUCH_URL)
+    .put('/db/_design/ddoc/_update/updatename/docid', body)
+    .reply(404, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  await expect(db.atomic('ddoc', 'updatename', 'docid', body)).rejects.toThrow('missing')
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should detect missing parameters - db.update', async () => {
+  const db = nano.db.use('db')
+  await expect(db.atomic()).rejects.toThrow('Invalid update')
+  await expect(db.atomic('ddoc')).rejects.toThrow('Invalid update')
+  await expect(db.atomic('ddoc', 'updatename')).rejects.toThrow('Invalid update')
+  await expect(db.atomic('', 'updatename', 'docid')).rejects.toThrow('Invalid update')
+})
+
+test('should detect missing parameters (callback) - db.update', async () => {
+  const db = nano.db.use('db')
+  return new Promise((resolve, reject) => {
+    db.atomic('', '', '', {}, (err, data) => {
+      expect(err).not.toBeNull()
+      resolve()
+    })
+  })
+})
diff --git a/test/design.createIndex.test.js b/test/design.createIndex.test.js
new file mode 100644
index 0000000..1c7e7a5
--- /dev/null
+++ b/test/design.createIndex.test.js
@@ -0,0 +1,82 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to create an index - POST /db/_index - db.index', async () => {
+  // mocks
+  const indexDef = {
+    index: {
+      fields: ['town', 'surname']
+    },
+    type: 'json',
+    name: 'townsurnameindex',
+    partitioned: false
+  }
+  const response = {
+    result: 'created',
+    id: '_design/a5f4711fc9448864a13c81dc71e660b524d7410c',
+    name: 'foo-index'
+  }
+  const scope = nock(COUCH_URL)
+    .post('/db/_index', indexDef)
+    .reply(200, response)
+
+  // test POST /db/_index
+  const db = nano.db.use('db')
+  const p = await db.createIndex(indexDef)
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should handle 404 - POST /db/_index - db.index', async () => {
+  // mocks
+  const indexDef = {
+    index: {
+      fields: ['town', 'surname']
+    },
+    type: 'json',
+    name: 'townsurnameindex',
+    partitioned: false
+  }
+  const response = {
+    error: 'not_found',
+    reason: 'missing'
+  }
+  const scope = nock(COUCH_URL)
+    .post('/db/_index', indexDef)
+    .reply(404, response)
+
+  // test POST /db/_index
+  const db = nano.db.use('db')
+  await expect(db.createIndex(indexDef)).rejects.toThrow('missing')
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should detect missing index - db.createIndex', async () => {
+  const db = nano.db.use('db')
+  await expect(db.createIndex()).rejects.toThrow('Invalid index definition')
+  await expect(db.createIndex('myindex')).rejects.toThrow('Invalid index definition')
+})
+
+test('should detect missing index (callback) - db.createIndex', async () => {
+  const db = nano.db.use('db')
+  return new Promise((resolve, reject) => {
+    db.createIndex('', (err, data) => {
+      expect(err).not.toBeNull()
+      resolve()
+    })
+  })
+})
diff --git a/test/design.find.test.js b/test/design.find.test.js
new file mode 100644
index 0000000..39bf9d3
--- /dev/null
+++ b/test/design.find.test.js
@@ -0,0 +1,83 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to query an index - POST /db/_find - db.find', async () => {
+  // mocks
+  const query = {
+    selector: {
+      $and: {
+        date: {
+          $gt: '2018'
+        },
+        name: 'Susan'
+      }
+    },
+    fields: ['name', 'date', 'orderid']
+  }
+  const response = {
+    docs: [
+      { name: 'Susan', date: '2019-01-02', orderid: '4411' },
+      { name: 'Susan', date: '2019-01-03', orderid: '8523' }
+    ]
+  }
+  const scope = nock(COUCH_URL)
+    .post('/db/_find', query)
+    .reply(200, response)
+
+  // test POST /db/_find
+  const db = nano.db.use('db')
+  const p = await db.find(query)
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should handle 404 - POST /db/_find - db.find', async () => {
+  // mocks
+  const query = {
+    selector: {
+      name: 'Susan'
+    }
+  }
+  const response = {
+    error: 'not_found',
+    reason: 'missing'
+  }
+  const scope = nock(COUCH_URL)
+    .post('/db/_find', query)
+    .reply(404, response)
+
+  // test POST /db/_find
+  const db = nano.db.use('db')
+  await expect(db.find(query)).rejects.toThrow('missing')
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should detect missing query - db.find', async () => {
+  const db = nano.db.use('db')
+  await expect(db.find()).rejects.toThrow('Invalid query')
+  await expect(db.find('susan')).rejects.toThrow('Invalid query')
+})
+
+test('should detect missing query (callback) - db.find', async () => {
+  const db = nano.db.use('db')
+  return new Promise((resolve, reject) => {
+    db.find('', (err, data) => {
+      expect(err).not.toBeNull()
+      resolve()
+    })
+  })
+})
diff --git a/test/design.findAsStream.test.js b/test/design.findAsStream.test.js
new file mode 100644
index 0000000..b330779
--- /dev/null
+++ b/test/design.findAsStream.test.js
@@ -0,0 +1,56 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to query an index as a stream- POST /db/_find - db.findAsStream', async () => {
+  // mocks
+  const query = {
+    selector: {
+      $and: {
+        date: {
+          $gt: '2018'
+        },
+        name: 'Susan'
+      }
+    },
+    fields: ['name', 'date', 'orderid']
+  }
+  const response = {
+    docs: [
+      { name: 'Susan', date: '2019-01-02', orderid: '4411' },
+      { name: 'Susan', date: '2019-01-03', orderid: '8523' }
+    ]
+  }
+  const scope = nock(COUCH_URL)
+    .post('/db/_find', query)
+    .reply(200, response)
+
+  return new Promise((resolve, reject) => {
+    // test POST /db/_find
+    const db = nano.db.use('db')
+    const s = db.findAsStream(query)
+    expect(typeof s).toBe('object')
+    let buffer = ''
+    s.on('data', (chunk) => {
+      buffer += chunk.toString()
+    })
+    s.on('end', () => {
+      expect(buffer).toBe(JSON.stringify(response))
+      expect(scope.isDone()).toBe(true)
+      resolve()
+    })
+  })
+})
diff --git a/test/design.search.test.js b/test/design.search.test.js
new file mode 100644
index 0000000..92be672
--- /dev/null
+++ b/test/design.search.test.js
@@ -0,0 +1,72 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to access a search index - GET /db/_design/ddoc/_search/searchname - db.search', async () => {
+  // mocks
+  const response = {
+    total_rows: 100000,
+    bookmark: 'g123',
+    rows: [
+      { a: 1, b: 2 }
+    ]
+  }
+  const params = { q: '*:*' }
+  const scope = nock(COUCH_URL)
+    .post('/db/_design/ddoc/_search/searchname', params)
+    .reply(200, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  const p = await db.search('ddoc', 'searchname', params)
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should be able to handle 404 - db.search', async () => {
+  // mocks
+  const response = {
+    error: 'not_found',
+    reason: 'missing'
+  }
+  const params = { q: '*:*' }
+  const scope = nock(COUCH_URL)
+    .post('/db/_design/ddoc/_search/searchname', params)
+    .reply(404, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  await expect(db.search('ddoc', 'searchname', params)).rejects.toThrow('missing')
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should detect missing parameters - db.search', async () => {
+  const db = nano.db.use('db')
+  await expect(db.search()).rejects.toThrow('Invalid view')
+  await expect(db.search('susan')).rejects.toThrow('Invalid view')
+  await expect(db.search('susan', '')).rejects.toThrow('Invalid view')
+  await expect(db.search('', 'susan')).rejects.toThrow('Invalid view')
+})
+
+test('should detect missing parameters (callback) - db.search', async () => {
+  const db = nano.db.use('db')
+  return new Promise((resolve, reject) => {
+    db.search('', '', (err, data) => {
+      expect(err).not.toBeNull()
+      resolve()
+    })
+  })
+})
diff --git a/test/design.searchAsStream.test.js b/test/design.searchAsStream.test.js
new file mode 100644
index 0000000..d86f35d
--- /dev/null
+++ b/test/design.searchAsStream.test.js
@@ -0,0 +1,46 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to access a search index as a stream - POST /db/_design/ddoc/_search/searchname - db.searchAsStream', async () => {
+  // mocks
+  const response = {
+    total_rows: 100000,
+    bookmark: 'g123',
+    rows: [
+      { a: 1, b: 2 }
+    ]
+  }
+  const params = { q: '*:*' }
+  const scope = nock(COUCH_URL)
+    .post('/db/_design/ddoc/_search/searchname', params)
+    .reply(200, response)
+
+  return new Promise((resolve, reject) => {
+    const db = nano.db.use('db')
+    const s = db.searchAsStream('ddoc', 'searchname', params)
+    expect(typeof s).toBe('object')
+    let buffer = ''
+    s.on('data', (chunk) => {
+      buffer += chunk.toString()
+    })
+    s.on('end', () => {
+      expect(buffer).toBe(JSON.stringify(response))
+      expect(scope.isDone()).toBe(true)
+      resolve()
+    })
+  })
+})
diff --git a/test/design.show.test.js b/test/design.show.test.js
new file mode 100644
index 0000000..258f1bb
--- /dev/null
+++ b/test/design.show.test.js
@@ -0,0 +1,66 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to use a show function - GET /db/_design/ddoc/_show/showname/docid - db.show', async () => {
+  const showFunction = function (doc, req) {
+    return 'Hello, world!'
+  }
+  // mocks
+  const scope = nock(COUCH_URL)
+    .get('/db/_design/ddoc/_show/showname/docid')
+    .reply(200, showFunction(), { 'Content-type': 'text/plain' })
+
+  // test POST /db/_find
+  const db = nano.db.use('db')
+  const p = await db.show('ddoc', 'showname', 'docid')
+  expect(p).toStrictEqual(showFunction())
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should be able to handle 404 - db.show', async () => {
+  // mocks
+  const response = {
+    error: 'not_found',
+    reason: 'missing'
+  }
+  const scope = nock(COUCH_URL)
+    .get('/db/_design/ddoc/_show/showname/docid')
+    .reply(404, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  await expect(db.show('ddoc', 'showname', 'docid')).rejects.toThrow('missing')
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should detect missing parameters - db.show', async () => {
+  const db = nano.db.use('db')
+  await expect(db.show()).rejects.toThrow('Invalid show')
+  await expect(db.show('ddoc')).rejects.toThrow('Invalid show')
+  await expect(db.show('ddoc', 'showname')).rejects.toThrow('Invalid show')
+  await expect(db.show('', 'showname', 'docid')).rejects.toThrow('Invalid show')
+})
+
+test('should detect missing parameters (callback) - db.show', async () => {
+  const db = nano.db.use('db')
+  return new Promise((resolve, reject) => {
+    db.show('', '', '', {}, (err, data) => {
+      expect(err).not.toBeNull()
+      resolve()
+    })
+  })
+})
diff --git a/test/design.view.test.js b/test/design.view.test.js
new file mode 100644
index 0000000..767b4f9
--- /dev/null
+++ b/test/design.view.test.js
@@ -0,0 +1,164 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to access a MapReduce view - GET /db/_design/ddoc/_view/viewname - db.view', async () => {
+  // mocks
+  const response = {
+    rows: [
+      { key: null, value: 23515 }
+    ]
+  }
+  const scope = nock(COUCH_URL)
+    .get('/db/_design/ddoc/_view/viewname')
+    .reply(200, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  const p = await db.view('ddoc', 'viewname')
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should be able to access a MapReduce view with opts - GET /db/_design/ddoc/_view/viewname - db.view', async () => {
+  // mocks
+  const response = {
+    rows: [
+      { key: 'BA', value: 21 },
+      { key: 'BB', value: 1 },
+      { key: 'BD', value: 98 },
+      { key: 'BE', value: 184 },
+      { key: 'BF', value: 32 },
+      { key: 'BG', value: 55 },
+      { key: 'BH', value: 8 },
+      { key: 'BI', value: 10 },
+      { key: 'BJ', value: 29 },
+      { key: 'BL', value: 1 },
+      { key: 'BM', value: 1 },
+      { key: 'BN', value: 4 },
+      { key: 'BO', value: 27 },
+      { key: 'BQ', value: 1 }
+    ]
+  }
+  const scope = nock(COUCH_URL)
+    .get('/db/_design/ddoc/_view/viewname?group=true&startkey="BA"&endkey="BQ"')
+    .reply(200, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  const p = await db.view('ddoc', 'viewname', { group: true, startkey: 'BA', endkey: 'BQ' })
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should be able to access a MapReduce view with keys - POST /db/_design/ddoc/_view/viewname - db.view', async () => {
+  // mocks
+  const keys = ['BA', 'BD']
+  const response = {
+    rows: [
+      { key: 'BA', value: 21 },
+      { key: 'BB', value: 1 }
+    ]
+  }
+  const scope = nock(COUCH_URL)
+    .post('/db/_design/ddoc/_view/viewname', { keys: keys })
+    .reply(200, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  const p = await db.view('ddoc', 'viewname', { keys: keys })
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should be able to access a MapReduce view with queries - POST /db/_design/ddoc/_view/viewname - db.view', async () => {
+  // mocks
+  const opts = {
+    queries: [
+      {
+        keys: [
+          'BA',
+          'BD'
+        ]
+      },
+      {
+        limit: 1,
+        skip: 2,
+        reduce: false
+      }
+    ]
+  }
+  const response = {
+    results: [
+      {
+        rows: [
+          { key: 'BA', value: 21 },
+          { key: 'BB', value: 1 }
+        ]
+      },
+      {
+        total_rows: 23515,
+        offset: 2,
+        rows: [
+          { id: '290594', key: 'AE', value: 1 }
+        ]
+      }
+    ]
+  }
+  const scope = nock(COUCH_URL)
+    .post('/db/_design/ddoc/_view/viewname', { queries: opts.queries })
+    .reply(200, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  const p = await db.view('ddoc', 'viewname', opts)
+  expect(p).toStrictEqual(response)
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should be able to handle 404 - db.view', async () => {
+  // mocks
+  const response = {
+    error: 'not_found',
+    reason: 'missing'
+  }
+  const scope = nock(COUCH_URL)
+    .get('/db/_design/ddoc/_view/viewname?group=true&startkey="BA"&endkey="BQ"')
+    .reply(404, response)
+
+  // test GET /db
+  const db = nano.db.use('db')
+  await expect(db.view('ddoc', 'viewname', { group: true, startkey: 'BA', endkey: 'BQ' })).rejects.toThrow('missing')
+  expect(scope.isDone()).toBe(true)
+})
+
+test('should detect missing parameters - db.view', async () => {
+  const db = nano.db.use('db')
+  await expect(db.view()).rejects.toThrow('Invalid view')
+  await expect(db.view('susan')).rejects.toThrow('Invalid view')
+  await expect(db.view('susan', '')).rejects.toThrow('Invalid view')
+  await expect(db.view('', 'susan')).rejects.toThrow('Invalid view')
+})
+
+test('should detect missing parameters (callback) - db.view', async () => {
+  const db = nano.db.use('db')
+  return new Promise((resolve, reject) => {
+    db.view('', '', (err, data) => {
+      expect(err).not.toBeNull()
+      resolve()
+    })
+  })
+})
diff --git a/test/design.viewAsStream.test.js b/test/design.viewAsStream.test.js
new file mode 100644
index 0000000..e332897
--- /dev/null
+++ b/test/design.viewAsStream.test.js
@@ -0,0 +1,43 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const Nano = require('..')
+const COUCH_URL = 'http://localhost:5984'
+const nano = Nano(COUCH_URL)
+const nock = require('nock')
+
+test('should be able to access a MapReduce view as a stream - GET /db/_design/ddoc/_view/viewname - db.viewAsStream', async () => {
+  // mocks
+  const response = {
+    rows: [
+      { key: null, value: 23515 }
+    ]
+  }
+  const scope = nock(COUCH_URL)
+    .get('/db/_design/ddoc/_view/viewname')
+    .reply(200, response)
+
+  return new Promise((resolve, reject) => {
+    const db = nano.db.use('db')
+    const s = db.viewAsStream('ddoc', 'viewname')
+    expect(typeof s).toBe('object')
+    let buffer = ''
+    s.on('data', (chunk) => {
+      buffer += chunk.toString()
+    })
+    s.on('end', () => {
+      expect(buffer).toBe(JSON.stringify(response))
+      expect(scope.isDone()).toBe(true)
+      resolve()
+    })
+  })
+})