Skip to content

Commit

Permalink
chore: some more refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
brunotot committed Sep 22, 2024
1 parent cc15170 commit 5228b2b
Show file tree
Hide file tree
Showing 25 changed files with 963 additions and 226 deletions.
182 changes: 100 additions & 82 deletions packages/mern-sample-app/app-node-express/src/ExpressApp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,85 @@
/* eslint-disable no-console */
/**
* @packageDocumentation
*
* ## Overview
* The `ExpressApp` class serves as the main class responsible for configuring and running an Express-based server.
* It handles the initialization of key components such as middleware, IoC modules, routes, and Swagger documentation.
* Additionally, it manages the MongoDB connection and provides detailed logging throughout the application lifecycle.
* This class provides the foundation for running and managing the server, making it extensible and configurable.
*
* ## Features
* - **Middleware Setup**: Supports adding and managing global middleware.
* - **IoC Module Integration**: Automatically scans and loads IoC (Inversion of Control) modules for dependency injection.
* - **MongoDB Integration**: Establishes a connection to MongoDB and injects the client into the server lifecycle.
* - **Swagger Documentation**: Integrates Swagger for API documentation, configured via environment variables.
* - **Global Error Handling**: Implements a global error handler to catch and log errors gracefully.
* - **Server Information Logging**: Logs detailed information about the server's environment, such as Node.js version, memory usage, and startup time.
*
* ## How to Use
*
* To create and initialize an `ExpressApp` instance:
*
* ```ts
* import { ExpressApp } from "@org/app-node-express/ExpressApp";
*
* const app = new ExpressApp();
*
* await app.init();
* await app.startListening();
* ```
*
* You can also pass custom middleware and IoC modules during instantiation:
*
* ```ts
* const app = new ExpressApp({
* middleware: [customMiddleware1, customMiddleware2],
* modules: { CustomModule: CustomModuleClass },
* });
* ```
*
* To execute additional logic when the app is fully initialized, use the `onReady` callback in the `init` method:
*
* ```ts
* await app.init({}, (app) => {
* console.log('App is fully initialized and ready!');
* });
* ```
*
* ## Key Methods and Components
*
* - **init**: Initializes the server by loading middleware, routes, IoC modules, and connecting to MongoDB.
* - **startListening**: Starts the server and begins listening for incoming HTTP requests on the configured port.
* - **#initializeDatabase**: Establishes the connection to MongoDB using `MongoDatabaseService`.
* - **#initializeIoc**: Configures and loads Inversion of Control (IoC) modules for dependency injection.
* - **#initializeGlobalMiddlewares**: Adds global middleware to the Express app.
* - **#initializeSwagger**: Integrates Swagger for API documentation.
* - **#initializeErrorHandlerMiddleware**: Sets up the global error handler for catching and logging application errors.
* - **#logTable**: Logs a table of detailed server information (Node.js version, memory usage, PID, etc.) when the server starts.
*
* ## Customization
* - **Middleware**: Add or override global middleware by passing an array of middleware functions to the `ExpressApp` constructor.
* - **IoC Modules**: Inject custom services or modules by passing them to the `modules` option when initializing the `ExpressApp`.
* - **Mocking Services**: During testing, you can mock services and modules by passing a `mocks` object to the `init` method.
* - **Error Handling**: Customize error handling by extending or modifying the `#initializeErrorHandlerMiddleware` method.
*
* ## Example Usage
*
* ```ts
* import { ExpressApp } from "@org/app-node-express/ExpressApp";
*
* const app = new ExpressApp({
* middleware: [myCustomMiddleware],
* modules: { MyService: MyServiceClass },
* });
*
* await app.init();
* await app.startListening();
* ```
*
* The server will automatically load Swagger documentation and connect to MongoDB. Any errors during startup will be logged, and the process will exit gracefully if critical issues occur.
*
* @module ExpressApp
*/

