Add MongoDb connection and refactor to consistent package structure

This commit is contained in:
Alexander Ungar 2023-03-20 20:05:25 +01:00
parent aaa607a40c
commit 879862dfad
33 changed files with 409 additions and 116 deletions

186
api/my-finance-pal.yml Normal file
View file

@ -0,0 +1,186 @@
openapi: 3.0.1
info:
version: 1.0.0
title: My Finance Pal
description: API of the personal finance budgeting app My Finance Pal
paths:
/budgets:
post:
description: Create a new budget
operationId: createBudget
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewBudget"
responses:
201:
description: Budget created
content:
application/json:
schema:
$ref: "#/components/schemas/Budget"
400:
description: Invalid budget
get:
description: Get all budgets
operationId: getBudgets
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Budget"
/budgets/{budgetId}:
get:
description: Get a budget for a given ID
operationId: getBudget
parameters:
- in: path
name: budgetId
required: true
description: The UUID of the budget
schema:
$ref: "#/components/schemas/BudgetId"
responses:
200:
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Budget"
404:
description: Budget not found
delete:
description: Delete a budget including all transactions
operationId: deleteBudget
parameters:
- in: path
name: budgetId
required: true
description: The UUID of the budget
schema:
$ref: "#/components/schemas/BudgetId"
responses:
204:
description: Budget successfully deleted
404:
description: Budget not found
components:
schemas:
BudgetId:
type: string
format: uuid
description: Unique identifier of one budget
example: 850963b9-a04d-4698-b767-c0a0096b37c5
Budget:
type: object
description: Planned money to be available for tracking expenses related to a certain purpose over a period of time
required:
- id
- name
- amount
- transactions
properties:
id:
$ref: "#/components/schemas/BudgetId"
name:
type: string
description: The name of the budget
example: Takeout
amount:
type: number
minimum: 0
description: The total amount available for the budget
example: 250
startDate:
type: string
format: date
description: Date marking the start of the budget period
example: 2023-04-01
endDate:
type: string
format: date
description: Date marking the end of the budget period
example: 2023-04-30
transactions:
type: array
description: All transactions of the budget
items:
$ref: "#/components/schemas/Transaction"
NewBudget:
type: object
description: Budget to be created
required:
- name
- amount
properties:
name:
type: string
description: The name of the budget
example: Takeout
amount:
type: number
minimum: 0
description: The total amount available for the budget
example: 250
startDate:
type: string
format: date
description: Date marking the start of the budget period
example: 2023-04-01
endDate:
type: string
format: date
description: Date marking the end of the budget period
example: 2023-04-30
Transaction:
type: object
description: An expense or income related to a single budget
required:
- id
- description
- amount
- date
properties:
id:
type: string
format: uuid
description: Unique identifier of a transaction
example: cfd3b0b7-bf5e-43d4-8341-940d5e07487d
description:
type: string
description: Description of the transaction
example: Healthy breakfast at Five Guys
amount:
type: number
description: Amount of money contained in the transaction
example: 23.94
date:
type: string
format: date
NewTransaction:
type: object
description: A new transaction to be created
required:
- description
- amount
- date
properties:
description:
type: string
description: Description of the transaction
example: Healthy breakfast at Five Guys
amount:
type: number
description: Amount of money contained in the transaction
example: 23.94
date:
type: string
format: date

View file

@ -1,11 +1,10 @@
version: "3.8"
services:
mongo:
restart: always
image: mongo:6.0.4
expose:
- 27017
ports:
- "27017:27017"
volumes:
- mongo_volume:/data/db

3
env/development.env vendored
View file

@ -1,3 +1,4 @@
## Server ##
PORT=3000
NODE_ENV=development
NODE_ENV=development
DATABASE_CONNECTION_STRING=mongodb://localhost:27017

View file

