A Promise-Based DocumentDB ODM Client for NodeJS
JavaScript
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
example
lib
test
.gitignore
.travis.yml
README.md
index.js
license.md
package.json

README.md

DoQmentDB - A Promise-Based DocumentDB Client

NPM version Build status Test coverage Dependency Status License Downloads

DoQmentDB is a tiny layer that provides the simplicity of MongoDB for DocumentDB users(support schema, hooks/middleware, atomic-transactions, udf and more).

Table of contents:

Get Started

(1) You can install DoQmentDB using 2 different methods:

  • clone & build this repository
  • via npm: by running $ npm install doqmentdb from your terminal

(2) Add to your project:

var DoqmentDB = require('doqmentdb');

(3) Start Playing with DoqmentDB:

var DoQmentDB  = require('doqmentdb');
// Create DocumentDB connection
var connection = new (require('documentdb').DocumentClient)(HOST, OPTIONS);
// Pass connection and database-name, if `test` does not exist it will create one.
var db = new DoQmentDB(connection, 'test');
// Create a CollectionManager instance, if `users` does not exist it will create one.
var users = db.use('users');
// Using schema
users.schema(model);
// Add hooks(see `users` full example)
users.pre('save', function(next) {
  var doc = this;
  doc.createdAt = new Date().toString();
  next();
});

// Each http function returns a `Promise` with two specific methods: success and error.
users.create({ name: '6534' })
  .then(console.log);

users.findById(1)
  .then(console.log);

users.findAndRemove({ isAdmin: false })
  .then(console.log);

Database

Create a DatabaseManager by passing connection and databaseName.

var DoQmentDB  = require('doqmentdb');
// Create DocumentDB connection
var connection = new (require('documentdb').DocumentClient)(HOST, OPTIONS);
// if `test` does not exist it will create one
var db = new DoQmentDB(connection, 'test');

create

Get name and crete new collection in the used db.
Usage: db.create(string)
Aliases: insert
Returns: Object

db.create('users')
  .then(console.log);

getDatabase

Return the used database.
Usage: db.getDatabase()

db.getDatabase()
  .then(console.log);

find

find collection by given object params.
Note: to return all documents, omit params argument or pass an empty object({}).
Usage: db.find(object[optional])
Returns: Array

db.find()
  .then(console.log); // Return all collections

db.find({ id: 'users' })
  .then(console.log); // Return collections where id equal to `users`

findById

find collection by given string id.
Usage: db.findById(string)
Returns: Object

db.findById('users')
  .then(console.log);

findOrCreate

get object properties, search for collection, if it does not exist create one.
Usage: db.findOrCreate(object)
Returns: Object

db.findOrCreate({ name: 'users', id: '#1' })
  .then(console.log);

remove

get collection id as a String, if it exists - remove it and return undefined, else return false.
Usage: db.remove(string)
Returns: undefined or Boolean

db.remove('test')
  .then(console.log);

use

get collection name and return CollectionManager instance.
Note: if the given collection does not exist it will create one.
Usage: var coll = db.use(string);
Returns: object instanceof CollectionManager

var users = db.use('users'); // This operation is not async

Collection

Create a CollectionManager by passing to .use function a collection name.

var users = db.use('users');
console.log(users.constructor.name); // Collection

create

get object properties, and create new document under the used collection.
Usage: users.create(object)
Aliases: insert
Returns: Object

users.create({ name: 'Ariel', admin: true })
  .then(console.log); // { name: 'Ariel', admin: true, id: '8...31', _self: ... }

createOrUpdate

create a new document under the current collection, or update an existing document with the same id.
Usage: users.createOrUpdate(object)
Aliases: upsert
Returns: Object

users.upsert({ id: 'my_user_id', admin: true })
  .then(console.log); // { id: 'my_user_id', admin: true, _self: ... }

getCollection

return the used collection.
Usage: users.getCollection()

users.getCollection()
  .then(console.log);

find

get object properties and return an array of results.
Usage: users.find(object)
Note: to return all collections, omit params argument or pass an empty object({}).
Returns: Array

users.find({ active: true })
  .then(console.log);

findOne

get object properties and return the first matching result.
Usage: users.findOne(object)
Returns: Object

users.findOne({ active: true, name: 'Bar' })
  .then(console.log);

findById

find document by giving a string id.
Usage: users.findById(string)
Returns: Object

users.findById('53...3')
  .then(console.log);

findAndRemove

get object properties to search, find the equivalents and remove them.
Usage: users.findAndRemove(object)
Returns: Array
Note: if you want atomic-transactions support(i.e: do things concurrently, e.g: distributed system), you need use this method prefix with $ sign.

users.findAndRemove({ name: 'Ariel' })
  .then(console.log);

// Using stored procedure
users.$findAndRemove({ name: 'Ariel' })
  .then(console.log);

// Remove all users
users.findAndRemove({})
  .then(console.log);

findOneAndRemove

