Nodejs Object-Oriented MongoDB Manager
Noomman is an npm package and can be installed using npm. Simply run the following command from the directory of your node project.
npm install --save noomman
To connect noomman to your database, use the noomman.connect() method. The method takes two arguments, the uri string for the connection, and the name of the database. Call connect when your app starts, usually in your app.js file.
const noomman = require('noomman');
...
const uri = "mongodb+srv://some:uri@somewhere"
const databaseName = "testdatabase";
await noomman.connect(uri, databaseName);
OR
noomman.connect(uri, databaseName).then( () => {
...
});
)
...
Create a Class Model, which tells noomman the expected shape and types of your data. The ClassModel constructor accepts an object containing the schema for the ClassModel. The schema is explained in depth here, but must at least contain a class name. Noomman has two main flavors of data it can store for your ClassModel. Attributes store simple types like Strings, Numbers, Dates, and Booleans. Relationships store Object Ids of related instances. Both Attributes and Relationships can be singular or non-singular.
Here is an example of a simple Animal Class Model. An animal has a name, and relationships to its parent animals.
const noomman = require('noomman');
const ClassModel = noomman.ClassModel;
const Animal = new ClassModel({
className: 'Animal',
attributes: [
{
name: 'name',
type: String,
},
],
relationships: [
{
name: 'parents',
toClass: 'Animal',
singular: false,
}
]
});
module.exports = Animal;
Once you have created your Class Model, you can start to use it. In noomman, there is a special class called Instance which you can use to create instances of your Class Model. You do this by calling new Instance() and passing in the desired Class Model.
const noomman = require('noomman');
const Animal = require('./Animal');
const Instance = noomman.Instance;
// create an Instance of Animal
const animal = new Instance(Animal);
Instances have built in methods to help you assign properties and interact with the database. Properties can be assigned directly, or using the Instance.assign() method. Continuing from the previous example we asign the animal a name.
animal.name = "Stewart";
OR
animal.assign({
name: "Stewart",
});
Now we can call Instance.save() to save our animal named Stewart to the database.
await animal.save();
All queries in noomman are executed using static methods on the ClassModel of the instance(s) you wish to find. There are 3 such methods, ClassModel.find(), ClassModel.findOne(), ClassModel.findById().
If we wish to find the instance of Animal named Stewart, we might use the findOne() method. Both find() and findOne() accept a query object as the first parameter. These query objects are expected to be valid mongodb query filters. The findOne() method returns a promise which will resolve to the the first instance of the ClassModel which matches the query filter, or null if no such instance is found.
// find an Instance of Animal with name equal to Stewart.
const stewart = await Animal.findOne({
name: "Stewart",
});
We could do the same thing with the find() method. The only difference is that find() will return a Promise which resolves to another class called an InstanceSet, rather than an Instance. An InstanceSet is subclass of the native JS Set class, which contains Instances of a particular Class Model, and provides some more helpful methods to work with.
Here is an example of find() to find the same Instance of Animal with the name Stewart. In this case, we have an extra step to extract the actual Instance itself from the InstanceSet. In this case, we accomplish this by spreading the InstanceSet into a normal Array, and then extracting the first element.
// find all Instances of Animal with name equal to Stewart.
const animalsNamedStewart = await Animal.find({
name: "Stewart",
});
// get the first instance from the set.
const stewart = [...animalsNamedStewart][0];
The findById() method is similar to findOne() except that it excepts an ObjectId as an argument. If we already have the ObjectId for the instance of Animal with the name Stewart, then we could find it like this...
// set a variable to the ObjectId of the Instance of Animal with the name Stewart
const stewartId = stewart._id;
// Pass the ObjectId to the findById method to retrieve the instance from the database.
const alsoStewart = await Animal.findById(stewartId);
So far we've set an attribute for an Instance. Now let's try setting a relationship to other instances. To set a relationship, you simply set the relationship property on an Instance to an Instance or InstanceSet of the approiate related Class Model, using either the Instance.assign() method or by assigning the property value directly.
Our Animal Class Model was defined with a non-singular relationship to other Animals named "parents". Because this is a non-singular relationship, we need to set it to an InstanceSet of class Animal. Here is how we might accomplish this task. First we create two more Animals, group them into an InstanceSet, and then assign the InstanceSet to the "parents" property. Finally we save the original Instance back to the database.
// find an Instance of Animal with name equal to Stewart.
const stewart = await Animal.findOne({
name: "Stewart",
});
// create two more animals which will be the parents of Stewart
const john = new Instance(Animal);
const jill = new Instance(Animal);
// give the parents names
john.name = "John";
jill.name = "Jill"
// create an InstanceSet of ClassModel Animal containing the two parents
const parentsOfStewart = new InstanceSet(Animal, [john, jill]);
// save John and Jill to the database
await parentsOfStewart.save();
// set the parents property of Stewart to the parents InstanceSet.
stewart.parents = parentsOfStewart;
// save stewart
await stewart.save();
Now Stewart has two parents! Good for him. Notice that we were able to save both John and Jill in a single line of code by calling InstanceSet.save(), which will save all the Instances contained in the InstanceSet.
Relationships have another powerful use in noomman. If we were working with the raw mongodb driver for javascript, we would first have to get the ObjectIds contained in the relationship, and then run another query to retrieve the actual Instances. In noomman we call this operation 'walking' a relationship, and it is as simple as getting the relationship property from an Instance.
// find an Instance of Animal with name equal to Stewart.
const stewart = await Animal.findOne({
name: "Stewart",
});
// get an InstanceSet containing Stewart's parents.
const parentsOfStewart = await stewart.parents;
// log the names of stewart's parents so we can see if this worked.
parentsOfStewart.forEach(parent => console.log(parent.name));
// outputs:
// John
// Jill
This deserves some explanation. Calling "stewart.parents" actualy executes a query for the related Instances, populates the parents property on Stewart so that if called again, we can skip the query, and then returns a Promise which resolves to an InstanceSet containing the related Instances. Also shown here, we can use InstanceSet.forEach() method to loop through all the Instances in the InstanceSet and execute some code.
The true power and differentiator of noomman in the ability for Class Models to inherit from one another. Class Models can inherit from multiple other Class Models. Every aspect of a Class Model is inheritable, including attributes, relationships, validations, methods, and more.
Lets create a couple sub Class Models of our Animal Class Model. One wil be Human, and the other Pet.
// Human.js
const noomman = require('noomman');
const Animal = require('./Animal');
const ClassModel = noomman.ClassModel;
const Human = new ClassModel({
className: 'Human',
superClasses: [ Animal ],
attributes: [
{
name: 'dateOfBirth',
type: Date,
},
],
relationships: [
{
name: 'pets',
toClass: 'Pet',
singular: false,
mirrorRelationship: 'owner',
}
],
});
module.exports = Human;
// Pet.js
const noomman = require('noomman');
const Animal = require('./Animal');
const ClassModel = noomman.ClassModel;
const Pet = new ClassModel({
className: 'Pet',
superClasses: [ Animal ],
attributes: [
{
name: 'adoptedOn',
type: Date,
},
],
relationships: [
{
name: 'owner',
toClass: 'Human',
singular: true,
mirrorRelationship: 'pets',
}
],
});
module.exports = Pet;
Inheritance is accomplished by including all the super Class Models for a sub Class Model within the "superClasses" property of the sub Class Model's schema. In our example, both Human and Pet have Animal as a super Class Model. This means that both Human and Pet also have a "name" attribute and a "parents" relationship.
In addition we add new Date-type attributes "dateOfBirth" for Human and "adoptedOn" for Pet. We add a non-singular relationship from Human to Pet called "pets" and a singular relationship from Pet to Human called "owner". These two relationships are actually two sides of a 1:Many relationship. They are connected to one another with the "mirrorRelationship" property, which is used to declare the other side of the two-way relationship.
Now that Animal has sub Class Models, we may decide that we shouldn't be creating instances of Animal itself. To accomplish this, lets add the "abstract" property to our Animal class schema. This will have noomman throw an error if any code attempts to create an Instance of Animal directly by calling new Instance(Animal).
const noomman = require('noomman');
const ClassModel = noomman.ClassModel;
const Animal = new ClassModel({
className: 'Animal',
abstract: true,
attributes: [
{
name: 'name',
type: String,
},
],
relationships: [
{
name: 'parents',
toClass: 'Animal',
singular: false,
}
]
});
module.exports = Animal;
Lets create a Human and two Animals for the following examples to use.
// create Instances of Human and Pet
const jessica = new Instance(Human);
const fluffy = new Instance(Pet);
const spot = new Instance(Pet);
// assign attributes and relationships to human
jessica.assign({
name: "Jessica",
dateOfBirth: new Date('2000-01-01');
pets: new InstanceSet(Pet, [fluffy, spot]);
});
// assign attributes and relationships to pets
fluffy.assign({
name: "Fluffy",
adoptedOn: new Date('2019-01-01'),
owner: jessica,
});
spot.assign({
name: "Spot",
adoptedOn: new Date('2019-06-01'),
owner: jessica,
});
// save our instances
await human.save();
await fluffy.save();
await spot.save();
We've created some data to work with. This is a good time to make a point about two-way relationships. Noomman enforces two-way relationship consistency, which means that when you save an Instance with a change to a two-way relationship, noomman will update the other side of the relatationship and save the related instances as well. For example, the following code is functionally equivalent to the above.
// create Instances of Human and Pet
const jessica = new Instance(Human);
const fluffy = new Instance(Pet);
const spot = new Instance(Pet);
// assign attributes and relationships to human
jessica.assign({
name: "Jessica",
dateOfBirth: new Date('2000-01-01');
pets: new InstanceSet(Pet, [fluffy, spot]);
});
// assign attributes and relationships to pets
fluffy.assign({
name: "Fluffy",
adoptedOn: new Date('2019-01-01'),
});
spot.assign({
name: "Spot",
adoptedOn: new Date('2019-06-01'),
});
// save our instances
await human.save();
Because the "pets" relationship on Human is a two-way relationship, setting the "pets" relationship for Jessica will set the "owner" relationship for the related pets when Jessica is saved. When we do call save on Jessica, noomman sets the other side of the relationship for each Pet, and saves those Pets in their current state. This is why we don't need to call save on fluffy or spot. Note that noomman also tracks changes to Instances since last save, so in the previous example, the calls to save fluffy and spot do not actually make a call to the database, because fluffy and spot have no changes on them since being saved as part of saving Jessica.
TLDR: calling save explicitly on each instance is not necessary in this case, but also doesn't cause any extra database calls. It's up to the developer whether they want to make the extra calls (for peace of mind or readability) or omit them (for cleaner, more exact code).
Since both Human and Pet are sub Class Models of Animal, we could query on the Animal class to find them (as opposed to querying on Human or Pet). For example, what if we wanted to find any Animal with the name Jessica (be it a Human or a Pet). We could do the following.
// find all Instances of Animal with a name equal to "Jessica".
const allAnimalsNamedJessica = Animal.find({
name: "Jessica"
});
Assuming we only have the data created above in the system, this will return an InstanceSet for Class Model Animal containing an Instance of Human that has the name Jessica. If we had an Instnace of Pet in the database with name equal to "Jessica", that would also be included in the InstanceSet. Noomman allows you to easily query for all Instances of a Class Model and all that Class Model's children, all the way down the inhertance tree.
Both our Human and Pet Class Model's inherit the "parents" relationship from Animal. That relationship was declared as a relationship from Animal to Animal. As is, this means that any Animal could be a parent to any other Animal. This is not quite how biology works, but it does allow a good example of a relationship that can be populated by different Instances of different Class Models. Just for fun, lets set Jessica as the parent of the two pets.
const petParent = new InstanceSet(Human, [jessica]);
fluffy.parents = petParent;
spot.parents = petParent;
await fluffy.save();
await spot.save();
Here we have set the parents of Fluffy and Spot to be Jessica. So even, though the "parents" relationship is to an Animal, we can set it to any InstanceSet containing Instances of any sub Class Model of Animal.
This is the end of the getting started guide. My hope is that this is enough to get you comfortable with the basics of noomman.
For more information please checkout the reference section, which contains more thorough explanations and examples of all the features of noomman, and the documentation section, which has detailed documentation of all the methods and classes contained in noomman.
Thanks for reading, and happy coding. :)