codeweaver 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +20 -0
- package/README.md +41 -14
- package/package.json +6 -1
- package/src/config.ts +8 -6
- package/src/constants.ts +6 -1
- package/src/main.ts +44 -11
- package/src/routers/orders/index.router.ts +3 -2
- package/src/routers/orders/order.controller.ts +3 -6
- package/src/routers/products/index.router.ts +2 -1
- package/src/routers/products/product.controller.ts +3 -9
- package/src/routers/users/index.router.ts +2 -1
- package/src/routers/users/user.controller.ts +2 -5
- package/src/utilities/container.ts +159 -0
- package/src/utilities/error-handling.ts +36 -0
- package/src/utilities/logger/base-logger.interface.ts +11 -0
- package/src/utilities/logger/logger.config.ts +95 -0
- package/src/utilities/logger/logger.service.ts +122 -0
- package/src/utilities/logger/winston-logger.service.ts +16 -0
- package/.vscode/settings.json +0 -3
- package/src/app.ts +0 -3
- package/src/utilities/router.ts +0 -0
package/.env.example
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Environment
|
|
2
|
+
NODE_ENV=development # or "production" depending on your deployment
|
|
3
|
+
|
|
4
|
+
# Server
|
|
5
|
+
HTTP=http://localhost:3000
|
|
6
|
+
PORT=3000
|
|
7
|
+
|
|
8
|
+
# Feature flags
|
|
9
|
+
SWAGGER=true
|
|
10
|
+
|
|
11
|
+
# Timeouts and limits
|
|
12
|
+
TIMEOUT=30
|
|
13
|
+
|
|
14
|
+
# Rate limiting (separate vars)
|
|
15
|
+
RATE_LIMIT_TIME_SPAN=60
|
|
16
|
+
RATE_LIMIT_ALLOWED_CALLS=100
|
|
17
|
+
|
|
18
|
+
# Memoization and cache
|
|
19
|
+
MEMOIZE_TIME=300
|
|
20
|
+
CACHE_SIZE=1000
|
package/README.md
CHANGED
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
- **Express**: A lightweight web framework for building server-side applications in Node.js.
|
|
11
11
|
- **TypeScript**: Adds strong typing for enhanced development experience and reduced runtime errors.
|
|
12
12
|
- **Modular Router Structure**: Automates importing and mounting routers, ensuring a clean separation of endpoints and logic for easier scalability.
|
|
13
|
+
- **Dependency resolver**: A simple dependency resolver that uses a lightweight container to manage and inject dependencies at runtime.
|
|
13
14
|
- **Swagger Integration**: Automatically generates interactive API documentation, facilitating easier understanding of available endpoints for developers and consumers.
|
|
14
15
|
- **Async Handlers**: Utilizes async/await syntax for cleaner, more maintainable asynchronous code without callback nesting.
|
|
15
16
|
- **Zod**: Implements schema validation for input data.
|
|
16
|
-
- **
|
|
17
|
+
- **Utils-decorators**: A collection of middleware utilities utils-decorators (throttling, caching, and error handling) designed to strengthen application resilience.
|
|
18
|
+
- **Logger**: A Winston-based logger (with LogForm) that provides scalable, leveled logging, structured JSON output, and pluggable transports (console and file)
|
|
19
|
+
|
|
20
|
+
Here's a revised Installation section that explicitly supports pnpm in addition to npm. I kept the formatting and steps intact, adding clear pnpm equivalents.
|
|
17
21
|
|
|
18
22
|
## Installation
|
|
19
23
|
|
|
@@ -29,9 +33,19 @@ To get started with the project, follow these steps:
|
|
|
29
33
|
|
|
30
34
|
2. **Install dependencies**:
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
Choose your package manager and install:
|
|
37
|
+
|
|
38
|
+
- Using npm:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- Using pnpm:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm install
|
|
48
|
+
```
|
|
35
49
|
|
|
36
50
|
3. **Run the application**:
|
|
37
51
|
|
|
@@ -39,14 +53,29 @@ To get started with the project, follow these steps:
|
|
|
39
53
|
npm start
|
|
40
54
|
```
|
|
41
55
|
|
|
56
|
+
Or, if you used pnpm:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pnpm start
|
|
60
|
+
```
|
|
61
|
+
|
|
42
62
|
4. **Visit the Swagger UI**: Open your browser and go to `http://localhost:3000/api-docs` to view the automatically generated API documentation.
|
|
43
63
|
|
|
44
64
|
5. **Build**: Compile the TypeScript files for the production environment:
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
- Using npm:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run build
|
|
70
|
+
npm run serve
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Using pnpm:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pnpm run build
|
|
77
|
+
pnpm run serve
|
|
78
|
+
```
|
|
50
79
|
|
|
51
80
|
## Sample Project Structure
|
|
52
81
|
|
|
@@ -89,9 +118,10 @@ Example of a basic router:
|
|
|
89
118
|
import { Router, Request, Response } from "express";
|
|
90
119
|
import asyncHandler from "express-async-handler";
|
|
91
120
|
import UserController from "./user.controller";
|
|
121
|
+
import { resolve } from "@/utilities/container";
|
|
92
122
|
|
|
93
123
|
const router = Router();
|
|
94
|
-
const userController =
|
|
124
|
+
const userController = resolve(UserController);
|
|
95
125
|
|
|
96
126
|
/**
|
|
97
127
|
* @swagger
|
|
@@ -210,17 +240,13 @@ import config from "@/config";
|
|
|
210
240
|
import { users } from "@/db";
|
|
211
241
|
import { User } from "@/entities/user.entity";
|
|
212
242
|
import { MapAsyncCache } from "@/utilities/cache/memory-cache";
|
|
243
|
+
import { Injectable } from "@/utilities/container";
|
|
213
244
|
|
|
214
245
|
function exceedHandler() {
|
|
215
246
|
const message = "Too much call in allowed window";
|
|
216
247
|
throw new ResponseError(message, 429);
|
|
217
248
|
}
|
|
218
249
|
|
|
219
|
-
function userNotFoundHandler(e: ResponseError) {
|
|
220
|
-
const message = "User not found.";
|
|
221
|
-
throw new ResponseError(message, 404, e?.message);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
250
|
function invalidInputHandler(e: ResponseError) {
|
|
225
251
|
const message = "Invalid input";
|
|
226
252
|
throw new ResponseError(message, 400, e?.message);
|
|
@@ -229,6 +255,7 @@ function invalidInputHandler(e: ResponseError) {
|
|
|
229
255
|
const usersCache = new MapAsyncCache<UserDto[]>(config.cacheSize);
|
|
230
256
|
const userCache = new MapAsyncCache<UserDto>(config.cacheSize);
|
|
231
257
|
|
|
258
|
+
@Injectable()
|
|
232
259
|
/**
|
|
233
260
|
* Controller for handling user-related operations
|
|
234
261
|
* @class UserController
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeweaver",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"main": "src/main.ts",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-codeweaver-app": "./command.js"
|
|
@@ -37,9 +37,14 @@
|
|
|
37
37
|
"dotenv": "^17.2.3",
|
|
38
38
|
"express": "^5.1.0",
|
|
39
39
|
"express-async-handler": "^1.2.0",
|
|
40
|
+
"express-winston": "^4.2.0",
|
|
40
41
|
"ioredis": "^5.8.2",
|
|
42
|
+
"logform": "^2.7.0",
|
|
43
|
+
"reflect-metadata": "^0.2.2",
|
|
41
44
|
"swagger-jsdoc": "^3.7.0",
|
|
42
45
|
"utils-decorators": "^2.10.0",
|
|
46
|
+
"winston": "^3.18.3",
|
|
47
|
+
"winston-daily-rotate-file": "^5.0.0",
|
|
43
48
|
"zod": "^4.0.14"
|
|
44
49
|
},
|
|
45
50
|
"devDependencies": {
|
package/src/config.ts
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
rateLimitTimeSpan,
|
|
5
5
|
rateLimitAllowedCalls,
|
|
6
6
|
timeout,
|
|
7
|
-
|
|
7
|
+
port,
|
|
8
8
|
cacheSize,
|
|
9
|
+
http,
|
|
9
10
|
} from "./constants";
|
|
10
11
|
import { SwaggerOptions } from "./swagger-options";
|
|
11
12
|
import { stringToBoolean } from "./utilities/conversion";
|
|
@@ -19,6 +20,7 @@ import { stringToBoolean } from "./utilities/conversion";
|
|
|
19
20
|
*/
|
|
20
21
|
interface Config {
|
|
21
22
|
devMode: boolean;
|
|
23
|
+
http: string;
|
|
22
24
|
port: number;
|
|
23
25
|
swagger: boolean;
|
|
24
26
|
swaggerOptions: SwaggerOptions;
|
|
@@ -29,11 +31,10 @@ interface Config {
|
|
|
29
31
|
cacheSize: number;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
const port = Number(process.env.PORT) || portNumber;
|
|
33
|
-
|
|
34
34
|
let config: Config = {
|
|
35
35
|
devMode: process.env.NODE_ENV !== productionEnvironment,
|
|
36
|
-
|
|
36
|
+
http: process.env.HTTP || http,
|
|
37
|
+
port: Number(process.env.PORT) || port,
|
|
37
38
|
swagger: stringToBoolean(process.env.SWAGGER || "true"),
|
|
38
39
|
swaggerOptions: {
|
|
39
40
|
swaggerDefinition: {
|
|
@@ -57,9 +58,10 @@ let config: Config = {
|
|
|
57
58
|
], // Path to the API docs
|
|
58
59
|
},
|
|
59
60
|
timeout: Number(process.env.TIMEOUT) || timeout,
|
|
60
|
-
rateLimitTimeSpan:
|
|
61
|
+
rateLimitTimeSpan:
|
|
62
|
+
Number(process.env.RATE_LIMIT_TIME_SPAN) || rateLimitTimeSpan,
|
|
61
63
|
rateLimitAllowedCalls:
|
|
62
|
-
Number(process.env.
|
|
64
|
+
Number(process.env.RATE_LIMIT_ALLOWED_CALLS) || rateLimitAllowedCalls,
|
|
63
65
|
memoizeTime: Number(process.env.MEMOIZE_TIME) || memoizeTime,
|
|
64
66
|
cacheSize: Number(process.env.CACHE_SIZE) || cacheSize,
|
|
65
67
|
};
|
package/src/constants.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
export const http = "http://localhost";
|
|
2
|
+
export const port = 3000;
|
|
1
3
|
export const timeout = 20000;
|
|
2
4
|
export const rateLimitTimeSpan = 60000;
|
|
3
5
|
export const rateLimitAllowedCalls = 300;
|
|
4
6
|
export const memoizeTime = 1000 * 60 * 60;
|
|
5
7
|
export const cacheSize = 1000;
|
|
6
8
|
export const productionEnvironment = "production";
|
|
7
|
-
export const
|
|
9
|
+
export const swaggerPath = "/api-docs";
|
|
10
|
+
export const routerDir = "/routers";
|
|
11
|
+
export const routerExtension = ".router";
|
|
12
|
+
export const defaultRouter = "index";
|
package/src/main.ts
CHANGED
|
@@ -3,7 +3,16 @@ import fs from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import config from "./config";
|
|
5
5
|
import { ResponseError } from "./utilities/error-handling";
|
|
6
|
-
|
|
6
|
+
import { resolve } from "./utilities/container";
|
|
7
|
+
import { WinstonLoggerService } from "./utilities/logger/winston-logger.service";
|
|
8
|
+
import "dotenv/config";
|
|
9
|
+
import {
|
|
10
|
+
defaultRouter,
|
|
11
|
+
routerDir,
|
|
12
|
+
routerExtension,
|
|
13
|
+
swaggerPath,
|
|
14
|
+
} from "./constants";
|
|
15
|
+
//import cors from "cors";
|
|
7
16
|
|
|
8
17
|
/**
|
|
9
18
|
* Recursively loads Express routers from directory
|
|
@@ -29,17 +38,20 @@ function loadRouters(routerPath: string, basePath: string = "") {
|
|
|
29
38
|
|
|
30
39
|
// Only handle router files
|
|
31
40
|
if (
|
|
32
|
-
!entry.name.endsWith(
|
|
33
|
-
!entry.name.endsWith(
|
|
41
|
+
!entry.name.endsWith(`${routerExtension}.ts`) &&
|
|
42
|
+
!entry.name.endsWith(`${routerExtension}.js`)
|
|
34
43
|
) {
|
|
35
44
|
continue;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
// Build route path safely
|
|
39
48
|
const routePath = path
|
|
40
|
-
.join(
|
|
49
|
+
.join(
|
|
50
|
+
basePath,
|
|
51
|
+
entry.name.replace(new RegExp(`\\${routerExtension}\\.([tj]s)$`), "")
|
|
52
|
+
)
|
|
41
53
|
.replace(/\\/g, "/")
|
|
42
|
-
.replace(
|
|
54
|
+
.replace(new RegExp(`\\/?${defaultRouter}$`), "");
|
|
43
55
|
|
|
44
56
|
// Optional: skip if the target path would be empty (maps to /)
|
|
45
57
|
const mountPath = "/" + (routePath || "");
|
|
@@ -52,11 +64,12 @@ function loadRouters(routerPath: string, basePath: string = "") {
|
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
const app = express();
|
|
67
|
+
//app.use(cors());
|
|
55
68
|
app.use(express.json());
|
|
56
69
|
app.use(express.urlencoded({ extended: true }));
|
|
57
70
|
|
|
58
71
|
// Automatically import all routers from the /src/routers directory
|
|
59
|
-
const routersPath = path.join(__dirname,
|
|
72
|
+
const routersPath = path.join(__dirname, routerDir);
|
|
60
73
|
loadRouters(routersPath);
|
|
61
74
|
|
|
62
75
|
// Swagger setup
|
|
@@ -64,22 +77,42 @@ if (config.swagger) {
|
|
|
64
77
|
const swaggerJsDoc = require("swagger-jsdoc");
|
|
65
78
|
const swaggerUi = require("swagger-ui-express");
|
|
66
79
|
const swaggerDocs = swaggerJsDoc(config.swaggerOptions);
|
|
67
|
-
app.use(
|
|
80
|
+
app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDocs));
|
|
68
81
|
}
|
|
69
82
|
|
|
83
|
+
const logger = resolve(WinstonLoggerService);
|
|
84
|
+
|
|
70
85
|
// General error handler
|
|
71
86
|
app.use(
|
|
72
|
-
(
|
|
73
|
-
|
|
87
|
+
(error: ResponseError, req: Request, res: Response, next: NextFunction) => {
|
|
88
|
+
const status = error.status ?? 500;
|
|
89
|
+
const errorObject = {
|
|
90
|
+
status,
|
|
91
|
+
code: error.code,
|
|
92
|
+
input: error.input,
|
|
93
|
+
stack: error.stack,
|
|
94
|
+
cause: error.cause,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
res.status(status).json(error);
|
|
98
|
+
if (status < 500) {
|
|
99
|
+
logger.warn(error.message, "Invalid request", error.details, errorObject);
|
|
100
|
+
} else {
|
|
101
|
+
if (status == 401 || status == 403) {
|
|
102
|
+
logger.fatal(error.message, "Forbidden", error.details, errorObject);
|
|
103
|
+
} else {
|
|
104
|
+
logger.error(error.message, "Server error", error.details, errorObject);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
74
107
|
}
|
|
75
108
|
);
|
|
76
109
|
|
|
77
110
|
// Start the server
|
|
78
111
|
app.listen(config.port, () => {
|
|
79
|
-
console.log(`Server is running on http
|
|
112
|
+
console.log(`Server is running on ${config.http}:${config.port}`);
|
|
80
113
|
if (config.devMode) {
|
|
81
114
|
console.log(
|
|
82
|
-
`Swagger UI is available at http
|
|
115
|
+
`Swagger UI is available at ${config.http}:${config.port}${swaggerPath}`
|
|
83
116
|
);
|
|
84
117
|
}
|
|
85
118
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Router, Request, Response } from "express";
|
|
2
2
|
import asyncHandler from "express-async-handler";
|
|
3
3
|
import OrderController from "./order.controller";
|
|
4
|
+
import { resolve } from "@/utilities/container";
|
|
4
5
|
|
|
5
6
|
const router = Router();
|
|
6
|
-
const orderController =
|
|
7
|
+
const orderController = resolve(OrderController);
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @swagger
|
|
@@ -60,7 +61,7 @@ router.post(
|
|
|
60
61
|
asyncHandler(async (req: Request, res: Response) => {
|
|
61
62
|
const order = await orderController.validateOrderCreationDto(req.body);
|
|
62
63
|
await orderController.create(order);
|
|
63
|
-
res.status(201).send()
|
|
64
|
+
res.status(201).send();
|
|
64
65
|
})
|
|
65
66
|
);
|
|
66
67
|
|
|
@@ -10,17 +10,13 @@ import config from "@/config";
|
|
|
10
10
|
import { orders } from "@/db";
|
|
11
11
|
import { Order, ZodOrder } from "@/entities/order.entity";
|
|
12
12
|
import { MapAsyncCache } from "@/utilities/cache/memory-cache";
|
|
13
|
+
import { Injectable } from "@/utilities/container";
|
|
13
14
|
|
|
14
15
|
function exceedHandler() {
|
|
15
16
|
const message = "Too much call in allowed window";
|
|
16
17
|
throw new ResponseError(message, 429);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function orderNotFoundHandler(e: ResponseError) {
|
|
20
|
-
const message = "Order not found.";
|
|
21
|
-
throw new ResponseError(message, 404, e.message);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
20
|
function invalidInputHandler(e: ResponseError) {
|
|
25
21
|
const message = "Invalid input";
|
|
26
22
|
throw new ResponseError(message, 400, e.message);
|
|
@@ -29,6 +25,7 @@ function invalidInputHandler(e: ResponseError) {
|
|
|
29
25
|
const ordersCache = new MapAsyncCache<OrderDto[]>(config.cacheSize);
|
|
30
26
|
const orderCache = new MapAsyncCache<OrderDto>(config.cacheSize);
|
|
31
27
|
|
|
28
|
+
@Injectable()
|
|
32
29
|
/**
|
|
33
30
|
* Controller for handling order-related operations
|
|
34
31
|
* @class OrderController
|
|
@@ -38,7 +35,7 @@ export default class OrderController {
|
|
|
38
35
|
// constructor(private readonly orderService: OrderService) { }
|
|
39
36
|
|
|
40
37
|
@onError({
|
|
41
|
-
func:
|
|
38
|
+
func: invalidInputHandler,
|
|
42
39
|
})
|
|
43
40
|
/**
|
|
44
41
|
* Validates a string ID and converts it to a number.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Router, Request, Response } from "express";
|
|
2
2
|
import asyncHandler from "express-async-handler";
|
|
3
3
|
import ProductController from "./product.controller";
|
|
4
|
+
import { resolve } from "@/utilities/container";
|
|
4
5
|
|
|
5
6
|
const router = Router();
|
|
6
|
-
const productController =
|
|
7
|
+
const productController = resolve(ProductController);
|
|
7
8
|
|
|
8
9
|
// CRUD Routes
|
|
9
10
|
|
|
@@ -18,17 +18,13 @@ import config from "@/config";
|
|
|
18
18
|
import { ResponseError } from "@/utilities/error-handling";
|
|
19
19
|
import { products } from "@/db";
|
|
20
20
|
import { Product, ZodProduct } from "@/entities/product.entity";
|
|
21
|
+
import { Injectable } from "@/utilities/container";
|
|
21
22
|
|
|
22
23
|
function exceedHandler() {
|
|
23
24
|
const message = "Too much call in allowed window";
|
|
24
25
|
throw new ResponseError(message, 429);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function productNotFoundHandler(e: ResponseError) {
|
|
28
|
-
const message = "Product not found.";
|
|
29
|
-
throw new ResponseError(message, 404, e.message);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
function invalidInputHandler(e: ResponseError) {
|
|
33
29
|
const message = "Invalid input";
|
|
34
30
|
throw new ResponseError(message, 400, e.message);
|
|
@@ -37,6 +33,7 @@ function invalidInputHandler(e: ResponseError) {
|
|
|
37
33
|
const productsCache = new MapAsyncCache<ProductDto[]>(config.cacheSize);
|
|
38
34
|
const productCache = new MapAsyncCache<ProductDto>(config.cacheSize);
|
|
39
35
|
|
|
36
|
+
@Injectable()
|
|
40
37
|
/**
|
|
41
38
|
* Controller for handling product-related operations
|
|
42
39
|
* @class ProductController
|
|
@@ -46,7 +43,7 @@ export default class ProductController {
|
|
|
46
43
|
// constructor(private readonly productService: ProductService) { }
|
|
47
44
|
|
|
48
45
|
@onError({
|
|
49
|
-
func:
|
|
46
|
+
func: invalidInputHandler,
|
|
50
47
|
})
|
|
51
48
|
/**
|
|
52
49
|
* Validates a string ID and converts it to a number.
|
|
@@ -138,9 +135,6 @@ export default class ProductController {
|
|
|
138
135
|
keyResolver: (id: number) => id.toString(),
|
|
139
136
|
expirationTimeMs: config.memoizeTime,
|
|
140
137
|
})
|
|
141
|
-
@onError({
|
|
142
|
-
func: productNotFoundHandler,
|
|
143
|
-
})
|
|
144
138
|
@rateLimit({
|
|
145
139
|
timeSpanMs: config.rateLimitTimeSpan,
|
|
146
140
|
allowedCalls: config.rateLimitAllowedCalls,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Router, Request, Response } from "express";
|
|
2
2
|
import asyncHandler from "express-async-handler";
|
|
3
3
|
import UserController from "./user.controller";
|
|
4
|
+
import { resolve } from "@/utilities/container";
|
|
4
5
|
|
|
5
6
|
const router = Router();
|
|
6
|
-
const userController =
|
|
7
|
+
const userController = resolve(UserController);
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @swagger
|
|
@@ -11,17 +11,13 @@ import config from "@/config";
|
|
|
11
11
|
import { users } from "@/db";
|
|
12
12
|
import { User } from "@/entities/user.entity";
|
|
13
13
|
import { MapAsyncCache } from "@/utilities/cache/memory-cache";
|
|
14
|
+
import { Injectable } from "@/utilities/container";
|
|
14
15
|
|
|
15
16
|
function exceedHandler() {
|
|
16
17
|
const message = "Too much call in allowed window";
|
|
17
18
|
throw new ResponseError(message, 429);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function userNotFoundHandler(e: ResponseError) {
|
|
21
|
-
const message = "User not found.";
|
|
22
|
-
throw new ResponseError(message, 404, e?.message);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
function invalidInputHandler(e: ResponseError) {
|
|
26
22
|
const message = "Invalid input";
|
|
27
23
|
throw new ResponseError(message, 400, e?.message);
|
|
@@ -30,6 +26,7 @@ function invalidInputHandler(e: ResponseError) {
|
|
|
30
26
|
const usersCache = new MapAsyncCache<UserDto[]>(config.cacheSize);
|
|
31
27
|
const userCache = new MapAsyncCache<UserDto>(config.cacheSize);
|
|
32
28
|
|
|
29
|
+
@Injectable()
|
|
33
30
|
/**
|
|
34
31
|
* Controller for handling user-related operations
|
|
35
32
|
* @class UserController
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
|
|
3
|
+
export type Constructor<T = any> = new (...args: any[]) => T;
|
|
4
|
+
|
|
5
|
+
interface Provider<T = any> {
|
|
6
|
+
/** The concrete class to instantiate (may be the same as token or a different implementation). */
|
|
7
|
+
useClass: Constructor<T>;
|
|
8
|
+
/** Cached singleton instance, if one has been created. */
|
|
9
|
+
instance?: T;
|
|
10
|
+
/** Whether to treat the provider as a singleton. Defaults to true. */
|
|
11
|
+
singleton?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const InjectableRegistry: Array<Constructor<any>> = [];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A tiny dependency injection container.
|
|
18
|
+
*
|
|
19
|
+
* Features:
|
|
20
|
+
* - Register tokens (classes) with optional options.
|
|
21
|
+
* - Resolve instances with automatic dependency resolution via design:paramtypes metadata.
|
|
22
|
+
* - Simple singleton lifetime by default (one instance per token).
|
|
23
|
+
*
|
|
24
|
+
* Notes:
|
|
25
|
+
* - Requires reflect-metadata to be loaded and "emitDecoratorMetadata"/"experimentalDecorators"
|
|
26
|
+
* enabled in tsconfig for design:paramtypes to exist.
|
|
27
|
+
*/
|
|
28
|
+
class Container {
|
|
29
|
+
private registrations = new Map<Constructor, Provider>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a class as a provider for the given token.
|
|
33
|
+
*
|
|
34
|
+
* If no options.useClass is provided, the token itself is used as the concrete class.
|
|
35
|
+
* If options.singleton is not provided, the provider defaults to singleton = true.
|
|
36
|
+
*
|
|
37
|
+
* @param token - The token (class constructor) that represents the dependency.
|
|
38
|
+
* @param options - Optional provider options.
|
|
39
|
+
* - useClass?: The concrete class to instantiate for this token.
|
|
40
|
+
* - singleton?: Whether to reuse a single instance (default: true).
|
|
41
|
+
*/
|
|
42
|
+
register<T>(
|
|
43
|
+
token: Constructor<T>,
|
|
44
|
+
options?: { useClass?: Constructor<T>; singleton?: boolean }
|
|
45
|
+
): void {
|
|
46
|
+
const useClass = options?.useClass ?? token;
|
|
47
|
+
const singleton = options?.singleton ?? true;
|
|
48
|
+
this.registrations.set(token, { useClass, singleton });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve an instance for the given token.
|
|
53
|
+
*
|
|
54
|
+
* Behavior:
|
|
55
|
+
* - If the token is not registered, attempt to instantiate directly (without DI).
|
|
56
|
+
* - If a singleton instance already exists for the token, return it.
|
|
57
|
+
* - Otherwise, resolve the concrete class (provider.useClass), construct it
|
|
58
|
+
* by recursively resolving its constructor parameter types, and cache
|
|
59
|
+
* the instance if singleton is true.
|
|
60
|
+
*
|
|
61
|
+
* @param token - The token (class constructor) to resolve.
|
|
62
|
+
* @returns An instance of the requested type T.
|
|
63
|
+
*/
|
|
64
|
+
resolve<T>(token: Constructor<T>): T {
|
|
65
|
+
const provider = this.registrations.get(token);
|
|
66
|
+
|
|
67
|
+
if (!provider) {
|
|
68
|
+
// If not registered, try to instantiate directly (without DI)
|
|
69
|
+
return this.construct(token);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (provider.instance) {
|
|
73
|
+
return provider.instance;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Resolve dependencies for the concrete class
|
|
77
|
+
const target = provider.useClass;
|
|
78
|
+
const instance = this.construct(target);
|
|
79
|
+
|
|
80
|
+
if (provider.singleton) {
|
|
81
|
+
provider.instance = instance;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return instance;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Internal helper to instantiate a class, resolving its constructor dependencies
|
|
89
|
+
* via design:paramtypes metadata.
|
|
90
|
+
*
|
|
91
|
+
* @param constructorFunction - The constructor function of the class to instantiate.
|
|
92
|
+
* @returns A new instance of type T with dependencies injected.
|
|
93
|
+
*/
|
|
94
|
+
private construct<T>(constructorFunction: Constructor<T>): T {
|
|
95
|
+
const paramTypes: any[] =
|
|
96
|
+
Reflect.getMetadata("design:paramtypes", constructorFunction) || [];
|
|
97
|
+
const args = paramTypes.map((paramType) => this.resolve(paramType));
|
|
98
|
+
return new constructorFunction(...args);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Bootstraps a container by wiring up all registered injectables.
|
|
103
|
+
*
|
|
104
|
+
* This helper walks the InjectableRegistry and registers each class with the container,
|
|
105
|
+
* honoring per-class options (like singleton) that may have been stored as metadata.
|
|
106
|
+
*
|
|
107
|
+
* Why this exists:
|
|
108
|
+
* - Keeps your registration centralized and automatic, so you don't have to call
|
|
109
|
+
* container.register(...) repeatedly for every service.
|
|
110
|
+
*
|
|
111
|
+
* Usage reminder:
|
|
112
|
+
* - Ensure Reflect Metadata is loaded before calling this, so that the di:injectable
|
|
113
|
+
* metadata is actually readable.
|
|
114
|
+
*/
|
|
115
|
+
function bootstrapContainer(container: Container) {
|
|
116
|
+
for (const constructorFunction of InjectableRegistry) {
|
|
117
|
+
// Read per-class options if you added metadata
|
|
118
|
+
const meta = Reflect.getMetadata("di:injectable", constructorFunction) as
|
|
119
|
+
| { singleton?: boolean }
|
|
120
|
+
| undefined;
|
|
121
|
+
const singleton = meta?.singleton ?? false;
|
|
122
|
+
container.register(constructorFunction, { singleton });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Marks a class for automatic DI registration.
|
|
128
|
+
* Options:
|
|
129
|
+
* - singleton: whether to register as a singleton (default true)
|
|
130
|
+
* - token: optional explicit token to register for (defaults to class constructor)
|
|
131
|
+
*/
|
|
132
|
+
export function Injectable(options?: {
|
|
133
|
+
singleton?: boolean;
|
|
134
|
+
token?: any;
|
|
135
|
+
}): ClassDecorator {
|
|
136
|
+
return (target: any) => {
|
|
137
|
+
// Push into registry for later registration
|
|
138
|
+
InjectableRegistry.push(target);
|
|
139
|
+
// You could also attach metadata if you want to customize per-class
|
|
140
|
+
Reflect.defineMetadata("di:injectable", options ?? {}, target);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A single, shared DI container for the app. */
|
|
145
|
+
const container = new Container();
|
|
146
|
+
bootstrapContainer(container);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a service wherever needed.
|
|
150
|
+
*
|
|
151
|
+
* Example
|
|
152
|
+
*
|
|
153
|
+
* import { resolve } from "@/utilities/container";
|
|
154
|
+
* const loggerService = resolve(LoggerService);
|
|
155
|
+
* loggerService.log(...);
|
|
156
|
+
*/
|
|
157
|
+
export function resolve<T>(token: Constructor<T>): T {
|
|
158
|
+
return container.resolve(token);
|
|
159
|
+
}
|
|
@@ -154,3 +154,39 @@ export function isSuccessful<T>(result: ReturnInfo<T>): boolean {
|
|
|
154
154
|
export function hasError<T>(result: ReturnInfo<T>): boolean {
|
|
155
155
|
return result[1] !== null;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Indicates whether a ReturnInfo value represents an error.
|
|
160
|
+
*
|
|
161
|
+
* This is the logical negation of isSuccess for a given ReturnInfo.
|
|
162
|
+
*
|
|
163
|
+
* @template T
|
|
164
|
+
* @param result - The ReturnInfo tuple [value | null, error | null]
|
|
165
|
+
* @returns true if an error is present (i.e., error is not null); false otherwise
|
|
166
|
+
*/
|
|
167
|
+
export function then<T>(
|
|
168
|
+
result: ReturnInfo<T>,
|
|
169
|
+
callback: (object: T) => void
|
|
170
|
+
): void {
|
|
171
|
+
if (isSuccessful(result)) {
|
|
172
|
+
callback(successfulResult(result)!);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Indicates whether a ReturnInfo value represents an error.
|
|
178
|
+
*
|
|
179
|
+
* This is the logical negation of isSuccess for a given ReturnInfo.
|
|
180
|
+
*
|
|
181
|
+
* @template T
|
|
182
|
+
* @param result - The ReturnInfo tuple [value | null, error | null]
|
|
183
|
+
* @returns true if an error is present (i.e., error is not null); false otherwise
|
|
184
|
+
*/
|
|
185
|
+
export function catchError<T>(
|
|
186
|
+
result: ReturnInfo<T>,
|
|
187
|
+
callback: (error: ResponseError) => void
|
|
188
|
+
): void {
|
|
189
|
+
if (hasError(result)) {
|
|
190
|
+
callback(error(result)!);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/logging/BaseLogger.ts
|
|
2
|
+
|
|
3
|
+
export type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace";
|
|
4
|
+
|
|
5
|
+
export interface Meta {
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BaseLogger {
|
|
10
|
+
log(level: LogLevel, message: string, meta?: Meta): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as winston from "winston";
|
|
2
|
+
import { TransformableInfo } from "logform";
|
|
3
|
+
import "winston-daily-rotate-file"; // Import the DailyRotateFile transport to extend winston transports
|
|
4
|
+
|
|
5
|
+
// Console format: colorized and pretty printed
|
|
6
|
+
// Custom formatting to include timestamp, context, level, and message
|
|
7
|
+
//
|
|
8
|
+
// Example: "2023-10-05T12:00:00.000Z [MyContext] info: This is a log message"
|
|
9
|
+
const consoleFormat = winston.format.combine(
|
|
10
|
+
winston.format.colorize({ all: true }),
|
|
11
|
+
winston.format.timestamp(),
|
|
12
|
+
winston.format.printf(
|
|
13
|
+
({
|
|
14
|
+
timestamp,
|
|
15
|
+
level,
|
|
16
|
+
message,
|
|
17
|
+
context,
|
|
18
|
+
...meta
|
|
19
|
+
}: TransformableInfo): string => {
|
|
20
|
+
const contextStr = context ? `[${context as string}]` : "[main]";
|
|
21
|
+
|
|
22
|
+
const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : "";
|
|
23
|
+
|
|
24
|
+
return `${timestamp as string} ${contextStr} ${level}: ${
|
|
25
|
+
message as string
|
|
26
|
+
} ${metaStr}`;
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// File format: JSON with timestamp
|
|
32
|
+
// This format is suitable for structured logging and easier parsing by
|
|
33
|
+
// log management systems
|
|
34
|
+
//
|
|
35
|
+
// Example: {"timestamp":"2023-10-05T12:00:00.000Z", "level":"info",
|
|
36
|
+
// "message":"This is a log message", "context":"MyContext"}
|
|
37
|
+
const fileFormat = winston.format.combine(
|
|
38
|
+
winston.format.timestamp(),
|
|
39
|
+
winston.format.json()
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Console transport for logging to the console with colorized output
|
|
43
|
+
// and custom formatting that includes timestamp, context, level, and message.
|
|
44
|
+
const consoleTransport = new winston.transports.Console({
|
|
45
|
+
format: consoleFormat,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// File transport for logging to files with daily rotation.
|
|
49
|
+
// It logs everything at the trace level and formats logs as JSON.
|
|
50
|
+
// The logs are stored in the "logs" directory with a date pattern
|
|
51
|
+
// in the filename.
|
|
52
|
+
// The logs are zipped and rotated daily, keeping logs for 14 days.
|
|
53
|
+
const fileTransport = new winston.transports.DailyRotateFile({
|
|
54
|
+
dirname: "logs",
|
|
55
|
+
filename: "app-%DATE%.log",
|
|
56
|
+
datePattern: "YYYY-MM-DD",
|
|
57
|
+
zippedArchive: true,
|
|
58
|
+
maxSize: "20m",
|
|
59
|
+
maxFiles: "14d",
|
|
60
|
+
level: "trace", // log everything to file / external service
|
|
61
|
+
format: fileFormat,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Add custom colors for log levels to enhance console output readability
|
|
65
|
+
// This allows log levels to be displayed in different colors in the console.
|
|
66
|
+
// Define a custom logger with explicit levels
|
|
67
|
+
const customLevels = {
|
|
68
|
+
levels: {
|
|
69
|
+
fatal: 0,
|
|
70
|
+
error: 1,
|
|
71
|
+
warn: 2,
|
|
72
|
+
info: 3,
|
|
73
|
+
debug: 4,
|
|
74
|
+
trace: 5,
|
|
75
|
+
},
|
|
76
|
+
colors: {
|
|
77
|
+
fatal: "red bold",
|
|
78
|
+
error: "red",
|
|
79
|
+
warn: "yellow",
|
|
80
|
+
info: "green",
|
|
81
|
+
debug: "blue",
|
|
82
|
+
trace: "grey",
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
winston.addColors(customLevels.colors);
|
|
87
|
+
|
|
88
|
+
export const logger = winston.createLogger({
|
|
89
|
+
levels: customLevels.levels,
|
|
90
|
+
level: "trace",
|
|
91
|
+
transports: [consoleTransport, fileTransport],
|
|
92
|
+
exitOnError: false,
|
|
93
|
+
exceptionHandlers: [consoleTransport, fileTransport],
|
|
94
|
+
rejectionHandlers: [consoleTransport, fileTransport],
|
|
95
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { BaseLogger, LogLevel, Meta } from "./base-logger.interface";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LoggerService is a custom logger service that integrates Winston for advanced logging capabilities.
|
|
5
|
+
*/
|
|
6
|
+
export class LoggerService {
|
|
7
|
+
/**
|
|
8
|
+
* Constructs a new LoggerService instance.
|
|
9
|
+
*
|
|
10
|
+
* @param logger - Injected logger instance
|
|
11
|
+
*/
|
|
12
|
+
public constructor(private logger: BaseLogger) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Logs an informational message with optional context and metadata.
|
|
16
|
+
* This method combines the provided metadata with the context and log level,
|
|
17
|
+
* and sends the log entry to the appropriate transports.
|
|
18
|
+
*
|
|
19
|
+
* @param message - The message to log
|
|
20
|
+
* @param context - Optional context information (e.g., module or method name)
|
|
21
|
+
* @param meta - Additional metadata to include in the log entry
|
|
22
|
+
*/
|
|
23
|
+
public log(level: LogLevel, message: string, meta: Meta = {}): void {
|
|
24
|
+
meta.timestamp = new Date().toISOString();
|
|
25
|
+
this.logger.log(level, message, meta);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Logs an informational message with optional context and metadata.
|
|
30
|
+
* This method combines the provided metadata with the context and log level,
|
|
31
|
+
* and sends the log entry to the appropriate transports.
|
|
32
|
+
*
|
|
33
|
+
* @param message - The message to log
|
|
34
|
+
* @param context - Optional context information (e.g., module or method name)
|
|
35
|
+
* @param meta - Additional metadata to include in the log entry
|
|
36
|
+
*/
|
|
37
|
+
public info(message: string, context?: string, meta: Meta = {}): void {
|
|
38
|
+
this.log("info", message, { context, ...meta });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Logs a fatal error message with optional trace, context, and metadata.
|
|
43
|
+
* This method combines the provided metadata with the trace and context,
|
|
44
|
+
* and sends the log entry to the appropriate transports.
|
|
45
|
+
*
|
|
46
|
+
* @param message - The fatal error message to log
|
|
47
|
+
* @param trace - Optional trace information for the error
|
|
48
|
+
* @param context - Optional context information (e.g., module or method name)
|
|
49
|
+
* @param meta - Additional metadata to include in the log entry
|
|
50
|
+
*/
|
|
51
|
+
public fatal(
|
|
52
|
+
message: string,
|
|
53
|
+
context?: string,
|
|
54
|
+
trace?: string,
|
|
55
|
+
meta: Meta = {}
|
|
56
|
+
): void {
|
|
57
|
+
this.log("fatal", message, { context, trace, ...meta });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Logs an error message with optional trace, context, and metadata.
|
|
62
|
+
* This method combines the provided metadata with the trace and context,
|
|
63
|
+
* and sends the log entry to the appropriate transports.
|
|
64
|
+
*
|
|
65
|
+
* @param message - The error message to log
|
|
66
|
+
* @param trace - Optional trace information for the error
|
|
67
|
+
* @param context - Optional context information (e.g., module or method name)
|
|
68
|
+
* @param meta - Additional metadata to include in the log entry
|
|
69
|
+
*/
|
|
70
|
+
public error(
|
|
71
|
+
message: string,
|
|
72
|
+
context?: string,
|
|
73
|
+
trace?: string,
|
|
74
|
+
meta: Meta = {}
|
|
75
|
+
): void {
|
|
76
|
+
this.log("error", message, { context, trace, ...meta });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Logs a warning message with optional context and metadata.
|
|
81
|
+
* This method combines the provided metadata with the context and log level,
|
|
82
|
+
* and sends the log entry to the appropriate transports.
|
|
83
|
+
*
|
|
84
|
+
* @param message - The warning message to log
|
|
85
|
+
* @param context - Optional context information (e.g., module or method name)
|
|
86
|
+
* @param meta - Additional metadata to include in the log entry
|
|
87
|
+
*/
|
|
88
|
+
public warn(
|
|
89
|
+
message: string,
|
|
90
|
+
context?: string,
|
|
91
|
+
trace?: string,
|
|
92
|
+
...meta: unknown[]
|
|
93
|
+
): void {
|
|
94
|
+
this.log("warn", message, { context, trace, ...meta });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Logs a debug message with optional context and metadata.
|
|
99
|
+
* This method combines the provided metadata with the context and log level,
|
|
100
|
+
* and sends the log entry to the appropriate transports.
|
|
101
|
+
*
|
|
102
|
+
* @param message - The debug message to log
|
|
103
|
+
* @param context - Optional context information (e.g., module or method name)
|
|
104
|
+
* @param meta - Additional metadata to include in the log entry
|
|
105
|
+
*/
|
|
106
|
+
public debug(message: string, context?: string, ...meta: unknown[]): void {
|
|
107
|
+
this.log("debug", message, { context, ...meta });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Logs a trace message with optional context and metadata.
|
|
112
|
+
* This method combines the provided metadata with the context and log level,
|
|
113
|
+
* and sends the log entry to the appropriate transports.
|
|
114
|
+
*
|
|
115
|
+
* @param message - The trace message to log
|
|
116
|
+
* @param context - Optional context information (e.g., module or method name)
|
|
117
|
+
* @param meta - Additional metadata to include in the log entry
|
|
118
|
+
*/
|
|
119
|
+
public trace(message: string, context?: string, ...meta: unknown[]): void {
|
|
120
|
+
this.log("trace", message, { context, ...meta });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { logger } from "./logger.config";
|
|
2
|
+
import { Injectable } from "../container";
|
|
3
|
+
import { LoggerService } from "./logger.service";
|
|
4
|
+
|
|
5
|
+
@Injectable({ singleton: true })
|
|
6
|
+
/**
|
|
7
|
+
* WinstonLoggerService is a custom logger service that integrates Winston for advanced logging capabilities.
|
|
8
|
+
*/
|
|
9
|
+
export class WinstonLoggerService extends LoggerService {
|
|
10
|
+
/**
|
|
11
|
+
* Constructs a new LoggerService instance.
|
|
12
|
+
*/
|
|
13
|
+
public constructor() {
|
|
14
|
+
super(logger);
|
|
15
|
+
}
|
|
16
|
+
}
|
package/.vscode/settings.json
DELETED
package/src/app.ts
DELETED
package/src/utilities/router.ts
DELETED
|
File without changes
|