DEV Community

Cover image for 39-Nodejs Course 2023: Using Query Builder In Crud Model
Hasan Zohdy
Hasan Zohdy

Posted on

39-Nodejs Course 2023: Using Query Builder In Crud Model

In our previous article, we created the Query Builder which performs general operations over MongoDB. In this article, we will use the Query Builder in our CRUD model.

Using Query Builder in CurdModel

Now we've finished the query builder operations, let's use it directly in the CurdModel.

Let's start with create method.

Create Method

Let's update the create method to use the query builder directly.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...

  /**
   * 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: ChildModel<T>,
    data: Document,
  ): Promise<T> {
    const modelData = { ...data };

    // generate a new id
    modelData.id = await this.generateNextId();

    // perform the insertion
    // please note that the create method returns directly the document with `_id` attached to it.
    const result = await queryBuilder.create(this.collectionName, modelData);

    return this.self(result as ModelDocument);
  }
}
Enter fullscreen mode Exit fullscreen mode

We generated the id and then we called the create method of the query builder which takes the collection name and the data and returns the document with _id column as well.

Update Method

Let's update the update method to use the query builder directly as well.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...

  /**
   * Update model by the given id
   */
  public static async update<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
    data: Document,
  ): Promise<T | null> {
    // perform the update
    const result = await queryBuilder.update(
      this.collectionName,
      { [this.primaryIdColumn]: id },
      data,
    );

    return result ? this.self(result as ModelDocument) : null;
  }
Enter fullscreen mode Exit fullscreen mode

We passed the collection name, the filter object, which we'll sue the primary id column and the value will be the given id, and the data to update.

If there is a result coming, then the update has been done, otherwise it will return null.

Thus we updated the return type of the method to return an instance of class if the update has been done, otherwise it will return null.

Replace Method

Now let's update the replace method.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...
  /**
   * Replace the entire document for the given document id with the given new data
   */
  public static async replace<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
    data: Document,
  ): Promise<T | null> {
    // perform the replace
    const result = await queryBuilder.replace(
      this.collectionName,
      { [this.primaryIdColumn]: id },
      data,
    );

    return result ? this.self(result as ModelDocument) : null;
  }
Enter fullscreen mode Exit fullscreen mode

Pretty much straightforward, we passed the collection name, the filter object, which we'll sue the primary id column and the value will be the given id, and the data to replace.

Upsert Method

Moving to upsert method.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...
  /**
   * Update the document if it exists, otherwise create a new one
   */
  public static async upsert<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
    data: Document,
  ): Promise<T> {
    // perform the upsert
    const result = await queryBuilder.upsert(
      this.collectionName,
      { [this.primaryIdColumn]: id },
      data,
    );

    return this.self(result as ModelDocument);
  }
Enter fullscreen mode Exit fullscreen mode

FindBy Method

Now let's update the findBy method.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...
  /**
   * Find document by the given column and value
   */
  public static async findBy<T>(
    this: ChildModel<T>,
    column: string,
    value: any,
  ): Promise<T | null> {
    const result = await queryBuilder.first(this.collectionName, {
      [column]: value,
    });

    return result ? this.self(result as ModelDocument) : null;
  }
Enter fullscreen mode Exit fullscreen mode

List Method

Now let's update the list method.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...
  /**
   * List multiple documents based on the given filter
   */
  public static async list<T>(
    this: ChildModel<T>,
    filter: Filter = {},
  ): Promise<T[]> {
    const result = await queryBuilder.list(this.collectionName, filter);

    return result.map(item => this.self(item as ModelDocument));
  }
Enter fullscreen mode Exit fullscreen mode

So far nothing is hard to keep up with, we're just replacing the code to use the query builder directly.

Paginate Method

Now let's go to the paginate method.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...
  /**
   * Paginate multiple documents based on the given filter
   */
  /**
   * Paginate records based on the given filter
   */
  public static async paginate<T>(
    this: ChildModel<T>,
    filter: Filter = {},
    page = 1,
    limit = 15,
  ): Promise<PaginationListing<T>> {
    const documents = await queryBuilder.list(
      this.collectionName,
      filter,
      query => {
        query.skip((page - 1) * limit).limit(limit);
      },
    );

    const totalDocumentsOfFilter = await queryBuilder.count(
      this.collectionName,
      filter,
    );

    const result: PaginationListing<T> = {
      documents: documents.map(document =>
        this.self(document as ModelDocument),
      ),
      paginationInfo: {
        limit,
        page,
        result: documents.length,
        total: totalDocumentsOfFilter,
        pages: Math.ceil(totalDocumentsOfFilter / limit),
      },
    };

    return result;
  }
Enter fullscreen mode Exit fullscreen mode

Now let's see what we did here, let's split it into three parts.

I made a small and new update, is setting default values for pagination, for example the filter will be an empty object, page number will default to 1 and limit will be 15.

Then we called the list method of the query builder, we passed the collection name, the filter, and a callback function which will be called with the query object, so we can modify the query object, for example, we can skip some records and limit the number of records.

Then we called the count method of the query builder, we passed the collection name and the filter, and we got the total number of documents of the given filter.

Then we created the result object, we mapped the documents to the model class, and we returned the result.

Actually you know, we might add another feature in the BaseModel to set the default limit per page.

// src/core/database/model/base-model.ts

export default abstract class BaseModel  {
  // ...

  /**
   * Document per page
   */
  public static perPage = 15;
}
Enter fullscreen mode Exit fullscreen mode

Now let's use it in the paginate method.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...
  /**
   * Paginate multiple documents based on the given filter
   */
  /**
   * Paginate records based on the given filter
   */
  public static async paginate<T>(
    this: ChildModel<T>,
    filter: Filter = {},
    page = 1,
    limit = this.perPage,
  ): Promise<PaginationListing<T>> {
    // ...
  }
Enter fullscreen mode Exit fullscreen mode

So we can now use it lie this:

const result = await User.paginate(); // no filter and limit items per page will be 15
Enter fullscreen mode Exit fullscreen mode

Delete Method

Moving to the delete method.

As you can recall, we're performing two operations here, one for deleting single document, and the other for deleting multiple documents.

// src/core/database/model/curd-model.ts
import queryBuilder from "../query-builder/query-builder";

export default abstract class CrudModel extends BaseModel {
  // ...

  /**
   * 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: ChildModel<T>,
    filter: PrimaryIdType | Filter,
  ): Promise<number> {
    if (
      filter instanceof ObjectId ||
      typeof filter === "string" ||
      typeof filter === "number"
    ) {
      return queryBuilder.deleteOne(this.collectionName, {
        [this.primaryIdColumn]: filter,
      });
    }

    return await queryBuilder.delete(this.collectionName, filter);
  }
Enter fullscreen mode Exit fullscreen mode

Pretty much nice, right?

🎨 Conclusion

In this article, we learned how to use the query builder directly in the crud model, which reduces the code and makes it more readable.

🚀 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)