@ -24,20 +24,22 @@
"envalid": "^7.3.1",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"express-async-handler": "^1.2.0",
"express-winston": "^4.2.0",
"helmet": "^6.0.1",
"http-status-codes": "^2.2.0",
"mongoose": "^7.0.2",
"openapi-typescript": "^6.2.0",
"ts-command-line-args": "^2.4.2",
"uuid": "^9.0.0",
"winston": "^3.8.2"
},
"devDependencies": {
"@types/uuid": "^9.0.1",
"@types/eslint": "^8.21.1",
"@types/express": "^4.17.17",
"@types/morgan": "^1.9.4",
"@types/node": "^18.15.0",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.7.0",

View file

@ -1,11 +1,11 @@
import express from "express";
import helmet from "helmet";
import environment from "./config/environment.js";
import expressLogger from "./logging/express-logger.js";
import ApiRouter from "./routes/ApiRouter.js";
import BudgetUseCases from "./usecase/budget/BudgetUseCases.js";
import BudgetMongoRepository from "./repository/budget/BudgetMongoRepository.js";
import { errorHandler } from "./middleware/error-handler.js";
import expressLogger from "./logging/expressLogger.js";
import ApiRouter from "./routes/apiRouter.js";
import BudgetService from "./usecase/budget/budgetService.js";
import BudgetMongoRepository from "./repository/budget/budgetMongoRepository.js";
import { errorHandler } from "./middleware/errorHandler.js";
const app = express();
const API_ROOT = "/api";
@ -18,11 +18,9 @@ if (environment.isProd) {
app.use(helmet());
}
const budgetRepository = new BudgetMongoRepository();
const budgetUseCases = new BudgetUseCases(budgetRepository);
const budgetUseCases = BudgetService(BudgetMongoRepository());
app.use(API_ROOT, ApiRouter(budgetUseCases));
// eslint-disable-next-line @typescript-eslint/no-misused-promises
app.use(errorHandler);
export default app;

View file

@ -1,9 +1,10 @@
import { cleanEnv, port, str } from "envalid";
import { cleanEnv, port, str, url } from "envalid";
import * as process from "process";
const env = cleanEnv(process.env, {
PORT: port(),
LOG_LEVEL: str({ default: "info" }),
DATABASE_CONNECTION_STRING: url(),
});
export default env;

8
src/config/mongoDb.ts Normal file
View file

@ -0,0 +1,8 @@
import mongoose from "mongoose";
import environment from "./environment.js";
const connect = async (): Promise<void> => {
await mongoose.connect(environment.DATABASE_CONNECTION_STRING);
};
export default { connect };

View file

@ -1,11 +1,11 @@
import { v4 as uuidv4 } from "uuid";
import { type Transaction } from "./Transaction.js";
import { type Transaction } from "./transaction.js";
export class BudgetId {
uuid: string;
constructor() {
this.uuid = uuidv4();
constructor(uuid?: string) {
this.uuid = uuid ?? uuidv4();
}
}

View file

@ -3,8 +3,8 @@ import { v4 as uuidv4 } from "uuid";
export class TransactionId {
uuid: string;
constructor() {
this.uuid = uuidv4();
constructor(uuid?: string) {
this.uuid = uuid ?? uuidv4();
}
}

View file

@ -1,12 +0,0 @@
import type BudgetRepository from "./BudgetRepository.js";
import insertBudget from "./lib/insert-budget.js";
class BudgetMongoRepository implements BudgetRepository {
insertBudget: BudgetRepository["insertBudget"];
constructor() {
this.insertBudget = insertBudget();
}
}
export default BudgetMongoRepository;

View file

@ -0,0 +1,18 @@
import type BudgetRepository from "./budgetRepository.js";
import { BudgetConverter } from "./entity/converters.js";
import { BudgetModel } from "./models.js";
export const insertBudget: BudgetRepository["insertBudget"] = async (
budget
) => {
const entity = BudgetConverter.toEntity(budget);
const model = new BudgetModel(entity);
const saved = await model.save();
return BudgetConverter.toDomain(saved);
};
const BudgetMongoRepository: () => BudgetRepository = () => ({
insertBudget,
});
export default BudgetMongoRepository;

View file

@ -1,4 +1,4 @@
import { type Budget } from "../../models/Budget.js";
import { type Budget } from "../../domain/budget.js";
interface BudgetRepository {
insertBudget: (budget: Budget) => Promise<Budget>;

View file

@ -0,0 +1,12 @@
import type TransactionEntity from "./transactionEntity.js";
interface BudgetEntity {
id: string;
name: string;
amount: number;
startDate?: Date;
endDate?: Date;
transactions: TransactionEntity[];
}
export default BudgetEntity;

View file

@ -0,0 +1,41 @@
import {
type Transaction,
TransactionId,
} from "../../../domain/transaction.js";
import type TransactionEntity from "./transactionEntity.js";
import { type Budget, BudgetId } from "../../../domain/budget.js";
import type BudgetEntity from "./budgetEntity.js";
export const TransactionConverter = {
toEntity: (domain: Transaction): TransactionEntity => ({
id: domain.id.uuid,
description: domain.description,
amount: domain.amount,
date: domain.date,
}),
toDomain: (entity: TransactionEntity): Transaction => ({
id: new TransactionId(entity.id),
description: entity.description,
amount: entity.amount,
date: entity.date,
}),
};
export const BudgetConverter = {
toEntity: (domain: Budget): BudgetEntity => ({
id: domain.id.uuid,
name: domain.name,
amount: domain.amount,
startDate: domain.startDate,
endDate: domain.endDate,
transactions: domain.transactions.map(TransactionConverter.toEntity),
}),
toDomain: (entity: BudgetEntity): Budget => ({
id: new BudgetId(entity.id),
name: entity.name,
amount: entity.amount,
startDate: entity.startDate,
endDate: entity.endDate,
transactions: entity.transactions.map(TransactionConverter.toDomain),
}),
};

View file

@ -0,0 +1,8 @@
interface TransactionEntity {
id: string;
description: string;
amount: number;
date: Date;
}
export default TransactionEntity;

View file

@ -1,9 +0,0 @@
import { type Budget } from "../../../models/Budget.js";
const insertBudget =
() =>
async (budget: Budget): Promise<Budget> => {
return budget;
};
export default insertBudget;

View file

@ -0,0 +1,11 @@
import mongoose from "mongoose";
import budgetSchema from "./schema/budgetSchema.js";
import transactionSchema from "./schema/transactionSchema.js";
import type BudgetEntity from "./entity/budgetEntity.js";
import type TransactionEntity from "./entity/transactionEntity.js";
export const BudgetModel = mongoose.model<BudgetEntity>("Budget", budgetSchema);
export const TransactionModel = mongoose.model<TransactionEntity>(
"Transaction",
transactionSchema
);

View file

@ -0,0 +1,16 @@
import mongoose, { Schema } from "mongoose";
import transactionSchema from "./transactionSchema.js";
import type BudgetEntity from "../entity/budgetEntity.js";
const Types = Schema.Types;
const budgetSchema = new mongoose.Schema<BudgetEntity>({
id: Types.UUID,
name: Types.String,
amount: Types.Number,
startDate: Types.Date,
endDate: Types.Date,
transactions: [transactionSchema],
});
export default budgetSchema;

View file

@ -0,0 +1,13 @@
import mongoose, { Schema } from "mongoose";
import type TransactionEntity from "../entity/transactionEntity.js";
const Types = Schema.Types;
const transactionSchema = new mongoose.Schema<TransactionEntity>({
id: Types.UUID,
description: Types.String,
amount: Types.Number,
date: Types.Date,
});
export default transactionSchema;

View file

@ -1,6 +1,6 @@
import { Router } from "express";
import type BudgetUseCases from "../usecase/budget/BudgetUseCases.js";
import BudgetRouter from "./budget/BudgetRouter.js";
import BudgetRouter from "./budget/budgetRouter.js";
import type BudgetUseCases from "../usecase/budget/budgetUseCases.js";
const paths = {
BUDGETS: "/budgets",

View file

@ -1,17 +0,0 @@
import { Router } from "express";
import type BudgetUseCases from "../../usecase/budget/BudgetUseCases.js";
import createBudget from "./create-budget.js";
const paths = {
CREATE_BUDGET: "/",
};
const BudgetRouter = (budgetUseCases: BudgetUseCases): Router => {
const router = Router();
// eslint-disable-next-line @typescript-eslint/no-misused-promises
router.post(paths.CREATE_BUDGET, createBudget(budgetUseCases));
return router;
};
export default BudgetRouter;

View file

@ -0,0 +1,39 @@
import {
type NextFunction,
type Request,
type Response,
Router,
} from "express";
import type BudgetUseCases from "../../usecase/budget/budgetUseCases.js";
import asyncHandler from "express-async-handler";
import { BudgetConverter, NewBudgetConverter, type NewBudgetDto } from "./dto/budget.js";
import { StatusCodes } from "http-status-codes";
const paths = {
CREATE_BUDGET: "/",
};
export const createBudget =
(budgetUseCases: BudgetUseCases) =>
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const dto: NewBudgetDto = req.body;
const newBudget = NewBudgetConverter.toDomain(dto);
const createdBudget = await budgetUseCases.createBudget(newBudget);
res
.status(StatusCodes.CREATED)
.json(BudgetConverter.toDto(createdBudget));
} catch (error) {
next(error);
}
};
const BudgetRouter = (budgetUseCases: BudgetUseCases): Router => {
const router = Router();
router.post(paths.CREATE_BUDGET, asyncHandler(createBudget(budgetUseCases)));
return router;
};
export default BudgetRouter;

View file

@ -1,20 +0,0 @@
import { type NextFunction, type Request, type Response } from "express";
import type BudgetUseCases from "../../usecase/budget/BudgetUseCases.js";
import { NewBudgetConverter, type NewBudgetDto } from "./dto/budget.js";
import { StatusCodes } from "http-status-codes";
const createBudget =
(budgetUseCases: BudgetUseCases) =>
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const dto: NewBudgetDto = req.body;
const newBudget = NewBudgetConverter.toDomain(dto);
const createdBudget = await budgetUseCases.createBudget(newBudget);
res.status(StatusCodes.CREATED).json(createdBudget);
} catch (error) {
next(error);
}
};
export default createBudget;

View file

@ -1,5 +1,5 @@
import { type components } from "../../../../generated/api.js";
import { Budget, type NewBudget } from "../../../models/Budget.js";
import { type Budget, type NewBudget } from "../../../domain/budget.js";
import { toDate } from "../../../util/date.js";
import { TransactionConverter } from "./transaction.js";

View file

@ -1,5 +1,5 @@
import { type components } from "../../../../generated/api.js";
import { type Transaction } from "../../../models/Transaction.js";
import { type Transaction } from "../../../domain/transaction.js";
export type TransactionDto = components["schemas"]["Transaction"];

View file

@ -1,9 +1,12 @@
import "./pre-start.js";
import "./preStart.js";
import * as http from "http";
import app from "./app.js";
import { listenToErrorEvents } from "./middleware/error-handler.js";
import { listenToErrorEvents } from "./middleware/errorHandler.js";
import logger from "./logging/logger.js";
import environment from "./config/environment.js";
import mongoDb from "./config/mongoDb.js";
await mongoDb.connect();
const onListening = (server: http.Server) => (): void => {
const addr = server.address();

View file

@ -1,13 +0,0 @@
import createBudget from "./lib/create-budget.js";
import { type Budget, type NewBudget } from "../../models/Budget.js";
import type BudgetRepository from "../../repository/budget/BudgetRepository.js";
class BudgetUseCases {
createBudget: (budget: NewBudget) => Promise<Budget>;
constructor(budgetRepository: BudgetRepository) {
this.createBudget = createBudget(budgetRepository);
}
}
export default BudgetUseCases;

View file

@ -0,0 +1,20 @@
import { type Budget, BudgetId } from "../../domain/budget.js";
import type BudgetRepository from "../../repository/budget/budgetRepository.js";
import type BudgetUseCases from "./budgetUseCases.js";
export const createBudget: (
repo: BudgetRepository
) => BudgetUseCases["createBudget"] = (repo) => async (newBudget) => {
const budget: Budget = {
id: new BudgetId(),
transactions: [],
...newBudget,
};
return await repo.insertBudget(budget);
};
const BudgetService: (repo: BudgetRepository) => BudgetUseCases = (repo) => ({
createBudget: createBudget(repo),
});
export default BudgetService;

View file

@ -0,0 +1,7 @@
import { type Budget, type NewBudget } from "../../domain/budget.js";
interface BudgetUseCases {
createBudget: (newBudget: NewBudget) => Promise<Budget>;
}
export default BudgetUseCases;

View file

@ -1,19 +0,0 @@
import type BudgetRepository from "../../../repository/budget/BudgetRepository.js";
import {
type Budget,
BudgetId,
type NewBudget,
} from "../../../models/Budget.js";
const createBudget =
(repo: BudgetRepository) =>
async (newBudget: NewBudget): Promise<Budget> => {
const budget: Budget = {
id: new BudgetId(),
transactions: [],
...newBudget,
};
return await repo.insertBudget(budget);
};
export default createBudget;