Created
December 31, 2016 14:01
-
-
Save bennadel/b93d4dcd5a678ba5f604f5d9a181843a to your computer and use it in GitHub Desktop.
Configuring PouchDB After Login For A Database-Per-User Architecture In Angular 2.4.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Component } from "@angular/core"; | |
// Import the application components and services. | |
import { FriendService } from "./friend.service"; | |
import { IFriend } from "./friend.service"; | |
import { PouchDBService } from "./pouchdb.service"; | |
interface IAddForm { | |
name: string; | |
} | |
@Component({ | |
moduleId: module.id, | |
selector: "my-app", | |
styleUrls: [ "./app.component.css" ], | |
template: | |
` | |
<!-- BEIGN: Logged-out view. --> | |
<template [ngIf]="( user === null )"> | |
<ul> | |
<li> | |
<a (click)="login( 'ben' )">Login as Ben</a> | |
</li> | |
<li> | |
<a (click)="login( 'kim' )">Login as Kim</a> | |
</li> | |
</ul> | |
</template> | |
<!-- END: Logged-out view. --> | |
<!-- BEIGN: Logged-in view. --> | |
<template [ngIf]="( user !== null )"> | |
<p> | |
<strong>Logged-in as {{ user }}</strong>. | |
<a (click)="logout()">Logout</a>. | |
</p> | |
<ul> | |
<li *ngFor="let friend of friends"> | |
{{ friend.name }} | |
— | |
<a (click)="deleteFriend( friend )">Delete</a> | |
</li> | |
</ul> | |
<div class="form"> | |
<input | |
type="text" | |
[value]="addForm.name" | |
(input)="addForm.name = $event.target.value" | |
(keydown.Enter)="processAddForm()" | |
/> | |
<button type="button" (click)="processAddForm()">Add Friend</button> | |
</div> | |
</template> | |
<!-- END: Logged-in view. --> | |
` | |
}) | |
export class AppComponent { | |
public addForm: IAddForm; | |
public friends: IFriend[]; | |
public user: string; | |
private friendService: FriendService; | |
private pouchdbService: PouchDBService; | |
// I initialize the component. | |
constructor( | |
friendService: FriendService, | |
pouchdbService: PouchDBService | |
) { | |
this.friendService = friendService; | |
this.pouchdbService = pouchdbService; | |
this.addForm = { | |
name: "" | |
}; | |
// To start out, the Friends collection will be empty; and, it must remain | |
// empty until the user logs-in because, until then, the PouchDB database has | |
// not been configured and we won't know where to read data from. | |
this.friends = []; | |
this.user = null; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I delete the given friend from the list. | |
public deleteFriend( friend: IFriend ) : void { | |
this.friendService | |
.deleteFriend( friend.id ) | |
.then( | |
() : void => { | |
this.loadFriends(); | |
}, | |
( error: Error ) : void => { | |
console.log( "Error:", error ); | |
} | |
) | |
; | |
} | |
// I login the user with the given identifier. | |
public login( userIdentifier: string ) : void { | |
// Now that a new user is logging in, we want to teardown any existing PouchDB | |
// database and reconfigure a new PouchDB database for the given user. This way, | |
// each user gets their own database in our database-per-user model. | |
// -- | |
// CAUTION: For simplicity, this is in the app-component; but, it should probably | |
// be encapsulated in some sort of "session" service. | |
this.pouchdbService.configureForUser( userIdentifier ); | |
this.user = userIdentifier; | |
// Once the new database is configured (synchronously), load the user's friends. | |
this.loadFriends(); | |
} | |
// I log the current user out. | |
public logout() : void { | |
// When logging the user out, we want to teardown the currently configured | |
// PouchDB database. This way, we can ensure that rogue asynchronous actions | |
// aren't going to accidentally try to interact with the database. | |
// -- | |
// CAUTION: For simplicity, this is in the app-component; but, it should probably | |
// be encapsulated in some sort of "session" service. | |
this.pouchdbService.teardown(); | |
this.user = null; | |
this.friends = []; | |
} | |
// I process the "add" form, creating a new friend with the given name. | |
public processAddForm() : void { | |
if ( ! this.addForm.name ) { | |
return; | |
} | |
this.friendService | |
.addFriend( this.addForm.name ) | |
.then( | |
( id: string ) : void => { | |
console.log( "New friend added:", id ); | |
this.loadFriends(); | |
this.addForm.name = ""; | |
}, | |
( error: Error ) : void => { | |
console.log( "Error:", error ); | |
} | |
) | |
; | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I load the persisted friends collection into the list. | |
private loadFriends() : void { | |
this.friendService | |
.getFriends() | |
.then( | |
( friends: IFriend[] ) : void => { | |
// NOTE: Since the persistence layer is not returning the data | |
// in any particular order, we're going to explicitly sort the | |
// collection by name. | |
this.friends = this.friendService.sortFriendsCollection( friends ); | |
}, | |
( error: Error ) : void => { | |
console.log( "Error", error ); | |
} | |
) | |
; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Injectable } from "@angular/core"; | |
// Import the application components and services. | |
import { IPouchDBAllDocsResult } from "./pouchdb.interfaces"; | |
import { IPouchDBGetResult } from "./pouchdb.interfaces"; | |
import { IPouchDBPutResult } from "./pouchdb.interfaces"; | |
import { IPouchDBRemoveResult } from "./pouchdb.interfaces"; | |
import { PouchDBService } from "./pouchdb.service"; | |
export interface IFriend { | |
id: string; | |
name: string; | |
} | |
interface IPouchDBGetFriendResult extends IPouchDBGetResult { | |
name: string; | |
} | |
@Injectable() | |
export class FriendService { | |
private pouchdbService: PouchDBService; | |
// I initialize the Friend service. | |
constructor( pouchdbService: PouchDBService ) { | |
// Rather than constructing a PouchDB instance directly, we're going to use the | |
// PouchDBService to provide a database instance on the fly. This way, the | |
// configuration for the PouchDB instance can be changed at any point during the | |
// application life-cycle. Each database interaction starts with a call to | |
// this.getDB() to access the "current" database rather than a cached one. | |
this.pouchdbService = pouchdbService; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I add a new friend with the given name. Returns a promise of the generated id. | |
public addFriend( name: string ) : Promise<string> { | |
// NOTE: All friends are given the key-prefix of "friend:". This way, when we go | |
// to query for friends, we can limit the scope to keys with in this key-space. | |
var promise = this.getDB() | |
.put({ | |
_id: ( "friend:" + ( new Date() ).getTime() ), | |
name: name | |
}) | |
.then( | |
( result: IPouchDBPutResult ) : string => { | |
return( result.id ); | |
} | |
) | |
; | |
return( promise ); | |
} | |
// I delete the friend with the given id. Returns a promise. | |
public deleteFriend( id: string ) : Promise<void> { | |
this.testId( id ); | |
// NOTE: For the "delete" action, we need to perform a series of database calls. | |
// In reality, these will be "instantaneous". However, philosophically, these are | |
// asynchronous calls. As such, I am setting the DB to a function-local value in | |
// order to ensure that both database calls - that compose the one workflow - are | |
// made on the same database. This eliminates the possibility that the "current | |
// database" may change in the middle of these chained actions. | |
var db = this.getDB(); | |
// When we delete a document, we have to provide a document that contains, at the | |
// least, the "_id" and the "_rev" property. Since the calling context doesn't | |
// have this, we'll use the .get() method to get the current doc, then use that | |
// result to delete the winning revision of the document. | |
var promise = db | |
.get( id ) | |
.then( | |
( doc: IPouchDBGetFriendResult ) : any => { | |
return( db.remove( doc ) ); | |
} | |
) | |
.then( | |
( result: IPouchDBRemoveResult ) : void => { | |
// Here, I'm just stripping out the result so that the PouchDB | |
// response isn't returned to the calling context. | |
return; | |
} | |
) | |
; | |
return( promise ); | |
} | |
// I get the collection of friends (in no particular sort order). Returns a promise. | |
public getFriends() : Promise<IFriend[]> { | |
var promise = this.getDB() | |
.allDocs({ | |
include_docs: true, | |
// In PouchDB, all keys are stored in a single collection. So, in order | |
// to return just the subset of "Friends" keys, we're going to query for | |
// all documents that have a "friend:" key prefix. This is known as | |
// "creative keying" in the CouchDB world. | |
startkey: "friend:", | |
endKey: "friend:\uffff" | |
}) | |
.then( | |
( result: IPouchDBAllDocsResult ) : IFriend[] => { | |
// Convert the raw data storage into something more natural for the | |
// calling context to consume. | |
var friends = result.rows.map( | |
( row: any ) : IFriend => { | |
return({ | |
id: row.doc._id, | |
name: row.doc.name | |
}); | |
} | |
); | |
return( friends ); | |
} | |
) | |
; | |
return( promise ); | |
} | |
// I sort the given collection of friends (in place) based on the name property. | |
public sortFriendsCollection( friends: IFriend[] ) : IFriend[] { | |
friends.sort( | |
function( a: IFriend, b: IFriend ) : number { | |
if ( a.name.toLowerCase() < b.name.toLowerCase() ) { | |
return( -1 ); | |
} else { | |
return( 1 ); | |
} | |
} | |
); | |
return( friends ); | |
} | |
// I test the given id to make sure it is valid for the Friends key-space. Since all | |
// PouchDB documents are stored in a single collection, we have to ensure that the | |
// given ID pertains to the subset of documents that represents Friends. If the id is | |
// valid, I return quietly; otherwise, I throw an error. | |
public testId( id: string ) : void { | |
if ( ! id.startsWith( "friend:" ) ) { | |
throw( new Error( "Invalid Id" ) ); | |
} | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I return the currently-configured PouchDB instance. | |
private getDB() : any { | |
return( this.pouchdbService.getDB() ); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// The PouchDB library is delivered as a CommonJS module and I am not yet sure how to | |
// configure my System.js setup to allow for a more simple import statement. This is the | |
// only thing that I can get to work at this time. | |
// -- | |
// CAUTION: TypeScript still complains, "Cannot find module 'pouchdb'." | |
import * as PouchDB from "pouchdb"; | |
export class PouchDBService { | |
private db: any; | |
// I initialize the service. | |
constructor() { | |
this.db = null; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I teardown any existing PouchDB instance and configure a new one for the given | |
// user identifier. All subsequent calls to getDB() will return the newly configured | |
// PouchDB instance. | |
public configureForUser( userIdentifier: string ) : void { | |
this.teardown(); | |
this.db = new PouchDB( this.getDatabaseName( userIdentifier ) ); | |
// TODO: Setup replication for remote database (not needed for this demo). | |
console.warn( "Configured new PouchDB database for,", this.db.name ); | |
} | |
// I get the active PouchDB instance. Throws an error if no PouchDB instance is | |
// available (ie, user has not yet been configured with call to .configureForUser()). | |
public getDB() : any { | |
if ( ! this.db ) { | |
throw( new Error( "Database is not available - please configure an instance." ) ); | |
} | |
return( this.db ); | |
} | |
// I teardown / deconfigure the existing database instance (if there is one). | |
// -- | |
// CAUTION: Subsequent calls to .getDB() will fail until a new instance is configured | |
// with a call to .configureForUser(). | |
public teardown() : void { | |
if ( ! this.db ) { | |
return; | |
} | |
// TODO: Stop remote replication for existing database (not needed for this demo). | |
this.db.close(); | |
this.db = null; | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I return a normalized database name for the given user identifier. | |
private getDatabaseName( userIdentifier: string ) : string { | |
// Database naming restrictions from https://wiki.apache.org/couchdb/HTTP_database_API | |
// -- | |
// A database must be named with all lowercase letters (a-z), digits (0-9), or | |
// any of the _$()+-/ characters and must end with a slash in the URL. The name | |
// has to start with a lowercase letter (a-z)... Uppercase characters are NOT | |
// ALLOWED in database names. | |
var dbName = userIdentifier | |
.toLowerCase() | |
.replace( /[^a-z0-9_$()+-]/g, "-" ) | |
; | |
return( "javascript-demos-pouchdb-angular2-" + dbName ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment