Giter Club home page Giter Club logo

domain-driven-hexagon's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

domain-driven-hexagon's Issues

Question: double validation?

Hi, thanks for the project, it is very good learning material.

I have a quick question: in the user module you have business rules on the property country of the VO address:

https://github.com/Sairyss/domain-driven-hexagon/blob/master/src/modules/user/domain/value-objects/address.value-object.ts#L29

And later, in the UC create-user you have a DTO model that also species (different) validation rules:

https://github.com/Sairyss/domain-driven-hexagon/blob/master/src/modules/user/use-cases/create-user/create-user.request.dto.ts#L21

Isn't that duplication problematic? Do you think we should try to keep both set of rules similar? Or the DTO rules are just a basic safety and only the Domain rules matter? And in any case: shouldn't the DTO rules be at least less restrictive than the Domain rules? Here it would not be possible to have a country between 30 and 50 characters.

Thanks again for your dedication.

Enable GitHub discussions ?

Hi @Sairyss

First of all, I would like to thank you for that awesome repository ๐Ÿ‘

As a beginner in Hexagonal architecture and DDD, your repository and all the documentation you wrote in the README.md really helps to wrap my mind about concepts and good practices to build a clean modular monolith.

I guess I'm not the only one in the TS/JS community really appreciating the effort you put in that repository, so in order to go further and involve more community members I would like to propose to enable Github Discussions in that repository instead of opening issues for clarifying or requesting advices.

Let me know what you think ?

Thanks again

resolver return

Hello !

The resolver should return

return match(result, {
      Ok: (id: string) => new IdResponse(id),
      Err: (error: Error) => {
        if (error instanceof UserAlreadyExistsError)
          throw new ConflictHttpException(error.message);
        throw error;
      },
    });

instead of

Ok: (id: string) => new IdResponse(id)

Add DomainCollection concept

First of all, I absolutely love this project, and am working to implement these patterns in production.

Anyway, to continue on the concept of what an "Aggregate Root" is, it can often be a "parent" entity that has relations containing one or many "child" entities (or Aggregates) that the parent has control or can act on. Since we are separating Domain Entities from ORM entities, we will need a way for an Aggregate Root to gain context of the child relations in an ORM-agnostic way.

I borrowed some of the naming conventions from Mikro ORM, prefixed with Domain to avoid naming collisions and better segregate the mental boundary.

Proposal:

For an x-to-many relation:

export interface DomainCollection<Child extends Entity<any>> {
  load(): Promise<Child[]>
  add(entity: Child): void
  addMany(entities: Child[]): void
  remove(entity: Child): void
  removeMany(entities: Child[]): void
  update(entities: Child[]): void
}

For an x-to-one relation:

export interface DomainReference<Child extends Entity<any>> {
  load(): Promise<Child>
  update(updatedEntity: Child): void
  remove(): void
}

And a first stab at an implementation of the DomainCollection:

interface LoadStrategy<Parent, Child> {
  load(p: Parent): Promise<Child[]>
}

