Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: documentation and error handling improvements #265

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7107f5e
chore(deps-dev): bump @types/node from 20.12.11 to 20.14.0
dependabot[bot] Jun 3, 2024
e7945ea
chore(deps-dev): bump mongoose from 8.3.4 to 8.4.1
dependabot[bot] Jun 3, 2024
cfe49f9
chore(deps-dev): bump eslint from 9.2.0 to 9.4.0
dependabot[bot] Jun 3, 2024
05f8cb3
chore(deps-dev): bump @typescript-eslint/eslint-plugin
dependabot[bot] Jun 3, 2024
8e371c5
chore(deps-dev): bump @typescript-eslint/parser from 7.8.0 to 7.11.0
dependabot[bot] Jun 3, 2024
554dbb5
Merge pull request #249 from Josuto/dependabot/npm_and_yarn/mongoose-…
Josuto Jun 5, 2024
c024507
Merge pull request #250 from Josuto/dependabot/npm_and_yarn/eslint-9.4.0
Josuto Jun 5, 2024
e00815b
Merge pull request #252 from Josuto/dependabot/npm_and_yarn/typescrip…
Josuto Jun 5, 2024
e613b82
Merge pull request #248 from Josuto/dependabot/npm_and_yarn/types/nod…
Josuto Jun 5, 2024
7caec4e
Merge pull request #251 from Josuto/dependabot/npm_and_yarn/typescrip…
Josuto Jun 5, 2024
73dc6de
chore: update dependencies
Josuto Jun 6, 2024
49ab8cd
chore: update dependencies to latest versions
Josuto Jun 6, 2024
4f39622
Merge pull request #253 from Josuto/update_dependencies
Josuto Jun 6, 2024
465549f
chore: version bump
Josuto Jun 6, 2024
6915d1c
chore: update repository url value
Josuto Jun 6, 2024
8e5be8e
chore: merge branch 'canary'
Josuto Jun 15, 2024
11affb9
docs: update doc with new domain model definition
Josuto Jun 15, 2024
9257c16
docs: add v6 changelog
Josuto Jun 27, 2024
9d05841
fix: include clause to error to improve debugging
Josuto Jul 3, 2024
e7245a9
docs: improved v6 changelog
Josuto Jul 3, 2024
77dfeb6
docs: fixed some CRUD operations docs
Josuto Jul 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 25 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ Or yarn:
yarn add monguito
```

Or pnpm:

```shell
pnpm add monguito
```

## Usage

Creating your repository with custom database operations is very straight forward. Say you want to create a custom
Expand All @@ -61,9 +67,12 @@ and `AudioBook`). Here's the implementation of a custom repository that deals wi
class MongooseBookRepository extends MongooseRepository<Book> {
constructor() {
super({
Default: { type: Book, schema: BookSchema },
PaperBook: { type: PaperBook, schema: PaperBookSchema },
AudioBook: { type: AudioBook, schema: AudioBookSchema },
type: Book,
schema: BookSchema,
subtypes: [
{ type: PaperBook, schema: PaperBookSchema },
{ type: AudioBook, schema: AudioBookSchema },
],
});
}

Expand Down Expand Up @@ -96,16 +105,11 @@ 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. 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](https://mongoosejs.com/docs/guide.html) of such domain object. The `Default` key is mandatory and
relates to the supertype, while the rest of the keys relate to the subtypes.
`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 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.
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

Expand All @@ -125,11 +129,8 @@ interface Repository<T extends Entity> {
id: string,
options?: FindByIdOptions,
) => Promise<Optional<S>>;
findOne: <S extends T>(
filters: any, // Deprecated since v5.0.1, use options.filters instead
options?: FindOneOptions,
) => Promise<Optional<S>>;
findAll: <S extends T>(options?: FindAllOptions) => Promise<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,
Expand Down Expand Up @@ -161,13 +162,13 @@ This value wraps an actual entity or `null` in case that no entity matches the g

### `findOne`

