diff --git a/bin/test.ts b/bin/test.ts index 7978bf2..d11a728 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,11 +1,15 @@ import { assert } from '@japa/assert' import { configure, processCLIArgs, run } from '@japa/runner' +import { fileSystem } from '@japa/file-system' processCLIArgs(process.argv.splice(2)) configure({ files: ['tests/**/*.spec.ts'], - plugins: [assert()], + plugins: [ + assert(), + fileSystem({ basePath: new URL('../test-files', import.meta.url), autoClean: false }), + ], }) run() diff --git a/package.json b/package.json index a52586b..1fd5079 100644 --- a/package.json +++ b/package.json @@ -83,5 +83,8 @@ "eslintConfig": { "extends": "@adonisjs/eslint-config/package" }, - "prettier": "@adonisjs/prettier-config" + "prettier": "@adonisjs/prettier-config", + "dependencies": { + "@japa/file-system": "^2.3.0" + } } diff --git a/test-files/app/models/account.ts b/test-files/app/models/account.ts new file mode 100644 index 0000000..81fcd1b --- /dev/null +++ b/test-files/app/models/account.ts @@ -0,0 +1,100 @@ +// @ts-nocheck +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 = {} + + // endregion + + // region Relationships + + @belongsTo(() => User) + declare user: BelongsTo + + @belongsTo(() => AccountType) + declare accountType: BelongsTo + + @hasOne(() => Payee) + declare payee: HasOne + + @hasMany(() => Stock) + declare stocks: HasMany + + @hasMany(() => Transaction) + declare transactions: HasMany + + // 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 +} diff --git a/test-files/app/models/test.ts b/test-files/app/models/test.ts new file mode 100644 index 0000000..c227a83 --- /dev/null +++ b/test-files/app/models/test.ts @@ -0,0 +1,14 @@ +// @ts-nocheck +import { DateTime } from 'luxon' +import { BaseModel, column } from '@adonisjs/lucid/orm' + +export default class Test extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + declare updatedAt: DateTime +} diff --git a/test-files/app/models/user.ts b/test-files/app/models/user.ts new file mode 100644 index 0000000..720e892 --- /dev/null +++ b/test-files/app/models/user.ts @@ -0,0 +1,56 @@ +// @ts-nocheck +import { DateTime } from 'luxon' +import hash from '@adonisjs/core/services/hash' +import { compose } from '@adonisjs/core/helpers' +import { BaseModel, column, hasMany } from '@adonisjs/lucid/orm' +import { withAuthFinder } from '@adonisjs/auth/mixins/lucid' +import Payee from '#models/payee' +import type { HasMany } from '@adonisjs/lucid/types/relations' +import Transaction from '#models/transaction' +import Income from '#models/income' +import StockPurchase from '#models/stock_purchase' +import Stock from '#models/stock' +import Account from '#models/account' + +const AuthFinder = withAuthFinder(() => hash.use('scrypt'), { + uids: ['email'], + passwordColumnName: 'password', +}) + +export default class User extends compose(BaseModel, AuthFinder) { + @column({ isPrimary: true }) + declare id: number + + @column() + declare fullName: string | null + + @column() + declare email: string + + @column({ serializeAs: null }) + declare password: string + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + declare updatedAt: DateTime | null + + @hasMany(() => Payee) + declare payees: HasMany + + @hasMany(() => Transaction) + declare transactions: HasMany + + @hasMany(() => Income) + declare incomes: HasMany + + @hasMany(() => StockPurchase) + declare stockPurchases: HasMany + + @hasMany(() => Stock) + declare stocks: HasMany + + @hasMany(() => Account) + declare accounts: HasMany +} diff --git a/test-files/expectations/account.txt b/test-files/expectations/account.txt new file mode 100644 index 0000000..3995e65 --- /dev/null +++ b/test-files/expectations/account.txt @@ -0,0 +1,60 @@ +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 { + 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 = {} + 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) { + 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 + } + + static fromArray(accounts: Account[]) { + if (!accounts) return [] + return accounts.map((account) => new AccountDto(account)) + } +} diff --git a/test-files/expectations/some_test.txt b/test-files/expectations/some_test.txt new file mode 100644 index 0000000..16e6051 --- /dev/null +++ b/test-files/expectations/some_test.txt @@ -0,0 +1,16 @@ +import Test from '#models/test' + +export default class SomeTestDto { + declare id: number + declare createdAt: string + + constructor(test: Test) { + 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 new file mode 100644 index 0000000..03e8d62 --- /dev/null +++ b/test-files/expectations/test.txt @@ -0,0 +1,16 @@ +import Test from '#models/test' + +export default class TestDto { + declare id: number + declare createdAt: string + + constructor(test: Test) { + 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 new file mode 100644 index 0000000..c17fcf5 --- /dev/null +++ b/test-files/expectations/user.txt @@ -0,0 +1,39 @@ +import User from '#models/user' +import PayeeDto from '#dtos/payee' +import TransactionDto from '#dtos/transaction' +import IncomeDto from '#dtos/income' +import StockPurchaseDto from '#dtos/stock_purchase' +import StockDto from '#dtos/stock' + +export default class UserDto { + declare id: number + declare fullName: string | null + declare email: string + declare password: string + declare createdAt: string + declare updatedAt: string | null + declare payees: PayeeDto[] + declare transactions: TransactionDto[] + declare incomes: IncomeDto[] + declare stockPurchases: StockPurchaseDto[] + declare stocks: StockDto[] + + constructor(user: User) { + this.id = user.id + this.fullName = user.fullName + this.email = user.email + this.password = user.password + this.createdAt = user.createdAt.toISO()! + this.updatedAt = user.updatedAt?.toISO()! + this.payees = PayeeDto.fromArray(user.payees) + this.transactions = TransactionDto.fromArray(user.transactions) + this.incomes = IncomeDto.fromArray(user.incomes) + 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/test-helpers/models/user.txt b/test-helpers/models/user.txt deleted file mode 100644 index d48791e..0000000 --- a/test-helpers/models/user.txt +++ /dev/null @@ -1,38 +0,0 @@ -import { DateTime } from 'luxon' -import { withAuthFinder } from '@adonisjs/auth' -import hash from '@adonisjs/core/services/hash' -import { compose } from '@adonisjs/core/helpers' -import { BaseModel, column, manyToMany } from '@adonisjs/lucid/orm' -import Organization from './organization.js' -import type { ManyToMany } from '@adonisjs/lucid/types/relations' - -const AuthFinder = withAuthFinder(() => hash.use('scrypt'), { - uids: ['email'], - passwordColumnName: 'password', -}) - -export default class User extends compose(BaseModel, AuthFinder) { - @column({ isPrimary: true }) - declare id: number - - @column() - declare fullName: string | null - - @column() - declare email: string - - @column() - declare password: string - - @column.dateTime({ autoCreate: true }) - declare createdAt: DateTime - - @column.dateTime({ autoCreate: true, autoUpdate: true }) - declare updatedAt: DateTime | null - - @manyToMany(() => Organization, { - pivotColumns: ['role_id'], - pivotTable: 'organization_users', - }) - declare organizations: ManyToMany -} diff --git a/tests/commands/make_dto.spec.ts b/tests/commands/make_dto.spec.ts new file mode 100644 index 0000000..c53f758 --- /dev/null +++ b/tests/commands/make_dto.spec.ts @@ -0,0 +1,88 @@ +import { test } from '@japa/runner' +import { AceFactory } from '@adonisjs/core/factories' +import MakeDto from '../../commands/make_dto.js' + +test.group('MakeAction', (group) => { + group.each.teardown(async ({ context }) => { + delete process.env.ADONIS_ACE_CWD + await context.fs.remove('app/dtos') + }) + + test('make a plain dto not referencing a model', async ({ fs, assert }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeDto, ['Plain']) + await command.exec() + + command.assertLog('green(DONE:) create app/dtos/plain.ts') + await assert.fileContains('app/dtos/plain.ts', 'export default class PlainDto {}') + }) + + test('make a dto referencing a model', async ({ fs, assert }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeDto, ['Test']) + await command.exec() + + 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.fileEquals('app/dtos/test.ts', expectedContents.trim()) + }) + + test('make a dto referencing a model with a different name', async ({ fs, assert }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeDto, ['some_test', '--model=test']) + await command.exec() + + 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.fileEquals('app/dtos/some_test.ts', expectedContents.trim()) + }) + + test('make a dto from a model with unmapped properties, getters, and default values', async ({ + fs, + assert, + }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeDto, ['account']) + await command.exec() + + 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.fileEquals('app/dtos/account.ts', expectedContents.trim()) + }) + + test('make a dto from a model additional definitions outside the model', async ({ + fs, + assert, + }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeDto, ['user']) + await command.exec() + + 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.fileEquals('app/dtos/user.ts', expectedContents.trim()) + }) +})