export class Collection<Parent extends Entity<any>, Child extends Entity<any>>
  implements DomainCollection<Child>
{
  constructor(parent: Parent) {
    this.#parent = parent
  }

  readonly #parent: Readonly<Parent>

  #loadStrategy?: LoadStrategy<Parent, Child>

  #items?: Map<ID, Child>

  update(entities: Child[]): void {
    entities.forEach((entity) => {
      this.items.set(entity.id, entity)
    })
  }

  add(entity: Child): void {
    const existing = this.items.get(entity.id)
    if (existing)
      throw new DomainException('Child entity with id already exists')
    this.items.set(entity.id, entity)
  }

  addMany(entities: Child[]): void {
    entities.forEach((entity) => {
      const existing = this.items.get(entity.id)
      if (existing)
        throw new DomainException('Child entity with id already exists')
      this.items.set(entity.id, entity)
    })
  }

  remove(entity: Child): void {
    this.items.delete(entity.id)
  }

  removeMany(entities: Child[]): void {
    entities.forEach((entity) => {
      this.items.delete(entity.id)
    })
  }

  withLoadStrategy(strategy: LoadStrategy<Parent, Child>) {
    this.#loadStrategy = strategy
  }

  async load() {
    if (!this.#loadStrategy)
      throw new Error('Load strategy has not been provided!')
    if (!this.#items) {
      const records = await this.#loadStrategy.load(this.#parent)
      this.#items = new Map(records.map((child) => [child.id, child]))
    }
    return Array.from(this.#items.values())
  }

  get items() {
    if (!this.#items) throw new Error('Collection has not been hydrated')
    return this.#items
  }
}

Thoughts with this approach:

  • OrmMappers would now need to have the child entity repository injected so that the loadStrategy can be hydrated, and this may lead to a significant increase in complication.
  • The Collection implementation probably belongs in the infrastructure layer, as it may differ due to the needs of the underlying data access layer.

Would love to see your take on this idea...

Add more information on the topic of authentication & authorization

Any idea on inside what layer authentication & authorization should be handled?, what is the best way to do that?.
my current implementation is that i have a UserGuard port which is basically an interface with the following methods:

import GuardError from './errors'

interface UserGuard {
  createKeyFor(user: User): Promise<string>
  findKeyHolder(key: string): AsyncOutcome<User, GuardError>
}

The testing implementation uses a Map object and an in-memory implementation of UserRepo it map access keys to user aggregate id values, the production implementation will use an actual caching store such as redis and the user database table.

The "access key" is essentially a typical session cookie, but it could be a JWT token or something else the management of these tokens is all up to the underlying implementation, domain and application layers are blind to this.

The UserGuard lives inside the identity subdomain of my application, it's where user identity data is managed other subdomains (for example Shipping & Billing subdomains) have their own guard ports that are very similar to UserGuard and the production implementation will use the same redis store, basically the user will log in and create an access key, the user then could use the access key to interact with the identity subdomain as a User or the billing subdomain as a Customer.

is this a good way of doing things ?, also where should authorization be done?, I'm doing most of it in the domain layer for example I'd have a domain service called payInvoice(customer: Customer, invoice: Invoice): Outcome<Invoice, Error> this service will return a failed outcome if the invoice belongs to a different customer or if the customer was denied the right to pay any invoices.

I think this repo should add a bit more information around authentication and where and maybe how it should be done.

Correct approach for different types of concept?

Let's say we have two concepts: Book and BookBeingSold. Book have methods like read, rate, and have properties like title and author. BookBeingSold have the same methods and properties but it also have additional methods buy, cancel, changePrice, and property price. The database have two tables books (id, title, author) and books-being-sold (id, book_id, price, is_deleted).

What should code look like for this scenario in DDD way? Should we create two separate modules book and bookBeingSold and two separate repositories? Should we inherit one module from another? Or we should just have one module book and one repository?

Best Approach for multi-tenancy

What is the best approach for multi-tenancy on TypeORM?

Currently, I created a TenantModule (Credits to Esposito Medium post to create and change de TypeORM connection on the fly.

But this doesn't seem right, since this ties the TypeORM connection and other sources to a general module instead of let each data source handle its way to multitenancy.

What do you think is the best practice for this?

UserEntity

What will be your approach for adding optional parameters to the user entity that you do not want to be added via the constructor?

Because it seems like the props value in the base entity only represents items added via the constructor and would not accept any other value otherwise.

Intermittent test failure for create-user

First off, thank you for this excellent example. I have learned a lot from it!

I'm encountering an intermittent test failure for the create-user e2e spec.

~/C/P/domain-driven-hexagon master = !3 ?2 โฏ yarn test                                                       4s 06:41:20 AM
yarn run v1.22.19
$ jest --config .jestrc.json
 PASS  tests/user/delete-user/delete-user.e2e-spec.ts (12.032 s)
 FAIL  tests/user/create-user/create-user.e2e-spec.ts (12.212 s)
  โ— Create a user โ€บ I can create a user

    expect(received).toBe(expected) // Object.is equality

    Expected: "string"
    Received: "undefined"

      42 |     then('I receive my user ID', () => {
      43 |       const response = ctx.latestResponse as IdResponse;
    > 44 |       expect(typeof response.id).toBe('string');
         |                                  ^
      45 |     });
      46 |
      47 |     and('I can see my user in a list of all users', async () => {

      at Object.stepFunction (tests/user/create-user/create-user.e2e-spec.ts:44:34)
      at node_modules/jest-cucumber/src/feature-definition-creation.ts:134:65

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 6 passed, 7 total
Snapshots:   0 total
Time:        12.906 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

When I log out the result monad at the controller for that particular endpoint, I see what might be a clue.

When all tests pass, I see the conflict monad printed to the console, then the successful monad that contains the user entity id

~/C/P/domain-driven-hexagon master = !3 ?2 โฏ yarn test                                                      17s 06:42:58 AM
yarn run v1.22.19
$ jest --config .jestrc.json
 PASS  tests/user/delete-user/delete-user.e2e-spec.ts (9.954 s)
  โ— Console

    console.log
      ResultType {
        [Symbol(Val)]: UserAlreadyExistsError: User already exists
            at CreateUserService.execute (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.service.ts:39:20)
            at processTicksAndRejections (node:internal/process/task_queues:95:5)
            at CreateUserHttpController.create (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.http.controller.ts:42:7) {
          cause: ConflictException [Error]: Record already exists
              at UserRepository.insert (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/libs/db/sql-repository.base.ts:118:15)
              at processTicksAndRejections (node:internal/process/task_queues:95:5)
              at /Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/libs/db/sql-repository.base.ts:215:24
              at execTransaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/connectionMethods/transaction.js:19:24)
              at transaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/connectionMethods/transaction.js:77:16)
              at /Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/binders/bindPool.js:120:24
              at createConnection (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/factories/createConnection.js:111:18)
              at Object.transaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/binders/bindPool.js:119:20)
              at CreateUserService.execute (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.service.ts:35:7)
              at CreateUserHttpController.create (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.http.controller.ts:42:7) {
            cause: [UniqueIntegrityConstraintViolationError],
            metadata: undefined,
            correlationId: 'r0aTGk',
            code: 'GENERIC.CONFLICT'
          },
          metadata: undefined,
          correlationId: 'r0aTGk',
          code: 'USER.ALREADY_EXISTS'
        },
        [Symbol(T)]: false
      } result

      at CreateUserHttpController.create (src/modules/user/commands/create-user/create-user.http.controller.ts:44:13)

 PASS  tests/user/create-user/create-user.e2e-spec.ts (10.035 s)
  โ— Console

    console.log
      ResultType {
        [Symbol(Val)]: '9ed7cc92-5903-4107-a6e3-f1cf7536eb1d',
        [Symbol(T)]: true
      } result

      at CreateUserHttpController.create (src/modules/user/commands/create-user/create-user.http.controller.ts:44:13)


Test Suites: 2 passed, 2 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        10.585 s, estimated 13 s
Ran all test suites.
โœจ  Done in 15.33s.

However, when the test fails, I see the success monad printed first, then the conflict monad

~/C/P/domain-driven-hexagon master = !2 ?2 โฏ yarn test                                                      16s 06:51:45 AM
yarn run v1.22.19
$ jest --config .jestrc.json
 PASS  tests/user/delete-user/delete-user.e2e-spec.ts
  โ— Console

    console.log
      ResultType {
        [Symbol(Val)]: '4d479c18-2f8a-4fd8-a4cc-8ae30e3ce6a3',
        [Symbol(T)]: true
      } result

      at CreateUserHttpController.create (src/modules/user/commands/create-user/create-user.http.controller.ts:44:13)

 FAIL  tests/user/create-user/create-user.e2e-spec.ts (5.084 s)
  โ— Console

    console.log
      ResultType {
        [Symbol(Val)]: UserAlreadyExistsError: User already exists
            at CreateUserService.execute (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.service.ts:39:20)
            at processTicksAndRejections (node:internal/process/task_queues:95:5)
            at CreateUserHttpController.create (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.http.controller.ts:42:7) {
          cause: ConflictException [Error]: Record already exists
              at UserRepository.insert (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/libs/db/sql-repository.base.ts:118:15)
              at processTicksAndRejections (node:internal/process/task_queues:95:5)
              at /Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/libs/db/sql-repository.base.ts:215:24
              at execTransaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/connectionMethods/transaction.js:19:24)
              at transaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/connectionMethods/transaction.js:77:16)
              at /Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/binders/bindPool.js:120:24
              at createConnection (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/factories/createConnection.js:111:18)
              at Object.transaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/binders/bindPool.js:119:20)
              at CreateUserService.execute (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.service.ts:35:7)
              at CreateUserHttpController.create (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.http.controller.ts:42:7) {
            cause: [UniqueIntegrityConstraintViolationError],
            metadata: undefined,
            correlationId: 'DkMKdX',
            code: 'GENERIC.CONFLICT'
          },
          metadata: undefined,
          correlationId: 'DkMKdX',
          code: 'USER.ALREADY_EXISTS'
        },
        [Symbol(T)]: false
      } result

      at CreateUserHttpController.create (src/modules/user/commands/create-user/create-user.http.controller.ts:44:13)

  โ— Create a user โ€บ I can create a user

    expect(received).toBe(expected) // Object.is equality

    Expected: "string"
    Received: "undefined"

      42 |     then('I receive my user ID', () => {
      43 |       const response = ctx.latestResponse as IdResponse;
    > 44 |       expect(typeof response.id).toBe('string');
         |                                  ^
      45 |     });
      46 |
      47 |     and('I can see my user in a list of all users', async () => {

      at Object.stepFunction (tests/user/create-user/create-user.e2e-spec.ts:44:34)
      at node_modules/jest-cucumber/src/feature-definition-creation.ts:134:65

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 6 passed, 7 total
Snapshots:   0 total
Time:        5.534 s, estimated 15 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

I'm looking into this more, but my first instinct is that either the tests are running in parallel and that is creating some race condition, although I don't see any jest arguments altering the worker count to enable parallel test execution. Alternatively there might be something wrong in the testing infrastructure for the response context and it being mutated in a non-deterministic way.

`StructuredClone()` function converts entity object into plain object, causing `undefined` properties when calling getters

Summary

using the structuredClone() function, as it creates a deep copy of the object, which means it will lose any functions, getters, or setters that were defined on the original object. Instead, you will end up with a plain object that only contains the data properties.

Description:

When using thegetPropsCopy()method to create a copy of an entity object, the StructuredClone() function is converting the entity object into a plain object. This is causing issues when calling the getters for properties of the entity object.

For example, when trying to access the address.country, address.postalCode, and address.street getters of a UserEntity object, the properties are returning undefined due to the conversion of the object by StructuredClone().

This issue seems to be related to the StructuredClone() function, and may be causing similar issues in other parts of the application.

 public getPropsCopy(): EntityProps & BaseEntityProps {
    const propsCopy = structuredClone({
      id: this._id,
      createdAt: this._createdAt,
      updatedAt: this._updatedAt,
      ...this.props,
    });
    return Object.freeze(propsCopy);
  }

Steps to Reproduce:

ref to. >

 toPersistence(entity: UserEntity): UserModel {
    const copy = entity.getPropsCopy();
    const record: UserModel = {
      id: copy.id,
      createdAt: copy.createdAt,
      updatedAt: copy.updatedAt,
      email: copy.email,
      country: copy.address.country, // getter
      postalCode: copy.address.postalCode, // getter
      street: copy.address.street, // getter
      role: copy.role,
    };
    return userSchema.parse(record);
  }
  • Create an instance of a UserEntity object.
  • Call the getPropsCopy() method to create a copy of the object.
  • Try to access the address.country, address.postalCode, and address.street getters of the copied object.

Expected Result:

The address.country, address.postalCode, and address.street getters should return the expected values.

Actual Result:

The address.country, address.postalCode, and address.street getters are returning undefined.

Domain service and persistence

All recommendations about use cases (application services) are clear when we deal with only one entities or aggregate.
When there are many entities you recommend to use domain service instead.

Use case contain all entity methods calls and the call to repository and persistence part.
If we use domain service instead, can we manage calls to repositories and the persistence part from the domain service ?
Or domain service will call use cases at the end ?

please tell me how to relationship

I don't know how to describe relations, so please let me know.

example:
individual-> telnumer [n]

How to register phone number information in the phone number table when creating an individual?

Queries using repositories

Looking into this find-users.query-handler.ts query handler i noticed you were using the user repository for fetching the users.
Since on the read side we're not looking into enforcing any business rules, like we are on the command/write side with Aggregates, is there a reason for using repositories?

For example if we were to have a Post aggregate and Comment entities within that aggregate there'd be no point in building out eachPost and fetching all of its Comment relations when fetching something like a list of all posts.

I couldn't find much on this online but i found this blog post which might explain this better than i can.

Btw, great work on the repo <3

entity.base.ts constructor always makes new Id

Nice work and great documentation. I'm wondering about the entity.base.ts constructor. It looks like a new ID is generated for an entity when the constructor is invoked. But there are cases where we want to instantiate an entity object that already has an ID (e.g. from persistence). Right?

From Eric Evans "DDD Reference"-pdf (2015). In the description of Repositories "...return fully instantiated objects or collections (encapsulate the storage technology) .... provide repositories only for aggregate roots keep application logic focused the model ...."

Entities relationship and performance

Thanks for this great repo, it's a mine of good practices and advice.
I've a remark about performance when we have relationship between entities.

We can have different cases , entities in same module or entities in same aggregate.
is there any recommendations to manage entities relationship and preserve good performance results?
Avoid to load all related entities, for example an user that have many posts and each action about post we should avoid to load all posts from user before to execute add, update or any.

Typo in the find-users GraphQL resolver's name

Hi!

I'm just exploring your repo and I think I noticed a typo in the name of the file /src/modules/user/queries/find-users/find-users.gralhql-resolver.ts. I guess it should say find-users.graphql-resolver.ts. :)

Thanks for sharing your work!

License

You put together a terrific resource!
I am currently writing a book on backend development (https://zero2prod.com) and I'd be interested in featuring (and linking) to your diagram on Hexagonal architecture. While I was checking the repository though I didn't manage to find a license covering the material - can you clarify what can and cannot be done with what is inside this repository?

Query handler breaking the dependency rule?

Hi @Sairyss!
Thanks for this awesome repo! It's been super helpful. I did encounter an issue recently though -

According to the readme - queries should be part of the application layer. But when looking at find-users.query-handler.ts, we can see it's importing dependencies from an outer layer - the infrastructure layer (like slonik and user.repository).

Doesn't that break the dependency rule? Or am I missing something?
If queries are at the same layer as application services, shouldn't they be required to use ports to communicate to "the outside world" just like application services are required to?

Thanks!
Simon

A couple of questions

First, and foremost, amazing work you've done. The explanation and diagrams are awesome. You did an impressive job by reviewing and integrating some of the most relevant architecture/software patterns. Impressive!

I'm working right now on a pretty complex project, that requires a big refactor and I'm researching some architecture alternatives. Based on that, I'm already coding something pretty similar to your suggestions, but I would like to ask you some questions:

  1. Which layer is responsible for events?
    In my scenario, I'm coding a CQRS+ES system (one of my main entities require a "time-machine" feature, so CQRS+ES is a pretty solid alternative), and, although you don't mention in the Architecture section, I see your recommended architecture a lot of inspiration from CQRS+ES itself.

That is something that intrigues me. I've read some sources recommending that events should be handled at the Domain layer. But other (the ones that I'm tempted to follow) suggest that the domain layer should not handle complexity such as events. Instead, they suggest handling those in the Application layer, consuming the Domain layer just for business logic. What is your opinion for that?

  1. Which layer is responsible for read/write models?
    Almost the same as above, I've seen many different implementations of read/write models. Some of those considering it as a member of the Application layer, others considering it a member of the Domain layer. What is your opinion is this matter?

Since you suggest that queries should bypass the domain layer to use the repository, I'm assuming you're considering that read models should live in the application layer. Is that correct? What about write models?

  1. Regarding folder structure, why not group by BC, instead of type?
    This question is related specifically to your suggested implementation of this architecture. You suggested grouping files based on their responsibility (folders like core, infrastructure, models). What do you think about grouping them based on their BC instead?

For example, instead of having:

<root folder>
โ”œโ”€โ”€ src
โ”‚   โ”œโ”€โ”€ core
โ”‚   โ”‚   โ”œโ”€โ”€ events
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ created-user.ts
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ dispatched-product.ts
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ ....
โ”‚   โ”‚   โ”œโ”€โ”€ commands
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ create-user.ts
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ dispatch-product.ts
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ ....
โ”‚   โ”‚   โ”œโ”€โ”€ ...

What do you think about having:

<root folder>
โ”œโ”€โ”€ src
โ”‚   โ”œโ”€โ”€ user
โ”‚   โ”‚   โ”œโ”€โ”€ events
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ created-user.ts
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ ....
โ”‚   โ”‚   โ”œโ”€โ”€ commands
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ create-user.ts
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ ....
โ”‚   โ”œโ”€โ”€ product
โ”‚   โ”‚   โ”œโ”€โ”€ events
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ dispatched-product.ts
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ ....
โ”‚   โ”‚   โ”œโ”€โ”€ commands
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ dispatch-product.ts
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ ....
โ”‚   โ”‚   โ”œโ”€โ”€ ...

I know it looks strange, but I've tested with this type of folder structure for a time now, and it scales pretty well. It is easier to navigate, and it usually makes your import paths smaller and easier to understand.

For the record, I'm also using 2 type of folder: lib and vendor. The first one I use for common, shared logic external from my core (integration with databases, abstract classes, and others). The second one (vendor) I use for framework-specific logic (like bootstrapping NestJS application, configuring TypeORM, and others).

I would like to hear your opinion about this as well :)

  1. Who is responsible for writing in the repository?
    Considering that you're following a CQRS approach, which layer is responsible for writing in the repository? (That question may be related to the second one). Is the application layer?

Again, great job :) looking forward for an answer.

