wires-mongo

1.6.61 • Public • Published

wires-mongo

An ambitious ORM for ambitious project

Features

  • Model mapping
  • Automatic "join" query
  • List relation joining with optimized query
  • Promise based
  • Schema with validations
  • Simple but powerful API
  • Good test coverage with comprehensive examples

Installation

$ npm install wires-mongo

Connecting db service

ORM does not have a connector, you need to register wires-domain service that returns mongo cursor.

$ npm install wires-domain
var domain = require("wires-domain")
var mongo = require('mongodb');
var Connection;
domain.service("$db", function() {
    return new Promise(function(resolve, reject) {
        if (Connection) {
            return resolve(Connection);
        }
        mongo.MongoClient.connect('mongodb://localhost:27017/wires_mongo_test', {
            server: {
                auto_reconnect: true
            }
        }, function(err, _db) {
            if (err) {
                return reject(err);
            }
            Connection = _db;
            return resolve(Connection);
        })
    })
});

Schemas

Schemas are mandatory. It allows you to do automatic validation on save, and brings transparency to your code.

var Model = require('wires-mongo')
var = Model.extend({
    collection: "super_test",
    schema: {
        _id: {},
        name: {
            required: true
        },
        email: {},
        password: {},
    }
})

Schema parameters

required Operation will be rejected if:

"required" it a type of boolean and target value is undefined*

schema: {
   _id: [],
   name: {
      required: true
   }
}

"required" it a type of function and returns something but undefined*

schema: {
   _id: [],
   name: {
      required: function(value) {
         if (value === "test") {
            return "SomeError";
         }
      }
   }
}

An exception can thrown as well

schema: {
   _id: [],
   name: {
      required: function(value) {
         if (value === "test") {
            throw {
               status: 400,
               message: "AllBad"
            }
         }
      }
   }
}

"required" it a type of RegExp and the expression gives nothing or value is undefined*

schema: {
   _id: [],
   name: {
      required: /\d{4}/
   },
   other: {}
}

ignore Means that field is settable but will be ignored when saved

schema: {
   _id: [],
   name: {
      ignore: true
   }
}

unique Applies to arrays only. Understands mongoids, models and strings

schema: {
   _id: [],
   name: {
      unique: true
   }
}

minLength Checks the minimum length of a string. Will throw an error if any object but string is passed

schema: {
   _id: [],
   name: {
      minLength: 20
   }
}

maxLength Checks the maximum length of a string. Will throw an error if any object but string is passed

schema: {
   _id: [],
   name: {
      maxLength: 5
   }
}

toJSON Function will be called on the model's toJSON. "this" is the current model

schema: {
   _id: [],
   name: {
      toJSON : function(value){
       return this.get("name").toLowerCase();
      }
   }
}

Indexes

Set "index" property to a field like so:

schema: {
    _id: [],
    title: {
       index: 1
    },
    description: {
       index: "text"
    },
    addition: {
       index: "text"
    }
 }

2 indexes will be created. { title : 1 } and {description : "text",addition : "text" }

There can be only one text index on a collection, therefore ORM reads propeties and combines all text indexes into one.

Create all indexes

Item.createAllIndexes().then(function(){
 // indexes created
});

Automatically creates all defined indexes on a specific model

Create index manually

Item.createIndex({
  title: "text",
  description: "text"
}).then(function(result) {
}).catch(reject);

You can use fully featured mongo API to create your own index

createIndex: function(fieldOrSpec, options)

Require and create indexes

For your own convience a services called $wiresMongoIndexer will require and create all indexes for models.

domain.require(function($wiresMongoIndexer) {
   return $wiresMongoIndexer("User", "Books", "Comments", ["Reviews", "Session"], ... )
}).then(function(){
   // All indexes are created
})

Set Attributes

All attributes are stored in "attrs" dictionary. Attribute will be ignored in case of missing in schema. To set an attribute, use

var user = new User();
user.set("name", "john")
// or
user.set({name : "john"})

It is possible to constuct model with a dictionary

var user = new User({name : "john"});

You can use dot notation to set/get a particular object

