better dto/entity structure for GET/POST

This commit is contained in:
Joe Arndt 2026-02-25 14:33:21 -06:00
parent 8f846bd253
commit 389ef8c7ce
9 changed files with 106 additions and 77 deletions

View file

@ -12,6 +12,6 @@ post {
body:json { body:json {
{ {
"name": "Groceries:Food" "name": "Auto:Gas"
} }
} }

View file

@ -12,6 +12,6 @@ post {
body:json { body:json {
{ {
"name": "Walmart" "name": "Casey's"
} }
} }

View file

@ -12,6 +12,6 @@ post {
body:json { body:json {
{ {
"name": "Sienna" "name": "Tundra"
} }
} }

View file

@ -1,5 +1,8 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Temporal } from '@js-temporal/polyfill'; import { Temporal } from '@js-temporal/polyfill';
import { Category } from '../../categories/entities/category.entity';
import { Merchant } from '../../merchants/entities/merchant.entity';
import { Tag } from '../../tags/entities/tag.entity';
export class CreateExpenseDto { export class CreateExpenseDto {
@ApiProperty({ @ApiProperty({
@ -13,23 +16,23 @@ export class CreateExpenseDto {
cents: number; cents: number;
@ApiProperty({ @ApiProperty({
description: 'Category ID of expense' description: 'Category of expense'
}) })
categoryId: string; category: Category;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Optional note about expense' description: 'Optional note about expense'
}) })
note?: string; note: string;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Optional merchant ID for the expense' description: 'Optional merchant for the expense'
}) })
merchantId?: string; merchant: Merchant;
@ApiPropertyOptional({ @ApiPropertyOptional({
type: [String], type: [Tag],
description: 'Optional list of tag IDs for the expense' description: 'Optional list of tags for the expense'
}) })
tagIds: string[]; tags: Tag[];
} }

View file

@ -28,16 +28,16 @@ export class GetExpenseDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Note about expense' description: 'Note about expense'
}) })
note?: string; note: string;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Merchant for the expense' description: 'Merchant for the expense'
}) })
merchant?: Merchant; merchant: Merchant;
@ApiPropertyOptional({ @ApiPropertyOptional({
type: [Tag], type: [Tag],
description: 'List of tags for the expense' description: 'List of tags for the expense'
}) })
tags?: Tag[]; tags: Tag[];
} }

View file

