diff --git a/.gitignore b/.gitignore index 4b56acf..2709a60 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Database +common-cents.db \ No newline at end of file diff --git a/bruno/Common Cents/Categories/folder.bru b/bruno/Common Cents/Categories/folder.bru index c32401e..ae65a21 100644 --- a/bruno/Common Cents/Categories/folder.bru +++ b/bruno/Common Cents/Categories/folder.bru @@ -1,5 +1,6 @@ meta { name: Categories + seq: 2 } auth { diff --git a/bruno/Common Cents/Expenses/LOC DEL Expense.bru b/bruno/Common Cents/Expenses/LOC DEL Expense.bru deleted file mode 100644 index dfb6024..0000000 --- a/bruno/Common Cents/Expenses/LOC DEL Expense.bru +++ /dev/null @@ -1,11 +0,0 @@ -meta { - name: LOC DEL Expense - type: http - seq: 3 -} - -delete { - url: {{localBaseUrl}}/expenses/2aa94170-2c57-4c4f-a1e7-13544ba72917 - body: none - auth: inherit -} diff --git a/bruno/Common Cents/Expenses/LOC DELETE Expense.bru b/bruno/Common Cents/Expenses/LOC DELETE Expense.bru new file mode 100644 index 0000000..6725687 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC DELETE Expense.bru @@ -0,0 +1,15 @@ +meta { + name: LOC DELETE Expense + type: http + seq: 5 +} + +delete { + url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}} + body: none + auth: inherit +} + +vars:pre-request { + resourceId: 41294f87-ab77-4fdd-9509-6968e6533962 +} diff --git a/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru b/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru index 935e4ee..c591a98 100644 --- a/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru +++ b/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru @@ -5,7 +5,11 @@ meta { } get { - url: {{localBaseUrl}}/expenses/2aa94170-2c57-4c4f-a1e7-13544ba72917 + url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}} body: none auth: inherit } + +vars:pre-request { + resourceId: 1d6d2842-b271-489b-bd93-e3ceaee5a139 +} diff --git a/bruno/Common Cents/Expenses/LOC GET Expenses.bru b/bruno/Common Cents/Expenses/LOC GET Expenses.bru index b2e8e2c..17a85b6 100644 --- a/bruno/Common Cents/Expenses/LOC GET Expenses.bru +++ b/bruno/Common Cents/Expenses/LOC GET Expenses.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{localBaseUrl}}/expenses + url: {{localBaseUrl}}/{{resourcePath}} body: none auth: inherit } diff --git a/bruno/Common Cents/Expenses/LOC PATCH Expense Full.bru b/bruno/Common Cents/Expenses/LOC PATCH Expense Full.bru deleted file mode 100644 index e9a5f02..0000000 --- a/bruno/Common Cents/Expenses/LOC PATCH Expense Full.bru +++ /dev/null @@ -1,23 +0,0 @@ -meta { - name: LOC PATCH Expense Full - type: http - seq: 6 -} - -patch { - url: {{localBaseUrl}}/expenses/0708e7f7-3a2a-4b93-81da-38954925ca78 - body: json - auth: inherit -} - -body:json { - { - "date": "2025-12-15", - "cents": 888, - "categoryId": "1", - "merchantId": "1", - "subcategoryIds": ["2"], - "tagIds": ["2"], - "description": "PATCHED Desc." - } -} diff --git a/bruno/Common Cents/Expenses/LOC POST Expense Full.bru b/bruno/Common Cents/Expenses/LOC POST Expense Full.bru deleted file mode 100644 index d0e6451..0000000 --- a/bruno/Common Cents/Expenses/LOC POST Expense Full.bru +++ /dev/null @@ -1,23 +0,0 @@ -meta { - name: LOC POST Expense Full - type: http - seq: 4 -} - -post { - url: {{localBaseUrl}}/expenses - body: json - auth: inherit -} - -body:json { - { - "date": "2025-12-01", - "cents": 987, - "categoryId": "1", - "merchantId": "1", - "subcategoryIds": ["1","2"], - "tagIds": ["1","2"], - "description": "NEW FULL expense" - } -} diff --git a/bruno/Common Cents/Expenses/LOC POST Expense Partial.bru b/bruno/Common Cents/Expenses/LOC POST Expense Partial.bru deleted file mode 100644 index bfc7302..0000000 --- a/bruno/Common Cents/Expenses/LOC POST Expense Partial.bru +++ /dev/null @@ -1,20 +0,0 @@ -meta { - name: LOC POST Expense Partial - type: http - seq: 5 -} - -post { - url: {{localBaseUrl}}/expenses - body: json - auth: inherit -} - -body:json { - { - "date": "2025-12-01", - "cents": 987, - "categoryId": "1", - "description": "NEW PARTIAL expense" - } -} diff --git a/bruno/Common Cents/Expenses/LOC POST Expense.bru b/bruno/Common Cents/Expenses/LOC POST Expense.bru new file mode 100644 index 0000000..a458095 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC POST Expense.bru @@ -0,0 +1,38 @@ +meta { + name: LOC POST Expense + type: http + seq: 3 +} + +post { + url: {{localBaseUrl}}/{{resourcePath}} + body: json + auth: inherit +} + +body:json { + { + "year": "2026", + "month": "01", + "day": "02", + "cents": 1000, + "description": "With cat, subcat and merchant and two tags", + "category": { + "id": "a73fbfdd-1949-4be9-8cdc-ea9197b9b8b6" + }, + "subCategory": { + "id": "270ceaea-9cb8-4c6a-846f-ea35ed4d12f7" + }, + "merchant": { + "id": "61246db4-3110-4fe2-bdab-d819b8fd0705" + }, + "tags": [ + { + "id": "2616f724-f3ce-46df-8b30-897f147f6b74" + }, + { + "id": "16ed84e0-2d17-45c7-ada8-6fe7f8cc8720" + } + ] + } +} diff --git a/bruno/Common Cents/Expenses/LOC PUT Expense.bru b/bruno/Common Cents/Expenses/LOC PUT Expense.bru new file mode 100644 index 0000000..7042846 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC PUT Expense.bru @@ -0,0 +1,21 @@ +meta { + name: LOC PUT Expense + type: http + seq: 4 +} + +put { + url: {{localBaseUrl}}/{{resourcePath}} + body: json + auth: inherit +} + +body:json { + { + "id": "0254a07d-9dfa-4c06-b7b3-48d30efff991", + "description": "Cat, no sub-cat or merchant", + "category": { + "id": "db434e98-5f32-4202-b69c-eb7d1d248b3e" + } + } +} diff --git a/bruno/Common Cents/Expenses/folder.bru b/bruno/Common Cents/Expenses/folder.bru index 9a3fbf0..5e0ac02 100644 --- a/bruno/Common Cents/Expenses/folder.bru +++ b/bruno/Common Cents/Expenses/folder.bru @@ -1,4 +1,11 @@ meta { name: Expenses - seq: 2 +} + +auth { + mode: inherit +} + +vars:pre-request { + resourcePath: expenses } diff --git a/bruno/Common Cents/Merchants/folder.bru b/bruno/Common Cents/Merchants/folder.bru index 2ffb460..dc13882 100644 --- a/bruno/Common Cents/Merchants/folder.bru +++ b/bruno/Common Cents/Merchants/folder.bru @@ -1,5 +1,6 @@ meta { name: Merchants + seq: 3 } auth { diff --git a/bruno/Common Cents/Sub-categories/folder.bru b/bruno/Common Cents/Sub-categories/folder.bru index a7f720c..47ece91 100644 --- a/bruno/Common Cents/Sub-categories/folder.bru +++ b/bruno/Common Cents/Sub-categories/folder.bru @@ -1,5 +1,6 @@ meta { name: Sub-categories + seq: 4 } auth { diff --git a/bruno/Common Cents/Tags/folder.bru b/bruno/Common Cents/Tags/folder.bru index aa07bea..ba06b7e 100644 --- a/bruno/Common Cents/Tags/folder.bru +++ b/bruno/Common Cents/Tags/folder.bru @@ -1,5 +1,6 @@ meta { name: Tags + seq: 5 } auth { diff --git a/common-cents.db b/common-cents.db deleted file mode 100644 index 656df02..0000000 Binary files a/common-cents.db and /dev/null differ diff --git a/src/app.module.ts b/src/app.module.ts index fd2edf1..4a525e2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,12 +9,16 @@ import { CategoriesModule } from './categories/categories.module'; import { Category } from './categories/entities/category.entity'; import { SubCategoriesModule } from './sub-categories/sub-categories.module'; import { SubCategory } from './sub-categories/entities/sub-category.entity'; +import { ExpensesModule } from './expenses/expenses.module'; +import { Expense } from './expenses/entities/expense.entity'; + +const entities = [Merchant, Tag, Category, SubCategory, Expense]; const sqliteConfig: TypeOrmModuleOptions = { - synchronize: true, // typeorm -h (schema:sync) + synchronize: true, type: 'sqlite', database: 'common-cents.db', - entities: [Merchant, Tag, Category, SubCategory] + entities } @Module({ @@ -23,9 +27,9 @@ const sqliteConfig: TypeOrmModuleOptions = { MerchantsModule, TagsModule, CategoriesModule, - SubCategoriesModule + SubCategoriesModule, + ExpensesModule ], - controllers: [AppController], - providers: [] + controllers: [AppController] }) export class AppModule { } diff --git a/src/categories/entities/category.entity.ts b/src/categories/entities/category.entity.ts index 25ee60c..806866f 100644 --- a/src/categories/entities/category.entity.ts +++ b/src/categories/entities/category.entity.ts @@ -5,6 +5,6 @@ export class Category { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ unique: true }) name: string; } diff --git a/src/expenses/dto/create-expense.dto.ts b/src/expenses/dto/create-expense.dto.ts new file mode 100644 index 0000000..52a8f38 --- /dev/null +++ b/src/expenses/dto/create-expense.dto.ts @@ -0,0 +1,10 @@ +import { Category } from '../../categories/entities/category.entity'; + +export class CreateExpenseDto { + year: string; + month: string; + day: string; + cents: number; + description: string; + category: Category +} diff --git a/src/expenses/dto/update-expense.dto.ts b/src/expenses/dto/update-expense.dto.ts new file mode 100644 index 0000000..e5be379 --- /dev/null +++ b/src/expenses/dto/update-expense.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateExpenseDto } from './create-expense.dto'; + +export class UpdateExpenseDto extends PartialType(CreateExpenseDto) { + id: string; +} diff --git a/src/expenses/entities/expense.entity.ts b/src/expenses/entities/expense.entity.ts new file mode 100644 index 0000000..36a116d --- /dev/null +++ b/src/expenses/entities/expense.entity.ts @@ -0,0 +1,39 @@ +import { Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Tag } from '../../tags/entities/tag.entity'; +import { SubCategory } from '../../sub-categories/entities/sub-category.entity'; +import { Category } from '../../categories/entities/category.entity'; +import { Merchant } from '../../merchants/entities/merchant.entity'; + +@Entity() +export class Expense { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + year: string; + + @Column() + month: string; + + @Column() + day: string; + + @Column() + cents: number; + + @Column({ nullable: true }) + description: string; + + @ManyToOne(() => Category, { eager: true }) + category: Category; + + @ManyToOne(() => SubCategory, { nullable: true, eager: true }) + subCategory: SubCategory; + + @ManyToOne(() => Merchant, { nullable: true, eager: true }) + merchant: Merchant; + + @ManyToMany(() => Tag, { nullable: true, eager: true }) + @JoinTable() + tags: Tag[]; +} diff --git a/src/expenses/expense-data.service.ts b/src/expenses/expense-data.service.ts new file mode 100644 index 0000000..ceff180 --- /dev/null +++ b/src/expenses/expense-data.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource, Repository } from 'typeorm'; +import { Expense } from './entities/expense.entity'; +import { UpdateExpenseDto } from './dto/update-expense.dto'; +import { CreateExpenseDto } from './dto/create-expense.dto'; + +@Injectable() +export class ExpenseDataService { + private expenses: Repository; + + public constructor(private dataSource: DataSource) { + this.expenses = this.dataSource.getRepository(Expense); + } + + public async getAll(): Promise { + return await this.expenses.find(); + } + + public async getById(id: string): Promise { + return await this.expenses.findOneBy({ id }); + } + + public async create(expense: CreateExpenseDto): Promise { + return await this.expenses.save(expense); + } + + public async update(expense: UpdateExpenseDto): Promise { + return await this.expenses.save(expense); + } + + public async delete(id: string): Promise { + await this.expenses.delete({ id }); + } +} diff --git a/src/expenses/expenses.controller.ts b/src/expenses/expenses.controller.ts new file mode 100644 index 0000000..98350b9 --- /dev/null +++ b/src/expenses/expenses.controller.ts @@ -0,0 +1,75 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + HttpCode, + HttpStatus, + BadRequestException, + NotFoundException, + InternalServerErrorException +} from '@nestjs/common'; +import { ExpensesService } from './expenses.service'; +import { CreateExpenseDto } from './dto/create-expense.dto'; +import { UpdateExpenseDto } from './dto/update-expense.dto'; +import { Expense } from './entities/expense.entity'; + +@Controller('expenses') +export class ExpensesController { + constructor(private readonly expensesService: ExpensesService) { } + + @Get() + @HttpCode(HttpStatus.OK) + public async findAll(): Promise { + return await this.expensesService.findAll(); + } + + @Get(':id') + @HttpCode(HttpStatus.OK) + public async findOne(@Param('id') id: string): Promise { + if (!id) { + throw new BadRequestException('No ID provided.'); + } + + try { + return await this.expensesService.findById(id); + } + catch (error) { + throw new NotFoundException(error); + } + } + + @Post() + @HttpCode(HttpStatus.CREATED) + public async create(@Body() expense: CreateExpenseDto): Promise { + if (!expense) { + throw new BadRequestException('Expense name cannot be empty.'); + } + + try { + return await this.expensesService.create(expense); + } + catch (error) { + throw new InternalServerErrorException(error); + } + } + + @Put() + @HttpCode(HttpStatus.OK) + public async update(@Body() expense: UpdateExpenseDto): Promise { + if (!expense.id) { + throw new BadRequestException('Expense ID cannot be empty.'); + } + + return await this.expensesService.update(expense); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + public async remove(@Param('id') id: string): Promise { + return await this.expensesService.remove(id); + } +} diff --git a/src/expenses/expenses.module.ts b/src/expenses/expenses.module.ts new file mode 100644 index 0000000..2179916 --- /dev/null +++ b/src/expenses/expenses.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ExpensesService } from './expenses.service'; +import { ExpensesController } from './expenses.controller'; +import { ExpenseDataService } from './expense-data.service'; + +@Module({ + controllers: [ExpensesController], + providers: [ExpensesService, ExpenseDataService], +}) +export class ExpensesModule { } diff --git a/src/expenses/expenses.service.ts b/src/expenses/expenses.service.ts new file mode 100644 index 0000000..4e822e6 --- /dev/null +++ b/src/expenses/expenses.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { CreateExpenseDto } from './dto/create-expense.dto'; +import { UpdateExpenseDto } from './dto/update-expense.dto'; +import { ExpenseDataService } from './expense-data.service'; +import { Expense } from './entities/expense.entity'; + +@Injectable() +export class ExpensesService { + public constructor(private expenseDataService: ExpenseDataService) { } + + public async findAll(): Promise { + return await this.expenseDataService.getAll(); + } + + public async findById(id: string): Promise { + const expense = await this.expenseDataService.getById(id); + if (!expense) { + throw new Error('No expense found'); + } + + return expense; + } + + public async create(expense: CreateExpenseDto): Promise { + return await this.expenseDataService.create(expense); + } + + public async update(expense: UpdateExpenseDto): Promise { + return await this.expenseDataService.update(expense); + } + + public async remove(id: string): Promise { + await this.expenseDataService.delete(id); + } +} diff --git a/src/merchants/entities/merchant.entity.ts b/src/merchants/entities/merchant.entity.ts index 0a5297f..87a61f9 100644 --- a/src/merchants/entities/merchant.entity.ts +++ b/src/merchants/entities/merchant.entity.ts @@ -5,6 +5,6 @@ export class Merchant { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ unique: true }) name: string; } diff --git a/src/sub-categories/entities/sub-category.entity.ts b/src/sub-categories/entities/sub-category.entity.ts index 87f85ae..f6bf282 100644 --- a/src/sub-categories/entities/sub-category.entity.ts +++ b/src/sub-categories/entities/sub-category.entity.ts @@ -5,6 +5,6 @@ export class SubCategory { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ unique: true }) name: string; } diff --git a/src/tags/entities/tag.entity.ts b/src/tags/entities/tag.entity.ts index fdf7a2d..ed84b59 100644 --- a/src/tags/entities/tag.entity.ts +++ b/src/tags/entities/tag.entity.ts @@ -5,6 +5,6 @@ export class Tag { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ unique: true }) name: string; }