
Security News
Browserslist-rs Gets Major Refactor, Cutting Binary Size by Over 1MB
Browserslist-rs now uses static data to reduce binary size by over 1MB, improving memory use and performance for Rust-based frontend tools.
A less-error-prone alternative syntax for Mongoose.
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.
$ npm install --save mongoof
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
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:
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
Declaring a data type can be considered as a validation, however, you can define more validation. And there are two types of 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.
For simple and common validations, there are few built-in validators that you can use:
required
min
max
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!"],
}
}
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);
},
}
}
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);
});
}
}
}
}
]
})
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]where
)sort
)read
)A query statement SHOULD begin with this
and end with a runner.
The only selector available is the where
selector. but you can apply operators on it as you wish.
this.where({key1:"value1",key2:"value2"}).read()
this.where({age:{$gt:18}}).read()
this.where({age:{$lt:18}}).read()
this.where({age:{$gte:18}}).read()
this.where({age:{$lte:18}}).read()
this.where({age:{$ne:18}}).read()
this.where({key:{$in:[0,2,4,6,8]}}).read()
this.where({key:{$nin:[1,3,5,7,9]}}).read()
this.where({$or:[{quantity:{$lt:20}},{price:10}]}).read();
this.where({$and:[{quantity:{$lt:20}},{price:10}]}).read();
this.where({price:{$not:{$gt:1.99}}}).read();
this.where({$nor:{available:false,mark:{$lt:50}}}).read();
this.where({refundedAmount:{$exists:true}}).read();
this.where({zipCode:{$type:"string"}}).read();
this.where({copies:{$mod:[2,0]}}).read(); // even
this.where({copies:{$mod:[2,1]}}).read(); // odd
this.where({email:{$regex:/\w+@gmail\.com/}}).read();
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();
Runners are the last methods in the query statement, they run the query, and return a promise
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 new document, takes one argument as the document to be created:
this.create({
firstName:"Alex",
lastName:"Corvi",
email:"alex@arrayy.com",
})
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 specific fields in a document(s) that satisfies the where
criteria.
this.where({lastLoggedIn:{$lt:1384655207000}}).multi(true).update({active:false});
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 a numerical value
this.where({title:"salesman"}).inc({salary:300}); // a raise for salesmen
this.where({title:"CEO"}).inc({salary:-300}); // :(
Pushing a value to an array
this.where({name:"Alex Corvi"}).push({projects:"mongoof"});
Pushing multiple values to an array
this.where({name:"Alex Corvi"}).pushAll({projects:["mongoof","vunode","pul"]});
Removes a value from an array
this.where({name:"Alex Corvi"}).pull({todos:"mongoof"});
Removes a value from an array
this.where({name:"Alex Corvi"}).pullAll({todos:["mongoof","vunode"]});
Push a value to an array only if it doesn't exists already
this.where({name:"Alex Corvi"}).addToSet({projects:"mongoof"});
Push multiple values to an array if they don't exist already
this.where({name:"Alex Corvi"}).addAllToSet({projects:["mongoof","vunode","pul"]});
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
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
Last but not least, the options object, which is just wrapper around mongoose schema options
require("mongoof").connect === require("mongoose").connect
require("mongoof").connection === require("mongoose").connection
const crud = require('mongoof').crud;
new crud("collectionName").read() // returns a promise just like this.read();
FAQs
an ODM built on top of mongoose
The npm package mongoof receives a total of 3 weekly downloads. As such, mongoof popularity was classified as not popular.
We found that mongoof demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Browserslist-rs now uses static data to reduce binary size by over 1MB, improving memory use and performance for Rust-based frontend tools.
Research
Security News
Eight new malicious Firefox extensions impersonate games, steal OAuth tokens, hijack sessions, and exploit browser permissions to spy on users.
Security News
The official Go SDK for the Model Context Protocol is in development, with a stable, production-ready release expected by August 2025.