@adocasts.com/dto

中文文档
0.0.10发布于 2025/06/23MITES Module近 30 天下载 6673

Easily make and generate DTOs from Lucid Models

原始使用文档
文档目录11 节
  1. Installation
  2. Generate DTOs Command
  3. Make DTO Command
  4. What If There Isn't An Account Model?
  5. Make Validator Command
  6. Generate Validators Command
  7. BaseDto Helpers
  8. Things To Note
  9. Example
  10. Example Validator
  11. Using Generated Validators

@adocasts.com/dto

Easily make and generate DTOs and validators from Lucid Models

gh-workflow-image npm-image license-image

Converting Lucid Models to DTO files can be a tedious task. This package aims to make it a little less so by reading your model's property definitions and porting them to a DTO. Will it be perfect? Likely not, but it should help cut back on the repetition needed to complete the task.

Installation

You can easily install and configure via the Ace CLI's add command.

node ace add @adocasts.com/dto
Manual Install & Configure

You can also manually install and configure if you'd prefer

npm install @adocasts.com/dto
node ace configure @adocasts.com/dto
Define DTO Import Path

The generated DTOs will use #dtos/* for relationship imports within the DTOs. As such, we recommend defining this import path within your package.json

"imports": {
  "#dtos/*": "./app/dtos/*.js"
}

Generate DTOs Command

Want to generate DTOs for all your models in one fell swoop? This is the command for you!

node ace generate:dtos

This will read all of your model files, collecting their properties and types. It'll then convert those property's types into serialization-safe types and relationships into their DTO representations.

You can also generate validators alongside DTOs by using the --validator flag:

node ace generate:dtos --validator
File Tree                       Class
------------------------------------------------
└── app/
    ├── dtos/
    │   ├── account.ts          AccountDto
    │   ├── account_group.ts    AccountGroupDto
    │   ├── account_type.ts     AccountTypeDto
    │   ├── income.ts           IncomeDto
    │   ├── payee.ts            PayeeDto
    │   └── user.ts             UserDto
    └── models/
        ├── account.ts          Account
        ├── account_group.ts    AccountGroup
        ├── account_type.ts     AccountType
        ├── income.ts           Income
        ├── payee.ts            Payee
        └── user.ts             User
  • Gets a list of your model files from the location defined within your adonisrc.ts file
  • Reads those files as plaintext, filering down to just property definitions
  • Determines the property name, it's types, whether it's a relationship, and if it's optionally modified ?
  • Converts those model types into serialized representations (currently a very loose conversion)
    • Note, at present, this does not account for serialization behaviors defined on the model property (like serializeAs)
  • Creates DTO property definitions from those conversions
  • Prepares constructor value setters for each property
  • Collects needed imports for relationships
  • Generates the DTO file
    • Note, if a file already exists at the DTOs determined location it will be skipped

Make DTO Command

Want to make a plain DTO file, or a single DTO from a single Model? This is the command for you!

To make a DTO named AccountDto within a file located at dto/account.ts, we can run the following:

node ace make:dto account

This will check to see if there is a model named Account. If a model is found, it will use that model's property definitions to generate the AccountDto. Otherwise, it'll generate just a AccountDto file with an empty class inside it.

You can also generate a validator alongside the DTO by using the --validator flag:

node ace make:dto account --validator

This will create both a DTO and a validator for the Account model.

File Tree                       Class
------------------------------------------------
└── app/
    ├── dtos/
    │   ├── account.ts          AccountDto
    └── models/
        ├── account.ts          Account

What If There Isn't An Account Model?

As mentioned above, a plain AccountDto class will be generated within a new dto/account.ts file, which will look like the below.

export default class AccountDto {}

Specifying A Different Model

If the DTO and Model names don't match, you can specify a specific Model to use via the --model flag.

node ace make:dto account --model=main_account

Now instead of looking for a model named Account it'll instead look for MainAccount and use it to create a DTO named AccountDto.

Make Validator Command

Want to make a validator for a model? This command works similarly to the make:dto command:

node ace make:validators account

This will check to see if there is a model named Account. If a model is found, it will use that model's property definitions to generate the accountValidator. Otherwise, it'll generate just a plain validator file with an empty schema.

