37-Nodejs Course 2023: Break II: Splitting Model
Tags: node, typescript, mongodb, fastify
Published: 2022-11-05 05:36:03

---
title: 37-Nodejs Course 2023: Break II: Splitting Model
published: true
description: 
series: Nodejs Course 2023
tags: nodejs, typescript, mongodb, fastify
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cfdeaty2jg32cmtcmt42.png
# Use a ratio of 100:42 for best results.
---

Let's take a break of development, now lets refine our base code.

## Creating Model Types

As we can see that we're adding the Model types inside the model file itself, which is fine but not the best practice. So, let's create a type file for the model and move our types there.

```ts
// src/core/database/model/types.ts
import { ObjectId } from "mongodb";
import Model from "./model";

/**
 * Base model to be extended with Child Models
 */
export type ChildModel<T> = typeof Model & (new () => T);

/**
 * The result of the paginate query
 */
export type PaginationListing<T> = {
  /**
   * Results of the query
   */
  documents: T[];
  /**
   * The pagination results
   */
  paginationInfo: {
    /**
     * Limit of the query
     */
    limit: number;
    /**
     * Results of the query
     */
    result: number;
    /**
     * Current page of the query
     */
    page: number;
    /**
     * total results of the query
     */
    total: number;
    /**
     * total pages of the query
     */
    pages: number;
  };
};
```

We moved here two types, the `ChildModel` type which we use to extend the return type of the model to match the child model, and the `PaginationListing` type which we use to return the pagination results.

## Primary Id Type

As we can see that in the `update` method we're adding the id type to be `string | ObjectId | number`, which is not the best practice, so let's create a type for the id and use it in the model.

```ts
// src/core/database/model/types.ts
/**
 * Primary id type
 */
export type PrimaryIdType = string | number | ObjectId;
```

Now the model will look like this at its final state:

```ts
// src/core/database/model/model.tsimport { Collection, ObjectId } from "mongodb";
import connection, { Connection } from "../connection";
import { Database } from "../database";
import masterMind from "./master-mind";
import { ChildModel, PaginationListing, PrimaryIdType } from "./types";

export default abstract class Model {
  /**
   * Collection Name
   */
  public static collectionName = "";

  /**
   * Define the initial value of the id
   */
  public static initialId = 1;

  /**
   * Define the amount to eb incremented by for the next generated id
   */
  public static incrementIdBy = 1;

  /**
   * Primary id column
   */
  public static primaryIdColumn = "id";

  /**
   * Connection instance
   */
  public static connection: Connection = connection;

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

  /**
   * Get collection query
   */
  public static query() {
    return this.connection.database.collection(this.collectionName);
  }

  /**
   * 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: Record<string, any>,
  ): Promise<T> {
    // 1- get the query of the collection
    const query = this.query();

    const modelData = { ...data };

    modelData.id = await this.generateNextId();

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

    modelData._id = result.insertedId;

    return this.self(modelData);
  }

  /**
   * Update model by the given id
   */
  public static async update<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
    data: Record<string, any>,
  ): Promise<T> {
    // get the query of the current collection
    const query = this.query();

    // execute the update operation

    const filter = {
      [this.primaryIdColumn]: id,
    };

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

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

  /**
   * 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: Record<string, any>,
  ): Promise<T> {
    const query = this.query();

    const filter = {
      [this.primaryIdColumn]: id,
    };

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

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

  /**
   * Find and update the document for the given filter with the given data or create a new document/record
   * if filter has no matching
   */
  public static async upsert<T>(
    this: ChildModel<T>,
    filter: Record<string, any>,
    data: Record<string, any>,
  ): Promise<T> {
    // get the query of the current collection
    const query = this.query();

    // execute the update operation
    const result = await query.findOneAndUpdate(
      filter,
      {
        $set: data,
      },
      {
        returnDocument: "after",
        upsert: true,
      },
    );

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

  /**
   * Find document by id
   */
  public static async find<T>(this: ChildModel<T>, id: PrimaryIdType) {
    return this.findBy(this.primaryIdColumn, id);
  }

  /**
   * Find document by the given column and value
   */
  public static async findBy<T>(
    this: ChildModel<T>,
    column: string,
    value: any,
  ): Promise<T | null> {
    const query = this.query();

    const result = await query.findOne({
      [column]: value,
    });

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

  /**
   * List multiple documents based on the given filter
   */
  public static async list<T>(
    this: ChildModel<T>,
    filter: Record<string, any> = {},
  ): Promise<T[]> {
    const query = this.query();

    const documents = await query.find(filter).toArray();

    return documents.map(document => this.self(document));
  }

  /**
   * Paginate records based on the given filter
   */
  public static async paginate<T>(
    this: ChildModel<T>,
    filter: Record<string, any>,
    page: number,
    limit: number,
  ): Promise<PaginationListing<T>> {
    const query = this.query();

    const documents = await query
      .find(filter)
      .skip((page - 1) * limit)
      .limit(limit)
      .toArray();

    const totalDocumentsOfFilter = await query.countDocuments(filter);

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

    return result;
  }

  /**
   * 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 | Record<string, any>,
  ): Promise<number> {
    const query = this.query();

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

      return result.deletedCount;
    }

    const result = await query.deleteMany(filter);

    return result.deletedCount;
  }

  /**
   * Generate next id
   */
  public static async generateNextId() {
    return await masterMind.generateNextId(
      this.collectionName,
      this.incrementIdBy,
      this.initialId,
    );
  }

  /**
   * Get last id of current model
   */
  public static async getLastId() {
    return await masterMind.getLastId(this.collectionName);
  }

  /**
   * Get an instance of child class
   */
  protected static self(data: Record<string, any>) {
    return new (this as any)(data);
  }

  /**
   * Get collection name
   */
  public getCollectionName(): string {
    return this.getStaticProperty("collectionName");
  }

  /**
   * Get collection query
   */
  public getQuery(): Collection {
    return this.getStaticProperty("query")();
  }

  /**
   * Get connection instance
   */
  public getConnection(): Connection {
    return this.getStaticProperty("connection");
  }

  /**
   * Get database instance
   */
  public getDatabase(): Database {
    return this.getConnection().database;
  }

  /**
   * Get static property
   */
  protected getStaticProperty(property: keyof typeof Model) {
    return (this.constructor as any)[property];
  }
}
```

