pouchorm

PouchDB ORM for Typescript. Work with Multiple Pouch databases or multiple collections in a single database. Supports all platforms PouchDB supports.

Usage no npm install needed!

<script type="module">
  import pouchorm from 'https://cdn.skypack.dev/pouchorm';
</script>

README

PouchORM

The definitive ORM for working with PouchDB.

The Pouch/Couch database ecosystem is still the best choice for client-side products that need the complex (and seemingly oxymoronic) sibling-features of Offline-First and Realtime collaboration.

But the base pouchDB interface is rather bare and oft-times painful to work with. PouchORM rolls in to the rescue and does a lot of heavy lifting for you and makes it easy to get going with PouchDB so you can focus on your data... not the database.

Highlights

  • Typescript is a first class citizen.
    • Will work with raw javascript, but you'll be missing out on the cool Typescript dev perks.
  • Introduces the concept of Collections to pouchdb
    • Multiple collections in a single Database
    • Multiple collections in multiple Databases
  • Supports web, electron, react-native, and anything else pouchdb supports.
  • Supports optional class validation

To install

npm i pouchorm

or if you prefer yarn: yarn add pouchorm

When using the optional class validation, also install class-validator as a dependency of your project using npm or yarn.

Changelog

  • v2.0.2
    • Added optional ID generics i.e PouchCollection<T,IDType>, IModel, and PouchModel
  • v2.0.0
    • feat: changed meta name $updatedBy to simply $by to conserve space.
  • v1.6.0
    • feat: Added simplified audit trace, specified by PouchORM.setUserId(...).
  • v1.5.0
    • feat: Added ORM support for managing syncing between multiple databases
  • v1.3
    • feat: Added Delta sync support

How to Use

Consider this definition of a model and it's collection.

// Person.ts

    import {IModel, PouchCollection, PouchORM} from "pouchorm";
    PouchORM.LOGGING = true; // enable diagnostic logging if desired
    
    export interface IPerson extends IModel {
        name: string;
        age: number;
        otherInfo: { [key: string]: any };
    }
    
    export class PersonCollection extends PouchCollection<IPerson> {
        
        // Optional. Overide to define collection-specific indexes.
        async beforeInit(): Promise<void> {
            
            await this.addIndex(['age']); // be sure to create an index for what you plan to filter by.
        }

        // Optional. Overide to perform actions after all the necessary indexes have been created.
        async afterInit(): Promise<void> {
           
        }
    
    }
    

IModel contains the meta fields needed by PouchDB and PouchORM to operate so every model interface definition needs to extend it. Only supports the same field types as pouchDB does.

PouchCollection is a generic abstract class that should be given your model type. This helps it guide you later and give you suggestions of how to work with your model.

In the case that you want the syntactic sugar of classing your models, or you want to use class validation, PouchModel is a generic class implementation of IModel that can be extended.

export class Person extends PouchModel<Person> {
    @IsString()
    name: string

    @IsNumber()
    age: number

    otherInfo: { [key: string]: any };
}

export class PersonCollection extends PouchCollection<Person> {
...

If you need to do things before and after initialization, you can override the async hook functions: beforeInit or afterInit;

Now that we have defined our Model and a Collection for that model, Here is how we instantiate collections. You should probably define and export collection instances somewhere in your codebase that you can easily import anywhere in your app.


    // instantiate a collection by giving it the dbname it should use
    export const personCollection: PersonCollection = new PersonCollection('db1');

    // Another collection. Notice how it shares the same dbname we passed into the previous collection instance.
    export const someOtherCollection: SomeOtherCollection = new SomeOtherCollection('db1'); 
    
    // In case we needed the same model but for a different database
    export const personCollection2: PersonCollection = new PersonCollection('db2');

From this point:

  • We have our definitions
  • We have our collection instances

We are ready to start CRUDing!

    import {personCollection} from '...'

    // Using collections
    let somePerson: IPerson = {
        name: 'Basket Mouth',
        age: 99,
    }
    let anotherPerson: IPerson = {
        name: 'Bovi',
        age: 45,
    }

    somePerson = await personCollection.upsert(somePerson);
    anotherPerson = await personCollection.upsert(anotherPerson);
    
    // somePerson has been persisted and will now also have some metafields like _id, _rev, etc.

    somePerson.age = 45;
    somePerson = await personCollection.upsert(somePerson);

    // changes to somePerson has been persisted. _rev would have also changed.

    const result: IPerson[] = await personCollection.find({age: 45})
    
    // result.length === 2

PouchCollection instance API reference

Consider that T is the provided type or class definition of your model.

Constructor

new Collection(dbname: string, opts?: PouchDB.Configuration.DatabaseConfiguration, validate: ClassValidate = ClassValidate.OFF)

Methods

  • find(criteria: Partial<T>): Promise<T[]>

  • findOrFail(criteria: Partial<T>): Promise<T[]>

