Skip to content

Commit

Permalink
Add support for changes and design_update commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Flamenco committed Feb 1, 2017
1 parent 1baea57 commit 3ccb874
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 1 deletion.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
162 changes: 162 additions & 0 deletions lib/node-couchdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.<TResult>}
*/
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
Expand Down Expand Up @@ -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({})
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down

0 comments on commit 3ccb874

Please sign in to comment.