Rest API Framework
MongoDB Object Document Mappers
Overview
Below are all files and directories in the REST API Framework related to MongoDB
└── server/
├── models/
│ ├── customTypes.js
│ └── User.js
└── utils/
└── mongodb/
├── buildMongoFilters.js
├── buildMongoOrders.js
├── validator.js
└── index.js
MongoDB Connector
Below is a simple example of how you can connect to MongoDB Server using the mongodb utils
import mongodb from "server/utils/mongodb"
await mongodb.connect({
protocol: "mongodb",
host: "127.0.0.1:27017",
username: "",
password: "",
params: "",
name: "database_name",
debug: true, // turn on to debug database executing
})
Once connected, the server will open a connection to the DB at mongodb://127.0.0.1:27017/ named database_name and cache the connection for later uses. This is a required step before you want to do anything with the database.
Object Document Mappers (ODMs)
What is ODMs?
ODM (Object Document Mapper) is a utility whose main purpose is to map the documents from a Document Database such as MongoDB to an object in code, allowing developers to interact with the database more easily and more conveniently.
Although MongoDB Documents are stored in the database as BSON type, which is very similar to JSON, they still have some extra field types that are not supported in Javascript (NodeJS). That"s why we need ODM to remap these field types. Besides that, one of the main reasons why people use ODM is the ability to validate the data (the last defender) before storing permanently into the database.
There are 2 most used MongoDB ODM NodeJS packages that you should take a look: Mongoosejs and MongoDB TypeORM
Glife REST API Framework ODM is inspired by Mongoose, a simpler version, with a lot of tweaking to make it more suitable for Glife Developers and also has a built-in Data Loader for better performance.
Glife REST API Framework ODM
Inside mongodb utils, there is a method called createModel which will allow you to create a Model and it will be cached inside the ODM. The ODM will then map the Model to the Document in MongoDB and expose methods so you can execute database CRUD operations conveniently.
createModel method calling signature
// Calling signature
mongodb.createModel(dbCollectionName, schema, options)
dbCollectionName: String. The collection name in the MongoDB you want to map and store dataschema: Object. The ODM will use this schema for validation and mapping. You can define a field schema in two ways, using aStringor anObject. Here"s the list of available properties in the field schema that you can set:
| Name | Type | Description |
|---|---|---|
| type | String (Required) | The data type of this field. The ODM currently supports 7 scalar types |
| isRequired | Boolean | If this field is omitted when creating a new record, an Error will be thrown. |
| isPrivate | Boolean | This field is private so it can only be used on the server side and will be removed when returning data to the clients. |
| isEmail | Boolean | Only valid for |
| default | Any | If the field has no value when creating record, the default value will be used instead. This value will be added before validation so make sure it is valid. |
| length | Number or [Number] | Only valid for |
| min | Number | Only valid for |
| max | Number | Only valid for |
| enum | [Any] | An array of valid values, the field value must be one of these to be valid. |
options: extra configurations for this model. All available options are:
| Name | Type | Description |
|---|---|---|
| virtualFields | Object | Or you can call them calculated fields. These fields are not stored in the database but instead will be calculated base on other fields when calling |
| includes | Object | Define relationship fields which can be resolved and included in when calling |
| indexes | Array | Indexes which will be passed to MongoDB Driver createIndexes method. |
| validations | Async Function | Custom validations which will be executed after the model validations have run |
Below is an example of how to create a Model
// server/models/User.js
import mongodb from "server/utils/mongodb"
const User = mongodb.createModel(
"users",
{
username: { type: "String", isRequired: true, length: [5] },
email: "String|Email|Required",
password: "String|Private|Required",
firstName: "String|Required",
lastName: "String",
phone: "Phone",
friendIds: "[ID]",
role: { type: "String", enum: ["user", "admin"], default: "user" }
},
{
virtualFields: {
fullName: (parent) => {
return `${parent.firstName} ${parent.lastName}`
}
},
includes: {
friends: { ref: "users", keys: "friendIds" }
posts: { ref: "posts", foreignKey: "authorId" }
},
indexes: [
{ key: { username: 1 }, unique: true },
{ key: { email: 1 }, unique: true }
],
validations: async(input) => {
// validating input
// throw error if not valid
},
}
)
export default User
The model file should export default the Model created by the createModel method.
Model methods
CRUD Operations
Once the model is created, you can interact with the corresponding collection using the following methods:
async model.find(filter, options): get an array of records that match thefiltercriteria. ReturnCursor<[Object]>async model.findOne(filter, options): get the first record that match thefiltercriteria. ReturnCursor<Object>async model.findById(id, options): wrappingfindOne()to get the record which has the id matched withid. ReturnCursor<Object>async model.findByIds(ids, options): wrappingfind()to get the array of records which have the id appeared in theids. ReturnCursor<[Object]>async model.countDocuments(filter, options): count the total of records that match thefiltercriteria. ReturnNumberasync model.createOne(doc, options): validate and insert thedocinto the database. ReturnCursor<Object>of created recordasync model.updateOne(filter, newDoc, options): validate and update the first record that match thefiltercriteria, new values innewDocwill replace old value in the database and keep the others the same. ReturnCursor<Object>of updated recordasync model.updateById(id, newDoc, options): wrappingupdateOne()to update the record which has the id matched withid. ReturnCursor<Object>of updated recordasync model.deleteOne(filter, options): wrappingupdateOne()to soft-delete (upsert thedeletedAtfield) the first record that match thefiltercriteria. ReturnCursor<Object>of deleted recordasync model.deleteById(id, options): wrappingdeleteOne()to soft-delete the record that has the id matched withid. ReturnCursor<Object>of deleted record
Cursor
Cursor is a special Class extends Object or Array class based on which methods are called. Basically, Cursor is the same as Object and Array classes but has 2 special methods:
async resolve(includes): a method to tell theCursorto resolvevirtualFieldsand relationship fields based on theincludespassed in.includesis anObjectto tell theCursorwhich fields to keep, which to be removed after resolved.toJSON(): call this method will return plainObjectorArraytype of theCursor(without extra methods or properties)
Hooks
For createOne, updateOne and deleteOne, there are also hooks which allow you to append common logic before or after the database operations are executed so you don"t have to duplicate code in many places
To add the hooks, simply call the on() method (Event Emitter) with the event named like this "{before|after}:{method}". For example:
const User = mongodb.createModel(...)
User.on("before:createOne", (doc, options) => {
// hook before createOne
})
User.on("after:updateOne", (filter, newDoc, options) => {
// hook after updateOne
})
You should know
If you want to hook both createOne and updateOne in just one place, you can use the before:save or after:save event. And to check if it is create or update, check the oldDocument property in the options
Relationship and Data Loader
Data Loader is a generic utility to be used as part of your application"s data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching and caching. You can check out how the Data Loader works in the Official DataLoader Repo
Data Loader allow the ODM to batch resolving relationship fields among multiple documents, instead of resolving one by one which will cost significant amount of time and work load to the database.
To define a relationship, you can use the includes option when creating the Model. includes is an Object containing the relationship fields with the resolving information that will be included in the record when call resolve() on the Cursor. Below is how you can define includes:
// includes signature
// { ref: relatedCollection, [key | keys | foreignKey]: keyToLookup, filter: findFilter }
const Post = mongodb.createModel("posts", {
title: "String",
content: "String",
authorId: "ID"
published: "Boolean"
}, {
includes: {
author: { ref: "users", key: "authorId" }
}
})
const User = mongodb.createModel("users", {
email: "String",
fullName: "String",
}, {
includes: {
posts: { ref: "posts", foreignKey: "authorId" },
publishedPosts: {
ref: "posts", foreignKey: "authorId",
filter: { published: true }
},
}
})
ref: the collection name of the related Model you want to establish the relationshipkey | keys | foreignKey: the name of the field whose value will be looked up when resolving the relationship with the_idfield.keyis when the current Model is belong to related Model (Postbelong toUserwithauthorIdkey). Will returnObjectinstance of the related record when resolve.keysis also when the current Model is belong to related Model but the key field value is an array of ID ([ID]). Will returnArrayof related records when resolve.foreignKeyis when the related Model is belong to current Model (Userwant to get thePostsbelong to it). Will returnArrayof related records when resolve.
filter: the filter you want to apply when resolving the related records (Userwant to get only thepublishedPostsfromPostModel)
For each include defined, there will be a dataloader that take responsibility to batch query the database and resolve related records.