File Tree                       Variable
------------------------------------------------
└── app/
    ├── validators/
    │   ├── account.ts          accountValidator
    └── models/
        ├── account.ts          Account

Specifying A Different Model

Just like with DTOs, you can specify a different model:

node ace make:validators account --model=main_account

Generate Validators Command

Want to generate validators for all your models in one fell swoop? This command works similarly to generate:dtos:

node ace generate:validators

This will read all of your model files, collecting their properties and types. It'll then convert those property's types into VineJS validator rules.

File Tree                       Variable
------------------------------------------------
└── app/
    ├── validators/
    │   ├── account.ts          accountValidator
    │   ├── account_group.ts    accountGroupValidator
    │   ├── account_type.ts     accountTypeValidator
    │   ├── income.ts           incomeValidator
    │   ├── payee.ts            payeeValidator
    │   └── user.ts             userValidator
    └── models/
        ├── account.ts          Account
        ├── account_group.ts    AccountGroup
        ├── account_type.ts     AccountType
        ├── income.ts           Income
        ├── payee.ts            Payee
        └── user.ts             User

BaseDto Helpers

Newly added in v0.0.4, we now include either a BaseDto or BaseModelDto depeneding on whether we're generating your DTO from a model or not. Both of these bases include a helper called fromArray. With this, you can pass in an array of source objects. We'll then loop over them and pass each into a new constructor. This does run with the assumption that you'll populate properties within your DTO constructors.

Here's a quick example

class Test extends BaseModel {
  @column()
  declare id: number
}

class TestDto extends BaseModelDto {
  declare id: number

  constructor(instance: Test) {
    super()
    this.id = instance.id
  }
}

const tests = await Test.createMany([{ id: 1 }, { id: 2 }, { id: 3 }])

const dtoArray = TestDto.fromArray(tests)
// [TestDto, TestDto, TestDto]

Additionally, BaseModelDto also includes a fromPaginator helper. This allows you to pass in an instance of the ModelPaginator to be converted into a SimplePaginatorDto we have defined within this package. You can also pass in a URL range start and end and we'll generate those URLs for you during the conversion.

Here's a simple example

class Test extends BaseModel {
  @column()
  declare id: number
}

class TestDto extends BaseModelDto {
  declare id: number

  constructor(instance: Test) {
    super()
    this.id = instance.id
  }
}

const tests = await Test.createMany([{ id: 1 }, { id: 2 }, { id: 3 }])

const paginator = await Test.query().paginate(1, 2)
const paginatorDto = TestDto.fromPaginator(paginator, { start: 1, end: 2 })
/**
 * {
 *    data: TestDto[],
 *    meta: SimplePaginatorDtoMetaContract
 * }
 */

const paginationUrls = paginatorDto.meta.pagesInRange
/**
 * [
 *    {
 *      url: '/?page=1',
 *      page: 1,
 *      isActive: true
 *    },
 *    {
 *      url: '/?page=2',
 *      page: 2,
 *      isActive: false
 *    },
 * ]
 */

If, for example, you're using something like Inertia, you can then type your props accordingly

import { SimplePaginatorDtoContract } from '@adocasts.com/dto/types'
import DifficultyDto from '#dtos/difficulty'

const props = defineProps<{
  paginated: SimplePaginatorDtoContract<DifficultyDto>
}>()

const rows = props.paginated.data
const info = props.paginated.meta
const urls = props.paginated.meta.pagesInRange

Things To Note

  • At present we assume the Model's name from the file name of the model.
  • There is NOT currently a setting to change the output directory of the DTOs
  • Due to reflection limitations, we're reading Models as plaintext. I'm no TypeScript wiz, so if you know of a better approach, I'm all ears!
    • Since we're reading as plaintext
      • Currently we're omitting decorators and their options

Example

So, we've use account as our example throughout this guide, so let's end by taking a look at what this Account Model looks like!

The Account Model
// app/models/account.ts