## Splitting Base Model

As you can see, the base model is getting bigger and bigger. So, we need to split it into multiple classes, we can do it by couple of ways, but let's use the simplest one, which is the inheritance.

Let's create the `BaseModel` class.

## Base Model

The base model will be the top class that will contain the basics methods and static properties for the model.

```ts
// src/core/database/models/base-model.ts
import { Collection } from "mongodb";
import connection, { Connection } from "../connection";
import { Database } from "../database";
import masterMind from "./master-mind";
import Model from "./model";

export default abstract class BaseModel {
  /**
   * Collection Name
   */
  public static collectionName = "";

  /**
   * Define the initial value of the id
   */
  public static initialId = 1;

  /**
   * Define the amount to eb incremented by for the next generated id
   */
  public static incrementIdBy = 1;

  /**
   * Primary id column
   */
  public static primaryIdColumn = "id";

  /**
   * Connection instance
   */
  public static connection: Connection = connection;

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

  /**
   * Get collection query
   */
  public static query() {
    return this.connection.database.collection(this.collectionName);
  }

  /**
   * Generate next id
   */
  public static async generateNextId() {
    return await masterMind.generateNextId(
      this.collectionName,
      this.incrementIdBy,
      this.initialId,
    );
  }

  /**
   * Get last id of current model
   */
  public static async getLastId() {
    return await masterMind.getLastId(this.collectionName);
  }

  /**
   * Get an instance of child class
   */
  protected static self(data: Record<string, any>) {
    return new (this as any)(data);
  }

  /**
   * Get static property
   */
  protected getStaticProperty(property: keyof typeof Model) {
    return (this.constructor as any)[property];
  }

  /**
   * Get collection name
   */
  public getCollectionName(): string {
    return this.getStaticProperty("collectionName");
  }

  /**
   * Get collection query
   */
  public getQuery(): Collection {
    return this.getStaticProperty("query")();
  }

  /**
   * Get connection instance
   */
  public getConnection(): Connection {
    return this.getStaticProperty("connection");
  }

  /**
   * Get database instance
   */
  public getDatabase(): Database {
    return this.getConnection().database;
  }
}
```

Now let's update our model to extend that model.

