Giter Site home page Giter Site logo

stemmlerjs / ddd-forum Goto Github PK

View Code? Open in Web Editor NEW
1.9K 56.0 392.0 1.59 MB

Hacker news-inspired forum app built with TypeScript using DDD practices from solidbook.io.

Home Page: https://dddforum.com

License: ISC License

HTML 0.40% CSS 0.09% TypeScript 97.15% JavaScript 0.81% Sass 1.45% Dockerfile 0.06% Shell 0.04%
hackernews ddd typescript

ddd-forum's Introduction

DDDForum.com

A SOLID hackernews-inspired forum site built with TypeScript using the clean architecture and DDD best practices.

DDDForum

About

DDDForum.com is the application that we build in solidbook.io - The Software Design and Architecture Handbook.

Running the project

  1. Install and start Docker if you haven't already.
  2. Copy the .env template file. Feel free to change passwords and app secrets.
cp .env.template .env
  1. Build and run the image to run the backend services.
docker-compose up
  1. Open up an additional console and then run:
npm run setup:dev
npm run start:both

You can visit the app by going to http://localhost:3000.

Demo

You can visit the site here.

Note: It's currently deployed on free tier Heroku, which has some undesirable side-effects like shutting off the server during periods of inactivity. So if it's down for you, refresh a couple of times. Thinking about migrating this to a serverless architecture later on.

Built with

Backend

  • Sequelize - The ORM for Node.js
  • Express.js - Lightweight webserver
  • Redis - For holding onto JWT tokens and refresh tokens

Frontend

Architecture

We built this based on the Clean Architecture, SOLID principles, and Domain-Driven Design best practices using TypeScript.

Clean architecture

There's obviously a lot that went into building this from front to back.

The Clean Architecture is a way to reason about where different types of application logic belongs.

Frame 8 (1)

There's a lot more to learn about the clean architecture, but for now- just know that it's a way to really separate the concerns of everything that goes into building complex enterprise applications. You'll never see any infrastructure-related code alongside domain layer code.

The clean architecture, when combined with Domain-Driven Design, is very powerful :)

In DDD, we build applications on top of a number of subdomains.

Subdomains

A subdomain is a cohesive unit of code that represents exactly one core concept and is responsible for a specific set of concerns in an application architecture. For example, every appliciation has a users subdomain. That's responsible for users, identity & access management, authentication, authorization, etc. Sometimes you don't want to build that yourself. Sometimes you can go with an off-the-shelf solution like Auth0. But there are subdomains in your application that you cannot simply outsource. These are the family jewels; the thing that's actually novel about your app. This is the subdomain that no one (except you) can code. Know why? Because only you have the domain knowledge to build it exactly the way that it should be built. You understand the domain.

In DDDForum, we have 2 subdomains: The users subdomain and the forum subdomain.

Frame 3 (1)

Each subdomain has a:

  • domain layer: where the highest-level policy, domain objects, and domain rules belong (user, email, etc)
  • application layer: where the use cases / features that utilize domain objects belong (createUser, login, etc)
  • adapter layer: where we define abstractions so that application layer code can interact with infrastructure layer concepts, without actually requiring on infrastructure (because that would break the dependency rule). Here we write things like IUserRepo - repository adapter, IJWTTokenService - an abstraction of a cache (redis) that manages tokens, etc.
  • infrastructure layer: where we create concrete implementations of the abstractions from the adapter layer so that they can be spun up at runtime thanks to the power of polymorhpism :) (more on this later).

If you haven't already, I recommend you read this article on use cases and subdomains.

Let's identify some of the actual concepts that exist in each subdomain.

users subdomain

In the users subdomain, we're only concerned with concepts that are related to authentication, roles, etc. Here are a few examples of classes and concepts that exist at each layer.

forum subdomain

In the forum subdomain, we're only concerned with concepts that have to do with building a forum. You won't see any domain concepts from the user in forum. In the forum subdomain, the concept most equivalent to a user, is a member.

Here are a few examples of concepts from the forum subdomain.

  • domain layer: member, comment, post, postVote, commentVote, commentVotesChanged
  • application layer: replyToComment, getMemberByUserName, upvotePost, downvotePost
  • adapter layer: ICommentRepo, IPostRepo, IMemberRepo
  • infrastructure layer: SequelizeCommentRepo, SequelizePostRepo, SequelizeMemberRepo

Project visualization

Here's a large-scale visualization of the repo. As I put more time into the front-end, it may change a little bit.

Visualization of this repo

Contributing

DDDForum is an open source project, and contributions of any kind are welcome! Open issues, bugs, and enhancements are all listed on the issues tab and labeled accordingly. Feel free to open bug tickets and make feature requests. Easy bugs and features will be tagged with the good first issue label.

Contributors āœØ

Thanks goes to these wonderful people (emoji key):


Anthony Denneulin

šŸ’»

Khalil Stemmler

šŸ’» šŸ› šŸ“– šŸš‡ šŸ¤”

Faisol Chehumar

šŸ’»

Trung Tran