import { DateTime } from 'luxon'
import { BaseModel, belongsTo, column, computed, hasMany, hasOne } from '@adonisjs/lucid/orm'
import User from './user.js'
import type { BelongsTo, HasMany, HasOne } from '@adonisjs/lucid/types/relations'
import AccountType from '#models/account_type'
import Payee from '#models/payee'
import Stock from '#models/stock'
import Transaction from '#models/transaction'
import AccountTypeService from '#services/account_type_service'
import { columnCurrency } from '#start/orm/column'
import type { AccountGroupConfig } from '#config/account'

export default class Account extends BaseModel {
  // region Columns

  @column({ isPrimary: true })
  declare id: number

  @column()
  declare userId: number

  @column()
  declare accountTypeId: number

  @column()
  declare name: string

  @column()
  declare note: string

  @column.date()
  declare dateOpened: DateTime | null

  @column.date()
  declare dateClosed: DateTime | null

  @columnCurrency()
  declare balance: number

  @columnCurrency()
  declare startingBalance: number

  @column.dateTime({ autoCreate: true })
  declare createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  declare updatedAt: DateTime

  // endregion

  // region Unmapped Properties

  aggregations: Record<string, number> = {}

  // endregion

  // region Relationships

  @belongsTo(() => User)
  declare user: BelongsTo<typeof User>

  @belongsTo(() => AccountType)
  declare accountType: BelongsTo<typeof AccountType>

  @hasOne(() => Payee)
  declare payee: HasOne<typeof Payee>

  @hasMany(() => Stock)
  declare stocks: HasMany<typeof Stock>

  @hasMany(() => Transaction)
  declare transactions: HasMany<typeof Transaction>

  // endregion

  // region Computed Properties

  @computed()
  get accountGroup(): AccountGroupConfig {
    return AccountTypeService.getAccountTypeGroup(this.accountTypeId)
  }

  @computed()
  get isCreditIncrease(): boolean {
    return AccountTypeService.isCreditIncreaseById(this.accountTypeId)
  }

  @computed()
  get isBudgetable() {
    return AccountTypeService.isBudgetable(this.accountTypeId)
  }

  @computed()
  get balanceDisplay() {
    return '$' + this.balance.toLocaleString('en-US')
  }

  // endregion
}

It's got

  • Column properties
  • Nullable properties
  • An unmapped property, which also contains a default value
  • Getters
  • Relationships

Let's see what we get when we generate our DTO!

node ace make:dto account
The Account DTO
import { BaseModelDto } from '@adocasts.com/dto/base'
import Account from '#models/account'
import UserDto from '#dtos/user'
import AccountTypeDto from '#dtos/account_type'
import PayeeDto from '#dtos/payee'
import StockDto from '#dtos/stock'
import TransactionDto from '#dtos/transaction'
import { AccountGroupConfig } from '#config/account'

export default class AccountDto extends BaseModelDto {
  declare id: number
  declare userId: number
  declare accountTypeId: number
  declare name: string
  declare note: string
  declare dateOpened: string | null
  declare dateClosed: string | null
  declare balance: number
  declare startingBalance: number
  declare createdAt: string
  declare updatedAt: string
  aggregations: Record<string, number> = {}
  declare user: UserDto | null
  declare accountType: AccountTypeDto | null
  declare payee: PayeeDto | null
  declare stocks: StockDto[]
  declare transactions: TransactionDto[]
  declare accountGroup: AccountGroupConfig
  declare isCreditIncrease: boolean
  declare isBudgetable: boolean
  declare balanceDisplay: string

  constructor(account?: Account) {
    super()

    if (!account) return
    this.id = account.id
    this.userId = account.userId
    this.accountTypeId = account.accountTypeId
    this.name = account.name
    this.note = account.note
    this.dateOpened = account.dateOpened?.toISO()!
    this.dateClosed = account.dateClosed?.toISO()!
    this.balance = account.balance
    this.startingBalance = account.startingBalance
    this.createdAt = account.createdAt.toISO()!
    this.updatedAt = account.updatedAt.toISO()!
    this.aggregations = account.aggregations
    this.user = account.user && new UserDto(account.user)
    this.accountType = account.accountType && new AccountTypeDto(account.accountType)
    this.payee = account.payee && new PayeeDto(account.payee)
    this.stocks = StockDto.fromArray(account.stocks)
    this.transactions = TransactionDto.fromArray(account.transactions)
    this.accountGroup = account.accountGroup
    this.isCreditIncrease = account.isCreditIncrease
    this.isBudgetable = account.isBudgetable
    this.balanceDisplay = account.balanceDisplay
  }
}