Resolver returning type

In the graphql resolver we need to unwrap the id since the type of the service is : Promise<Result<AggregateID, ManipulatorAlreadyExistsError>>

Remplace resolver by :

@Resolver()
export class CreateManipulatorGraphqlResolver {
  constructor(private readonly commandBus: CommandBus) {}

  @Mutation(() => IdGqlResponse)
  async create(
    @Args('input') input: CreateManipulatorGqlRequestDto,
  ): Promise<IdGqlResponse> {
    const command = new CreateManipulatorCommand(input);

    const result = await this.commandBus.execute(command);

    return new IdGqlResponse(result.unwrap());
  }
}

core: ValueObjectProps & DomainPrimitive inconsistency

Hi, in value-object.base.ts we define 3 types:

export type Primitives = string | number | boolean;
export interface DomainPrimitive<T = Primitives> {
  value: T;
}

type ValueObjectProps<T> = T extends Primitives | Date ? DomainPrimitive<T> : T;

If T is a Primitives or a Date then ValueObjectProps will be a DomainPrimitive of T but T = Primitives forbid T from being a Date. I think typescript should not let us write that. If we replace the "=" with an "extend" the problem is clear:

https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcAKUCWBbTNMBuwAznALxzExYB2A5nAD5w0Cu2ARsFE3BxBAA2wAIY0A3AFgAUKEiw4mGjG4AzEQGNUAEQjYRSjDjyFgAHgAqcUCpoATUkdz4ixAHxwA3nAD0PuADkZAFwUMBggprAdnyIgTbA9sQBMnBwBCKCrMAAXHAWUtIAvjIySChwAGqZ2QDyHABWwBowGBBgxJYeFFYJSehYzqakzNoiKnAA-HC6+oaDJkRdcHkFvv4wpJikNBDwEADWcGKI2NDAMkA

