codeweaver 1.0.15 → 1.1.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/README.md +252 -40
- package/package.json +17 -13
- package/src/app.ts +21 -3
- package/src/config.ts +33 -0
- package/src/packages/ts-zod-decorators/index.ts +3 -0
- package/src/packages/ts-zod-decorators/validate.decorator.ts +20 -0
- package/src/packages/ts-zod-decorators/validator.class.ts +72 -0
- package/src/packages/ts-zod-decorators/zod-input.decorator.ts +12 -0
- package/src/packages/ts-zod-decorators/zod-output.decorator.ts +11 -0
- package/src/routers/index.ts +2 -2
- package/src/routers/orders/dto/order.dto.ts +40 -0
- package/src/routers/orders/index.ts +131 -10
- package/src/routers/orders/order.controller.ts +191 -0
- package/src/routers/products/dto/product.dto.ts +36 -0
- package/src/routers/products/index.ts +104 -64
- package/src/routers/products/product.controller.ts +237 -0
- package/src/routers/users/dto/user.dto.ts +14 -2
- package/src/routers/users/index.ts +54 -36
- package/src/routers/users/user.controller.ts +144 -16
- package/src/types.ts +16 -0
- package/src/utilities.ts +47 -0
- package/tsconfig.json +9 -3
- package/tsconfig.paths.json +10 -0
package/README.md
CHANGED
|
@@ -2,25 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
**Codeweaver** is a lightweight
|
|
5
|
+
**Codeweaver** is a lightweight microframework created with `Express`, `TypeScript`, and `Zod` (v4), seamlessly integrated with `Swagger` for comprehensive API documentation. Its modular architecture for routers promotes scalability and organized development, making it easy to expand and maintain.
|
|
6
6
|
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **Modular Router Structure**: Each router is automatically imported and mounted, providing clean separation of endpoints and logic.
|
|
10
|
-
- **Express Framework**: A lightweight web application framework for building web applications in Node.js.
|
|
11
|
-
- **TypeScript**: Provides strong typing for better development experience and less runtime errors.
|
|
12
|
-
- **Swagger Integration**: Automatically generates interactive API documentation, making it easy for developers and consumers to understand the available endpoints.
|
|
13
|
-
- **Async Handlers**: Supports async/await syntax for writing cleaner and more maintainable asynchronous code without deeply nested callbacks.
|
|
14
|
-
|
|
15
|
-
## Technologies Used
|
|
7
|
+
## Features and Technologies Used
|
|
16
8
|
|
|
17
9
|
- **Node.js**
|
|
18
|
-
- **Express
|
|
19
|
-
- **TypeScript
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
10
|
+
- **Express**: A lightweight web framework for building server-side applications in Node.js.
|
|
11
|
+
- **TypeScript**: Adds strong typing for enhanced development experience and reduced runtime errors.
|
|
12
|
+
- **Modular Router Structure**: Automates importing and mounting routers, ensuring a clean separation of endpoints and logic for easier scalability.
|
|
13
|
+
- **Swagger Integration**: Automatically generates interactive API documentation, facilitating easier understanding of available endpoints for developers and consumers.
|
|
14
|
+
- **Async Handlers**: Utilizes async/await syntax for cleaner, more maintainable asynchronous code without callback nesting.
|
|
15
|
+
- **Zod**: Implements schema validation for input data.
|
|
16
|
+
- **ts-zod-decorators**: Enables validation using Zod schemas through decorators.
|
|
17
|
+
- **utils-decorators**: Provides middleware utilities such as throttling and error handling for a more robust application.
|
|
24
18
|
|
|
25
19
|
## Installation
|
|
26
20
|
|
|
@@ -29,6 +23,7 @@ To get started with the project, follow these steps:
|
|
|
29
23
|
1. **Clone the repository**:
|
|
30
24
|
|
|
31
25
|
```bash
|
|
26
|
+
npm install -g codeweaver
|
|
32
27
|
npx create-codeweaver-app my-app
|
|
33
28
|
cd my-app
|
|
34
29
|
```
|
|
@@ -51,6 +46,7 @@ To get started with the project, follow these steps:
|
|
|
51
46
|
|
|
52
47
|
```bash
|
|
53
48
|
npm run build
|
|
49
|
+
npm run serve
|
|
54
50
|
```
|
|
55
51
|
|
|
56
52
|
## Sample Project Structure
|
|
@@ -84,30 +80,106 @@ To get started with the project, follow these steps:
|
|
|
84
80
|
|
|
85
81
|
Each router file in the `/routers` directory is organized to handle related endpoints. The `app.ts` file automatically imports all routers and mounts them to the main Express application, making it simple to add new routes without modifying central files.
|
|
86
82
|
|
|
87
|
-
Files that end with `.controller`, `.service`, `.spec`, `.dto`, `.middleware`, `.error`, or `.decorator`, as well as those that start with `_` or `@`, are excluded from the router list and can be utilized for various other purposes within the application.
|
|
83
|
+
Files that end with `.controller`, `.service`, `.spec`, `.dto`, `.middleware`, `.error`, `.class`, or `.decorator`, as well as those that start with `_` or `@`, are excluded from the router list and can be utilized for various other purposes within the application.
|
|
88
84
|
|
|
89
85
|
Example of a basic router:
|
|
90
86
|
|
|
91
87
|
```typescript
|
|
92
88
|
import { Router, Request, Response } from "express";
|
|
93
89
|
import asyncHandler from "express-async-handler";
|
|
90
|
+
import UserController from "./user.controller";
|
|
91
|
+
import { sendError } from "@src/utilities";
|
|
94
92
|
|
|
95
93
|
const router = Router();
|
|
94
|
+
const userController = new UserController();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @swagger
|
|
98
|
+
* /users:
|
|
99
|
+
* post:
|
|
100
|
+
* summary: Create a user
|
|
101
|
+
* description: Create a new user.
|
|
102
|
+
* consumes:
|
|
103
|
+
* - application/json
|
|
104
|
+
* produces:
|
|
105
|
+
* - application/json
|
|
106
|
+
* parameters:
|
|
107
|
+
* - in: body
|
|
108
|
+
* name: user
|
|
109
|
+
* required: true
|
|
110
|
+
* schema:
|
|
111
|
+
* type: object
|
|
112
|
+
* required:
|
|
113
|
+
* - username
|
|
114
|
+
* - email
|
|
115
|
+
* - password
|
|
116
|
+
* properties:
|
|
117
|
+
* username:
|
|
118
|
+
* type: string
|
|
119
|
+
* minLength: 3
|
|
120
|
+
* example: JessicaSmith
|
|
121
|
+
* email:
|
|
122
|
+
* type: string
|
|
123
|
+
* format: email
|
|
124
|
+
* example: user@example.com
|
|
125
|
+
* password:
|
|
126
|
+
* type: string
|
|
127
|
+
* minLength: 6
|
|
128
|
+
* example: securePassword123
|
|
129
|
+
* responses:
|
|
130
|
+
* 201:
|
|
131
|
+
* description: User created
|
|
132
|
+
*/
|
|
133
|
+
router.post(
|
|
134
|
+
"/",
|
|
135
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
136
|
+
const user = await userController.create(req.body);
|
|
137
|
+
res.status(201).json(user);
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @swagger
|
|
143
|
+
* /users/{id}:
|
|
144
|
+
* get:
|
|
145
|
+
* summary: Get a user by ID
|
|
146
|
+
* parameters:
|
|
147
|
+
* - name: id
|
|
148
|
+
* in: path
|
|
149
|
+
* required: true
|
|
150
|
+
* description: The ID of the product
|
|
151
|
+
* schema:
|
|
152
|
+
* type: integer
|
|
153
|
+
* responses:
|
|
154
|
+
* 200:
|
|
155
|
+
* description: A user object
|
|
156
|
+
* 404:
|
|
157
|
+
* description: user not found
|
|
158
|
+
*/
|
|
159
|
+
router.get(
|
|
160
|
+
"/:id",
|
|
161
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
162
|
+
const user = await userController.get(req.params.id);
|
|
163
|
+
|
|
164
|
+
if ("id" in user == false) sendError(res, user);
|
|
165
|
+
else res.json(user);
|
|
166
|
+
})
|
|
167
|
+
);
|
|
96
168
|
|
|
97
169
|
/**
|
|
98
170
|
* @swagger
|
|
99
|
-
*
|
|
171
|
+
* /users:
|
|
100
172
|
* get:
|
|
101
|
-
* summary: Get
|
|
102
|
-
* description: Returns
|
|
173
|
+
* summary: Get users
|
|
174
|
+
* description: Returns a list of user objects.
|
|
103
175
|
* responses:
|
|
104
176
|
* 200:
|
|
105
|
-
* description:
|
|
177
|
+
* description: A list of user objects
|
|
106
178
|
*/
|
|
107
179
|
router.get(
|
|
108
180
|
"/",
|
|
109
181
|
asyncHandler(async (req: Request, res: Response) => {
|
|
110
|
-
res.
|
|
182
|
+
res.json(await userController.getAll());
|
|
111
183
|
})
|
|
112
184
|
);
|
|
113
185
|
|
|
@@ -131,13 +203,25 @@ Here’s a brief breakdown of key components used in the `UserController`:
|
|
|
131
203
|
```typescript
|
|
132
204
|
import { z } from "zod";
|
|
133
205
|
|
|
134
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Zod schema for User entity
|
|
208
|
+
* @typedef {Object} ZodUser
|
|
209
|
+
* @property {number} id - Unique identifier (min 1)
|
|
210
|
+
* @property {string} username - Username (min 3 chars)
|
|
211
|
+
* @property {string} email - Valid email format
|
|
212
|
+
* @property {string} password - Password (min 6 chars)
|
|
213
|
+
*/
|
|
214
|
+
export const ZodUser = z.object({
|
|
215
|
+
id: z.number().min(1).int(),
|
|
135
216
|
username: z.string().min(3),
|
|
136
217
|
email: z.string().email(),
|
|
137
218
|
password: z.string().min(6),
|
|
138
219
|
});
|
|
139
220
|
|
|
140
|
-
export
|
|
221
|
+
export const ZodUserCreationDto = ZodUser.omit({ id: true });
|
|
222
|
+
|
|
223
|
+
export type User = z.infer<typeof ZodUser>;
|
|
224
|
+
export type UserCreationDto = z.infer<typeof ZodUserCreationDto>;
|
|
141
225
|
```
|
|
142
226
|
|
|
143
227
|
- **Throttling and Rate Limiting**: The `@rateLimit` decorator is applied to safeguard the application's endpoints from abuse by limiting how frequently a particular method can be invoked.
|
|
@@ -149,37 +233,165 @@ By using a well-organized controller structure, this project makes it easier to
|
|
|
149
233
|
Here is a quick reference to the UserController in practice:
|
|
150
234
|
|
|
151
235
|
```typescript
|
|
152
|
-
import {
|
|
153
|
-
import {
|
|
154
|
-
import {
|
|
236
|
+
import { User, ZodUserCreationDto, UserCreationDto } from "./dto/user.dto";
|
|
237
|
+
import { onError, rateLimit, timeout } from "utils-decorators";
|
|
238
|
+
import { Validate, ZodInput } from "@pkg/ts-zod-decorators";
|
|
239
|
+
import { ResponseError } from "@src/types";
|
|
240
|
+
import { tryParseId } from "@src/utilities";
|
|
241
|
+
|
|
242
|
+
// Array to store users (as a mock database)
|
|
243
|
+
const users = [
|
|
244
|
+
{
|
|
245
|
+
id: 1,
|
|
246
|
+
username: "johndoe",
|
|
247
|
+
email: "johndoe@gmail.com",
|
|
248
|
+
password: "S3cur3P@ssw0rd",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: 2,
|
|
252
|
+
username: "janesmith",
|
|
253
|
+
email: "janesmith@yahoo.com",
|
|
254
|
+
password: "P@ssw0rd2024",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: 3,
|
|
258
|
+
username: "michael89",
|
|
259
|
+
email: "michael89@hotmail.com",
|
|
260
|
+
password: "M1chael!2024",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: 4,
|
|
264
|
+
username: "lisa.wong",
|
|
265
|
+
email: "lisa.wong@example.com",
|
|
266
|
+
password: "L1saW0ng!2024",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: 5,
|
|
270
|
+
username: "alex_k",
|
|
271
|
+
email: "alex.k@gmail.com",
|
|
272
|
+
password: "A1ex#Key2024",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: 6,
|
|
276
|
+
username: "emilyj",
|
|
277
|
+
email: "emilyj@hotmail.com",
|
|
278
|
+
password: "Em!ly0101",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: 7,
|
|
282
|
+
username: "davidparker",
|
|
283
|
+
email: "david.parker@yahoo.com",
|
|
284
|
+
password: "D@v!d2024",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 8,
|
|
288
|
+
username: "sophia_m",
|
|
289
|
+
email: "sophia.m@gmail.com",
|
|
290
|
+
password: "Sophi@2024",
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 9,
|
|
294
|
+
username: "chrisw",
|
|
295
|
+
email: "chrisw@outlook.com",
|
|
296
|
+
password: "Chri$Wong21",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: 10,
|
|
300
|
+
username: "natalie_b",
|
|
301
|
+
email: "natalie_b@gmail.com",
|
|
302
|
+
password: "N@talie#B2024",
|
|
303
|
+
},
|
|
304
|
+
];
|
|
155
305
|
|
|
156
306
|
function exceedHandler() {
|
|
157
|
-
|
|
307
|
+
const message = "Too much call in allowed window";
|
|
308
|
+
|
|
309
|
+
throw new Error(message, {
|
|
310
|
+
cause: { status: 500, message } satisfies ResponseError,
|
|
311
|
+
});
|
|
158
312
|
}
|
|
159
313
|
|
|
160
|
-
function
|
|
161
|
-
|
|
314
|
+
function getUserErrorHandler(e: Error) {
|
|
315
|
+
const message = "User not found.";
|
|
316
|
+
|
|
317
|
+
throw new Error(message, {
|
|
318
|
+
cause: { status: 404, message, details: e.message } satisfies ResponseError,
|
|
319
|
+
});
|
|
162
320
|
}
|
|
163
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Controller for handling user-related operations
|
|
324
|
+
* @class UserController
|
|
325
|
+
* @desc Provides methods for user management including CRUD operations
|
|
326
|
+
*/
|
|
164
327
|
export default class UserController {
|
|
165
|
-
//constructor(private readonly userService: UserService) { }
|
|
328
|
+
// constructor(private readonly userService: UserService) { }
|
|
166
329
|
|
|
167
|
-
// Throttle the createUser method to 1 request per 200 milliseconds
|
|
168
330
|
@rateLimit({
|
|
169
331
|
timeSpanMs: 60000,
|
|
170
332
|
allowedCalls: 300,
|
|
171
333
|
exceedHandler,
|
|
172
334
|
})
|
|
335
|
+
@Validate
|
|
336
|
+
/**
|
|
337
|
+
* Create a new user
|
|
338
|
+
* @param {UserCreationDto} user - User creation data validated by Zod schema
|
|
339
|
+
* @returns {Promise<void>}
|
|
340
|
+
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
341
|
+
* @throws {ResponseError} 400 - Invalid input data
|
|
342
|
+
*/
|
|
343
|
+
public async create(@ZodInput(ZodUserCreationDto) user: UserCreationDto) {
|
|
344
|
+
users.push({ ...user, id: users.length + 1 } satisfies User);
|
|
345
|
+
}
|
|
346
|
+
|
|
173
347
|
@onError({
|
|
174
|
-
func:
|
|
348
|
+
func: getUserErrorHandler,
|
|
175
349
|
})
|
|
176
|
-
@
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
350
|
+
@rateLimit({
|
|
351
|
+
timeSpanMs: 60000,
|
|
352
|
+
allowedCalls: 300,
|
|
353
|
+
exceedHandler,
|
|
354
|
+
})
|
|
355
|
+
/**
|
|
356
|
+
* Get user by ID
|
|
357
|
+
* @param {string} id - User ID as string
|
|
358
|
+
* @returns {Promise<User | ResponseError>} User details or error object
|
|
359
|
+
* @throws {ResponseError} 404 - User not found
|
|
360
|
+
* @throws {ResponseError} 400 - Invalid ID format
|
|
361
|
+
*/
|
|
362
|
+
public async get(id: string): Promise<User | ResponseError> {
|
|
363
|
+
const userId = tryParseId(id);
|
|
364
|
+
if (typeof userId != "number") return userId satisfies ResponseError;
|
|
365
|
+
const user = users.find((user) => user.id === userId);
|
|
366
|
+
|
|
367
|
+
if (!user)
|
|
368
|
+
return {
|
|
369
|
+
status: 404,
|
|
370
|
+
message: "User dose not exist.",
|
|
371
|
+
} satisfies ResponseError;
|
|
372
|
+
|
|
373
|
+
return user satisfies User;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
@timeout(20000)
|
|
377
|
+
@rateLimit({
|
|
378
|
+
timeSpanMs: 60000,
|
|
379
|
+
allowedCalls: 300,
|
|
380
|
+
exceedHandler,
|
|
381
|
+
})
|
|
382
|
+
/**
|
|
383
|
+
* Get all users with masked passwords
|
|
384
|
+
* @returns {Promise<User[]>} List of users with hidden password fields
|
|
385
|
+
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
386
|
+
*/
|
|
387
|
+
public async getAll(): Promise<User[]> {
|
|
388
|
+
return users.map(
|
|
389
|
+
(user) =>
|
|
390
|
+
({
|
|
391
|
+
...user,
|
|
392
|
+
password: "?",
|
|
393
|
+
} satisfies User)
|
|
394
|
+
);
|
|
183
395
|
}
|
|
184
396
|
}
|
|
185
397
|
```
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeweaver",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"main": "src/app.ts",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-codeweaver-app": "./command.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"start": "nodemon src/app.ts",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"start": "nodemon -r tsconfig-paths/register src/app.ts",
|
|
10
|
+
"dev": "nodemon -r tsconfig-paths/register src/app.ts",
|
|
11
|
+
"build": "tsc && tsc-alias && copyfiles -u 1 \"src/**/*.{json,env,sqlite,db,sql,yaml,yml,xml,txt,md}\" dist",
|
|
12
|
+
"serve": "cross-env TSCONFIG_PATH=tsconfig.paths.json node -r tsconfig-paths/register dist/app.js"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
14
15
|
"codeweaver",
|
|
@@ -27,23 +28,26 @@
|
|
|
27
28
|
"swagger",
|
|
28
29
|
"zod"
|
|
29
30
|
],
|
|
30
|
-
"author": "
|
|
31
|
+
"author": "js-code-crafter",
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"description": "A lightweight framework built on top of Express and TypeScript",
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"express": "^
|
|
35
|
+
"express": "^5.1.0",
|
|
35
36
|
"express-async-handler": "^1.2.0",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"zod": "^3.23.8"
|
|
37
|
+
"utils-decorators": "^2.10.0",
|
|
38
|
+
"zod": "^4.0.14"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@types/express": "^5.0.
|
|
42
|
-
"@types/node": "^
|
|
43
|
-
"
|
|
41
|
+
"@types/express": "^5.0.3",
|
|
42
|
+
"@types/node": "^24.1.0",
|
|
43
|
+
"copyfiles": "^2.4.1",
|
|
44
|
+
"cross-env": "^10.0.0",
|
|
45
|
+
"nodemon": "^3.1.10",
|
|
44
46
|
"swagger-jsdoc": "^6.2.8",
|
|
45
47
|
"swagger-ui-express": "^5.0.1",
|
|
46
48
|
"ts-node": "^10.9.2",
|
|
47
|
-
"
|
|
49
|
+
"tsc-alias": "^1.8.16",
|
|
50
|
+
"tsconfig-paths": "^4.2.0",
|
|
51
|
+
"typescript": "^5.9.2"
|
|
48
52
|
}
|
|
49
53
|
}
|
package/src/app.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import express
|
|
1
|
+
import express, {
|
|
2
|
+
ErrorRequestHandler,
|
|
3
|
+
NextFunction,
|
|
4
|
+
Request,
|
|
5
|
+
Response,
|
|
6
|
+
} from "express";
|
|
2
7
|
import fs from "fs";
|
|
3
8
|
import path from "path";
|
|
4
9
|
import config from "./config";
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Recursively loads Express routers from directory
|
|
13
|
+
* @param {string} routerPath - Directory path to scan
|
|
14
|
+
* @param {string} [basePath=""] - Base route path
|
|
15
|
+
*/
|
|
7
16
|
function loadRouters(routerPath: string, basePath: string = "") {
|
|
8
17
|
fs.readdirSync(routerPath).forEach((file) => {
|
|
9
18
|
// Check if the filename should be excluded based on certain criteria
|
|
@@ -39,7 +48,11 @@ function loadRouters(routerPath: string, basePath: string = "") {
|
|
|
39
48
|
// Exclude JavaScript test specification files
|
|
40
49
|
file.endsWith(".spec.js") ||
|
|
41
50
|
// Exclude TypeScript test specification files
|
|
42
|
-
file.endsWith(".spec.ts")
|
|
51
|
+
file.endsWith(".spec.ts") ||
|
|
52
|
+
// Exclude JavaScript test specification files
|
|
53
|
+
file.endsWith(".class.js") ||
|
|
54
|
+
// Exclude TypeScript test specification files
|
|
55
|
+
file.endsWith(".class.ts")
|
|
43
56
|
)
|
|
44
57
|
// If any of the above conditions are true, exit the function early
|
|
45
58
|
return;
|
|
@@ -85,6 +98,11 @@ if (config.devMode) {
|
|
|
85
98
|
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs));
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
// General error handler
|
|
102
|
+
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
|
|
103
|
+
res.status(500).json(err);
|
|
104
|
+
});
|
|
105
|
+
|
|
88
106
|
// Start the server
|
|
89
107
|
app.listen(config.port, () => {
|
|
90
108
|
console.log(`Server is running on http://localhost:${config.port}`);
|
package/src/config.ts
CHANGED
|
@@ -1,29 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server configuration interface
|
|
3
|
+
* @interface
|
|
4
|
+
* @property {string} url - Base server URL
|
|
5
|
+
*/
|
|
1
6
|
interface Server {
|
|
2
7
|
url: string;
|
|
3
8
|
}
|
|
4
9
|
|
|
10
|
+
/**
|
|
11
|
+
* API information structure
|
|
12
|
+
* @interface
|
|
13
|
+
* @property {string} title - API title
|
|
14
|
+
* @property {string} version - API version
|
|
15
|
+
* @property {string} description - API description
|
|
16
|
+
*/
|
|
5
17
|
interface Info {
|
|
6
18
|
title: string;
|
|
7
19
|
version: string;
|
|
8
20
|
description: string;
|
|
9
21
|
}
|
|
10
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Swagger definition structure
|
|
25
|
+
* @interface
|
|
26
|
+
* @property {string} openApi - OpenAPI specification version
|
|
27
|
+
* @property {Info} info - API information
|
|
28
|
+
* @property {Server[]} servers - List of server configurations
|
|
29
|
+
*/
|
|
11
30
|
interface SwaggerDefinition {
|
|
12
31
|
openApi: string;
|
|
13
32
|
info: Info;
|
|
14
33
|
servers: Server[];
|
|
15
34
|
}
|
|
16
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Swagger configuration options
|
|
38
|
+
* @interface
|
|
39
|
+
* @property {SwaggerDefinition} swaggerDefinition - Swagger definition object
|
|
40
|
+
* @property {string[]} apis - Paths to API documentation files
|
|
41
|
+
*/
|
|
17
42
|
interface SwaggerOptions {
|
|
18
43
|
swaggerDefinition: SwaggerDefinition;
|
|
19
44
|
apis: string[];
|
|
20
45
|
}
|
|
21
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Main application configuration
|
|
49
|
+
* @interface
|
|
50
|
+
* @property {boolean} devMode - Development mode flag
|
|
51
|
+
* @property {string} port - Server port
|
|
52
|
+
* @property {SwaggerOptions} swaggerOptions - Swagger configuration
|
|
53
|
+
*/
|
|
22
54
|
interface Config {
|
|
23
55
|
devMode: boolean;
|
|
24
56
|
port: string;
|
|
25
57
|
swaggerOptions: SwaggerOptions;
|
|
26
58
|
}
|
|
59
|
+
|
|
27
60
|
const port = process.env.PORT || "3000";
|
|
28
61
|
const config: Config = {
|
|
29
62
|
devMode: process.env.NODE_ENV !== "production",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-types */
|
|
2
|
+
import ZodValidator from './validator.class';
|
|
3
|
+
|
|
4
|
+
export const Validate: MethodDecorator = (
|
|
5
|
+
target: Object,
|
|
6
|
+
propertyKey: string | symbol,
|
|
7
|
+
descriptor: PropertyDescriptor,
|
|
8
|
+
) => {
|
|
9
|
+
const originalMethod = descriptor.value;
|
|
10
|
+
descriptor.value = async function (...args: unknown[]) {
|
|
11
|
+
ZodValidator.validateInput(target, propertyKey as string, args);
|
|
12
|
+
const result = originalMethod.apply(this, args);
|
|
13
|
+
let resultValue = result;
|
|
14
|
+
if (result instanceof Promise) {
|
|
15
|
+
resultValue = await result;
|
|
16
|
+
ZodValidator.validateOutput(target, propertyKey as string, resultValue);
|
|
17
|
+
}
|
|
18
|
+
return resultValue;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-types */
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
|
|
4
|
+
type Target = Object;
|
|
5
|
+
type MethodName = string;
|
|
6
|
+
type Metadata = {
|
|
7
|
+
paramsIndex: number[];
|
|
8
|
+
inputSchema?: z.ZodObject<any, any>;
|
|
9
|
+
outputSchema?: z.ZodObject<any, any>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default class ZodValidator {
|
|
13
|
+
private static methodValidatorMap: Map<Target, Map<MethodName, Metadata>> =
|
|
14
|
+
new Map();
|
|
15
|
+
|
|
16
|
+
static registerInputParameterValidationSchema(
|
|
17
|
+
target: Object,
|
|
18
|
+
methodName: MethodName,
|
|
19
|
+
paramIndex: number,
|
|
20
|
+
schema: z.ZodObject<any, any>
|
|
21
|
+
) {
|
|
22
|
+
let paramMap = this.methodValidatorMap.get(target)!;
|
|
23
|
+
if (!paramMap) {
|
|
24
|
+
paramMap = new Map();
|
|
25
|
+
this.methodValidatorMap.set(target, paramMap);
|
|
26
|
+
}
|
|
27
|
+
let metadata = paramMap.get(methodName)!;
|
|
28
|
+
if (!metadata) {
|
|
29
|
+
metadata = { paramsIndex: [], inputSchema: schema };
|
|
30
|
+
paramMap.set(methodName, metadata);
|
|
31
|
+
}
|
|
32
|
+
metadata.paramsIndex.push(paramIndex);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static registerMethodValidationOutputSchema(
|
|
36
|
+
target: Object,
|
|
37
|
+
methodName: MethodName,
|
|
38
|
+
schema: z.ZodObject<any, any>
|
|
39
|
+
) {
|
|
40
|
+
let paramMap = this.methodValidatorMap.get(target)!;
|
|
41
|
+
if (!paramMap) {
|
|
42
|
+
paramMap = new Map();
|
|
43
|
+
this.methodValidatorMap.set(target, paramMap);
|
|
44
|
+
}
|
|
45
|
+
let metadata = paramMap.get(methodName)!;
|
|
46
|
+
if (!metadata) {
|
|
47
|
+
metadata = { paramsIndex: [], outputSchema: schema };
|
|
48
|
+
paramMap.set(methodName, metadata);
|
|
49
|
+
}
|
|
50
|
+
metadata.outputSchema = schema;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static validateInput(
|
|
54
|
+
target: Object,
|
|
55
|
+
methodName: string,
|
|
56
|
+
paramValues: unknown[]
|
|
57
|
+
) {
|
|
58
|
+
const methodMetadataMap = this.methodValidatorMap.get(target)!;
|
|
59
|
+
const metadata = methodMetadataMap.get(methodName)!;
|
|
60
|
+
for (const [index, input] of paramValues.entries()) {
|
|
61
|
+
if (metadata.paramsIndex.indexOf(index) != -1) {
|
|
62
|
+
metadata.inputSchema?.parse(input);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static validateOutput(target: Object, methodName: string, output: unknown) {
|
|
68
|
+
const methodMetadataMap = this.methodValidatorMap.get(target)!;
|
|
69
|
+
const metadata = methodMetadataMap.get(methodName)!;
|
|
70
|
+
metadata.outputSchema?.parse(output);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ZodObject } from "zod";
|
|
2
|
+
import ZodValidator from "./validator.class";
|
|
3
|
+
|
|
4
|
+
export const ZodInput =
|
|
5
|
+
<T extends ZodObject<any, any>>(schema: T): ParameterDecorator =>
|
|
6
|
+
(target, propertyKey, parameterIndex) =>
|
|
7
|
+
ZodValidator.registerInputParameterValidationSchema(
|
|
8
|
+
target,
|
|
9
|
+
propertyKey as string,
|
|
10
|
+
parameterIndex,
|
|
11
|
+
schema
|
|
12
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ZodObject } from "zod";
|
|
2
|
+
import ZodValidator from "./validator.class";
|
|
3
|
+
|
|
4
|
+
export const ZodOutput =
|
|
5
|
+
<T extends ZodObject<any, any>>(schema: T): MethodDecorator =>
|
|
6
|
+
(target, propertyKey) =>
|
|
7
|
+
ZodValidator.registerMethodValidationOutputSchema(
|
|
8
|
+
target,
|
|
9
|
+
propertyKey as string,
|
|
10
|
+
schema
|
|
11
|
+
);
|
package/src/routers/index.ts
CHANGED
|
@@ -11,12 +11,12 @@ const router = Router();
|
|
|
11
11
|
* description: Returns the home page.
|
|
12
12
|
* responses:
|
|
13
13
|
* 200:
|
|
14
|
-
* description:
|
|
14
|
+
* description: Hi there!
|
|
15
15
|
*/
|
|
16
16
|
router.get(
|
|
17
17
|
"/",
|
|
18
18
|
asyncHandler(async (req: Request, res: Response) => {
|
|
19
|
-
res.send("
|
|
19
|
+
res.send("Hi there!");
|
|
20
20
|
})
|
|
21
21
|
);
|
|
22
22
|
|