Creating a Dynamic service for NestJS and mongoose
Tags: nestjs, typescript, mongodb, mongoose
Published: 2023-10-30 13:14:21

Building a big application in NestJs require a lot of pieces of code to be repeated, so to make it DRY, i implemented in my business' a Dynamic service that help me create crud services easily.

This is the CRUD service


```ts
import { Model, Document, FilterQuery } from 'mongoose';
import { WhereDto } from '../dto/where.dto';
import { IResponseDto } from '../dto/response.dto';
import { MongoError } from 'typeorm';
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  InternalServerErrorException,
} from '@nestjs/common';

@Catch(MongoError)
export class CrudService<T extends Document> implements ExceptionFilter {
  catch(exception: typeof MongoError, host: ArgumentsHost) {}

  constructor(private model: Model<T>) {}

  PAGE: number = 0;
  PAGE_SIZE: number = 5;
  WHERE: WhereDto;

  /**
   * Find all records that match the given conditions.
   *
   * @param {WhereDto} where - The conditions to match.
   * @return {Promise<IResponseDto<T>>} A promise that resolves to the response DTO.
   */
  async findAll(where: WhereDto): Promise<IResponseDto<T>> {
    console.log(where);

    this.PAGE = where.page;
    this.WHERE = where;

    delete where.page;

    const objectsCount = await this.model.count(where);
    const pageCount = Math.ceil(objectsCount / this.PAGE_SIZE);

    const data = await this.model
      .find(where)
      .skip((this.PAGE - 1) * this.PAGE_SIZE)
      .limit(this.PAGE_SIZE);

    const pagination = {
      page: this.PAGE,
      pageSize: this.PAGE_SIZE,
      pageCount: pageCount,
      total: objectsCount,
    };

    return {
      data: data,
      meta: {
        pagination: pagination,
      },
    };
  }

  /**
   * Creates a new document in the database.
   *
   * @param {Partial<T>} data - The data to be used to create the document.
   * @return {Promise<T>} The newly created document.
   */
  async create(data: Partial<T>): Promise<T> {
    console.log('in create');

    try {
      const doc = new this.model(data);
      return doc.save();
    } catch (e) {
      throw new InternalServerErrorException(
        'Error occurred while fetching data from MongoDB.',
        e,
      );
    }
  }

  /**
   * Finds a single record by its id.
   *
   * @param {string} id - The id of the record to find.
   * @return {Promise<IResponseDto<T> | null>} A promise that resolves to the found record or null.
   */
  async findOne(id: string): Promise<IResponseDto<T> | null> {
    const result = await this.model.findById(id).exec();
    if (result) {
      // if we want to keep the result as find all as array
      const dataArray: T[] = [result]; // Convert the result to an array
      return {
        data: result,
        msg: 'SUCCESS',
      };
    } else {
      return {
        msg: 'SUCCESS',
      };
    }
  }
  /**
   * Updates a document in the database with the given id and data.
   *
   * @param {string} id - The id of the document to update.
   * @param {Partial<T>} data - The data to update the document with.
   * @return {Promise<T | null>} A promise that resolves to the updated document, or null if not found.
   */
  async update(id: string, data: Partial<T>): Promise<T | null> {
    return this.model.findByIdAndUpdate(id, data, { new: true }).exec();
  }

  /**
   * Removes a document from the model by its ID.
   *
   * @param {string} id - The ID of the document to remove.
   * @return {Promise<T | null>} A promise that resolves to the removed document,
   * or null if no document was found with the given ID.
   */
  async remove(id: string): Promise<T | null> {
    return this.model.findByIdAndRemove(id).exec();
  }
}


```

Lets say that there is a **_country crud_** and i want to implement it in my **CountryService**


```ts
import { Inject, Injectable } from '@nestjs/common';
import { CreateCountryDto } from './dto/create-country.dto';
import { UpdateCountryDto } from './dto/update-country.dto';
// import { Country } from './entities/country.entity';
import { Model } from 'mongoose';
import { CrudService } from 'src/tools/services/crud.service';
import { WhereDto } from 'src/tools/dto/where.dto';
import { IResponseDto } from 'src/tools/dto/response.dto';
import { Country } from './country.interface';

@Injectable()
export class CountryService {
  // constructor(@InjectModel(Country.name) private readonly countryModel: Model<Country>) {}

  constructor(
    @Inject('COUNTRY_MODEL')
    private countryModel: Model<Country>,
  ) {}

  private readonly crudService = new CrudService<Country>(this.countryModel);

  async create(data: Partial<Country>): Promise<Country> {
    return this.crudService.create(data);
  }

  async findOne(id: string): Promise<IResponseDto<Country>> {
    return this.crudService.findOne(id);
  }

  async update(id: string, data: Partial<Country>): Promise<Country | null> {
    return this.crudService.update(id, data);
  }

  async remove(id: string): Promise<Country | null> {
    return this.crudService.remove(id);
  }

  async findAll(where: WhereDto): Promise<IResponseDto<Country>> {
    return this.crudService.findAll(where);
  }
}
```

To make the CRUD service works perfectly and give a consistent results i add those interfaces 

```ts
export interface IResponseDto<T> {
  readonly msg?: String;
  readonly data?: Array<T> | T;
  readonly meta?: IMeta;
}

export interface IMeta {
  pagination?: IPaginationResponse;
}

export interface IPaginationResponse {
  page?: number;
  pageSize?: number;
  pageCount?: number;
  total?: number;
}

```