Suggested fix:

Use "extends" instead of "=" and either have DomainPrimitive accept T as a Date or a Primitives:

export interface DomainPrimitive<T extends Primitives | Date> {
  value: T;
}

Or define a new type DomainDate:

export interface DomainPrimitive<T extends Primitives> {
  value: T;
}

export interface DomainDate<T extends Date> {
  value: T;
}

type ValueObjectProps<T> = T extends Primitives ? DomainPrimitive<T> : T extends Date ? DomainDate<T> : T;

Tell me if I misunderstood something. Maybe the "=" as an importance I am not aware of?

Dependency Inversion in new version

I suggest that instead directly import UserRepository to @Inject with UserRepositoryPort, it still make the handler depend on repository, the port interface here make no use and just for view.

user.module.ts

const repositories = [
  {
    provide: 'UserRepository',
    useClass: UserRepository,
  },
];

create-user.service.ts

  constructor(
    @Inject('UserRepository') private readonly repository: UserRepositoryPort,
  ) {}

Now the @Inject use the class name UserRepository provided in user.module.ts to know exactly dependency to inject to service, no need to import UserRepository directly inside service file

A question regarding use cases and database transactions

I'm sorry if this isn't the right place to ask such a question, I've searched for hours everywhere and cannot seem to find an answer.

