-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from nxcd/feature/event-repository-class
Refactor repositories to add new EventRepository abstract class
- Loading branch information
Showing
7 changed files
with
178 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,10 +25,11 @@ | |
- [API Summary](#api-summary) | ||
- [EventEntity](#evententity) | ||
- [Repositories](#repositories) | ||
- [MongodbEventRepository](#mongodbeventrepository) | ||
- [Sessions](#sessions) | ||
- [Interfaces](#interfaces) | ||
- [IPaginatedQueryResult](#ipaginatedqueryresult) | ||
- [IEntityConstructor](#ientityconstructor) | ||
- [My repository is not included, what do I do?](#my-repository-is-not-included-what-do-i-do) | ||
- [Adding your repository to the list](#adding-your-repository-to-the-list) | ||
|
||
## Instalation | ||
|
||
|
@@ -279,90 +280,90 @@ Since different databases have different event sourcing implementations, for now | |
|
||
> Note that different repository classes might behave differently depending on who created the class, please refer to the PR section or fill in an issue if you're experiencing troubles. | ||
### MongodbEventRepository | ||
- [MongoDB event-based repository](./docs/MongodbEventRepository.md) | ||
|
||
Data repository made for MongoDB databases. This repository **must** be extended by another class implementing its own methods. The base abstract class must have some properties when instantiated such as: | ||
## Interfaces | ||
|
||
- Must receive the `Collection` object from Mongodb | ||
### IPaginatedQueryResult | ||
|
||
> **Note:** The `collection` **OBJECT**, not the collection **NAME** | ||
Represents a paginated query: | ||
|
||
- Must receive the main entity constructor (not the instance) | ||
```ts | ||
interface IPaginatedQueryResult<TDocument> { // TDocument is the type that represents the data which will be returned from the database (it is used internally) | ||
documents: TDocument[] // Documents in the current page | ||
count: number // Total results in the page | ||
range: { | ||
from: number, // Index of the first result | ||
to: number // Index of the last result | ||
} | ||
total: number // Query total | ||
} | ||
``` | ||
|
||
By default, the class already has some base methods: | ||
### IEntityConstructor | ||
|
||
- `save (entity: TEntity)`: Which will serialize and save the received entity (which must be of the same type you passed to the generic type `TEntity` in `MongodbEventRepository<TEntity>`) on the database. | ||
Represents the constructor of an entity | ||
|
||
> This method works by firstly trying to find the entity by its ID, if the ID cannot be found in the database, then a new document will be created, following the `{_id, events, state}` format where `events` should start as an empty array and, at each `save`, the `pendingEvents` array will be merged to it. Soon after that, the `confirmEvents` method will be called, thus clearing the `pendingEvents` array. | ||
> | ||
> `state` will be the last reduced state of the entity, which will be obtained by calling the `state` getter we just defined earlier. | ||
```ts | ||
interface IEntityConstructor<Entity> { | ||
new(events?: IEvent<any>[]): Entity | ||
} | ||
``` | ||
|
||
- `findById (id: ObjectId)`: Will search in the database for a record with the informed `id`. | ||
## My repository is not included, what do I do? | ||
|
||
> This record should be created when the class is instantiated using the `create` method | ||
Since this lib is open source and generic enough to be used by multiple repositories, there's no way to know which repositories the users are going to be using. So we added a way for you to create your own. | ||
|
||
- `bulkUpdate (entities: IEventEntity[])`: Save events from several instances of an entity at once | ||
- `withSession (session: ClientSession)`: Begins a MongoDB session to initiate a transaction (only on Mongo 4.0) and returns an object with the available methods which can be executed within a session. If this following command throws an error, the whole session suffers a rollback, otherwise it is commited. | ||
- `_runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [key: string]: 1|-1 } = {})`: Executes a query aplying pagination to the result. Returns an object that follows the [IPaginatedQueryResult](#ipaginatedqueryresult) interface. | ||
In order to create a repository, your class **must** extend the `EventRepository` class, which is fully abstract and is as follows: | ||
|
||
#### Sessions | ||
```ts | ||
export interface IEntityConstructor<Entity> { | ||
new(events?: IEvent<any>[]): Entity | ||
} | ||
|
||
If your MongoDB version is 4.0 or higher (with transaction support), in order to execute a command using a transaction, follow this example: | ||
export abstract class EventRepository<TEntity extends IEventEntity> { | ||
|
||
```ts | ||
import { Db, MongoClient } from 'mongodb' | ||
import { MongodbEventRepository } from '@nxcd/paradox' | ||
import { Person } from './classes/Person' | ||
protected readonly _Entity: IEntityConstructor<TEntity> | ||
|
||
class PersonRepository extends MongodbEventRepository<Person> { | ||
constructor(connection: Db) { | ||
super(connection.collection(Person.collection), Person) | ||
constructor (Entity: IEntityConstructor<TEntity>) { | ||
this._Entity = Entity | ||
} | ||
|
||
async search (filters: { name: string }, page: number = 1, size: number = 50) { | ||
const query = filters.name | ||
? { 'state.name': filters.name } | ||
: { } | ||
abstract async save (entity: TEntity): Promise<TEntity> | ||
|
||
const { documents, count, range, total } = await this._runPaginatedQuery(query, page, size) | ||
const entities = documents.map(({ events }) => new Person().setPersistedEvents(events)) | ||
abstract async findById (id: any): Promise<TEntity | null> | ||
|
||
return { entities, count, range, total } | ||
} | ||
abstract async runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 }): Promise<IPaginatedQueryResult<{ events: IEvent<TEntity>[] }>> | ||
} | ||
``` | ||
|
||
(async function () { | ||
const connection = (await MongoClient.connect('mongodb://urldomongodbaqui')).db('crowd') | ||
const personRepository = new PersonRepository(connection) | ||
const johnDoe = Person.create('[email protected]', 'jdoe') | ||
await personRepository.save(johnDoe) // Creates a new event | ||
const allJanes = await personRepository.search({ name: 'jane' }, 1, 10) // Returns an object following IPaginatedQueryResult interface | ||
In order to maintain consistency between implementations, the following methods **must** be implemented: | ||
|
||
johnDoe.changeEmail({ newEmail: '[email protected]' }, 'jdoe') | ||
const [ janeDoe ] = allJanes | ||
janeDoe.changeEmail({ newEmail: '[email protected]' }, 'janedoe') | ||
- `save`: Should save the given entity to the database and return the entity | ||
- `findById`: Should find an entity by its ID in the database. It is important to notice that, once found, the returned value should be a newly created instance of that entity (this is where you're going to use the `setPersistedEvents` method) | ||
- `runPaginatedQuery`: Should return a paginated query from the database | ||
|
||
Besides these methods, any class that extends `EventRepository` will inherit the `_Entity` property, which refers to the entity constructor. This will be used when returning the newly created entity from the database during the `findById` method and seting its persisted events on the newly instantiated class, like so: | ||
|
||
const session = connection.startSession() | ||
await personRepository.withSession(session).bulkUpdate([ johnDoe, janeDoe ]) // Updates both entities using a transaction | ||
})() | ||
```ts | ||
async function findById (id) { | ||
/* finds the data */ | ||
const instance = this._Entity() // Creates a new instance of <Entity> | ||
return instance.setPersistedEvents(yourEvents) // Sets the returned data into the instance | ||
} | ||
``` | ||
|
||
> If you version does **not** support transactions, an Database Error is thrown | ||
Those are the required implementations, any additional functionalities you'd like to include in the repository can be added at will. | ||
|
||
## Interfaces | ||
> For further explanation and examples, refer to the [MongodbEventRepository file in the `src` folder](./docs/MongodbEventRepository.md) | ||
### IPaginatedQueryResult | ||
### Adding your repository to the list | ||
|
||
Represents a paginated query: | ||
If you'd like to add your repository to the list of included repositories, please fill in a PR and don't forget to stick to some conventions: | ||
|
||
```ts | ||
interface IPaginatedQueryResult<TDocument> { // TDocument is the type that represents the data which will be returned from the database (it is used internally) | ||
documents: TDocument[] // Documents in the current page | ||
count: number // Total results in the page | ||
range: { | ||
from: number, // Index of the first result | ||
to: number // Index of the last result | ||
} | ||
total: number // Query total | ||
} | ||
``` | ||
- All names are CamelCase | ||
- Private variables come with an `_` preceding it | ||
- Do **not** forget to add the documentation to this repository in the `docs/` folder (the file should be the same name as your class) | ||
- Do **not** forget to add your repository to the list in this README along with the link to its own docs | ||
|
||
Thank you for your contribution :D |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# MongodbEventRepository | ||
|
||
- [MongodbEventRepository](#mongodbeventrepository) | ||
- [Sessions](#sessions) | ||
|
||
Data repository made for MongoDB databases. This repository **must** be extended by another class implementing its own methods. The base abstract class must have some properties when instantiated such as: | ||
|
||
- Must receive the `Collection` object from Mongodb | ||
|
||
> **Note:** The `collection` **OBJECT**, not the collection **NAME** | ||
- Must receive the main entity constructor (not the instance) | ||
|
||
By default, the class already has some base methods: | ||
|
||
- `save (entity: TEntity)`: Which will serialize and save the received entity (which must be of the same type you passed to the generic type `TEntity` in `MongodbEventRepository<TEntity>`) on the database. | ||
|
||
> This method works by firstly trying to find the entity by its ID, if the ID cannot be found in the database, then a new document will be created, following the `{_id, events, state}` format where `events` should start as an empty array and, at each `save`, the `pendingEvents` array will be merged to it. Soon after that, the `confirmEvents` method will be called, thus clearing the `pendingEvents` array. | ||
> | ||
> `state` will be the last reduced state of the entity, which will be obtained by calling the `state` getter we just defined earlier. | ||
- `findById (id: ObjectId)`: Will search in the database for a record with the informed `id`. | ||
|
||
> This record should be created when the class is instantiated using the `create` method | ||
- `bulkUpdate (entities: IEventEntity[])`: Save events from several instances of an entity at once | ||
- `withSession (session: ClientSession)`: Begins a MongoDB session to initiate a transaction (only on Mongo 4.0) and returns an object with the available methods which can be executed within a session. If this following command throws an error, the whole session suffers a rollback, otherwise it is commited. | ||
- `_runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [key: string]: 1|-1 } = {})`: Executes a query aplying pagination to the result. Returns an object that follows the [IPaginatedQueryResult](#ipaginatedqueryresult) interface. | ||
|
||
> **Note:** `_runPaginatedQuery` should only be used **inside** the child class because it'll return a collection of **documents** and not a collection of **entities**. This harms the principle for entity-first design this repository is all about. In order to create a useful method, please refer to the example below on how to create a `search` method using `_runPaginatedQuery` properly | ||
## Sessions | ||
|
||
If your MongoDB version is 4.0 or higher (with transaction support), in order to execute a command using a transaction, follow this example: | ||
|
||
```ts | ||
import { Db, MongoClient } from 'mongodb' | ||
import { MongodbEventRepository } from '@nxcd/paradox' | ||
import { Person } from './classes/Person' | ||
|
||
class PersonRepository extends MongodbEventRepository<Person> { | ||
constructor(connection: Db) { | ||
super(connection.collection(Person.collection), Person) | ||
} | ||
|
||
async search (filters: { name: string }, page: number = 1, size: number = 50) { | ||
const query = filters.name | ||
? { 'state.name': filters.name } | ||
: { } | ||
|
||
const { documents, count, range, total } = await this._runPaginatedQuery(query, page, size) | ||
const entities = documents.map(({ events }) => new Person().setPersistedEvents(events)) | ||
|
||
return { entities, count, range, total } | ||
} | ||
} | ||
|
||
(async function () { | ||
const connection = (await MongoClient.connect('mongodb://urldomongodbaqui')).db('crowd') | ||
const personRepository = new PersonRepository(connection) | ||
const johnDoe = Person.create('[email protected]', 'jdoe') | ||
await personRepository.save(johnDoe) // Creates a new event | ||
const allJanes = await personRepository.search({ name: 'jane' }, 1, 10) // Returns an object following IPaginatedQueryResult interface | ||
|
||
johnDoe.changeEmail({ newEmail: '[email protected]' }, 'jdoe') | ||
const [ janeDoe ] = allJanes | ||
janeDoe.changeEmail({ newEmail: '[email protected]' }, 'janedoe') | ||
|
||
const session = connection.startSession() | ||
await personRepository.withSession(session).bulkUpdate([ johnDoe, janeDoe ]) // Updates both entities using a transaction | ||
})() | ||
``` | ||
|
||
> If you version does **not** support transactions, an Database Error is thrown |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { IEvent } from '@nxcd/tardis' | ||
import { IEventEntity } from '../../interfaces/IEventEntity' | ||
import { IPaginatedQueryResult } from '../../interfaces/IPaginatedQueryResult' | ||
|
||
export interface IEntityConstructor<Entity> { | ||
new(events?: IEvent<any>[]): Entity | ||
} | ||
|
||
export abstract class EventRepository<TEntity extends IEventEntity> { | ||
|
||
protected readonly _Entity: IEntityConstructor<TEntity> | ||
|
||
constructor (Entity: IEntityConstructor<TEntity>) { | ||
this._Entity = Entity | ||
} | ||
|
||
abstract async save (entity: TEntity): Promise<TEntity> | ||
|
||
abstract async findById (id: any): Promise<TEntity | null> | ||
|
||
abstract async _runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 }): Promise<IPaginatedQueryResult<{ events: IEvent<TEntity>[] }>> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters