Hello 👋 even though I know this has been requested numerous times already, I'd like to share my arguments as to why I think that `Ecto.Repo` needs additional callbacks, and how the lack of it hurts the quality of my code and unnecessarily performs more computations than it should.
I hope some decision makers can truly take a little time to consider these points as they have been a source of frustrations, even though I am fond of Ecto.
1. callback to fill virtual fields on fetched schemas
Say a schema defines the following fields: a duration stored in DB, and two virtual fields to easily access the amount of hours and minutes separately:
```
field :duration, :integer
field :duration_hours, :integer, virtual: true
field :duration_minutes, :integer, virtual: true
```
Another example would be a virtual field `:full_name` that concatenates the `:first_name` and `:last_name`.
Without a callback, I have to explicitly fill the virtual fields after each `Ecto.Repo` call that loads data:
```
# some query
|> Repo.all()
|> # fill the virtual fields
```
There are two problems with that:
First, it's error-prone: you have to systematically call the function to fill the virtual field after *each* `Ecto.Repo` call that loads data. Why after every single call? Because of preloads (associations/nested entities should have their virtual fields filled)! (especially when preloads are passed as options to the context function) It's error-prone because one can forget to add the call. It is also obviously very repetitive code.
Secondly, as we might preload associations, after the `Ecto.Repo` call we have to recursively find all the entities and check if any of them need to have their virtual fields filled. These seem unnecessary computations, as a hook on the field would avoid us from looking recursively in the results.
```
# some query
|> Repo.all()
|> fill_virtual_fields()
```
Another drawback is that the context must be aware of those virtual fields, while a callback could abstract this away and details kept in the schema.
2. callback to apply on fetched data
I am designing the permission system for my app, and one idea was to automatically filter out all the unauthorized data returned from a `Ecto.Repo` call.
Say a user wants to retrieve a list of items. Those items may be accessible or not to this user according to user-defined access rules. I think it'd be really nice if I could filter out items for which the current user doesn't have access to in a callback that I'd hook after the `Ecto.Repo` call.
I've read briefly on the motivation of not having such a hook (because it was actually there a long time ago); one main reason is that an `Ecto.Repo` call can be part of a transaction ; it would then be a very bad place to e.g. send out some confirmation email. But this can be documented as a caution to use such a hook.
So after `Ecto.Repo` call that loads data, I now have:
```
# some query
|> Repo.all()
|> fill_virtual_fields()
|> filter_out_unauthorized()
```
Again, as such protected entities can be nested, because of preloads for example, I have to add the call after *each* `Ecto.Repo` call.
Left without such hook, I have to trust the developer that he or she won't forget to call the function that filters out the fetched entities based on current user's permissions. When we talk about permissions and data leakage, this is just not an acceptable solution, and I have to resort to something else.
One could maybe say that I shouldn't even be fetching entities that are unauthorized in the first place; however, I can't modify the query and add joins and such, as the rules are stored in tables that are hard to join with, and it'd add a lot of complexity and lots of DB changes. A hook can easily *guarantee* me that all fetched data and nested data are authorized.
The idea was to recursively filter and call e.g. a `permit?/2` function, using pattern matching:
```
def
permit?(current_user, %Location{} = location)
def permit?(current_user, %Resource{} = resource)
def permit?(current_user, %Event{} = event)
# etc.
```
It seems the two problems are the same, but I have intentionally separated them into 2 points as those should be solved differently. The issue regarding virtual fields should be solved through a hook on the field (to avoid unnecessary recursivity). The second issue should be solved through a callback on the results of the `Ecto.Repo` call.