Skip to content

Introduction

When developing backend API services, DTOs are crucial for parameter validation and Swagger metadata generation. If DTOs can't be dynamically inferred, similar to type inference, then we still need to manually create them. As the business grows, complex relations between models make manually creating DTOs increasingly cumbersome.

Vona ORM dynamically infer and generate DTOs to eliminate redundant type definitions and boost development productivity.

DTO Tools

Vona ORM provides the following DTO Tools:

NameDescription
$Dto.getAnnotate the return result
$Dto.listAndCountAnnotate the return result with paging
$Dto.query/DtoQueryBaseAnnotate the query parameters
$Dto.queryPage/DtoQueryPageBaseAnnotate the query parameters with paging
$Dto.createAnnotate the Create parameters
$Dto.updateAnnotate the Update parameters
$Dto.aggregateAnnotate the return result of the aggregation operation
$Dto.groupAnnotate the return result of the grouping operation

DTO Usage

The following uses the Order/Product model as an example to demonstrate how to perform query operations on the main-details models.

1. Model Relations Definition

First, define a 1:n relation between the Order model and the Product model.

typescript
@Model({
  entity: EntityOrder,
  relations: {
    products: $relation.hasMany(() => ModelProduct, 'orderId', {
      columns: ['id', 'name', 'price', 'quantity', 'amount'],
    }),
  },
})
class ModelOrder {}

2. Creating the API Endpoint

Create a Controller and Provide a findAll Method.

typescript
class ControllerOrder {
  @Web.get('findAll')
  async findAll() {
    return this.scope.model.order.select({
      include: {
        products: true,
      },
    });
  }
}

3. Dynamically Inferring and Generating DTO

Since the result returned by this API is a main-details structure, we cannot simply use an EntityOrder array to annotate the return type. Instead, DTOs are used for dynamic infer and generation.

diff
+ import { $Dto } from 'vona-module-a-orm';

class ControllerOrder {
  @Web.get('findAll')
+ @Api.body(v.array($Dto.get(() => ModelOrder, { include: { products: true } })))
  async findAll() {
    return this.scope.model.order.select({
      include: {
        products: true,
      },
    });
  }
}
  • @Api.body: Annotates the return result
  • v.array: Annotates an array
  • $Dto.get: Used for dynamic DTO infer and generation

The DTO generated by $Dto.get uses a main-details structure, which Swagger/OpenAPI is as follows:

4. Encapsulating the DTO

We can also create a new DTO class to encapsulate the $Dto.get code for use elsewhere.

  1. In VSCode, use the Vona Create/Dto context menu to create a DTO code skeleton:
typescript
@Dto()
export class DtoOrderResult {}
  1. Encapsulate the DTO using inheritance:
diff
+ import { $Dto } from 'vona-module-a-orm';

@Dto()
export class DtoOrderResult
+ extends $Dto.get(() => ModelOrder, { include: { products: true } }) {}
  1. Now, let's refactor the previous code using DtoOrderResult:
diff
class ControllerOrder {
  @Web.get('findAll')
+ @Api.body(v.array(DtoOrderResult))
+ async findAll(): Promise<DtoOrderResult[]> {
    return this.scope.model.order.select({
      include: {
        products: true,
      },
    });
  }
}
  • Line 3: Directly annotating the type with v.array(DtoOrderResult)
  • Line 4: The method return type is Promise<DtoOrderResult[]>

Released under the MIT License.