Add TypeORM DB and Resources #2

Merged
joe merged 8 commits from add-sqlite into master 2026-02-09 00:10:26 +00:00
64 changed files with 2916 additions and 360 deletions

View file

@ -1,6 +0,0 @@
{
"singleQuote": true,
"trailingComma": "none",
"printWidth": 160,
"bracketSameLine": true
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC DELETE Category
type: http
seq: 5
}
delete {
url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}}
body: none
auth: inherit
}
vars:pre-request {
resourceId: cbf30070-9ff7-419f-a567-f7d145be445b
}

View file

@ -0,0 +1,11 @@
meta {
name: LOC GET Categories
type: http
seq: 1
}
get {
url: {{localBaseUrl}}/{{resourcePath}}
body: none
auth: inherit
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC GET Category By ID
type: http
seq: 2
}
get {
url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}}
body: none
auth: inherit
}
vars:pre-request {
resourceId: 1d6d2842-b271-489b-bd93-e3ceaee5a139
}

View file

@ -0,0 +1,17 @@
meta {
name: LOC POST Category
type: http
seq: 3
}
post {
url: {{localBaseUrl}}/{{resourcePath}}
body: json
auth: inherit
}
body:json {
{
"name": "Category Three"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: LOC PUT Category
type: http
seq: 4
}
put {
url: {{localBaseUrl}}/{{resourcePath}}
body: json
auth: inherit
}
body:json {
{
"id": "1d6d2842-b271-489b-bd93-e3ceaee5a139",
"name": "Merchant One"
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Categories
}
auth {
mode: inherit
}
vars:pre-request {
resourcePath: categories
}

View file

@ -1,3 +1,4 @@
meta {
name: Expenses
seq: 2
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC DELETE Merchant
type: http
seq: 5
}
delete {
url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}}
body: none
auth: inherit
}
vars:pre-request {
resourceId: cbf30070-9ff7-419f-a567-f7d145be445b
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC GET Merchant By ID
type: http
seq: 2
}
get {
url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}}
body: none
auth: inherit
}
vars:pre-request {
resourceId: 1d6d2842-b271-489b-bd93-e3ceaee5a139
}

View file

@ -0,0 +1,11 @@
meta {
name: LOC GET Merchants
type: http
seq: 1
}
get {
url: {{localBaseUrl}}/{{resourcePath}}
body: none
auth: inherit
}

View file

@ -0,0 +1,17 @@
meta {
name: LOC POST Merchant
type: http
seq: 3
}
post {
url: {{localBaseUrl}}/{{resourcePath}}
body: json
auth: inherit
}
body:json {
{
"name": "Merchant Three"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: LOC PUT Merchant
type: http
seq: 4
}
put {
url: {{localBaseUrl}}/{{resourcePath}}
body: json
auth: inherit
}
body:json {
{
"id": "1d6d2842-b271-489b-bd93-e3ceaee5a139",
"name": "Merchant One"
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Merchants
}
auth {
mode: inherit
}
vars:pre-request {
resourcePath: merchants
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC DELETE Sub-category
type: http
seq: 5
}
delete {
url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}}
body: none
auth: inherit
}
vars:pre-request {
resourceId: cbf30070-9ff7-419f-a567-f7d145be445b
}

View file

@ -0,0 +1,11 @@
meta {
name: LOC GET Sub-categories
type: http
seq: 1
}
get {
url: {{localBaseUrl}}/{{resourcePath}}
body: none
auth: inherit
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC GET Sub-category By ID
type: http
seq: 2
}
get {
url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}}
body: none
auth: inherit
}
vars:pre-request {
resourceId: 1d6d2842-b271-489b-bd93-e3ceaee5a139
}

View file