šŸš‡

This project follows the all-contributors specification. Contributions of any kind welcome!

License

This project is licensed under the ISC License - see the LICENSE.md file for details

ddd-forum's People

Contributors

allcontributors[bot] avatar denneulin avatar dependabot[bot] avatar stemmlerjs avatar

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

ddd-forum's Issues

Should modules have different connections to the DB?

Thanks a lot for what you're doing here and on "white-label". Very helpful to set right from the beginning new projects!

Can't figure out how to debug docker container in vscode

I'm fairly new to docker containers and despite following various articles online, can't figure out how to attach vscode to a docker container running from the local terminal for debugging the node app (breakpoints, etc) or to alternatively spin up the docker container right from vscode (using Docker and Remote - Containers extensions). I figure that perhaps someone might have experience on how to get this working, and was hoping I could get some guidance regarding it if possible please

ValueObject fail does not match use case error structure

Hey, Exploring the CreateUser use case I noticed that if a value object fails then the server responds with a undefined error in the response. For example try to create a user with a password length of 2.

Should a validation error class be made that extends Result<UseCaseError>?

const dtoResult = Result.combine([
  emailOrError, passwordOrError, usernameOrError 
]);

if (dtoResult.isFailure) {
  return left(new AppError.ValidationError(dtoResult.error)) as Response;
}

[Question] Passing type and not using it in interface

Hello.

Path: src\shared\domain\events\IHandle.ts

Why do you pass type IDomainEvent in this file and don't use it at all?

import { IDomainEvent } from "./IDomainEvent";

export interface IHandle<IDomainEvent> { 
  setupSubscriptions(): void;
}

Maybe instead of

export interface IHandle<IDomainEvent> {

it should be:

export interface IHandle extends IDomainEvent

Guard against value objects

Hi Khalil šŸ‘‹šŸ» ! First of all, I'm a great fan of your book and your blog I believe they are really valuable resources and I hope you will publish your video series soon.

While I was navigating your code, I realised you are guarding against certain properties multiple times one from usecase layer, mappers and the other one is in aggregate creation like below (username, email):

const guardResult = Guard.againstNullOrUndefinedBulk([
{ argument: props.username, argumentName: 'username' },
{ argument: props.email, argumentName: 'email' }
]);
if (!guardResult.succeeded) {
return Result.fail<User>(guardResult.message)
}

async execute (request: CreateUserDTO): Promise<Response> {
const emailOrError = UserEmail.create(request.email);
const passwordOrError = UserPassword.create({ value: request.password });
const usernameOrError = UserName.create({ name: request.username });
const dtoResult = Result.combine([
emailOrError, passwordOrError, usernameOrError
]);
if (dtoResult.isFailure) {
return left(Result.fail<void>(dtoResult.error)) as Response;
}
const email: UserEmail = emailOrError.getValue();
const password: UserPassword = passwordOrError.getValue();
const username: UserName = usernameOrError.getValue();

Is there any reason for that or is it just forgotten piece of code? And what do you think about creating value objects inside create methods of aggregates. It feels a bit cluttered when we create all value objects inside of usecases and I feel it's more appropriate to pass InputDTO to our create methods and expect either object or errors from it. I would appreciate your opinions on this.

Pagination for posts

Posts on the front page should be paginated for when it grows really large! :)

Why is lodash being imported into Comment Entity?

First of all thank you for share your knowledge with the community. I'm new to concepts like clean architecture and DDD so this repo and your blog has helped me a LOT for understanding. šŸŽ‰

The last two weeks I've been reading Uncle Bob - "Clean Architecture" and has been hard to me get the concept of an Entity(Enterprise Business Rules). According to the book(p. 190):

An Entity is an object within our computer system that embodies a small set of critical business rules operating on Critical Business Data.

I understand that they contain rules that are not application specific(Use Cases) and should not be affected by any external change, basically pure objects or data structures. So I thought that is broken when lodash is imported directly in the Comment entity.

Also, I don't know if it's the same case in class UniqueEntityID when importing the library uuid/v4 and then importing it in base entity Entity.

Would this make entities dependent on the lodash and uuid/v4 libraries?

Use onEnterPress to detect submissions

On the "login" and "signup" pages, it would be nice if pressing enter also issued a submit.

Not too hard to hook up.

There might also be other places in the app that need it as well though.

[Question] Uniqueness validation & constructing domain objects in read operations (queries)

Hi, @stemmlerjs

First of all, I want to say thank you for such a good explanation of DDD. I read all of your articles related to this topic and became the owner of your SOLID book for in-depth study of the material.

After starting integration with DDD in my project I faced a couple of issues (questions) that cannot be resolved by myself.

I want to apologize in advance if these questions will be asked in the wrong place

List of the questions:

  • Do I need to construct the entire aggregate root during the read (query) operations? (See Update 1 at the end of the post)
    Is it correct to have a couple of methods in a repository to be able to fetch requested data from the storage without constructing an aggregate root? It seems to be a redundant step to map the database model into domain objects and afterward into DTO if no changes will be applied to the fetched object. (following CQS)

