Skip to content

Commit

Permalink
Add error handling and refactorings
Browse files Browse the repository at this point in the history
  • Loading branch information
ungaralex committed Mar 11, 2024
1 parent b52069d commit 7ebae02
Show file tree
Hide file tree
Showing 38 changed files with 357 additions and 213 deletions.
83 changes: 64 additions & 19 deletions api/my-finance-pal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ tags:
description: Managing budgets
- name: incomes
description: Earn money
servers:
- url: http://localhost:3000
description: Local Development
paths:
/budgets:
/incomes/{incomeId}/budgets:
post:
tags:
- budgets
description: Create a new budget
operationId: createBudget
parameters:
- $ref: "#/components/parameters/IncomeIdParam"
requestBody:
required: true
content:
Expand All @@ -32,6 +37,22 @@ paths:
$ref: "#/components/schemas/Budget"
400:
description: Invalid budget
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
404:
description: Income for new budget not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
409:
description: Sum of spendings of new budget and existing budgets is bigger than total income
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
get:
tags:
- budgets
Expand All @@ -46,18 +67,15 @@ paths:
type: array
items:
$ref: "#/components/schemas/Budget"
/budgets/{budgetId}/expenses:
/incomes/{incomeId}/budgets/{budgetId}/expenses:
post:
tags:
- expenses
description: Register a new expense for a budget
operationId: trackExpense
parameters:
- in: path
name: budgetId
required: true
schema:
$ref: "#/components/parameters/BudgetIdParam"
- $ref: "#/components/parameters/BudgetIdParam"
- $ref: "#/components/parameters/IncomeIdParam"
requestBody:
required: true
content:
Expand All @@ -73,6 +91,16 @@ paths:
$ref: "#/components/schemas/Expense"
404:
description: Budget not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
409:
description: Expense date out of budget bounds
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/incomes:
post:
tags:
Expand Down Expand Up @@ -113,15 +141,11 @@ paths:
description: Add a new source for a single income
operationId: addIncomeSource
parameters:
- in: path
name: incomeId
required: true
schema:
$ref: "#/components/schemas/IncomeId"
- $ref: "#/components/parameters/IncomeIdParam"
requestBody:
required: true
content:
applicaton/json:
application/json:
schema:
$ref: "#/components/schemas/NewIncomeSource"
responses:
Expand All @@ -131,8 +155,18 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/IncomeSource"
400:
description: Invalid income payload
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
404:
description: Income not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"

components:
parameters:
Expand All @@ -142,12 +176,12 @@ components:
required: true
schema:
$ref: "#/components/schemas/BudgetId"
ExpenseIdParam:
IncomeIdParam:
in: path
name: expenseId
name: incomeId
required: true
schema:
$ref: "#/components/schemas/ExpenseId"
$ref: "#/components/schemas/IncomeId"
schemas:
IncomeSourceId:
type: string
Expand Down Expand Up @@ -215,12 +249,9 @@ components:
type: object
description: Budget to be created
required:
- incomeId
- name
- limit
properties:
incomeId:
$ref: "#/components/schemas/IncomeId"
name:
type: string
description: The name of the budget
Expand All @@ -246,10 +277,13 @@ components:
description: Planned money to be available for tracking expenses related to a certain purpose over a period of time
required:
- id
- incomeId
- expenses
properties:
id:
$ref: "#/components/schemas/BudgetId"
incomeId:
$ref: "#/components/schemas/IncomeId"
expenses:
type: array
items:
Expand Down Expand Up @@ -289,3 +323,14 @@ components:
properties:
id:
$ref: "#/components/schemas/ExpenseId"
Error:
type: object
description: Error response
required:
- message
properties:
message:
type: string
errors:
type: object
additionalProperties: true
76 changes: 44 additions & 32 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,26 @@ import helmet from "helmet";
import environment from "./config/environment";
import expressLogger from "./infrastructure/adapter/in/express/logging/expressLogger";
import ApiRouter from "./infrastructure/adapter/in/express/routes/apiRouter";
import { errorHandler } from "./infrastructure/adapter/in/express/middleware/errorHandler";
import { errorHandler } from "./infrastructure/adapter/in/express/middleware/error/errorHandler";
import * as OpenApiValidator from "express-openapi-validator";
import * as path from "path";
import createBudgetUseCase from "./core/application/usecase/createBudgetUseCase";
import BudgetMongoPersistenceAdapter from "./infrastructure/adapter/out/budget/persistence/mongo/budgetMongoPersistenceAdapter";
import { getIncomeById } from "./core/application/service/IncomeApplicationService";
import {
getIncomeById,
IncomeApplicationService,
} from "./core/application/service/IncomeApplicationService";
import IncomeMongoPersistenceAdapter from "./infrastructure/adapter/out/income/persistence/mongo/incomeMongoPersistenceAdapter";
import getBudgetsUseCase from "./core/application/usecase/getBudgetsUseCase";
import trackExpenseUseCase from "./core/application/usecase/trackExpenseUseCase";
import {
BudgetApplicationService,
getBudgetByExpenseId,
getBudgetById,
} from "./core/application/service/budgetApplicationService";
import BudgetRouter from "./infrastructure/adapter/in/budget/http/budgetRouter";
import ExpenseRouter from "./infrastructure/adapter/in/expense/http/expenseRouter";
import trackExpenseUseCase from "./core/application/usecase/trackExpenseUseCase";
import IncomeRouter from "./infrastructure/adapter/in/income/http/incomeRouter";
import getIncomesUseCase from "./core/application/usecase/getIncomesUseCase";
import createIncomeUseCase from "./core/application/usecase/createIncomeUseCase";
import addIncomeSourceUseCase from "./core/application/usecase/addIncomeSourceUseCase";
Expand Down Expand Up @@ -44,7 +50,7 @@ app.use(
// validate incoming requests
validateRequests: true,
// also validate our responses to the clients
validateResponses: true,
// validateResponses: true,
}),
);