get object properties, and remove the first matching result.
Usage: users.findOneAndRemove(object)
Returns: undefined or Boolean
Note: if you want atomic-transactions support(i.e: do things concurrently, e.g: distributed system), you need use this method prefix with $ sign.

users.findOneAndRemove({ name: 'Ariel', admin: true })
  .then(console.log);

// Using stored procedure
users.$findOneAndRemove({ name: 'Ariel', admin: true })
  .then(console.log);

findAndModify

get object properties to search, find the equivalents and modify them(extend operation).
Usage: users.findAndModify(object, extend)
Aliases: update
Returns: Array
Note: if you want atomic-transactions support(i.e: do things concurrently, e.g: distributed system), you need use this method prefix with $ sign.

users.update({ name: 'Ariel', admin: true }, { admin: false })
  .then(console.log);

// Push 'a' and 'b' to `list` field(do it concurrently)
['a', 'b'].forEach(function(ch) {
  users.$update({}, { list: { $push: ch } });
});

findOneAndModify

get object properties and modify(extend operation) the first matching.
Usage: users.findOneAndModify(object, extend)
Aliases: updateOne
Returns: Object
Note: if you want atomic-transactions support(i.e: do things concurrently, e.g: distributed system), you need use this method prefix with $ sign.

users.findOneAndModify({ admin: false }, { admin: true })
  .then(console.log);

// Using stored procedure
users.$findOneAndModify({ admin: false }, { admin: true })
  .then(console.log);

findOrCreate

get object properties, search for document, if it not exist create one.
Usage: users.findOrCreate(object)
Returns: Object
Note: if you want atomic-transactions support(i.e: do things concurrently, e.g: distributed system), you need use this method prefix with $ sign.

users.findOrCreate({ admin: false, name: 'Ariel' })
  .then(console.log);

// Using stored procedure
users.$findOrCreate({ admin: false, name: 'Ariel' })
  .then(console.log);

Queries

Operators

  • Logical & Conjunctive:
    • $or OR
    • $and AND
    • $not NOT
    • $nor NOT(... OR ...)
  • Comparison:
    • $gt >
    • $gte >=
    • $lt <
    • $lte <=
    • $ne <> or !=
  • UDF:
    • $in like Array.prototype.some(...)
    • $all like Array.prototype.every(...)
    • $type typeof value
    • $regex new RegExp(...).test(value)
    • $size test array.length

Examples

users.find({ a: 1, b: 2, c: '3' })
// ... r WHERE r.a=1 AND r.b=2 AND r.c="3"

users.find({ $or: [{ a: 2, b: 3}, { c: 3 }] })
// ... r WHERE ((r.a=2 AND r.b=3) OR r.c=3)

users.find({ $not: { a: 1, b: 2, c: 3 } })
// ... r WHERE NOT(r.a=1 AND r.b=2 AND r.c=3)

users.find({ $nor: [ { a: 1 }, { b: 3 }]})
// ... r WHERE NOT(r.a=1 OR r.b=3)

users.find({ $nor: [ { a: 1, b: 1 }, { c: 3 } ] })
// ... r WHERE NOT((r.a=1 AND r.b=1) OR r.c=3)

users.find({ $not: { name: { $gt: 3 }, age: 12 } })
// ... r WHERE NOT(r.name > 3 AND r.age=12)

users.find({ $not: { name: { $ne: 'bar' } } })
// ... r WHERE NOT(r.name <> "bar")

users.find({ $or: [
        { name: { $ne: 'Ariel' } },
        { age: { $lte: 26 } },
        { $and: [
          { isAdmin: { $ne: false } },
          { isUser: { $ne: false } }
        ]}
      ]})
// ... r WHERE r.name <> "Ariel" OR r.age <= 26 OR (r.isAdmin <> false AND r.isUser <> false)

users.find({ coins: { $in: 2 } })
// ... r WHERE inUDF(r.coins, 2)

users.find({ $not: { age: { $type: 'number' } } })
// ... r WHERE NOT(typeUDF(r.age, "number"))

Operations

When using one of the update operations(e.g: .update(), .findAndModify(), etc...), you could use the built-in prototype functions(based on the type) prefixed with the $ sign.
Usage: users.update({ ... }, { keyName: { $method: value } })
Note: value could be single or array of arguments.

// Find all, and push 2 to `arr` field
users.update({}, { arr: { $push: 2 } });

// Suffix all users `name` with #
users.update({}, { name: { $concat: '#' } });

// Trim the name from `foo` to `o`
users.update({  name: 'foo' }, { name: { $substr: [1,1] } });

Schema