  • Where to put field uniqueness validation? (for example: user with such username already exist)
    Do I need to call it an application service (use-case) before creating an aggregate root (it seems like leaky abstraction)? Or... Do I need to rely on the repository interface inside the aggregate root and call this validation before creating it (inside static create method)?

  • In the case of following CQRS principles what should be returned to the user inside a GraphQL response?

  1. For instance, I want to remove a user or any other entity. Would be enough to return a removed entity id? Wouldn't it violate a rule that mutations should be void?
  2. Or If I want to add a new user? Would it be okay to return a newly created user in a response?

The third question is the most confusing to me. In case a server returns the only id of created entity it means that a client has to send 1 more request to a server to fetch a newly created object. At least I think so...

// Pseudo code
const response = useMutation(createUser) // { userId: '1' }
const user = useQuery(getUser, { userId: response.userId }) // { userId: '1', role: 'admin', ...other fields }

I would like to know your opinion,
Thank you

Update 1

I was managed to asnwer one of my questions by re-reading this article How to Design & Persist Aggregates - Domain-Driven Design w/ TypeScript

DTOs can have a tight requirement to fulfill a user inferface, so instead of filling up an aggregate with all that info, just retrieve the data you need, directly from the repository/repositories to create the DTO.

As far I understand that repository is allowed to return the data which is required by DTO without constructing the entire aggregate root.

[Question] Sharing of errors between client & server.

Hi @stemmlerjs me again :(

Another query for you!

I can see you are using very similar error handling in the client and server code which is great!

However my question is would you consider having a shared package for them both to consume which would contain all the errors? The reason I ask is that right now all the errors need to be duplicated across both.

This shared package could also house some shared types in the future.

Let me know how you would suggest handling this.

Cheers!

[Question] How best to break up large use cases?

Hi @stemmlerjs !

I was wondering if you have any insight into how best to break up large use-cases?

I have some constructors which now look like the following:

  private usersRepo: UsersRepo;
  private dealsRepo: DealsRepo;
  private brandsRepo: BrandsRepo;
  private audiencesRepo: AudiencesRepo;
  private googleService: GoogleService;
  private brandService: BrandService;

Is this where the concept of a service would come in and we would have a relationship such as controller -> use-case -> services[brand, user, ...]

Thanks in advance!

[Question] Typescript errors in Result class

Hi Khalil, first, thank you for this amazing repository to learn DDD in practice.

I'm trying to understand each part of this code, beginning with the basics. In src/shared/core/Result.ts when I code in my text editor, it shows some error in typescript, like this:

Errors in code
    Type 'string | T | undefined' is not assignable to type 'string | T'.
      Type 'undefined' is not assignable to type 'string | T'.
    Type 'T | undefined' is not assignable to type 'T'.
      'T' could be instantiated with an arbitrary type which could be unrelated to 'T | undefined'.
    Argument of type 'null' is not assignable to parameter of type 'string | U | undefined'.

You can see here, in TS playground.

The logic is fine. Your article is great and I get the idea, the problem is with typescript.

Because I'm new to TS and your definitions of DDD, I don't know if my solution is correct. Could you, or anyone else, check for me?

toggle solution code
export class Result<T> {
  public isSuccess: boolean;
  public isFailure: boolean
  private error: T | string | null | undefined;
  private _value: T | undefined;

  public constructor (isSuccess: boolean, error?: T | string | null, value?: T) {
    if (isSuccess && error) {
      throw new Error("InvalidOperation: A result cannot be successful and contain an error");
    }
    if (!isSuccess && !error) {
      throw new Error("InvalidOperation: A failing result needs to contain an error message");
    }

    this.isSuccess = isSuccess;
    this.isFailure = !isSuccess;
    this.error = error;
    this._value = value;
    
    Object.freeze(this);
  }

  public getValue () : T | undefined{
    if (!this.isSuccess) {
      console.log(this.error,);
      throw new Error("Can't get the value of an error result. Use 'errorValue' instead.")
    } 

    return this._value;
  }

  public getErrorValue (): T {
    return this.error as T;
  }

  public static ok<U> (value?: U) : Result<U> {
    return new Result<U>(true, null, value);
  }

  public static fail<U> (error: string): Result<U> {
    return new Result<U>(false, error);
  }

  public static combine (results: Result<any>[]) : Result<any> {
    for (let result of results) {
      if (result.isFailure) return result;
    }
    return Result.ok();
  }
}

export type Either<L, A> = Left<L, A> | Right<L, A>;

export class Left<L, A> {
  readonly value: L;

  constructor(value: L) {
    this.value = value;
  }

  isLeft(): this is Left<L, A> {
    return true;
  }

  isRight(): this is Right<L, A> {
    return false;
  }
}

export class Right<L, A> {
  readonly value: A;

  constructor(value: A) {
    this.value = value;
  }

  isLeft(): this is Left<L, A> {
    return false;
  }

  isRight(): this is Right<L, A> {
    return true;
  }
}

export const left = <L, A>(l: L): Either<L, A> => {
  return new Left(l);
};

export const right = <L, A>(a: A): Either<L, A> => {
  return new Right<L, A>(a);
};

[Question] Cyclic dependencies

After generating a graph of module deps, I noticed these circular dependencies:

  1. src/modules/forum/domain/member.ts > src/modules/forum/domain/events/member-created.ts
  2. src/shared/domain/aggregate-root.ts > src/shared/domain/events/domain-events-manager.ts
  3. src/modules/users/domain/user.ts > src/modules/users/domain/events/user-created.ts

This obviously breaks the acyclic dependency principle.

@stemmlerjs Is this somehow acceptable from your POV?

[Question]: Usage of left/right monad & try/catch

@stemmlerjs sorry but one more question :)

I was wondering why in the DDD forum, solidbook and your blog where you are using left/right there is still a lot of try/catch is the purpose not for the functional monad to remove the need to use try/catch?

Example:

Making local dev easier and cleaning up the docs

I'm finding that a lot of users struggle with using docker to run, test, and debug the app (see #45).

Right now, Docker is responsible for:

  • the backend app
  • the frontend app
  • the services (MySQL, redis)

Instead, I think what I'd prefer is to set this up so that it's like this:

Docker

  • the services

User-space (start in a console)

  • the backend
  • the frontend app

I think the real utility of using Docker is purely going to be just so that everyone can get quick access to services without having to mess around with installing and configuring stuff.

To do this, I believe I can merely comment out the web portion of the Dockerfile. Will also need to clean up docs.

[Redux]: When a vote changes, update the view

When someone changes their vote on a comment or post, we need to update the view.

Currently, we have to refresh the browser to see the changes.

On upvote:

  • if the user hasn't already upvoted, it should be +1
  • if the user already upvoted, it should stay the same
  • if the user downvoted, it should + 1

On downvote:

  • if the user hasn't already downvoted, it should be -1
  • if the user already downvoted, it should stay the same
  • if the user upvoted, it should -1

Putting ORM Models Inside Modules

On a conceptual level, would it make more sense to put Sequelize models inside respective modules in modules/[module]/infra/* as opposed to shared/infra/database/sequelize/models?

We're co-locating them only because it would be difficult to initialize all models for the ORM? Or do they belong in shared infra folder conceptually?

[Question] Why no unit tests?

Hello,

First of all, thank you @stemmlerjs for putting that together. That repo and your courses are the most applied, reusable piece of teaching I've found on the DDD matters.

However, while you go in great lengths about architectural concerns, I was surprised by how few unit tests there are in your code repository. Usually, TDD is a good companion to DDD. Do you have thoughts to share about it ?

[Question] Organising email sending service

Great work with this project, I'm new to DDD but your work (this repo and your website) has helped me get started. šŸ‘

I've a couple of questions about how to organise services such as an email sending service. Suppose the use case is: when someone replies to my post I should receive an email.

  1. Would the email service be defined in the infra layer and it'd subscribe to events e.g. postCreated?
  2. Suppose we wanted to make sure that when creating a post both the post and the email must be sent or the request to create the post fails - how is that handled? I see some comments to Unit Of Work but not sure how that works in concrete terms.

[question] models vs repository mode

First of all this is great and thank you for this valuable contribution to the community. The ecosystem is a mess and I can't tell you how many times I've seen architecture end up being an afterthought when it's far too late.

I've been working out a similar approach although using the hexagonal architecture and TypeORM, but have considered switching back to Sequelize mostly because it's what my coworkers are familiar with and partially because of the rigidity of TypeORM despite the first class repository and TypeScript support. The use-case approach of the clean architecture is also really growing on me as I start to tackle more complex problems and application services grow in complexity.

Have you considered using sequelize-typescript's Repository Mode and if so why did you end up going with the active record approach. I appreciate that you isolate this through the repository abstraction and totally think that's a reasonable approach. An alternative, which to be fair is less pure (violates SRP) but involves less mapping would be to use models under sequelize-typescript's repository mode. This leaves out all the extra baggage that comes with the active record approach so you could get away, in most cases, with using the model as both your ORM and domain model. Any thoughts on this? This has been a win for me in terms of eliminating friction for developer adoption.

I'm also curious how you're dealing with persisting aggregates in Sequelize - I know you can use includes for queries, but can you automatically persist to embedded entities (e.g. if an Order has LineItem[] and you update that LineItem[] through the Order can you then get the Order model to persist the LineItem[]changes whenever save is called)? Or do you just have to handle this manually in the repository. To be fair you should strive to keep your aggregates small so it should be rare for this scenario to get overly complex.

[Question] How to test error branches of application services layer?

Thanks alot for this repo + white-label @stemmlerjs , I've learned a ton of how to deal with complexity in enterprise applications.

After applying some of the principles in a personal project, I was wondering how to test the error branches of the app service layer when it depends on a validation/business rule in the domain layer. For example in the createPost application service:

const titleOrError = PostTitle.create({ value: request.title });
if (titleOrError.isFailure) {
return left(titleOrError);
}

Here's the resulting business rules in the domain layer for a PostTitle:

public static create (props: PostTitleProps): Result<PostTitle> {
const nullGuardResult = Guard.againstNullOrUndefined(props.value, 'postTitle');
if (!nullGuardResult.succeeded) {
return Result.fail<PostTitle>(nullGuardResult.message);
}
const minGuardResult = Guard.againstAtLeast(this.minLength, props.value);
const maxGuardResult = Guard.againstAtMost(this.maxLength, props.value);
if (!minGuardResult.succeeded) {
return Result.fail<PostTitle>(minGuardResult.message);
}
if (!maxGuardResult.succeeded) {
return Result.fail<PostTitle>(maxGuardResult.message);
}
return Result.ok<PostTitle>(new PostTitle(props));

In order to test that the application service returns the correct result, we must write an integration test as the if statement depends on an broken business rule/validation error occurring in the domain layer. This results in combinatorial explosion where the branch in the domain model is tested twice (once in the domain layer with a unit test, and once in each application service that depends on this rule being broken).

Is there a suggest way to test the application service error branches in isolation? The only way that it seems possible is stubbing the static create method for each domain entity in the test setup to return the correct Result class, which seems less than optimal.

IDs as value objects fail equality check

Because value objects use shallow object equality calling equals on the ID value object will always fail. This requires you to access the UniqueEntityID inside the value object prop and calling the equals on that.

CommentId.value.equals(SomeOtherCommentId.value)
vs
CommentId.equals(SomeOtherCommentId (will always fail)

should we just be adding deep equality instead of shallow equality to the value object equality checks?

Can't run local instance of ddd-forum due to mysql authentication error

Hi,

I cloned the repo and followed the setup instructions to copy the env template file to .env which I did. The contents of my .env file are:

DDD_FORUM_IS_PRODUCTION=false
DDD_FORUM_APP_SECRET=defaultappsecret
DDD_FORUM_REDIS_URL=
DDD_FORUM_REDIS_PORT=
DDD_FORUM_DB_USER=chun
DDD_FORUM_DB_PASS=12345678
DDD_FORUM_DB_HOST=
DDD_FORUM_DB_DEV_DB_NAME=data_dev
DDD_FORUM_DB_TEST_DB_NAME=data_test
DDD_FORUM_DB_PROD_DB_NAME=data_prod

if I run docker-compose up, I get the following error in my terminal:

Starting ddd_forum_mysql ... done
Starting ddd_forum_redis ... done
Starting ddd-forum_adminer_1 ... done
Starting ddd_app             ... done
Attaching to ddd_forum_redis, ddd_forum_mysql, ddd-forum_adminer_1, ddd_app
ddd_forum_mysql | 2021-01-06 01:53:50+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.22-1debian10 started.
ddd_app    | Waitin for mysql to start...
ddd_forum_redis | 1:C 06 Jan 2021 01:53:50.722 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
ddd_forum_redis | 1:C 06 Jan 2021 01:53:50.723 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=1, just started
ddd_forum_redis | 1:C 06 Jan 2021 01:53:50.723 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
ddd_forum_mysql | 2021-01-06 01:53:50+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
ddd_forum_mysql | 2021-01-06 01:53:50+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.22-1debian10 started.
ddd_forum_redis | 1:M 06 Jan 2021 01:53:50.724 * Running mode=standalone, port=6379.
ddd_forum_redis | 1:M 06 Jan 2021 01:53:50.724 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
ddd_forum_redis | 1:M 06 Jan 2021 01:53:50.724 # Server initialized
ddd_forum_redis | 1:M 06 Jan 2021 01:53:50.725 * Loading RDB produced by version 6.0.9
ddd_forum_redis | 1:M 06 Jan 2021 01:53:50.725 * RDB age 202 seconds
ddd_forum_redis | 1:M 06 Jan 2021 01:53:50.725 * RDB memory usage when created 0.76 Mb
ddd_forum_redis | 1:M 06 Jan 2021 01:53:50.725 * DB loaded from disk: 0.000 seconds
ddd_forum_redis | 1:M 06 Jan 2021 01:53:50.725 * Ready to accept connections
adminer_1  | [Wed Jan  6 01:53:51 2021] PHP 7.4.13 Development Server (http://[::]:8080) started
ddd_forum_mysql | 2021-01-06T01:53:51.196146Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.22) starting as process 1
ddd_forum_mysql | 2021-01-06T01:53:51.208664Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
ddd_forum_mysql | 2021-01-06T01:53:51.445568Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
ddd_forum_mysql | 2021-01-06T01:53:51.562691Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
ddd_forum_mysql | 2021-01-06T01:53:51.657957Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
ddd_forum_mysql | 2021-01-06T01:53:51.658195Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
ddd_forum_mysql | 2021-01-06T01:53:51.661323Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
ddd_forum_mysql | 2021-01-06T01:53:51.688163Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.22'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
ddd_app    | MySQL started
ddd_app    |
ddd_app    | > [email protected] db:create:dev /usr/src/ddd
ddd_app    | > cross-env NODE_ENV=development node scripts/db/create
ddd_app    |
ddd_app    | /usr/src/ddd/scripts/db/create.js:25
ddd_app    |   if (err) throw err;
ddd_app    |            ^
ddd_app    |
ddd_app    | Error: Access denied for user ''@'172.18.0.5' (using password: NO)
ddd_app    |     at Packet.asError (/usr/src/ddd/node_modules/mysql2/lib/packets/packet.js:708:17)
ddd_app    |     at ClientHandshake.execute (/usr/src/ddd/node_modules/mysql2/lib/commands/command.js:28:26)
ddd_app    |     at Connection.handlePacket (/usr/src/ddd/node_modules/mysql2/lib/connection.js:408:32)
ddd_app    |     at PacketParser.onPacket (/usr/src/ddd/node_modules/mysql2/lib/connection.js:70:12)
ddd_app    |     at PacketParser.executeStart (/usr/src/ddd/node_modules/mysql2/lib/packet_parser.js:75:16)
ddd_app    |     at Socket.<anonymous> (/usr/src/ddd/node_modules/mysql2/lib/connection.js:77:25)
ddd_app    |     at Socket.emit (events.js:315:20)
ddd_app    |     at addChunk (_stream_readable.js:295:12)
ddd_app    |     at readableAddChunk (_stream_readable.js:271:9)
ddd_app    |     at Socket.Readable.push (_stream_readable.js:212:10) {
ddd_app    |   code: 'ER_ACCESS_DENIED_ERROR',
ddd_app    |   errno: 1045,
ddd_app    |   sqlState: '28000',
ddd_app    |   sqlMessage: "Access denied for user ''@'172.18.0.5' (using password: NO)"
ddd_app    | }
ddd_app    | npm ERR! code ELIFECYCLE
ddd_app    | npm ERR! errno 1
ddd_app    | npm ERR! [email protected] db:create:dev: `cross-env NODE_ENV=development node scripts/db/create`
.
.
.

I get this error whether I set "DDD_FORUM_DB_HOST" to localhost or leave blank. I'm sure I'm probably not setting up the .env file correctly or something.

Would anyone have any idea why this might be happening?

Controller validation with Joi

I've been trying to follow this repositories way of doing things, and I have to say it's blowing my mind on how sleek this implementation is. I noticed that there is no proper validation of the HTTP request data such as using Joi or yup.

I know that there is a validation mechanism in the UseCase class CreateUserUseCase.ts when creating the ValueObjects, but I felt like there needs to be one layer of validation in a higher level of the architecture which sits near the Infrastructure Layer (most outer layer).

I tried to implement it in my project using ddd-forum structure, and I would like anyone's opinion in the implementation if it makes sense and is the correct way of doing things.

shared/core/AppError.ts

....

export class ValidationError extends Result<UseCaseError> {
  public constructor(error: any) {
    super(false, {
      message: `A validation error occured`,
      error: error,
    } as UseCaseError);
    logger.info(`[AppError]: A validation error occured`);
    logger.error(error);
  }
}

shared/core/Validatior.ts

import Joi from "joi";

import { Result, left, right, Either } from "../../shared/core/Result";
import { ValidationError } from "./AppError";

type ValidationResponse = Either<Result<ValidationError> | Result<any>, Result<void>>;

export class Validator {
  public static async validate(
    schema: Joi.Schema,
    dto: any,
  ): Promise<ValidationResponse> {
    try {
      await schema.validateAsync({ ...dto });
      return right(Result.ok<void>());
    } catch (err) {
      const error: Joi.ValidationError = err;
      return left(Result.fail<void>(error.details[0]["message"])) as ValidationResponse;
    }
  }
}

src/modules/users/useCases/createUser/CreateUserController.ts

....
async executeImpl(req: DecodedExpressRequest, res: express.Response): Promise<any> {
    let dto: CreateUserDTO = req.body as CreateUserDTO;
    dto = {
      name: TextUtils.sanitize(dto.name),
      email: TextUtils.sanitize(dto.email),
    };

    const validationSchema = Joi.object({
      name: Joi.string().min(2).max(10).strip().required(),
      email: Joi.string().email({ minDomainSegments: 2 }).required(),
    });

    try {
      const validation = await Validator.validate(validationSchema, dto);
      if (validation.isLeft()) {
        const error = validation.value;
        return this.unprocessable(res, error.errorValue());
      }
      ......
  }
....

What to do with persistence data if business rules change

Hi,

An example here is the PostTitle value object. It has rules that a title must have a min of 2 and a max of 85.

Lets say the business decides to change the min to 5 on any new posts.

When we pull existing data from the persistence store any post with a title that's only 2 characters long will fail when we're converting the raw object in the PostMapper to the domain. What should happen here?

Injection of Domain Services

Hello, I've been reading your Upvote Post useCase and i came across domain Services. My question is since they belong to the domain Layer, and Use Cases use their concrete Classes without any abstract interfaces, is there any reason we are injecting them in the useCase constructor and not directly importing them as we do with Entities/Aggregates?

[Question]: userId property in User class (also postId in Post etc.)

A few questions basically:

  1. What is the purpose of the userId/postId properties when there is already one called id in the AggregateRoot class? Especially since they are initialized with the same value.
  2. Why is UserId an Entity?
  3. Why does its create static method return a Result even though it will always be a success?

Combined error in result issue

ForEach Won't return the error.

Can change to for loop

 public static combine(results: Result<any>[]): Result<any> {
    for (let key in results) {
      if (results[key].isFailure) {
        return results[key];
      }
    }
    return Result.ok();
  }

Question about extending domain event

Hello @stemmlerjs,

I learn many things from your solidbook and direct me here. Not sure where I should post a question. Let me know if there's a specific channel/platform for QnA.

I look at the code on how the event is prepared. I believe this is the place, in the aggregate root.

post.addDomainEvent(new PostCreated(post));

It says, when the aggregate id is not null, then raise PostCreated event. It is created inside the create method.

However the create method is called from many places, like when you really want to create a new post from the use case, when you edit a post, even from the PostMap to convert DTO to domain object.

I mean like:

if the id from args is null, then it's considered a new post
otherwise it could be an update.

Now, let's take another case, we need to track another event like PostUpdated event, we need it only when the post is edited by the author, not by upvote/downvote.

By the snippet above, I assume that, when the id is not null, then we can prepare for the PostUpdated event.

When we execute a getPostByPostId from repo, it calls the PostMap to convert the PostModel to domain model (and prepare the PostUpdated event).

Since the getPostByPostId is called from the UpvoteComment use case, it's preparing the PostUpdated event that I don't expect.

How do you handle this kind of scenario?

Why usecase controller is extending an infrastructure class?

App layer shouldn't be aware of infrastructure implementation, why we extend an infrastructure implementation class?

I thought that the usecase controller shouldn't know about if the usecase is implemented through websocket, HTTP, TCP/IP or Message Broker; this makes the BaseController coupled to the HTTP protocol (VERBS).

I think the idea would be to return always a Result, develop a infrastructure implementation like HTTPRequestHandler (which requires a UseCase's Controller) with HTTP verbs as methods, and then reuse it by Express, Koa... even at the same runtime or not. (progressive library migration, performance testing...)

We could create another TCPRequestController, MQRequestController, which all of them requires the same as HTTPRequestHandler.

export interface IRequestHandler<RQ> {
    req: RQ;

    handle<RQ>(req: RQ, controller: any): void;
    handle<RQ, RS>(req: RQ, controller: any, res: RS): void;
}
export interface IHttpRequestHandler<RQ, RS> extends IRequestHandler<RQ> {
  res: RS;
}
export abstract class HttpRequestHandler<RQ, RS> implements IHttpRequestHandler<RQ, RS>/*, IHttpRequestHandlerResponses*/ {
  req: any;
  res: any;
  controller: any;