```ts
// src/core/database/models/model.ts
import { ObjectId } from "mongodb";
import BaseModel from "./base-model";
import { ChildModel, PaginationListing, PrimaryIdType } from "./types";

export default abstract class Model 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: Record<string, any>,
  ): Promise<T> {
    // 1- get the query of the collection
    const query = this.query();

    const modelData = { ...data };

    modelData.id = await this.generateNextId();

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

    modelData._id = result.insertedId;

    return this.self(modelData);
  }

  /**
   * Update model by the given id
   */
  public static async update<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
    data: Record<string, any>,
  ): Promise<T> {
    // get the query of the current collection
    const query = this.query();

    // execute the update operation

    const filter = {
      [this.primaryIdColumn]: id,
    };

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

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

  /**
   * 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: Record<string, any>,
  ): Promise<T> {
    const query = this.query();

    const filter = {
      [this.primaryIdColumn]: id,
    };

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

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

  /**
   * Find and update the document for the given filter with the given data or create a new document/record
   * if filter has no matching
   */
  public static async upsert<T>(
    this: ChildModel<T>,
    filter: Record<string, any>,
    data: Record<string, any>,
  ): Promise<T> {
    // get the query of the current collection
    const query = this.query();

    // execute the update operation
    const result = await query.findOneAndUpdate(
      filter,
      {
        $set: data,
      },
      {
        returnDocument: "after",
        upsert: true,
      },
    );

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

  /**
   * Find document by id
   */
  public static async find<T>(this: ChildModel<T>, id: PrimaryIdType) {
    return this.findBy(this.primaryIdColumn, id);
  }

  /**
   * Find document by the given column and value
   */
  public static async findBy<T>(
    this: ChildModel<T>,
    column: string,
    value: any,
  ): Promise<T | null> {
    const query = this.query();

    const result = await query.findOne({
      [column]: value,
    });

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

  /**
   * List multiple documents based on the given filter
   */
  public static async list<T>(
    this: ChildModel<T>,
    filter: Record<string, any> = {},
  ): Promise<T[]> {
    const query = this.query();

    const documents = await query.find(filter).toArray();

    return documents.map(document => this.self(document));
  }

  /**
   * Paginate records based on the given filter
   */
  public static async paginate<T>(
    this: ChildModel<T>,
    filter: Record<string, any>,
    page: number,
    limit: number,
  ): Promise<PaginationListing<T>> {
    const query = this.query();

    const documents = await query
      .find(filter)
      .skip((page - 1) * limit)
      .limit(limit)
      .toArray();

    const totalDocumentsOfFilter = await query.countDocuments(filter);

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

    return result;
  }

  /**
   * 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 | Record<string, any>,
  ): Promise<number> {
    const query = this.query();

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

      return result.deletedCount;
    }

    const result = await query.deleteMany(filter);

    return result.deletedCount;
  }
}
```

What we did here is we moved all variables and connection methods to the base class, the Model now is left with CRUD operations which are common for all models.

But let's move these crud operations also to another base class, let's call it `CrudModel`, that `CurdModel` will extend the `BaseModel` and the `Model` will extend the `CrudModel`.

