codeweaver 1.0.15 → 2.0.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/.vscode/settings.json +3 -0
- package/README.md +261 -56
- package/package.json +21 -15
- package/src/config.ts +56 -15
- package/src/constants.ts +6 -0
- package/src/main.ts +82 -0
- package/src/routers/{index.ts → index.router.ts} +2 -2
- package/src/routers/orders/dto/order.dto.ts +41 -0
- package/src/routers/orders/index.router.ts +157 -0
- package/src/routers/orders/order.controller.ts +178 -0
- package/src/routers/products/dto/product.dto.ts +37 -0
- package/src/routers/products/index.router.ts +200 -0
- package/src/routers/products/product.controller.ts +217 -0
- package/src/routers/users/dto/user.dto.ts +17 -3
- package/src/routers/users/index.router.ts +96 -0
- package/src/routers/users/user.controller.ts +138 -18
- package/src/swagger-options.ts +54 -0
- package/src/utilities/assign.ts +81 -0
- package/src/utilities/error-handling.ts +120 -0
- package/src/utilities/types.ts +23 -0
- package/tsconfig.json +6 -3
- package/tsconfig.paths.json +8 -0
- package/src/app.ts +0 -95
- package/src/routers/orders/index.ts +0 -40
- package/src/routers/products/index.ts +0 -167
- package/src/routers/users/index.ts +0 -81
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
|
|
2
|
+
import {
|
|
3
|
+
Product,
|
|
4
|
+
ProductCreationDto,
|
|
5
|
+
ProductDto,
|
|
6
|
+
ProductUpdateDto,
|
|
7
|
+
ZodProductCreationDto,
|
|
8
|
+
ZodProductUpdateDto,
|
|
9
|
+
} from "./dto/product.dto";
|
|
10
|
+
import { Validate, ZodInput } from "ts-zod4-decorators";
|
|
11
|
+
import { ResponseError } from "@/utilities/types";
|
|
12
|
+
import { parseId } from "@/utilities/error-handling";
|
|
13
|
+
import config from "@/config";
|
|
14
|
+
|
|
15
|
+
// Array to store products (as a mock database)
|
|
16
|
+
const products: Product[] = [
|
|
17
|
+
{
|
|
18
|
+
id: 1,
|
|
19
|
+
name: "ASUS ROG Zephyrus G15",
|
|
20
|
+
price: 45000000,
|
|
21
|
+
description: "Gaming laptop with AMD Ryzen 9 5900HS and RTX 3080 GPU",
|
|
22
|
+
category: "Electronics",
|
|
23
|
+
stock: 15,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 2,
|
|
27
|
+
name: "Sony WH-1000XM5 Wireless Headphones",
|
|
28
|
+
price: 12000000,
|
|
29
|
+
description:
|
|
30
|
+
"Premium noise-canceling over-ear headphones with 30hr battery",
|
|
31
|
+
category: "Electronics",
|
|
32
|
+
stock: 8,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 3,
|
|
36
|
+
name: "LG Smart Inverter Microwave",
|
|
37
|
+
price: 25000000,
|
|
38
|
+
description: "1.7 cu.ft countertop microwave with smart sensor cooking",
|
|
39
|
+
category: "Appliances",
|
|
40
|
+
stock: 5,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 4,
|
|
44
|
+
name: "Trek Marlin 5 Mountain Bike",
|
|
45
|
+
price: 18000000,
|
|
46
|
+
description: "Entry-level mountain bike with aluminum frame and 21 speeds",
|
|
47
|
+
category: "Sports",
|
|
48
|
+
stock: 3,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 5,
|
|
52
|
+
name: "DeLonghi Espresso Machine",
|
|
53
|
+
price: 6500000,
|
|
54
|
+
description: "Compact espresso maker with manual milk frother",
|
|
55
|
+
category: "Kitchen",
|
|
56
|
+
stock: 12,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 6,
|
|
60
|
+
name: "Anker Wireless Charger",
|
|
61
|
+
price: 1200000,
|
|
62
|
+
description: "15W fast wireless charger with anti-slip surface",
|
|
63
|
+
category: "Mobile Accessories",
|
|
64
|
+
stock: 30,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 7,
|
|
68
|
+
name: "Logitech MX Master 3 Mouse",
|
|
69
|
+
price: 4500000,
|
|
70
|
+
description: "Ergonomic wireless mouse with Darkfield tracking",
|
|
71
|
+
category: "Computer Accessories",
|
|
72
|
+
stock: 18,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 8,
|
|
76
|
+
name: "Kindle Paperwhite",
|
|
77
|
+
price: 3800000,
|
|
78
|
+
description: 'Waterproof e-reader with 6.8" 300ppi display',
|
|
79
|
+
category: "Electronics",
|
|
80
|
+
stock: 9,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 9,
|
|
84
|
+
name: "Dyson V11 Vacuum Cleaner",
|
|
85
|
+
price: 32000000,
|
|
86
|
+
description: "Cordless stick vacuum with LCD screen and 60min runtime",
|
|
87
|
+
category: "Home Appliances",
|
|
88
|
+
stock: 7,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function exceedHandler() {
|
|
93
|
+
const message = "Too much call in allowed window";
|
|
94
|
+
throw new ResponseError(message, 429);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getProductErrorHandler(e: Error) {
|
|
98
|
+
const message = "Product not found.";
|
|
99
|
+
throw new ResponseError(message, 404, e.message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Controller for handling product-related operations
|
|
104
|
+
* @class ProductController
|
|
105
|
+
* @desc Provides methods for product management including CRUD operations
|
|
106
|
+
*/
|
|
107
|
+
export default class ProductController {
|
|
108
|
+
@rateLimit({
|
|
109
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
110
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
111
|
+
exceedHandler,
|
|
112
|
+
})
|
|
113
|
+
@Validate
|
|
114
|
+
/**
|
|
115
|
+
* Creates a new product with validated data
|
|
116
|
+
* @param product - Product creation data validated by Zod schema
|
|
117
|
+
* @returns Newly created product with generated ID
|
|
118
|
+
*/
|
|
119
|
+
public async create(
|
|
120
|
+
@ZodInput(ZodProductCreationDto) product: ProductCreationDto
|
|
121
|
+
): Promise<ProductDto> {
|
|
122
|
+
products.push({
|
|
123
|
+
...product,
|
|
124
|
+
id: products.length + 1,
|
|
125
|
+
} satisfies Product);
|
|
126
|
+
|
|
127
|
+
return product as ProductDto;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@memoizeAsync(config.memoizeTime)
|
|
131
|
+
@timeout(config.timeout)
|
|
132
|
+
@rateLimit({
|
|
133
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
134
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
135
|
+
exceedHandler,
|
|
136
|
+
})
|
|
137
|
+
/**
|
|
138
|
+
* Retrieves all products with truncated descriptions
|
|
139
|
+
* @returns List of products with summarized descriptions
|
|
140
|
+
*/
|
|
141
|
+
public async getAll(): Promise<ProductDto[]> {
|
|
142
|
+
return products.map((product) => ({
|
|
143
|
+
...product,
|
|
144
|
+
description: product.description?.substring(0, 50) + "..." || "",
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@memoizeAsync(config.memoizeTime)
|
|
149
|
+
@onError({
|
|
150
|
+
func: getProductErrorHandler,
|
|
151
|
+
})
|
|
152
|
+
@rateLimit({
|
|
153
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
154
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
155
|
+
exceedHandler,
|
|
156
|
+
})
|
|
157
|
+
/**
|
|
158
|
+
* Finds a product by its ID
|
|
159
|
+
* @param id - Product ID as string
|
|
160
|
+
* @returns Product details or error object if not found
|
|
161
|
+
*/
|
|
162
|
+
public async get(id: string): Promise<ProductDto> {
|
|
163
|
+
const productId = parseId(id);
|
|
164
|
+
const product = products.find((product) => product.id === productId);
|
|
165
|
+
if (product == null) throw new ResponseError("User dose not exist.", 404);
|
|
166
|
+
return product;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@rateLimit({
|
|
170
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
171
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
172
|
+
exceedHandler,
|
|
173
|
+
})
|
|
174
|
+
@Validate
|
|
175
|
+
/**
|
|
176
|
+
* Updates an existing product
|
|
177
|
+
* @param {string} id - Product ID to update
|
|
178
|
+
* @param {ProductUpdateDto} updateData - Partial product data to update
|
|
179
|
+
* @returns {Promise<Product>} Updated product or error object
|
|
180
|
+
* @throws {ResponseError} 404 - Product not found
|
|
181
|
+
* @throws {ResponseError} 400 - Invalid ID format or update data
|
|
182
|
+
*/
|
|
183
|
+
public async update(
|
|
184
|
+
id: string,
|
|
185
|
+
@ZodInput(ZodProductUpdateDto) updateData: ProductUpdateDto
|
|
186
|
+
): Promise<ProductDto> {
|
|
187
|
+
const product = await this.get(id);
|
|
188
|
+
if ("id" in product == false) return product satisfies ResponseError;
|
|
189
|
+
|
|
190
|
+
if (product) {
|
|
191
|
+
Object.assign(product, updateData);
|
|
192
|
+
} else {
|
|
193
|
+
throw new ResponseError("Product dose not exist.", 404);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return product;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@rateLimit({
|
|
200
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
201
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
202
|
+
exceedHandler,
|
|
203
|
+
})
|
|
204
|
+
/**
|
|
205
|
+
* Deletes a product by ID
|
|
206
|
+
* @param {string} id - Product ID to delete
|
|
207
|
+
* @returns {Promise<Product>} Deleted product or error object
|
|
208
|
+
* @throws {ResponseError} 404 - Product not found
|
|
209
|
+
* @throws {ResponseError} 400 - Invalid ID format
|
|
210
|
+
*/
|
|
211
|
+
public async delete(id: string): Promise<ProductDto> {
|
|
212
|
+
const productId = parseId(id);
|
|
213
|
+
const index = products.findIndex((product) => product.id === productId);
|
|
214
|
+
if (index == -1) throw new ResponseError("Product dose not exist.", 404);
|
|
215
|
+
return products.splice(index, 1)[0];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Zod schema for User entity
|
|
5
|
+
* @typedef {Object} ZodUser
|
|
6
|
+
* @property {number} id - Unique identifier (min 1)
|
|
7
|
+
* @property {string} username - Username (min 3 chars)
|
|
8
|
+
* @property {string} email - Valid email format
|
|
9
|
+
* @property {string} password - Password (min 6 chars)
|
|
10
|
+
*/
|
|
11
|
+
export const ZodUser = z.object({
|
|
12
|
+
id: z.number().min(1).int(),
|
|
4
13
|
username: z.string().min(3),
|
|
5
|
-
email: z.
|
|
14
|
+
email: z.email(),
|
|
6
15
|
password: z.string().min(6),
|
|
7
16
|
});
|
|
8
17
|
|
|
9
|
-
export
|
|
18
|
+
export const ZodUserCreationDto = ZodUser.omit({ id: true });
|
|
19
|
+
export const ZodUserDto = ZodUser.omit({ password: true });
|
|
20
|
+
|
|
21
|
+
export type User = z.infer<typeof ZodUser>;
|
|
22
|
+
export type UserCreationDto = z.infer<typeof ZodUserCreationDto>;
|
|
23
|
+
export type UserDto = z.infer<typeof ZodUserDto>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import asyncHandler from "express-async-handler";
|
|
3
|
+
import UserController from "./user.controller";
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
const userController = new UserController();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @swagger
|
|
10
|
+
* /users:
|
|
11
|
+
* post:
|
|
12
|
+
* summary: Create a user
|
|
13
|
+
* description: Create a new user.
|
|
14
|
+
* consumes:
|
|
15
|
+
* - application/json
|
|
16
|
+
* produces:
|
|
17
|
+
* - application/json
|
|
18
|
+
* parameters:
|
|
19
|
+
* - in: body
|
|
20
|
+
* name: user
|
|
21
|
+
* required: true
|
|
22
|
+
* schema:
|
|
23
|
+
* type: object
|
|
24
|
+
* required:
|
|
25
|
+
* - username
|
|
26
|
+
* - email
|
|
27
|
+
* - password
|
|
28
|
+
* properties:
|
|
29
|
+
* username:
|
|
30
|
+
* type: string
|
|
31
|
+
* minLength: 3
|
|
32
|
+
* example: JessicaSmith
|
|
33
|
+
* email:
|
|
34
|
+
* type: string
|
|
35
|
+
* format: email
|
|
36
|
+
* example: user@example.com
|
|
37
|
+
* password:
|
|
38
|
+
* type: string
|
|
39
|
+
* minLength: 6
|
|
40
|
+
* example: securePassword123
|
|
41
|
+
* responses:
|
|
42
|
+
* 201:
|
|
43
|
+
* description: User created
|
|
44
|
+
*/
|
|
45
|
+
router.post(
|
|
46
|
+
"/",
|
|
47
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
48
|
+
const user = await userController.create(req.body);
|
|
49
|
+
res.status(201).json(user);
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @swagger
|
|
55
|
+
* /users/{id}:
|
|
56
|
+
* get:
|
|
57
|
+
* summary: Get a user by ID
|
|
58
|
+
* parameters:
|
|
59
|
+
* - name: id
|
|
60
|
+
* in: path
|
|
61
|
+
* required: true
|
|
62
|
+
* description: The ID of the product
|
|
63
|
+
* schema:
|
|
64
|
+
* type: integer
|
|
65
|
+
* responses:
|
|
66
|
+
* 200:
|
|
67
|
+
* description: A user object
|
|
68
|
+
* 404:
|
|
69
|
+
* description: user not found
|
|
70
|
+
*/
|
|
71
|
+
router.get(
|
|
72
|
+
"/:id",
|
|
73
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
74
|
+
const user = await userController.get(req.params.id);
|
|
75
|
+
res.json(user);
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @swagger
|
|
81
|
+
* /users:
|
|
82
|
+
* get:
|
|
83
|
+
* summary: Get users
|
|
84
|
+
* description: Returns a list of user objects.
|
|
85
|
+
* responses:
|
|
86
|
+
* 200:
|
|
87
|
+
* description: A list of user objects
|
|
88
|
+
*/
|
|
89
|
+
router.get(
|
|
90
|
+
"/",
|
|
91
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
92
|
+
res.json(await userController.getAll());
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
export = router;
|
|
@@ -1,33 +1,153 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
User,
|
|
3
|
+
ZodUserCreationDto,
|
|
4
|
+
UserCreationDto,
|
|
5
|
+
UserDto,
|
|
6
|
+
} from "./dto/user.dto";
|
|
7
|
+
import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
|
|
8
|
+
import { Validate, ZodInput } from "ts-zod4-decorators";
|
|
9
|
+
import { ResponseError } from "@/utilities/types";
|
|
10
|
+
import { parseId } from "@/utilities/error-handling";
|
|
11
|
+
import config from "@/config";
|
|
12
|
+
|
|
13
|
+
// Array to store users (as a mock database)
|
|
14
|
+
const users = [
|
|
15
|
+
{
|
|
16
|
+
id: 1,
|
|
17
|
+
username: "johndoe",
|
|
18
|
+
email: "johndoe@gmail.com",
|
|
19
|
+
password: "S3cur3P@ssw0rd",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 2,
|
|
23
|
+
username: "janesmith",
|
|
24
|
+
email: "janesmith@yahoo.com",
|
|
25
|
+
password: "P@ssw0rd2024",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 3,
|
|
29
|
+
username: "michael89",
|
|
30
|
+
email: "michael89@hotmail.com",
|
|
31
|
+
password: "M1chael!2024",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 4,
|
|
35
|
+
username: "lisa.wong",
|
|
36
|
+
email: "lisa.wong@example.com",
|
|
37
|
+
password: "L1saW0ng!2024",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 5,
|
|
41
|
+
username: "alex_k",
|
|
42
|
+
email: "alex.k@gmail.com",
|
|
43
|
+
password: "A1ex#Key2024",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 6,
|
|
47
|
+
username: "emilyj",
|
|
48
|
+
email: "emilyj@hotmail.com",
|
|
49
|
+
password: "Em!ly0101",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 7,
|
|
53
|
+
username: "davidparker",
|
|
54
|
+
email: "david.parker@yahoo.com",
|
|
55
|
+
password: "D@v!d2024",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 8,
|
|
59
|
+
username: "sophia_m",
|
|
60
|
+
email: "sophia.m@gmail.com",
|
|
61
|
+
password: "Sophi@2024",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 9,
|
|
65
|
+
username: "chrisw",
|
|
66
|
+
email: "chrisw@outlook.com",
|
|
67
|
+
password: "Chri$Wong21",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 10,
|
|
71
|
+
username: "natalie_b",
|
|
72
|
+
email: "natalie_b@gmail.com",
|
|
73
|
+
password: "N@talie#B2024",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
4
76
|
|
|
5
77
|
function exceedHandler() {
|
|
6
|
-
|
|
78
|
+
const message = "Too much call in allowed window";
|
|
79
|
+
throw new ResponseError(message, 429);
|
|
7
80
|
}
|
|
8
81
|
|
|
9
|
-
function
|
|
10
|
-
|
|
82
|
+
function getUserErrorHandler(e: Error) {
|
|
83
|
+
const message = "User not found.";
|
|
84
|
+
throw new ResponseError(message, 404, e.message);
|
|
11
85
|
}
|
|
12
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Controller for handling user-related operations
|
|
89
|
+
* @class UserController
|
|
90
|
+
* @desc Provides methods for user management including CRUD operations
|
|
91
|
+
*/
|
|
13
92
|
export default class UserController {
|
|
14
|
-
//constructor(private readonly userService: UserService) { }
|
|
93
|
+
// constructor(private readonly userService: UserService) { }
|
|
15
94
|
|
|
16
|
-
// Throttle the createUser method to 1 request per 200 milliseconds
|
|
17
95
|
@rateLimit({
|
|
18
|
-
timeSpanMs:
|
|
19
|
-
allowedCalls:
|
|
96
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
97
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
20
98
|
exceedHandler,
|
|
21
99
|
})
|
|
100
|
+
@Validate
|
|
101
|
+
/**
|
|
102
|
+
* Create a new user
|
|
103
|
+
* @param {UserCreationDto} user - User creation data validated by Zod schema
|
|
104
|
+
* @returns {Promise<void>}
|
|
105
|
+
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
106
|
+
* @throws {ResponseError} 400 - Invalid input data
|
|
107
|
+
*/
|
|
108
|
+
public async create(@ZodInput(ZodUserCreationDto) user: UserCreationDto) {
|
|
109
|
+
users.push({ ...user, id: users.length + 1 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@memoizeAsync(config.memoizeTime)
|
|
22
113
|
@onError({
|
|
23
|
-
func:
|
|
114
|
+
func: getUserErrorHandler,
|
|
24
115
|
})
|
|
25
|
-
@
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
116
|
+
@rateLimit({
|
|
117
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
118
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
119
|
+
exceedHandler,
|
|
120
|
+
})
|
|
121
|
+
/**
|
|
122
|
+
* Get user by ID
|
|
123
|
+
* @param {string} id - User ID as string
|
|
124
|
+
* @returns {Promise<User>} User details or error object
|
|
125
|
+
* @throws {ResponseError} 404 - User not found
|
|
126
|
+
* @throws {ResponseError} 400 - Invalid ID format
|
|
127
|
+
*/
|
|
128
|
+
public async get(id: string): Promise<UserDto> {
|
|
129
|
+
const response = parseId(id);
|
|
130
|
+
const user = users.find((user) => user.id === response);
|
|
131
|
+
if (user == null) throw new ResponseError("User dose not exist.", 404);
|
|
132
|
+
return user satisfies User;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@memoizeAsync(config.memoizeTime)
|
|
136
|
+
@timeout(config.timeout)
|
|
137
|
+
@rateLimit({
|
|
138
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
139
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
140
|
+
exceedHandler,
|
|
141
|
+
})
|
|
142
|
+
/**
|
|
143
|
+
* Get all users with masked passwords
|
|
144
|
+
* @returns {Promise<User[]>} List of users with hidden password fields
|
|
145
|
+
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
146
|
+
*/
|
|
147
|
+
public async getAll(): Promise<UserDto[]> {
|
|
148
|
+
return users.map((user) => ({
|
|
149
|
+
...user,
|
|
150
|
+
password: "?",
|
|
151
|
+
}));
|
|
32
152
|
}
|
|
33
153
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memoizeTime,
|
|
3
|
+
productionEnvironment,
|
|
4
|
+
rateLimitTimeSpan,
|
|
5
|
+
rateLimitAllowedCalls,
|
|
6
|
+
timeout,
|
|
7
|
+
portNumber,
|
|
8
|
+
} from "./constants";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Server configuration interface
|
|
12
|
+
* @interface
|
|
13
|
+
* @property {string} url - Base server URL
|
|
14
|
+
*/
|
|
15
|
+
interface Server {
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* API information structure
|
|
21
|
+
* @interface
|
|
22
|
+
* @property {string} title - API title
|
|
23
|
+
* @property {string} version - API version
|
|
24
|
+
* @property {string} description - API description
|
|
25
|
+
*/
|
|
26
|
+
interface Info {
|
|
27
|
+
title: string;
|
|
28
|
+
version: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Swagger definition structure
|
|
34
|
+
* @interface
|
|
35
|
+
* @property {string} openApi - OpenAPI specification version
|
|
36
|
+
* @property {Info} info - API information
|
|
37
|
+
* @property {Server[]} servers - List of server configurations
|
|
38
|
+
*/
|
|
39
|
+
interface SwaggerDefinition {
|
|
40
|
+
openApi: string;
|
|
41
|
+
info: Info;
|
|
42
|
+
servers: Server[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Swagger configuration options
|
|
47
|
+
* @interface
|
|
48
|
+
* @property {SwaggerDefinition} swaggerDefinition - Swagger definition object
|
|
49
|
+
* @property {string[]} apis - Paths to API documentation files
|
|
50
|
+
*/
|
|
51
|
+
export interface SwaggerOptions {
|
|
52
|
+
swaggerDefinition: SwaggerDefinition;
|
|
53
|
+
apis: string[];
|
|
54
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z, ZodRawShape } from "zod";
|
|
2
|
+
import { ResponseError } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Strictly convert obj (type T1) to T2 using a Zod schema.
|
|
6
|
+
*
|
|
7
|
+
* - Throws if obj has extra fields beyond those defined in the schema.
|
|
8
|
+
* - Validates fields with the schema; on failure, throws with a descriptive message.
|
|
9
|
+
* - Returns an object typed as T2 (inferred from the schema).
|
|
10
|
+
*
|
|
11
|
+
* @param obj - Source object of type T1
|
|
12
|
+
* @param schema - Zod schema describing the target type T2
|
|
13
|
+
* @returns T2 inferred from the provided schema
|
|
14
|
+
*/
|
|
15
|
+
export function assignStrictlyFromSchema<T1 extends object, T2 extends object>(
|
|
16
|
+
obj: T1,
|
|
17
|
+
schema: z.ZodObject<any>
|
|
18
|
+
): T2 {
|
|
19
|
+
// 1) Derive the runtime keys from the schema's shape
|
|
20
|
+
const shape = (schema as any)._def?.shape as ZodRawShape | undefined;
|
|
21
|
+
if (!shape) {
|
|
22
|
+
throw new ResponseError(
|
|
23
|
+
"assignStrictlyFromSchema: provided schema has no shape.",
|
|
24
|
+
500
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const keysSchema = Object.keys(shape) as Array<keyof any>;
|
|
29
|
+
|
|
30
|
+
// 2) Extra keys check
|
|
31
|
+
const objKeys = Object.keys(obj) as Array<keyof T1>;
|
|
32
|
+
const extraKeys = objKeys.filter((k) => !keysSchema.includes(k as any));
|
|
33
|
+
if (extraKeys.length > 0) {
|
|
34
|
+
throw new ResponseError(
|
|
35
|
+
`assignStrictlyFromSchema: source object contains extra field(s) not present on target: ${extraKeys.join(
|
|
36
|
+
", "
|
|
37
|
+
)}`,
|
|
38
|
+
500
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3) Required-field check for T2 (all keys in schema must be present and non-undefined)
|
|
43
|
+
const missingOrUndefined = keysSchema.filter((k) => {
|
|
44
|
+
const v = (obj as any)[k];
|
|
45
|
+
return v === undefined || v === null;
|
|
46
|
+
});
|
|
47
|
+
if (missingOrUndefined.length > 0) {
|
|
48
|
+
throw new ResponseError(
|
|
49
|
+
`assignStrictlyFromSchema: missing required field(s): ${missingOrUndefined.join(
|
|
50
|
+
", "
|
|
51
|
+
)}`,
|
|
52
|
+
500
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4) Build a plain object to pass through Zod for validation
|
|
57
|
+
const candidate: any = {};
|
|
58
|
+
for (const k of keysSchema) {
|
|
59
|
+
if (k in (obj as any)) {
|
|
60
|
+
candidate[k] = (obj as any)[k];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 5) Validate against the schema
|
|
65
|
+
const result = schema.safeParse(candidate);
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
// Modern, non-format error reporting
|
|
68
|
+
const issues = result.error.issues.map((i) => ({
|
|
69
|
+
path: i.path, // where the issue occurred
|
|
70
|
+
message: i.message, // human-friendly message
|
|
71
|
+
code: i.code, // e.g., "too_small", "invalid_type"
|
|
72
|
+
}));
|
|
73
|
+
// You can log issues or throw a structured error
|
|
74
|
+
throw new Error(
|
|
75
|
+
`assignStrictlyFromSchema: validation failed: ${JSON.stringify(issues)}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 6) Return the validated data typed as T2
|
|
80
|
+
return result.data as T2;
|
|
81
|
+
}
|