If i have a use case that fetches a two aggregates of different types from their repositories, and passes them to a domain service that preforms business logic on the aggregates and returns them to the use case, how would i save all the aggregates back to the database in a single transaction ?.

one solution i came up with is to have one of the repositories methods take the second aggregate as an argument to include in the database transaction:

interface IMemberRepo {
  upgradeMemberToGold: (member: Member, payment: Payment) => Promise<void>
}

// or
interface IPaymentRepo {
  markPaymentAsFulfilled: (payment: Payment, member: Member) => Promise<void>
}

if a member aggregate gets upgraded to gold there must be a payment aggregate with fulfilled: true that exists, the above solution works but I'm not sure if it 100% adheres to DDD principles.

Some questions

Hello,

First of all, thank you for your awesome work, this is very inspiring .

  1. I wondering what is the purpose of the (empry) folder "providers" in "infrastructure"? I found nothing in the doc.
  2. Why do you rename "core" to "libs"? I loved the previous name, so I'ld like to know the reasons for this renaming.
  3. Why the file "infrastructure/interceptors/exception.interceptor.ts" is not in "libs" folder, as it seems generic?

Thank you very much :)!

Authentication module

In one of the MRs, someone asked a question about an example of authorship. In 2021 you wrote that if you find time you will implement such an example. Is this up to date?

