DEV Community

Cover image for 36-Nodejs Course 2023: Database Master Mind
Hasan Zohdy
Hasan Zohdy

Posted on

36-Nodejs Course 2023: Database Master Mind

Now we've seen how to operate with database CRUD operations, where we used the _id field to identify the document. But what if we want to use a different field to identify to have an auto incremented id with an integer value? In this article, we will see how to do that.

Auto Incremented Id

So how this works? We'll create a database collection (table), i'll call it MasterMinder, that collection we'll use as our base collection to store for now two columns, the collection name that we want to auto increment the id and the last id that we used.

How this works

When we create a new record in the database using create method we've created, we'll add a column internally called id, what we'll need here is to get the last id of the current Model's collection name from the MasterMind collection, if that collection was not created yet, we'll create it and set the last id to 1 or to equal the initialValue that we'll add as a feature later, then we'll increment the last id by 1 and save it in the MasterMind collection.

Implementation

So let's start by creating a new file called master-mind.ts inside src/core/database/model directory, and add the following code:

// src/core/database/model/master-mind.tsimport connection, { Connection } from "./../connection";

export class MasterMind {
  /**
   * Database Connection
   */
  public connection: Connection = connection;

  /**
   * Get the last id of the collection
   */
  public async getLastId(collectionName: string): Promise<number> {
    // get the collection query
    const masterMind = await this.connection.database.collection("MasterMind");
    // find the record of the given collection name
    const masterMindData = await masterMind.findOne({ collectionName });

    // if the record exists, then return it
    if (masterMindData) {
      return masterMindData.id;
    }

    // if the record does not exist, then return zero
    return 0;
  }

  /**
   * Increment and return the last updated id for the given collection
   * If the collection does not exist, then create it and return the initial value
   */
  public async generateNextId(
    collectionName: string,
    incrementBy = 1,
    initialValue = 1,
  ): Promise<number> {
    // get the collection query
    const masterMind = await this.connection.database.collection("MasterMind");

    // find the record of the given collection name
    const masterMindData = await masterMind.findOne({ collectionName });

    // if the record exists, then increment the id by the given incrementBy value
    if (masterMindData) {
      const lastId = masterMindData.id + incrementBy;
      // update the record with the new id
      await masterMind.updateOne({ collectionName }, { $set: { id: lastId } });
      // return the new id
      return lastId;
    }

    // if the record does not exist, then create it with the given initial value
    await masterMind.insertOne({ collectionName, id: initialValue });

    // return the initial value
    return initialValue;
  }
}

const masterMind = new MasterMind();

export default masterMind;
Enter fullscreen mode Exit fullscreen mode

So we have created a class that will operate with two operations, one just to getting the last id, which will return 0 if the collection does not exist, this case is not usually happens but it's good to have it, and the second operation is to generate the next id, which will return the initial value if the collection does not exist, and will increment the last id by the given incrementBy value.

Now let's go to our model and apply the changes.

// src/core/database/model/model.ts
import masterMind from './master-mind';

export default abstract class Model {
  // ...
  /**
   * Define the initial value for the auto incremented id
   */
  public static initialId = 1;

  /**
   * define the increment by value for the auto incremented id
   */
  public static incrementBy = 1;

  /**
   * Generate and return next id of current collection
   */
  public static async generateNextId() {
    return await masterMind.generateNextId(
      this.collectionName,
      this.incrementBy,
      this.initialId,
    );
  }

  /**
   * Create a new record in the database for the current model (child class of this one)
   * and return a new instance of it with the created data and the new generated id
   */
  public static async create<T>(
    this: BaseModel<T>,
    data: Record<string, any>,
  ): Promise<T> {
    // 1- get the query of the collection
    const query = this.query();

    const modelData = { ...data };

    // check if the data does not have an id column
    if (!modelData.id) {
      modelData.id = await this.generateNextId();
    }

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

    // associate the mongodb _id as well
    modelData._id = result.insertedId;

    return this.self(modelData);
  }
}
Enter fullscreen mode Exit fullscreen mode

What we did here is we added two static variables, one for the initial value and the other for the increment by value, and we used them in the create method, but we checked first if the data that is coming does not have an id column, as in some rare situations the id is being generated outside the model, and we don't want to override it, so if there is no id, then we'll generate one.

Now let's give it a try.

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

