Skip to content

Commit

Permalink
chore: refactor config structure
Browse files Browse the repository at this point in the history
  • Loading branch information
brunotot committed May 4, 2024
1 parent 0132eea commit cad5180
Show file tree
Hide file tree
Showing 33 changed files with 149 additions and 125 deletions.
Binary file removed packages/backend/graph.png
Binary file not shown.
4 changes: 2 additions & 2 deletions packages/backend/src/App.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from "express";
import * as swaggerUi from "swagger-ui-express";
import { ContractManager, Environment, Logger, MongoClient } from "@org/backend/config";
import { RouterCollection, Environment, Logger, MongoClient } from "@org/backend/config";
import { GLOBAL_MIDDLEWARES } from "@org/backend/infrastructure";
import { CONTRACTS, operationMapper, suppressConsole } from "@org/shared";
import { generateOpenApi } from "@ts-rest/open-api";
Expand Down Expand Up @@ -65,7 +65,7 @@ export class App {

#initializeRoutes() {
const s = initServer();
const router = s.router(CONTRACTS, ContractManager.getInstance().getRouters());
const router = s.router(CONTRACTS, RouterCollection.getInstance().getRouters());
suppressConsole(() => createExpressEndpoints(CONTRACTS, router, this.app));
}

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/config/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
Every local module inside `config` must only import from `config` using relative paths.

TODO ...
29 changes: 12 additions & 17 deletions packages/backend/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
export * from "./singleton/Bottle";
export * from "./singleton/Environment";
export * from "./singleton/Logger";
export * from "./singleton/MongoClient";
export * from "./singleton/InjectableManager";
export * from "./singleton/ContractManager";
export * from "./singleton/JwtManager"
/* @org/backend/config/managers */
export * from "./managers/InjectorMetadataManager";
export * from "./managers/JwtManager"

import "./init";
/* @org/backend/config/singletons */
export * from "./singletons/ServiceRegistry";
export * from "./singletons/Environment";
export * from "./singletons/Logger";
export * from "./singletons/MongoClient";
export * from "./singletons/RouterCollection";

/* @org/backend/config/setup */
import "./setup";

process.on("uncaughtException", err => {
console.error("There was an uncaught error", err);
process.exit(1);
});

process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
process.exit(1);
});
7 changes: 0 additions & 7 deletions packages/backend/src/config/init.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { type ClassMetadataInjectType, ClassMetadataEntry } from "@tsvdec/decora
import { type TODO } from "@org/shared";
import { type MetaClassInjectionData } from "@org/backend/types";

