Description
First of all, thanks for the amazing lib. I love its potential! I'm fiddling with the groups functionality and I really like how it enables me to keep my "transformers" in on the class itself. When the model updates and I need to add another field, I write how that field will be visible to different "views" immediately instead of hunting down ten thousand transformer files.
Details and backstory
I'm trying to find a good pattern to transform data for sending an API response, but I'm finding limitations since the "groups" are doing the OR operator between them. This sort-of-a-feature-request and sort-of-a-question and sort-of-a-proposal at the same time.
For example, my User model has (at least) two "axes" on how we can categorize the response which includes the user of id 1
.
- How is user
1
related tome
? (Is1
actuallyme
, are we friends on the platform or are we strangers?) This gives us three categories: me, friend and stranger. - Am I fetching this user as part of a list (eg. search results, less info) or a single user (full profile info)? This gives us two categories: list and single.
Notice how the first three categories cover the entire spectrum of possible users. Also, it's not possible that a user is in two of the three categories at the same time -- they are disjunctive. Same with the last two.
Problem
The problem is that I cannot say that a certain field is visible only when fetching my own info (me) in the single view (single). If I say @Expose({ groups: ['me', 'single'] })
, that would mean me OR single, so fetching a stranger's info on the single view would include that field as well.
As the lib currently stands, to do what I want to do would mean having to create the Cartesian product of all categories. Instead of me, friend, stranger, list and single, I'd need to have me-single, me-list, friend-single, friend-list, stranger-single, stranger-list. The problem is that this list grows quickly and is very difficult to maintain.
Proposal 1
One way I see this possible is to introduce some sort of query builder for the AND/OR expressions.
@Expose({
groups: and(or('me', 'friend'), 'single') // ['me-single', 'friend-single']
})
classToPlain(User, user, { groups: ['me', 'single'] }) // matches
classToPlain(User, user, { groups: ['friend', 'list'] }) // doesn't match (requires single)
But this is a bit wonky and not very scalable. I like the second idea much better.
Proposal 2
The other way would be to make groups
a function instead. I think this is a more flexible approach and also probably much easier to implement in class-transformer
itself. Just pass in an array of groups and let the function (which returns a boolean) decide if it's included or not.
@Expose({
groups: groups => (groups.some(g => g == 'me') || groups.some(g => g == 'friend')) && groups.some(g => g == 'single'),
})
// usage same as previous
classToPlain(User, user, { groups: ['me', 'single'] }) // matches
classToPlain(User, user, { groups: ['friend', 'list'] }) // doesn't match (requires single)
Basically, the current implementation accepts groups
as string[]
and then just checks if there's an intersection:
My suggestion is that, if groups
satisfies typeof groups == 'function'
, we just call this.options.groups(groups)
. I haven't tested it at all, but it seems like that's the only thing that needs to change... I think. I'll give it a shot soon.
If would be up to developer to define more complex utility functions to enable syntax like in Proposal 1.