diff --git a/README.md b/README.md index e9e990c..4e9e10a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ +

+ Monguito +

+ +

+ Easy and fast build of custom MongoDB repositories +

+ +
+ ![Code size](https://img.shields.io/github/languages/code-size/josuto/node-abstract-repository) ![Min code size](https://img.shields.io/bundlephobia/minzip/node-abstract-repository) [![CI](https://github.com/josuto/nestjs-abstract-repository/actions/workflows/pipeline.yml/badge.svg?branch=main)](https://github.com/josuto/node-abstract-repository/actions/workflows/pipeline.yml) @@ -5,46 +15,52 @@ [![Downloads stat](https://img.shields.io/npm/dt/node-abstract-repository)](http://www.npmtrends.com/node-abstract-repository) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) -This is a lightweight and type-safe library that implements an abstract -and [polymorphic](https://www.mongodb.com/developer/products/mongodb/polymorphic-pattern/) -[repository](https://www.martinfowler.com/eaaCatalog/repository.html) -for [Node.js](https://nodejs.org/), currently focused on [MongoDB](https://www.mongodb.com/). 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. +
# Main Contents -- [Installation](#installation) -- [Usage](#usage) +- [What is `monguito` Exactly?](#what-is-monguito-exactly) +- [Getting Started](#getting-started) - [Examples](#examples) - [Write Your Own Repository Interfaces](#write-your-own-repository-interfaces) - [Some Important Implementation Details](#some-important-implementation-details) - [Comparison to other Alternatives](#comparison-to-other-alternatives) -- [Add Support for other Database Technologies](#add-support-for-other-database-technologies) - [Contributors](#contributors) -# Installation +# What is `monguito` Exactly? + +`monguito` is a lightweight and type-safe [MongoDB](https://www.mongodb.com/) handling library for [Node.js](https://nodejs.org/) applications that implements both the generic [repository](https://www.martinfowler.com/eaaCatalog/repository.html) and the [polymorphic](https://www.mongodb.com/developer/products/mongodb/polymorphic-pattern/) patterns. + +It allows developers to define any custom MongoDB repository in a fast, easy, and structured manner, releasing them from having to write basic CRUD operations, while decoupling domain from persistence logic. Despite its small size, it includes several optional features such as seamless audit data handling support. + +Last but not least, `monguito` wraps [Mongoose](https://mongoosejs.com/), a very popular and solid MongoDB ODM for Node.js applications. Furthermore, it leverages Mongoose [schemas](https://mongoosejs.com/docs/guide.html) to enable developers focus on their own persistance models, leaving everything else to the library. + +Check it out! 🚀 -You can install `node-abstract-repository`with npm: +# Getting Started + +## Installation + +You can install `monguito` with npm: ```shell -npm install node-abstract-repository +npm install monguito ``` Or yarn: ```shell -yarn add node-abstract-repository +yarn add monguito ``` -# Usage +## 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`: ```typescript -export class MongooseBookRepository +class MongooseBookRepository extends MongooseRepository { constructor() { super({ @@ -77,7 +93,7 @@ const bookRepository = new MongooseBookRepository(); const books: Book[] = bookRepository.findAll(); ``` -No more leaking of the persistence logic into your domain/application logic! +No more leaking of the persistence logic into your domain/application logic! 🤩 ## Polymorphic Domain Model Specification @@ -113,7 +129,7 @@ follow the [Dependency Inversion principle](https://en.wikipedia.org/wiki/Depend abstractions, not implementations_. To do so, you simply need to add one extra artefact to your code: ```typescript -export interface BookRepository extends Repository { +interface BookRepository extends Repository { findByIsbn: (isbn: string) => Promise>; } ``` @@ -123,7 +139,7 @@ your repository (e.g., Mongoose-based or [MongoDB Node Driver](https://www.mongo -based, Postgres, MySQL, etc.) Then, make your custom repository implement `BookRepository` as follows: ```typescript -export class MongooseBookRepository +class MongooseBookRepository extends MongooseRepository implements BookRepository { @@ -136,7 +152,7 @@ implementation class implement `Repository`, where `T` is your domain model s custom book repository example: ```typescript -export class MongooseBookRepository +class MongooseBookRepository extends MongooseRepository implements Repository { @@ -151,18 +167,20 @@ export class MongooseBookRepository Here is a possible definition for the aforementioned polymorphic book domain model: ```typescript -export class Book implements Entity { +type BookType = { + id?: string; + title: string; + description: string; + isbn: string; +}; + +class Book implements Entity { readonly id?: string; readonly title: string; readonly description: string; readonly isbn: string; - constructor(book: { - id?: string; - title: string; - description: string; - isbn: string; - }) { + constructor(book: BookType) { this.id = book.id; this.title = book.title; this.description = book.description; @@ -170,35 +188,16 @@ export class Book implements Entity { } } -export class PaperBook extends Book { +type PaperBookType = BookType & { edition: number }; + +class PaperBook extends Book { readonly edition: number; - constructor(paperBook: { - id?: string; - title: string; - description: string; - isbn: string; - edition: number; - }) { + constructor(paperBook: PaperBookType) { super(paperBook); this.edition = paperBook.edition; } } - -export class AudioBook extends Book { - readonly hostingPlatforms: string[]; - - constructor(audioBook: { - id?: string; - title: string; - description: string; - isbn: string; - hostingPlatforms: string[]; - }) { - super(audioBook); - this.hostingPlatforms = audioBook.hostingPlatforms; - } -} ``` The one thing that may catch your attention is the interface `Entity` that `Book` implements. Inspired in the _Entity_ @@ -220,10 +219,12 @@ mind that the current semantics for these operations are those provided at `Mong these operations to behave differently then you must override it at your custom repository implementation. ```typescript -export interface Repository { +type PartialEntityWithId = { id: string } & Partial; + +interface Repository { findById: (id: string) => Promise>; findAll: (filters?: any, sortBy?: any) => Promise; - save: (entity: S | ({ id: string } & Partial)) => Promise; + save: (entity: S | PartialEntityWithId, userId?: string) => Promise; deleteById: (id: string) => Promise; } ``` @@ -235,7 +236,8 @@ export interface Repository { 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. + to produce MongoDB identifiers to prevent `id` collisions and undesired entity updates. Finally, this function specifies + an optional `userId` argument to enable user audit data handling (more on that topic later). - `deleteById` deletes an entity matching the given `id` if it exists. When it does, the function returns `true`. Otherwise, it returns `false`. @@ -245,13 +247,13 @@ be sure that the resulting values of the CRUD operations are of the type you exp ## Utilities to Define Your Custom Schemas -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`: +The `extendSchema` function eases the specification of the Mongoose schemas of your domain model and let `moquito` +to handle the required implementation details. This function is specially convenient when defining schemas for +polymorphic data structures. The following example depicts the definition of `BookSchema`, `PaperBookSchema`, +and `AudioBookSchema`: ```typescript -export const BookSchema = extendSchema( +const BookSchema = extendSchema( BaseSchema, { title: {type: String, required: true}, @@ -261,11 +263,11 @@ export const BookSchema = extendSchema( {timestamps: true}, ); -export const PaperBookSchema = extendSchema(BookSchema, { +const PaperBookSchema = extendSchema(BookSchema, { edition: {type: Number, required: true, min: 1}, }); -export const AudioBookSchema = extendSchema(BookSchema, { +const AudioBookSchema = extendSchema(BookSchema, { hostingPlatforms: {type: [{type: String}], required: true}, }); ``` @@ -273,27 +275,81 @@ export const AudioBookSchema = extendSchema(BookSchema, { Make sure that the schema for your supertype domain object extends from `BaseSchema`. It is required by `MongooseRepository` to properly deserialise your domain objects. +## Built-in Audit Data Support + +You can enable `monguito`'s out-of-the-box audit data handling by just making your domain objects implement +the `Auditable` interface. It specifies the data to audit i.e., creation and last update time and (optionally) +user. Any domain object can implement this interface and add audit data as part of its constructor arguments. +This approach is particularly useful for those domain objects that inherit the members of a superclass. Here is +an example of the use of `Auditable`: + +```typescript +type AuditableBookType = BookType & Auditable; + +class AuditableBook implements Entity, Auditable { + readonly id?: string; + readonly title: string; + readonly description: string; + readonly isbn: string; + readonly createdAt?: Date; + readonly createdBy?: string; + readonly updatedAt?: Date; + readonly updatedBy?: string; + + constructor(book: AuditableBookType) { + this.id = book.id; + this.title = book.title; + this.description = book.description; + this.isbn = book.isbn; + this.createdAt = book.createdAt; + this.createdBy = book.createdBy; + this.updatedAt = book.updatedAt; + this.updatedBy = book.updatedBy; + } +} +``` + +If you would rather avoid all this boilerplate and you are not planning to make your domain object extend from any other +class, you can make it inherit from `AuditableClass`. This is an abstract class included in `moquito` that implements +`Auditable` and both declares and instantiates all the audit data for you. You may then use `AuditableClass` as follows: + +```typescript +type AuditableBookType = BookType & Auditable; + +class AuditableBook extends AuditableClass implements Entity { + readonly id?: string; + readonly title: string; + readonly description: string; + readonly isbn: string; + + constructor(book: AuditableBookType) { + super(book); + this.id = book.id; + this.title = book.title; + this.description = book.description; + this.isbn = book.isbn; + } +} +``` + +`monguito` will produce and save the audit data for any domain object implementing `Auditable` or extending +`AuditableClass` that is to be stored in MongoDB invoking the repository `save` operation. The user audit data is +optional; if you want `monguito` to handle it for you, simply invoke `save` with a value for the `userId` input +parameter. + # Comparison to other Alternatives -First and foremost, this approach is simpler and more lightweight than other existing database integration +First and foremost, `monguito` is simpler and more lightweight than other existing database integration alternatives (e.g., [TypeORM](https://typeorm.io/) or [Typegoose](https://typegoose.github.io/typegoose/)). -Additionally, TypeORM has mainly been developed for relational databases -and [presents several limitations compared to Mongoose](https://eliezer.medium.com/typeorm-mongodb-review-8855903228b1). +Additionally, TypeORM has mainly been developed for relational databases and +[presents several limitations compared to Mongoose](https://eliezer.medium.com/typeorm-mongodb-review-8855903228b1). Typegoose, on another hand, is yet another Mongoose wrapper that provides TypeScript typing to Mongoose schemas and models, but it implements the [Data Mapper](https://martinfowler.com/eaaCatalog/dataMapper.html) 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](https://www.bezkoder.com/mongoose-one-to-many-relationship/)). - -# Add Support for other Database Technologies +the Repository pattern, which in complex domain model scenarios results in query logic duplication. Moreover, +`monguito` is also type-safe. -Extending the repository to provide an implementation -for [MongoDB Node Driver](https://www.mongodb.com/docs/drivers/node/current/) 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. +Considering that Mongoose is currently the most mature MongoDB handling utility, we decided to keep it as `monguito`'s +foundation. # Project Validation @@ -317,8 +373,6 @@ and [Aral Roca](https://github.com/aralroca) for all the insightful conversation Author - [Josu Martinez](https://es.linkedin.com/in/josumartinez) -[![Twitter](https://img.shields.io/twitter/follow/elkartech?style=social)](https://twitter.com/elkartech) - # License This project is [MIT licensed](LICENSE). diff --git a/examples/nestjs-mongoose-book-manager/README.md b/examples/nestjs-mongoose-book-manager/README.md index 8437aaf..0149f73 100644 --- a/examples/nestjs-mongoose-book-manager/README.md +++ b/examples/nestjs-mongoose-book-manager/README.md @@ -1,13 +1,12 @@ -This is an example of how to use the `node-abstract-repository` library in a NestJS application that uses MongoDB. It -is a dummy book manager that exposes three simple endpoints i.e., create, update, and delete a book, as well as list all +This is an example of how to use `monguito` in a NestJS application that uses MongoDB. It is a dummy book +manager that exposes three simple endpoints i.e., create, update, and delete a book, as well as list all books. A book may be of type `Book` or any of its subtypes i.e., `PaperBook` and `AudioBook`. > **Warning** > > Some basic knowledge on [NestJS](https://docs.nestjs.com/) is assumed, as well as that you have read the main -> documentation of [node-abstract-repository](../../README.md). The goal of this documentation is not to provide a -> comprehensive guide on the `node-abstract-repository` library usage. Thus, you may want to check -> the [sample application code](./src) as you go reading. +> documentation of [monguito](../../README.md). The goal of this documentation is not to provide a comprehensive +guide on `monguito` usage. Thus, you may want to check the [sample application code](./src) as you go reading. # Main Contents @@ -222,7 +221,7 @@ e.g., [Domain-Driven Design (tactical design)](https://enterprisecraftsmanship.c I have decided not to do so for simplicity purposes; the book manager presented here is such an extremely simple CRUD application that introducing services would be over-engineering. I rather implement the minimum amount of code necessary for the sake of maximising the actual purpose of this documentation: illustrate how to integrate -the `node-abstract-repository` library on a NodeJS-based enterprise application. +`monguito` on a NodeJS-based enterprise application. Moreover, you would probably not write a `deserialise` function to enable the transformation of JSON request bodies into domain objects when dealing with `POST` requests. Instead, you would rather use diff --git a/examples/nestjs-mongoose-book-manager/package.json b/examples/nestjs-mongoose-book-manager/package.json index 6e24722..fa2ca4b 100644 --- a/examples/nestjs-mongoose-book-manager/package.json +++ b/examples/nestjs-mongoose-book-manager/package.json @@ -1,7 +1,7 @@ { "name": "nestjs-mongoose-book-manager", "version": "0.2.0", - "description": "Sample NestJS application that uses the node-abstract-repository library.", + "description": "Sample NestJS application that uses monguito.", "author": "Josu Martinez", "private": true, "license": "MIT", @@ -29,7 +29,7 @@ "@nestjs/platform-express": "^9.0.0", "class-transformer": "^0.5.1", "mongoose": "^7.5.0", - "node-abstract-repository": "link:../../", + "monguito": "link:../../", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0", "typescript-optional": "^3.0.0-alpha.3" diff --git a/examples/nestjs-mongoose-book-manager/src/book.repository.ts b/examples/nestjs-mongoose-book-manager/src/book.repository.ts index e896b6f..e596a40 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.repository.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.repository.ts @@ -2,7 +2,7 @@ import { IllegalArgumentException, MongooseRepository, Repository, -} from 'node-abstract-repository'; +} from 'monguito'; import { AudioBook, Book, PaperBook } from './book'; import { AudioBookSchema, BookSchema, PaperBookSchema } from './book.schemas'; import { Injectable } from '@nestjs/common'; diff --git a/examples/nestjs-mongoose-book-manager/src/book.schemas.ts b/examples/nestjs-mongoose-book-manager/src/book.schemas.ts index 7ad4619..ec8602e 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.schemas.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.schemas.ts @@ -1,4 +1,4 @@ -import { BaseSchema, extendSchema } from 'node-abstract-repository'; +import { BaseSchema, extendSchema } from 'monguito'; export const BookSchema = extendSchema( BaseSchema, diff --git a/examples/nestjs-mongoose-book-manager/src/book.ts b/examples/nestjs-mongoose-book-manager/src/book.ts index 8f16e31..f227ca5 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.ts @@ -1,4 +1,4 @@ -import { Entity } from 'node-abstract-repository'; +import { Entity } from 'monguito'; export class Book implements Entity { readonly id?: string; diff --git a/examples/nestjs-mongoose-book-manager/yarn.lock b/examples/nestjs-mongoose-book-manager/yarn.lock index 2be0b4e..de4d0f3 100644 --- a/examples/nestjs-mongoose-book-manager/yarn.lock +++ b/examples/nestjs-mongoose-book-manager/yarn.lock @@ -4659,6 +4659,10 @@ mongoose@^7.1.1, mongoose@^7.5.0: ms "2.1.3" sift "16.0.1" +"monguito@link:../..": + version "0.0.0" + uid "" + mpath@0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.9.0.tgz#0c122fe107846e31fc58c75b09c35514b3871904" @@ -4736,12 +4740,6 @@ node-abort-controller@^3.0.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== -"node-abstract-repository@link:../..": - version "2.7.0" - dependencies: - mongoose "^7.1.1" - typescript-optional "^3.0.0-alpha.3" - node-emoji@1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" diff --git a/monguito.code-workspace b/monguito.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/monguito.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/monguito.png b/monguito.png new file mode 100644 index 0000000..3312411 Binary files /dev/null and b/monguito.png differ diff --git a/package.json b/package.json index 23917ff..048ca08 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "node-abstract-repository", + "name": "monguito", "version": "2.8.0", "description": "Node.js lightweight abstract and polymorphic MongoDB repository implementation.", "author": { @@ -9,7 +9,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/josuto/node-abstract-repository.git" + "url": "https://github.com/josuto/monguito.git" }, "keywords": [ "node",