1293 lines
35 KiB
JavaScript
1293 lines
35 KiB
JavaScript
// 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 { URL } = require('url')
|
|
const http = require('http')
|
|
const https = require('https')
|
|
const assert = require('assert')
|
|
const querystring = require('qs')
|
|
const axios = require('axios')
|
|
const stream = require('stream')
|
|
const pkg = require('../package.json')
|
|
const AGENT_DEFAULTS = { keepAlive: true, maxSockets: 50, keepAliveMsecs: 30000 }
|
|
const defaultHttpAgent = new http.Agent(AGENT_DEFAULTS)
|
|
const defaultHttpsAgent = new https.Agent(AGENT_DEFAULTS)
|
|
const SCRUBBED_STR = 'XXXXXX'
|
|
const ChangesReader = require('./changesreader.js')
|
|
const CookieJar = require('./cookie.js')
|
|
const MultiPartFactory = require('./multipart.js')
|
|
|
|
function isEmpty (val) {
|
|
return val == null || !(Object.keys(val) || val).length
|
|
}
|
|
|
|
function getCallback (opts, callback) {
|
|
if (typeof opts === 'function') {
|
|
callback = opts
|
|
opts = {}
|
|
}
|
|
opts = opts || {}
|
|
return {
|
|
opts,
|
|
callback
|
|
}
|
|
}
|
|
|
|
// feed this any number of arguements, it will return true if
|
|
// any of them are missing (falsey)
|
|
function missing (...params) {
|
|
return params.some(param => !param)
|
|
}
|
|
|
|
// calls the supplied callback or if not supplied, returns a rejected promise
|
|
function callbackOrRejectError (callback, error = new Error('Invalid parameters')) {
|
|
if (callback) {
|
|
return callback(error, null)
|
|
} else {
|
|
return Promise.reject(error)
|
|
}
|
|
}
|
|
|
|
module.exports = exports = function dbScope (cfg) {
|
|
let serverScope = {}
|
|
|
|
if (typeof cfg === 'string') {
|
|
cfg = { url: cfg }
|
|
}
|
|
|
|
assert.strictEqual(typeof cfg, 'object',
|
|
'You must specify the endpoint url when invoking this module')
|
|
assert.ok(/^https?:/.test(cfg.url), 'url is not valid')
|
|
|
|
cfg = Object.assign({}, cfg)
|
|
|
|
serverScope.config = cfg
|
|
cfg.requestDefaults = cfg.requestDefaults || {}
|
|
|
|
const dummyLogger = () => {}
|
|
const log = typeof cfg.log === 'function' ? cfg.log : dummyLogger
|
|
const parseUrl = 'parseUrl' in cfg ? cfg.parseUrl : true
|
|
|
|
// create cookieJar for this Nano
|
|
cfg.cookieJar = new CookieJar()
|
|
|
|
function maybeExtractDatabaseComponent () {
|
|
if (!parseUrl) {
|
|
return
|
|
}
|
|
|
|
const path = new URL(cfg.url)
|
|
const pathArray = path.pathname.split('/').filter(function (e) { return e })
|
|
const db = pathArray.pop()
|
|
const rootPath = path.pathname.replace(/\/?$/, '/..')
|
|
|
|
if (db) {
|
|
cfg.url = urlResolveFix(cfg.url, rootPath).replace(/\/?$/, '')
|
|
return db
|
|
}
|
|
}
|
|
|
|
function scrubURL (str) {
|
|
if (str) {
|
|
str = str.replace(/\/\/(.*)@/, `//${SCRUBBED_STR}:${SCRUBBED_STR}@`)
|
|
}
|
|
return str
|
|
}
|
|
|
|
function scrubRequest (req, cloned) {
|
|
// scrub credentials
|
|
req.url = scrubURL(req.url)
|
|
if (req.headers.cookie) {
|
|
req.headers.cookie = 'XXXXXXX'
|
|
}
|
|
if (req.auth) {
|
|
if (!cloned) {
|
|
req.auth = JSON.parse(JSON.stringify(req.auth)) // clone just auth if not already cloned
|
|
}
|
|
req.auth.username = SCRUBBED_STR
|
|
req.auth.password = SCRUBBED_STR
|
|
}
|
|
}
|
|
|
|
const responseHandler = function (response, req, opts, resolve, reject, callback) {
|
|
const statusCode = response.status || (response.response && response.response.status) || 500
|
|
if (response.isAxiosError && response.response) {
|
|
response = response.response
|
|
}
|
|
let body = response.data
|
|
response.statusCode = statusCode
|
|
|
|
// cookie parsing
|
|
if (response.headers) {
|
|
const h = response.headers['set-cookie']
|
|
if (h && h.length) {
|
|
h.forEach((header) => {
|
|
cfg.cookieJar.parse(header, req.url)
|
|
})
|
|
}
|
|
}
|
|
|
|
// let parsed
|
|
const responseHeaders = Object.assign({
|
|
uri: scrubURL(req.url),
|
|
statusCode
|
|
}, response.headers)
|
|
if (!response.status) {
|
|
if (axios.isCancel(response)) {
|
|
if (resolve) {
|
|
resolve('canceled')
|
|
}
|
|
if (callback) {
|
|
callback(null, 'canceled', responseHeaders)
|
|
}
|
|
return
|
|
}
|
|
|
|
log({ err: 'socket', body, headers: responseHeaders })
|
|
if (reject) {
|
|
reject(new Error(`error happened in your connection. Reason: ${response.message}`))
|
|
}
|
|
if (callback) {
|
|
const returnError = new Error(`error happened in your connection. Reason: ${response.message}`)
|
|
returnError.scope = 'socket'
|
|
returnError.errid = 'request'
|
|
callback(returnError)
|
|
}
|
|
return
|
|
}
|
|
|
|
delete responseHeaders.server
|
|
delete responseHeaders['content-length']
|
|
|
|
/* if (opts.dontParse) {
|
|
parsed = body
|
|
} else {
|
|
try { parsed = JSON.parse(body) } catch (err) { parsed = body }
|
|
} */
|
|
|
|
if (statusCode >= 200 && statusCode < 400) {
|
|
log({ err: null, body, headers: responseHeaders })
|
|
if (resolve) {
|
|
resolve(body)
|
|
}
|
|
if (callback) {
|
|
callback(null, body, responseHeaders)
|
|
}
|
|
return
|
|
}
|
|
|
|
// cloudant stacktrace
|
|
if (typeof body === 'string') {
|
|
body = { message: body }
|
|
}
|
|
|
|
if (!body.message && (body.reason || body.error)) {
|
|
body.message = (body.reason || body.error)
|
|
}
|
|
|
|
// fix cloudant issues where they give an erlang stacktrace as js
|
|
delete body.stack
|
|
|
|
// scrub credentials
|
|
scrubRequest(req)
|
|
|
|
log({ err: 'couch', body, headers: responseHeaders })
|
|
|
|
const message = body.message || 'couch returned ' + statusCode
|
|
const errors = new Error(message)
|
|
errors.scope = 'couch'
|
|
errors.statusCode = statusCode
|
|
errors.request = req
|
|
errors.headers = responseHeaders
|
|
errors.errid = 'non_200'
|
|
errors.name = 'Error'
|
|
errors.description = message
|
|
// add any attributes from the HTTP response into the
|
|
// Error object (except message, which would overwrite
|
|
// the text message of the Error)
|
|
delete body.message
|
|
Object.assign(errors, body)
|
|
|
|
if (reject) {
|
|
reject(errors)
|
|
}
|
|
if (callback) {
|
|
callback(errors)
|
|
}
|
|
}
|
|
|
|
const streamResponseHandler = function (response, req, stream) {
|
|
const statusCode = response.status || (response.response && response.response.status) || 500
|
|
if (response.isAxiosError && response.response) {
|
|
response = response.response
|
|
}
|
|
const message = response.statusText
|
|
|
|
scrubRequest(req)
|
|
|
|
const responseHeaders = Object.assign({
|
|
uri: req.url,
|
|
statusCode
|
|
}, response.headers)
|
|
|
|
const error = new Error(message)
|
|
error.scope = 'couch'
|
|
error.statusCode = statusCode
|
|
error.request = req
|
|
error.headers = responseHeaders
|
|
error.errid = 'non_200'
|
|
error.name = 'Error'
|
|
error.description = message
|
|
error.reason = message
|
|
|
|
log({ err: 'couch', body: message, headers: responseHeaders })
|
|
|
|
stream.emit('error', error)
|
|
}
|
|
|
|
function relax (opts, callback) {
|
|
if (typeof opts === 'function') {
|
|
callback = opts
|
|
opts = { path: '' }
|
|
}
|
|
|
|
if (typeof opts === 'string') {
|
|
opts = { path: opts }
|
|
}
|
|
|
|
if (!opts) {
|
|
opts = { path: '' }
|
|
callback = null
|
|
}
|
|
|
|
const qs = Object.assign({}, opts.qs)
|
|
|
|
const headers = {
|
|
'content-type': 'application/json',
|
|
accept: 'application/json',
|
|
'user-agent': `${pkg.name}/${pkg.version} (Node.js ${process.version})`,
|
|
'Accept-Encoding': 'deflate, gzip'
|
|
}
|
|
|
|
const req = Object.assign({
|
|
method: (opts.method || 'GET'),
|
|
headers,
|
|
uri: cfg.url
|
|
}, {
|
|
...cfg.requestDefaults,
|
|
headers: Object.assign(headers, cfg.requestDefaults && cfg.requestDefaults.headers ? cfg.requestDefaults.headers : {})
|
|
})
|
|
|
|
// https://github.com/mikeal/request#requestjar
|
|
const isJar = opts.jar || cfg.jar || (cfg.requestDefaults && cfg.requestDefaults.jar)
|
|
|
|
if (opts.signal) {
|
|
req.signal = opts.signal
|
|
}
|
|
|
|
if (isJar) {
|
|
req.withCredentials = true
|
|
}
|
|
|
|
// http://wiki.apache.org/couchdb/HTTP_database_API#Naming_and_Addressing
|
|
if (opts.db) {
|
|
req.uri = urlResolveFix(req.uri, encodeURIComponent(opts.db))
|
|
}
|
|
|
|
if (opts.multipart) {
|
|
// generate the multipart/related body, header and boundary to
|
|
// upload multiple binary attachments in one request
|
|
const mp = new MultiPartFactory(opts.multipart)
|
|
opts.contentType = mp.header
|
|
req.body = mp.data
|
|
}
|
|
|
|
req.headers = Object.assign(req.headers, opts.headers, cfg.defaultHeaders)
|
|
|
|
if (opts.path) {
|
|
req.uri += '/' + opts.path
|
|
} else if (opts.doc) {
|
|
if (!/^_design|_local/.test(opts.doc)) {
|
|
// http://wiki.apache.org/couchdb/HTTP_Document_API#Naming.2FAddressing
|
|
req.uri += '/' + encodeURIComponent(opts.doc)
|
|
} else {
|
|
// http://wiki.apache.org/couchdb/HTTP_Document_API#Document_IDs
|
|
req.uri += '/' + opts.doc
|
|
}
|
|
|
|
// http://wiki.apache.org/couchdb/HTTP_Document_API#Attachments
|
|
if (opts.att) {
|
|
req.uri += '/' + opts.att
|
|
}
|
|
}
|
|
|
|
// prevent bugs where people set encoding when piping
|
|
if (opts.encoding !== undefined) {
|
|
req.encoding = opts.encoding
|
|
delete req.headers['content-type']
|
|
delete req.headers.accept
|
|
}
|
|
|
|
if (opts.contentType) {
|
|
req.headers['content-type'] = opts.contentType
|
|
delete req.headers.accept
|
|
}
|
|
|
|
if (opts.accept) {
|
|
req.headers.accept = opts.accept
|
|
}
|
|
|
|
// http://guide.couchdb.org/draft/security.html#cookies
|
|
if (cfg.cookie) {
|
|
req.headers['X-CouchDB-WWW-Authenticate'] = 'Cookie'
|
|
req.headers.cookie = cfg.cookie
|
|
}
|
|
|
|
// http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
|
|
if (typeof opts.qs === 'object' && !isEmpty(opts.qs)) {
|
|
['startkey', 'endkey', 'key', 'keys', 'start_key', 'end_key'].forEach(function (key) {
|
|
if (key in opts.qs) {
|
|
qs[key] = JSON.stringify(opts.qs[key])
|
|
}
|
|
})
|
|
req.qs = qs
|
|
}
|
|
|
|
// add any cookies for this domain
|
|
const cookie = cfg.cookieJar.getCookieString(req.uri)
|
|
if (cookie) {
|
|
req.headers.cookie = cookie
|
|
}
|
|
|
|
if (opts.body) {
|
|
if (Buffer.isBuffer(opts.body) || opts.dontStringify) {
|
|
req.body = opts.body
|
|
} else {
|
|
req.body = JSON.stringify(opts.body, function (key, value) {
|
|
// don't encode functions
|
|
if (typeof (value) === 'function') {
|
|
return value.toString()
|
|
} else {
|
|
return value
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if (opts.form) {
|
|
req.headers['content-type'] =
|
|
'application/x-www-form-urlencoded; charset=utf-8'
|
|
req.body = querystring.stringify(opts.form).toString('utf8')
|
|
}
|
|
|
|
// ask request to render query string arrays as repeated values e.g.
|
|
// ?drilldown=["author","Dickens"]&drilldown=["publisher","Penguin"]
|
|
req.qsStringifyOptions = { arrayFormat: 'repeat' }
|
|
|
|
// This where the HTTP request is made.
|
|
// Nano used to use the now-deprecated "request" library but now we're going to
|
|
// use axios, so let's modify the "req" object to suit axios
|
|
req.url = req.uri
|
|
delete req.uri
|
|
req.method = req.method.toLowerCase()
|
|
req.params = req.qs
|
|
delete req.qs
|
|
req.paramsSerializer = {
|
|
serialize: (params) => querystring.stringify(params, { arrayFormat: 'brackets' })
|
|
}
|
|
req.data = req.body
|
|
delete req.body
|
|
req.maxRedirects = 0
|
|
if (opts.stream) {
|
|
req.responseType = 'stream'
|
|
} else if (opts.dontParse) {
|
|
req.responseType = 'arraybuffer'
|
|
}
|
|
|
|
// scrub and log
|
|
const scrubbedReq = {
|
|
method: req.method,
|
|
headers: JSON.parse(JSON.stringify(req.headers)),
|
|
url: req.url
|
|
}
|
|
scrubRequest(scrubbedReq, true)
|
|
log(scrubbedReq)
|
|
|
|
// add http agents
|
|
req.httpAgent = cfg.requestDefaults.agent || defaultHttpAgent
|
|
req.httpsAgent = cfg.requestDefaults.agent || defaultHttpsAgent
|
|
const ax = axios.create({
|
|
httpAgent: req.httpAgent,
|
|
httpsAgent: req.httpsAgent
|
|
})
|
|
|
|
// actually do the HTTP request
|
|
if (opts.stream) {
|
|
// return the Request object for streaming
|
|
const outStream = new stream.PassThrough()
|
|
ax(req)
|
|
.then((response) => {
|
|
response.data.pipe(outStream)
|
|
}).catch(e => {
|
|
streamResponseHandler(e, req, outStream)
|
|
})
|
|
return outStream
|
|
} else {
|
|
if (typeof callback === 'function') {
|
|
ax(req).then((response) => {
|
|
responseHandler(response, req, opts, null, null, callback)
|
|
}).catch((e) => {
|
|
responseHandler(e, req, opts, null, null, callback)
|
|
})
|
|
} else {
|
|
return new Promise((resolve, reject) => {
|
|
ax(req).then((response) => {
|
|
responseHandler(response, req, opts, resolve, reject)
|
|
}).catch((e) => {
|
|
responseHandler(e, req, opts, resolve, reject)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/server/authn.html#cookie-authentication
|
|
function auth (username, password, callback) {
|
|
return relax({
|
|
method: 'POST',
|
|
db: '_session',
|
|
form: {
|
|
name: username,
|
|
password
|
|
}
|
|
}, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/server/authn.html#post--_session
|
|
function session (callback) {
|
|
return relax({ db: '_session' }, callback)
|
|
}
|
|
|
|
// https://docs.couchdb.org/en/latest/api/server/common.html#api-server-root
|
|
function info (callback) {
|
|
return relax({ path: '' }, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/server/common.html#get--_db_updates
|
|
function updates (qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
return relax({
|
|
db: '_db_updates',
|
|
qs: opts
|
|
}, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/common.html#put--db
|
|
function createDb (dbName, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
if (missing(dbName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({ db: dbName, method: 'PUT', qs: opts }, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/common.html#delete--db
|
|
function destroyDb (dbName, callback) {
|
|
if (missing(dbName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({ db: dbName, method: 'DELETE' }, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/common.html#get--db
|
|
function getDb (dbName, callback) {
|
|
if (missing(dbName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({ db: dbName }, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/server/common.html#get--_all_dbs
|
|
function listDbs (callback) {
|
|
return relax({ db: '_all_dbs' }, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/server/common.html#get--_all_dbs
|
|
function listDbsAsStream () {
|
|
return relax({ db: '_all_dbs', stream: true })
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/compact.html#post--db-_compact
|
|
function compactDb (dbName, ddoc, callback) {
|
|
if (typeof ddoc === 'function') {
|
|
callback = ddoc
|
|
ddoc = null
|
|
}
|
|
if (missing(dbName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({
|
|
db: dbName,
|
|
doc: '_compact',
|
|
att: ddoc,
|
|
method: 'POST'
|
|
}, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/changes.html#get--db-_changes
|
|
function changesDb (dbName, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
if (missing(dbName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({ db: dbName, path: '_changes', qs: opts }, callback)
|
|
}
|
|
|
|
function changesDbAsStream (dbName, opts) {
|
|
return relax({ db: dbName, path: '_changes', stream: true, qs: opts })
|
|
}
|
|
|
|
function _serializeAsUrl (db) {
|
|
if (typeof db === 'object' && db.config && db.config.url && db.config.db) {
|
|
return urlResolveFix(db.config.url, encodeURIComponent(db.config.db))
|
|
} else {
|
|
try {
|
|
// if it parses, return it
|
|
const parsed = new URL(db)
|
|
return parsed.toString()
|
|
} catch (e) {
|
|
// otherwise treat it as a database name
|
|
return urlResolveFix(cfg.url, encodeURIComponent(db))
|
|
}
|
|
}
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/server/common.html#post--_replicate
|
|
function replicateDb (source, target, opts0, callback0) {
|
|
const { opts, callback } = getCallback(opts0, callback0)
|
|
|
|
if (missing(source, target)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
// _replicate
|
|
opts.source = _serializeAsUrl(source)
|
|
opts.target = _serializeAsUrl(target)
|
|
|
|
return relax({ db: '_replicate', body: opts, method: 'POST' }, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/server/common.html#uuids
|
|
function uuids (count, callback) {
|
|
if (typeof count === 'function') {
|
|
callback = count
|
|
count = 1
|
|
}
|
|
count = count || 1
|
|
return relax({ method: 'GET', path: '_uuids', qs: { count } }, callback)
|
|
}
|
|
|
|
// http://guide.couchdb.org/draft/replication.html
|
|
function enableReplication (source, target, opts0, callback0) {
|
|
const { opts, callback } = getCallback(opts0, callback0)
|
|
|
|
if (missing(source, target)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
// _replicator
|
|
opts.source = _serializeAsUrl(source)
|
|
opts.target = _serializeAsUrl(target)
|
|
|
|
return relax({ db: '_replicator', body: opts, method: 'POST' }, callback)
|
|
}
|
|
|
|
// http://guide.couchdb.org/draft/replication.html
|
|
function queryReplication (id, opts0, callback0) {
|
|
const { opts, callback } = getCallback(opts0, callback0)
|
|
if (missing(id)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({ db: '_replicator', method: 'GET', path: id, qs: opts }, callback)
|
|
}
|
|
|
|
// http://guide.couchdb.org/draft/replication.html
|
|
function disableReplication (id, rev, opts0, callback0) {
|
|
const { opts, callback } = getCallback(opts0, callback0)
|
|
if (missing(id, rev)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
const req = {
|
|
db: '_replicator',
|
|
method: 'DELETE',
|
|
path: id,
|
|
qs: Object.assign(opts, { rev })
|
|
}
|
|
return relax(req, callback)
|
|
}
|
|
|
|
function docModule (dbName) {
|
|
let docScope = {}
|
|
dbName = decodeURIComponent(dbName)
|
|
|
|
// http://docs.couchdb.org/en/latest/api/document/common.html#put--db-docid
|
|
// http://docs.couchdb.org/en/latest/api/database/common.html#post--db
|
|
function insertDoc (doc, qs0, callback0) {
|
|
const req = { db: dbName, body: doc, method: 'POST' }
|
|
|
|
let { opts, callback } = getCallback(qs0, callback0)
|
|
|
|
if (typeof opts === 'string') {
|
|
opts = { docName: opts }
|
|
}
|
|
|
|
if (opts) {
|
|
if (opts.docName) {
|
|
req.doc = opts.docName
|
|
req.method = 'PUT'
|
|
delete opts.docName
|
|
}
|
|
req.qs = opts
|
|
}
|
|
|
|
return relax(req, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/document/common.html#delete--db-docid
|
|
function destroyDoc (docName, rev, callback) {
|
|
if (missing(docName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
doc: docName,
|
|
method: 'DELETE',
|
|
qs: { rev }
|
|
}, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/document/common.html#get--db-docid
|
|
function getDoc (docName, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
|
|
if (missing(docName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({ db: dbName, doc: docName, qs: opts }, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/document/common.html#head--db-docid
|
|
function headDoc (docName, callback) {
|
|
if (missing(docName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
if (callback) {
|
|
relax({
|
|
db: dbName,
|
|
doc: docName,
|
|
method: 'HEAD',
|
|
qs: {}
|
|
}, callback)
|
|
} else {
|
|
// this function doesn't pass on the Promise from relax because it needs
|
|
// to return the headers when resolving the Promise
|
|
return new Promise(function (resolve, reject) {
|
|
relax({
|
|
db: dbName,
|
|
doc: docName,
|
|
method: 'HEAD',
|
|
qs: {}
|
|
}, function (err, body, headers) {
|
|
if (err) {
|
|
reject(err)
|
|
} else {
|
|
resolve(headers)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#get--db-_all_docs
|
|
function listDoc (qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
return relax({ db: dbName, path: '_all_docs', qs: opts }, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#get--db-_all_docs
|
|
function listDocAsStream (opts) {
|
|
return relax({ db: dbName, path: '_all_docs', qs: opts, stream: true })
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_all_docs
|
|
function fetchDocs (docNames, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
opts.include_docs = true
|
|
|
|
if (missing(docNames) || typeof docNames !== 'object' ||
|
|
!docNames.keys || !Array.isArray(docNames.keys) ||
|
|
docNames.keys.length === 0) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
path: '_all_docs',
|
|
method: 'POST',
|
|
qs: opts,
|
|
body: docNames
|
|
}, callback)
|
|
}
|
|
|
|
function fetchRevs (docNames, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
|
|
if (missing(docNames) || typeof docNames !== 'object' ||
|
|
!docNames.keys || !Array.isArray(docNames.keys) ||
|
|
docNames.keys.length === 0) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
path: '_all_docs',
|
|
method: 'POST',
|
|
qs: opts,
|
|
body: docNames
|
|
}, callback)
|
|
}
|
|
|
|
function view (ddoc, viewName, meta, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
|
|
if (missing(ddoc, viewName) && !meta.viewPath) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
if (typeof meta.stream !== 'boolean') {
|
|
meta.stream = false
|
|
}
|
|
|
|
// prevent mutation of the client qs object by using a clone
|
|
const qs1 = Object.assign({}, opts)
|
|
|
|
const viewPath = meta.viewPath || '_design/' + ddoc + '/_' + meta.type +
|
|
'/' + viewName
|
|
|
|
if (meta.type === 'search') {
|
|
return relax({
|
|
db: dbName,
|
|
path: viewPath,
|
|
method: 'POST',
|
|
body: qs1,
|
|
stream: meta.stream
|
|
}, callback)
|
|
} else if (qs1 && qs1.keys) {
|
|
const body = { keys: qs1.keys }
|
|
delete qs1.keys
|
|
return relax({
|
|
db: dbName,
|
|
path: viewPath,
|
|
method: 'POST',
|
|
qs: qs1,
|
|
body,
|
|
stream: meta.stream
|
|
}, callback)
|
|
} else if (qs1 && qs1.queries) {
|
|
const body = { queries: qs1.queries }
|
|
delete qs1.queries
|
|
return relax({
|
|
db: dbName,
|
|
path: viewPath,
|
|
method: 'POST',
|
|
qs: qs1,
|
|
body
|
|
}, callback)
|
|
} else {
|
|
const req = {
|
|
db: dbName,
|
|
method: meta.method || 'GET',
|
|
path: viewPath,
|
|
qs: qs1,
|
|
stream: meta.stream
|
|
}
|
|
|
|
if (meta.body) {
|
|
req.body = meta.body
|
|
}
|
|
|
|
return relax(req, callback)
|
|
}
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/ddoc/views.html#post--db-_design-ddoc-_view-view
|
|
function viewDocs (ddoc, viewName, qs, callback) {
|
|
return view(ddoc, viewName, { type: 'view' }, qs, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/ddoc/views.html#post--db-_design-ddoc-_view-view
|
|
function viewDocsAsStream (ddoc, viewName, qs) {
|
|
return view(ddoc, viewName, { type: 'view', stream: true }, qs)
|
|
}
|
|
|
|
// cloudant
|
|
function viewSearch (ddoc, viewName, qs, callback) {
|
|
return view(ddoc, viewName, { type: 'search' }, qs, callback)
|
|
}
|
|
|
|
// cloudant
|
|
function viewSearchAsStream (ddoc, viewName, qs) {
|
|
return view(ddoc, viewName, { type: 'search', stream: true }, qs)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/ddoc/render.html#get--db-_design-ddoc-_show-func
|
|
function showDoc (ddoc, viewName, docName, qs, callback) {
|
|
if (missing(ddoc, viewName, docName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return view(ddoc, viewName + '/' + docName, { type: 'show' }, qs, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/ddoc/render.html#put--db-_design-ddoc-_update-func-docid
|
|
function updateWithHandler (ddoc, viewName, docName, body, callback) {
|
|
if (typeof body === 'function') {
|
|
callback = body
|
|
body = undefined
|
|
}
|
|
if (missing(ddoc, viewName, docName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return view(ddoc, viewName + '/' + encodeURIComponent(docName), {
|
|
type: 'update',
|
|
method: 'PUT',
|
|
body
|
|
}, callback)
|
|
}
|
|
|
|
function viewWithList (ddoc, viewName, listName, qs, callback) {
|
|
return view(ddoc, listName + '/' + viewName, {
|
|
type: 'list'
|
|
}, qs, callback)
|
|
}
|
|
|
|
function viewWithListAsStream (ddoc, viewName, listName, qs, callback) {
|
|
return view(ddoc, listName + '/' + viewName, {
|
|
type: 'list', stream: true
|
|
}, qs, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_bulksDoc
|
|
function bulksDoc (docs, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
return relax({
|
|
db: dbName,
|
|
path: '_bulk_docs',
|
|
body: docs,
|
|
method: 'POST',
|
|
qs: opts
|
|
}, callback)
|
|
}
|
|
|
|
// http://docs.couchdb.org/en/latest/api/document/common.html#creating-multiple-attachments
|
|
function insertMultipart (doc, attachments, qs, callback) {
|
|
if (typeof qs === 'string') {
|
|
qs = { docName: qs }
|
|
}
|
|
qs = qs || {}
|
|
|
|
const docName = qs.docName
|
|
delete qs.docName
|
|
|
|
if (missing(doc, attachments, docName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
doc = Object.assign({ _attachments: {} }, doc)
|
|
|
|
const multipart = []
|
|
|
|
attachments.forEach(function (att) {
|
|
doc._attachments[att.name] = {
|
|
follows: true,
|
|
length: Buffer.isBuffer(att.data) ? att.data.length : Buffer.byteLength(att.data),
|
|
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
|
content_type: att.content_type
|
|
}
|
|
multipart.push(att)
|
|
})
|
|
|
|
multipart.unshift({
|
|
content_type: 'application/json',
|
|
data: JSON.stringify(doc),
|
|
name: 'document'
|
|
})
|
|
|
|
return relax({
|
|
db: dbName,
|
|
method: 'PUT',
|
|
contentType: 'multipart/related',
|
|
doc: docName,
|
|
qs,
|
|
multipart
|
|
}, callback)
|
|
}
|
|
|
|
function getMultipart (docName, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
opts.attachments = true
|
|
|
|
if (missing(docName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
doc: docName,
|
|
encoding: null,
|
|
accept: 'multipart/related',
|
|
qs: opts
|
|
}, callback)
|
|
}
|
|
|
|
function insertAtt (docName, attName, att, contentType, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
if (missing(docName, attName, att, contentType)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
att: attName,
|
|
method: 'PUT',
|
|
contentType,
|
|
doc: docName,
|
|
qs: opts,
|
|
body: att,
|
|
dontStringify: true
|
|
}, callback)
|
|
}
|
|
|
|
function getAtt (docName, attName, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
|
|
if (missing(docName, attName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
att: attName,
|
|
doc: docName,
|
|
qs: opts,
|
|
encoding: null,
|
|
dontParse: true
|
|
}, callback)
|
|
}
|
|
|
|
function getAttAsStream (docName, attName, opts) {
|
|
return relax({
|
|
db: dbName,
|
|
att: attName,
|
|
doc: docName,
|
|
qs: opts,
|
|
stream: true,
|
|
encoding: null,
|
|
dontParse: true
|
|
})
|
|
}
|
|
|
|
function destroyAtt (docName, attName, qs, callback) {
|
|
if (missing(docName, attName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
att: attName,
|
|
method: 'DELETE',
|
|
doc: docName,
|
|
qs
|
|
}, callback)
|
|
}
|
|
|
|
function find (query, callback) {
|
|
if (missing(query) || typeof query !== 'object') {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
path: '_find',
|
|
method: 'POST',
|
|
body: query
|
|
}, callback)
|
|
}
|
|
|
|
function findAsStream (query) {
|
|
return relax({
|
|
db: dbName,
|
|
path: '_find',
|
|
method: 'POST',
|
|
body: query,
|
|
stream: true
|
|
})
|
|
}
|
|
|
|
function createIndex (indexDef, callback) {
|
|
if (missing(indexDef) || typeof indexDef !== 'object') {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
path: '_index',
|
|
method: 'POST',
|
|
body: indexDef
|
|
}, callback)
|
|
}
|
|
|
|
function partitionInfo (partitionKey, callback) {
|
|
if (missing(partitionKey)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partitionKey)
|
|
}, callback)
|
|
}
|
|
|
|
function partitionedList (partitionKey, qs0, callback0) {
|
|
const { opts, callback } = getCallback(qs0, callback0)
|
|
if (missing(partitionKey)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partitionKey) + '/_all_docs',
|
|
qs: opts
|
|
}, callback)
|
|
}
|
|
|
|
function partitionedListAsStream (partitionKey, qs) {
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partitionKey) + '/_all_docs',
|
|
qs,
|
|
stream: true
|
|
})
|
|
}
|
|
|
|
function partitionedFind (partition, query, callback) {
|
|
if (missing(partition, query) || typeof query !== 'object') {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partition) + '/_find',
|
|
method: 'POST',
|
|
body: query
|
|
}, callback)
|
|
}
|
|
|
|
function partitionedFindAsStream (partition, query) {
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partition) + '/_find',
|
|
method: 'POST',
|
|
body: query,
|
|
stream: true
|
|
})
|
|
}
|
|
|
|
function partitionedSearch (partition, ddoc, searchName, opts, callback) {
|
|
if (missing(partition, ddoc, searchName, opts) || typeof opts !== 'object') {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partition) + '/_design/' + ddoc + '/_search/' + searchName,
|
|
qs: opts
|
|
}, callback)
|
|
}
|
|
|
|
function partitionedSearchAsStream (partition, ddoc, searchName, opts) {
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partition) + '/_design/' + ddoc + '/_search/' + searchName,
|
|
qs: opts,
|
|
stream: true
|
|
})
|
|
}
|
|
|
|
function partitionedView (partition, ddoc, viewName, opts, callback) {
|
|
if (missing(partition, ddoc, viewName)) {
|
|
return callbackOrRejectError(callback)
|
|
}
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partition) + '/_design/' + ddoc + '/_view/' + viewName,
|
|
qs: opts
|
|
}, callback)
|
|
}
|
|
|
|
function partitionedViewAsStream (partition, ddoc, viewName, opts) {
|
|
return relax({
|
|
db: dbName,
|
|
path: '_partition/' + encodeURIComponent(partition) + '/_design/' + ddoc + '/_view/' + viewName,
|
|
qs: opts,
|
|
stream: true
|
|
})
|
|
}
|
|
|
|
// db level exports
|
|
docScope = {
|
|
info: function (cb) {
|
|
return getDb(dbName, cb)
|
|
},
|
|
replicate: function (target, opts, cb) {
|
|
return replicateDb(dbName, target, opts, cb)
|
|
},
|
|
compact: function (cb) {
|
|
return compactDb(dbName, cb)
|
|
},
|
|
changes: function (qs, cb) {
|
|
return changesDb(dbName, qs, cb)
|
|
},
|
|
changesAsStream: function (qs) {
|
|
return changesDbAsStream(dbName, qs)
|
|
},
|
|
changesReader: new ChangesReader(dbName, relax),
|
|
auth,
|
|
session,
|
|
insert: insertDoc,
|
|
get: getDoc,
|
|
head: headDoc,
|
|
destroy: destroyDoc,
|
|
bulk: bulksDoc,
|
|
list: listDoc,
|
|
listAsStream: listDocAsStream,
|
|
fetch: fetchDocs,
|
|
fetchRevs,
|
|
config: { url: cfg.url, db: dbName },
|
|
multipart: {
|
|
insert: insertMultipart,
|
|
get: getMultipart
|
|
},
|
|
attachment: {
|
|
insert: insertAtt,
|
|
get: getAtt,
|
|
getAsStream: getAttAsStream,
|
|
destroy: destroyAtt
|
|
},
|
|
show: showDoc,
|
|
atomic: updateWithHandler,
|
|
updateWithHandler,
|
|
baseView: view,
|
|
search: viewSearch,
|
|
searchAsStream: viewSearchAsStream,
|
|
view: viewDocs,
|
|
viewAsStream: viewDocsAsStream,
|
|
find,
|
|
findAsStream,
|
|
createIndex,
|
|
viewWithList,
|
|
viewWithListAsStream,
|
|
server: serverScope,
|
|
replication: {
|
|
enable: function (target, opts, cb) {
|
|
return enableReplication(dbName, target, opts, cb)
|
|
},
|
|
disable: function (id, revision, opts, cb) {
|
|
return disableReplication(id, revision, opts, cb)
|
|
},
|
|
query: function (id, opts, cb) {
|
|
return queryReplication(id, opts, cb)
|
|
}
|
|
},
|
|
partitionInfo,
|
|
partitionedList,
|
|
partitionedListAsStream,
|
|
partitionedFind,
|
|
partitionedFindAsStream,
|
|
partitionedSearch,
|
|
partitionedSearchAsStream,
|
|
partitionedView,
|
|
partitionedViewAsStream
|
|
}
|
|
|
|
docScope.view.compact = function (ddoc, cb) {
|
|
return compactDb(dbName, ddoc, cb)
|
|
}
|
|
|
|
return docScope
|
|
}
|
|
|
|
// server level exports
|
|
serverScope = Object.assign(serverScope, {
|
|
db: {
|
|
create: createDb,
|
|
get: getDb,
|
|
destroy: destroyDb,
|
|
list: listDbs,
|
|
listAsStream: listDbsAsStream,
|
|
use: docModule,
|
|
scope: docModule,
|
|
compact: compactDb,
|
|
replicate: replicateDb,
|
|
replication: {
|
|
enable: enableReplication,
|
|
disable: disableReplication,
|
|
query: queryReplication
|
|
},
|
|
changes: changesDb,
|
|
updates
|
|
},
|
|
use: docModule,
|
|
scope: docModule,
|
|
request: relax,
|
|
relax,
|
|
dinosaur: relax,
|
|
auth,
|
|
session,
|
|
updates,
|
|
uuids,
|
|
info
|
|
})
|
|
|
|
const db = maybeExtractDatabaseComponent()
|
|
|
|
return db ? docModule(db) : serverScope
|
|
}
|
|
|
|
/*
|
|
* and now an ascii dinosaur
|
|
* _
|
|
* / _) ROAR! i'm a vegan!
|
|
* .-^^^-/ /
|
|
* __/ /
|
|
* /__.|_|-|_|
|
|
*
|
|
* thanks for visiting! come again!
|
|
*/
|
|
|
|
function urlResolveFix (couchUrl, dbName) {
|
|
if (/[^/]$/.test(couchUrl)) {
|
|
couchUrl += '/'
|
|
}
|
|
return new URL(dbName, couchUrl).toString()
|
|
}
|