Manage your documents with schema.
fields:

  • type
    • required
    • used for type comparing, (e.g: String, Boolean, Number, etc..).
  • default
    • optional
    • value fallback
  • regex
    • optional
    • regex validation, (e.g: email validation - /^[a-zA-Z0-9@:%_\+.~#?&//=|/d]{10,}$/).
  • error
    • optional
    • return message to fields that fail in the validation phase(regex/type). see: example
  • expose
    • optional
    • expose by default is true, unless you set it to false, it's means that all the find operations returns the documents without exposing this fields. see: example

Example using schema:
schema: model.js

module.exports = {
  /**
   * @field name
   * @default no default value
   */
  name: {
    type: String,
    'default': ''
  },

  /**
   * @field email
   * @default no default value
   * @regex email, min-length = 10
   */
  email: {
    type: String,
    'default': '',
    regex: /^[a-zA-Z0-9@:%_\+.~#?&//=|/d]{10,}$/,
    error: '`email` must be type string, valid email address, and least 10 chars',
    expose: true
  },

  /**
   * @field password
   * @default no default value
   * @regex password
   */
  password: {
    type: String,
    'default': '',
    regex: /^.*(?=.{8,})(?=.*[a-zA-Z])(?=.*\d)(?=.*[!#$%&? "]).*$/,
    error: '`password` must be type string, contain 8 chars and at least one number, ' +
      'one letter and one unique character such as !#$%&? "',
    expose: false
  },

  /**
   * @field isAdmin
   * @default false
   */
  isAdmin: {
    type: Boolean,
    'default': false
  }
};

using schema(model.js)

var DoQmentDB  = require('doqmentdb');          
var model      = require('./model');            // Get model/schema
var connection = new (require('documentdb')     // Create DocumentDB connection
.DocumentClient)(CONFIG.HOST, CONFIG.OPTIONS);

var db = new DoQmentDB(connection, CONFIG.DB);  // Create DBManager 'test'
var users = db.use('users');                    // Create CollectionManager 'users'
users.schema(model);                            // Using schema

users.create({ password: 'Ar2!as_s'})
  .then(console.log)
  .catch(console.log);
  /*
   [Error:
   `email` must be type string, valid email address, and least 10 chars
   ]
   */

users.create({ name: 'Ariel', email: 'ariel.com', password: 'Ar2!as_s'})
  .then(console.log)
  .catch(console.log);
/*
 [Error:
 `email` must be type string, valid email address, and least 10 chars
 ]
 */

users.create({ name: 'Ariel', email: 'a8m@gm.com', password: 'Ar2!as_s'})
  .then(console.log)
  .catch(console.log);
/*
 {
   name: 'Ariel',
   email: 'a8m@gm.com',
   password: 'Ar2!as_s',
   id: '2eb7...c0',
    ...
 }
 */
users.find({})
  .then(console.log);
/*
 Get all documents but without exposing fields(i.e: omit `password` field)
 */

see: How to architect your models

Middleware

Middleware/Hooks are executed at the document level(create/save/insert, update, remove/delete).
There are two types of middleware, pre and post.

pre

Usage: users.pre(operation, callback)
Note: pre middleware are executed one after another, when each middleware calls next.
Example:

users.pre('save', function(next) {
  var doc = this;
  doc.createdAt = new Date().toString();
  next();
}, function(next) {
  var doc = this;
  doc.updatedAt = new Date().toString();
  next();
});

// Do something async
users.pre('save', function(next) {
  var doc = this;
  bcrypt.genSalt(10, function(err, salt) {
    bcrypt.hash(doc.password, salt, function(err, hash) {
      doc.password = hash;
      next();
    });
  });
});
// ##Note: the order is important, this example order:
// `createdAt()`, `updatedAT()`, `hash/bcrypt()`, and then the `.create` operation will called

post

Usage: users.post(operation, callback)
Note: post middleware are executed in parallel.
Example:

users.post('save', function(doc) {
  logger(new Date(), doc, 'saved!')
});

Atomic Transactions

Since v0.2.6 DoQmentDB supports atomic-transactions using a built-in sporcs(i.e: stored procedures) to handle concurrently well.
Note: To perform some operation this way, you should prefix it with $.
Read More: DocumentDB - Atomic Transactions

// Lets take some example of `consuming` from two differents
// Service-Bus queues and update the same `model`/`document`
//
// Note: This also could happen in a distributed system, when two operations happens in parallel

// We have a `stores` collection that holds the `sales` and the `users`
// fields per `store`(a Document)
// We are using the `atomic` version of `update`, because we don't want to lose data
sbs.receiveQueueMessage('sales', function(msg) {
  stores.$update({ id: msg.id }, { sales: { $push: msg.sale } });
  // Polling again...
});

sbs.receiveQueueMessage('users', function(msg) {
  stores.$update({ id: msg.id }, { users: { $push: msg.user } });
  // ...
});

Examples

Changelog

0.2.9

  • Schema Fix- issue #26

0.2.8

  • Add aliases: updateOne and $updateOne(the conccurent one)
  • refactor the built-in stored procedure(findAndModify)

0.2.6

Since 0.2.6 DoQmentDB support atomic transactions using DocumentDB stored procedures.
Methods that support:

  • update/findAndModify
  • findOneAndModify
  • findOrCreate
  • findAndRemove
  • findOneAndRemove

If you want to use one of this methods, you should use them prefix with $ sign.

// Push 'a' and 'b' to `list` field(do it concurrently)
['a', 'b'].forEach(function(ch) {
  users.$update({}, { list: { $push: ch } });
});