user.set('somedict.name','john')
user.set('a.b.c.d',{})

Note that set does not understand arrays, but get does

user.get('somedict.name')
user.get('somelist.0.name')

Unset attribute

Use "unset" to remove attribute(s) from the database

item.unset('name')
item.unset('email', 'pass')
item.unset('email', 'pass', ['other_field', 'something_else'])

You can pass arguments or lists (they will be flattened). You should call "save" to perform the operation.

Queries

Find accepts native mongodb query.

TestUser.find({name : "john"}).first().then(function(model) {
    model.attrs.name.should.be.equal("john")
    done();
}).catch(function(e) {
    logger.fatal(e.stack || e)
});

You can also use key and value as first and second arguments to fetch something using simple criterion.

TestUser.find("name", "john")

You can use either first() or all() for performing mongodb request

Random

You can call firstRandom() the get random result from a set. In the background counts records in the database, and adds "skip" parameter with random number

TestUser.find().firstRandom();

Don't use limit() or skip() while performing firstRandom(). It will be overridden.

Find by id

IF you use find with 1 argument, wires-mongo assumes you want to find a record by id. You can pass a string, model reference, or ObjectID accordingly

TestUser.find("559a508ce147b840c4986535")
TestUser.find(otherReference)
TestUser.find(ObjectID("559a508ce147b840c4986535"))
// is all the same

Makes a query:

{ _id : "559a508ce147b840c4986535"}

(findById is deprecated)

FindByText

If you schema fields have { index : "text" } property you can easily start performing full featured mongo text search

Let's say your model looks like this:

var Item = Model.extend({
      collection: "test_items_index",
      schema: {
         _id: [],
         title: {
            index: "text"
         },
         description: {
            index: "text"
         }
      }
});
```js
 
 
After all indexes have been created you can use findByText method:
```js
Item.findByText("dogs and chocolate").all();

You sort the results by relevance as well:

Item.findByText("dogs and chocolate", { sort: true }).all();

With/Join

It is possible to automatically fetch referenced items. Let's say, we have a record Item, that has a reference "current_tag" that is a model "Tag"

Item.find().with("current_tag", Tag).all()

Instead of getting ObjectId as a result, activerecord will collect all ids that need to be fetched, and will make one opmtimized query to retrieve items for you! The same applies to lists that have ObjectId within

Item.find().with("tags", Tag)

You can use nested "with" statements with attached query condition if needed

User.find(user)
   .required()
   .with("sessions", Session)
   .with("friends", User.with("friends", User.find({active : true})))

See with-query tests for better understanding

Required record

Record can automatically be reject if not found. Apply "required()" to your query

Item.find({name : "test"}).required().then(function(){
 
}).catch(function(error){
 
});

if one of the "with" queries has a "required" parameter, entire promise will be rejected as well.

You can pass a custom message if needed

Item.find({name : "test"}).required("This record is very important")

Count

A simple query for count

TestUser.find().count().then(function(num) {
   num.should.be.equal(2);
})

Sorting

You can sort items using the according method:

TestUser.sort("name"); // Default is asc
TestUser.sort("name", "desc"); // Setting direction
TestUser.sort({ // Passing some specific condition
   score: {
      $meta: "textScore"
   }
});

Query with projection

It is possible to pass a projection. Add a projection to your model's properties

projections: {
    user: ["name", "email"],
    world: {
        exclude: ["password"],
    }
},

You can use "exclude" to exclude specific properties from the query. To set current projection

var user = new TestUser();
user.projection("user");

You can also pass an object

var user = new TestUser();
user.projection({
   score: {
      $meta: "textScore"
   }
});

See tests for better understanding

Paginator

Items can be paginated. Wires-mongo uses https://www.npmjs.com/package/pagination module.

Item.find().paginate({page: 1, perPage: 10, range : 10})

All defined options are optional. Returns a promise, in fact an alternative for "all" request with a small difference - The output looks like this:

{
  "paginator" : {},
  "items" : {}
}

Saving

Like any activerecord we detect what type of query it is by absence or presence of _id attribute