Expand All @@ -57,36 +63,42 @@ const budgetAppService: BudgetApplicationService = {
getBudgetBy: BudgetMongoPersistenceAdapter.getByExpenseId,
}),
};
app.use(
ApiRouter(
createBudgetUseCase(
{
getAllBudgetsBy: BudgetMongoPersistenceAdapter.getAllByIncomeId,
persist: BudgetMongoPersistenceAdapter.persist,
},
{
getIncomeBy: getIncomeById({
getIncomeBy: IncomeMongoPersistenceAdapter.getById,
}),
},
),
getBudgetsUseCase({ getAllBudgets: BudgetMongoPersistenceAdapter.getAll }),
trackExpenseUseCase(
{ persist: BudgetMongoPersistenceAdapter.persist },
{ getBudgetBy: budgetAppService.getById },
),
getIncomesUseCase({ getAllIncomes: IncomeMongoPersistenceAdapter.getAll }),
createIncomeUseCase({ persist: IncomeMongoPersistenceAdapter.persist }),
addIncomeSourceUseCase(
{ persist: IncomeMongoPersistenceAdapter.persist },
{
getIncomeBy: getIncomeById({
getIncomeBy: IncomeMongoPersistenceAdapter.getById,
}),
},
),
const incomeAppService: IncomeApplicationService = {
getById: getIncomeById({
getIncomeBy: IncomeMongoPersistenceAdapter.getById,
}),
};
const budgetRouter = BudgetRouter(
createBudgetUseCase(
{
getAllBudgetsBy: BudgetMongoPersistenceAdapter.getAllByIncomeId,
persist: BudgetMongoPersistenceAdapter.persist,
},
{
getIncomeBy: incomeAppService.getById,
},
),
getBudgetsUseCase({
getAllBudgetsBy: BudgetMongoPersistenceAdapter.getAllByIncomeId,
}),
);
const expenseRouter = ExpenseRouter(
trackExpenseUseCase(
{ persist: BudgetMongoPersistenceAdapter.persist },
{ getBudgetBy: budgetAppService.getById },
),
);
const incomeRouter = IncomeRouter(
getIncomesUseCase({ getAllIncomes: IncomeMongoPersistenceAdapter.getAll }),
createIncomeUseCase({ persist: IncomeMongoPersistenceAdapter.persist }),
addIncomeSourceUseCase(
{ persist: IncomeMongoPersistenceAdapter.persist },
{ getIncomeBy: incomeAppService.getById },
),
);

const apiRouter = ApiRouter(budgetRouter, expenseRouter, incomeRouter);
app.use(apiRouter);

// IMPORTANT! Always add an error handler to avoid unexpected crashes of the app!
// If not caught, every exception will lead to Node.js terminating the process!
Expand Down
1 change: 0 additions & 1 deletion src/core/application/port/budgetPersistencePort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ interface BudgetPersistencePort {
getByExpenseId: (id: ExpenseId) => Promise<Budget | undefined>;
persist: (budget: Budget) => Promise<Budget>;
getAllByIncomeId: (id: IncomeId) => Promise<Budget[]>;
getAll: () => Promise<Budget[]>;
}

export default BudgetPersistencePort;
7 changes: 2 additions & 5 deletions src/core/application/service/IncomeApplicationService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Income, type IncomeId } from "../../domain/model/income/income";
import { AppError } from "../../../infrastructure/adapter/in/express/middleware/errorHandler";
import type IncomePersistencePort from "../port/incomePersistencePort";
import IncomeNotFoundError from "../../domain/error/income/incomeNotFoundError";

export interface IncomeApplicationService {
getById: (id: IncomeId) => Promise<Income>;
Expand All @@ -11,10 +11,7 @@ export const getIncomeById: (ports: {
}) => IncomeApplicationService["getById"] = (ports) => async (id) => {
const income = await ports.getIncomeBy(id);
if (income === undefined) {
throw new AppError(
"IncomeNotFound",
`Income with id ${id.value} does not exist`,
);
throw new IncomeNotFoundError(id);
}

return income;
Expand Down
13 changes: 4 additions & 9 deletions src/core/application/service/budgetApplicationService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Budget, BudgetId } from "../../domain/model/expense/budget";
import { AppError } from "../../../infrastructure/adapter/in/express/middleware/errorHandler";
import { type ExpenseId } from "../../domain/model/expense/expense";
import type BudgetPersistencePort from "../port/budgetPersistencePort";
import BudgetNotFoundError from "../../domain/error/budget/budgetNotFoundError";
import ExpenseNotFoundError from "../../domain/error/expense/expenseNotFoundError";

export interface BudgetApplicationService {
getById: (id: BudgetId) => Promise<Budget>;
Expand All @@ -13,10 +14,7 @@ export const getBudgetById: (ports: {
}) => BudgetApplicationService["getById"] = (ports) => async (id) => {
const budget = await ports.getBudgetBy(id);
if (budget === undefined) {
throw new AppError(
"BudgetNotFound",
`Budget with id ${id.value} not found`,
);
throw new BudgetNotFoundError(id);
}

return budget;
Expand All @@ -27,10 +25,7 @@ export const getBudgetByExpenseId: (ports: {
}) => BudgetApplicationService["getByExpenseId"] = (ports) => async (id) => {
const budget = await ports.getBudgetBy(id);
if (budget === undefined) {
throw new AppError(
"BudgetNotFound",
`Budget with expense having id ${id.value} not found`,
);
throw new ExpenseNotFoundError(id);
}

return budget;
Expand Down
2 changes: 1 addition & 1 deletion src/core/application/usecase/addIncomeSourceUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const addIncomeSource: (
const updatedIncome = await ports.persist(income);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return updatedIncome.getSourceBy(incomeId)!;
return updatedIncome.getSourceBy(incomeSource.id)!;
};

export default addIncomeSource;
10 changes: 2 additions & 8 deletions src/core/application/usecase/createBudgetUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { type Budget } from "../../domain/model/expense/budget";
import { AppError } from "../../../infrastructure/adapter/in/express/middleware/errorHandler";
import { isOverspending } from "../../domain/service/balanceDomainService";
import { validateNoOverspending } from "../../domain/service/balanceDomainService";
import type BudgetPersistencePort from "../port/budgetPersistencePort";
import { type IncomeApplicationService } from "../service/IncomeApplicationService";

Expand All @@ -15,12 +14,7 @@ const createBudget: (
) => CreateBudgetUseCase = (ports, appServices) => async (budget) => {
const income = await appServices.getIncomeBy(budget.incomeId);
const existingBudgets = await ports.getAllBudgetsBy(budget.incomeId);
if (isOverspending(existingBudgets.concat(budget), income)) {
throw new AppError(
"BudgetOverspending",
"Sum of budget limits is bigger than income",
);
}
validateNoOverspending(existingBudgets.concat(budget), income);

return await ports.persist(budget);
};
Expand Down
9 changes: 5 additions & 4 deletions src/core/application/usecase/getBudgetsUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { type Budget } from "../../domain/model/expense/budget";
import type BudgetPersistencePort from "../port/budgetPersistencePort";
import { IncomeId } from "../../domain/model/income/income";

export type GetBudgetsUseCase = () => Promise<Budget[]>;
export type GetBudgetsUseCase = (incomeId: IncomeId) => Promise<Budget[]>;

const getBudgets: (ports: {
getAllBudgets: BudgetPersistencePort["getAll"];
}) => GetBudgetsUseCase = (ports) => async () => {
return await ports.getAllBudgets();
getAllBudgetsBy: BudgetPersistencePort["getAllByIncomeId"];
}) => GetBudgetsUseCase = (ports) => async (incomeId) => {
return await ports.getAllBudgetsBy(incomeId);
};

export default getBudgets;
Loading

0 comments on commit 7ebae02

Please sign in to comment.