@ -0,0 +1,17 @@
meta {
name: LOC POST Sub-category
type: http
seq: 3
}
post {
url: {{localBaseUrl}}/{{resourcePath}}
body: json
auth: inherit
}
body:json {
{
"name": "Sub-category Three"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: LOC PUT Sub-category
type: http
seq: 4
}
put {
url: {{localBaseUrl}}/{{resourcePath}}
body: json
auth: inherit
}
body:json {
{
"id": "1d6d2842-b271-489b-bd93-e3ceaee5a139",
"name": "Merchant One"
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Sub-categories
}
auth {
mode: inherit
}
vars:pre-request {
resourcePath: sub-categories
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC DELETE Tag
type: http
seq: 5
}
delete {
url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}}
body: none
auth: inherit
}
vars:pre-request {
resourceId: cbf30070-9ff7-419f-a567-f7d145be445b
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC GET Tag By ID
type: http
seq: 2
}
get {
url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}}
body: none
auth: inherit
}
vars:pre-request {
resourceId: 1d6d2842-b271-489b-bd93-e3ceaee5a139
}

View file

@ -0,0 +1,11 @@
meta {
name: LOC GET Tags
type: http
seq: 1
}
get {
url: {{localBaseUrl}}/{{resourcePath}}
body: none
auth: inherit
}

View file

@ -0,0 +1,17 @@
meta {
name: LOC POST Tag
type: http
seq: 3
}
post {
url: {{localBaseUrl}}/{{resourcePath}}
body: json
auth: inherit
}
body:json {
{
"name": "Tag Three"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: LOC PUT Tag
type: http
seq: 4
}
put {
url: {{localBaseUrl}}/{{resourcePath}}
body: json
auth: inherit
}
body:json {
{
"id": "1d6d2842-b271-489b-bd93-e3ceaee5a139",
"name": "Merchant One"
}
}

View file

@ -0,0 +1,11 @@
meta {
name: Tags
}
auth {
mode: inherit
}
vars:pre-request {
resourcePath: tags
}

BIN
common-cents.db Normal file

Binary file not shown.

View file

@ -1,6 +1,5 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
@ -10,7 +9,6 @@ export default tseslint.config(
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
@ -21,15 +19,14 @@ export default tseslint.config(
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
}
}
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
'@typescript-eslint/no-unsafe-argument': 'warn'
}
}
);

1929
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -24,8 +24,11 @@
"@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"sqlite3": "^5.1.7",
"typeorm": "^0.3.28"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@ -42,7 +45,6 @@
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",

View file

@ -1,22 +0,0 @@
// import { Test, TestingModule } from '@nestjs/testing';
// import { AppController } from './app.controller';
// import { AppService } from './app.service';
//
// describe('AppController', () => {
// let appController: AppController;
//
// beforeEach(async () => {
// const app: TestingModule = await Test.createTestingModule({
// controllers: [AppController],
// providers: [AppService],
// }).compile();
//
// appController = app.get<AppController>(AppController);
// });
//
// describe('root', () => {
// it('should return "Hello World!"', () => {
// expect(appController.getHello()).toBe('Hello World!');
// });
// });
// });

View file

@ -1,11 +1,31 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ExpensesService } from './services/expenses.service';
import { ExpensesController } from './controllers/expenses/expenses.controller';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { MerchantsModule } from './merchants/merchants.module';
import { Merchant } from './merchants/entities/merchant.entity';
import { TagsModule } from './tags/tags.module';
import { Tag } from './tags/entities/tag.entity';
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';
const sqliteConfig: TypeOrmModuleOptions = {
synchronize: true, // typeorm -h (schema:sync)
type: 'sqlite',
database: 'common-cents.db',
entities: [Merchant, Tag, Category, SubCategory]
}
@Module({
imports: [],
controllers: [AppController, ExpensesController],
providers: [ExpensesService]
imports: [
TypeOrmModule.forRoot(sqliteConfig),
MerchantsModule,
TagsModule,
CategoriesModule,
SubCategoriesModule
],
controllers: [AppController],
providers: []
})
export class AppModule { }

View file

