Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created December 31, 2016 14:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bennadel/b93d4dcd5a678ba5f604f5d9a181843a to your computer and use it in GitHub Desktop.
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
// 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 }}
&mdash;
<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 );
}
)
;
}
}
// 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() );
}
}
// 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