Пока все хорошо, мы обновили наш базовый код модели и освободили место в нашем Model класс, но мы забыли обновить некоторые типы в предыдущей статье, а именно добавляемые данные и параметры фильтра, просто откройте crud-model файл и найдите эту строку Record<string, any> вы обнаружите, что он дублируется, как 12-13 раз, давайте создадим тип данных, которые будут добавлены, и параметры фильтра.

// src/core/database/types.ts
import { ObjectId, WithId } from "mongodb";
// ...

/**
 * Filter object
 */
export type Filter = Record<string, any>;

/**
 * Document data will be used in insertion, updates or replace
 */
export type Document = Record<string, any>;

/**
 * Model Document that contains the model with both mongodb _id and our custom id
 */
export type ModelDocument = WithId<{
  id: number;
  [key: string]: any;
}>;
Войти в полноэкранный режим

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

Мы добавили сюда три типа, первый для параметров фильтров, второй для данных, которые будут добавлены, обновлены или заменены, а третий для документа модели, который содержит модель с обоими mongodb _id и наш обычай id.

Document type — это просто объект, который может содержать данные любого типа.

ModelDocument это Document с MongoDb _id колонка и id также колонка.

Теперь давайте обновим его в crud-model файл.

// src/core/database/crud-model.ts
import { ObjectId } from "mongodb";
import BaseModel from "./bae-model";
import {
  ChildModel,
  Document,
  Filter,
  ModelDocument,
  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>,
    // 👇🏻 Use the Document type here
    data: Document,
  ): 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;

    // 👇🏻 Cast the return data type as Model Document
    return this.self(modelData as ModelDocument);
  }

  /**
   * Update model by the given id
   */
  public static async update<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
    // 👇🏻 Use the Document type here
    data: Document,
  ): 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",
      },
    );

    // 👇🏻 Cast the return data type as Model Document
    return this.self(result.value as ModelDocument);
  }

  /**
   * Replace the entire document for the given document id with the given new data
   */
  public static async replace<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
    // 👇🏻 Use the Document type here
    data: Document,
  ): Promise<T> {
    const query = this.query();

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

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

    // 👇🏻 Cast the return data type as Model Document
    return this.self(result.value as ModelDocument);
  }

  /**
   * 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>,
    // 👇🏻 Use the filter type here
    filter: Filter,
    // 👇🏻 Use the Document type here
    data: Document,
  ): 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,
      },
    );

    // 👇🏻 Cast the return data type as Model Document
    return this.self(result.value as ModelDocument);
  }

  /**
   * 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,
    });

    // 👇🏻 Cast the return data type as Model Document
    return result ? this.self(result as ModelDocument) : null;
  }

  /**
   * List multiple documents based on the given filter
   */
  public static async list<T>(
    this: ChildModel<T>,
    // 👇🏻 Use the filter type here
    filter: Filter = {},
  ): Promise<T[]> {
    const query = this.query();

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

    // 👇🏻 Cast the return data type as Model Document
    return documents.map(document => this.self(document as ModelDocument));
  }

  /**
   * Paginate records based on the given filter
   */
  public static async paginate<T>(
    this: ChildModel<T>,
    // 👇🏻 Use the filter type here
    filter: Filter,
    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 as ModelDocument),
      ),
      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>,
    // 👇🏻 Use the filter type here
    filter: PrimaryIdType | Filter,
  ): Promise<number> {
    //
  }
}
Войти в полноэкранный режим

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

Мы заменили все фильтры и данные, которые будут использоваться при вставке/обновлении или удалении, на использование Document тип, который является просто объектом.

Filter также является простым объектом, просто имеет причудливое имя.

Когда мы возвращаем новый экземпляр модели, мы приводим возвращаемый тип данных как ModelDocument который является типом документа, который мы получаем из базы данных, поскольку он наверняка содержит _id колонка и id также колонка.

Теперь еще одна вещь, которую нужно сделать, это обновить тип данных конструктора, чтобы использовать BaseModel в конструкторе.

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

