MongoDB

Koomi ORM

Overview

KoomiORM is a database mapper library developed by KoomiTeam, inspired by mongoosejs and built on top of NodeJS mongodb native driver. KoomiORM is easier to use with less advanced features than mongoose, so it allows KoomiTeam to control the speed of the queries and also can customize the features depend on the future projects requirement.


MongoDB Connector

Below is a example on how you can connect to a MongoDB Server using KoomiORM

import { koomiOrm } from "@koomi/koomiorm"

await koomiOrm.connect({
  protocol: "mongodb",
  host: "127.0.0.1:27017",
  username: "app",
  password: "",
  params: "",
  name: "database_name",
  authenticateDb: "/" // if not provide the default authenticate db is /{database_name}
  debug: true, // turn on to debug database executing
})

The method will return mongodb native MongoClient

Once connected, the server will open a connection to the database_name DB located at mongodb://127.0.0.1:27017/ and cache the connection for later uses. . This is a required step before you want to do anything with the database.

Basic Usages

Types

Currently, KoomiOrm supports 16 different Types.

  • Types.String: Text value
  • Types.Int: Integer number value
  • Types.Float: Float number value
  • Types.Boolean: true or false
  • Types.Date: Date value will automatically convert to timestamp (e.g. "01/01/2023" date will be stored as 1672506000000 in the database)
  • Types.Object: Embeded object value
  • Types.ID: MongoDB ObjectId value
  • Types.ArrayOfString: Array of String type values
  • Types.ArrayOfInt: Array of Int type values
  • Types.ArrayOfFloat: Array of Float type values
  • Types.ArrayOfBoolean: Array of Boolean type values
  • Types.ArrayOfDate: Array of Date type values
  • Types.ArrayOfObject: Array of Object type values
  • Types.ArrayOfID: Array of ID type values

Model

The most important concept in KoomiOrm is called Model. KoomiOrm uses Model to map it to the Collection in MongoDB and expose methods allow you to execute database CRUD operations more convenient. For example, inside MongoDB you have collection users you can create model User with the model name users to map it to that collection.

Inside koomiOrm instance, there is a method called createModel which will allow you to create a Model. The created model will be cached inside KoomiOrm and can be access anywhere in the application using the koomiOrm.model("modelName") method.

createModel method signature

// Signature
koomiOrm.createModel(dbCollectionName, schema, options)
  • dbCollectionName: String | The collection name in MongoDB you want to map (eg. users)
  • schema: Object | KoomiOrm will use this schema for validation and mapping. You can define a field schema in two ways, using a String or an Object. Here"s the list of available properties in the field schema that you can set:
NameTypeDescription
typeString (Required)

The data type of this field.

KoomiOrm currently supports 16 Types

isRequiredBoolean

If this field is omitted when creating a new record, an Error will be thrown.

defaultAny

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.

lengthNumber or [Number]

Only valid for String field type. If it is a Number, the field value must be the exact same length to be valid, and if it is an Array, the field value must have the length between length[0] and length[1].

minNumber

Only valid for Int and Float field type, the field type value must be bigger then min.

maxNumber

Only valid for Int and Float field type, the field type value must be smaller then max.

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:
NameTypeDescription
resolversObject

You can also call them calculated fields. These fields are not stored in the database but instead will be calculated base on other fields when calling resolve() method.

relationshipsObject

These are special resolvers, allow you to define relationship fields which can also be resolved and included in when calling resolve() method, but not like resolver they're resolved using built in and more optimal methods. Check the Relationship Section for more information about how to define a relationship.

indexesArray

Indexes which will be passed to MongoDB Driver createIndexes method.

Below is an example of how to create a Model

// server/models/User.js

import { koomiOrm, Types } from  "@koomi/koomiorm"

const User = mongodb.createModel(
  "users",
  {
    username: { type: Types.String , isRequired: true, length: [5] },
    email: { type: Types.String, isRequired: true },
    password: { type: Types.String, isRequired: true },
    firstName: { type: Types.String, isRequired: true },
    lastName: Types.String,
    friendIds: Types.ArrayOfID,
    role: { type: Types.Enum, enum: ["user", "admin"], default: "user" }
  },
  {
    resolvers: {
      fullName: (doc) => {
        return `${doc.firstName} ${doc.lastName}`.trim()
      }
    },
    relationships: {
      friends: { ref: "users", keys: "friendIds" }
      posts: { ref: "posts", foreignKey: "authorId" }
    },
    indexes: [
      { key: { username: 1 }, unique: true },
      { key: { email: 1 }, unique: true }
    ]
  }
)

export default User

Best Practice

Before you can interact with the collections, you need to define the Model. So it's a best practive to just import all the Model files right after you connect to the database, which allow koomiOrm to create and cache all the models and later part you just need to call koomiOrm.model("modelName") to get the model you want to interact with.

import { koomiOrm } from "@koomi/koomiorm"

const initMongo = async() => {
  await koomiOrm.connect({
    protocol: "mongodb",
    host: "127.0.0.1:27017",
    username: "app",
    password: "",
    params: "",
    name: "database_name",
    authenticateDb: "/" // if not provide the default authenticate db is /{database_name}
    debug: true, // turn on to debug database executing
  })
  import("server/models/User")
  import("server/models/Post")
}

...
initMongo()
...

CRUD Operations

Once the model is created, you can interact with the corresponding Mongodb collection using the following methods:

  • async model.findMany(filter, options): get an array of records that match the filter criteria. Return Cursor<[Object]>
  • async model.findOne(filter, options): get the first record that match the filter criteria. Return Cursor<Object>
  • async model.findById(id, options): wrapping findOne() to get the record which has the id matched with id. Return Cursor<Object>
  • async model.findByIds(ids, options): wrapping find() to get the array of records which have the id appeared in the ids. Return Cursor<[Object]>
  • async model.countDocuments(filter, options): count the total of records that match the filter criteria. Return Number
  • async model.createOne(doc, options): validate and insert the doc into the database. Return Cursor<Object> of created record
  • async model.createMany(docs, options): validate and insert all the docs into the database. Return Cursor<Array> of created records
  • async model.updateOne(filter, newDoc, options): validate and update the first record that match the filter criteria, new values in newDoc will replace old value in the database and keep the others the same. Return Cursor<Object> of updated record
  • async model.updateMany(filter, newDoc, options): validate and update all the records that match the filter criteria, new values in newDoc will replace old value in the database and keep the others the same. Return Cursor<Array> of updated records
  • async model.softDeleteMany(filter, options): wrapping updateMany() to soft-delete (upsert the deletedAt field) all the records that match the filter criteria. Return the Ids of deleted records
  • async model.deleteMany(filter, options): permanently delete all the records that match the filter criteria. Return the result of deleteMany

Note

Besides the above CRUD Operations, model instance also publish the (Native Mongodb Collection)[https://www.mongodb.com/docs/manual/reference/method/js-collection/], allow you to perform any native operations. You can get the collection by calling model.getCollection()

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 a special method:

  • async resolve(includes): a method to tell the Cursor to resolve resolver and relationships based on the includes passed in. includes is an Object to tell the Cursor which fields to keep, which to be omited after resolving.
import { koomiOrm } from "@koomi/koomiorm"

let post = await koomiOrm.models("posts").findOne({ ... })

console.log(post.author) // undefined

post = await post.resolve({ author: true })

console.log(post.author) // { id: ..., email: ..., ...}

Relationship and Data Loader

Data Loader is a generic utility to be used as part of your application 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 KoomiOrm to batch resolving relationship fields among multiple documents, instead of resolving one by one which will cost significant amount of time and database work load.

To define a relationship, you can use the relationship option when creating the Model. relationships is an Object containing all the relationship fields which will be included in the record when call resolve() on the Cursor. Below is how you can define relationships:

// relationships 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 relationship
  • key | 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 to User with authorId key). Will return Object 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 return Array of related records when resolve.
    • foreignKey is when the related Model is belong to current Model (User want to get the Posts belong to it). Will return Array of related records when resolve.
  • filter: the filter you want to apply when resolving the related records (User want to get only the publishedPosts from Post Model)
Previous
Query