  public handle<RQ, RS>(req: RQ, controller: any, res?: RS): void {
    this.req = req;
    this.res = res;
    this.controller = controller;

    this.controller.call();
  };

  abstract jsonResponse(): any;
}

HttpRequestHandler could have all HTTP verbs implemented or just leave the responsability for the ending library implementation:

export interface IExpressRouteHandler extends IHttpRequestHandler<express.Request, express.Response> {}

export abstract class ExpressRouteHandler extends HttpRequestHandler<express.Request, express.Response> {
  req: express.Request;
  res: express.Response;

  public static jsonResponse(res: express.Response, code: number, message: string) {
    return res.status(code).json({ message });
  }

  public ok(res: express.Response, dto?: string) {
    if(!!dto) {
      return res.status(200).json(dto);
    } else {
      return res.sendStatus(200);
    }
  }

  public created<T>(res: express.Response) {
    return res.sendStatus(201);
  }

  public clientError(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 400, message ? message : 'Unauthorized');
  }

  public unauthorized(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 401, message ? message : 'Unauthorized');
  }

  public paymentRequired(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 402, message ? message : 'Payment required');
  }

  public forbidden(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 403, message ? message : 'Forbidden');
  }

  public notFound(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 404, message ? message : 'Not found');
  }

  public conflict(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 409, message ? message : 'Conflict');
  }

  public tooMany(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 429, message ? message : 'Too many requests');
  }

  public todo() {
    return ExpressRouteHandler.jsonResponse(this.res, 400, 'TODO');
  }

  public fail(error: Error | string) {
    console.log(error);
    return this.res.status(500).json({
      message: error.toString()
    });
  }
}

