mongoof
v0.0.2
Published
an ODM built on top of mongoose
Downloads
1
Readme
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:
this
keyword [REQUIRED]- Selector [optional] (example:
where
) - Modifier [optional] (example:
sort
) - Runner [REQUIRED] (example:
read
)
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 thewhere
selector criteria, update them all.
NOTE
sort
,limit
andskip
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:"[email protected]",
})
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
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();