@ -0,0 +1,75 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
HttpCode,
HttpStatus,
BadRequestException,
NotFoundException,
InternalServerErrorException
} from '@nestjs/common';
import { CategoriesService } from './categories.service';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
import { Category } from './entities/category.entity';
@Controller('categories')
export class CategoriesController {
constructor(private readonly categoriesService: CategoriesService) { }
@Get()
@HttpCode(HttpStatus.OK)
public async findAll(): Promise<Category[]> {
return await this.categoriesService.findAll();
}
@Get(':id')
@HttpCode(HttpStatus.OK)
public async findOne(@Param('id') id: string): Promise<Category> {
if (!id) {
throw new BadRequestException('No ID provided.');
}
try {
return await this.categoriesService.findById(id);
}
catch (error) {
throw new NotFoundException(error);
}
}
@Post()
@HttpCode(HttpStatus.CREATED)
public async create(@Body() category: CreateCategoryDto): Promise<Category> {
if (!category.name) {
throw new BadRequestException('Category name cannot be empty.');
}
try {
return await this.categoriesService.create(category);
}
catch (error) {
throw new InternalServerErrorException(error);
}
}
@Put()
@HttpCode(HttpStatus.OK)
public async update(@Body() category: UpdateCategoryDto): Promise<Category> {
if (!category.id) {
throw new BadRequestException('Category ID cannot be empty.');
}
return await this.categoriesService.update(category);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
public async remove(@Param('id') id: string): Promise<void> {
return await this.categoriesService.remove(id);
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { CategoriesService } from './categories.service';
import { CategoriesController } from './categories.controller';
import { CategoryDataService } from './category-data.service';
@Module({
controllers: [CategoriesController],
providers: [CategoriesService, CategoryDataService]
})
export class CategoriesModule { }

View file

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
import { CategoryDataService } from './category-data.service';
import { Category } from './entities/category.entity';
@Injectable()
export class CategoriesService {
public constructor(private categoryDataService: CategoryDataService) { }
public async findAll(): Promise<Category[]> {
return await this.categoryDataService.getAll();
}
public async findById(id: string): Promise<Category> {
const category = await this.categoryDataService.getById(id);
if (!category) {
throw new Error('No category found');
}
return category;
}
public async create(category: CreateCategoryDto): Promise<Category> {
return await this.categoryDataService.create(category.name);
}
public async update(category: UpdateCategoryDto): Promise<Category> {
return await this.categoryDataService.update(category);
}
public async remove(id: string): Promise<void> {
await this.categoryDataService.delete(id);
}
}

View file

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Category } from './entities/category.entity';
import { UpdateCategoryDto } from './dto/update-category.dto';
@Injectable()
export class CategoryDataService {
private categories: Repository<Category>;
public constructor(private dataSource: DataSource) {
this.categories = this.dataSource.getRepository(Category);
}
public async getAll(): Promise<Category[]> {
return await this.categories.find();
}
public async getById(id: string): Promise<Category | null> {
return await this.categories.findOneBy({ id });
}
public async create(name: string): Promise<Category> {
return await this.categories.save({ name });
}
public async update(category: UpdateCategoryDto): Promise<Category> {
return await this.categories.save(category);
}
public async delete(id: string): Promise<void> {
await this.categories.delete({ id });
}
}

View file

@ -0,0 +1,3 @@
export class CreateCategoryDto {
name: string;
}

View file

@ -0,0 +1,4 @@
export class UpdateCategoryDto {
id: string;
name: string;
}

View file

@ -0,0 +1,10 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Category {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
}

View file

@ -1,24 +0,0 @@
import { PartialType } from '@nestjs/mapped-types';
export class CreateExpenseDto {
date: Date;
cents: number;
categoryId: string;
merchantId?: string;
subcategoryIds?: string[];
tagIds?: string[];
description?: string;
}
export class UpdateExpenseDto extends PartialType(CreateExpenseDto) {}
export class GetExpenseDto {
id: string;
date: Date;
cents: number;
category: string;
merchant?: string;
subcategories?: string[];
tags?: string[];
description?: string;
}

View file

@ -1,30 +0,0 @@
export class Expense {
id: string;
date: Date;
cents: number;
categoryId: string;
merchantId?: string;
subcategoryIds: string[];
tagIds: string[];
description?: string;
}
export class Category {
id: string;
category: string;
}
export class SubCategory {
id: string;
subcategory: string;
}
export class Merchant {
id: string;
merchant: string;
}
export class Tag {
id: string;
tag: string;
}

View file

@ -1,44 +0,0 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, NotFoundException, HttpCode } from '@nestjs/common';
import { ExpensesService } from '../../services/expenses.service';
import { CreateExpenseDto, UpdateExpenseDto } from './expense.dto';
@Controller('expenses')
export class ExpensesController {
constructor(private readonly expensesService: ExpensesService) {}
@Post()
create(@Body() createExpenseDto: CreateExpenseDto) {
return this.expensesService.create(createExpenseDto);
}
@Get()
findAll() {
return this.expensesService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
const expense = this.expensesService.findOne(id);
if (!expense) {
throw new NotFoundException();
}
return expense;
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateExpenseDto: UpdateExpenseDto) {
const expense = this.expensesService.update(id, updateExpenseDto);
if (!expense) {
throw new NotFoundException();
}
return expense;
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id: string) {
return this.expensesService.remove(id);
}
}

View file

@ -0,0 +1,3 @@
export class CreateMerchantDto {
name: string;
}

View file

@ -0,0 +1,4 @@
export class UpdateMerchantDto {
id: string;
name: string;
}

View file

@ -0,0 +1,10 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Merchant {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
}

View file

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Merchant } from './entities/merchant.entity';
import { UpdateMerchantDto } from './dto/update-merchant.dto';
@Injectable()
export class MerchantDataService {
private merchants: Repository<Merchant>;
public constructor(private dataSource: DataSource) {
this.merchants = this.dataSource.getRepository(Merchant);
}
public async getAll(): Promise<Merchant[]> {
return await this.merchants.find();
}
public async getById(id: string): Promise<Merchant | null> {
return await this.merchants.findOneBy({ id });
}
public async create(name: string): Promise<Merchant> {
return await this.merchants.save({ name });
}
public async update(merchant: UpdateMerchantDto): Promise<Merchant> {
return await this.merchants.save(merchant);
}
public async delete(id: string): Promise<void> {
await this.merchants.delete({ id });
}
}

