Сделаем перерыв в разработке, теперь давайте доработаем наш базовый код.


Создание типов моделей

Как мы видим, мы добавляем типы моделей внутри самого файла модели, что хорошо, но не является лучшей практикой. Итак, давайте создадим файл типов для модели и переместим туда наши типы.

// 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;
  };
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Мы переехали сюда двух типов, ChildModel тип, который мы используем для расширения возвращаемого типа модели, чтобы он соответствовал дочерней модели, и PaginationListing тип, который мы используем для возврата результатов разбивки на страницы.


Основной тип идентификатора

Как мы видим, в update мы добавляем тип идентификатора string | ObjectId | numberчто не лучшая практика, поэтому давайте создадим тип для идентификатора и используем его в модели.

// src/core/database/model/types.ts
/**
 * Primary id type
 */
export type PrimaryIdType = string | number | ObjectId;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь модель будет выглядеть так в своем конечном состоянии:

// 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];
  }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Разделение базовой модели

Как видите, базовая модель становится все больше и больше. Итак, нам нужно разбить его на несколько классов, мы можем сделать это несколькими способами, но давайте воспользуемся самым простым, то есть наследованием.

Давайте создадим BaseModel учебный класс.


Базовая модель

Базовая модель будет высшим классом, который будет содержать основные методы и статические свойства модели.

// 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;
  }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь давайте обновим нашу модель, чтобы расширить эту модель.

// 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;
  }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Что мы сделали здесь, так это переместили все переменные и методы соединения в базовый класс, модель теперь осталась с операциями CRUD, которые являются общими для всех моделей.

Но давайте перенесем эти грубые операции еще и в другой базовый класс, назовем его CrudModelчто CurdModel расширит BaseModel и Model расширит CrudModel.

// 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;
  }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Мы только что перенесли операции CRUD в CrudModel и Model сейчас в основном пуст, но нам нужно сделать так, чтобы он расширил CrudModel.

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

export default abstract class Model extends CrudModel {}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь модель в значительной степени чистая xD, но теперь у нас действительно все хорошо, так как мы можем разделить низкоуровневый код, чтобы он находился в BaseModel класс, а грубые операции в другом классе, что оставит хорошее место для нашего Model class иметь свои собственные методы и свойства, так как мы еще не начали это делать.


🎨 Заключение

В этой статье мы создали types файл и перемещаем туда наши объявления, мы также разделяем модель на два базовых класса, один из которых содержит базовые методы, а другой содержит операции CRUD.


🚀 Репозиторий проектов

Вы можете найти последние обновления этого проекта на Гитхаб


😍 Присоединяйтесь к нашему сообществу

Присоединяйтесь к нашему сообществу на Раздор для получения помощи и поддержки (канал Node Js 2023).


🎞️ Видеокурс (арабская озвучка)

Если вы хотите изучить этот курс в формате видео, вы можете найти его на YouTubeкурс на арабском языке.


💰 Бонусный контент 💰

Вы можете взглянуть на эти статьи, это определенно повысит ваши знания и производительность.

Общие темы

Пакеты и библиотеки

Пакеты React Js

Курсы (Статьи)