question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add "before" and "after" hooks to ReadModels and, optionally, to Commands

See original GitHub issue

Feature 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:

  1. before hook: Executed before the query is made. It receives the FilterFor object with all the filter the user has specified and the current UserEnvelope. It can either throw exceptions or return a new FilterFor 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.
  2. 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:closed
  • Created 2 years ago
  • Comments:5 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
juanjomancommented, May 27, 2021

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 allows id, username and role, 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:

  1. In the case that it exists, for that one. Otherwise none. This is because you could update an entity that it’s “not yours”
  2. Those that match the query + match the ownerId field
0reactions
juanjomancommented, Jul 6, 2021

This was already merged on v0.17.0 (read-models) and v0.18.0 (commands)

Read more comments on GitHub >

github_iconTop Results From Across the Web

What are Cucumber Hooks And How to Use ... - Tools QA
Cucumber supports hooks, which are blocks of code that run before or after each scenario. You can define them anywhere in your project...
Read more >
cucumber-js/hooks.md at main - GitHub
Hooks can be conditionally selected for execution based on the tags of the scenario. const {After, Before} = require('@cucumber/cucumber ...
Read more >
Cucumber Hooks - Baeldung
Let's first look at the individual hooks. We'll then look at a full example where we'll see how hooks execute when combined.
Read more >
Features - Booster Cloud Documentation
Every Command and ReadModel in Booster has an authorize policy that tells Booster who can access it. ... Adding before hooks to your...
Read more >
Decoupling Logic with Domain Events [Guide] - Khalil Stemmler
In this article, we'll walk through the process of using Domain Events to clean up how we decouple complex domain logic across the ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found