```ts
// src/core/database/models/crud-model.ts
import { ObjectId } from "mongodb";
import BaseModel from "./base-model";
import { ChildModel, PaginationListing, PrimaryIdType } from "./types";

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: Record<string, any>,
  ): Promise<T> {
    // 1- get the query of the collection
    const query = this.query();

    const modelData = { ...data };

    modelData.id = await this.generateNextId();

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

    modelData._id = result.insertedId;

    return this.self(modelData);
  }

  /**
   * Update model by the given id
   */
  public static async update<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
    data: Record<string, any>,
  ): Promise<T> {
    // get the query of the current collection
    const query = this.query();

    // execute the update operation

    const filter = {
      [this.primaryIdColumn]: id,
    };

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

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

  /**
   * 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: Record<string, any>,
  ): Promise<T> {
    const query = this.query();

    const filter = {
      [this.primaryIdColumn]: id,
    };

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

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

  /**
   * Find and update the document for the given filter with the given data or create a new document/record
   * if filter has no matching
   */
  public static async upsert<T>(
    this: ChildModel<T>,
    filter: Record<string, any>,
    data: Record<string, any>,
  ): Promise<T> {
    // get the query of the current collection
    const query = this.query();

    // execute the update operation
    const result = await query.findOneAndUpdate(
      filter,
      {
        $set: data,
      },
      {
        returnDocument: "after",
        upsert: true,
      },
    );

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

  /**
   * Find document by id
   */
  public static async find<T>(this: ChildModel<T>, id: PrimaryIdType) {
    return this.findBy(this.primaryIdColumn, id);
  }

  /**
   * Find document by the given column and value
   */
  public static async findBy<T>(
    this: ChildModel<T>,
    column: string,
    value: any,
  ): Promise<T | null> {
    const query = this.query();

    const result = await query.findOne({
      [column]: value,
    });

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

  /**
   * List multiple documents based on the given filter
   */
  public static async list<T>(
    this: ChildModel<T>,
    filter: Record<string, any> = {},
  ): Promise<T[]> {
    const query = this.query();

    const documents = await query.find(filter).toArray();

    return documents.map(document => this.self(document));
  }

  /**
   * Paginate records based on the given filter
   */
  public static async paginate<T>(
    this: ChildModel<T>,
    filter: Record<string, any>,
    page: number,
    limit: number,
  ): Promise<PaginationListing<T>> {
    const query = this.query();

    const documents = await query
      .find(filter)
      .skip((page - 1) * limit)
      .limit(limit)
      .toArray();

    const totalDocumentsOfFilter = await query.countDocuments(filter);

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

    return result;
  }

  /**
   * 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 | Record<string, any>,
  ): Promise<number> {
    const query = this.query();

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

      return result.deletedCount;
    }

    const result = await query.deleteMany(filter);

    return result.deletedCount;
  }
}
```

We just moved the CRUD operations to the `CrudModel` and the `Model` now is mostly empty, but we need to make it extend the `CrudModel`.

```ts
// src/core/database/models/model.ts
import CrudModel from "./crud-model";

export default abstract class Model extends CrudModel {}
```

Now the model is pretty much clean xD, but we're really really good now as we can split the low level code to be in the `BaseModel` class, and the crud operations in another class, which will leave a good space to our `Model` class to have its own methods and properties as we didn't start doing it yet.

## 🎨 Conclusion

In this article, we created a `types` file and move our declarations there, we also split the model into two base classes, one contains the base methods and the other contains the CRUD operations.

## 🚀 Project Repository

You can find the latest updates of this project on [Github](https://github.com/hassanzohdy/nodejs-2023)

## 😍 Join our community

Join our community on [Discord](https://discord.gg/pb2vmdfhGf) 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](https://www.youtube.com/playlist?list=PLGO8ntvxgiZMJc7RN2lIq9WmMOlWZGzmz), 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

- [Event Driven Architecture: A Practical Guide in Javascript](https://dev.to/hassanzohdy/event-driven-architecture-the-best-paradigm-that-i-love-to-work-with-in-javascript-and-node-js-1gnk)
- [Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript](https://dev.to/hassanzohdy/best-practices-for-case-styles-camel-pascal-snake-and-kebab-case-in-node-and-javascript-55oi)
- [After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
](https://dev.to/hassanzohdy/after-6-years-of-practicing-mongodb-here-are-my-thoughts-on-mongodb-vs-mysql-574b)

Packages & Libraries

- [Collections: Your ultimate Javascript Arrays Manager](https://dev.to/hassanzohdy/collections-your-ultimate-javascript-array-handler-3o15)
- [Supportive Is: an elegant utility to check types of values in JavaScript](https://dev.to/hassanzohdy/supportive-is-an-elegant-utility-to-check-types-of-values-in-javascript-1b3e)
- [Localization: An agnostic i18n package to manage localization in your project](https://dev.to/hassanzohdy/mongez-localization-the-simplest-way-to-translate-your-website-regardless-your-favorite-framework-4gi3)

 React Js Packages

- [useFetcher: easiest way to fetch data in React Js](https://dev.to/hassanzohdy/usefetcher-the-easiest-way-to-fetch-data-in-react-45o9)

Courses (Articles)

- [React Js: Let"s Create File Manager With React Js and Node Js](https://dev.to/hassanzohdy/lets-create-a-file-manager-from-scratch-with-react-and-typescript-chapter-i-a-good-way-to-expand-your-experience-5g4k)