Wondering what’s next for npm?Check out our public roadmap! »

    mongoof

    0.0.2 • Public • Published

    Mongoof

    A less-error-prone alternative syntax for Mongoose.

    Introduction

    If you're like me, won't go through Mongoose without making those damn casting errors then this library is for you.

    The syntax has been designed to get the most out of Mongoose with simple and elegant syntactic sugar. The API is a small and intuitive one compared to Mongoose. For me, it made me way more productive and feel confident about my models.

    Install

    Install with npm

    $ npm install --save mongoof
    

    Guide

    There should be a model for each collection in your database, this model will be used to validate data and to communicate with the database system for CRUD operations.

    Your models will have three objects:

     
    const Model = require("mongoof").model;
     
    // collection name
    new Model("users")
    .schema({
        // Object 1 Schema [REQUIRED]
    })
    .options({
        // Object 2 Options [OPTIONAL]
    })
    .methods({
        // Object 3 Methods [OPTIONAL]
    })
    .compile(); // don't forget to compile

    Declaring a Schema

    A schema defines your data in several ways: which data type to expect (Array, Object, String, Date ...etc), the default value, and other validation properties like the maximum or minimum values.

    Typically with mongoof, you can do 4 things:

    1. Declaring Data types

    Data types declaration is the most basic validation, and is required for each key.

    Example: A store items schema:

    const Model = require("mongoof").model;
    new Model("store")
    .schema({
        title:{
            type:Model.String,
        },
        price:{
            type:Model.Number,
        },
        numOfOrders:{
            type:Model.Number,
        },
        htmlDescription:{
            type:Model.String
        }
    });

    If you're only declaring data types then you can use short hands:

    const Model = require("mongoof").model;
    new Model("store")
    .schema({
        title: Model.String,
        price: Model.Number,
        numOfOrders: Model.Number,
        htmlDescription:Model.String
    });
    • Model.String
    • Model.Number
    • Model.Date
    • Model.Buffer
    • Model.Boolean
    • Model.Mixed (can accept various types)
    • Model.Array
    • Model.ObjectId (when referencing other document's IDs)
    • Model.Virtual (we'll discuss this one in a bit)

    NOTE

    Please note how each data type is capitalized and prefixed with Model

    2. Declaring Validations

    Declaring a data type can be considered as a validation, however, you can define more validation. And there are two types of validations:

    Functional validations

    Functional validation can be used to implement any kind of validation. When defining a schema the validate key can take either an object (single functional validator) or array (a set of functional validators).

    The model will assume that those functions are synchronous. However if a function takes two arguments then the second arguments will be considered as a callback thus the function will be treated as an asynchronous function.

    {
        email:{
            type:Model.String,
            validate:{
                validator:function(email){
                    if(email.indexOf("@") < 1 || email.indexOf(".") < 1) return false;
                    else return true;
                },
                msg:"Sorry your email appears to be invalid"
            }
        }
    }

    Example 2: A set of Asynchronous/Synchronous validators

     
    {
        email:{
            type:Model.String,
            validate:[
                // validation 1: Asynchronous
                {
                    validator:function(value,callback){
                        setTimout(function(){
                            callback(true);
                        },500)
                    },
                    message:"Error message"
                },
                // validation 2: Synchronous
                {
                    validator:function(value){
                        return true;
                    },
                    message:"Error message"
                }
            ]
        }
    }
     

    NOTE

    If you don't want to set a custom error message then you can use the short hand, by defining a function instead of the object.

    Built-in validations

    For simple and common validations, there are few built-in validators that you can use:

    • For all data types:
      • required
    • For numbers:
      • min
      • max
    • For strings:
      • enum
      • match
      • maxlength
      • minlength
     
    {
        gmail:{
            type:String,
            required:true,
            match:[/\w+@gmail\.com/,"You should use gmail"],
            minlength:[10,"Email is not long enough"],
            maxlength:[99,"Email is too long"]
        },
        portfolio:{
            type:Model.Array,
            required:true
        },
        favoriteFootballTeam:{
            type:Model.String,
            enum:["Liverpool","Chelsea","Arsenal"]
        },
        height:{
            type:Model.Number,
            min:[50,"Your hight should be more than 50 cm to watch the game"],
            max:[280,"You'll block others from watching if we let you in :( sorry!"],
        }
    }
     

    3. Declaring Modifiers

    Schematic modification is a function that transforms a specific value into another before being saved in MongoDB.

    Example: This can be useful for example when saving email addresses, since it should be case-insensitive, but a user input might be uppercased, lowercased, capitalized .. etc, To make sure we have a consistent letter-casing, we would apply a modifier:

     
    {
        email:{
            type:Model.String,
            modify:function(value){
                return value.toLowerCase();
            },
            // validators will always apply after modification
            // so this validation will always pass
            validate:{
                validator:(value) => value.toLowerCase() === value,
                msg:""
            }
        }
    }
     

    Asynchronous Example

     
    {
        email:{
            type:Model.String,
            modify:function(value,callback){
                setTimeout(function(){
                    callback(value.toLowerCase());
                },2000);
            },
        }
    }
     

    4. Declaring Virtual Getters

    Virtual values are values that depends on stored values but not persisted (stored) there.

    Example: if you had a key firstName and another key lastName and your API utilizes the full name (firstName + " " + lastName), you can define a virtual value that computes the full name based on the firstName and lastName, but it's not stored in your database, yet whenever you read values through this model, you get the virtual value too.

    Example 1: Getting absolute path of a file:

     
    {
        file:{
            // a relative path would be something like: /uploads/filename.png
            relativePath:Model.String,
            // absolute path is computed based on the relative path:
            absolutePath:{
                type:Model.Virtual // it will not be persisted
                getter:function(){
                    // this refers to the whole document
                    return path.join(process.cwd(),"public",this.file.relativePath);
                }
            }
        }
    }
     

    Example 2: Seeing whether a book is available or not based on multiple values

     
    // a store bought a number of copies
    // they rented some
    // then sold some
    // they found some of copies were damaged
    // and then fixed some
    // how would they calculate the available copies?
    {
        bought:Model.Number,
        rented:Model.Number,
        sold:Model.Number,
        damaged:Model.Number,
        fixed:Model.Number
        available:{
            type:Model.Virtual,
            getter:function(){
                return this.bought - this.rented - this.sold - this.damaged + this.fixed;
            }
        }
    }
     

    Example 3: Asynchronous currency conversion

    const GBP2USD = require("gbp2usd");
     
    //...
     
    {
        price:Model.Number,
        usdPrice:{
            type:Model.Virtual,
            getter:function(callback){
                GBP2USD(this.price,function(priceInUSD){
                    callback(priceInUSD);
                });
            }
        }
    }

    Conclusion

    Here's an example of an almost finished schema

     
    const Model = require("mongoof").model;
     
    // this model is for a book store
    // we'll use this library to validate ISBN numbers
    const validateISBN = require("isbn-validator");
    // currency conversion library
    const gbp2usd = require("gbp2usd");
    // 
    new Model("books")
    .schema({
        title:Model.String,
        author:Model.String,
        year:{
            type:Model.Number,
            // using built-in validators
            max:2020,
            min:1500,
        },
        publisher:Model.String,
        ISBN:{
            type:Model.String,
            validate:{
                // The ISBN validator is asynchronous and promise based
                validator:function(value,callback){
                    validateISBN(value)
                    .then(function(valid){
                        callback(valid)
                    })
                    .catch(function(err){
                        callback(false);
                        console.log(err);
                    });
                },
                msg:"Invalid ISBN number"
            }
        },
        copies:{
            type:Model.Number,
            min:0
        },
        price:{
            type:Model.Number,
            min:0
        },
        // virtual value
        usdPrice:{
            type:Model.Virtual,
            getter:function(callback){
                GBP2USD(this.price,function(priceInUSD){
                    callback(priceInUSD);
                });
            }
        }
        previewPages:[
            {
                number:{
                    type:Model.Number,
                    min:1
                },
                img:{
                    type:Model.String,
                    // this modifer will write a base64 encoded image
                    // and save a string referring to the path of the image
                    // instead of the base64 string
                    modify:function(base64,callback){
                        if(base64.length < 50) callback(base64); // it's not base64
                        else {
                            base64 = base64.replace(/^data:image\/\w+;base64,/, '');
                            // random file name generation
                            let filename = Math.random().toString(36).substr(4)+".png";
                            let filePath = "uploads/previews/"+filename;
                            fs.writeFile(filePath, base64, {encoding: 'base64'}, function(err){
                                callback(filePath);
                            });
                        }
                    }
                }
            }
        ]
    })

    Declaring Methods

    Methods are CRUD operations (create, read, update and delete) performed on the collection the model concerned with.

    CRUD operations in mongoof are promise base.

    Mongoof CRUD API is rather simple, but very expressive:

    Example reading all entries from a collection

    const Model = require("mongoof").model;
     
    new Model("users")
    .schema({/* .. */})
    .methods({
     
        MethodName:function(){
            // ..
            return new Promise(function(resolve,reject){
                this.read()
                .then(function(out){
                    resolve(out);
                })
                .catch(function(err){
                    reject(err);
                })
                
            });
        }
    })
     

    As you can see from the above example this keyword refers to the collection and read applies an operations, and returns a promise.

    This is referred to as query statement.

    A query statement is composed of:

    1. this keyword [REQUIRED]
    2. Selector optional
    3. Modifier optional
    4. Runner REQUIRED

    A query statement SHOULD begin with this and end with a runner.

    Available selectors:

    The only selector available is the where selector. but you can apply operators on it as you wish.

    • Simple where selector:
        this.where({key1:"value1",key2:"value2"}).read()
    

    Comparison Operators

    • Operator: greater than
        this.where({age:{$gt:18}}).read()
    
    • Operator: less than
        this.where({age:{$lt:18}}).read()
    
    • Operator: greater or equal to
        this.where({age:{$gte:18}}).read()
    
    • Operator: less or equal to
        this.where({age:{$lte:18}}).read()
    
    • Operator: NOT equal to
        this.where({age:{$ne:18}}).read()
    
    • Operator: Equal to one of these values
        this.where({key:{$in:[0,2,4,6,8]}}).read()
    
    • Operator: NOT in one of these values
        this.where({key:{$nin:[1,3,5,7,9]}}).read()
    

    Logical Operators

    • Operator: or
        this.where({$or:[{quantity:{$lt:20}},{price:10}]}).read();
    
    • Operator: and
        this.where({$and:[{quantity:{$lt:20}},{price:10}]}).read();
    
    • Operator: not
        this.where({price:{$not:{$gt:1.99}}}).read();
    
    • Operator: nor
        this.where({$nor:{available:false,mark:{$lt:50}}}).read();
    

    Primary Operators

    • Operator: exists
        this.where({refundedAmount:{$exists:true}}).read();
    
    • Operator: type
        this.where({zipCode:{$type:"string"}}).read();
    

    Evaluation Operators

    • Operator: mod (reminder of division)
        this.where({copies:{$mod:[2,0]}}).read(); // even
        this.where({copies:{$mod:[2,1]}}).read(); // odd
    
    • Operator: regular expression
        this.where({email:{$regex:/\w+@gmail\.com/}}).read();
    

    Available Modifiers

    Modifier When? default possible example
    distinct reading false "any key" this.distinct("name").read()
    sort reading NaN -1,1 this.sort({lastName:-1,age:1}).read()
    limit reading false => 0 this.limit(10).read();
    skip reading false => 0 this.skip(10).read();
    upsert updating false true,false this.where({a:5}).upsert(true).update({a:10})
    multi updating false true,false this.where({a:5}).multi(true).update({a:10})

    NOTE

    Upsert means that if no document did satisfy the where selector criteria, create a new one. While update means that if multiple documents satisfied the where selector criteria, update them all.

    NOTE

    sort,limit and skip are usually used together for pagination. Example: this.sort({time:-1}).limit(10).skip(10).read();

    Available Runners

    Runners are the last methods in the query statement, they run the query, and return a promise

    read

    Takes no argument to read all the fields, takes an array of arguments to read specific set of fields, or takes a string to read only one field

        // all
        this.read();
        // only full name
        this.read("fullname");
        // age and last hospitalization date
        this.read(["age","lastHospitalizationDate"]);
    

    create

    Create new document, takes one argument as the document to be created:

    this.create({
        firstName:"Alex",
        lastName:"Corvi",
        email:"alex@arrayy.com",
        
    })
    

    delete

    Deletes all documents that satisfies the where criteria, when no where selector is used it will empty the collection.

    this.where(age:{$lt:19}).delete(); // delete all non-adult users
    this.delete(); // delete all user
    

    update

    Update specific fields in a document(s) that satisfies the where criteria.

    this.where({lastLoggedIn:{$lt:1384655207000}}).multi(true).update({active:false});
    

    unset

    Removes a field from the document

    this.where({active:false}).unset({salary:"",anotherKey:""});
    // short hand (to unset only one value):
    this.where({active:false}).unset("salary");
    

    increment

    Increment a numerical value

    this.where({title:"salesman"}).inc({salary:300}); // a raise for salesmen
    
    this.where({title:"CEO"}).inc({salary:-300}); // :(
    
    

    push

    Pushing a value to an array

    this.where({name:"Alex Corvi"}).push({projects:"mongoof"});
    

    PushAll

    Pushing multiple values to an array

    this.where({name:"Alex Corvi"}).pushAll({projects:["mongoof","vunode","pul"]});
    

    pull

    Removes a value from an array

    this.where({name:"Alex Corvi"}).pull({todos:"mongoof"});
    

    pullAll

    Removes a value from an array

    this.where({name:"Alex Corvi"}).pullAll({todos:["mongoof","vunode"]});
    

    addToSet

    Push a value to an array only if it doesn't exists already

    this.where({name:"Alex Corvi"}).addToSet({projects:"mongoof"});
    

    addAllToSet

    Push multiple values to an array if they don't exist already

    this.where({name:"Alex Corvi"}).addAllToSet({projects:["mongoof","vunode","pul"]});
    

    pop

    pop an item out of an array

    this.where({name:"Alex"}).pop({wishlist:-1}); // pops the last item
    this.where({name:"Alex"}).pop({wishlist:1}); // pops the first item
    

    How to call a method?

    Now in our model, we defined these methods:

     
    const Model = require("mongoof").model;
     
    new Model("users")
    .schema({/* ... */})
    .methods({
       read:function(){
           return new Promise(function(resolve,rejects){
               this.read()
               .then(function(out){
                   resolve(out);
               })
           })
       }
    })
    .compile(); // don't EVER, EVER, EVER forget to compile
     

    Somewhere else (in our API maybe .. ):

    const Model = require("mongoof").model;
    Model("users").read(); // will call the function we defined above

    Options

    Last but not least, the options object, which is just wrapper around mongoose schema options

    find out more

    Mongoose aliases:

    require("mongoof").connect === require("mongoose").connect require("mongoof").connection === require("mongoose").connection

    CRUD library outside model methods

    const crud = require('mongoof').crud;
    new crud("collectionName").read() // returns a promise just like this.read();

    Install

    npm i mongoof

    DownloadsWeekly Downloads

    1

    Version

    0.0.2

    License

    MIT

    Last publish

    Collaborators

    • avatar