SkillAgentSearch skills...

Monguito

Lightweight MongoDB Abstract Repository implementation for Node.js apps

Install / Use

/learn @Josuto/Monguito

README

<p align="center"> <img src="monguito.png" width="120" alt="Monguito" /> </p> <p align="center"> Create custom MongoDB repositories fast and easily </p> <div align="center">

Min code size CI NPM Downloads stat PRs Welcome

</div>

Main Contents

What is monguito?

monguito is a lightweight and type-safe MongoDB handling library for Node.js applications that implements both the abstract repository and the polymorphic patterns.

It allows you (dear developer) to define any custom MongoDB repository in a fast, easy, and structured manner, releasing you from having to write all the boilerplate code for basic CRUD operations, and also decoupling your domain layer from the persistence logic. Moreover, despite its small size, it includes several optional features such as seamless audit data handling support.

Last but not least, monguito wraps Mongoose, a very popular and solid MongoDB ODM for Node.js applications. monguito enables you to use any Mongoose feature such as aggregation pipelines or middleware functions. Furthermore, it leverages Mongoose schemas to enable developers focus on their own persistance models, leaving everything else to the library.

Getting Started

Installation

You can install monguito with npm:

npm install monguito

Or yarn:

yarn add monguito

Or pnpm:

pnpm add monguito

Usage

Creating your repository with custom database operations is very straight forward. Say you want to create a custom repository to handle database operations over instances of a Book and any of its subtypes (e.g., PaperBook and AudioBook). Here's the implementation of a custom repository that deals with persistable instances of Book:

class MongooseBookRepository extends MongooseRepository<Book> {
  constructor() {
    super({
      type: Book,
      schema: BookSchema,
      subtypes: [
        { type: PaperBook, schema: PaperBookSchema },
        { type: AudioBook, schema: AudioBookSchema },
      ],
    });
  }

  async findByIsbn<T extends Book>(isbn: string): Promise<Optional<T>> {
    if (!isbn)
      throw new IllegalArgumentException('The given ISBN must be valid');
    return this.entityModel
      .findOne({ isbn: isbn })
      .exec()
      .then((book) =>
        Optional.ofNullable(this.instantiateFrom(book) as unknown as T),
      );
  }
}

That's it! MongooseBookRepository is a custom repository that inherits a series of CRUD operations and adds its own e.g., findByIsbn. It extends MongooseRepository, a generic template that specifies several basic CRUD operations i.e., findById, findOne, findAll, save, and deleteById. Besides, you can use the protected entityModel defined at MongooseRepository to execute any Mongoose operation you wish, as it happens at the definition of findByIsbn.

Here is an example on how to create and use an instance of the custom MongooseBookRepository:

const bookRepository = new MongooseBookRepository();
const books: Book[] = bookRepository.findAll();

No more leaking of the persistence logic into your domain/application logic! 🤩

Polymorphic Domain Model Specification

MongooseBookRepository handles database operations over a polymorphic domain model that defines Book as supertype and PaperBook and AudioBook as subtypes. This means that, while these subtypes may have a different structure from its supertype, MongooseBookRepository can write and read objects of Book, PaperBook, and AudioBook to and from the same collection books. Code complexity to support polymorphic domain models is hidden at MongooseRepository; all is required is that MongooseRepository receives an object describing the domain model.

This object specifies the type and schema of the supertype (Book and BookSchema, respectively, in this case). The schema enables entity object validation on write operations. Regarding type, Monguito requires it to create an internal representation of the domain model. Additionally, when type does not refer to an abstract type it serves as a constructor required to instantiate the domain objects resulting from the execution of the CRUD operations included in Monguito's repositories (i.e., MongooseRepository and MongooseTransactionalRepository) or any custom repository. On another hand, the domain model subtypes (if any) are also encoded in the domain model object. subtypes is an array of objects that specify a type and schema for a domain model subtype, and (possibly) other subtypes. Hence, the domain model object is of a recursive nature, allowing developers to seamlessly represent any kind of domain model, no matter its complexity.

Beware that any leaf domain model type cannot be abstract! Leaf domain model types are susceptible of being instantiated during MongoDB document deserialisation; any abstract leaf domain model type will result in a TypeScript error. That would be the case if PaperBook is declared an abstract class, or if the domain model is composed by only Book and such a class is declared an abstract class.

Supported Database Operations

The library supports two kinds of CRUD operations: basic and transactional. Both kinds specify atomic operations; however, while most of the former are inherently atomic (all but save), the latter require some transactional logic to ensure atomicity. Moreover, basic CRUD operations can be safely executed on a MongoDB standalone instance, but transactional CRUD operations are only atomic when run as part of a larger cluster e.g., a sharded cluster or a replica set. Using a MongoDB cluster in your production environment is, by the way, the official recommendation.

Let's now explore these two kinds of operations in detail.

Basic CRUD Operations

Repository is the generic interface implemented by MongooseRepository. Its definition is as follows:

type PartialEntityWithId<T> = { id: string } & Partial<T>;

interface Repository<T extends Entity> {
  findById: <S extends T>(
    id: string,
    options?: FindByIdOptions,
  ) => Promise<Optional<S>>;
  findOne: <S extends T>(options?: FindOneOptions<S>) => Promise<Optional<S>>;
  findAll: <S extends T>(options?: FindAllOptions<S>) => Promise<S[]>;
  save: <S extends T>(
    entity: S | PartialEntityWithId<S>,
    options?: SaveOptions,
  ) => Promise<S>;
  deleteById: (id: string, options?: DeleteByIdOptions) => Promise<boolean>;
}

T refers to a domain object type that implements Entity (e.g., Book), and S refers to a subtype of such a domain object type (e.g., PaperBook or AudioBook). This way, you can be sure that the resulting values of the CRUD operations are of the type you expect.

[!NOTE] Keep in mind that the current semantics for these operations are those provided at MongooseRepository. If you want any of these operations to behave differently then you must override it at your custom repository implementation.

[!NOTE] All CRUD operations include an options parameter as part of their signature. This parameter specifies some optional configuration options. There are two types of options: behavioural and transactional. The former dictate the behaviour of the operation, thus influencing the operation result, while the later are required to execute the operation within a MongoDB transaction. You may read more about transactional options in the following section. This section focuses on behavioural options only.

findById

Returns an Optional entity matching the given id. This value wraps an actual entity or null in case that no entity matches the given id.

[!NOTE] The Optional type is meant to create awareness about the nullable nature of the operation result on the custom repository clients. This type helps client code developers to easily reason about all possible result types without having to handle slippery null values or exceptions (i.e., the alternatives to Optional), as mentioned by Joshua Bloch in his book Effective Java. Furthermore, the Optional API is quite complete and includes many elegant solutions to handle all use cases. Check it out!

findOne

Returns an Optional entity matching the value of some given filters option property. If no value is provided, then an arbitrary stored (if any) e

View on GitHub
GitHub Stars35
CategoryDevelopment
Updated1mo ago
Forks2

Languages

TypeScript

Security Score

95/100

Audited on Feb 24, 2026

No findings