I'm interested in this topic and would like to see what it might look like with your eye.

Domain Events

First of all, thank you for this great repo.

When you talk about domain events, you note that you have implemented your own version because NestJS CQRS lacks await. Maybe Iโ€™m misunderstanding, but I think the events are now reactive, so you can await them and even get a result https://docs.nestjs.com/recipes/cqrs#events

execute start:dev throws error

async save(entity: Entity): Promise<Entity> {
entity.validate(); // Protecting invariant before saving
const ormEntity = this.mapper.toOrmEntity(entity);
const result = await this.repository.save(ormEntity);

when excute start:dev it throws error

src/libs/ddd/infrastructure/database/base-classes/typeorm.repository.base.ts:47:47 - error TS2769: No overload matches this call.
  Overload 1 of 4, '(entities: DeepPartial<OrmEntity>[], options?: SaveOptions | undefined): Promise<(DeepPartial<OrmEntity> & OrmEntity)[]>', gave the following error.
    Argument of type 'OrmEntity' is not assignable to parameter of type 'DeepPartial<OrmEntity>[]'.
  Overload 2 of 4, '(entity: DeepPartial<OrmEntity>, options?: SaveOptions | undefined): Promise<DeepPartial<OrmEntity> & OrmEntity>', gave the following error.
    Argument of type 'OrmEntity' is not assignable to parameter of type 'DeepPartial<OrmEntity>'.

47     const result = await this.repository.save(ormEntity);

what am i missing?

Question: Mapping between Many-to-Many relation

Hi, I have been using your repository as a guide to develop an application and I must say your work is amazing. I've been trying to develop a library management system and whenever I find myself confused, I try to approach a solution similar to your work. However, I am having difficulties in something you haven't covered yet.
I am trying to define a many-to-many relationship between OrmEntities ( in my case book and author ). In my domain entities, I have a book entity that has as attribute an array of author entity, so when I am trying to map the props of book to Orm props, I find myself needing to map also the props of author ( which has already its own mapper ). What I have implemented currently is that I use the author mapper in the book mapper to map the props of the author.
I was wondering how should I approach mapping between entities. Is calling a mapper inside another mapper the correct approach?

Thank you and looking forward for your answer!

Q: Nested Gql field resolvers and auth

Hi, thank you for this great repo.

I have two questions.

  1. How do you handle nested field resolvers when two related entities are in different modules? e.g.
query getUser($filterUser: GetUserFilter!, $filterWallet: GetFilterWallet!) {
   getUser(filter: $filterUser) {
    id
    wallets(filter: $filterWallet) {
      id
    }
  }
}
  1. Where authentication and authorization belong?
    Let's say in our system user can update only his wallet and other entities he owns and each entity is in separate module. Should user module be shared between all modules? Can I use nestjs guards for protecting endpoints/mutations/queries?

GraphQL Question

Just thought I'd take a moment to thank you for this awesome repo. I regularly struggle for technical sources online suitable for enterprise level development; this is one of the best I've seen on this subject.

One question comes to mind, how would you organise GraphQL within this space? Originally l considered a singular graphql model isolated... but that seems inelegant having thought on it.

Saving aggregate

Hi, your code is a true source of inspiration regarding ddd, lots of explanations, great job!

In your code there is only save and delete for the user. For update I have some issues regarding saving/updating the aggregate.

  1. First load the aggregate from repo/unitOfWork
  2. Do some changes based on dto
  3. Save the aggregate

Using typeorm(in the end) the save/update method would update all aggregate(step 1) properties (not only the changed ones).

If we have two requests that change two different properties(firstName, lastName) and the first request has some delay/more work/etc, that means that the last request would be overwritten by the first. ( Somehow the repo shoud save only what changed in the aggregate?)

Thanks,

Good work !

I've stumbled upon this repo by chance and I think it's great, thanks for this !

Why are ports defined in infrastructure layer?

Not sure if I'm missing something but why are database ports such as user.repository.port.ts placed in infrastructure layer next to its implementation?
Shouldn't it be defined in application/domain core where its injected?

UnitOfWork creating many QueryRunners and not release them

Your uow (in my typeorm-unit-of-work.base.ts) class creates many connections and block any query to the database.

import { EntityTarget, getConnection, QueryRunner, Repository } from 'typeorm';
import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel';

import { ILoggerPort, IUnitOfWorkPort } from 'src/shared/domain/ports';
import { ServiceResponseDtoBase } from 'src/shared/interface-adapters/base-classes/service-response-dto.base';

export class TypeORMUnitOfWork implements IUnitOfWorkPort {
  constructor(private readonly logger: ILoggerPort) {}

  private _queryRunners: Map<string, QueryRunner> = new Map();

  // Use this and check in cron job (executed every 10 min) 
  getQueryRunners() {
    return this._queryRunners;
  }

  getQueryRunner(correlationId: string): QueryRunner {
    const queryRunner = this._queryRunners.get(correlationId);
    if (!queryRunner) {
      throw new Error(
        'Query runner not found. Incorrect correlationId or transaction is not started. To start a transaction wrap operations in a "execute" method.',
      );
    }
    return queryRunner;
  }

  getOrmRepository<Entity>(
    entity: EntityTarget<Entity>,
    correlationId: string,
  ): Repository<Entity> {
    const queryRunner = this.getQueryRunner(correlationId);
    return queryRunner.manager.getRepository(entity);
  }

  async execute<T>(
    correlationId: string,
    callback: () => Promise<T>,
    options?: { isolationLevel: IsolationLevel },
  ): Promise<T> {
    if (!correlationId) {
      throw new Error('Correlation ID must be provided');
    }
    this.logger.setContext(`${this.constructor.name}:${correlationId}`);
    const queryRunner = getConnection().createQueryRunner();
    this._queryRunners.set(correlationId, queryRunner);
    this.logger.debug(`[Starting transaction]`);
    await queryRunner.startTransaction(options?.isolationLevel);
    let result: T | ServiceResponseDtoBase<T>;
    // Here i use my own wrapper, like from source 
    try {
      result = await callback();
      if (
        ServiceResponseDtoBase.isError(
          result as unknown as ServiceResponseDtoBase<T>,
        )
      ) {
        await this.rollbackTransaction(
          correlationId,
          (result as unknown as ServiceResponseDtoBase<any>).data,
        );
        return result;
      }
    } catch (error) {
      await this.rollbackTransaction<T>(correlationId, error as Error);
      throw error;
    }
    try {
      await queryRunner.commitTransaction();
    } finally {
      await this.finish(correlationId);
    }
    this.logger.debug(`[Transaction committed]`);
    return result;
  }

  private async rollbackTransaction<T>(correlationId: string, error: Error) {
    const queryRunner = this.getQueryRunner(correlationId);
    try {
      await queryRunner.rollbackTransaction();
      this.logger.debug(
        `[Transaction rolled back] ${(error as Error).message}`,
      );
    } finally {
      await this.finish(correlationId);
    }
  }

  private async finish(correlationId: string): Promise<void> {
    const queryRunner = this.getQueryRunner(correlationId);
    try {
      await queryRunner.release();
    } finally {
      this._queryRunners.delete(correlationId);
    }
  }
}

And in my cron job just inject this UoW and call getQueryRunners().size, so around 140 objects after 30 mins in production...
Any advice or there are reasons why?

Relationship between `UserRepository` and `TypeormRepositoryBase`

While working through your database implementation, I was wondering why UserRepository does not use TypeormRepositoryBase.findOne?

private async findOneByEmail(
email: string,
): Promise<UserOrmEntity | undefined> {
const user = await this.userRepository.findOne({
where: { email },
});
return user;
}

Shouldn't we do this instead:

const emailVO = new Email(email);
const user = await this.findOne({ email: emailVO });
return user;

And a second question: It seems like UserRepository.prepareQuery removes all query parameters except for id? Why?

// Used to construct a query
protected prepareQuery(
params: QueryParams<UserProps>,
): WhereCondition<UserOrmEntity> {
const where: QueryParams<UserOrmEntity> = {};
if (params.id) {
where.id = params.id.value;
}
return where;
}

Order by query

Hello, thanks for great repository.

I'm wondering how ordering shall be implemented with this interface?

export interface OrderBy {
  [key: number]: -1 | 1;
}

How can I pass DTO to sort some column ASC/DESC in controller?

Very nice!!

Not really an issue, but wanna congratulate you on this repo, it's very good content.

I'm glad more ppl is walking the path I walked some years ago, even with a repository on github like I did .

Although I must say your diagram looks a lot better than mine :)
68747470733a2f2f646f63732e676f6f676c652e636f6d2f64726177696e67732f642f652f32504143582d317651357073373275615a63454a7a776e4a6250 37686c3269334b32484861744e63736f79473274675832766e724e357878444b4c70354a6d35627a7a6d5a64762f7075623f773d39363026683d363537