Returns an [`Optional`](https://github.com/bromne/typescript-optional#readme) entity matching the given `filters` parameter value. If no value is provided, then an arbitrary stored (if any) entity is returned. In case there are more than one matching entities, `findOne` returns the first entity satisfying the condition. The result value wraps an actual entity or `null` if no entity matches the given conditions.
Returns an [`Optional`](https://github.com/bromne/typescript-optional#readme) entity matching the value of some given `filters` option property. If no value is provided, then an arbitrary stored (if any) entity is returned. In case there are more than one matching entities, `findOne` returns the first entity satisfying the condition. The result value wraps an actual entity or `null` if no entity matches the given conditions.

### `findAll`

Returns an array including all the persisted entities, or an empty array otherwise.

This operation accepts some optional behavioural options:
This operation accepts some option properties:

- `filters`: a [MongoDB search criteria](https://www.mongodb.com/docs/manual/tutorial/query-documents/) to filter results
- `sortBy`: a [MongoDB sort criteria](https://www.mongodb.com/docs/manual/reference/method/cursor.sort/#mongodb-method-cursor.sort)
Expand All @@ -179,7 +180,7 @@ This operation accepts some optional behavioural options:

Persists the given entity by either inserting or updating it and returns the persisted entity. If the entity specifies an `id` field, this function updates it, unless it does not exist in the pertaining collection, in which case this operation results in an exception being thrown. Otherwise, if the entity does not specify an `id` field, it inserts it into the collection. Beware that trying to persist a new entity that includes a developer specified `id` is considered a _system invariant violation_; only Mongoose is able to produce MongoDB identifiers to prevent `id` collisions and undesired entity updates.

This operation accepts `userId` as an optional behavioural option to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details on this topic).
This operation accepts `userId` as an option property to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details on this topic).

> [!WARNING]
> The version of `save` specified at `MongooseRepository` is not [atomic](#supported-database-operations). If you are to execute it in a concurrent environment, make sure that your custom repository extends `MongooseTransactionalRepository` instead.
Expand All @@ -200,7 +201,7 @@ export interface TransactionalRepository<T extends Entity>
options?: SaveAllOptions,
) => Promise<S[]>;

deleteAll: (options?: DeleteAllOptions) => Promise<number>;
deleteAll: <S extends T>(options?: DeleteAllOptions<S>) => Promise<number>;
}
```

Expand All @@ -211,11 +212,11 @@ export interface TransactionalRepository<T extends Entity>

Persists the given list of entities by either inserting or updating them and returns the list of persisted entities. As with the `save` operation, `saveAll` inserts or updates each entity of the list based on the existence of the `id` field. In the event of any error, this operation rollbacks all its changes. In other words, it does not save any given entity, thus guaranteeing operation atomicity.

This operation accepts `userId` as an optional behavioural option to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details on this topic).
This operation accepts `userId` as an option property to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details on this topic).

### `deleteAll`

Deletes all the entities that match the MongoDB a given search criteria specified as `options.filters` behavioural option and returns the total amount of deleted entities. Beware that if no search criteria is provided, then `deleteAll` deletes all the stored entities. In the event of any error, this operation rollbacks all its changes. In other words, it does not delete any stored entity, thus guaranteeing operation atomicity.
Deletes all the entities matching value of some given `filters` option property and returns the total amount of deleted entities. Beware that if no value is provided for `filters` is provided, then `deleteAll` deletes all the stored entities. In the event of any error, this operation rollbacks all its changes. In other words, it does not delete any stored entity, thus guaranteeing operation atomicity.

# Examples

Expand All @@ -229,7 +230,7 @@ Moreover, if you are interested in knowing how to inject and use a custom reposi
If you are to inject your newly created repository into an application that uses a Node.js-based framework
(e.g., [NestJS](https://nestjs.com/) or [Express](https://expressjs.com/)) then you may want to do some extra effort and
follow the [Dependency Inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) to _depend on
abstractions, not implementations_. Simply need to add one extra artefact to your code:
abstractions, not implementations_. You simply need to add one extra artefact to your code:

```typescript
interface BookRepository extends Repository<Book> {
Expand Down
98 changes: 98 additions & 0 deletions docs/v6-changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Monguito v6 Changelog

Monguito v6 comes with few changes aimed to improve development experience on custom repository implementation. Here is the full list of changes:

## Breaking Changes

- [Introduction of a New Domain Model Type](#introduction-of-a-new-domain-model-type)
- [Unpublished Monguito Types](#unpublished-monguito-types)
- [New Semantics for `findOne` and `deleteAll`](#new-semantics-for-findone-and-deleteall)
- [Changes on CRUD Operations `option` Properties](#changes-on-crud-operations-option-properties)

## Introduction of a New Domain Model Type

### Problem Statement

Prior to v6, custom repository constructors had to declare a map of type `TypeMap` to represent the domain model to be handled by the repository. This map specifies a domain model supertype definition object identified by the `Default` key and (optionally) a definition object for every domain model subtype identified by a key that must match the name of the subtype.

Here is an example for the `Book` domain model specification using `TypeMap`:

```typescript
{
Default: { type: Book, schema: BookSchema },
PaperBook: { type: PaperBook, schema: PaperBookSchema },
AudioBook: { type: AudioBook, schema: AudioBookSchema },
}
```

This domain model map presents two big issues. On the one hand, many custom repository developers are not aware of the subtype key naming constraint or they find it verbose and confusing. On another hand, this map is limited in nature since it cannot capture complex domain models. Consider an scenario where `PaperBook` has two subypes: `BlackAndWhitePaperBook` and `ColorPaperBook`. There is no way to represent this subtype hierarchy using `TypeMap`.

### Solution Definition

We re-designed the domain model type to overcome the aforementioned problems. Also, we renamed `TypeMap` to `DomainModel` as this way it becomes self-explanatory. Here is an alternative example for the `Book` domain model specification using `DomainModel`:

```typescript
{
type: Book,
schema: BookSchema,
subtypes: [
{ type: PaperBook, schema: PaperBookSchema },
{ type: AudioBook, schema: AudioBookSchema },
],
}
```

This declaration is more succinct and understandable. Besides, the new domain model type enables _recursive subtype definitions_, thus allowing the declaration of complex domain models. The following domain model example specifies the aforementined scenario:

```typescript
{
type: Book,
schema: BookSchema,
subtypes: [
{
type: PaperBook,
schema: PaperBookSchema,
subtypes: [
{ type: BlackAndWhitePaperBook, schema: BlackAndWhitePaperBookSchema },
{ type: ColorPaperBook, schema: ColorPaperBookSchema },
]
},
{ type: AudioBook, schema: AudioBookSchema },
],
}
```

This new data structure also enabled us to introduce some further TypeScript constraints to disallow _abstract leaf domain type definitions_. Any leaf domain type must be instantiable. This means that in the previous examples `AudioBook` cannot be declared as an abstract class; doing so would result in a TypeScript error, thus improving development experience. Any other root or intermediate domain type definitions may be declared as concrete or abstract classes.

### Migration Steps

Follow the following easy steps to migrate your pre-v6 domain model map to the new domain model type:

1. Extract the supertype definition object (e.g., `{ type: Book, schema: BookSchema }`) as the main contents of your domain model object
2. Create a `subtypes` array property in your domain model object
3. Include any subtype definition object (e.g., `{ type: PaperBook, schema: PaperBookSchema }`) to the `subtypes` array property of your domain model object
4. Repeat steps 2 and 3 for any nested subtype definition objects where appropriate

## Unpublished Monguito Types

We have simplified Monguito API to ease the maintenance of the library. In particular, we unpublished several types related to the new domain model definition. We expect Monguito developers to follow the [recommended way to define their domain model](#solution-definition) without requiring to specify any of these types. As mentioned earlier, we also renamed `TypeMap` to `DomainModel`.

The list of unpublished types are:

- `AbsConstructor`
- `Constructor`
- `SubtypeData`
- `SubtypeMap`
- `SupertypeData`

## New Semantics for `findOne` and `deleteAll`

Prior to v6, `findOne` specified a `filters` parameter as part of its signature. We decided to move it to `options` in v6, as we did with it in `findAll` and `deleteAll` operations for syntactic consistency purposes. Also, if the value of `filters` is `null` in any of these three operations, then the expected result is that of invoking the operation omitting such an options property e.g., `findAll` returns all existing entities.

## Changes on CRUD Operations `option` Properties

We have introduced the following changes on CRUD operation option properties in v6 for development experience improvement purposes:

- Removed `connection` from `TransactionOptions`, since this property is only required by transactional operations (callback functions of `runInTransaction`) to create a new MongoDB session if none already exists. We made `connection` an optional property for the `options` parameter of `runInTransaction` instead. **This change affects all CRUD operations**.
- Constrained the type of `FindAllOptions.sortBy` to be `string` or `SortOrder` (it used to be `any`).
- Constrained the type of `filters` option property to be Mongoose `FilterQuery` (it used to be `any`). `FindOneOptions`, `FindAllOptions`, and `DeleteAllOptions` are now generic types, as required by `FilterQuery`.
24 changes: 17 additions & 7 deletions examples/nestjs-mongoose-book-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,12 @@ export class MongooseBookRepository
constructor(@InjectConnection() connection: Connection) {
super(
{
Default: { type: Book, schema: BookSchema },
PaperBook: { type: PaperBook, schema: PaperBookSchema },
AudioBook: { type: AudioBook, schema: AudioBookSchema },
type: Book,
schema: BookSchema,
subtypes: [
{ type: PaperBook, schema: PaperBookSchema },
{ type: AudioBook, schema: AudioBookSchema },
],
},
connection,
);
Expand Down Expand Up @@ -180,6 +183,13 @@ export class BookController {
private readonly bookRepository: TransactionalRepository<Book>,
) {}

@Get(':id')
async findById(@Param('id') id: string): Promise<Book> {
return (await this.bookRepository.findById(id)).orElseThrow(
() => new NotFoundException(`Book with ID ${id} not found`),
);
}

@Get()
async findAll(): Promise<Book[]> {
return this.bookRepository.findAll();
Expand All @@ -198,10 +208,10 @@ export class BookController {
@Patch(':id')
async update(
@Param('id') id: string,
@Body() book: PartialBook,
@Body() book: Partial<Book>,
): Promise<Book> {
book.id = id;
return this.save(book);
const bookToUpdate = { ...book, id };
return this.save(bookToUpdate);
}

@Post('/all')
Expand Down Expand Up @@ -232,7 +242,7 @@ export class BookController {
try {
return await this.bookRepository.save(book);
} catch (error) {
throw new BadRequestException(error);
throw new BadRequestException('Bad request', { cause: error });
}
}
}
Expand Down
24 changes: 12 additions & 12 deletions examples/nestjs-mongoose-book-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,37 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.3.8",
"@nestjs/core": "^10.3.8",
"@nestjs/common": "^10.3.9",
"@nestjs/core": "^10.3.9",
"@nestjs/mapped-types": "^2.0.5",
"@nestjs/mongoose": "^10.0.6",
"@nestjs/platform-express": "^10.3.8",
"@nestjs/platform-express": "^10.3.9",
"class-transformer": "^0.5.1",
"mongoose": "^8.3.4",
"mongoose": "^8.4.1",
"monguito": "link:../../",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.3.2",
"@nestjs/schematics": "^10.1.1",
"@nestjs/testing": "^10.3.8",
"@nestjs/testing": "^10.3.9",
"@types/express": "^4.17.21",
"@types/jest": "29.5.12",
"@types/node": "20.12.11",
"@types/node": "20.14.2",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"eslint": "^9.2.0",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^9.4.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-no-only-or-skip-tests": "^2.6.2",
"eslint-plugin-prettier": "^5.1.3",
"jest": "29.7.0",
"mongodb-memory-server": "^9.2.0",
"prettier": "^3.2.5",
"mongodb-memory-server": "^9.3.0",
"prettier": "^3.3.1",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "29.1.2",
"ts-jest": "29.1.4",
"ts-node": "^10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "^5.4.5"
Expand Down
Loading