@ -18,7 +18,13 @@ export class Expense {
id: string; id: string;
@Column() @Column()
date: string; year: number;
@Column()
month: number;
@Column()
day: number;
@Column() @Column()
cents: number; cents: number;

View file

@ -1,6 +1,9 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm'; import { DataSource, Repository } from 'typeorm';
import { Expense } from './entities/expense.entity'; import { Expense } from './entities/expense.entity';
import { Category } from '../categories/entities/category.entity';
import { Merchant } from '../merchants/entities/merchant.entity';
import { Tag } from '../tags/entities/tag.entity';
@Injectable() @Injectable()
export class ExpenseDataService { export class ExpenseDataService {
@ -19,14 +22,13 @@ export class ExpenseDataService {
} }
public async create(expense: CreateExpense): Promise<Expense> { public async create(expense: CreateExpense): Promise<Expense> {
const created = await this.expenses.save(expense); return await this.expenses.save<Expense>(expense as Expense);
return await this.expenses.findOneBy({ id: created.id }) as Expense;
} }
public async update(expense: UpdateExpense): Promise<Expense> { // public async update(expense: UpdateExpense): Promise<Expense> {
await this.expenses.save(expense); // await this.expenses.save(expense);
return await this.expenses.findOneBy({ id: expense.id }) as Expense; // return await this.expenses.findOneBy({ id: expense.id }) as Expense;
} // }
public async delete(id: string): Promise<void> { public async delete(id: string): Promise<void> {
await this.expenses.delete({ id }); await this.expenses.delete({ id });
@ -34,18 +36,14 @@ export class ExpenseDataService {
} }
export interface CreateExpense { export interface CreateExpense {
date: string; year: number;
month: number;
day: number;
cents: number; cents: number;
category: { category: Category;
id: string; note: string;
}; merchant: Merchant;
note?: string; tags: Tag[];
merchant?: {
id: string;
};
tags?: {
id: string;
}[];
} }
export interface UpdateExpense extends CreateExpense{ export interface UpdateExpense extends CreateExpense{

View file

@ -48,6 +48,7 @@ export class ExpensesController {
if (!expense) { if (!expense) {
throw new BadRequestException('Expense name cannot be empty.'); throw new BadRequestException('Expense name cannot be empty.');
} }
// TODO: Validate date/cents/category exist...
try { try {
return await this.expensesService.create(expense); return await this.expensesService.create(expense);
@ -57,15 +58,15 @@ export class ExpensesController {
} }
} }
@Put() // @Put()
@HttpCode(HttpStatus.OK) // @HttpCode(HttpStatus.OK)
public async update(@Body() expense: UpdateExpenseDto): Promise<GetExpenseDto> { // public async update(@Body() expense: UpdateExpenseDto): Promise<GetExpenseDto> {
if (!expense.id) { // if (!expense.id) {
throw new BadRequestException('Expense ID cannot be empty.'); // throw new BadRequestException('Expense ID cannot be empty.');
} // }
//
return await this.expensesService.update(expense); // return await this.expensesService.update(expense);
} // }
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)

View file

@ -4,6 +4,7 @@ import { UpdateExpenseDto } from './dto/update-expense.dto';
import { ExpenseDataService } from './expense-data.service'; import { ExpenseDataService } from './expense-data.service';
import { GetExpenseDto } from './dto/get-expense.dto'; import { GetExpenseDto } from './dto/get-expense.dto';
import { Temporal } from '@js-temporal/polyfill'; import { Temporal } from '@js-temporal/polyfill';
import { Expense } from './entities/expense.entity';
@Injectable() @Injectable()
export class ExpensesService { export class ExpensesService {
@ -13,58 +14,78 @@ export class ExpensesService {
const expenses = await this.expenseDataService.getAll(); const expenses = await this.expenseDataService.getAll();
return expenses.map(exp => { return expenses.map(exp => {
return { ...exp, date: Temporal.PlainDate.from(exp.date) }; return {
id: exp.id,
date: new Temporal.PlainDate(exp.year, exp.month, exp.day),
cents: exp.cents,
category: exp.category,
merchant: exp.merchant,
note: exp.note,
tags: exp.tags
};
}) })
} }
public async findById(id: string): Promise<GetExpenseDto> { public async findById(id: string): Promise<GetExpenseDto> {
const expense = await this.expenseDataService.getById(id); const exp = await this.expenseDataService.getById(id);
if (!expense) { if (!exp) {
throw new Error('No expense found'); throw new Error('No expense found');
} }
return { ...expense, date: Temporal.PlainDate.from(expense.date) }; return {
id: exp.id,
date: new Temporal.PlainDate(exp.year, exp.month, exp.day),
cents: exp.cents,
category: exp.category,
merchant: exp.merchant,
note: exp.note,
tags: exp.tags
};
} }
public async create(createExpense: CreateExpenseDto): Promise<GetExpenseDto> { public async create(createExpense: CreateExpenseDto): Promise<GetExpenseDto> {
const date = createExpense.date.toString(); const date = createExpense.date.toString().split('-');
const category = { id: createExpense.categoryId }; const exp = await this.expenseDataService.create({
const merchant = createExpense.merchantId ? { id: createExpense.merchantId } : undefined; year: Number(date[0]),
const tags = createExpense.tagIds?.map(id => { month: Number(date[1]),
return { id }; day: Number(date[2]),
})
const expense = await this.expenseDataService.create({
date,
cents: createExpense.cents, cents: createExpense.cents,
note: createExpense.note, note: createExpense.note,
category, category: createExpense.category,
merchant, merchant: createExpense.merchant,
tags tags: createExpense.tags
}); });
return { ...expense, date: Temporal.PlainDate.from(expense.date) }; return {
id: exp.id,
date: new Temporal.PlainDate(exp.year, exp.month, exp.day),
cents: exp.cents,
category: exp.category,
merchant: exp.merchant,
note: exp.note,
tags: exp.tags
};
} }
public async update(updateExpense: UpdateExpenseDto): Promise<GetExpenseDto> { // public async update(updateExpense: UpdateExpenseDto): Promise<GetExpenseDto> {
const date = updateExpense.date.toString(); // const date = updateExpense.date.toString();
const category = { id: updateExpense.categoryId }; // const category = { id: updateExpense.categoryId };
const merchant = updateExpense.merchantId ? { id: updateExpense.merchantId } : undefined; // const merchant = updateExpense.merchantId ? { id: updateExpense.merchantId } : undefined;
const tags = updateExpense.tagIds?.map(id => { // const tags = updateExpense.tagIds?.map(id => {
return { id }; // return { id };
}) // })
const expense = await this.expenseDataService.update({ // const expense = await this.expenseDataService.update({
id: updateExpense.id, // id: updateExpense.id,
date, // date,
cents: updateExpense.cents, // cents: updateExpense.cents,
note: updateExpense.note, // note: updateExpense.note,
category, // category,
merchant, // merchant,
tags // tags
}); // });
//
return { ...expense, date: Temporal.PlainDate.from(expense.date) }; // return { ...expense, date: Temporal.PlainDate.from(expense.date) };
} // }
public async remove(id: string): Promise<void> { public async remove(id: string): Promise<void> {
await this.expenseDataService.delete(id); await this.expenseDataService.delete(id);