Open gwesterman opened 1 year ago
Interesting use case. I think a separate QueryDTO would be the cleaner solution. Especially if it needs extra type conversion.
So, query params are all strings by default. For basic types, type conversion should be simple.
?q=foo&q=bar&bool=true&count=10
should give
{
q: ['foo', 'bar'],
bool: true,
count: 10,
}
But what if the user defines more complex types, e.g., relations etc.?
model Profile {
user User
}
I too think that a dedicated QueryDTO would make a lot of sense.
I do not however think, that the QueryDTO has to cover relations, as those would be available under their own path using a dedicated QueryDTO.
/profiles/
would accept query parameters defined in QueryProfileDTO
/profiles/123/followers
would also accept QueryProfileDTO
(followers are other profiles/users)
/profiles/123/galleries
would accept QueryGalleryDTO
The /profiles/
endpoint can return a ResultDTO that includes the actual data requested, information about pagination and sorting and also additional links, pointing to the linked resources that can be queried separately.
But I would consider all of this API implementation details, that do not directly concern this library.
Mapping multiple DTOs into a single QueryDTO can get quite messy if the DTO properties are not unique.
Sometimes you want endpoints that take relations into consideration of course, as this would reduce the amount of queries needed, but in this case I would manually create a dedicated DTO, which can also be done using Nest's PartialType() mapping utility function, based on the QueryDTOs in question.
Since the QueryDTOs, the new QueryDTO is based on, have all the transformation decorators in place these custom QueryDTOs should be able to make use of them, too.
My question above was about how to handle relations and complex types. Since query params can only be simple strings, I think the fields meta
and user
in the example below should just be ignored. So, only fields of basic types (string, number, boolean) can be queried.
The following model:
model Profile {
id String @id
age Int
isAdmin Boolean
tags String[]
meta Json
user User @relation(...)
}
could result in something like this:
export class QueryProfileDto {
@ApiProperty({
required: false,
nullable: true
})
@IsOptional()
@IsString()
id?: string;
@ApiProperty({
type: 'integer',
format: 'int32',
required: false,
nullable: true
})
@IsOptional()
@IsInteger()
@Type(() => Number)
age?: number;
@ApiProperty({
required: false,
nullable: true
})
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === '1')
isAdmin?: boolean;
@ApiProperty({
isArray: true,
required: false,
nullable: true
})
@IsOptional()
@IsArray()
@IsString()
tags?: string[];
}
Possible query url:
?tags=foo&tags=bar&age=40&isAdmin=1
I think | null
is not necessary, because unlike in the request body you can't pass null
. Either the query param is defined or not.
Maybe tags
also needs some custom @Transform
, because a single ?tags=foo
may result only in a string
, but not string[]
.
Currently, validation decorators are only added to
CreateDTO
andUpdateDTO
, the types of DTOs which are used to send data to the API, unlikeEntity
andplain DTO
, which are returned by the API and therefore normally do not need validation decorators.Scenarios
Usage of Query() together with a DTO
I use the
plain DTO
to send data to the API, but since it lacks validation decorators the necessary type transformations are not applied, forcing me to transform the values manually.The
plain DTO
is used to type the query parameters of my GET endpoints. This allows consumers of the API to filter results using a combination of any of the DTO's fields. This is done by mapping theplain DTO
to a QueryDTO using Nest's PartialType() utility function, which creates a copy of theplain DTO
with all fields now optional.Since
QueryDTO
is now based on theplain DTO
it always reflects the table and the API consumer can apply whatever filter combination they want.Other possible scenarios
Nest's mapping utility functions can also be used to create more fine-grained DTOs or Entities for specific scenarios, for instance by using PickType() to trim the DTO down to what is needed.
For this to work well though, the base DTOs/Entities that are used together with these utility functions need to have class validators.
I propose an additional configuration flag that, if set to true, causes these validators to be added to both the
plain DTO
andEntity
.Problem when using Query() together with a DTO
Transformation using validation decorators only works when using Query(), if
enableImplicitConversion
enabled:This is not optimal, as there are downsides to using
enableImplicitConversion
.Solution
Explicitly adding a transform decorator to the property does the trick:
Note
Instead of adding these decorators to
plain DTO
andEntity
, a new DTO type could be introduced, which includes all fields theplain DTO
possesses, but also adds the required validation and transform decorators.