export class InjectableManager extends ClassMetadataEntry<MetaClassInjectionData> {
static from(injection: ClassMetadataInjectType) {
return new InjectableManager(injection);
export class InjectorMetadataManager extends ClassMetadataEntry<MetaClassInjectionData> {
static getBy(injection: ClassMetadataInjectType) {
return new InjectorMetadataManager(injection);
}

private constructor(injection: ClassMetadataInjectType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type TODO, ErrorResponse } from "@org/shared";
import { type Request, type Response } from "express";
import type { VerifyErrors } from "jsonwebtoken";
import jwt from "jsonwebtoken";
import { Environment } from "@org/backend/config/singleton/Environment";
import { Environment } from "@org/backend/config/singletons/Environment";

export type TokenData = {
token: string;
Expand All @@ -17,7 +17,7 @@ export type JwtPrincipal = {
export class JwtManager {
readonly #req: Request;

static build(req: Request): JwtManager {
static getBy(req: Request): JwtManager {
return new JwtManager(req);
}

Expand Down Expand Up @@ -65,12 +65,12 @@ export class JwtManager {
res.clearCookie("jwt", { httpOnly: true, sameSite: "none", secure: true });
}

public setSecureCookie(res: Response, token: string): void {
public setSecureCookie(res: Response, token: string, days: number = 7): void {
res.cookie("jwt", token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
maxAge: days * 24 * 60 * 60 * 1000, // 7 days
});
}
}
16 changes: 16 additions & 0 deletions packages/backend/src/config/setup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import z from "zod";
import { extendZodWithOpenApi } from "@anatine/zod-openapi";
import { Environment } from "@org/backend/config/singletons/Environment";

extendZodWithOpenApi(z);
Environment.getInstance();

process.on("uncaughtException", err => {
console.error("There was an uncaught error", err);
process.exit(1);
});

process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
process.exit(1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import type { StreamOptions } from "morgan";
import { join } from "path";
import winston from "winston";
import winstonDaily from "winston-daily-rotate-file";

// @backend
import { Environment } from "@org/backend/config/singleton/Environment";
import { Environment } from "@org/backend/config/singletons/Environment";

export class Logger {
private static instance: Logger;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type MongoClientOptions, MongoClient as MongoClientNative } from "mongodb";
import { Environment } from "@org/backend/config/singleton/Environment";
import { Environment } from "@org/backend/config/singletons/Environment";

function buildMongoUri(): string {
const { DB_HOST, DB_PORT } = Environment.getInstance().vars;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { type RouteMiddleware } from "@org/backend/types";
import { type ContractName, type TODO } from "@org/shared";

export class ContractManager {
private static instance: ContractManager;
export class RouterCollection {
private static instance: RouterCollection;

#routers: TODO;

public static getInstance(): ContractManager {
ContractManager.instance ??= new ContractManager();
return ContractManager.instance;
public static getInstance(): RouterCollection {
RouterCollection.instance ??= new RouterCollection();
return RouterCollection.instance;
}

private constructor() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { default as BottleJs } from "bottlejs";
import { type Class } from "@org/shared";
import { InjectableManager } from "@org/backend/config/singleton/InjectableManager";
import { InjectorMetadataManager } from "@org/backend/config/managers/InjectorMetadataManager";

export class Bottle {
private static instance: Bottle;
export class ServiceRegistry {
private static instance: ServiceRegistry;

readonly bottle: BottleJs;
readonly container: BottleJs.IContainer<string>;
readonly injectionClasses: Class[];

public static getInstance(): Bottle {
Bottle.instance ??= new Bottle();
return Bottle.instance;
public static getInstance(): ServiceRegistry {
ServiceRegistry.instance ??= new ServiceRegistry();
return ServiceRegistry.instance;
}

private constructor() {
Expand All @@ -24,22 +24,22 @@ export class Bottle {
if (typeof nameOrContext === "string") {
return this.container[nameOrContext] as T;
}
const containerName = InjectableManager.from(nameOrContext).value.name;
const containerName = InjectorMetadataManager.getBy(nameOrContext).value.name;
return this.container[containerName] as T;
}

public iocStartup() {
const injectionClasses = this.injectionClasses;

const dependencySchema: Record<string, string[]> = injectionClasses.reduce((acc, Class) => {
const { name, dependencies = [] } = InjectableManager.from(Class).value;
const { name, dependencies = [] } = InjectorMetadataManager.getBy(Class).value;
return { ...acc, [name]: dependencies };
}, {});

function sortInjectionClasses(classes: Class[], dependencySchema: Record<string, string[]>) {
return [...classes].sort((classA, classB) => {
const { name: nameA } = InjectableManager.from(classA).value;
const { name: nameB } = InjectableManager.from(classB).value;
const { name: nameA } = InjectorMetadataManager.getBy(classA).value;
const { name: nameB } = InjectorMetadataManager.getBy(classB).value;
if (dependencySchema[nameA].length === 0) return -1;
if (dependencySchema[nameB].length === 0) return 1;
if (dependencySchema[nameA].includes(nameB)) return 1;
Expand All @@ -51,7 +51,7 @@ export class Bottle {
const sortedInjectionClasses = sortInjectionClasses(injectionClasses, dependencySchema);

sortedInjectionClasses.forEach(Class => {
const manager = InjectableManager.from(Class);
const manager = InjectorMetadataManager.getBy(Class);
const decoration = manager.value;
const name = decoration.name;
const constructorParams = decoration.constructorParams;
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/decorators/@Autowired.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createFieldDecorator } from "@tsvdec/decorators";
import { Bottle, InjectableManager } from "@org/backend/config";
import { ServiceRegistry, InjectorMetadataManager } from "@org/backend/config";

export function Autowired<This, Value>() {
return createFieldDecorator<This, Value>(({ meta }) => {
const context = meta.context;
const fieldName = String(context.name);
InjectableManager.from(context).addDependency(fieldName);
InjectorMetadataManager.getBy(context).addDependency(fieldName);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return function (_value: Value) {
return Bottle.getInstance().inject<Value>(fieldName);
return ServiceRegistry.getInstance().inject<Value>(fieldName);
};
});
}
9 changes: 4 additions & 5 deletions packages/backend/src/decorators/@Contract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createMethodDecorator } from "@tsvdec/decorators";
import { Bottle, ContractManager, Logger } from "@org/backend/config";
import { ServiceRegistry, RouterCollection, Logger } from "@org/backend/config";
import { type ContractName, type TODO, ErrorResponse } from "@org/shared";
import { type ErrorLogRepository } from "@org/backend/infrastructure";
import HttpStatus from "http-status";
Expand All @@ -13,7 +13,7 @@ export function Contract<const Name extends ContractName, This, Fn extends Route
async function handler(data: TODO): Promise<TODO> {
const context = meta.context;
try {
return await target.call(Bottle.getInstance().inject(context), data);
return await target.call(ServiceRegistry.getInstance().inject(context), data);
} catch (error) {
const errorResponse =
error instanceof ErrorResponse
Expand All @@ -25,18 +25,17 @@ export function Contract<const Name extends ContractName, This, Fn extends Route
);
const errorContent = errorResponse.content;
const errorLogRepository =
Bottle.getInstance().inject<ErrorLogRepository>("errorLogRepository");
ServiceRegistry.getInstance().inject<ErrorLogRepository>("errorLogRepository");
try {
await errorLogRepository.insertOne(errorContent);
} catch (error) {
Logger.getInstance().logger.error("Error logging failed", error);
}
//return res.status(errorContent.status).json(errorContent);
return { status: 500, body: errorContent };
}
}

ContractManager.getInstance().addRouter(routeName, handler, middleware);
RouterCollection.getInstance().addRouter(routeName, handler, middleware);

return handler as Fn;
});
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/decorators/@Injectable.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { TODO } from "@org/shared";
import { createClassDecorator, type ClassDecoratorSupplier } from "@tsvdec/decorators";
import { Bottle, InjectableManager } from "@org/backend/config";
import { ServiceRegistry, InjectorMetadataManager } from "@org/backend/config";

export function Injectable<This extends new () => TODO>(supplier?: ClassDecoratorSupplier<This>) {
return createClassDecorator<This>(data => {
const { clazz: constructor, meta } = data;
const context = meta.context;
const constructorName: string = constructor.name;
const targetName = normalizeTargetName(constructorName);
InjectableManager.from(context).setName(targetName);
Bottle.getInstance().injectionClasses.push(constructor);
InjectorMetadataManager.getBy(context).setName(targetName);
ServiceRegistry.getInstance().injectionClasses.push(constructor);
supplier?.(data);
});
}
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/decorators/@Repository.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Class } from "@org/shared";
import type z from "zod";

import { InjectableManager } from "@org/backend/config";
import { InjectorMetadataManager } from "@org/backend/config";
import { Injectable } from "@org/backend/decorators/@Injectable";

export function Repository<This extends Class>(zodSchema: z.AnyZodObject) {
const modelName = zodSchema.description;
return Injectable<This>(data => {
const context = data.meta.context;
InjectableManager.from(context).setConstructorParams([modelName]);
InjectorMetadataManager.getBy(context).setConstructorParams([modelName]);
});
}
6 changes: 3 additions & 3 deletions packages/backend/src/decorators/@Transactional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ClientSession } from "mongoose";
import { startSession } from "mongoose";
import { createMethodDecorator } from "@tsvdec/decorators";

import { Bottle, InjectableManager } from "@org/backend/config";
import { ServiceRegistry, InjectorMetadataManager } from "@org/backend/config";

function isClientSession(obj: TODO): obj is ClientSession {
return (
Expand All @@ -25,8 +25,8 @@ export function Transactional() {
const session = isTransactionActive ? lastArg : await startSession();
!isTransactionActive && session.startTransaction();
try {
const container = InjectableManager.from(context).value.name;
const _this = Bottle.getInstance().inject(container);
const container = InjectorMetadataManager.getBy(context).value.name;
const _this = ServiceRegistry.getInstance().inject(container);
const result = isTransactionActive
? await fn.call(_this, ...args)
: await fn.call(_this, ...args, session);
Expand Down
11 changes: 6 additions & 5 deletions packages/backend/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./@Autowired";
export * from "./@Injectable";
export * from "./@Repository";
export * from "./@Transactional";
export * from "./@Contract";
/* @org/backend/decorators */
export * from "@org/backend/decorators/@Autowired";
export * from "@org/backend/decorators/@Injectable";
export * from "@org/backend/decorators/@Repository";
export * from "@org/backend/decorators/@Transactional";
export * from "@org/backend/decorators/@Contract";
4 changes: 2 additions & 2 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* @packageDocumentation This package provides a set of decorators, domain, infrastructure, and web components for building a backend application.
*/

export * from "@org/backend/infrastructure";
export * from "@org/backend/config";
export * from "@org/backend/decorators";
export * from "@org/backend/infrastructure";
export * from "@org/backend/types";
export * from "@org/backend/web";
export * from "@org/backend/config";
24 changes: 14 additions & 10 deletions packages/backend/src/infrastructure/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
export * from "./repository/interface/PaginableRepository";
export * from "./repository/MongoRepository";
export * from "./repository/interface/UserRepository";
export * from "./repository/interface/ErrorLogRepository";
export * from "./repository/impl/UserRepositoryImpl";
export * from "./repository/impl/ErrorLogRepositoryImpl";
export * from "./service/UserService";
export * from "./service/impl/UserServiceImpl";

/* @org/backend/infrastructure/middleware */
export * from "./middleware/globals/withCompression";
export * from "./middleware/globals/withCookieParser";
export * from "./middleware/globals/withCors";
Expand All @@ -21,4 +13,16 @@ export * from "./middleware/locals/withRateLimit";
export * from "./middleware/locals/withUserRoles";
export * from "./middleware/locals/withValidatedBody";
export * from "./middleware/locals/withPaginableParams";
export * from "./middleware/globals/index";
export * from "./middleware/globals/index";

/* @org/backend/infrastructure/repository */
export * from "./repository/interface/PaginableRepository";
export * from "./repository/MongoRepository";
export * from "./repository/interface/UserRepository";
export * from "./repository/interface/ErrorLogRepository";
export * from "./repository/impl/UserRepositoryImpl";
export * from "./repository/impl/ErrorLogRepositoryImpl";

/* @org/backend/infrastructure/service */
export * from "./service/UserService";
export * from "./service/impl/UserServiceImpl";
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function withJwt(tokenType: "access" | "refresh" = "access"): RequestHand
tokenType === "access" ? "ACCESS_TOKEN_SECRET" : "REFRESH_TOKEN_SECRET"
];
return async (req, res, next) => {
const jwtManager = JwtManager.build(req);
const jwtManager = JwtManager.getBy(req);
try {
const result = await jwtManager.verifyToken(tokenSecret);
res.locals.tokenData = result;
Expand Down
Loading

0 comments on commit cad5180

Please sign in to comment.