import { ModelDocument } from "./types";

export default abstract class BaseModel {
  // ...
  /**
   * Constructor
   */
  public constructor(public data: Partial<ModelDocument> = {}) {
    //
  }
}
Войти в полноэкранный режим

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

Таким образом, данные в модели могут содержать или не содержать id и/или _id но также может содержать и другие столбцы.

Мы обозначили его Partial type, так как это может быть пустой объект, например, если бы мы сделали что-то вроде этого:

const user = new User({
  name: 'hasan',
});
Войти в полноэкранный режим

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


Конструктор запросов

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

Итак, давайте создадим новый файл в src/core/database/query-builder папку и назовите ее query-builder.ts.

// src/core/database/query-builder/query-builder.ts
import { WithId } from "mongodb";
import connection, { Connection } from "../connection";

export class QueryBuilder {
  /**
   * Connection
   */
  protected connection: Connection = connection;

  /**
   * Get query of the given collection name
   */
  public query(collectionName: string) {
    return this.connection.database.collection(collectionName);
  }
}

const queryBuilder = new QueryBuilder();

export default queryBuilder;
Войти в полноэкранный режим

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

В основном так же, как BaseModel код, мы внедрили соединение и добавили метод с именем query чтобы получить запрос данного имени коллекции.

Позже мы экспортировали экземпляр QueryBuilder класс как queryBuilder поэтому мы можем использовать его напрямую.


Создать метод

Теперь добавим create метод, этот метод получает два аргумента, первый — это имя коллекции, а второй — данные, которые мы хотим вставить.

import { WithId } from "mongodb";
import connection, { Connection } from "../connection";
import { Document } from "../model/types";

export class QueryBuilder {
  /**
   * Connection
   */
  protected connection: Connection = connection;

  /**
   * Make a create operation
   */
  public async create(
    collectionName: string,
    data: Document,
  ): Promise<WithId<Document>> {
    const query = this.query(collectionName);

    const result = await query.insertOne(data);

    return {
      ...data,
      _id: result.insertedId,
    };
  }

  /**
   * Get query of the given collection name
   */
  public query(collectionName: string) {
    return this.connection.database.collection(collectionName);
  }
}

const queryBuilder = new QueryBuilder();

export default queryBuilder;
Войти в полноэкранный режим

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

Как мы видим, мы возвращаем тот же переданный документ с _id столбец, добавленный к нему, и сделал его возвращаемым типом документа с _id столбец.


Метод обновления

Теперь добавим update метод, этот метод получает три аргумента, первый — имя коллекции, второй — фильтр, а третий — данные, которые мы хотим обновить.

import { Document, Filter } from "../model/types";
import { WithId } from "mongodb";

export class QueryBuilder {
  // ...
  /**
   * Update document for the given filter
   */
  public async update(
    collectionName: string,
    filter: Filter,
    data: Document,
  ): Promise<WithId<Document> | null> {
    const query = this.query(collectionName);

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

    if (result.ok) {
      return result.value;
    }

    return null;
  }
}
Войти в полноэкранный режим

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

Здесь мы вернем два типа значений: первое — это обновленный документ, а второе — null если операция обновления не удалась.


Метод Upsert

Теперь добавим upsert метод, этот метод получает три аргумента, первый — имя коллекции, второй — фильтр, а третий — данные, которые мы хотим обновить.

import { Document, Filter } from "../model/types";
import { WithId } from "mongodb";

export class QueryBuilder {
  // ...

  /**
   * Update Or create document for the given filter
   */
  public async upsert(
    collectionName: string,
    filter: Filter,
    data: Document,
  ): Promise<WithId<Document> | null> {
    const query = this.query(collectionName);

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

    if (result.ok) {
      return result.value;
    }

    return null;
  }
}
Войти в полноэкранный режим

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

Здесь мы вернем два типа значений: первое — это обновленный документ, а второе — null если операция обновления не удалась.


Метод замены

