From 8de0894deb3d3d9b6d4527eccf2c62e9eaa167b8 Mon Sep 17 00:00:00 2001 From: Alexander Ungar Date: Sat, 1 Apr 2023 12:40:30 +0200 Subject: [PATCH] Add comments --- api/my-finance-pal.yml | 2 +- src/app.ts | 14 ++++++++++++++ src/config/environment.ts | 3 +++ src/logging/logger.ts | 11 +++++++++++ src/middleware/errorHandler.ts | 2 ++ src/routes/apiPaths.ts | 1 + src/routes/apiRouter.ts | 1 + src/server.ts | 5 ++++- src/usecase/budget/budgetUseCases.ts | 15 +++++++++++++++ src/usecase/expense/expenseService.ts | 10 ++++++++++ src/usecase/expense/expenseUseCases.ts | 5 +++++ src/util/date.ts | 9 +++++++++ 12 files changed, 76 insertions(+), 2 deletions(-) diff --git a/api/my-finance-pal.yml b/api/my-finance-pal.yml index 2a458d5..e37ab20 100644 --- a/api/my-finance-pal.yml +++ b/api/my-finance-pal.yml @@ -143,7 +143,7 @@ components: spent: type: number minimum: 0 - description: The summed up spent amount of all transaction of that budget + description: The summed up spent amount of all expenses of that budget - $ref: "#/components/schemas/NewBudget" BudgetSummary: allOf: diff --git a/src/app.ts b/src/app.ts index ae37fbd..861b433 100644 --- a/src/app.ts +++ b/src/app.ts @@ -14,22 +14,32 @@ import * as path from "path"; const app = express(); +// add a winston express logger middleware to log traffic and other events emitted by express app.use(expressLogger); +// Enable JSON body parsing app.use(express.json()); +// allow parsing of URL encoded payloads app.use(express.urlencoded({ extended: true })); +// add some additional security on PROD by setting some important HTTP headers automatically if (environment.isProd) { app.use(helmet()); } +// validate payloads based on our OpenAPI specification. Fails if something +// does not meet the contract that we have defined and returns a verbose error +// message specifying what exactly is the problem app.use( OpenApiValidator.middleware({ apiSpec: path.join(__dirname, "..", "api", "my-finance-pal.yml"), + // validate incoming requests validateRequests: true, + // also validate our responses to the clients validateResponses: true, }) ); +// Instantiate dependencies and pass them to the respective components needed for our use cases const budgetUseCases = BudgetService( BudgetSummaryMongoRepository(), BudgetMongoRepository() @@ -37,6 +47,10 @@ const budgetUseCases = BudgetService( const expenseUseCases = ExpenseService(ExpenseMongoRepository()); app.use(ApiRouter(budgetUseCases, expenseUseCases)); +// 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! +// Also place the error handler at the end of your app configuration as this should +// be the last middleware that is called, so we can handle any error that might occur before! app.use(errorHandler); export default app; diff --git a/src/config/environment.ts b/src/config/environment.ts index 1916061..79eba6e 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -1,6 +1,9 @@ import { cleanEnv, port, str, url } from "envalid"; import * as process from "process"; +// Use cleanenv to read all environment variables and validate them agains our +// typesafe specification. This allows us to fail early on app startup in case +// some of the variables are missing or in the wrong format. const env = cleanEnv(process.env, { PORT: port(), LOG_LEVEL: str({ default: "info" }), diff --git a/src/logging/logger.ts b/src/logging/logger.ts index fe0b838..d47fb79 100644 --- a/src/logging/logger.ts +++ b/src/logging/logger.ts @@ -18,4 +18,15 @@ if (!environment.isProd) { ); } +/* +For production we are still missing a log aggregation stack as well +as some monitoring solution! These two things are imperative when it +comes to creating a production ready state-of-the-art application! +Please have a look at solutions like: +Datadog, Elastic APM, Grafana, Prometheus, InfluxDB and many more. +All cloud providers also offer native solutions for the above, so also +check the pricing and features of these as they are often easily +integrable into cloud native applications. +*/ + export default logger; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index 73e2f8d..0bff931 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -3,6 +3,8 @@ import logger from "../logging/logger"; import * as util from "util"; import { type NextFunction, type Request, type Response } from "express"; +// This whole handling logic is copied from https://github.com/practicajs/practica was and modified to fit our application + let httpServerRef: Http.Server; export class AppError extends Error { diff --git a/src/routes/apiPaths.ts b/src/routes/apiPaths.ts index ec239e5..ebf1d98 100644 --- a/src/routes/apiPaths.ts +++ b/src/routes/apiPaths.ts @@ -1,5 +1,6 @@ import { type operations, type paths } from "../../generated/api"; +// Typesafe constant holding all REST endpoint paths based on the OpenAPI specification const apiPaths: Record = { createBudget: "/budgets", getBudgets: "/budgets", diff --git a/src/routes/apiRouter.ts b/src/routes/apiRouter.ts index 0bf0420..3e0370d 100644 --- a/src/routes/apiRouter.ts +++ b/src/routes/apiRouter.ts @@ -4,6 +4,7 @@ import type BudgetUseCases from "../usecase/budget/budgetUseCases"; import ExpenseRouter from "./expense/expenseRouter"; import type ExpenseUseCases from "../usecase/expense/expenseUseCases"; +// Express router bundling all individual routes of our app const ApiRouter = ( budgetUseCases: BudgetUseCases, expenseUseCases: ExpenseUseCases diff --git a/src/server.ts b/src/server.ts index b2bd5eb..e63be5a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import "./preStart"; +import "./preStart"; // always have this at the top of this file in order to execute these scripts first import * as http from "http"; import app from "./app"; import { listenToErrorEvents } from "./middleware/errorHandler"; @@ -10,6 +10,7 @@ void (async () => { await mongoDb.connect(); })(); +// callback function logging out server information const onListening = (server: http.Server) => (): void => { const addr = server.address(); const bind = @@ -17,7 +18,9 @@ const onListening = (server: http.Server) => (): void => { logger.info(`Listening on ${bind}`); }; +// let's first create a server based on our Express application const server = http.createServer(app); +// then add an error handler for anything uncaught by the app listenToErrorEvents(server); server.on("listening", onListening(server)); server.listen(environment.PORT); diff --git a/src/usecase/budget/budgetUseCases.ts b/src/usecase/budget/budgetUseCases.ts index 231d251..a107b28 100644 --- a/src/usecase/budget/budgetUseCases.ts +++ b/src/usecase/budget/budgetUseCases.ts @@ -6,9 +6,24 @@ import { } from "../../domain/budget"; interface BudgetUseCases { + /** + * Creates a new budget + * @param newBudget The new budget to be created + */ createBudget: (newBudget: NewBudget) => Promise; + /** + * Gets the summary of a budget including all expenses + * @param budgetId The ID of the budget to fetch + */ getBudgetSummary: (budgetId: BudgetId) => Promise; + /** + * Gets a list of all budgets (without expenses) + */ getBudgets: () => Promise; + /** + * Deletes a budget including all related expenses + * @param budgetId The ID of the budget to delete + */ deleteBudget: (budgetId: BudgetId) => Promise<{ deleted: boolean }>; } diff --git a/src/usecase/expense/expenseService.ts b/src/usecase/expense/expenseService.ts index c619dc5..64edee7 100644 --- a/src/usecase/expense/expenseService.ts +++ b/src/usecase/expense/expenseService.ts @@ -18,6 +18,16 @@ const addNewExpenseToBudget: ( id: new UUID(), }; const updatedSpent = calculateSpent(existingExpenses, expense); + + /* + Here we could also check if updatedSpent > budget.limit, but + we will omit this for now. It might also be a valid use case + that one spends more than they set as their limit + ==> clarification with business required + The same goes for a transaction date, that is not within the + boundaries of the budget. + */ + return await insert(budgetId, updatedSpent, expense); }; diff --git a/src/usecase/expense/expenseUseCases.ts b/src/usecase/expense/expenseUseCases.ts index 8c75ba0..bee5768 100644 --- a/src/usecase/expense/expenseUseCases.ts +++ b/src/usecase/expense/expenseUseCases.ts @@ -2,6 +2,11 @@ import { type BudgetId } from "../../domain/budget"; import { type NewExpense, type Expense } from "../../domain/expense"; interface ExpenseUseCases { + /** + * Adds a new expense to an existing budget + * @param budgetId The ID of the budget for which the expense should be added + * @param newExpense The new expense to be added + */ addToBudget: ( budgetId: BudgetId, newExpense: NewExpense diff --git a/src/util/date.ts b/src/util/date.ts index 066918c..9f676c1 100644 --- a/src/util/date.ts +++ b/src/util/date.ts @@ -1,9 +1,18 @@ +/** + * Parses an ISO-8601 date string into a JS Date object + * @param isoString The date string to be parsed + */ export const toDate = (isoString?: string): Date | undefined => { if (isoString !== null && isoString !== undefined) { return new Date(isoString); } }; +/** + * Translates a JS Date into an ISO date string + * E.g. 2023-04-01T10:01:57Z => 2023-04-01 + * @param date The date to be written in ISO date format + */ export const toIsoDate = (date?: Date): string | undefined => { if (date !== null && date !== undefined) { const isoDateTime = date.toISOString();