Add "before" and "after" hooks to ReadModels and, optionally, to Commands
See original GitHub issueFeature Request
Description
Although the proposed solution for this issue covers several use cases, I’m going to explain the need for this with one use case that has more priority:
Right now, we have the possibility to authorize reading a ReadModel only to specific roles. This is great but not enough for any common application. Consider the following ReadModel:
@ReadModel({
authorize: [User]
})
class UserProfile {
//...
}
Every client that is logged in with the role “User” can access any instance of UserProfile. This is normally incorrect and insecure. What we really want it to allow accessesing the instance only if the current user is the owner of that profile.
That is currently impossible in Booster, but needed for any application (for example Mayday would need this)
In Commands, this problem is not that important, because the user can check if the current user can access that command inside the command handler and throw an exception if they can’t. However, ReadModels doesn’t have any place to add these kinds of checks, and that’s what this this issue is for.
Possible Solution
The proposed solution here adds a general mechanism that solves the above problem, but can also be used to other kind of use cases, like transforming data before sending it to the client, adding extra filters before executing the query, etc.
Proposal: Add two “hooks” to read models:
- before hook: Executed before the query is made. It receives the
FilterFor
object with all the filter the user has specified and the currentUserEnvelope
. It can either throw exceptions or return a newFilterFor
object to be used in the actual query sent to the provider. Very useful to reject requests under some conditions or further refine the filter before doing the actual query. - after hook: Executed after the query is made. It receives the array of ReadModels that are about to be sent to the user and the current
UserEnvelope
. It can throw exceptions or return the actual array of read models to be sent back to the user. Very useful for data transformations related to the presentational aspect.
Example:
@ReadModel({
authorize: [User]
})
class UserProfile {
@Before // <--- This is a "before" hook
public static checkID(filter: FilterFor<UserProfile>, currentUser?: UserEnvelope): FilterFor<UserProvile> {
// Role "User" can only access their profile
if (filter.id !== currentUser.id) {
throw new NotAuthorizedError("...")
}
return filter
}
@After // <--- This is an "after" hook
public static useProperCurrency(profiles: Array<UserProfile>, currentUser?: UserEnvelope): UserProfile {
// We store all payments in cents of euros, but the user can choose the currency they want their money to be deployed in
for(profile of profiles) {
switch(profile.currency) {
case 'dollars':
profile.balance = toDollars(profile.balance)
break
case 'yen':
profile.balance = toYens(profile.balance)
break
}
}
return profiles
}
//...
}
Alternative sintaxes
The sintax proposed in the above example is one way of doing this. Let’s call it way A) Using decorators. There are two extra alternative ways:
Syntax B) Specify hooks in the ReadModel decorator
@ReadModel({
authorize: [User]
before: [UserProfile.checkID]
after: [UserProvile.useProperCurrency]
})
class UserProfile {
public static checkID(filter: FilterFor<UserProfile>, currentUser?: UserEnvelope): FilterFor<UserProvile> {...}
public static useProperCurrency(profiles: Array<UserProfile>, currentUser?: UserEnvelope): UserProfile {...}
// ...
}
Syntax C) User a fixed name for hooks
@ReadModel({
authorize: [User]
})
class UserProfile {
public static before(filter: FilterFor<UserProfile>, currentUser?: UserEnvelope): FilterFor<UserProvile> {...}
public static after(profiles: Array<UserProfile>, currentUser?: UserEnvelope): UserProfile {...}
// ...
}
Discussion about syntaxes
Of all the syntax options, C) is the most restrictive, as only allow one hook per read model, while you might want to have several ones. Also the method names “before” and “after” doesn’t show the business intention behind that code
Option A) seems pretty nice and allows you to both name your hooks as you want and have more than one “before” hook if you need to. The only drawback (if seen this way) is that all the hooks must be defined inside the ReadModel class, and you might have some generic hooks that would like to be reused in several read models.
Finally Option B) is the most flexible one: It has all the advantages of A) and it also allows you to set as hooks any function that might be defined anywhere, even inside a third party package. For example:
import { toXML } from "xml-transformation-hooks"
@ReadModel({
authorize: [User]
before: [UserProfile.checkID]
after: [UserProvile.useProperCurrency, toXML] // Now we return data in XML, for example
})
class UserProfile {
public static checkID(filter: FilterFor<UserProfile>, currentUser?: UserEnvelope): FilterFor<UserProvile> {...}
public static useProperCurrency(profiles: Array<UserProfile>, currentUser?: UserEnvelope): UserProfile {...}
// ...
}
Another option is to allow both option A and B. This needs to be discussed.
Aditional information
Adding this hooks to Commands would be a nice to have, but it is not a priority at all, because the handle
method can do everything the hooks allow to do in ReadModels.
Issue Analytics
- State:
- Created 2 years ago
- Comments:5 (5 by maintainers)
Top GitHub Comments
Thanks for the explanation @alvaroloes! Then we’ll need to edit the sign-up lambda to accept more parameters than role: https://github.com/boostercloud/rocket-auth-aws-infrastructure/blob/main/src/lambdas/sign-up.ts. Right now
UserEnvelope
only allowsid
,username
androle
, maybe we need to modify that also.Ok if that’s the case (having different ways of identifying the “owner”) then we should go with the
@before
hook proposal here (option B sounds good to me).About solution number 2:
ownerId
fieldThis was already merged on v0.17.0 (read-models) and v0.18.0 (commands)