I tried to deal with this but I've found a concern:
What kind of response should the usecase controller return?

Here's my alpha repository https://github.com/imsergiobernal/efecto-kettlebell

`null` values in the `Map.toDomain(c)` return.

hi @stemmlerjs - first of all thanks for some great blog posts & this repo.

There are multiple places where we would get a list from the repo and then do:

return resultFromRepo.map(SomeMap.toDomain);

We are therefore returning (Entity | null)[] is this expected? At what layer in our application shall we filter / clean this list to return only valid values?

createUserController doesn't handle errors correctly.

Found a small bug where conflict function is not passing the response object.
switch (error.constructor) { case CreateUserErrors.UsernameTakenError: return this.conflict(error.errorValue().message) case CreateUserErrors.EmailAlreadyExistsError: return this.conflict(error.errorValue().message) default: return this.fail(res, error.errorValue().message); }

Encountered some errors while trying to run locally. (TS2339: Property 'body' does not exist on type 'DecodedExpressRequest'.)

Is this an existing issue?

Environment:

$ npm --version && node --version
6.14.16
v12.22.12
$ git status && git log -1
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   ../../Dockerfile
        modified:   ../../package-lock.json
        modified:   package-lock.json
        modified:   yarn.lock

no changes added to commit (use "git add" and/or "git commit -a")
commit 24df03e5e3f617065855266fcf7250425f6e53b5 (HEAD -> 
master, origin/master, origin/HEAD)
Merge: 3aa0b8d 24b4833
Author: Khalil Stemmler <[email protected]>
Date:   Sat Jun 10 01:59:04 2023 -0400

    Merge pull request #123 from stemmlerjs/ids-should-be-value-objects

    Changed ids to value objects