View file

@ -0,0 +1,75 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
HttpCode,
HttpStatus,
NotFoundException,
BadRequestException,
InternalServerErrorException
} from '@nestjs/common';
import { MerchantsService } from './merchants.service';
import { CreateMerchantDto } from './dto/create-merchant.dto';
import { UpdateMerchantDto } from './dto/update-merchant.dto';
import { Merchant } from './entities/merchant.entity';
@Controller('merchants')
export class MerchantsController {
public constructor(private readonly merchantsService: MerchantsService) { }
@Get()
@HttpCode(HttpStatus.OK)
public async findAll(): Promise<Merchant[]> {
return await this.merchantsService.findAll();
}
@Get(':id')
@HttpCode(HttpStatus.OK)
public async findOne(@Param('id') id: string): Promise<Merchant> {
if (!id) {
throw new BadRequestException('No ID provided.');
}
try {
return await this.merchantsService.findById(id);
}
catch (error) {
throw new NotFoundException(error);
}
}
@Post()
@HttpCode(HttpStatus.CREATED)
public async create(@Body() merchant: CreateMerchantDto): Promise<Merchant> {
if (!merchant.name) {
throw new BadRequestException('Merchant name cannot be empty.');
}
try {
return await this.merchantsService.create(merchant);
}
catch (error) {
throw new InternalServerErrorException(error);
}
}
@Put()
@HttpCode(HttpStatus.OK)
public async update(@Body() merchant: UpdateMerchantDto): Promise<Merchant> {
if (!merchant.id) {
throw new BadRequestException('Merchant ID cannot be empty.');
}
return await this.merchantsService.update(merchant);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
public async remove(@Param('id') id: string): Promise<void> {
return await this.merchantsService.remove(id);
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MerchantsService } from './merchants.service';
import { MerchantsController } from './merchants.controller';
import { MerchantDataService } from './merchant-data.service';
@Module({
controllers: [MerchantsController],
providers: [MerchantsService, MerchantDataService]
})
export class MerchantsModule { }

View file

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { CreateMerchantDto } from './dto/create-merchant.dto';
import { UpdateMerchantDto } from './dto/update-merchant.dto';
import { MerchantDataService } from './merchant-data.service';
import { Merchant } from './entities/merchant.entity';
@Injectable()
export class MerchantsService {
public constructor(private merchantDataService: MerchantDataService) { }
public async findAll(): Promise<Merchant[]> {
return await this.merchantDataService.getAll();
}
public async findById(id: string): Promise<Merchant> {
const merchant = await this.merchantDataService.getById(id);
if (!merchant) {
throw new Error('Merchant not found.');
}
return merchant;
}
public async create(merchant: CreateMerchantDto): Promise<Merchant> {
return await this.merchantDataService.create(merchant.name);
}
public async update(merchant: UpdateMerchantDto): Promise<Merchant> {
return await this.merchantDataService.update(merchant);
}
public async remove(id: string): Promise<void> {
await this.merchantDataService.delete(id);
}
}

View file

@ -1,141 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Category, Expense, Merchant, SubCategory, Tag } from '../controllers/expenses/expense.entities';
import { CreateExpenseDto, GetExpenseDto, UpdateExpenseDto } from '../controllers/expenses/expense.dto';
import { randomUUID } from 'node:crypto';
@Injectable()
export class ExpensesService {
private categories: Category[] = [
{
id: '1',
category: 'Category 1'
}
];
private merchants: Merchant[] = [
{
id: '1',
merchant: 'Merchant 1'
}
];
private subcategories: SubCategory[] = [
{
id: '1',
subcategory: 'Subcategory 1'
},
{
id: '2',
subcategory: 'Subcategory 2'
}
];
private tags: Tag[] = [
{
id: '1',
tag: 'Tag 1'
},
{
id: '2',
tag: 'Tag 2'
}
];
private expenses: Expense[] = [
{
id: '2aa94170-2c57-4c4f-a1e7-13544ba72917',
date: new Date('2025-12-01'),
cents: 15443,
categoryId: '1',
merchantId: '1',
subcategoryIds: ['1', '2'],
tagIds: ['1', '2'],
description: 'Full existing expense'
},
{
id: '0708e7f7-3a2a-4b93-81da-38954925ca78',
date: new Date('2025-12-02'),
cents: 8723,
categoryId: '1',
subcategoryIds: [],
tagIds: [],
description: 'Partial existing expense'
}
];
public create(createExpenseDto: CreateExpenseDto) {
const expense = {
id: randomUUID(),
date: new Date(createExpenseDto.date),
cents: createExpenseDto.cents,
categoryId: createExpenseDto.categoryId,
merchantId: createExpenseDto.merchantId,
subcategoryIds: createExpenseDto.subcategoryIds ?? [],
tagIds: createExpenseDto.tagIds ?? [],
description: createExpenseDto.description
} as Expense;
this.expenses.push(expense);
return this.mapExpense(expense);
}
public findAll() {
return this.expenses.map((expense) => {
return this.mapExpense(expense);
});
}
public findOne(id: string) {
const expense = this.getExpense(id);
return expense ? this.mapExpense(expense) : undefined;
}
public update(id: string, updateExpenseDto: UpdateExpenseDto) {
let index;
const expense = this.expenses.find((exp, idx) => {
if (exp.id === id) {
index = idx;
return exp;
}
});
if (expense && index) {
this.expenses[index] = {
...expense,
...updateExpenseDto
};
return this.mapExpense(this.expenses[index]);
}
return undefined;
}
public remove(id: string) {
this.expenses = this.expenses.filter((expense) => expense.id !== id);
}
private getExpense(id: string) {
return this.expenses.find((expense) => expense.id === id);
}
private getSubcategories(ids: string[]) {
return this.subcategories.filter((sub) => ids.includes(sub.id)).map((s) => s.subcategory);
}
private getTags(ids: string[]) {
return this.tags.filter((tag) => ids.includes(tag.id)).map((t) => t.tag);
}
private mapExpense(expense: Expense) {
const category = this.categories.find((category) => category.id === expense.categoryId);
const merchant = this.merchants.find((merchant) => merchant.id === expense.merchantId);
return {
id: expense.id,
date: expense.date,
cents: expense.cents,
category: category?.category,
merchant: merchant?.merchant,
subcategories: this.getSubcategories(expense.subcategoryIds),
tags: this.getTags(expense.tagIds),
description: expense.description
} as GetExpenseDto;
}
}

View file

@ -0,0 +1,3 @@
export class CreateSubCategoryDto {
name: string;
}

View file

@ -0,0 +1,4 @@
export class UpdateSubCategoryDto {
id: string;
name: string;
}

View file

@ -0,0 +1,10 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class SubCategory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
}

