diff --git a/README.md b/README.md index 24a6fc1..58faa7d 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,64 @@ couch.mango(dbName, mangoQuery, parameters).then(({data, headers, status}) => { }); ``` +## Trigger A Design Update +```javascript +const update = { + firstname: 'Rita', + middlename: '__delete__' +}; + +couch.design_update('database', 'someDocId', 'default', 'partialUpdate', update).then(({data, headers, status}) => { + // data is json response + // headers is an object with all response headers + // status is statusCode number +}, err => { + // either request error occured + // ...or err.code=EDOCMISSING if document is missing + // ...or err.code=EUNKNOWN if statusCode is unexpected +}); +``` + +## Subscribe To Changes +```javascript +const dbName = "database"; +const mangoQuery = { + selector: { + $gte: {firstname: 'Ann'}, + $lt: {firstname: 'George'} + } +}; + +// This method will first be called with a null change and valid context, before any changes are posted. +// The context has an unsubscribe() method that will release resources and disconnect. +function changeCallback (change, context) { + if (!change) { + // Store the context, and call context.unsubscribe() to release the socket when done listening. + } + else { + // change is the changed row + // heartbeats are filtered out + } +}; + +// These are the defaults +const parameters = { + feed: 'continuous', + heartbeat: true, + since: 'now' +}; + +couch.changes(dbName, mangoQuery, parameters, callback, 'selector', selectorFilter).then(({data, headers, status}) => { + // data is json response + // headers is an object with all response headers + // status is statusCode number +}, err => { + // either request error occured + // ...or err.code=EDOCMISSING if document is missing + // ...or err.code=EUNKNOWN if statusCode is unexpected +}); +``` + ## Insert a document ```javascript couch.insert("databaseName", { diff --git a/lib/node-couchdb.js b/lib/node-couchdb.js index 5b51145..879ece6 100644 --- a/lib/node-couchdb.js +++ b/lib/node-couchdb.js @@ -29,6 +29,7 @@ export default class NodeCouchDB { this._baseUrl = `${instanceOpts.protocol}://${instanceOpts.host}:${instanceOpts.port}`; this._cache = instanceOpts.cache; + this._auth = instanceOpts.auth; this._requestWrappedDefaults = request.defaults({ auth: instanceOpts.auth, @@ -240,6 +241,46 @@ export default class NodeCouchDB { }); } + /** + * Trigger a design update script in CouchDB. Returns a promise which is + * - resolved with {data, headers, status} object + * - rejected with `request` original error + + * @param dbName database name + * @param id documentId + * @param designId design document id + * @param updateId update script id + * @param data update payload + * @returns {*} + */ + design_update(dbName, docId, designId, updateId, data) { + if (!docId || !data) { + const err = new Error('id and data fields should exist when updating the document'); + err.code = 'EFIELDMISSING'; + + return Promise.reject(err); + } + + return this._requestWrapped({ + method: 'PUT', + url: `${this._baseUrl}/${dbName}/_design/${designId}/_update/${updateId}/${encodeURIComponent(docId)}`, + body: data + }).then(({res, body}) => { + this._checkDocumentManipulationStatus(res.statusCode, body) + + + if (res.statusCode !== 201 && res.statusCode !== 202) { + throw new RequestError('EUNKNOWN', `Unexpected status code while inserting document into the database: ${res.statusCode}`, body); + } + + return { + data: body, + headers: res.headers, + status: res.statusCode + }; + }); + } + /** * Delete a document in the database. Returns a promise which is * - resolved with {data, headers, status} object @@ -325,6 +366,56 @@ export default class NodeCouchDB { }); } + /** + /** + * Subscribes to the _changes endpoint. The default query params are to feed=continuous, timeout=Infinite, heartbeat=true, since=now + * @param dbName + * @param query + * @param changesCallback receives changes as JSON objects. + * This method will initially be called with a null change and a valid context. + * This initial callback will occur before any received data from CouchDB + * The context object has an unsubscribe() method needed for closing the connection and releasing resources. + * @returns {Promise.} + */ + changes(dbName, query, changesCallback, changesFilter, changesFilterData) { + for (let prop in query) { + if (KEYS_TO_ENCODE.includes(prop)) { + query[prop] = JSON.stringify(query[prop]); + } + } + + const requestOpts = { + url: `${this._baseUrl}/${dbName}/_changes`, + qs: query, + auth: this._requestWrappedDefaults.auth, + changesCallback + }; + + if (!changesFilter) { + requestOpts.method = 'GET'; + } else { + requestOpts.method = 'POST'; + requestOpts.body = changesFilterData + } + + return this._requestWrapped(requestOpts) + .then(({res, body}) => { + if (res.statusCode === 404) { + throw new RequestError('EDOCMISSING', 'Document is not found', body); + } + + if (res.statusCode !== 200 && res.statusCode !== 304) { + throw new RequestError('EUNKNOWN', `Unexpected status code while fetching documents from the database: ${res.statusCode}`, body); + } + + return { + data: body, + headers: res.headers, + status: res.statusCode + }; + }); + } + /** * Get UUIDs for new documents. Returns a promise which is * - resolved with array of new unique ids @@ -378,6 +469,10 @@ export default class NodeCouchDB { opts = {url: opts}; } + if (opts.changesCallback) { + return this._handleChangesRequest(opts); + } + const cacheKey = this._getCacheKey(opts); const whenCacheChecked = (!this._cache || (opts.method && opts.method !== 'GET')) ? Promise.resolve({}) @@ -410,6 +505,73 @@ export default class NodeCouchDB { }); } + _handleChangesRequest(opts) { + return new Promise((resolve, reject) => { + const feed = opts.qs.feed || 'continuous'; + const heartbeat = opts.qs.heartbeat || 'true'; + const since = opts.qs.since || 'now'; + const url = `${opts.url}/_changes?feed=${feed}&heartbeat=${heartbeat}&since=${since}`; + // request.js was not working in browser, so we use native + var httpRequest = new XMLHttpRequest(); + httpRequest.open("GET", url, true); // async + if (this._auth.user !== undefined){ + var authorizationBasic = btoa(this._auth.user + ':' + this._auth.pass); + httpRequest.setRequestHeader('Authorization', 'Basic ' + authorizationBasic); + } + httpRequest.setRequestHeader('Accept', 'application/json'); + httpRequest.onreadystatechange = OnStateChange; + + function OnStateChange() { + switch (httpRequest.readyState) { + case 1: + break; + case 2: + break; + case 3: + const json = httpRequest.responseText; + json.split('\n') + .filter(line => line.length > 0) + .forEach(line => { + let parsed; + try { + parsed = JSON.parse(line); + } catch (e) { + //console.warn(e); + return; + } + try { + opts.changesCallback(parsed, context); + } catch (e) { + console.warn(e); + } + }); + break; + case 4: + break; + default: + break; + }; + } + httpRequest.send(null); + + const context = { + unsubscribe: function () { + httpRequest.abort(); + //BUG request.js not working in browser + //req.abort(); + } + }; + opts.changesCallback(null, context); + resolve({ + res: { + statusCode: 200, + headers: [] + }, + body: '', + }); + }); + } + /** * Gets cache key built from request options diff --git a/test/test.js b/test/test.js index c4a4047..62890d2 100644 --- a/test/test.js +++ b/test/test.js @@ -48,7 +48,7 @@ describe('node-couchdb tests', () => { for (let method of [ 'useCache', 'listDatabases', 'createDatabase', 'dropDatabase', - 'insert', 'update', 'del', 'get', 'mango', + 'insert', 'update', 'del', 'get', 'mango', 'changes', 'uniqid' ]) { assert.typeOf(couch[method], 'function', `instance[${method}] is not a function`);