stemmlerjs / ddd-forum Goto Github PK
View Code? Open in Web Editor NEWHacker news-inspired forum app built with TypeScript using DDD practices from solidbook.io.
Home Page: https://dddforum.com
License: ISC License
Hacker news-inspired forum app built with TypeScript using DDD practices from solidbook.io.
Home Page: https://dddforum.com
License: ISC License
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:
Instead, I think what I'd prefer is to set this up so that it's like this:
Docker
User-space (start in a console)
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.
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?
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:
ddd-forum/src/modules/forum/useCases/post/createPost/CreatePost.ts
Lines 50 to 54 in f78ebf0
Here's the resulting business rules in the domain layer for a PostTitle
:
ddd-forum/src/modules/forum/domain/postTitle.ts
Lines 22 to 41 in f78ebf0
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.
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.
ddd-forum/src/modules/forum/domain/post.ts
Line 242 in 9aceab2
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?
@stemmlerjs hope you are well!
I would like to know why in repos you have chosen to try catch and throw Errors.
Yet in most other places you are returning a Result.fail
or Result.ok
.
Thanks!
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!
Posts on the front page should be paginated for when it grows really large! :)
When the user isn't logged in, if they hover over any of the vote options, a tooltip should appear saying "Want to vote? You need to sign up here".
The word "here" should link to /join
.
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());
}
......
}
....
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?
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
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.
@all-contributors please add @denneulin for Development
After generating a graph of module deps, I noticed these circular dependencies:
This obviously breaks the acyclic dependency principle.
@stemmlerjs Is this somehow acceptable from your POV?
I was wondering why you chose UserId as Entity, i thought throughly and felt a bit more inclined towards UserId should be a ValueObject. And I found couple of articles regarding that:
3 Reasons to Model Identity as a Value Object
Entity vs Value Object: the ultimate list of differences
Could you please share your opinions, why did you choose Entity ?
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?
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?
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?
@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:
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?
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?
Sorry for the n00b question but why the front-end "layer" does not apply the same concepts of Use Cases, Repo, Entity, etc.
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.
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.
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); }
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)
...
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):
ddd-forum/src/modules/users/domain/user.ts
Lines 93 to 100 in ea39350
ddd-forum/src/modules/users/useCases/createUser/CreateUserUseCase.ts
Lines 28 to 44 in ea39350
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.
My question is:
Thanks a lot for what you're doing here and on "white-label". Very helpful to set right from the beginning new projects!
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.
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
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 ?
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
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?
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.
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!
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.
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
A few questions basically:
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?
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);
};
We can build out an admin dashboard to manage what happens on the site:
As an admin user, I can:
etc... Open for suggestions :)
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();
}
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 ?
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;
}
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:
On downvote:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.