View file

@ -0,0 +1,75 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
HttpCode,
HttpStatus,
BadRequestException,
NotFoundException,
InternalServerErrorException
} from '@nestjs/common';
import { SubCategoriesService } from './sub-categories.service';
import { CreateSubCategoryDto } from './dto/create-sub-category.dto';
import { UpdateSubCategoryDto } from './dto/update-sub-category.dto';
import { SubCategory } from './entities/sub-category.entity';
@Controller('sub-categories')
export class SubCategoriesController {
constructor(private readonly subCategoriesService: SubCategoriesService) { }
@Get()
@HttpCode(HttpStatus.OK)
public async findAll(): Promise<SubCategory[]> {
return await this.subCategoriesService.findAll();
}
@Get(':id')
@HttpCode(HttpStatus.OK)
public async findOne(@Param('id') id: string): Promise<SubCategory> {
if (!id) {
throw new BadRequestException('No ID provided.');
}
try {
return await this.subCategoriesService.findById(id);
}
catch (error) {
throw new NotFoundException(error);
}
}
@Post()
@HttpCode(HttpStatus.CREATED)
public async create(@Body() subCategory: CreateSubCategoryDto): Promise<SubCategory> {
if (!subCategory.name) {
throw new BadRequestException('Sub-category name cannot be empty.');
}
try {
return await this.subCategoriesService.create(subCategory);
}
catch (error) {
throw new InternalServerErrorException(error);
}
}
@Put()
@HttpCode(HttpStatus.OK)
public async update(@Body() subCategory: UpdateSubCategoryDto): Promise<SubCategory> {
if (!subCategory.id) {
throw new BadRequestException('Sub-category ID cannot be empty.');
}
return await this.subCategoriesService.update(subCategory);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
public async remove(@Param('id') id: string): Promise<void> {
return await this.subCategoriesService.remove(id);
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SubCategoriesService } from './sub-categories.service';
import { SubCategoriesController } from './sub-categories.controller';
import { SubCategoryDataService } from './sub-category-data.service';
@Module({
controllers: [SubCategoriesController],
providers: [SubCategoriesService, SubCategoryDataService]
})
export class SubCategoriesModule { }

View file

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { CreateSubCategoryDto } from './dto/create-sub-category.dto';
import { UpdateSubCategoryDto } from './dto/update-sub-category.dto';
import { SubCategoryDataService } from './sub-category-data.service';
import { SubCategory } from './entities/sub-category.entity';
@Injectable()
export class SubCategoriesService {
public constructor(private subCategoryDataService: SubCategoryDataService) { }
public async findAll(): Promise<SubCategory[]> {
return await this.subCategoryDataService.getAll();
}
public async findById(id: string): Promise<SubCategory> {
const subCategory = await this.subCategoryDataService.getById(id);
if (!subCategory) {
throw new Error('No sub-category found');
}
return subCategory;
}
public async create(subCategory: CreateSubCategoryDto): Promise<SubCategory> {
return await this.subCategoryDataService.create(subCategory.name);
}
public async update(subCategory: UpdateSubCategoryDto): Promise<SubCategory> {
return await this.subCategoryDataService.update(subCategory);
}
public async remove(id: string): Promise<void> {
await this.subCategoryDataService.delete(id);
}
}

View file

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { SubCategory } from './entities/sub-category.entity';
import { UpdateSubCategoryDto } from './dto/update-sub-category.dto';
@Injectable()
export class SubCategoryDataService {
private subCategories: Repository<SubCategory>;
public constructor(private dataSource: DataSource) {
this.subCategories = this.dataSource.getRepository(SubCategory);
}
public async getAll(): Promise<SubCategory[]> {
return await this.subCategories.find();
}
public async getById(id: string): Promise<SubCategory | null> {
return await this.subCategories.findOneBy({ id });
}
public async create(name: string): Promise<SubCategory> {
return await this.subCategories.save({ name });
}
public async update(subCategory: UpdateSubCategoryDto): Promise<SubCategory> {
return await this.subCategories.save(subCategory);
}
public async delete(id: string): Promise<void> {
await this.subCategories.delete({ id });
}
}

View file

@ -0,0 +1,3 @@
export class CreateTagDto {
name: string;
}

View file

@ -0,0 +1,4 @@
export class UpdateTagDto {
id: string;
name: string;
}

View file

@ -0,0 +1,10 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Tag {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
}

View file

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Tag } from './entities/tag.entity';
import { UpdateTagDto } from './dto/update-tag.dto';
@Injectable()
export class TagDataService {
private tags: Repository<Tag>;
public constructor(private dataSource: DataSource) {
this.tags = this.dataSource.getRepository(Tag);
}
public async getAll(): Promise<Tag[]> {
return await this.tags.find();
}
public async getById(id: string): Promise<Tag | null> {
return await this.tags.findOneBy({ id });
}
public async create(name: string): Promise<Tag> {
return await this.tags.save({ name });
}
public async update(tag: UpdateTagDto): Promise<Tag> {
return await this.tags.save(tag);
}
public async delete(id: string): Promise<void> {
await this.tags.delete({ id });
}
}

View file

@ -0,0 +1,75 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
HttpCode,
HttpStatus,
BadRequestException,
NotFoundException,
InternalServerErrorException
} from '@nestjs/common';
import { TagsService } from './tags.service';
import { CreateTagDto } from './dto/create-tag.dto';
import { UpdateTagDto } from './dto/update-tag.dto';
import { Tag } from './entities/tag.entity';
@Controller('tags')
export class TagsController {
public constructor(private readonly tagsService: TagsService) { }
@Get()
@HttpCode(HttpStatus.OK)
public async findAll(): Promise<Tag[]> {
return await this.tagsService.findAll();
}
@Get(':id')
@HttpCode(HttpStatus.OK)
public async findOne(@Param('id') id: string): Promise<Tag> {
if (!id) {
throw new BadRequestException('No ID provided.');
}
try {
return await this.tagsService.findById(id);
}
catch (error) {
throw new NotFoundException(error);
}
}
@Post()
@HttpCode(HttpStatus.CREATED)
public async create(@Body() tag: CreateTagDto): Promise<Tag> {
if (!tag.name) {
throw new BadRequestException('Tag name cannot be empty.');
}
try {
return await this.tagsService.create(tag);
}
catch (error) {
throw new InternalServerErrorException(error);
}
}
@Put()
@HttpCode(HttpStatus.OK)
public async update(@Body() tag: UpdateTagDto): Promise<Tag> {
if (!tag.id) {
throw new BadRequestException('Tag ID cannot be empty.');
}
return await this.tagsService.update(tag);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
public async remove(@Param('id') id: string): Promise<void> {
return await this.tagsService.remove(id);
}
}

10
src/tags/tags.module.ts Normal file
View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TagsService } from './tags.service';
import { TagsController } from './tags.controller';
import { TagDataService } from './tag-data.service';
@Module({
controllers: [TagsController],
providers: [TagsService, TagDataService]
})
export class TagsModule { }

35
src/tags/tags.service.ts Normal file
View file

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { CreateTagDto } from './dto/create-tag.dto';
import { UpdateTagDto } from './dto/update-tag.dto';
import { TagDataService } from './tag-data.service';
import { Tag } from './entities/tag.entity';
@Injectable()
export class TagsService {
public constructor(private tagDataService: TagDataService) { }
public async findAll(): Promise<Tag[]> {
return await this.tagDataService.getAll();
}
public async findById(id: string): Promise<Tag> {
const tag = await this.tagDataService.getById(id);
if (!tag) {
throw new Error('No tag found');
}
return tag;
}
public async create(tag: CreateTagDto): Promise<Tag> {
return await this.tagDataService.create(tag.name);
}
public async update(tag: UpdateTagDto): Promise<Tag> {
return await this.tagDataService.update(tag);
}
public async remove(id: string): Promise<void> {
await this.tagDataService.delete(id);
}
}