Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 155 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @adocasts.com/dto

> Easily make and generate DTOs from Lucid Models
> Easily make and generate DTOs and validators from Lucid Models

[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url]

Expand Down Expand Up @@ -53,6 +53,12 @@ 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:

```shell
node ace generate:dtos --validator
```

```
File Tree Class
------------------------------------------------
Expand Down Expand Up @@ -98,6 +104,14 @@ 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:

```shell
node ace make:dto account --validator
```

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

```
File Tree Class
------------------------------------------------
Expand Down Expand Up @@ -127,6 +141,67 @@ 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:

```shell
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:

```shell
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`:

```shell
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.
Expand Down Expand Up @@ -427,6 +502,85 @@ It's got the
- 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:

```shell
node ace make:validator account
```

##### The Account Validator

```ts
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:

```typescript
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
}
}
}

[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adocasts/package-dto/test.yml?style=for-the-badge
[gh-workflow-url]: https://github.com/adocasts/package-dto/actions/workflows/test.yml 'Github action'
[npm-image]: https://img.shields.io/npm/v/@adocasts.com/dto/latest.svg?style=for-the-badge&logo=npm
Expand Down
22 changes: 21 additions & 1 deletion commands/generate_dtos.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { BaseCommand } from '@adonisjs/core/ace'
import { BaseCommand, flags } from '@adonisjs/core/ace'
import { CommandOptions } from '@adonisjs/core/types/ace'
import DtoService from '../services/dto_service.js'
import ModelService from '../services/model_service.js'
import { stubsRoot } from '../stubs/main.js'
import { ImportService } from '../services/import_service.js'
import ValidatorService from '../services/validator_service.js'

export default class GererateDtos extends BaseCommand {
static commandName = 'generate:dtos'
Expand All @@ -12,9 +13,16 @@ export default class GererateDtos extends BaseCommand {
strict: true,
}

@flags.boolean({
description: 'Generate validators alongside DTOs',
alias: 'v',
})
declare validator: boolean

async run() {
const modelService = new ModelService(this.app)
const dtoService = new DtoService(this.app)
const validatorService = this.validator ? new ValidatorService(this.app) : null

const files = await modelService.getFromFiles()
const unreadable = files.filter((file) => !file.model.isReadable)
Expand All @@ -29,14 +37,26 @@ export default class GererateDtos extends BaseCommand {

for (const file of files) {
const dto = dtoService.getDtoInfo(file.model.name, file.model)
const validator = validatorService?.getValidatorInfo(file.model.name, file.model)
const codemods = await this.createCodemods()
const imports = ImportService.getImportStatements(dto, file.modelFileLines)

// Create the DTO
await codemods.makeUsingStub(stubsRoot, 'make/dto/main.stub', {
model: file.model,
dto,
imports,
})

// If validator flag is set, also create a validator
if (this.validator && validator) {
const validatorImports = ImportService.getImportStatements(validator, file.modelFileLines)
await codemods.makeUsingStub(stubsRoot, 'make/validator/main.stub', {
model: file.model,
validator,
imports: validatorImports,
})
}
}
}
}
42 changes: 42 additions & 0 deletions commands/generate_validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BaseCommand } from '@adonisjs/core/ace'
import { CommandOptions } from '@adonisjs/core/types/ace'
import ValidatorService from '../services/validator_service.js'
import ModelService from '../services/model_service.js'
import { stubsRoot } from '../stubs/main.js'
import { ImportService } from '../services/import_service.js'

export default class GenerateValidators extends BaseCommand {
static commandName = 'generate:validators'
static description = 'Reads, converts, and generates vine validators from all Lucid Models'
static options: CommandOptions = {
strict: true,
}

async run() {
const modelService = new ModelService(this.app)
const validatorService = new ValidatorService(this.app)

const files = await modelService.getFromFiles()
const unreadable = files.filter((file) => !file.model.isReadable)

if (unreadable.length) {
this.logger.error(
`Unable to find or read one or more models: ${unreadable.map((file) => file.model.name).join(', ')}`
)
this.exitCode = 1
return
}

for (const file of files) {
const validator = validatorService.getValidatorInfo(file.model.name, file.model)
const codemods = await this.createCodemods()
const imports = ImportService.getImportStatements(validator, file.modelFileLines)

await codemods.makeUsingStub(stubsRoot, 'make/validator/main.stub', {
model: file.model,
validator,
imports,
})
}
}
}
33 changes: 31 additions & 2 deletions commands/make_dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { stubsRoot } from '../stubs/main.js'
import DtoService from '../services/dto_service.js'
import ModelService from '../services/model_service.js'
import { ImportService } from '../services/import_service.js'
import ValidatorService from '../services/validator_service.js'

export default class MakeDto extends BaseCommand {
static commandName = 'make:dto'
Expand All @@ -24,12 +25,20 @@ export default class MakeDto extends BaseCommand {
})
declare model?: string

@flags.boolean({
description: 'Generate a validator alongside the DTO',
alias: 'v',
})
declare validator: boolean

async run() {
const modelService = new ModelService(this.app)
const dtoService = new DtoService(this.app)
const validatorService = this.validator ? new ValidatorService(this.app) : null

const { model, modelFileLines } = await modelService.getModelInfo(this.model, this.name)
const dto = dtoService.getDtoInfo(this.name, model)
const validator = validatorService?.getValidatorInfo(this.name, model)
const codemods = await this.createCodemods()

if (!model.isReadable && this.model) {
Expand All @@ -39,17 +48,37 @@ export default class MakeDto extends BaseCommand {
return
} else if (!model.isReadable) {
// model not specifically wanted and couldn't be found or read? create plain DTO
return codemods.makeUsingStub(stubsRoot, 'make/dto/plain.stub', {
await codemods.makeUsingStub(stubsRoot, 'make/dto/plain.stub', {
dto,
})

// If validator flag is set, also create a plain validator
if (this.validator && validator) {
await codemods.makeUsingStub(stubsRoot, 'make/validator/plain.stub', {
validator,
})
}

return
}

const imports = ImportService.getImportStatements(dto, modelFileLines)

return codemods.makeUsingStub(stubsRoot, 'make/dto/main.stub', {
// Create the DTO
await codemods.makeUsingStub(stubsRoot, 'make/dto/main.stub', {
dto,
model,
imports,
})

// If validator flag is set, also create a validator
if (this.validator && validator) {
const validatorImports = ImportService.getImportStatements(validator, modelFileLines)
await codemods.makeUsingStub(stubsRoot, 'make/validator/main.stub', {
validator,
model,
imports: validatorImports,
})
}
}
}
Loading
Loading