From 0f1586fdc7f35bc26686fdc21794487d1ea9114b Mon Sep 17 00:00:00 2001 From: Lucas Santos Date: Mon, 26 Nov 2018 20:32:29 -0200 Subject: [PATCH 1/6] Refactor repositories to add new EventRepository abstract class --- README.md | 59 +++++++++++++++++++ src/classes/repositories/EventRepository.ts | 22 +++++++ .../repositories/MongodbEventRepository.ts | 28 +++++---- src/index.ts | 2 +- src/interfaces/IEventRepository.ts | 4 -- tsconfig.json | 4 +- 6 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 src/classes/repositories/EventRepository.ts delete mode 100644 src/interfaces/IEventRepository.ts diff --git a/README.md b/README.md index f380188..4791f58 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ - [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) ## Instalation @@ -365,3 +367,60 @@ interface IPaginatedQueryResult { // TDocument is the type that repre total: number // Query total } ``` + +### IEntityConstructor + +Represents the constructor of an entity + +```ts +interface IEntityConstructor { + new(events?: IEvent[]): Entity +} +``` + +## My repository is not included, what do I do? + +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. + +In order to create a repository, your class **must** extend the `EventRepository` class, which is fully abstract and is as follows: + +```ts +export interface IEntityConstructor { + new(events?: IEvent[]): Entity +} + +export abstract class EventRepository { + + protected readonly _Entity: IEntityConstructor + + constructor (Entity: IEntityConstructor) { + this._Entity = Entity + } + + abstract async save (entity: TEntity): Promise + + abstract async findById (id: any): Promise + + abstract async runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 }): Promise[] }>> +} +``` + +In order to maintain consistency between implementations, the following methods **must** be implemented: + +- `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: + +```ts +async function findById (id) { + /* finds the data */ + const instance = this._Entity() // Creates a new instance of + return instance.setPersistedEvents(yourEvents) // Sets the returned data into the instance +} +``` + +Those are the required implementations, any additional functionalities you'd like to include in the repository can be added at will. + +> For further explanation and examples, refer to the [MongodbEventRepository file in the `src` folder](./src/classes/repositories/MongodbEventRepository.ts) \ No newline at end of file diff --git a/src/classes/repositories/EventRepository.ts b/src/classes/repositories/EventRepository.ts new file mode 100644 index 0000000..dc0a997 --- /dev/null +++ b/src/classes/repositories/EventRepository.ts @@ -0,0 +1,22 @@ +import { IEvent } from '@nxcd/tardis' +import { IEventEntity } from '../../interfaces/IEventEntity' +import { IPaginatedQueryResult } from '../../interfaces/IPaginatedQueryResult' + +export interface IEntityConstructor { + new(events?: IEvent[]): Entity +} + +export abstract class EventRepository { + + protected readonly _Entity: IEntityConstructor + + constructor (Entity: IEntityConstructor) { + this._Entity = Entity + } + + abstract async save (entity: TEntity): Promise + + abstract async findById (id: any): Promise + + abstract async runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 }): Promise[] }>> +} \ No newline at end of file diff --git a/src/classes/repositories/MongodbEventRepository.ts b/src/classes/repositories/MongodbEventRepository.ts index 927de8e..3397b4c 100644 --- a/src/classes/repositories/MongodbEventRepository.ts +++ b/src/classes/repositories/MongodbEventRepository.ts @@ -1,7 +1,7 @@ import { IEvent } from '@nxcd/tardis' -import { Collection, ObjectId, ClientSession } from 'mongodb' +import { EventRepository, IEntityConstructor } from './EventRepository' import { IEventEntity } from '../../interfaces/IEventEntity' -import { IEventRepository } from '../../interfaces/IEventRepository' +import { Collection, ObjectId, ClientSession } from 'mongodb' import { IPaginatedQueryResult } from '../../interfaces/IPaginatedQueryResult' interface IDatabaseDocument { @@ -9,19 +9,20 @@ interface IDatabaseDocument { events: IEvent[] } -interface Constructor { - new(events?: IEvent[]): Entity -} - -export abstract class MongodbEventRepository implements IEventRepository { +export abstract class MongodbEventRepository extends EventRepository { protected _collection: Collection - private _Entity: Constructor - constructor (collection: Collection, Entity: Constructor) { + constructor (collection: Collection, Entity: IEntityConstructor) { + super(Entity) this._collection = collection - this._Entity = Entity } + /** + * Tries to execute function using given session + * @param {Function} fn Function to be executed + * @param {ClientSession} session MongoDB user session + * @returns {*} Function result + */ protected async _tryRunningWithSession (fn: Function, session: ClientSession) { session.startTransaction() try { @@ -76,7 +77,7 @@ export abstract class MongodbEventRepository imple * @param {{[field: string]: 1|-1}} sort Fields to sort * @returns {Promise} Set of results */ - protected async _runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 } = {}): Promise[] }>> { + async runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 } = {}): Promise[] }>> { const skip = (Number(page) - 1) * Number(size) const limit = Number(size) @@ -136,6 +137,11 @@ export abstract class MongodbEventRepository imple return new this._Entity().setPersistedEvents(document.events) } + /** + * Executes the following command using a MongoDB session + * @param {ClientSession} session MongoDB client session + * @returns {{bulkUpdate: Function}} Available commands + */ withSession (session: ClientSession) { return { bulkUpdate: (entities: IEventEntity[]) => this._tryRunningWithSession(() => this.bulkUpdate(entities, session), session) diff --git a/src/index.ts b/src/index.ts index 9a08c0f..7788cb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ export { EventEntity } from './classes/EventEntity' export { IEventEntity } from './interfaces/IEventEntity' -export { IEventRepository } from './interfaces/IEventRepository' +export * from './classes/repositories/EventRepository' export { IPaginatedQueryResult } from './interfaces/IPaginatedQueryResult' export { MongodbEventRepository } from './classes/repositories/MongodbEventRepository' diff --git a/src/interfaces/IEventRepository.ts b/src/interfaces/IEventRepository.ts deleted file mode 100644 index 237b3d5..0000000 --- a/src/interfaces/IEventRepository.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IEventRepository { - save (entity: Entity): Promise - findById (id: any): Promise -} diff --git a/tsconfig.json b/tsconfig.json index c1e4885..874fdca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,7 +42,7 @@ /* Additional Checks */ "noUnusedLocals": true, /* Report errors on unused locals. */ - "noUnusedParameters": true, + "noUnusedParameters": false, /* Report errors on unused parameters. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ @@ -73,4 +73,4 @@ "exclude": [ "./examples" ] -} +} \ No newline at end of file From 7c591285c287ba883c6759d4450f58a74fe9eb1f Mon Sep 17 00:00:00 2001 From: Lucas Santos Date: Mon, 26 Nov 2018 20:40:00 -0200 Subject: [PATCH 2/6] Split docs --- .../docs/MongodbEventRepository.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/classes/repositories/docs/MongodbEventRepository.md diff --git a/src/classes/repositories/docs/MongodbEventRepository.md b/src/classes/repositories/docs/MongodbEventRepository.md new file mode 100644 index 0000000..df0577f --- /dev/null +++ b/src/classes/repositories/docs/MongodbEventRepository.md @@ -0,0 +1,72 @@ +# 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`) 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. + +## 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 { + 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('johndoe@doe.com', '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: 'johndoe@company.com' }, 'jdoe') + const [ janeDoe ] = allJanes + janeDoe.changeEmail({ newEmail: 'janedoe@doe.com' }, '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 \ No newline at end of file From 364e19433874720e2a132c762db7126a53448854 Mon Sep 17 00:00:00 2001 From: Lucas Santos Date: Mon, 26 Nov 2018 20:40:07 -0200 Subject: [PATCH 3/6] Add docs for creating new repository --- README.md | 86 +++++++++---------------------------------------------- 1 file changed, 14 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 4791f58..381b26c 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,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 @@ -280,75 +279,7 @@ 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 - -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`) 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. - -#### 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 { - 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('johndoe@doe.com', '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: 'johndoe@company.com' }, 'jdoe') - const [ janeDoe ] = allJanes - janeDoe.changeEmail({ newEmail: 'janedoe@doe.com' }, '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 +- [MongoDB event-based repository](./src/classes/repositories/docs/MongodbEventRepository.md) ## Interfaces @@ -423,4 +354,15 @@ async function findById (id) { Those are the required implementations, any additional functionalities you'd like to include in the repository can be added at will. -> For further explanation and examples, refer to the [MongodbEventRepository file in the `src` folder](./src/classes/repositories/MongodbEventRepository.ts) \ No newline at end of file +> For further explanation and examples, refer to the [MongodbEventRepository file in the `src` folder](./src/classes/repositories/MongodbEventRepository.ts) + +### Adding your repository to the list + +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: + +- All names are CamelCase +- Private variables come with an `_` preceding it +- Do **not** forget to add the documentation to this repository in the `src/classes/repositories/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 \ No newline at end of file From 4e8e9eb1be769d55d391123290f9d7654692c1d7 Mon Sep 17 00:00:00 2001 From: Lucas Santos Date: Tue, 27 Nov 2018 10:56:05 -0200 Subject: [PATCH 4/6] Update docs location --- README.md | 6 +++--- .../repositories/docs => docs}/MongodbEventRepository.md | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename {src/classes/repositories/docs => docs}/MongodbEventRepository.md (100%) diff --git a/README.md b/README.md index 381b26c..4dfbcff 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ 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. -- [MongoDB event-based repository](./src/classes/repositories/docs/MongodbEventRepository.md) +- [MongoDB event-based repository](./docs/MongodbEventRepository.md) ## Interfaces @@ -354,7 +354,7 @@ async function findById (id) { Those are the required implementations, any additional functionalities you'd like to include in the repository can be added at will. -> For further explanation and examples, refer to the [MongodbEventRepository file in the `src` folder](./src/classes/repositories/MongodbEventRepository.ts) +> For further explanation and examples, refer to the [MongodbEventRepository file in the `src` folder](./docs/MongodbEventRepository.md) ### Adding your repository to the list @@ -362,7 +362,7 @@ If you'd like to add your repository to the list of included repositories, pleas - All names are CamelCase - Private variables come with an `_` preceding it -- Do **not** forget to add the documentation to this repository in the `src/classes/repositories/docs/` folder (the file should be the same name as your class) +- 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 \ No newline at end of file diff --git a/src/classes/repositories/docs/MongodbEventRepository.md b/docs/MongodbEventRepository.md similarity index 100% rename from src/classes/repositories/docs/MongodbEventRepository.md rename to docs/MongodbEventRepository.md From ee1aeac011341aeb40903aa0d7004fc7ee23906d Mon Sep 17 00:00:00 2001 From: Lucas Santos Date: Tue, 27 Nov 2018 14:55:42 -0200 Subject: [PATCH 5/6] Make method with _ so it'll not be used outside of the class --- src/classes/repositories/EventRepository.ts | 2 +- src/classes/repositories/MongodbEventRepository.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/classes/repositories/EventRepository.ts b/src/classes/repositories/EventRepository.ts index dc0a997..06a90e0 100644 --- a/src/classes/repositories/EventRepository.ts +++ b/src/classes/repositories/EventRepository.ts @@ -18,5 +18,5 @@ export abstract class EventRepository { abstract async findById (id: any): Promise - abstract async runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 }): Promise[] }>> + abstract async _runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 }): Promise[] }>> } \ No newline at end of file diff --git a/src/classes/repositories/MongodbEventRepository.ts b/src/classes/repositories/MongodbEventRepository.ts index 3397b4c..104e396 100644 --- a/src/classes/repositories/MongodbEventRepository.ts +++ b/src/classes/repositories/MongodbEventRepository.ts @@ -77,7 +77,7 @@ export abstract class MongodbEventRepository exten * @param {{[field: string]: 1|-1}} sort Fields to sort * @returns {Promise} Set of results */ - async runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 } = {}): Promise[] }>> { + async _runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 } = {}): Promise[] }>> { const skip = (Number(page) - 1) * Number(size) const limit = Number(size) From c430fa1137d333346ca2a6a5059fc181f39254ff Mon Sep 17 00:00:00 2001 From: Lucas Santos Date: Tue, 27 Nov 2018 14:59:31 -0200 Subject: [PATCH 6/6] Add disclaimet to docs --- docs/MongodbEventRepository.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/MongodbEventRepository.md b/docs/MongodbEventRepository.md index df0577f..dc5cbd1 100644 --- a/docs/MongodbEventRepository.md +++ b/docs/MongodbEventRepository.md @@ -27,6 +27,8 @@ By default, the class already has some base methods: - `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: