32-Nodejs Course 2023: Database Models: Crud Operations

hassanzohdy

Hasan Zohdy

Posted on November 3, 2022

32-Nodejs Course 2023: Database Models: Crud Operations

We have successfully created a our base model and connected it to database, we also made a base structure that we'll count on in this article to start create our base crud operations.

CRUD Operations

CRUD is an acronym for Create, Read, Update and Delete. These are the four basic functions of persistent storage.

These operations will be all static methods so we don't need to get an instance of the model to use them as the model will be used when there is a document inside it.

For example, User.create method will create a new user in the database and return a new instance of the user model with the data from the database.

Our base model will have the following methods:

  • create: Create a new document in the database.
  • update: Update a document in the database by id.
  • delete: Delete a document in the database by id.
  • find: Find a single document in the database by id.
  • findBy: Find a single document in the database by a given column/field name.
  • list: Get list of documents in the database.

Remember, these all will be static methods.

Create

This method will receive an object of data that will be created in the database and return a new model instance with the data from the database.

// src/core/database/model/model.ts
import { Collection } from "mongodb";
import connection, { Connection } from "../connection";
import { Database } from "../database";

/**
 * Base Model type that we need to define
 * to tell typescript that we're going to return a child that extends that base model
 * which will be the T (The child class)
 */
type BaseModel<T> = typeof Model & (new () => T);

export default abstract class Model {
  // ...

  /**
   * Create a new document in the database.
   */
  public static async create<T>(
    // this define what does this will mean here
    // T: the Type of child class
    this: BaseModel<T>,
    data: Record<string, any>,
  ): Promise<T> {
    // get the collection query
    const query = this.query();

    // insert the data
    const result = await query.insertOne(data);

    // store the data in a new object
    const modelData = { ...data };

    // get the inserted id
    modelData._id = result.insertedId;

    // now return a new model instance with the data from the database
    return new (this as any)(modelData);
  }
}
Enter fullscreen mode Exit fullscreen mode

Wooohoo, i know there a lot of typescript stuff here, but don't worry, we'll explain it in a bit.

First we defined BaseModel type which we 're telling him that the T is the child class type that will extend the Model class and we'll use it to define what does this will mean in the create method.

Then we defined the create method as a static method that will receive a data object and return a new model instance with the data from the database.

Let's split it into parts:

The method will be async, and of course, any method that will have an operation on the database will be async.

It receives a an object with type Record which means it must be an object with any key and value.

The return type is a promise and the type of the promise is a new instance of model itself.

typeof this.constructor which means it will return the typeof the model itself.

The workflow of the methods works as this:

  • Get the collection query.
  • Insert the data.
  • Store the data in a new object.
  • Get the inserted id.
  • Return a new model instance with the data from the database.

this.constructor as any is a hack to get the type of the model itself.

But now when we return a new instance of the model, which will be an instance of the User model, the data are passed to the constructor, but we didn't create any constructors yet, so let's do it.

// src/core/database/model/model.ts
import { Collection } from "mongodb";
import connection, { Connection } from "../connection";
import { Database } from "../database";

export default abstract class Model {
  // ...

  /**
   * Model constructor.
   */
  public constructor(public data: Record<string, any> = {}) {
    //
  }

  /**
   * Create a new document in the database.
   */
  public static async create<T>(
    // this define what does this will mean here
    // T: the Type of child class
    this: BaseModel<T>,
    data: Record<string, any>,
  ): Promise<T> {
    // get the collection query
    const query = this.query();

    // insert the data
    const result = await query.insertOne(data);

    // store the data in a new object
    const modelData = { ...data };

    // get the inserted id
    modelData._id = result.insertedId;

    // now return a new model instance with the data from the database
    return new (this as any)(modelData);
  }
}
Enter fullscreen mode Exit fullscreen mode

Actually we can make a protected method to return a new instnace of the model itself, so we don't need to do this every time.

// src/core/database/model/model.ts
import { Collection } from "mongodb";
import connection, { Connection } from "../connection";
import { Database } from "../database";

export default abstract class Model {
  // ...

  /**
   * Model constructor.
   */
  public constructor(public data: Record<string, any> = {}) {
    //
  }

  /**
   * Create a new document in the database.
   */
  public static async create<T>(
    // this define what does this will mean here
    // T: the Type of child class
    this: BaseModel<T>,
    data: Record<string, any>,
  ): Promise<T> {
    // get the collection query
    const query = this.query();

    // insert the data
    const result = await query.insertOne(data);

    // store the data in a new object
    const modelData = { ...data };

    // get the inserted id
    modelData._id = result.insertedId;

    // now return a new model instance with the data from the database
    return this.self(data);
  }

  /**
   * Create a new model instance.
   */
  protected static self<T>(data: Record<string, any>): T {
    return new (this as any)(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's give it a try.

Open src/app/users/routes.ts file and add the following code:

// src/app/users/routes.ts
import User from "./models/user";

// we will use set time out to wait for database connection first
// this is just for testing
setTimeout(async () => {
  const user = await User.create({
    name: "Test",
  });

  console.log(user.data);
}, 4000);
Enter fullscreen mode Exit fullscreen mode

Now if we hovered over the const user to see its return type it will appear like this:

User Model

Which exactly what we were doing earlier in that complex typescript definitions.

You should see now in your terminal something like this:

Created User

We'll stop at this point so you can digest what we did, and we'll continue in the next part.

🎨 Conclusion

In this article we learned how to work with typescript types to define the return type of same class from a static method, also we created a create method that receives an object and insert it into the database for any model that extends the Model class.

In the next part we'll continue with the Model class and we'll add more methods to it.

🎨 Project Repository

You can find the latest updates of this project on Github

😍 Join our community

Join our community on Discord to get help and support (Node Js 2023 Channel).

🎞️ Video Course (Arabic Voice)

If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.

💰 Bonus Content 💰

You may have a look at these articles, it will definitely boost your knowledge and productivity.

General Topics

Packages & Libraries

React Js Packages

Courses (Articles)

💖 💪 🙅 🚩
hassanzohdy
Hasan Zohdy

Posted on November 3, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related