diff --git a/packages/mern-sample-app/app-node-express/src/ExpressApp.ts b/packages/mern-sample-app/app-node-express/src/ExpressApp.ts index fcb9c64a..da2694cf 100644 --- a/packages/mern-sample-app/app-node-express/src/ExpressApp.ts +++ b/packages/mern-sample-app/app-node-express/src/ExpressApp.ts @@ -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"; @@ -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"; @@ -49,10 +130,7 @@ export class ExpressApp { return `${Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100} MB`; } - public async init( - mocks: Record = {}, - onReady?: (app: ExpressApp) => void, - ): Promise { + public async init(mocks: Record = {}): Promise { log.info("Initializing Swagger"); this.#initializeSwagger(); log.info("Initializing IoC container"); @@ -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 { - 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) { @@ -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 }) { - 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}┘`); - } } diff --git a/packages/mern-sample-app/app-node-express/src/env.ts b/packages/mern-sample-app/app-node-express/src/env.ts index 0bfac8c6..d49f360d 100644 --- a/packages/mern-sample-app/app-node-express/src/env.ts +++ b/packages/mern-sample-app/app-node-express/src/env.ts @@ -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"; @@ -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(",")), diff --git a/packages/mern-sample-app/app-node-express/src/infrastructure/decorators/contract.ts b/packages/mern-sample-app/app-node-express/src/infrastructure/decorators/contract.ts index 2f8ac7ab..8cff1528 100644 --- a/packages/mern-sample-app/app-node-express/src/infrastructure/decorators/contract.ts +++ b/packages/mern-sample-app/app-node-express/src/infrastructure/decorators/contract.ts @@ -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>( +export function contract>( contract: Route, - ...middlewareData: (RouteMiddlewareFactory | RouteMiddlewareFactory[])[] + ...middlewareData: (TsRest.RouteMiddlewareFactory | TsRest.RouteMiddlewareFactory[])[] ) { const middlewareFactories = middlewareData.flat(); @@ -23,7 +18,7 @@ export function contract); + const result = await target.call(container, data as TsRest.RouteInput); await databaseService.commitTransaction(session); return result; } catch (error: unknown) { @@ -35,15 +30,7 @@ export function contract `[role:${role}]`).join(" "); -} diff --git a/packages/mern-sample-app/app-node-express/src/infrastructure/decorators/index.ts b/packages/mern-sample-app/app-node-express/src/infrastructure/decorators/index.ts index bf76bc5e..efe9278b 100644 --- a/packages/mern-sample-app/app-node-express/src/infrastructure/decorators/index.ts +++ b/packages/mern-sample-app/app-node-express/src/infrastructure/decorators/index.ts @@ -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) { + * // 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"; diff --git a/packages/mern-sample-app/app-node-express/src/infrastructure/index.ts b/packages/mern-sample-app/app-node-express/src/infrastructure/index.ts new file mode 100644 index 00000000..a1dd5ba4 --- /dev/null +++ b/packages/mern-sample-app/app-node-express/src/infrastructure/index.ts @@ -0,0 +1,70 @@ +/** + * @packageDocumentation + * + * ## Overview + * The `infrastructure` directory contains the foundational files and folders specific to the application, including controllers, middleware, repositories, services, and decorators. + * It provides core functionality such as user management, routing, security, and dependency injection, while also allowing flexibility for project-specific customizations. + * The `decorators` subdirectory contains core decorators used across the application, though it allows for additional custom decorators if needed by the project. + * + * Unlike core libraries, this folder is project-specific and may vary across implementations. It includes: + * + * - **Controllers**: Handle user-related API requests and route interactions. + * - **Decorators**: Provide core decorators for IoC, autowiring, and route contract management. + * - **Middleware**: Manages request authorization, logging, session handling, and MongoDB session context. + * - **Repository**: Abstracts database interactions with Keycloak for user data retrieval. + * - **Services**: Provides business logic, including user-related services, with mappings from Keycloak to local data models. + * + * ## Features + * - **Controllers**: Defines controllers like `UserController` that handle user-related requests and route mappings using contracts and secured middleware. + * - **Custom Decorators**: Core decorators for injecting dependencies (`inject`), autowiring (`autowired`), and handling routes with transaction support (`contract`). + * - **Middleware Management**: Integrates Keycloak for authorization (`withAuthorization`), session management (`withRouteSession`), request logging (`withMorgan`), and context handling (`withRouteContext`). + * - **Repository Pattern**: Provides an abstraction over Keycloak Admin API interactions through `UserRepository`, which handles user data retrieval. + * - **Service Layer**: The `UserService` class handles core business logic around user management, including mapping Keycloak data to the app’s internal format. + * + * ## How to Use + * + * ### Using Controllers + * Define routes in `UserController` using contracts and secured middleware: + * ```ts + * import { UserController } from "@org/app-node-express/infrastructure/controllers"; + * + * const userController = new UserController(); + * await userController.findAll(); + * ``` + * + * ### Using Custom Decorators + * The `inject` and `autowired` decorators provide dependency injection and autowiring of services: + * ```ts + * import { inject, autowired } from "@org/app-node-express/infrastructure/decorators"; + * + * @inject() + * class MyService { + * @autowired("UserService") + * private userService: UserService; + * } + * ``` + * + * ### Middleware Integration + * Use middleware to handle authorization, logging, session management, and route contexts: + * ```ts + * import { withAuthorization, withMorgan, withRouteSession, withRouteContext } from "@org/app-node-express/infrastructure/middleware"; + * + * app.use(withAuthorization()); + * app.use(withMorgan()); + * app.use(withRouteSession()); + * app.use(withRouteContext()); + * ``` + * + * ### Repositories and Services + * The `UserRepository` and `UserService` abstract interactions with Keycloak and provide business logic for user data: + * ```ts + * const userService = new UserService(); + * const users = await userService.findAll(); + * ``` + * + * ## Customization + * - **Controllers**: Extend or create new controllers using the contract pattern with decorators like `contract` and secured middleware. + * - **Decorators**: Add custom decorators as needed by extending the core decorators in the `decorators` subdirectory. + * - **Middleware**: Adjust or create custom middleware for handling requests, authorization, logging, or context management based on specific project needs. + * - **Repositories and Services**: Extend or modify the repository and service layers to handle additional business logic or external API interactions. + */ diff --git a/packages/mern-sample-app/app-node-express/src/infrastructure/middleware/withMorgan.ts b/packages/mern-sample-app/app-node-express/src/infrastructure/middleware/withMorgan.ts index 69994af6..451353f1 100644 --- a/packages/mern-sample-app/app-node-express/src/infrastructure/middleware/withMorgan.ts +++ b/packages/mern-sample-app/app-node-express/src/infrastructure/middleware/withMorgan.ts @@ -5,20 +5,12 @@ import type { RouteMiddlewareFactory } from "@org/app-node-express/lib/@ts-rest"; import type { RequestHandler } from "express"; -import type { StreamOptions } from "morgan"; -import type * as Winston from "winston"; import { inject } from "@org/app-node-express/infrastructure/decorators"; import { IocRegistry } from "@org/app-node-express/lib/bottlejs"; -import { log } from "@org/app-node-express/logger"; +import { createStream, log } from "@org/app-node-express/lib/winston"; import morgan from "morgan"; -function createStream(logger: Winston.Logger): StreamOptions { - return { - write: (msg: string) => logger.info(msg.substring(0, msg.lastIndexOf("\n"))), - }; -} - export interface MorganMiddleware { middleware(): RequestHandler[]; } diff --git a/packages/mern-sample-app/app-node-express/src/initServer.ts b/packages/mern-sample-app/app-node-express/src/initServer.ts new file mode 100644 index 00000000..c70fb405 --- /dev/null +++ b/packages/mern-sample-app/app-node-express/src/initServer.ts @@ -0,0 +1,110 @@ +/** + * @packageDocumentation + * + * ## Overview + * This module provides the `initServer` function, which is responsible for initializing the Express application. + * It loads necessary IoC modules with mocks and initializes the server with configured {@link middleware global middleware}. + * + * ## Features + * - Scans IoC modules from the build directory + * - Initializes the Express server with middleware and modules + * - Supports mock services for testing environments + * - Logs and handles system errors during initialization + * + * ## How to Use + * + * ### Imports + * + * ```ts + * import { initServer } from "@org/app-node-express/initServer"; + * ``` + * + * ### Basic example of minimal setup + * + * ```ts + * const server = await initServer(); + * // server.startListening(); + * ``` + * + * ### Mocking IoC components + * + * ```ts + * const server = await initServer({ + * mocks: { + * Authorization: AuthorizationMock, + * OtherComponent: OtherComponentMock, + * // ... + * }, + * }); + * ``` + * + * ### Specifying custom absolute path to output directory + * + * ```ts + * const server = await initServer({ + * outDir: "/absolute/path/to/out/dir", + * }); + * ``` + * + * ### Specifying custom relative paths from output directory to use for scanning IoC modules + * + * ```ts + * const server = await initServer({ + * scanDirs: [ + * "infrastructure", + * "lib", + * ], + * }) + * ``` + * + * ### Await in non-async contexts + * + * ```ts + * initServer().then(server => { + * // server.startListening(); + * }); + * ``` + * + * @module initServer + */ + +import type { NoArgsClass } from "@org/lib-commons"; + +import path from "path"; + +import { env } from "@org/app-node-express/env"; +import { ExpressApp } from "@org/app-node-express/ExpressApp"; +import { scanIocModules } from "@org/app-node-express/lib/bottlejs"; +import { log } from "@org/app-node-express/lib/winston"; +import { middleware } from "@org/app-node-express/middleware"; + +export type InitServerConfig = Partial<{ + mocks: Record; + outDir: string; + scanDirs: string[]; +}>; + +export async function initServer(config?: InitServerConfig): Promise { + const { + mocks = {}, + outDir = path.join(process.cwd(), "dist"), + scanDirs = env.SERVER_IOC_SCAN_DIRS, + } = config ?? {}; + + let server: ExpressApp; + + try { + const modules = await scanIocModules(outDir, scanDirs); + server = new ExpressApp({ middleware, modules }); + await server.init(mocks); + } catch (error: unknown) { + if (error instanceof Error) { + log.error(error.stack); + } else { + log.error(JSON.stringify(error)); + } + process.exit(1); + } + + return server; +} diff --git a/packages/mern-sample-app/app-node-express/src/lib/@ts-rest/index.ts b/packages/mern-sample-app/app-node-express/src/lib/@ts-rest/index.ts index dfd5a25d..2ab64bf8 100644 --- a/packages/mern-sample-app/app-node-express/src/lib/@ts-rest/index.ts +++ b/packages/mern-sample-app/app-node-express/src/lib/@ts-rest/index.ts @@ -1,3 +1,58 @@ +/** + * @packageDocumentation + * + * ## Overview + * This module provides functionality for defining and managing routes within an Express application using `@ts-rest`. + * It includes utilities for initializing routes and Swagger documentation, as well as a service for managing and adding routers + * with customizable middleware. The `TsRestRouterService` acts as a singleton, ensuring consistent management of all defined routes and their middleware. + * + * ## Features + * - Initialize Express routes using `@ts-rest`. + * - Build and serve Swagger documentation with customizable OAuth2 redirect, CSS, and JS paths. + * - Define routes with associated handlers and middleware using the `TsRestRouterService`. + * - Manage routes and middleware dynamically, allowing for easy extension and configuration. + * - Suppresses console logs during endpoint creation for cleaner output. + * + * ## How to Use + * + * ### Initialize Express Routes + * ```ts + * import { initializeExpressRoutes } from "@org/app-node-express/lib/@ts-rest"; + * import express from "express"; + * + * const app = express(); + * initializeExpressRoutes(app); + * ``` + * + * ### Initialize Swagger Documentation + * ```ts + * import { initializeSwagger } from "@org/app-node-express/lib/@ts-rest"; + * + * initializeSwagger({ + * app, + * oauth2RedirectUrl: "/oauth2-redirect", + * version: "1.0.0", + * cssPath: "/css/swagger.css", + * jsPath: "/js/swagger.js", + * endpoint: "/api-docs", + * }); + * ``` + * + * ### Add Routers and Middleware + * ```ts + * import { TsRestRouterService } from "@org/app-node-express/lib/@ts-rest"; + * + * const routerService = TsRestRouterService.getInstance(); + * routerService.addRouter(someRouteContract, handler, [someMiddlewareFactory]); + * const routers = routerService.getRouters(); + * ``` + * + * ## Customization + * - **Add Routers**: Use `TsRestRouterService` to add routers by specifying a route contract, handler, and middleware factories. + * - **Custom Swagger**: Adjust Swagger's OAuth2 redirect URL, CSS, and JS paths by passing them to `initializeSwagger()`. + * - **Dynamic Middleware**: Define middleware dynamically by using `RouteMiddlewareFactory` functions that return `express.RequestHandler[]`. + */ + export * from "./TsRestRouterService"; export * from "./TsRestExpressRouteTypes"; export * from "./TsRestExpressService"; diff --git a/packages/mern-sample-app/app-node-express/src/lib/bottlejs/IocScanner.ts b/packages/mern-sample-app/app-node-express/src/lib/bottlejs/IocScanner.ts index 18cec96f..92d67603 100644 --- a/packages/mern-sample-app/app-node-express/src/lib/bottlejs/IocScanner.ts +++ b/packages/mern-sample-app/app-node-express/src/lib/bottlejs/IocScanner.ts @@ -8,18 +8,18 @@ import { IocClassMetadata } from "@org/app-node-express/lib/bottlejs/IocClassMet /** * Scans the specified directories for classes containing **\@inject** decorator. * - * @param packageRoot - The absolute path to the root directory of the package. + * @param outDir - The absolute path to the build directory of the package. * @param relativeDirs - An array of directory paths relative to the package root that should be scanned for components. * @returns A promise that resolves to a record containing component names and their corresponding classes. */ export async function scanIocModules( - packageRoot: string, + outDir: string, relativeDirs: string[], ): Promise> { const registeredComponents: Record = {}; for (const dir of relativeDirs) { - const components = await loadComponentsFromDir(packageRoot, dir); + const components = await loadComponentsFromDir(outDir, dir); components.forEach(component => { registeredComponents[component.name] = component.class; }); diff --git a/packages/mern-sample-app/app-node-express/src/lib/bottlejs/index.ts b/packages/mern-sample-app/app-node-express/src/lib/bottlejs/index.ts index 15c68322..73a9d0ac 100644 --- a/packages/mern-sample-app/app-node-express/src/lib/bottlejs/index.ts +++ b/packages/mern-sample-app/app-node-express/src/lib/bottlejs/index.ts @@ -1,3 +1,47 @@ +/** + * @packageDocumentation + * + * ## Overview + * This module provides a framework for scanning directories and managing dependency injection using an IoC (Inversion of Control) container, + * powered by `BottleJs`. It enables the automatic discovery of classes annotated with the **`@inject`** decorator and registers them in the IoC container. + * The `IocRegistry` class manages the lifecycle and initialization of components, while `IocClassMetadata` handles metadata associated with each component, + * such as name and dependencies. + * + * ## Features + * - **Automatic Component Discovery**: Scans specified directories for classes with the **`@inject`** decorator. + * - **IoC Registry**: Manages component registration and dependency resolution using `BottleJs`. + * - **Dependency Management**: Automatically resolves dependencies between components, ensuring proper initialization order. + * - **Metadata Management**: Associates metadata such as component names and dependencies with classes through `IocClassMetadata`. + * + * ## How to Use + * + * ### Scan IoC Modules + * ```ts + * import { scanIocModules } from "@org/app-node-express/lib/bottlejs"; + * + * const components = await scanIocModules("/path/to/project", ["src/components", "src/services"]); + * console.log(components); // Outputs the discovered components and their classes + * ``` + * + * ### Initialize IoC Registry + * ```ts + * import { IocRegistry } from "@org/app-node-express/lib/bottlejs"; + * + * const registry = IocRegistry.getInstance(); + * registry.iocStartup(components); + * ``` + * + * ### Inject Dependencies + * ```ts + * const serviceInstance = registry.inject("MyService"); + * ``` + * + * ## Customization + * - **Add New Components**: Extend the IoC container by adding classes with the **`@inject`** decorator. + * - **Custom Metadata**: Use `IocClassMetadata` to manually set class names or dependencies when needed. + * - **Modify Component Scanning**: Adjust the directories scanned for components by modifying the paths passed to `scanIocModules`. + */ + export * from "./IocRegistry"; export * from "./IocClassMetadata"; export * from "./IocScanner"; diff --git a/packages/mern-sample-app/app-node-express/src/lib/index.ts b/packages/mern-sample-app/app-node-express/src/lib/index.ts new file mode 100644 index 00000000..0ea810e0 --- /dev/null +++ b/packages/mern-sample-app/app-node-express/src/lib/index.ts @@ -0,0 +1,88 @@ +/** + * @packageDocumentation + * + * ## Overview + * The `lib` directory provides several core modules for the application, including utilities for RESTful routes, IoC (Inversion of Control) dependency injection, + * Keycloak integration for authentication and authorization, and MongoDB for data persistence. Each module is designed to streamline specific aspects of server-side + * application development, from route management and dependency injection to session handling and database operations. + * + * ## Features + * - **REST API Support**: Provides type-safe route handling, Swagger documentation, and middleware integration for Express routes using `@ts-rest`. + * - **IoC and Dependency Injection**: Manages class metadata and dependencies through the `IocRegistry` and `IocClassMetadata` classes, enabling efficient dependency injection. + * - **Keycloak Integration**: Handles Keycloak authentication and token management, including session storage via `express-session` and `memorystore`. + * - **MongoDB Integration**: Simplifies database operations with MongoDB, including CRUD operations, pagination, and transaction management. + * + * ## How to Use + * + * ### REST API Integration (`@ts-rest`) + * - **Initialize Express Routes**: + * ```ts + * import { initializeExpressRoutes } from "@org/app-node-express/lib"; + * const app = express(); + * initializeExpressRoutes(app); + * ``` + * - **Swagger Integration**: + * ```ts + * import { initializeSwagger } from "@org/app-node-express/lib"; + * initializeSwagger({ + * app, + * oauth2RedirectUrl: "/oauth2-redirect", + * version: "1.0.0", + * cssPath: "/css/swagger.css", + * jsPath: "/js/swagger.js", + * endpoint: "/api-docs", + * }); + * ``` + * + * ### IoC and Dependency Injection (`bottlejs`) + * - **Startup IoC Registry**: + * ```ts + * import { IocRegistry } from "@org/app-node-express/lib/bottlejs"; + * const registry = IocRegistry.getInstance(); + * registry.iocStartup(components); + * ``` + * + * ### Keycloak Integration (`keycloak`) + * - **Keycloak Session Handling**: + * ```ts + * import { buildKeycloakSession } from "@org/app-node-express/lib/keycloak"; + * app.use(buildKeycloakSession()); + * ``` + * - **Fetch Keycloak Token**: + * ```ts + * import { KeycloakTokenManager } from "@org/app-node-express/lib/keycloak"; + * const tokenManager = new KeycloakTokenManager(); + * const token = await tokenManager.getToken(); + * ``` + * + * ### MongoDB Integration (`mongodb`) + * - **Initialize MongoDB Client**: + * ```ts + * import { MongoDatabaseService } from "@org/app-node-express/lib/mongodb"; + * const dbService = MongoDatabaseService.getInstance(); + * dbService.client = MongoDatabaseService.buildMongoClient(); + * await dbService.client.connect(); + * ``` + * - **Use MongoDB Repository for CRUD**: + * ```ts + * class MyRepository extends MongoRepository { + * constructor() { + * super(MyDocumentSchema, ["name", "email"]); + * } + * } + * const repo = new MyRepository(); + * const allDocuments = await repo.findAll(); + * ``` + * + * ## Customization + * - **REST Routes and Middleware**: Customize REST routes by adding or modifying middleware via the `RouteMiddlewareFactory` and handling complex route contracts. + * - **IoC Management**: Add or modify components in the IoC container using the `IocRegistry`, and extend `IocClassMetadata` for custom dependency management. + * - **Keycloak API Access**: Extend `KeycloakDao` for additional API interactions or adjust session configurations to suit specific authentication requirements. + * - **MongoDB Operations**: Extend the `MongoRepository` class for advanced database operations such as filtering, sorting, and custom queries. + */ + +export * from "./@ts-rest"; +export * from "./bottlejs"; +export * from "./keycloak"; +export * from "./mongodb"; +export * from "./winston"; diff --git a/packages/mern-sample-app/app-node-express/src/lib/keycloak/index.ts b/packages/mern-sample-app/app-node-express/src/lib/keycloak/index.ts index 056a0ebc..9d2c6090 100644 --- a/packages/mern-sample-app/app-node-express/src/lib/keycloak/index.ts +++ b/packages/mern-sample-app/app-node-express/src/lib/keycloak/index.ts @@ -1,3 +1,50 @@ +/** + * @packageDocumentation + * + * ## Overview + * This module provides functionality for interacting with Keycloak, including managing OAuth tokens and handling Keycloak-related API requests. + * The `KeycloakTokenManager` handles token fetching and caching for efficient API calls, while the `KeycloakDao` provides methods for making authenticated API requests to the Keycloak admin endpoints. + * Additionally, this module includes session management with `express-session` and `memorystore` to manage user sessions securely. + * + * ## Features + * - **Keycloak Token Management**: Fetch and cache OAuth tokens for authentication with Keycloak. + * - **Keycloak Admin API Interaction**: Provides helper methods for making authenticated requests to Keycloak admin endpoints. + * - **Session Management**: Implements session management with `express-session` and `memorystore` for handling Keycloak sessions. + * - **Memory Store for Sessions**: Uses `memorystore` to store sessions with automatic expiration handling. + * + * ## How to Use + * + * ### Fetch a Keycloak Token + * ```ts + * import { KeycloakTokenManager } from "@org/app-node-express/lib/keycloak"; + * + * const tokenManager = new KeycloakTokenManager(); + * const token = await tokenManager.getToken(); + * console.log(token); // Outputs the fetched token + * ``` + * + * ### Make a Keycloak Admin API Request + * ```ts + * import { KeycloakDao } from "@org/app-node-express/lib/keycloak"; + * + * const keycloakDao = new KeycloakDao(); + * const userData = await keycloakDao.get("/users"); + * console.log(userData); // Outputs data fetched from Keycloak + * ``` + * + * ### Set Up Keycloak Session + * ```ts + * import { buildKeycloakSession } from "@org/app-node-express/lib/keycloak"; + * + * app.use(buildKeycloakSession()); + * ``` + * + * ## Customization + * - **Token Configuration**: Adjust the token request configuration in `KeycloakTokenManager` by modifying `KEYCLOAK_LOGIN_CREDENTIALS` or headers in `buildLoginConfig()`. + * - **API Requests**: Extend `KeycloakDao` to add more request methods for interacting with different Keycloak endpoints. + * - **Session Management**: Customize the session store by adjusting the `keycloakMemoryStore` configuration or using a different store implementation. + */ + export * from "./KeycloakMemoryStore"; export * from "./KeycloakTokenManager"; export * from "./KeycloakDao"; diff --git a/packages/mern-sample-app/app-node-express/src/lib/mongodb/index.ts b/packages/mern-sample-app/app-node-express/src/lib/mongodb/index.ts index 46bb1223..467df892 100644 --- a/packages/mern-sample-app/app-node-express/src/lib/mongodb/index.ts +++ b/packages/mern-sample-app/app-node-express/src/lib/mongodb/index.ts @@ -1,3 +1,58 @@ +/** + * @packageDocumentation + * + * ## Overview + * This module provides MongoDB integration for the Express application, including a `MongoDatabaseService` class for managing + * the MongoDB client and handling transactions, and a `MongoRepository` abstract class for implementing data access layers with + * CRUD operations, pagination, and searching capabilities. The module also includes types for filtering, sorting, and searching in MongoDB collections. + * + * ## Features + * - **MongoDB Client Management**: The `MongoDatabaseService` manages the MongoDB connection and transactions, with support for automatic rollback and commit. + * - **CRUD Operations**: The `MongoRepository` class provides an abstraction for CRUD operations (`find`, `insert`, `update`, and `delete`) on MongoDB collections. + * - **Pagination Support**: Built-in pagination methods for efficiently handling large datasets. + * - **Search and Filtering**: Supports regex-based search and advanced filtering across MongoDB documents. + * - **Session Management**: Integrates with Express route context for handling MongoDB sessions. + * + * ## How to Use + * + * ### Initialize MongoDB Client + * ```ts + * import { MongoDatabaseService } from "@org/app-node-express/lib/mongodb"; + * + * const dbService = MongoDatabaseService.getInstance(); + * dbService.client = MongoDatabaseService.buildMongoClient(); + * await dbService.client.connect(); + * ``` + * + * ### Use the MongoRepository for CRUD Operations + * ```ts + * import { MongoRepository } from "@org/app-node-express/lib/mongodb"; + * + * class MyRepository extends MongoRepository { + * constructor() { + * super(MyDocumentSchema, ["name", "email"]); + * } + * } + * + * const repo = new MyRepository(); + * const allDocuments = await repo.findAll(); + * const paginatedResults = await repo.findAllPaginated({ page: 1, rowsPerPage: 10 }); + * ``` + * + * ### MongoDB Transactions + * ```ts + * const session = dbService.client.startSession(); + * await dbService.startTransaction(session); +// perform database operations... + * await dbService.commitTransaction(session); + * ``` + * + * ## Customization + * - **Repository Customization**: Extend the `MongoRepository` class to implement additional methods or adjust the default CRUD behavior for your collections. + * - **Advanced Search and Filter**: Use the `buildMatchPipeline` and `buildSortPipeline` methods for complex querying of MongoDB collections. + * - **Transaction Handling**: Customize the transaction handling in `MongoDatabaseService` by overriding `commitTransaction` or `rollbackTransaction` methods for advanced use cases. + */ + export * from "./MongoDatabaseService"; export * from "./MongoTypes"; export * from "./MongoRepository"; diff --git a/packages/mern-sample-app/app-node-express/src/lib/winston/index.ts b/packages/mern-sample-app/app-node-express/src/lib/winston/index.ts new file mode 100644 index 00000000..a4298476 --- /dev/null +++ b/packages/mern-sample-app/app-node-express/src/lib/winston/index.ts @@ -0,0 +1,144 @@ +/** + * @packageDocumentation + * + * ## Overview + * This module sets up a customized logger using the `winston` library, providing color-coded log levels and timestamped messages. + * The logger is designed to format log output in a structured and readable way, suitable for both development and production environments. + * + * ## Features + * - Color-coded log levels (error, warn, info, debug) + * - Timestamped log entries in the format `YYYY-MM-DD HH:mm:ss` + * - Pretty-printed log level alignment for consistent readability + * - JSON output format for structured logging + * - Logs to the console by default with customizable transports + * + * ## How to Use + * ```ts + * import { log } from "@org/app-node-express/logger"; + * + * log.error('This is an error message'); + * log.warn('This is a warning message'); + * log.info('This is an informational message'); + * log.debug('This is a debug message'); + * ``` + * + * ## Customization + * - **Change Log Format**: Modify the `format` in `createLogger()` to add or adjust log metadata. + * - **Add Transports**: Extend the logger by adding new transports (e.g., file logging, remote logging services). + * - **Adjust Log Level Colors**: Customize the colors for different log levels by changing the `LOGGER_COLORS` object. + */ + +/* eslint-disable no-console */ + +import type { StreamOptions } from "morgan"; + +import * as Winston from "winston"; + +export type WinstonLogger = Winston.Logger; + +const TIMESTAMP_FORMAT = "YYYY-MM-DD HH:mm:ss"; + +const LOGGER_COLORS: Winston.config.AbstractConfigSetColors = { + error: "red", + warn: "yellow", + info: "cyan", + debug: "green", +}; + +export const log = createLogger(); + +export function createStream(logger: WinstonLogger): StreamOptions { + return { + write: (msg: string) => logger.info(msg.substring(0, msg.lastIndexOf("\n"))), + }; +} + +/** + * + * 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 │ + * └──────────────────────────────────────┘ + * ``` + */ +export function logBanner(props: { title: string; data: Record }) { + 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}┘`); +} + +function createLogger(): WinstonLogger { + Winston.addColors(LOGGER_COLORS); + + return Winston.createLogger({ + format: Winston.format.combine( + Winston.format.timestamp({ format: TIMESTAMP_FORMAT }), + Winston.format.json(), + ), + transports: [ + new Winston.transports.Console({ + format: Winston.format.combine( + Winston.format.colorize(), + Winston.format.printf(buildMessage), + ), + }), + ], + }); +} + +function buildMessage(info: Winston.Logform.TransformableInfo): string { + const getLogLevelPrettyPrinted = (coloredLogLevel: string) => { + const AVAILABLE_LOG_LEVELS = ["error", "warn", "info", "debug"]; + const COLORED_LOG_OFFSET = 10; + const longestChar = AVAILABLE_LOG_LEVELS.reduce((acc, lvl) => Math.max(acc, lvl.length), 0); + const longestCharSanitized = coloredLogLevel.length - COLORED_LOG_OFFSET; + const diff = longestChar - longestCharSanitized; + const repeat = diff > 0 ? diff : 0; + return " ".repeat(repeat) + coloredLogLevel; + }; + + return `[${info.timestamp}] ${getLogLevelPrettyPrinted(info.level)}: ${info.message}`; +} diff --git a/packages/mern-sample-app/app-node-express/src/logger.ts b/packages/mern-sample-app/app-node-express/src/logger.ts deleted file mode 100644 index e026e5a8..00000000 --- a/packages/mern-sample-app/app-node-express/src/logger.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as Winston from "winston"; - -export const log = createLogger(); - -function createLogger(): Winston.Logger { - Winston.addColors({ - error: "red", - warn: "yellow", - info: "cyan", - debug: "green", - }); - - return Winston.createLogger({ - format: Winston.format.combine( - Winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - Winston.format.json(), - ), - transports: [ - new Winston.transports.Console({ - format: Winston.format.combine( - Winston.format.colorize(), - Winston.format.printf( - info => `[${info.timestamp}] ${getLogLevelPrettyPrinted(info.level)}: ${info.message}`, - ), - ), - }), - ], - }); -} - -function getLogLevelPrettyPrinted(coloredLogLevel: string) { - const AVAILABLE_LOG_LEVELS = ["error", "warn", "info", "debug"]; - const COLORED_LOG_OFFSET = 10; - - const longestLogLevelCharLength = AVAILABLE_LOG_LEVELS.reduce( - (acc, lvl) => Math.max(acc, lvl.length), - 0, - ); - - const coloredRawLogLevelLength = coloredLogLevel.length - COLORED_LOG_OFFSET; - const diff = longestLogLevelCharLength - coloredRawLogLevelLength; - const repeat = diff > 0 ? diff : 0; - return " ".repeat(repeat) + coloredLogLevel; -} diff --git a/packages/mern-sample-app/app-node-express/src/main.ts b/packages/mern-sample-app/app-node-express/src/main.ts index 990c31e0..50384fed 100644 --- a/packages/mern-sample-app/app-node-express/src/main.ts +++ b/packages/mern-sample-app/app-node-express/src/main.ts @@ -1,7 +1,17 @@ -import { startup } from "./startup"; +import path from "path"; + +import { env } from "@org/app-node-express/env"; +import { initServer } from "@org/app-node-express/initServer"; async function main() { - await startup(); + // Initialize Express server + const server = await initServer({ + outDir: path.join(process.cwd(), "dist"), + scanDirs: env.SERVER_IOC_SCAN_DIRS, + }); + + // Start listening for connections + server.startListening(); } main(); diff --git a/packages/mern-sample-app/app-node-express/src/meta/DecoratorMetadata.ts b/packages/mern-sample-app/app-node-express/src/meta/DecoratorMetadata.ts index 6092ee75..d078a6d7 100644 --- a/packages/mern-sample-app/app-node-express/src/meta/DecoratorMetadata.ts +++ b/packages/mern-sample-app/app-node-express/src/meta/DecoratorMetadata.ts @@ -1,28 +1,19 @@ import type { NoArgsClass } from "@org/lib-commons"; -// @ts-expect-error Stage 3 decorators polyfill. -Symbol.metadata ??= Symbol("Symbol.metadata"); - -declare global { - interface Function { - // @ts-expect-error Stage 3 decorators polyfill. - [Symbol.metadata]: DecoratorMetadataObject; - } -} - export type DecoratorMetadataInjectType = NoArgsClass | DecoratorContext; export class DecoratorMetadata { - public static for(target: DecoratorMetadataInjectType) { - return new DecoratorMetadata(target); - } - #target: DecoratorMetadataInjectType; #metadataRef: DecoratorMetadataObject; - private constructor(target: DecoratorMetadataInjectType) { + public constructor(target: DecoratorMetadataInjectType) { + // Cannot assign to 'metadata' because it is a read-only property. ts(2540) + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#decorator-metadata + // @ts-expect-error TypeScript v5.2 decorators polyfill + Symbol.metadata ??= Symbol("Symbol.metadata"); + this.#target = target; - this.#metadataRef = this.#getMetadataRef(this.#target); + this.#metadataRef = this.getMetadataRef(this.#target); } public hasKey(key: string) { @@ -37,15 +28,20 @@ export class DecoratorMetadata { this.#metadataRef[key] = value; } - #getMetadataRef(target: DecoratorMetadataInjectType): DecoratorMetadataObject { - if (typeof target === "function") { - // @ts-expect-error Stage 3 decorators polyfill. + private getMetadataRef(target: DecoratorMetadataInjectType): DecoratorMetadataObject { + if (this.isClass(target)) { target[Symbol.metadata] ??= {}; - // @ts-expect-error Stage 3 decorators polyfill. return target[Symbol.metadata]!; } - // @ts-expect-error Stage 3 decorators polyfill. + + // Cannot assign to 'metadata' because it is a read-only property. ts(2540) + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#decorator-metadata + // @ts-expect-error TypeScript v5.2 decorators polyfill target.metadata ??= {}; return target.metadata; } + + private isClass(target: DecoratorMetadataInjectType): target is NoArgsClass { + return typeof target === "function"; + } } diff --git a/packages/mern-sample-app/app-node-express/src/meta/DecoratorMetadataEntry.ts b/packages/mern-sample-app/app-node-express/src/meta/DecoratorMetadataEntry.ts index 92123901..aa3b6d4e 100644 --- a/packages/mern-sample-app/app-node-express/src/meta/DecoratorMetadataEntry.ts +++ b/packages/mern-sample-app/app-node-express/src/meta/DecoratorMetadataEntry.ts @@ -9,12 +9,23 @@ export abstract class DecoratorMetadataEntry { protected constructor(target: DecoratorMetadataInjectType, initialState: () => Value) { this.#initialState = initialState; - this.#metadata = DecoratorMetadata.for(target); + this.#metadata = new DecoratorMetadata(target); this.#key = this.constructor.name; } public get value(): Value { - if (!this.#metadata.hasKey(this.#key)) this.#metadata.setValue(this.#key, this.#initialState()); + this.populateIfEmpty(); return this.#metadata.getValue(this.#key) as Value; } + + public set value(value: Value) { + this.populateIfEmpty(); + this.#metadata.setValue(this.#key, value); + } + + private populateIfEmpty() { + const metadataExists = this.#metadata.hasKey(this.#key); + if (metadataExists) return; + this.#metadata.setValue(this.#key, this.#initialState()); + } } diff --git a/packages/mern-sample-app/app-node-express/src/meta/index.ts b/packages/mern-sample-app/app-node-express/src/meta/index.ts index c0cd8acc..6281c686 100644 --- a/packages/mern-sample-app/app-node-express/src/meta/index.ts +++ b/packages/mern-sample-app/app-node-express/src/meta/index.ts @@ -1,2 +1,56 @@ +/** + * @packageDocumentation + * + * ## Overview + * This module defines classes to manage and store metadata for decorators in JavaScript/TypeScript using a Stage 3 decorators polyfill. + * It provides utility classes `DecoratorMetadata` and `DecoratorMetadataEntry` to handle metadata association with classes or functions + * in a structured way, ensuring that metadata is correctly stored and retrieved across different parts of an application. + * + * ## Features + * - Adds and retrieves metadata for classes and functions. + * - Uses Stage 3 decorator polyfill to support future decorator syntax. + * - Provides `DecoratorMetadataEntry` to simplify state management with metadata. + * - Supports both function-based and context-based metadata storage. + * + * ## How to Use + * ```ts + * import { DecoratorMetadata, DecoratorMetadataInjectType } from "@org/app-node-express/meta"; + * + * class SomeClass {}; + * + * // Using DecoratorMetadata + * + * const metadata = new DecoratorMetadata(SomeClass); + * metadata.setValue('key', 'someValue'); + * + * const newReference = new DecoratorMetadata(SomeClass); + * console.log(newReference.getValue('key')); // Outputs 'someValue' + * + * // Using DecoratorMetadataEntry + * + * class MyMetadata extends DecoratorMetadataEntry { + * constructor(target: DecoratorMetadataInjectType) { + * super(target, () => "initialValue"); + * } + * + * writeCustomValue(value: string) { + * this.value = value; + * } + * } + * + * const myMetadata = new MyMetadata(SomeClass); + * myMetadata.writeCustomValue("foo"); + * + * const newReference = new MyMetadata(SomeClass); + * console.log(newReference.value); // Outputs 'foo' + * ``` + * + * ## Customization + * - **Extend `DecoratorMetadataEntry`**: Create custom metadata entry classes by extending `DecoratorMetadataEntry` and providing your own initial state. + * - **Manage Metadata Keys**: Add, retrieve, or check the existence of custom metadata keys with the `DecoratorMetadata` class. + * + * @module meta + */ + export * from "./DecoratorMetadata"; export * from "./DecoratorMetadataEntry"; diff --git a/packages/mern-sample-app/app-node-express/src/middleware/index.ts b/packages/mern-sample-app/app-node-express/src/middleware/index.ts index 3a3dd7c9..a8b7de31 100644 --- a/packages/mern-sample-app/app-node-express/src/middleware/index.ts +++ b/packages/mern-sample-app/app-node-express/src/middleware/index.ts @@ -1,3 +1,36 @@ +/** + * @packageDocumentation + * + * ## Overview + * This module defines and exports the middleware stack used by **all** Express routes. + * The middleware is applied in a specific order to handle various tasks such as serving static assets, + * managing sessions, handling security, and logging, ensuring that requests are processed securely and efficiently. + * + * ## Features + * - Serves static assets + * - Manages sessions and request context + * - Compresses HTTP responses + * - Parses JSON, cookies, and URL-encoded bodies + * - Implements CORS and credentials handling + * - Adds security layers (CSP, HPP) + * - Logs HTTP requests with Morgan + * - Handles route authorization + * + * ## How to Use + * ```ts + * import { ExpressApp } from "@org/app-node-express/ExpressApp" + * import { middleware } from "@org/app-node-express/middleware"; + * + * const app = new ExpressApp({ middleware }); + * ``` + * + * ## Customization + * - **Add/Remove Middleware**: Modify the `middleware` array to add or remove specific middleware functions. + * - **Rearrange Order**: Be cautious when changing the order, as some middleware (like `withStaticAssets`) must run first for optimal performance. + * + * @module middleware + */ + import { withAuthorization } from "@org/app-node-express/infrastructure/middleware/withAuthorization"; import { withMorgan } from "@org/app-node-express/infrastructure/middleware/withMorgan"; import { withRouteContext } from "@org/app-node-express/infrastructure/middleware/withRouteContext"; @@ -13,10 +46,8 @@ import { withJsonParser } from "@org/app-node-express/middleware/withJsonParser" import { withStaticAssets } from "@org/app-node-express/middleware/withStaticAssets"; import { withUrlEncoded } from "@org/app-node-express/middleware/withUrlEncoded"; -// Order matters! - export const middleware = [ - withStaticAssets(), // withStaticAssets should always be on top! + withStaticAssets(), withRouteSession(), withRouteContext(), withCompression(), diff --git a/packages/mern-sample-app/app-node-express/src/startup.ts b/packages/mern-sample-app/app-node-express/src/startup.ts deleted file mode 100644 index 6ba8c620..00000000 --- a/packages/mern-sample-app/app-node-express/src/startup.ts +++ /dev/null @@ -1,35 +0,0 @@ -import path from "path"; - -import { env } from "@org/app-node-express/env"; -import { ExpressApp } from "@org/app-node-express/ExpressApp"; -import { scanIocModules } from "@org/app-node-express/lib/bottlejs"; -import { log } from "@org/app-node-express/logger"; -import { middleware } from "@org/app-node-express/middleware"; -import { type NoArgsClass } from "@org/lib-commons"; - -import { MongoDatabaseService } from "./lib/mongodb"; - -export async function startup( - mocks: Record = {}, - onReady?: (app: ExpressApp) => void, - listen: boolean = true, -) { - try { - const PATH_TO_BUILD_DIR = path.join(process.cwd(), "dist"); - const modules = await scanIocModules(PATH_TO_BUILD_DIR, env.SERVER_IOC_SCAN_DIRS); - const server = new ExpressApp({ middleware, modules }); - await server.init(mocks, onReady); - const databaseService = MongoDatabaseService.getInstance(); - databaseService.client = server.mongoClient; - if (listen) { - await server.startListening(); - } - } catch (error: unknown) { - if (typeof error === "object" && error !== null && "message" in error) { - log.error((error as { message: string }).message); - } else { - log.error(error); - } - process.exit(1); - } -} diff --git a/packages/mern-sample-app/app-node-express/test/setup/setupFiles.ts b/packages/mern-sample-app/app-node-express/test/setup/setupFiles.ts index 91944ab8..c3153889 100644 --- a/packages/mern-sample-app/app-node-express/test/setup/setupFiles.ts +++ b/packages/mern-sample-app/app-node-express/test/setup/setupFiles.ts @@ -1,11 +1,15 @@ /// import { MongoDatabaseService } from "../../dist/lib/mongodb/MongoDatabaseService"; -import { startup } from "../../dist/startup"; +import { initServer } from "../../dist/initServer"; import __mocks__ from "../__mocks__"; +import { initServer } from "@ts-rest/express"; beforeAll(async () => { - await startup(__mocks__, server => (globalThis.expressApp = server.expressApp), false); + const server = await initServer({ + mocks: __mocks__, + }); + globalThis.expressApp = server.expressApp; }); afterAll(async () => { diff --git a/packages/mern-sample-app/app-node-express/tsconfig.json b/packages/mern-sample-app/app-node-express/tsconfig.json index 5a08c89c..63f4d7a5 100644 --- a/packages/mern-sample-app/app-node-express/tsconfig.json +++ b/packages/mern-sample-app/app-node-express/tsconfig.json @@ -6,6 +6,7 @@ "types": [], "esModuleInterop": true, "moduleResolution": "Node", + "lib": ["ESNext.Decorators"], "baseUrl": "./", "paths": { diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/Home/HomePage.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/Home/HomePage.tsx index 4096aa58..a26dfe97 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/Home/HomePage.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/Home/HomePage.tsx @@ -1,4 +1,4 @@ -import type { PaginationOptions, UserPaginationResultDto , User } from "@org/lib-api-client"; +import type { PaginationOptions, UserPaginationResultDto, User } from "@org/lib-api-client"; import * as icons from "@mui/icons-material"; import * as mui from "@mui/material"; @@ -34,6 +34,7 @@ export function HomePage() { const deleteUser = useCallback( async (username: string) => { + // @ts-expect-error Remove later const response = await tsRestApiClient.User.deleteByUsername({ body: { username } }); if (response.status !== 201) throw new Error("Failed to delete user."); fetchUsers(); diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/Home/UserCreateFormButton.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/Home/UserCreateFormButton.tsx index 52f48cb4..f7390f8c 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/Home/UserCreateFormButton.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/Home/UserCreateFormButton.tsx @@ -34,6 +34,7 @@ export function UserCreateFormButton({ afterUpdate }: UserCreateFormButtonProps) // Handle form submission // eslint-disable-next-line no-console console.log("Form submitted:", user); + // @ts-expect-error Remove later await tsRestApiClient.User.createOne({ body: user, });