  • findOne(criteria: Partial<T>): Promise<T>

  • findOneOrFail(criteria: Partial<T>): Promise<T>

  • findById(_id: string): Promise<T>

  • findByIdOrFail(_id: string): Promise<T>

  • removeById(id: string): Promise<void>

  • remove(item: T): Promise<void>

  • upsert(item: T, deltaFunc?: (existing: T) => T): Promise<T>

  • bulkUpsert(items: T[]): Promise<(Response|Error)[]>

  • bulkRemove(items: T[]): Promise<(Response|Error)[]>

Class Validation

Class validation brings the power of strong typing and data validation to PouchDB.

The validation uses the class-validator library, and should work anywhere that PouchDB works. This can be turned on at the global PouchORM level using PouchORM.VALIDATE or at the collection level when creating a new instance of PouchCollection.

By default, upsert calls PouchORM.getClassValidator() when validation is turned on. This dynamically imports to PouchORM.ClassValidator with the full instance of the required library. The method can also be called at any time so that class validation methods, decorators, and so on may used your application without the need to statically import the library. However, if class-validator has not been installed to node_modules, this will crash PouchORM when PouchORM.getClassValidator() is called and/or you attempt to use PouchORM.ClassValidator.

For complete details and advanced usage of class-validator, see their documentation.

PouchORM metadata

PouchORM adds some metadata fields to each documents to make certain features possible. Key of which are $timestamp and $collectionType.

$timestamp

This gets updated with a unix timestamp upon upserting a document. This is also auto-indexed for time-sensitive ordering (i.e so items don't show up in random locations in results each time, which can be disconcerting)

$collectionType

There is no concept of tables or collections in PouchDB. Only databases. This field helps us differentiate what collection each document belongs to. This is also auto-indexed for your convenience.

$by (v1.6.x)

PouchORM can help you append a userId to each originating change to specify who changed a document last. Simply use PouchORM.setUserId(...) to specify who the local/active user is, and PouchORM will put that id here. If this is not set, this field will be ...

If you need more stringent audit log capabilities, that's something you should implement for your application.

Custom ID generation

You can control the way IDs are generated for new items. Just define the idGenerator function property in a collection object. This can be a normal or async function that returns a string.

import {personCollection} from '...'


personCollection.idGenerator = () => {
  return 'randomIdString';
};

const p = await personCollection.upsert({...})
p._id === 'randomIdString' // true

You can also do:

personCollection.idGenerator = async () => {
  const anotherString = await someAsyncIDStringBuilder()
  return anotherString;
};

// or better yet, cleanly override the property in the class for consistency

export class PersonCollection extends PouchCollection<IPerson> {

  // override
  async idGenerator(){
    return 'randomIdString';
  }
}

Installing PouchDB plugins

You can access the base PouchDB module used by PouchORM with PouchORM.PouchDB. You can install plugins you need with that e.g PouchORM.PouchDB.plugin(...). PouchORM already comes with the plugin pouchdb-find which is essential for any useful querying of the database.

Accessing the raw pouchdb database

Every instance has a reference to the internally instantiated db collectionInstance.db that you can use to reference other methods of the raw pouch db instance e.g personCollection.db.putAttachment(...).

You can use this for anything that does not directly involve accessing documents e.g adding an attachment is fine. But caution must be followed when you want to use this to manipulate a document directly, as pouch orm marks documents with helpful metadata it uses to enhance your development experience, particularly $timestamp and $collectionType.

It is generally better to rely on the exposed functions in your collection instance.

If you want more pouchdb feature support, feel free to open an issue. This library is also very simple to grok, so feel free to send in a PR!

Deleting the Database

import {PouchORM} from 'pouchorm'
...
PouchORM.deleteDatabase(dbName: string)

It goes without saying that this cannot be undone, so be careful with this! Also, any loaded PouchCollection instances you still have will now throw the error "database is destroyed" if you try to run any DB access operations on them.

Realtime Sync!

Last but not least, PouchDB is all about sync. You could always access the native Pouch DB object and run sync operations.

But as of v1.5, some sugar has been added to make this a simplified PouchORM experience as well.

Introducing PouchORM.startSync(fromPath, toPath, opts) where either paths could be local paths/names or a remote db url path. Within opts, you can specify callbacks that trigger upon specific events during the realtime sync e.g onChange, onError,onStart, etc. Have a look at the reference.

You can also cancel real-time sync by PouchORM.stopSync(fromPath, toPath?). If the second parameter is null, it will stop all sync ops for that db regardless of destination.

Supporting the Project

If you use PouchORM and it's helping you do awesome stuff, be a sport and Buy Me A Coffee or Become a Patron!. PRs are also welcome. NOTE: Tests required for new PR acceptance. Those are easy to make as well.

Contributors

  • Iyobo Eki
  • Aaron Huggins