If you are curious, you can also check out my talk about it.

Keep it up!

questions

hello, I'm in process of exploring this wonderful repo and I really hope that you will continue to work on it until it becomes a real-world example project I could always come back to.

But there are some things a bit confusing for me, mostly the "what should be where". Some stuff I have kind of understood but this is still confusing for me. For example, domain-event-handlers and email are under modules but they don't seem to contain any kind of code that would relate to modules, so why are they there?
Understandably, email looks like a work in progress but the directory is missing the modules part which would help me to tie it together. But the domain-event-handlers seem out of place for me.

Also, is there somewhere a reason why the lint rules are what they are?

I'm also having trouble setting up the dev environment. specifically, when I start the DB docker and then run start:dev, I get an exception that test-db doesn't exist.

now that I'm thinking about it, I can't seem to find any instructions on how to start the dev env. like, step-by-step what should be done and what do I need before I can run it.

PS: it seems that while adding a licence, you forgot to update it in package.json, but I doubt it has any significance since it's not in the npm registry.

Managing `Entity.updatedAt`

First of all: Thank you for publishing this! Amazing job :)

I am wondering how to manage the Entity's updatedAt timestamp. Should I set it manually in every method of every Entity subclass that performs an update?

For example, when updating the UserEntity address:

updateAddress(props: UpdateUserAddressProps): void {
this.props.address = new Address({
...this.props.address,
...props,
} as AddressProps);
}