var user = new TestUser({
    name: "john",
});
user.save().then(function(newuser) {
    // At this point we have _id attribute set
    // Modify user name and return new promise
    return newuser.set("name", "ivan").save()
}).then(function(success){
 
}).catch(function(e) {
  logger.fatal(e.stack || e)
});

Default values

Add "defaults" key to your schema to set a default value. Note, it will be set only when database request is performed.

schema: {
        _id: [],
        name: {},
        published: {
            defaults: false
        },
      date : {
         defaults : function(){
            return new Date()
         }
      }
    }

Events on save

You can decorate saving with multiple methods. Add them to your model Triggered before create request:

onBeforeCreate: function(resolve, reject) {
    resolve();
}

Triggered before update request:

onBeforeUpdate: function(resolve, reject) {
    resolve();
},

Triggered all the time when save is called

onBeforeSave: function(resolve, reject) {
    resolve();
}

Removing

Removing requires _id attribute set.

Triggers 2 events, that's why response is an array that contains results from promises

user.remove().then(function(response) {
    // Amount of rows is in the second argument
    response[1].should.be.equal(1);
    done();
})

You can also remove all found records. No instance required. However, we will create instance per found item and perform "remove" on it.

new User().find({name : /test/}).removeAll();

Array manupulations and events

You can use "add" and "exclude" methods to do manupulations with arrays Let's say, we have a model

var Item = Model.extend({
   collection: "test_items_array",
   schema: {
      _id: {},
      tags: {
         reference : true
      }
   },
   onAddToTags : function(tag){
   },
   onExcludeFromTags : function(tag){
   }

Adding item to array

Add a tag to the tags collection will look like:

var item = new Item();
item.add(tag, "tags").then(function(){
})

Each time you call "add" a corresponding method will be called (if defined). It's form using "onAddTo" + YouPropertyNameInCameCase

You can return a promise if you like. It will be resolved accordingly.

Excluding item from array

var item = new Item();
item.exclude(tag, "tags").then(function(){
})

It calls "onExcludeFrom" + YouPropertyNameInCameCase if defined. It has the exact same behavior as the adding method

Ensure unique

Sometimes before saving you want to be sure that a record is unique.

var item = new Item({title : "hello"});
item.ensureUnique({title: 'hello'}, 'You are wrong! (Optional error)');
item.save();

Will perform a query and reject if record is found

Cascade remove

Reference can be automatically removed. Add this property to a model

cascade_remove: ['@exclude Blog.tags'],

Several directive can be applied

@exclude

cascade_remove: ['@exclude Blog.tags'],

Exludes id from the list (in the example it's "tags" from the Blog model

@remove

cascade_remove: ['@remove Blog.item'],

Searches for records that match Blog.item == self._id and removes them

@nullify

cascade_remove: ['@nullify Blog.someother_tag'],

Sets Blog.someother_tag to null

Events on remove

Put these method into your model. Throw any exception or reject! But don't forget to resolve :-)

onBeforeRemove: function(resolve, reject) {
    resolve();
},
onAfterRemove: function(resolve, reject) {
    resolve();
},

Access helpers

equals

You can compare to models by using "equals" method Passing a string will automatically convert it to mongoid and compare it with the current _id

record.equals("5555d4877be0283353c28467") // true
record.equals(sameRecords) // true

inArray

Checks if current model is in an array. Understands array of strings, mondoIds and models

record.inArray(["5555d4877be0283353c28467"]) // true
record.inArray([ObjectId])// true
record.inArray([record])// true

filter

Allows you to filter your results before resolving the promise

Item.find().filter(function() {
   return this.get('name') === "Item 1";
}).all().then(function(results) {
   results.length.should.be.equal(1)
   done();
})

toJSON

When all() is called, list is being prototyped with $toJSON method, that will recursively serialize all objects

Item.find().all().then(function(results) {
   results.$toJSON()
})

On top of of that each model has "toJSON" method

Readme

Keywords

none

Package Sidebar

Install

npm i wires-mongo

Weekly Downloads

72

Version

1.6.61

License

ISC

Last publish

Collaborators

  • nchanged