Gathering detailed insights and metrics for node-abstract-repository
Gathering detailed insights and metrics for node-abstract-repository
Gathering detailed insights and metrics for node-abstract-repository
Gathering detailed insights and metrics for node-abstract-repository
monguito
MongoDB Abstract Repository implementation for Node.js
@teamteanpm2024/in-enim-deserunt
Original repository: <https://github.com/falsandtru/spica>
@crabas0npm/excepturi-molestiae-aliquam
> forked from [@crabas0npm/excepturi-molestiae-aliquamode](https://www.npmjs.com/package/@crabas0npm/excepturi-molestiae-aliquamode) v11.1.0. as the original repository seems [no longer maintained](https://github.com/mysticatea/@crabas0npm/excepturi-moles
@micromint1npm/maxime-expedita-tempora
> forked from [@micromint1npm/maxime-expedita-temporaode](https://www.npmjs.com/package/@micromint1npm/maxime-expedita-temporaode) v11.1.0. as the original repository seems [no longer maintained](https://github.com/mysticatea/@micromint1npm/maxime-expedit
Lightweight MongoDB Abstract Repository implementation for Node.js apps
npm install node-abstract-repository
Typescript
Module System
Node Version
NPM Version
TypeScript (99.19%)
JavaScript (0.72%)
Shell (0.08%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
35 Stars
677 Commits
2 Forks
18 Branches
1 Contributors
Updated on Apr 21, 2025
Latest Version
2.8.0
Package Id
node-abstract-repository@2.8.0
Unpacked Size
80.83 kB
Size
15.14 kB
File Count
17
NPM Version
9.8.1
Node Version
16.16.0
Published on
Sep 26, 2023
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
2
2
This is a lightweight and type-safe library that implements an abstract and polymorphic repository for Node.js, currently focused on MongoDB. It has been designed to help developers focus on defining any custom repository in a fast, easy, and structured manner, releasing them from having to write basic CRUD operations, while decoupling domain from persistence logic.
You can install node-abstract-repository
with npm:
1npm install node-abstract-repository
Or yarn:
1yarn add node-abstract-repository
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
:
1export class MongooseBookRepository 2 extends MongooseRepository<Book> { 3 constructor() { 4 super({ 5 Default: {type: Book, schema: BookSchema}, 6 PaperBook: {type: PaperBook, schema: PaperBookSchema}, 7 AudioBook: {type: AudioBook, schema: AudioBookSchema}, 8 }); 9 } 10 11 async findByIsbn<T extends Book>(isbn: string): Promise<Optional<T>> { 12 if (!isbn) 13 throw new IllegalArgumentException('The given ISBN must be valid'); 14 return this.entityModel 15 .findOne({isbn: isbn}) 16 .exec() 17 .then((book) => Optional.ofNullable(this.instantiateFrom(book) as unknown as T)); 18 } 19}
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
e.g., findById
, 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
:
1const bookRepository = new MongooseBookRepository(); 2const books: Book[] = bookRepository.findAll();
No more leaking of the persistence logic into your domain/application logic!
MongooseBookRepository
handles database operations over a polymorphic domain model that defines Book
as supertype
and PaperBook
and AudioBook
as subtypes. Code complexity to support polymorphic domain models is hidden
at MongooseRepository
; all that is required is that MongooseRepository
receives a map describing the domain model.
Each map entry key relates to a domain object type, and the related entry value is a reference to the constructor and
the database schema of such domain object. The Default
key is mandatory and
relates to the supertype, while the rest of the keys relate to the subtypes. Beware that subtype keys are named after
the type name. If it so happens that you do not have any subtype in your domain model, no problem! Just specify the
domain object that your custom repository is to handle as the sole map key-value, and you are done.
Regarding schemas: I believe that writing your own database schemas is a good practice, as opposed of using decorators at your domain model. This is mainly to avoid marrying the underlying infrastructure, thus enabling you to easily get rid of this repository logic if something better comes in. It also allows you to have more control on the persistence properties of your domain objects. After all, database definition is a thing that Mongoose is really rock-solid about.
You may find an example of how to instantiate and use a repository that performs CRUD operations over instances
of Book
and its aforementioned subtypes under book.repository.test.ts
. This is a
complete set of unit test cases used to validate this project.
Moreover, if you are interested in knowing how to inject and use a custom repository in a NestJS application, visit
nestjs-mongoose-book-manager
. But before jumping to that link, I would
recommend that you read the following section.
If you are to inject your newly created repository into an application that uses a Node.js-based framework (e.g., NestJS or Express) then you may want to do some extra effort and follow the Dependency Inversion principle and depend on abstractions, not implementations. To do so, you simply need to add one extra artefact to your code:
1export interface BookRepository extends Repository<Book> { 2 findByIsbn: <T extends Book>(isbn: string) => Promise<Optional<T>>; 3}
This interface allows you to create instances of BookRepository
, and seamlessly switch between implementations for
your repository (e.g., Mongoose-based or MongoDB Node Driver
-based, Postgres, MySQL, etc.) Then, make your custom repository implement BookRepository
as follows:
1export class MongooseBookRepository 2 extends MongooseRepository<Book> 3 implements BookRepository { 4 5 // The rest of the code remains the same as before 6}
If you are not willing to add any new operation at your custom repository, then you could make your repository
implementation class implement Repository<T>
, where T
is your domain model supertype. Here is an alternative for the
custom book repository example:
1export class MongooseBookRepository 2 extends MongooseRepository<Book> 3 implements Repository<Book> { 4 5 // Simply add a constructor setting your domain model map as before 6}
Here is a possible definition for the aforementioned polymorphic book domain model:
1export class Book implements Entity { 2 readonly id?: string; 3 readonly title: string; 4 readonly description: string; 5 readonly isbn: string; 6 7 constructor(book: { 8 id?: string; 9 title: string; 10 description: string; 11 isbn: string; 12 }) { 13 this.id = book.id; 14 this.title = book.title; 15 this.description = book.description; 16 this.isbn = book.isbn; 17 } 18} 19 20export class PaperBook extends Book { 21 readonly edition: number; 22 23 constructor(paperBook: { 24 id?: string; 25 title: string; 26 description: string; 27 isbn: string; 28 edition: number; 29 }) { 30 super(paperBook); 31 this.edition = paperBook.edition; 32 } 33} 34 35export class AudioBook extends Book { 36 readonly hostingPlatforms: string[]; 37 38 constructor(audioBook: { 39 id?: string; 40 title: string; 41 description: string; 42 isbn: string; 43 hostingPlatforms: string[]; 44 }) { 45 super(audioBook); 46 this.hostingPlatforms = audioBook.hostingPlatforms; 47 } 48}
The one thing that may catch your attention is the interface Entity
that Book
implements. Inspired in the Entity
concept from Domain-Driven Design, Entity
models any persistable
domain object type. The interface defines an optional id
field that all persistable domain object types must define.
The optional nature of the field is due to the fact that its value is internally set by Mongoose. Thus, its value can
safely be undefined
until the pertaining domain object instance is inserted (i.e., stored for the first time) in the
database.
The fact that Entity
is an interface instead of an abstract class is not a coincidence; JavaScript is a single
inheritance-based programming language, and I strongly believe that you are entitled to design the domain model at your
will, with no dependencies to other libraries. But all that being said, you may decide not to use it at all, and that
would be just fine. All you need to do is ensure that your domain objects specify an optional id
field.
To explain these, let's have a look to Repository
, the generic interface that MongooseRepository
implements. 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.
1export interface Repository<T extends Entity> { 2 findById: <S extends T>(id: string) => Promise<Optional<S>>; 3 findAll: <S extends T>(filters?: any, sortBy?: any) => Promise<S[]>; 4 save: <S extends T>(entity: S | ({ id: string } & Partial<S>)) => Promise<S>; 5 deleteById: (id: string) => Promise<boolean>; 6}
findById
returns an Optional
value of the searched entity.findAll
returns an array including all the persisted entities, or an empty array otherwise. Although not mandatory,
it accepts both filtering and sorting parameters.save
persists a given entity by either inserting or updating it and returns the persisted entity. It the entity does
not specify an id
, this function inserts the entity. Otherwise, this function expects the entity to exist in the
collection; if it does, the function updates it. Otherwise, throws an exception. This is because trying to persist a
new entity that includes a developer specified id
represents a system invariant violation; only Mongoose is able
to produce MongoDB identifiers to prevent id
collisions and undesired entity updates.deleteById
deletes an entity matching the given id
if it exists. When it does, the function returns true
.
Otherwise, it returns false
.A final note on the definition of Repository
: 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.
This project includes a couple of utilities to ease the specification of custom domain object-related Mongoose schemas.
The extendSchema
function enables you to create schemas for subtype domain objects that inherit from the supertype
domain object schema. This is specially convenient when defining schemas for polymorphic data structures. The following
example depicts the definition of BookSchema
, PaperBookSchema
, and AudioBookSchema
:
1export const BookSchema = extendSchema( 2 BaseSchema, 3 { 4 title: {type: String, required: true}, 5 description: {type: String, required: false}, 6 isbn: {type: String, required: true, unique: true}, 7 }, 8 {timestamps: true}, 9); 10 11export const PaperBookSchema = extendSchema(BookSchema, { 12 edition: {type: Number, required: true, min: 1}, 13}); 14 15export const AudioBookSchema = extendSchema(BookSchema, { 16 hostingPlatforms: {type: [{type: String}], required: true}, 17});
Make sure that the schema for your supertype domain object extends from BaseSchema
. It is required
by MongooseRepository
to properly deserialise your domain objects.
First and foremost, this approach is simpler and more lightweight than other existing database integration alternatives (e.g., TypeORM or Typegoose). Additionally, TypeORM has mainly been developed for relational databases and presents several limitations compared to Mongoose. Typegoose, on another hand, is yet another Mongoose wrapper that provides TypeScript typing to Mongoose schemas and models, but it implements the Data Mapper pattern instead of the Repository pattern. Moreover, this approach is also type-safe. Although it could be interesting to base the abstract repository on Typegoose in the future, it would add a new abstraction layer, thus complicating the current solution both in logic and size. Considering that Mongoose is currently the most mature MongoDB handling utility, it might be a better idea to leveraging the abstract repository with other Mongoose features ( e.g., implementing various types of relationships between documents belonging to different collections).
Extending the repository to provide an implementation
for MongoDB Node Driver or even for another database technology
such as MySQL or PostgreSQL is easy. All you need to do is first create an abstract template for the required database
technology, make it implement the Repository
interface, and then add all the logic required for each of its methods.
The application runs an in-memory instance of MongoDB. Its implementation is provided by
the mongodb-memory-server
NPM dependency.
1# run integration tests 2$ yarn install & yarn test 3 4# run integration tests with coverage 5$ yarn install & yarn test:cov
Thanks to Alexander Peiker, Sergi Torres, and Aral Roca for all the insightful conversations on this topic.
Author - Josu Martinez
This project is MIT licensed.
No vulnerabilities found.
No security vulnerabilities found.