I have a large business management app that uses a lot of database tables and must do complex operations that must touch several different entities, for example an administrative operation should have an impact in the accounting reports, etc. The point is, even though almost every entity has its own CRUD managed by different controllers, often I need to chain an operation over one entity to another operation in a seemingly unrelated service, which is located in a separate module altogether. Even so, there's times in which directly triggering the second operation must reflect on the first one being executed also. Initially I simply used DI in the services to trigger the external methods that needed to be called, but when introducing these bidirectional relationships I quickly started running into circular dependencies.
I can try and solve this through forwardRef or even some refactoring, but this is a recurring problem and I would like to find a solution that lets me "chain" these operations without much overhead. To this moment, I thought of @nestjs/event-emitter, but it wouldn't let me await for the external operations, and I even thought about using pipes for this, but I don't think this to be a clear solution either.
Anyone ran into a similar pattern and has an idea on how to face this?
#Avoid coupling when chaining seemingly unrelated operations
1 messages · Page 1 of 1 (latest)
Event emitter's emitAsync lets you await all emitted events (given their handlers return a promise) and it migt just be what you need
For more complex stuff, see @nestjs/cqrs
Thanks a lot! I didn't know about the emitAsync method, as is isn't in Nest's docs, and thought the underlying library worked like mitt, without asynchronous events. As for cqrs, I'll check it out and see if I can leverage some of its functionalities
Turns out, event-emitter won't work with request-scoped providers. I forgot to mention that my app is multi-tenanted and uses durable providers to retrieve different DB connections, so events are discarded :(. I could have worked by using ModuleRef in the listeners, but I think it will require less overhead to just use it in the "emitters". Thanks for the idea anyways!
yeah, request scope is pain to work with, especially when it's rooted deep withing the app
I may offer https://www.npmjs.com/package/nestjs-cls as a remedy for the request scope problem if you think it can help your design
I wish I had come up with this library earlier! When reading about how to migrate my app to a multi-tenant architecture, the only similar solution I found was async-hooks, which I read was pretty unstable. Unfortunately, now I don't think I have the time to refactor this into using this library, so ModuleRef will have to do for now.
AsyncLocalStorage (which is based on ansyc_hooks) actually only became stable as of recently, so it makes sense you didn't find it earlier
Have you thought about using observables?
Sounds like you need to be able to mutate thing(s) and then propagate that change both up and down the chain right?
@cinder plaza how would that be? Sorry, I don't have much experience with Observables (other than convetring them to promises haha). I think it might do the trick though, but still needs a refactor in order to discard the request-scoped providers
Well, it's not gonna be copy-pasta based on what (I think) you need
Can you explain the flow?
What I need, in most cases, works as following:
- A durable request-scoped middleware assigns a tenantId to the Request object
- A controller method is called, applies validation pipes and passes the dto to its service
- The service, which injects a durable request-scope provider which uses said tenantId to provide the corresponding DataSource, operates on a specific entity
4 (not always, but in many cases) Another service, in a different module, with the same tenant-specific DataSource, is used to perform another operation on a different Entity. Often, this second service can receive in its method an EntityManager instance, which is helpful when working with transactions (rolling back operation 1 when operation 2 fails). However, this second service cannot discard this request-scope injected provider because it needs to be able to run as its own, when called separately. - The return value of the second service is awaited for in the first service, and appended to the response which is passed to the controller and then sent in the response body.
- Sometimes, service 2 will ALSO need to call the corresponding method from service 1, making the two endpoint calls "equivalent". This, when using normal DI, results in a circular dependency
I think I'm having the same issue. Although I've been getting past it by just directly calling the data layer in the services where I need it. But I'm guessing if you have a lot of business logic you won't want this duplicated.
CQRS looks quite neat actually