It's got the

  • Needed imports (it'll try to get them all by also referencing the Model's imports)
  • Column properties from our Model
  • Nullable property's nullability
  • Unmapped property from our Model, plus it's default value
  • Relationships converted into DTO representations
  • Getters and their types, when specified. If types are inferred, the type will default to string or boolean if variable name starts with is
  • Constructor value setters for all of the above
  • A helper method fromArray that'll normalize to an empty array if need be

Example Validator

Let's see what we get when we generate a validator for our Account model:

node ace make:validator account
The Account Validator
import vine from '@vinejs/vine'
import Account from '#models/account'
import { AccountGroupConfig } from '#config/account'

export const accountValidator = vine.compile(
  vine.object({
    id: vine.number(),
    userId: vine.number(),
    accountTypeId: vine.number(),
    name: vine.string().trim(),
    note: vine.string().trim(),
    dateOpened: vine.string().datetime().optional(),
    dateClosed: vine.string().datetime().optional(),
    balance: vine.number(),
    startingBalance: vine.number(),
    createdAt: vine.string().datetime(),
    updatedAt: vine.string().datetime(),
    user: vine.object({}),
    accountType: vine.object({}),
    payee: vine.object({}),
    stocks: vine.array(vine.object({})),
    transactions: vine.array(vine.object({})),
    accountGroup: vine.object({}),
    isCreditIncrease: vine.boolean(),
    isBudgetable: vine.boolean(),
    balanceDisplay: vine.string().trim()
  })
)

It's got:

  • Needed imports from the model
  • Validation rules for all model properties
  • Appropriate type conversions (e.g., DateTime to string().datetime())
  • Optional modifiers for nullable properties
  • Object and array validators for relationships

Using Generated Validators

Once you've generated a validator, you can use it in your controllers or routes to validate incoming data:

import { accountValidator } from '#validators/account'
import { HttpContext } from '@adonisjs/core/http'

export default class AccountsController {
  async store({ request, response }: HttpContext) {
    try {
      // Validate the request data
      const data = await accountValidator.validate(request.all())

      // Create the account
      const account = await Account.create(data)

      // Return the account as a DTO
      return response.created(new AccountDto(account))
    } catch (error) {
      // Handle validation errors
      if (error.code === 'E_VALIDATION_ERROR') {
        return response.unprocessableEntity(error.messages)
      }

      throw error
    }
  }
}

版本与发布记录

@adocasts.com/dto 共发布 10 个版本,首次发布于 2024/06/10;过去 12 个月发布了 0 个版本。

版本发布日期距今
0.0.10latest2025/06/231.4 年前
0.0.92024/11/142 年前
0.0.82024/11/142 年前
0.0.72024/11/082 年前
0.0.62024/07/252 年前
0.0.5-02024/07/252 年前

关于 @adocasts.com/dto 的常见问题

@adocasts.com/dto 是什么?

Easily make and generate DTOs from Lucid Models

如何安装 @adocasts.com/dto?

在项目目录运行 npm install @adocasts.com/dto,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@adonisjs/core(^6.2.0)、@adonisjs/lucid(^21.1.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@adocasts.com/dto 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/adocasts.com__dto 是否存在。

@adocasts.com/dto 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@adocasts.com/dto 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.6.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@adocasts.com/dto 可以商用吗?

@adocasts.com/dto 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@adocasts.com/dto 的下载量有多少?

根据 npm 官方下载统计,@adocasts.com/dto 在 2026/10/05 – 2026/11/03 这 30 天内被下载 6,673 次,日均约 222 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@adocasts.com/dto 还在维护吗?

最新版本 0.0.10 发布于 2025/06/23(1.4 年前);至今共发布 10 个版本,其中过去 12 个月发布了 0 个;首次发布于 2024/06/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。