@mkeen/rxcouch
Version:
Real Time RxJs Based CouchDB Client
305 lines (214 loc) β’ 10.1 kB
Markdown
# RxCouch
`π Universal`
RxJS powers this real-time CouchDB client written in TypeScript that runs in the browser or nodejs. The change feed is handled automatically. Everything's a `BehaviorSubject`, so you get two-way binding for free. Enough said.
## Prerequisites
RxJS 6+
CouchDB 2.3+, CouchDB 3.0+
## Installation
**NPM:** `npm install /rxcouch`
**Yarn:** `yarn install /rxcouch`
## Developer Documentation
### Including RxCouch in Your Project
```typescript
import { CouchDB } from '@mkeen/rxcouch';
```
### Initialize RxCouch for Connecting to a CouchDB Database
```typescript
const couchDbConnection = new CouchDB(
{
dbName: 'people',
}
);
```
### Basic Configuration Options
CouchDB is initialized with: `(RxCouchConfig, AuthorizationBehavior?, Observable<CouchDBCredentials>?)`
An `RxCouchConfig` looks like this by default:
{ `host`: 'localhost'
`port`: 5984
`ssl`: false
`trackChanges`: true,
`dbName`: '_users' }
### Configuring Authentication
CouchDB supports [Basic Auth, Cookies, and JWT](https://docs.couchdb.org/en/stable/api/server/authn.html) authentication schemes. This library supports Cookies or JWT (bearer token). For development purposes only, this library also supports open installs of couchdb without security.
Let's take a look at how to initialize RxCouch for connecting to a CouchDB database that has Cookie Authentication configured.
```typescript
import { BehaviorSubject } from 'rxjs';
import { CouchDB,
CouchSession,
AuthorizationBehavior } from '/rxcouch';
const couchSession: CouchSession = new CouchSession(
AuthorizationBehavior.cookie,
`${COUCH_SSL? 'https://' : 'http://'}${COUCH_HOST}:${COUCH_PORT}/_session`,
new BehaviorSubject({username: 'username', password: 'password'}}),
);
const couchDbConnection = new CouchDB(
{
dbName: 'people',
host: 'localhost',
trackChanges: true,
},
couchSession
);
//...
```
The big detail to note here is that the final argument passed to the CouchSession initializer is an `Observable`. In this example it's hardcoded. In a real-world implementation, you'll create an `Observable` that emits when a user submits a login form, or when a configuration value is read, and pass that `Observable` into the initializer.
It's common and recommended to share a single session instance across several CouchDB instances.
Since we're hardcoding the credentials `Observable` argument, the above example will result in an authentication attempt being made to the speficied CouchDB host (without using HTTPS) immediately.
### Getting Info About the Current Session
To determine which user (if any) is currently logged into CouchDB, you can call the `session()` function. `session` returns an `Observable` that emits a `CouchDBSession`.
```typescript
//...
couchDbConnection.session().subscribe((session: CouchDBSession) => {
console.log('Currently session info: ', session);
});
```
If you're using CouchDB to authenticate users in your application, you could call `session` when your app is initialized in order to determine if a user is logged in, and if so, any other information stored in the `_users` document.
### Dealing with Documents
Whether authentication is used or not, a document is always returned to you by RxCouch in the form a `BehaviorSubject` which provides a real-time two-way data binding with the document.
#### Subscribe to Any Document in Real Time
```typescript
interface Person {
name: string;
email?: string;
phone?: string;
}
const myDocument: BehaviorSubject<Person> = couchDbConnection.doc('778...05b');
myDocument.subscribe((document: Person) => {
console.log('Most recent person: ', document);
});
//...
```
###### Result
```typescript
Most recent person: { _id: "7782f0743bee05005a548ba8af00205b", _rev: "10-bcaab49ec87c678686984d1c4873cd3e", name: 'Mike" }
```
#### Update The Same Document in Real Time
```typescript
//...
const myCompletelyChangedDocument = {
name: 'some new name here',
email: 'mwk@mikekeen.com',
phone: '323-209-5336'
}
myDocument.next(myCompletelyChangedDocument);
```
###### All Connected Clients Result:
```typescript
Most recent person: { _id: "7782f0743bee05005a548ba8af00205b", _rev: "11-bf3003bb5f63b875db4284f319a0b918", name: "some new name here", email: "mwk.com", phone: "323-209-5336"}
```
### Creating And Modifying Documents
In the above example, a document was fetched by its `_id`. The `doc()` function alternatively accepts an object as an argument. If an object is passed in, one of two things will happen:
1. If the object contains both an `_id` and `_rev` field, the rest of the fields in the object will be used to update the document that matches the `_id`.
2. If the object does not contain both an `_id` and a `_rev` field, then a new document will be created based on the fields contained in the object passed.
Whichever of the above happens, `doc` returns a `BehaviorSubject` that will reflect all future changes to the relevant document, and pass all changes upstream when `.next()` is called.
#### Create a New Document and Subscribe to Future Changes
```typescript
//...
const newlyCreatedPerson = couchDbConnection.doc({
name: 'tracy',
email: 'tracy@company.com',
phone: '323-209-5336'
}).subscribe((doc: Person) => {
console.log('Person Feed: ', doc);
});
```
###### Result
```typescript
Person Feed: {"_id": "7782f0743bee05005a548ba8af00b4f5", "_rev": "1-2fee21d51258845545ea6506ab138919", "name": "tracy", "email": "tracy.com", "phone": "323-209-5336"}
```
#### Modify an Existing Document and Subscribe to Future Changes
There are two ways to modify a document that already exists in CouchDB.
One way, is to simply pass a modified version of the existing document to `doc` like in the example below:
```typescript
//...
couchDbConnection.doc({
_id: '7782f0743bee05005a548ba8af00b4f5',
_rev: '1-2fee21d51258845545ea6506ab138919',
name: 'tracy',
email: 'tracy@company-modified.com',
phone: '323-209-5336'
}).subscribe((doc) => {
console.log('Document Modified: ', doc);
})
```
###### Result
```typescript
Document Modified: { "_id": "7782f0743bee05005a548ba8af00b4f5", "_rev": "2-e654adf15f28b99f26ae1f0dbe8e7c36", "name": "tracy", "email": "tracy-modified.com", "phone": "323-209-5336" }
Person Feed: {"_id": "7782f0743bee05005a548ba8af00b4f5", "_rev": "2-e654adf15f28b99f26ae1f0dbe8e7c36", "name": "tracy", "email": "tracy-modified.com", "phone": "323-209-5336" }
```
Another way to modify a document that already exists in CouchDB is to just call `next` on an already fetched document's `BehaviorSubject`.
```typescript
//..
newlyCreatedPerson.next({
name: 'tracy Modified!',
email: 'tracy@company-modified-again.com',
phone: '323-209-5336'
});
```
###### Result
```typescript
Person Feed: {"_id": "7782f0743bee05005a548ba8af00b4f5", "_rev": "3-8da970f593132c80ccee83fc4708ce33", "name": "tracy Modified!", "email": "tracy-modified-again.com", "phone": "323-209-5336" }
```
### Finding Documents With a Query
RxCouch exposes `find` from `CouchDB` that uses [CouchDB Selector Syntax](https://docs.couchdb.org/en/2.2.0/api/database/find.html#selector-syntax) to make queries, and returns a list of matching documents. Warning: Documents returned are not `BehaviorSubject`s!
### Get a List of Documents That Match a Query
Call `find` with `(CouchDBFindQuery)`.
An `CouchDBFindQuery`, when passed into `find`, should conform to [CouchDB Selector Syntax](https://docs.couchdb.org/en/2.2.0/api/database/find.html#selector-syntax). It can have the following optional properties of the following types:
`selector: CouchDBFindSelector`
`limit: number`
`skip: number`
`sort: CouchDBFindSort[]`
`fields: string[]`
`use_index: string | []`
`r: number`
`bookmark: string`
`update: boolean`
`stable: boolean`
`stale: string`
`execution_stats: boolean`
A call to `find` will return an `Observable` that will emit a list of documents that match the passed query.
```typescript
//...
couchDbConnection.find({
selector: {
name: 'tracy'
}
}).subscribe((matchingPeople: Person[]) => {
console.log('Matching people: ', matchingPeople);
});
```
###### Result
```typescript
Matching people: [{ "_id": "7782f0743bee05005a548ba8af00b4f5", "_rev": "3-8da970f593132c80ccee83fc4708ce33", "name": "tracy Modified!", "email": "tracy-modified-again.com", "phone": "323-209-5336" }]
```
#### Get Real-Time Feeds for Found Documents
```typescript
//...
couchDbConnection.find({
selector: {
name: 'some new name here'
}
}).subscribe((matchingPeople: Person[]) => {
const documentFeeds = matchingPeople.map((person: Person) => {
return couchDbConnection.doc(matchingPeople)
});
documentFeeds.forEach((feed) => {
feed.subscribe(
(document: Person) => console.log('Latest person: ', document)
);
});
});
```
###### Result
```typescript
Latest person: { "_id": "7782f0743bee05005a548ba8af00b4f5", "_rev": "3-8da970f593132c80ccee83fc4708ce33", "name": "tracy Modified!", "email": "tracy-modified-again.com", "phone": "323-209-5336" }
```
## π Under the Hood
#### Document Tracking
Documents are automatically cached and then tracked for changes. `CouchDBDocumentCollection` handles both caching and change tracking.
##### Cache
Instances of `CouchDB` have a method called `doc`. Any documents that flow through `doc` are cached in a `CouchDBDocumentCollection`. They can later be retrieved by _id in the form of a `BehaviorSubject` that supports two way binding. A hash of the document is indexed (by document id) for change tracking purposes.
##### Change Tracking
Before document changes are propagated either to or from a `BehaviorSubject`, the potentially changed document is hashed and compared against a previously indexed hash of the document. If the hashes don't match, the document has changed, and it will be passed into the `next` method of the relavent indexed `BehaviorSubject`. Finally, the indexed hash entry for the document will be updated.
## Thank you for using RxCouch!
πΊπΈ