domain-driven-hexagon
domain-driven-hexagon copied to clipboard
Relationship between `UserRepository` and `TypeormRepositoryBase`
While working through your database implementation, I was wondering why UserRepository
does not use TypeormRepositoryBase.findOne
?
https://github.com/Sairyss/domain-driven-hexagon/blob/0b55c3c290f197c920cb563add62e04f83faae96/src/modules/user/database/user.repository.ts#L35-L43
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?
https://github.com/Sairyss/domain-driven-hexagon/blob/0b55c3c290f197c920cb563add62e04f83faae96/src/modules/user/database/user.repository.ts#L61-L70
- This is just an example to show that you can use TypeOrm repos if you have specific queries that can't be simply queried using
this.findOne()
. In this case either one works, choose any that you like more. -
prepareQuery
is meant for constructing a query from value objects, since you need to extract values from them. In this example use case we allow querying users only by ID. If you want to allow querying users by, lets say, country, you'd have to add
if (params.address?.country) {
where.country = params.address.country
}
Keep in mind that this is an example implementation and might not be the best solution for all use cases.
Thank you!
To me it feels like in prepareQuery
I would have to duplicate most of the logic from OrmMapper.toOrmProps
, only that each line will be wrapped in a if (params...) { ... }
clause. Maybe there is a way to re-use the logic from the mapper and use a more abstract if
clause?
The problem is that mapper requires all (or almost all) fields to be present to save the entity, but in a query all fields are optional. Also a query may have different forms, not just simple ones like discussed here. Here is an example of a prepareQuery
from one of my projects:
...
if (status) {
receiver.status = status;
sender.status = status;
}
if (params?.createdBefore) {
receiver.createdAt = LessThanOrEqual(params.createdBefore);
sender.createdAt = LessThanOrEqual(params.createdBefore);
}
return [sender, receiver];
As you can see this would be impossible to map just by using a mapper, since you can have operators in your query like <= or >=, or you may need to query in a joined tables (like sender
and receiver
in an example above)
Ah, I see! Now I understand why one would want a more flexible way of preparing queries.
My worry, however, is that this implementation suggests a generality that is not there. The repository exposes a method
findOne(params: QueryParams<EntityProps>)
where
type QueryParams<EntityProps> = DeepPartial<BaseEntityProps & EntityProps>
To anyone who has not studied prepareQuery
in detail, this looks like a method that allows one to search the repository using arbitrary combinations of entity properties.
But when I call, say, UserRepository.findOne({ country: 'Germany' })
then the country param will be silently deleted and the result will be the same as if I had called UserRepository.findOne({ })
. That could lead to nasty bugs.
My "solution" was to remove the find...
methods (except for findById
, which works the same for every entity) from TypeormRepositoryBase
and resort to custom implementations in each repository, in the spirit of UserRepository.findOneByCountry(country: string)
.
That's right, you have to adjust either a type to accept only the parameters specified in prepareQuery
, or modify Repository to your liking. I'll think how to improve it in the future so it's less confusing.