Should this include a line this._updatedAt = DateVO.now();?

Nest removed Logger.setContext()

Thanks so much for effort writing all this, this repo has been my go to for helping me grok DDD books and their concepts in the context of something tangible.

I'm working through your example implementation and noticed that at some point Nest upgraded their Logger implementation, so the Logger.setContext() method is no longer available. Instead you're expected to pass the context as an optional second parameter in the log call.

All I can think is to add the log method overloads to the Logger port interface and then add a private class attribute for the logContext wherever you've previously called setContext().

Is there a cleaner way to deal with this that I'm missing?

it shows compile error

async save(entity: Entity): Promise<Entity> {
entity.validate(); // Protecting invariant before saving
const ormEntity = this.mapper.toOrmEntity(entity);
const result = await this.repository.save(ormEntity);

when excute start:dev it throws error

src/libs/ddd/infrastructure/database/base-classes/typeorm.repository.base.ts:47:47 - error TS2769: No overload matches this call.
  Overload 1 of 4, '(entities: DeepPartial<OrmEntity>[], options?: SaveOptions | undefined): Promise<(DeepPartial<OrmEntity> & OrmEntity)[]>', gave the following error.
    Argument of type 'OrmEntity' is not assignable to parameter of type 'DeepPartial<OrmEntity>[]'.
  Overload 2 of 4, '(entity: DeepPartial<OrmEntity>, options?: SaveOptions | undefined): Promise<DeepPartial<OrmEntity> & OrmEntity>', gave the following error.
    Argument of type 'OrmEntity' is not assignable to parameter of type 'DeepPartial<OrmEntity>'.

47     const result = await this.repository.save(ormEntity);

what am i missing?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.