$ docker ps 
CONTAINER ID   IMAGE          COMMAND                  CREATED      STATUS       PORTS
   NAMES
5533134a011e   adminer        "entrypoint.sh php -ā€¦"   4 
days ago   Up 2 hours   0.0.0.0:8080->8080/tcp
   ddd-forum_adminer_1
1af89adb8716   mysql:latest   "docker-entrypoint.sā€¦"   4 
days ago   Up 2 hours   0.0.0.0:3306->3306/tcp, 33060/tcp   ddd_forum_mysql
eb086eec08f7   redis:latest   "docker-entrypoint.sā€¦"   4 
days ago   Up 2 hours   0.0.0.0:6379->6379/tcp
   ddd_forum_redis

Error

...
[0] [nodemon] watching extensions: ts,js,pug,css        
[0] [nodemon] starting `ts-node ./src/index.ts`
[0] 
[0] D:\Workspace\Development\Skand\backend\refs\ddd-forum\node_modules\ts-node\src\index.ts:245
[0]     return new TSError(diagnosticText, diagnosticCodes)
[0]            ^
[0] TSError: āØÆ Unable to compile TypeScript:
[0] src/modules/users/useCases/createUser/CreateUserController.ts(19,34): error TS2339: Property 'body' does not exist on type 'DecodedExpressRequest'.
[0]
[0]     at createTSError (D:\Workspace\Development\Skand\backend\refs\ddd-forum\node_modules\ts-node\src\index.ts:245:12)
[0]     at reportTSError (D:\Workspace\Development\Skand\backend\refs\ddd-forum\node_modules\ts-node\src\index.ts:249:19)
[0]     at getOutput (D:\Workspace\Development\Skand\backend\refs\ddd-forum\node_modules\ts-node\src\index.ts:357:34)
[0]     at Object.compile (D:\Workspace\Development\Skand\backend\refs\ddd-forum\node_modules\ts-node\src\index.ts:415:32)
...

