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 aString
or 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 thefilter
criteria. ReturnCursor<[Object]>
async model.findOne(filter, options)
: get the first record that match thefilter
criteria. 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 thefilter
criteria. ReturnNumber
async model.createOne(doc, options)
: validate and insert thedoc
into the database. ReturnCursor<Object>
of created recordasync model.updateOne(filter, newDoc, options)
: validate and update the first record that match thefilter
criteria, new values innewDoc
will 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 thedeletedAt
field) the first record that match thefilter
criteria. 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 theCursor
to resolvevirtualFields
and relationship fields based on theincludes
passed in.includes
is anObject
to tell theCursor
which fields to keep, which to be removed after resolved.toJSON()
: call this method will return plainObject
orArray
type 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_id
field.key
is when the current Model is belong to related Model (Post
belong toUser
withauthorId
key). Will returnObject
instance of the related record when resolve.keys
is also when the current Model is belong to related Model but the key field value is an array of ID ([ID]
). Will returnArray
of related records when resolve.foreignKey
is when the related Model is belong to current Model (User
want to get thePosts
belong to it). Will returnArray
of related records when resolve.
filter
: the filter you want to apply when resolving the related records (User
want to get only thepublishedPosts
fromPost
Model)
For each include
defined, there will be a dataloader
that take responsibility to batch query the database and resolve related records.