setTimeout(async () => {
  const user = await User.create({
    name: "hasan",
  });

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

Now you will see something like this:

Generated id

If you just saved the file again, it will generate a new record with a new id, and if you check the MasterMind collection, you will see that the id has been incremented.

Updating methods to use the new id

As you can tell, we were using MongoDB's _id as the id of the record, but now we are using our own id, so we need to update the methods to use the new id.

You know, actually we can make it more advanced, we can allow the user to define what is the primary key that we use as id in the finding methods, but for now, we'll just use the new id.

// src/core/database/model/model.ts

export default abstract class Model {
  // ....  
  /**
   * Primary column
   */
  public static primaryColumn = "id";
}
Enter fullscreen mode Exit fullscreen mode

We defined a new static variable called primaryColumn, and we set it to id by default, so now we can use it in any method that deals with the id, and also we'll change the type of the id to accept both string and number.

// src/core/database/model/model.ts

// update method

  /**
   * Update model by the given id
   */
  public static async update<T>(
    this: BaseModel<T>,
    // πŸ‘‡πŸ» replace the type of string to be string | number or ObjectId
    id: string | ObjectId | number,
    data: Record<string, any>,
  ): Promise<T> {
    // get the query of the current collection
    const query = this.query();

    // execute the update operation

    // πŸ‘‡πŸ» replace the _id column with the primary column
    const filter = {
      [this.primaryColumn]: id,
    };

    const result = await query.findOneAndUpdate(
      filter,
      {
        $set: data,
      },
      {
        returnDocument: "after",
      },
    );

    return this.self(result.value as Record<string, any>);
  }

  // replace method

  /**
   * Replace the entire document for the given document id with the given new data
   */
  public static async replace<T>(
    this: BaseModel<T>,
    // πŸ‘‡πŸ» replace the type of string to be string | number or ObjectId
    id: string | ObjectId | number,
    data: Record<string, any>,
  ): Promise<T> {
    const query = this.query();

    // πŸ‘‡πŸ» replace the _id column with the primary column
    const filter = {
      [this.primaryColumn]: id,
    };

    const result = await query.findOneAndReplace(filter, data, {
      returnDocument: "after",
    });

    return this.self(result.value as Record<string, any>);
  }

  // find method

  /**
   * Find document by id
   */
    // πŸ‘‡πŸ» replace the type of string to be string | number or ObjectId
  public static async find<T>(this: BaseModel<T>, id: string | ObjectId | number) {
    // πŸ‘‡πŸ» replace the _id column with the primary column
    return this.findBy(this.primaryColumn, id);
  }
Enter fullscreen mode Exit fullscreen mode

Now regarding the delete method, the type we were using using is ObjectId or an object, so we'll add also the number type.

In the check part, we'll check if the given value is ObjectId string or number , then we'll perform a filter by multiple columns, otherwise, we'll perform a filter by the primary column.

// src/core/database/model/model.ts

  /**
   * Delete single document if the given filter is an ObjectId of mongodb
   * Otherwise, delete multiple documents based on the given filter object
   */
  public static async delete<T>(
    this: BaseModel<T>,
    filter: string | ObjectId | number | Record<string, any>,
  ): Promise<number> {
    const query = this.query();

    if (
      filter instanceof ObjectId ||
      typeof filter === "number" ||
      typeof filter === "string"
    ) {
      const result = await query.deleteOne({
        [this.primaryColumn]: filter,
      });

      return result.deletedCount;
    }

    const result = await query.deleteMany(filter);

    return result.deletedCount;
  }
Enter fullscreen mode Exit fullscreen mode

Our last remaining question

What if the user's id now is 2 then the next one will be 3, what happens if the user with id 3 is deleted, what will be the next id that will be generated?, of course it will be 4 as the id will not stop at the deleted ones but will go from the last saved value in the MasterMind collection.

🎨 Conclusion

In this article, we learned how to generate an auto-increment id for our MongoDB documents, and we also learned how to use it in our models, and we also learned how to update the methods to use the new id.

In our next article, it will be another refactoring article, we'll refactor the Model class to be more generic and more reusable, and we'll also add some new features 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)

Top comments (0)

An Animated Guide to Node.js Event Loop

Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

What happens under the hood when Node.js works on tasks such as database queries? We will explore it by following this piece of code step by step.