import type { RouteMiddlewareFactory } from "@org/app-node-express/lib/@ts-rest";
import type { MongoClient } from "@org/app-node-express/lib/mongodb";
Expand All @@ -8,7 +89,7 @@ import { env } from "@org/app-node-express/env";
import { initializeExpressRoutes, initializeSwagger } from "@org/app-node-express/lib/@ts-rest";
import { IocRegistry } from "@org/app-node-express/lib/bottlejs";
import { MongoDatabaseService } from "@org/app-node-express/lib/mongodb";
import { log } from "@org/app-node-express/logger";
import { log, logBanner } from "@org/app-node-express/lib/winston";
import { getTypedError } from "@org/lib-api-client";
import express from "express";

Expand Down Expand Up @@ -49,10 +130,7 @@ export class ExpressApp {
return `${Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100} MB`;
}

public async init(
mocks: Record<string, NoArgsClass> = {},
onReady?: (app: ExpressApp) => void,
): Promise<void> {
public async init(mocks: Record<string, NoArgsClass> = {}): Promise<void> {
log.info("Initializing Swagger");
this.#initializeSwagger();
log.info("Initializing IoC container");
Expand All @@ -66,34 +144,31 @@ export class ExpressApp {
log.info("Connecting to database");
await this.#initializeDatabase();
log.info("App successfully initialized!");
onReady?.(this);
}

public async startListening(): Promise<void> {
return new Promise(resolve => {
log.info("Server connecting...");
this.expressApp.listen(this.port, () => {
this.#logTable({
title: `[Express] ${env.SERVER_NAME} v${env.SERVER_VERSION}`,
data: {
"🟢 NodeJS": process.version,
"🏠 Env": env.SERVER_ENV,
"🔑 Keycloak": this.keycloakUrl,
"📝 Swagger": env.TS_REST_SWAGGER_ENDPOINT,
"🆔 PID": `${process.pid}`,
"🧠 Memory": this.memoryUsage,
"📅 Started": new Date().toLocaleString(),
},
});
log.info(`🚀 App listening on port ${this.port}`);
resolve();
public startListening(): void {
log.info("Server connecting...");
this.expressApp.listen(this.port, () => {
logBanner({
title: `[Express] ${env.SERVER_NAME} v${env.SERVER_VERSION}`,
data: {
"🟢 NodeJS": process.version,
"🏠 Env": env.SERVER_ENV,
"🔑 Keycloak": this.keycloakUrl,
"📝 Swagger": env.TS_REST_SWAGGER_ENDPOINT,
"🆔 PID": `${process.pid}`,
"🧠 Memory": this.memoryUsage,
"📅 Started": new Date().toLocaleString(),
},
});
log.info(`🚀 Server listening on port ${this.port}`);
});
}

async #initializeDatabase() {
this.#mongoClient = MongoDatabaseService.buildMongoClient();
await this.#mongoClient.connect();
MongoDatabaseService.getInstance().client = this.#mongoClient;
}

#initializeIoc(mocks: Record<string, NoArgsClass>) {
Expand Down Expand Up @@ -135,61 +210,4 @@ export class ExpressApp {
};
this.expressApp.use(errorHandler);
}

/**
*
* An example output might be:
*
* ```
* ┌──────────────────────────────────────┐
* │ [Express] app-node-express v0.0.1 │
* ├──────────────────────────────────────┤
* │ 🟢 NodeJS : v21.7.0 │
* │ 🏠 Env : development │
* │ 📝 Swagger : /api-docs │
* │ 🆔 PID : 61178 │
* │ 🧠 Memory : 24.65 MB │
* │ 📅 Started : 8/19/2024, 7:40:59 PM │
* └──────────────────────────────────────┘
* ```
*/
#logTable(props: { title: string; data: Record<string, string> }) {
const title = props.title;
const data = props.data;
const kvSeparator = " : ";
const padding = 2;

const center = (text: string, length: number) => {
const remainingSpace = length - text.length;
const leftBorderCount = Math.floor(remainingSpace / 2);
const rightBorderCount = remainingSpace - leftBorderCount;
const left = " ".repeat(leftBorderCount);
const right = " ".repeat(rightBorderCount);
return `${left}${text}${right}`;
};

const spacer = " ".repeat(padding);
const hrY = kvSeparator;
const maxKeyLength = Math.max(...Object.keys(data).map(key => key.length));

const keyValueLengths = Object.values(data).map(
value => maxKeyLength + hrY.length + value.length,
);

const containerWidth = Math.max(title.length, ...keyValueLengths) + padding * 2;

const content = Object.entries(data).map(([key, value]) => {
const keyPadding = " ".repeat(maxKeyLength - key.length);
const text = `${key}${keyPadding}${hrY}${value}`;
const remainder = " ".repeat(containerWidth - text.length - spacer.length * 2);
return `│${spacer}${text}${remainder}${spacer}│`;
});

const hrX = `${"─".repeat(containerWidth)}`;
console.info(`┌${hrX}┐`);
console.info(`│${center(title, containerWidth)}│`);
console.info(`├${hrX}┤`);
content.forEach(text => console.info(text));
console.info(`└${hrX}┘`);
}
}
46 changes: 42 additions & 4 deletions packages/mern-sample-app/app-node-express/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
/**
* @packageDocumentation Environment setup.
* @packageDocumentation
*
* ## Overview
* This module is responsible for parsing, validating, and managing environment variables for the Express application.
* It uses `zod` schemas to ensure that all required environment variables are present and correctly typed. The module
* also integrates `dotenv` to load variables from `.env` files, allowing configuration across different environments like
* development, testing, and production.
*
* ## Features
* - Validates environment variables using `zod` schemas
* - Automatically loads environment variables from `.env.[environment].local`
* - Handles MongoDB and Keycloak configuration
* - Parses and transforms CORS, Keycloak, and Swagger-related settings
* - Provides fallback default values for non-mandatory variables
*
* ## How to Use
* ```ts
* import { env } from "@org/app-node-express/env";
*
* // Access the validated environment variables
* console.log(env.SERVER_PORT);
* console.log(env.DATABASE_URL);
* ```
*
* For test environments, you can check with:
* ```ts
* import { testMode } from "@org/app-node-express/env";
*
* if (testMode()) {
* console.log("Running in test mode");
* }
* ```
*
* ## Customization
* - **Add New Variables**: To extend the schema, add new fields to the `ENVIRONMENT_VARS` schema.
* - **Change Defaults**: Modify default values in the schema definition for any variables.
* - **Error Handling**: Customize error handling logic within the `parseEnvironmentVars()` function to provide more tailored error messages.
*
* @module env
*/

import path from "path";
Expand Down Expand Up @@ -35,9 +73,9 @@ const ENVIRONMENT_VARS = z.object({

// @ts-rest
TS_REST_SWAGGER_ENDPOINT: z.string().default("/api-docs").transform(s => (s.startsWith("/") ? s : `/${s}`)),
TS_REST_SWAGGER_CSS_PATH: z.string().default("/css/swagger.css"),
TS_REST_SWAGGER_JS_PATH: z.string().default("/js/swagger.js"),
TS_REST_SWAGGER_OAUTH2_REDIRECT_ENDPOINT: z.string().default("/oauth2-redirect.html"),
TS_REST_SWAGGER_CSS_PATH: z.string().default("/css/swagger.css").transform(s => (s.startsWith("/") ? s : `/${s}`)),
TS_REST_SWAGGER_JS_PATH: z.string().default("/js/swagger.js").transform(s => (s.startsWith("/") ? s : `/${s}`)),
TS_REST_SWAGGER_OAUTH2_REDIRECT_ENDPOINT: z.string().default("/oauth2-redirect.html").transform(s => (s.startsWith("/") ? s : `/${s}`)),

// cors
CORS_ALLOWED_ORIGINS: z.string().transform(s => s.split(",")),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import type {
RouteHandler,
RouteInput,
RouteMiddlewareFactory,
} from "@org/app-node-express/lib/@ts-rest";
import type { AppRoute } from "@ts-rest/core";

import { TsRestRouterService } from "@org/app-node-express/lib/@ts-rest";
import * as TsRest from "@org/app-node-express/lib/@ts-rest";
import { IocRegistry } from "@org/app-node-express/lib/bottlejs";
import { MongoDatabaseService } from "@org/app-node-express/lib/mongodb";
import { getTypedError } from "@org/lib-api-client";

export function contract<const Route extends AppRoute, This, Fn extends RouteHandler<Route>>(
export function contract<const Route extends AppRoute, This, Fn extends TsRest.RouteHandler<Route>>(
contract: Route,
...middlewareData: (RouteMiddlewareFactory | RouteMiddlewareFactory[])[]
...middlewareData: (TsRest.RouteMiddlewareFactory | TsRest.RouteMiddlewareFactory[])[]
) {
const middlewareFactories = middlewareData.flat();

Expand All @@ -23,7 +18,7 @@ export function contract<const Route extends AppRoute, This, Fn extends RouteHan
try {
databaseService.startTransaction(session);
const container = IocRegistry.getInstance().inject(context);
const result = await target.call(container, data as RouteInput<Route>);
const result = await target.call(container, data as TsRest.RouteInput<Route>);
await databaseService.commitTransaction(session);
return result;
} catch (error: unknown) {
Expand All @@ -35,15 +30,7 @@ export function contract<const Route extends AppRoute, This, Fn extends RouteHan
}
}

contract.summary = buildContractSummary(contract.summary);
TsRestRouterService.getInstance().addRouter(contract, handler, middlewareFactories);
TsRest.TsRestRouterService.getInstance().addRouter(contract, handler, middlewareFactories);
return handler as Fn;
};
}

function buildContractSummary(plainSummary: string = "" /*, roles: string[]*/) {
// TODO Handle somewhen in the future
const disableFeature = true;
if (disableFeature) return plainSummary;
//return (plainSummary ?? "") + " " + roles.map(role => `[role:${role}]`).join(" ");
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,62 @@
/**
* @packageDocumentation
*
* ## Overview
* This module provides decorators and utility functions for dependency injection, route handling, and transaction management
* within an Express application using the IoC (Inversion of Control) pattern. The `inject` and `autowired` decorators facilitate
* automatic dependency injection, while the `contract` decorator integrates route handlers with transaction support.
*
* ## Features
* - **Dependency Injection**: The `inject` decorator registers a class in the IoC container with a specified or inferred name.
* - **Autowired Fields**: The `autowired` decorator automatically injects dependencies into class fields by resolving them from the IoC container.
* - **Route Contract Handling**: The `contract` decorator handles route definition, transaction management, and error handling for routes.
* - **Transaction Management**: Automatically manages MongoDB transactions within route handlers.
*
* ## How to Use
*
* ### Injecting Dependencies
* Use the `inject` decorator to register a class in the IoC container:
* ```ts
* import { inject } from "@org/app-node-express/infrastructure/decorators";
*
* @inject()
* class MyService {
* // Service logic here
* }
* ```
*
* ### Autowiring Dependencies
* Use the `autowired` decorator to inject dependencies into class fields:
* ```ts
* import { inject, autowired } from "@org/app-node-express/infrastructure/decorators";
*
* @inject()
* class MyController {
* @autowired() myService: MyService;
* }
* ```
*
* ### Handling Routes with Transaction Support
* Use the `contract` decorator to define routes that include middleware and MongoDB transaction management:
* ```ts
* import { inject, contract } from "@org/app-node-express/infrastructure/decorators";
* import { MyRouteContract } from "@org/lib-api-client";
*
* @inject()
* class MyController {
* @contract(MyRouteContract)
* async handleRoute(data: RouteInput<MyRouteContract>) {
* // Business logic
* }
* }
* ```
*
* ## Customization
* - **Inject Custom Names**: Specify a custom name for a class or field when using `inject` or `autowired` to avoid name collisions.
* - **Middleware Integration**: Use the `contract` decorator with middleware factories to add middleware to routes before execution.
* - **Transaction Handling**: Customize the transaction handling in `contract` by modifying the logic inside the decorator or transaction methods.
*/

export * from "./autowired";
export * from "./contract";
export * from "./inject";
Loading

0 comments on commit 5228b2b

Please sign in to comment.