Теперь добавим replace метод, этот метод получает три аргумента, первый — имя коллекции, второй — фильтр, а третий — данные, которые мы хотим заменить.

import { Document, Filter } from "../model/types";
import { WithId } from "mongodb";

export class QueryBuilder {
  // ...
  /**
   * Replace the entire document for the given filter
   */
  public async replace(collectionName: string, filter: Filter, data: Document) {
    const query = this.query(collectionName);

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

    return result.ok ? result.value : null;
  }
}
Войти в полноэкранный режим

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

Здесь мы вернем два типа значений: первое — это обновленный документ, а второе — null если операция обновления не удалась.


Первый метод (найти один документ)

Теперь добавим first метод, этот метод вернет первый совпадающий фильтр, этот метод получает три аргумента, первый — имя коллекции, второй — фильтр, а третий Найдите параметры MongoDB

Зачем нам добавлять такую ​​опцию? потому что нам нужно сделать этот построитель запросов более общим и абстрактным.

import { Document, Filter } from "../model/types";
import { FindOptions, WithId } from "mongodb";

export class QueryBuilder {
  // ...
  /**
   * Find first matched document for the given filter
   */
  public async first(
    collectionName: string,
    filter: Filter,
    filterOptions?: FindOptions,
  ) {
    const query = this.query(collectionName);

    return await query.findOne(filter, filterOptions);
  }
Войти в полноэкранный режим

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


Метод списка (найти несколько документов)

Теперь добавим list метод, этот метод вернет список совпадающих фильтров, этот метод получает четыре аргумента, первый — имя коллекции, второй — фильтр, третий — НайтиКурсор обратный вызов, чтобы мы могли выполнять дополнительные операции, например, использовать skip а также limit методы, и последний из них Найдите параметры MongoDB

import { Document, Filter } from "../model/types";
import { FindOptions, WithId } from "mongodb";

export class QueryBuilder {
  // ...

  /**
   * Find list of documents based on the given filter
   */
  public async list(
    collectionName: string,
    filter: Filter,
    filterQuery?: (cursor: FindCursor) => void,
    filterOptions?: FindOptions,
  ) {
    const query = this.query(collectionName).find(filter, filterOptions);

    if (filterQuery) {
      filterQuery(query);
    }

    return await query.toArray();
  }
Войти в полноэкранный режим

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

Чуть позже мы увидим его практическое применение.


Считать

Теперь добавим еще один, раньше мы его отдельно не делали, а добавили в paginate метод, который является countDocumentsэто также полезно для подсчета документов, поэтому давайте создадим count метод, который получает два аргумента, первый из которых является именем коллекции, а второй — фильтром.

import { Document, Filter } from "../model/types";
import { FindOptions, WithId } from "mongodb";

export class QueryBuilder {
  // ...
  /**
   * Count documents based on the given filter
   */
  public async count(
    collectionName: string,
    filter: Filter,
  ): Promise<number> {
    const query = this.query(collectionName);

    return await query.countDocuments(filter);
  }
Войти в полноэкранный режим

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


Удаление

Давайте теперь создадим два метода: один для удаления одного документа, а другой для удаления нескольких документов.

// ...
  /**
   * Delete Single document
   */
  public async deleteOne(
    collectionName: string,
    filter: Filter,
  ): Promise<number> {
    const query = this.query(collectionName);

    const result = await query.deleteOne(filter);

    return result.deletedCount;
  }

  /**
   * Delete multiple documents
   */
  public async delete(collectionName: string, filter: Filter): Promise<number> {
    const query = this.query(collectionName);

    const result = await query.deleteMany(filter);

    return result.deletedCount;
  }
Войти в полноэкранный режим

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


🎨 Заключение

В этой статье мы создали построитель запросов, который поможет нам выполнять операции CRUD в MongoDB в целом, мы также добавили некоторые полезные методы, такие как paginate, count, delete, deleteOne, first, list, replace, upsert, update, create.


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

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


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

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


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

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


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

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

Общие темы

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

Пакеты React Js

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