From a36411b5a8bdae86185be0116a854124dd9dc85c Mon Sep 17 00:00:00 2001 From: Tom Gobich Date: Wed, 24 Jul 2024 20:28:30 -0400 Subject: [PATCH] added(#1): BaseDto, BaseModelDto, and SimplePaginatorDto --- README.md | 144 ++++++++++++++++++++++---- package.json | 50 +++++---- src/base/base_dto.ts | 20 ++++ src/base/base_model_dtos.ts | 24 +++++ src/base/main.ts | 4 + src/paginator/main.ts | 3 + src/paginator/simple_paginator_dto.ts | 50 +++++++++ src/types.ts | 28 +++++ stubs/make/dto/main.stub | 24 ++--- stubs/make/dto/plain.stub | 9 +- test-files/expectations/account.txt | 10 +- test-files/expectations/plain.txt | 7 ++ test-files/expectations/some_test.txt | 10 +- test-files/expectations/test.txt | 10 +- test-files/expectations/user.txt | 10 +- tests/commands/make_dto.spec.ts | 28 ++++- tests/dtos/base.spec.ts | 88 ++++++++++++++++ 17 files changed, 435 insertions(+), 84 deletions(-) create mode 100644 src/base/base_dto.ts create mode 100644 src/base/base_model_dtos.ts create mode 100644 src/base/main.ts create mode 100644 src/paginator/main.ts create mode 100644 src/paginator/simple_paginator_dto.ts create mode 100644 test-files/expectations/plain.txt create mode 100644 tests/dtos/base.spec.ts diff --git a/README.md b/README.md index 229c009..be124b2 100644 --- a/README.md +++ b/README.md @@ -4,41 +4,53 @@ [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] -Converting Lucid Models to DTO files can be a tedious task. +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 +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. + ```shell node ace add @adocasts.com/dto ``` + ##### Manual Install & Configure + You can also manually install and configure if you'd prefer + ```shell npm install @adocasts.com/dto ``` + ```shell node ace configure @adocasts.com/dto ``` ##### Define DTO Import Path -The generated DTOs will use `#dtos/*` for relationship imports within the DTOs. + +The generated DTOs will use `#dtos/*` for relationship imports within the DTOs. As such, we recommend defining this import path within your `package.json` + ```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! + ```shell 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 +It'll then convert those property's types into serialization-safe types and relationships into their DTO representations. ``` @@ -73,15 +85,19 @@ File Tree Class - 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: + ```shell node ace make:dto account ``` -This will check to see if there is a model named `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. + ``` File Tree Class ------------------------------------------------ @@ -93,20 +109,103 @@ File Tree Class ``` ### 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. + ```ts 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. + ```shell node ace make:dto account --model=main_account ``` -Now instead of looking for a model named `Account` it'll instead + +Now instead of looking for a model named `Account` it'll instead look for `MainAccount` and use it to create a DTO named `AccountDto`. +## 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 + +```ts +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 + +```ts +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 + * }, + * ] + */ +``` + ## 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! @@ -114,10 +213,12 @@ look for `MainAccount` and use it to create a DTO named `AccountDto`. - Currently we're omitting decorators and their options ## Example -So, we've use account as our example throughout this guide, + +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 + ```ts // app/models/account.ts @@ -220,22 +321,26 @@ export default class Account extends BaseModel { // endregion } - ``` -It's got -- Column properties + +It's got + +- Column properties - Nullable properties - An unmapped property, which also contains a default value -- Getters +- Getters - Relationships Let's see what we get when we generate our DTO! + ```shell node ace make:dto account ``` ##### The Account DTO + ```ts +import { BaseModelDto } from '@adocasts.com/dto/base' import Account from '#models/account' import UserDto from '#dtos/user' import AccountTypeDto from '#dtos/account_type' @@ -244,7 +349,7 @@ import StockDto from '#dtos/stock' import TransactionDto from '#dtos/transaction' import { AccountGroupConfig } from '#config/account' -export default class AccountDto { +export default class AccountDto extends BaseModelDto { declare id: number declare userId: number declare accountTypeId: number @@ -268,6 +373,8 @@ export default class AccountDto { declare balanceDisplay: string constructor(account?: Account) { + super() + if (!account) return this.id = account.id this.userId = account.userId @@ -291,15 +398,11 @@ export default class AccountDto { this.isBudgetable = account.isBudgetable this.balanceDisplay = account.balanceDisplay } - - static fromArray(accounts: Account[]) { - if (!accounts) return [] - return accounts.map((account) => new AccountDto(account)) - } } ``` 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 @@ -310,12 +413,9 @@ It's got the - A helper method `fromArray` that'll normalize to an empty array if need be [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" - +[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 -[npm-url]: https://www.npmjs.com/package/@adocasts.com/dto/v/latest "npm" - +[npm-url]: https://www.npmjs.com/package/@adocasts.com/dto/v/latest 'npm' [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript - [license-url]: LICENSE.md [license-image]: https://img.shields.io/github/license/adocasts/package-dto?style=for-the-badge diff --git a/package.json b/package.json index 415ef3f..37658d3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "exports": { ".": "./build/index.js", "./types": "./build/src/types.js", - "./commands": "./build/commands/main.js" + "./commands": "./build/commands/main.js", + "./base": "./build/src/base/main.js", + "./paginator": "./build/src/paginator/main.js" }, "scripts": { "clean": "del-cli build", @@ -39,20 +41,10 @@ "prepublishOnly": "npm run build", "index:commands": "adonis-kit index build/commands" }, - "keywords": ["adonisjs", "lucid", "dto", "generate", "make"], - "author": "tomgobich,adocasts.com", - "license": "MIT", - "homepage": "https://github.com/adocasts/package-dto#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/adocasts/package-dto.git" - }, - "bugs": { - "url": "https://github.com/adocasts/package-dto/issues" - }, "devDependencies": { "@adonisjs/assembler": "^7.2.3", "@adonisjs/core": "^6.3.1", + "@adonisjs/lucid": "^21.1.0", "@adonisjs/eslint-config": "^1.3.0", "@adonisjs/prettier-config": "^1.3.0", "@adonisjs/tsconfig": "^1.3.0", @@ -69,9 +61,34 @@ "ts-node": "^10.9.2", "typescript": "^5.4.2" }, + "dependencies": { + "@japa/file-system": "^2.3.0" + }, "peerDependencies": { - "@adonisjs/core": "^6.2.0" + "@adonisjs/core": "^6.2.0", + "@adonisjs/lucid": "^21.1.0" }, + "author": "tomgobich,adocasts.com", + "license": "MIT", + "homepage": "https://github.com/adocasts/package-dto#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/adocasts/package-dto.git" + }, + "bugs": { + "url": "https://github.com/adocasts/package-dto/issues" + }, + "keywords": [ + "adonisjs", + "lucid", + "dto", + "generate", + "make" + ], + "eslintConfig": { + "extends": "@adonisjs/eslint-config/package" + }, + "prettier": "@adonisjs/prettier-config", "publishConfig": { "access": "public", "tag": "latest" @@ -90,12 +107,5 @@ "exclude": [ "tests/**" ] - }, - "eslintConfig": { - "extends": "@adonisjs/eslint-config/package" - }, - "prettier": "@adonisjs/prettier-config", - "dependencies": { - "@japa/file-system": "^2.3.0" } } diff --git a/src/base/base_dto.ts b/src/base/base_dto.ts new file mode 100644 index 0000000..cd35adc --- /dev/null +++ b/src/base/base_dto.ts @@ -0,0 +1,20 @@ +import { StaticDto } from '../types.js' + +export default class BaseDto { + /** + * Creates an array of DTO objects from an array of source objects. + * + * @template SourceObject - The type of the source objects. + * @template Dto - The type of the DTO objects. + * @param {StaticDto} this - The static DTO class. + * @param {SourceObject[]} sources - The array of source objects. + * @return {Dto[]} An array of DTO objects. + */ + static fromArray( + this: StaticDto, + sources: SourceObject[] + ) { + if (!sources) return [] + return sources.map((source) => new this(source)) + } +} diff --git a/src/base/base_model_dtos.ts b/src/base/base_model_dtos.ts new file mode 100644 index 0000000..19e170b --- /dev/null +++ b/src/base/base_model_dtos.ts @@ -0,0 +1,24 @@ +import { LucidRow } from '@adonisjs/lucid/types/model' +import { SimplePaginatorDtoMetaRange, StaticDto } from '../types.js' +import SimplePaginatorDto from '../paginator/simple_paginator_dto.js' +import BaseDto from './base_dto.js' +import { SimplePaginatorContract } from '@adonisjs/lucid/types/querybuilder' + +export default class BaseModelDto extends BaseDto { + /** + * Creates a new instance of the SimplePaginatorDto class using the provided paginator and DTO. + * + * @template Model - The type of the LucidRow model. + * @template Dto - The type of the BaseDto. + * @param {SimplePaginatorContract} paginator - The paginator to use for the SimplePaginatorDto. + * @param {SimplePaginatorDtoMetaRange} [range] - The range of pages to include in the SimplePaginatorDto. + * @return {SimplePaginatorDto} - The created SimplePaginatorDto. + */ + static fromPaginator( + this: StaticDto, + paginator: SimplePaginatorContract, + range?: SimplePaginatorDtoMetaRange + ) { + return new SimplePaginatorDto(paginator, this, range) + } +} diff --git a/src/base/main.ts b/src/base/main.ts new file mode 100644 index 0000000..7f00798 --- /dev/null +++ b/src/base/main.ts @@ -0,0 +1,4 @@ +import BaseDto from './base_dto.js' +import BaseModelDto from './base_model_dtos.js' + +export { BaseDto, BaseModelDto } diff --git a/src/paginator/main.ts b/src/paginator/main.ts new file mode 100644 index 0000000..e2dac71 --- /dev/null +++ b/src/paginator/main.ts @@ -0,0 +1,3 @@ +import SimplePaginatorDto from './simple_paginator_dto.js' + +export { SimplePaginatorDto } diff --git a/src/paginator/simple_paginator_dto.ts b/src/paginator/simple_paginator_dto.ts new file mode 100644 index 0000000..c3bba46 --- /dev/null +++ b/src/paginator/simple_paginator_dto.ts @@ -0,0 +1,50 @@ +import { LucidRow } from '@adonisjs/lucid/types/model' +import { + SimplePaginatorDtoContract, + SimplePaginatorDtoMetaContract, + SimplePaginatorDtoMetaRange, + StaticDto, +} from '../types.js' +import BaseDto from '../base/base_dto.js' +import { SimplePaginatorContract } from '@adonisjs/lucid/types/querybuilder' + +export default class SimplePaginatorDto + implements SimplePaginatorDtoContract +{ + declare data: Dto[] + declare meta: SimplePaginatorDtoMetaContract + + /** + * Constructs a new instance of the SimplePaginatorDto class. + * + * @param {SimplePaginatorContract} paginator - The paginator object containing the data. + * @param {StaticDto} dto - The static DTO class used to map the data. + * @param {SimplePaginatorDtoMetaRange} [range] - Optional range for the paginator. + */ + constructor( + paginator: SimplePaginatorContract, + dto: StaticDto, + range?: SimplePaginatorDtoMetaRange + ) { + this.data = paginator.all().map((row) => new dto(row)) + + this.meta = { + total: paginator.total, + perPage: paginator.perPage, + currentPage: paginator.currentPage, + lastPage: paginator.lastPage, + firstPage: paginator.firstPage, + firstPageUrl: paginator.getUrl(1), + lastPageUrl: paginator.getUrl(paginator.lastPage), + nextPageUrl: paginator.getNextPageUrl(), + previousPageUrl: paginator.getPreviousPageUrl(), + } + + if (range?.start || range?.end) { + const start = range?.start || paginator.firstPage + const end = range?.end || paginator.lastPage + + this.meta.pagesInRange = paginator.getUrlsForRange(start, end) + } + } +} diff --git a/src/types.ts b/src/types.ts index e69de29..8ba5849 100644 --- a/src/types.ts +++ b/src/types.ts @@ -0,0 +1,28 @@ +export type StaticDto = { new (model: Model): Dto } + +export interface SimplePaginatorDtoContract { + data: Dto[] + meta: SimplePaginatorDtoMetaContract +} + +export interface SimplePaginatorDtoMetaContract { + total: number + perPage: number + currentPage: number + lastPage: number + firstPage: number + firstPageUrl: string + lastPageUrl: string + nextPageUrl: string | null + previousPageUrl: string | null + pagesInRange?: { + url: string + page: number + isActive: boolean + }[] +} + +export type SimplePaginatorDtoMetaRange = { + start: number + end: number +} diff --git a/stubs/make/dto/main.stub b/stubs/make/dto/main.stub index fdad873..aa26f95 100644 --- a/stubs/make/dto/main.stub +++ b/stubs/make/dto/main.stub @@ -1,23 +1,23 @@ -{{{ - exports({ - to: dto.exportPath - }) -}}} +import { BaseModelDto } from '@adocasts.com/dto/base' import {{ model.name }} from '#models/{{ string.snakeCase(model.name) }}' {{ #each imports as statement }} {{{ '\n' }}}{{ statement }} {{ /each }} -export default class {{ dto.className }} {{{ '{' }}}{{ #each dto.properties as property }} +export default class {{ dto.className }} extends BaseModelDto {{{ '{' }}}{{ #each dto.properties as property }} {{{ property.declaration }}}{{ /each }} constructor({{ model.variable }}?: {{ model.name }}) { - if (!{{ model.variable }}) return{{ #each dto.properties as property }} - this.{{ property.name }} = {{{ property.valueSetter }}}{{ /each }} - } + super() - static fromArray({{ string.plural(model.variable) }}: {{ model.name }}[]) { - if (!{{ string.plural(model.variable) }}) return [] - return {{ string.plural(model.variable) }}.map(({{ model.variable }}) => new {{ dto.className }}({{ model.variable }})) + if (!{{ model.variable }}) return +{{ #each dto.properties as property }} + this.{{ property.name }} = {{{ property.valueSetter }}}{{ /each }} } } + +{{{ + exports({ + to: dto.exportPath + }) +}}} diff --git a/stubs/make/dto/plain.stub b/stubs/make/dto/plain.stub index b49cf38..b938fe0 100644 --- a/stubs/make/dto/plain.stub +++ b/stubs/make/dto/plain.stub @@ -1,6 +1,13 @@ +import { BaseDto } from '@adocasts.com/dto/base' + +export default class {{ dto.className }} extends BaseDto { + constructor() { + super() + } +} + {{{ exports({ to: dto.exportPath }) }}} -export default class {{ dto.className }} {} diff --git a/test-files/expectations/account.txt b/test-files/expectations/account.txt index 9deba29..c0d0142 100644 --- a/test-files/expectations/account.txt +++ b/test-files/expectations/account.txt @@ -1,3 +1,4 @@ +import { BaseModelDto } from '@adocasts.com/dto/base' import Account from '#models/account' import UserDto from '#dtos/user' import AccountTypeDto from '#dtos/account_type' @@ -6,7 +7,7 @@ import StockDto from '#dtos/stock' import TransactionDto from '#dtos/transaction' import { AccountGroupConfig } from '#config/account' -export default class AccountDto { +export default class AccountDto extends BaseModelDto { declare id: number declare userId: number declare accountTypeId: number @@ -30,6 +31,8 @@ export default class AccountDto { declare balanceDisplay: string constructor(account?: Account) { + super() + if (!account) return this.id = account.id this.userId = account.userId @@ -53,9 +56,4 @@ export default class AccountDto { this.isBudgetable = account.isBudgetable this.balanceDisplay = account.balanceDisplay } - - static fromArray(accounts: Account[]) { - if (!accounts) return [] - return accounts.map((account) => new AccountDto(account)) - } } diff --git a/test-files/expectations/plain.txt b/test-files/expectations/plain.txt new file mode 100644 index 0000000..5c73943 --- /dev/null +++ b/test-files/expectations/plain.txt @@ -0,0 +1,7 @@ +import { BaseDto } from '@adocasts.com/dto/base' + +export default class PlainDto extends BaseDto { + constructor() { + super() + } +} diff --git a/test-files/expectations/some_test.txt b/test-files/expectations/some_test.txt index 2c62eda..4b86d70 100644 --- a/test-files/expectations/some_test.txt +++ b/test-files/expectations/some_test.txt @@ -1,17 +1,15 @@ +import { BaseModelDto } from '@adocasts.com/dto/base' import Test from '#models/test' -export default class SomeTestDto { +export default class SomeTestDto extends BaseModelDto { declare id: number declare createdAt: string constructor(test?: Test) { + super() + if (!test) return this.id = test.id this.createdAt = test.createdAt.toISO()! } - - static fromArray(tests: Test[]) { - if (!tests) return [] - return tests.map((test) => new SomeTestDto(test)) - } } diff --git a/test-files/expectations/test.txt b/test-files/expectations/test.txt index 35dd472..1b2560a 100644 --- a/test-files/expectations/test.txt +++ b/test-files/expectations/test.txt @@ -1,17 +1,15 @@ +import { BaseModelDto } from '@adocasts.com/dto/base' import Test from '#models/test' -export default class TestDto { +export default class TestDto extends BaseModelDto { declare id: number declare createdAt: string constructor(test?: Test) { + super() + if (!test) return this.id = test.id this.createdAt = test.createdAt.toISO()! } - - static fromArray(tests: Test[]) { - if (!tests) return [] - return tests.map((test) => new TestDto(test)) - } } diff --git a/test-files/expectations/user.txt b/test-files/expectations/user.txt index 2236e98..ad5fffa 100644 --- a/test-files/expectations/user.txt +++ b/test-files/expectations/user.txt @@ -1,3 +1,4 @@ +import { BaseModelDto } from '@adocasts.com/dto/base' import User from '#models/user' import PayeeDto from '#dtos/payee' import TransactionDto from '#dtos/transaction' @@ -5,7 +6,7 @@ import IncomeDto from '#dtos/income' import StockPurchaseDto from '#dtos/stock_purchase' import StockDto from '#dtos/stock' -export default class UserDto { +export default class UserDto extends BaseModelDto { declare id: number declare fullName: string | null declare email: string @@ -19,6 +20,8 @@ export default class UserDto { declare stocks: StockDto[] constructor(user?: User) { + super() + if (!user) return this.id = user.id this.fullName = user.fullName @@ -32,9 +35,4 @@ export default class UserDto { this.stockPurchases = StockPurchaseDto.fromArray(user.stockPurchases) this.stocks = StockDto.fromArray(user.stocks) } - - static fromArray(users: User[]) { - if (!users) return [] - return users.map((user) => new UserDto(user)) - } } diff --git a/tests/commands/make_dto.spec.ts b/tests/commands/make_dto.spec.ts index c53f758..b71d16c 100644 --- a/tests/commands/make_dto.spec.ts +++ b/tests/commands/make_dto.spec.ts @@ -16,8 +16,14 @@ test.group('MakeAction', (group) => { const command = await ace.create(MakeDto, ['Plain']) await command.exec() + const expectedContents = await fs.contents('expectations/plain.txt') + command.assertLog('green(DONE:) create app/dtos/plain.ts') - await assert.fileContains('app/dtos/plain.ts', 'export default class PlainDto {}') + await assert.fileContains( + 'app/dtos/plain.ts', + 'export default class PlainDto extends BaseDto {' + ) + await assert.fileEquals('app/dtos/plain.ts', expectedContents.trim()) }) test('make a dto referencing a model', async ({ fs, assert }) => { @@ -31,7 +37,10 @@ test.group('MakeAction', (group) => { const expectedContents = await fs.contents('expectations/test.txt') command.assertLog('green(DONE:) create app/dtos/test.ts') - await assert.fileContains('app/dtos/test.ts', 'export default class TestDto {') + await assert.fileContains( + 'app/dtos/test.ts', + 'export default class TestDto extends BaseModelDto {' + ) await assert.fileEquals('app/dtos/test.ts', expectedContents.trim()) }) @@ -46,7 +55,10 @@ test.group('MakeAction', (group) => { const expectedContents = await fs.contents('expectations/some_test.txt') command.assertLog('green(DONE:) create app/dtos/some_test.ts') - await assert.fileContains('app/dtos/some_test.ts', 'export default class SomeTestDto {') + await assert.fileContains( + 'app/dtos/some_test.ts', + 'export default class SomeTestDto extends BaseModelDto {' + ) await assert.fileEquals('app/dtos/some_test.ts', expectedContents.trim()) }) @@ -64,7 +76,10 @@ test.group('MakeAction', (group) => { const expectedContents = await fs.contents('expectations/account.txt') command.assertLog('green(DONE:) create app/dtos/account.ts') - await assert.fileContains('app/dtos/account.ts', 'export default class AccountDto {') + await assert.fileContains( + 'app/dtos/account.ts', + 'export default class AccountDto extends BaseModelDto {' + ) await assert.fileEquals('app/dtos/account.ts', expectedContents.trim()) }) @@ -82,7 +97,10 @@ test.group('MakeAction', (group) => { const expectedContents = await fs.contents('expectations/user.txt') command.assertLog('green(DONE:) create app/dtos/user.ts') - await assert.fileContains('app/dtos/user.ts', 'export default class UserDto {') + await assert.fileContains( + 'app/dtos/user.ts', + 'export default class UserDto extends BaseModelDto {' + ) await assert.fileEquals('app/dtos/user.ts', expectedContents.trim()) }) }) diff --git a/tests/dtos/base.spec.ts b/tests/dtos/base.spec.ts new file mode 100644 index 0000000..448dfbb --- /dev/null +++ b/tests/dtos/base.spec.ts @@ -0,0 +1,88 @@ +import { test } from '@japa/runner' +import { BaseDto, BaseModelDto } from '../../src/base/main.js' +import { BaseModel, column } from '@adonisjs/lucid/orm' +import { SimplePaginator } from '@adonisjs/lucid/database' +import SimplePaginatorDto from '../../src/paginator/simple_paginator_dto.js' + +test.group('Internal DTOs', (group) => { + group.each.teardown(async ({ context }) => { + delete process.env.ADONIS_ACE_CWD + await context.fs.remove('app/dtos') + }) + + test('should add type-safe fromArray utility on BaseDto', async ({ assert }) => { + class Test { + declare id: number + constructor(id: number) { + this.id = id + } + } + + class TestDto extends BaseDto { + declare id: number + constructor(instance: Test) { + super() + this.id = instance.id + } + } + + const dtoArray = TestDto.fromArray([new Test(1), new Test(2), new Test(3)]) + + assert.isArray(dtoArray) + + dtoArray.map((dto) => assert.instanceOf(dto, TestDto)) + }) + + test('should add type-safe fromArray utility on BaseModelDto', async ({ assert }) => { + class Test extends BaseModel { + @column() + declare id: number + } + + class TestDto extends BaseModelDto { + declare id: number + constructor(instance: Test) { + super() + this.id = instance.id + } + } + + const test1 = new Test().merge({ id: 1 }) + const test2 = new Test().merge({ id: 2 }) + const test3 = new Test().merge({ id: 3 }) + + const dtoArray = TestDto.fromArray([test1, test2, test3]) + + assert.isArray(dtoArray) + + dtoArray.map((dto) => assert.instanceOf(dto, TestDto)) + }) + + test('should allow conversion to SimplePaginatorDto for Lucid Models', async ({ assert }) => { + class Test extends BaseModel { + @column() + declare id: number + } + + class TestDto extends BaseModelDto { + declare id: number + constructor(instance: Test) { + super() + this.id = instance.id + } + } + + const test1 = new Test().merge({ id: 1 }) + const test2 = new Test().merge({ id: 2 }) + const test3 = new Test().merge({ id: 3 }) + + const paginator = new SimplePaginator(3, 2, 1, test1, test2, test3) + const paginatorDto = TestDto.fromPaginator(paginator, { start: 1, end: 2 }) + + assert.instanceOf(paginatorDto, SimplePaginatorDto) + assert.isArray(paginatorDto.data) + assert.lengthOf(paginatorDto.meta.pagesInRange!, 2) + + paginatorDto.data.map((dto) => assert.instanceOf(dto, TestDto)) + }) +})