LowlaDB Adapter

Adapters allow LowlaDB to connect to almost any back-end database or application.

Introduction

LowlaDB adapters provide a common interface to a wide variety of back-end databases and applications. The interface to adapters has been designed to be as simple as possible to implement on a wide variety of systems.

LowlaDB includes a default implementation of adapters for MongoDB and PostgreSQL that can be embedded within a Node.js application. These implementations are built around a common Node.js API. The easiest way to implement an adapter is to take advantage of this API, but it is also possible to create an adapter directly from the specification if you want to use a platform other than Node.js.

Installation

The default LowlaDB adapter is packaged as a Node.js module designed to plug into an Express application. To install it, modify your dependencies in package.json to include lowladb-node.

{
  "dependencies": {
    "body-parser": "~1.8.1",
    "cookie-parser": "~1.3.3",
    "debug": "~2.0.0",
    "express": "~4.9.0",
    "jade": "~1.6.0",
    "morgan": "~1.3.0",
    "serve-favicon": "~2.1.3",
    
    "lowladb-node": "~0.0.5",
  }
}

With the dependencies in place, you need to construct a new instance of the module, optionally providing configuration options.

var lowladb = require('lowladb-node');
var app = express();

lowladb.configureRoutes(app, [optional options] );

The available options are described in the API section below.

Specification

The LowlaDB client, syncer and adapter all communicate via a simple HTTP-based protocol. This section defines the parts of the protocol that an adapter must implement.

Definitions

ID
A text identifier that uniquely identifies a document. IDs are generated by adapters and, for new records created remotely, by clients. An id created by a client must contain sufficient information for the adapter to be able to create the corresponding document in the correct location.
Version
A text identifier for a particular version of a document. Versions are only ever compared for equality, they do not need to support any kind of ordering. The only requirement is that the version must change when a document changes. Typical implementations include
  • an increasing counter
  • a hash of the document
  • a timestamp

Versions are always generated by adapters.

Sequence
An increasing counter maintained by the syncer to order modifications and identify changes that have occurred since a client last synced.
ClientNs
Client namespace, i.e., the database and collection where a document should be stored on the client. This is expressed in the usual MongoDB form db.collection where collection may itself contain embedded periods.

Pull

The pull stage of synchronization ‘pulls’ modified records from the adapter down to the client. The client begins this process by retrieving the list of modified ids from the syncer. For each id, the client verifies whether it already has the correct version of the document. If not, it adds the id to the list of ids that need to be pulled. Once it has collected sufficient ids, it requests the documents by making a POST request to the adapter at the endpoint

/lowla/pull

with a json body in the form

{
  "ids": [
    "<id>", "<id>", {...}
  ]
}

The adapter should generate a response of the form

[
  {
    "id": "<id>",
    "clientNs": "<clientNs>",
    "deleted": true|false
  },
  {
    // Document JSON
  },
  {...}
]
Notes
  • The response consists of an array of javascript objects. The objects usually alternate between objects containing metadata and objects giving actual document contents. The exception to this is that when the metadata object has deleted: true, there is no following document and the next object, if any, will again be metadata.
  • For performance or scalability reasons, the adapter need not return all of the requested documents. The client will re-request any ids that were not returned. This continues until the adapter returns an empty response (i.e., an array containing no objects.) At this point the client will move on to any remaining ids that need to be pulled. However, the client will be unable to update its sync sequence in this situation and it will request these documents again the next time it retrieves a list of modifications from the syncer.
  • As long as pulls are successful, the client will update its sync sequence as it pulls documents. Thus if a sync terminates before the client can retrieve all the modifications provided by the syncer, the client is able to resume from where it left off on the next sync.

Push

The push stage of synchronization ‘pushes’ modified records from the client directly up to the adapter. The client collects the necessary change data and then makes a POST request to the adapter at the endpoint

/_lowla/push

with a request body of the form

{
  "documents": [
    {
      "_lowla" : {
        "id": "<id>",
        "version": "<version>",
        "deleted": true [optional]
      },
      "ops" : {
        "$set": {
          "fieldToModify": "newvalue",
          ...
        },
        "$unset": {
          "fieldToDelete": "dummyvalue",
          ...
        }
      }
    },
    {...}
  ]
}

The adapter should generate a response of the form

[
  {
    "id": "<id>",
    "version": "<version>"
    "clientNs": "<clientNs>",
    "deleted": true|false,
	"clientId": "<id that was pushed>"
  },
  {
    // Document JSON
  },
  {...}
]
Notes
  • The response consists of an array of javascript objects. The objects usually alternate between objects containing metadata and objects giving actual document contents. The exception to this is that when the metadata object has deleted: true, there is no following document and the next object, if any, will again be metadata.
  • The client may not (and usually will not) include all modifications in a single push request.
  • For performance or scalability reasons, the adapter need not return responses for all of the pushed documents. The client will re-push any documents that were not returned. This continues until the adapter returns an empty response (i.e., an array containing no objects.) At this point the client will move on to any remaining ids that need to be pushed. However, the client will push these documents again the next time it performs a push sync.
  • If the client is not able to generate suitable ids for new documents, the adapter may return a record using a different id than the value that was pushed. In this case, the original id will be returned in the metadata in the clientId property. If this property is set, the client should delete the original document under the old id and insert the returned document under the new id.
  • The adapter must eventually provide a response for every document pushed. It can choose to reject a document by returning deleted: true in the metadata, but it cannot simply ignore a document as the client will continue to push it until it receives a response.
  • It is the job of the adapter to handle conflicts when a document is edited in multiple places. It can detect conflicts by comparing the version on an incoming document with the current version in the back-end. For most applications a simple strategy suffices such as always accepting the incoming document or always rejecting the incoming document. In either case, the adapter must send the document that it ultimately stored in the back-end back to the client.

API

The default adapter has a single API, configureRoutes that is used to instantiate and configure both the default syncer and adapter.

var lowladb = require('lowladb-node');
var app = express();

var config = lowladb.configureRoutes(app, options);

The following options are supported

datastore
The datastore that the adapter should use. If omitted, the adapter uses its built-in NeDB datastore.
logger
An object capable of performing logging with a console-like API. If omitted, the syncer uses console.

The default adapter itself contains generic functionality for communicating with the syncer and client and defers to an instance of a datastore to handle communication with the specific backend database or application. The API for datastores is small and designed to be straightforward to implement on many platforms. If you are planning on writing a datastore, we recommend starting from the NeDB implementation. You can find further examples, along with examples of how to package a datastore for inclusion in LowlaDB, in the MongoDB and PostgreSQL datastores.