Atomically send event and update the database `outbox pattern`

In some parts of the code, like this one :
ddd-forum/src/modules/users/useCases/createUser/CreateUserUseCase.ts :

const userOrError: Result<User> = User.create({
        email, password, username,
      });

      if (userOrError.isFailure) {
        return left(
          Result.fail<User>(userOrError.getErrorValue().toString())
        ) as Response;
      }

      const user: User = userOrError.getValue();

      await this.userRepo.save(user);

      return right(Result.ok<void>())

    } catch (err) {
      return left(new AppError.UnexpectedError(err)) as Response;
    }

we both need to send a domain event (here User.create will dispatch a UserCreated event) and update the database (here, await this.userRepo.save(user);).
But these operations need to be atomic to avoid inconsistencies.
In this example, once UserCreated is dispatched, it is then listened by the forum module which will create a new member based on this event.
This means that if this database persist operation fails :

await this.userRepo.save(user);

we will have a member created without its associated user.

To avoid that, the outbox pattern must be implemented. Here is a reference explaining the problem and the solution : https://microservices.io/patterns/data/transactional-outbox.html .

What do you think ?

Enabling strict in tsconfig breaks many things

Enable the strict mode in the tsconfig and check the errors. Seems we are not adhering to type safety in many places. I'm a novice and learning from your repo. Not really sure whether enabling strict mode is really needed or not.

Set max nesting distance on threads

Currently, if we have a thread with a lot of comments nested in on it, the nesting will continue and continue to push further to the right.

Hypothetically, it could get to the point where nested threads are completely unreadable because they're so squished to the right.

We should have a max nesting distance.

There's a recursive function responsible for the nesting, but we should pass in the current index in order to prevent from nesting any further.

Maybe 5 or 6 layers deep.

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.