Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes Support #24

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this to another PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#26

```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
});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit tricky API and it's more similar to publish-subscribe, than to promises which get fulfilled one time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use the promise to return the result of the subscribe request, and the callback for the actual subscription.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean exactly this. Promise represents the asynchronous operation which finishes either with some useful result (resolve) or fails with some error (reject). Publish/Subscribe (EventEmitter, Rx.Observable) represent continuous operations which can emit different events.

Is there any need for result of subscribe request? How should the callback code work if subscribe request fails?

Copy link
Contributor Author

@Flamenco Flamenco Feb 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback is not invoked at all if the promise fails. We could return an interface that has a subscribe method in the promise, but then we would have to cache any intermediate data.

I think you need both the promise and subscription. There is an async operation and a separate subscription.

I guess we could send an error to the subscription if the connection fails; Then you would not need to return a promise at all. But that would break chaining operations, and make the API less consistent.

```

## Insert a document
```javascript
couch.insert("databaseName", {
Expand Down
161 changes: 161 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,55 @@ 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 +468,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 +504,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