codeweaver 1.1.0 → 2.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/.vscode/settings.json +3 -0
- package/README.md +114 -181
- package/package.json +12 -7
- package/src/app.ts +2 -112
- package/src/config.ts +33 -64
- package/src/constants.ts +7 -0
- package/src/db.ts +183 -0
- package/src/entities/order.entity.ts +68 -0
- package/src/entities/product.entity.ts +75 -0
- package/src/entities/user.entity.ts +38 -0
- package/src/main.ts +85 -0
- package/src/routers/orders/dto/order.dto.ts +54 -29
- package/src/routers/orders/{index.ts → index.router.ts} +13 -13
- package/src/routers/orders/order.controller.ts +118 -120
- package/src/routers/products/dto/product.dto.ts +86 -30
- package/src/routers/products/{index.ts → index.router.ts} +14 -15
- package/src/routers/products/product.controller.ts +136 -161
- package/src/routers/users/dto/user.dto.ts +14 -18
- package/src/routers/users/{index.ts → index.router.ts} +6 -7
- package/src/routers/users/user.controller.ts +87 -118
- package/src/swagger-options.ts +39 -0
- package/src/utilities/assign.ts +66 -0
- package/src/utilities/cache/memory-cache.ts +74 -0
- package/src/utilities/cache/redis-cache.ts +111 -0
- package/src/utilities/conversion.ts +158 -0
- package/src/utilities/error-handling.ts +156 -0
- package/src/utilities/router.ts +0 -0
- package/tsconfig.json +1 -4
- package/tsconfig.paths.json +8 -10
- package/src/packages/ts-zod-decorators/index.ts +0 -3
- package/src/packages/ts-zod-decorators/validate.decorator.ts +0 -20
- package/src/packages/ts-zod-decorators/validator.class.ts +0 -72
- package/src/packages/ts-zod-decorators/zod-input.decorator.ts +0 -12
- package/src/packages/ts-zod-decorators/zod-output.decorator.ts +0 -11
- package/src/types.ts +0 -16
- package/src/utilities.ts +0 -47
- /package/src/routers/{index.ts → index.router.ts} +0 -0
|
@@ -1,237 +1,212 @@
|
|
|
1
|
-
import { onError, rateLimit, timeout } from "utils-decorators";
|
|
2
1
|
import {
|
|
3
|
-
|
|
2
|
+
memoizeAsync,
|
|
3
|
+
onError,
|
|
4
|
+
rateLimit,
|
|
5
|
+
timeout,
|
|
6
|
+
before,
|
|
7
|
+
} from "utils-decorators";
|
|
8
|
+
import {
|
|
4
9
|
ProductCreationDto,
|
|
10
|
+
ProductDto,
|
|
5
11
|
ProductUpdateDto,
|
|
6
12
|
ZodProductCreationDto,
|
|
7
13
|
ZodProductUpdateDto,
|
|
8
14
|
} from "./dto/product.dto";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
id: 1,
|
|
17
|
-
name: "ASUS ROG Zephyrus G15",
|
|
18
|
-
price: 45000000,
|
|
19
|
-
description: "Gaming laptop with AMD Ryzen 9 5900HS and RTX 3080 GPU",
|
|
20
|
-
category: "Electronics",
|
|
21
|
-
stock: 15,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: 2,
|
|
25
|
-
name: "Sony WH-1000XM5 Wireless Headphones",
|
|
26
|
-
price: 12000000,
|
|
27
|
-
description:
|
|
28
|
-
"Premium noise-canceling over-ear headphones with 30hr battery",
|
|
29
|
-
category: "Electronics",
|
|
30
|
-
stock: 8,
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: 3,
|
|
34
|
-
name: "LG Smart Inverter Microwave",
|
|
35
|
-
price: 25000000,
|
|
36
|
-
description: "1.7 cu.ft countertop microwave with smart sensor cooking",
|
|
37
|
-
category: "Appliances",
|
|
38
|
-
stock: 5,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
id: 4,
|
|
42
|
-
name: "Trek Marlin 5 Mountain Bike",
|
|
43
|
-
price: 18000000,
|
|
44
|
-
description: "Entry-level mountain bike with aluminum frame and 21 speeds",
|
|
45
|
-
category: "Sports",
|
|
46
|
-
stock: 3,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: 5,
|
|
50
|
-
name: "DeLonghi Espresso Machine",
|
|
51
|
-
price: 6500000,
|
|
52
|
-
description: "Compact espresso maker with manual milk frother",
|
|
53
|
-
category: "Kitchen",
|
|
54
|
-
stock: 12,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: 6,
|
|
58
|
-
name: "Anker Wireless Charger",
|
|
59
|
-
price: 1200000,
|
|
60
|
-
description: "15W fast wireless charger with anti-slip surface",
|
|
61
|
-
category: "Mobile Accessories",
|
|
62
|
-
stock: 30,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: 7,
|
|
66
|
-
name: "Logitech MX Master 3 Mouse",
|
|
67
|
-
price: 4500000,
|
|
68
|
-
description: "Ergonomic wireless mouse with Darkfield tracking",
|
|
69
|
-
category: "Computer Accessories",
|
|
70
|
-
stock: 18,
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
id: 8,
|
|
74
|
-
name: "Kindle Paperwhite",
|
|
75
|
-
price: 3800000,
|
|
76
|
-
description: 'Waterproof e-reader with 6.8" 300ppi display',
|
|
77
|
-
category: "Electronics",
|
|
78
|
-
stock: 9,
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: 9,
|
|
82
|
-
name: "Dyson V11 Vacuum Cleaner",
|
|
83
|
-
price: 32000000,
|
|
84
|
-
description: "Cordless stick vacuum with LCD screen and 60min runtime",
|
|
85
|
-
category: "Home Appliances",
|
|
86
|
-
stock: 7,
|
|
87
|
-
},
|
|
88
|
-
];
|
|
15
|
+
import { MapAsyncCache } from "@/utilities/cache/memory-cache";
|
|
16
|
+
import { convert, stringToInteger } from "@/utilities/conversion";
|
|
17
|
+
import config from "@/config";
|
|
18
|
+
import { ResponseError } from "@/utilities/error-handling";
|
|
19
|
+
import { products } from "@/db";
|
|
20
|
+
import { Product, ZodProduct } from "@/entities/product.entity";
|
|
89
21
|
|
|
90
22
|
function exceedHandler() {
|
|
91
23
|
const message = "Too much call in allowed window";
|
|
92
|
-
|
|
93
|
-
throw new Error(message, {
|
|
94
|
-
cause: { status: 500, message } satisfies ResponseError,
|
|
95
|
-
});
|
|
24
|
+
throw new ResponseError(message, 429);
|
|
96
25
|
}
|
|
97
26
|
|
|
98
|
-
function
|
|
99
|
-
const message = "
|
|
27
|
+
function productNotFoundHandler(e: ResponseError) {
|
|
28
|
+
const message = "Product not found.";
|
|
29
|
+
throw new ResponseError(message, 404, e.message);
|
|
30
|
+
}
|
|
100
31
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
32
|
+
function invalidInputHandler(e: ResponseError) {
|
|
33
|
+
const message = "Invalid input";
|
|
34
|
+
throw new ResponseError(message, 400, e.message);
|
|
104
35
|
}
|
|
105
36
|
|
|
37
|
+
const productsCache = new MapAsyncCache<ProductDto[]>(config.cacheSize);
|
|
38
|
+
const productCache = new MapAsyncCache<ProductDto>(config.cacheSize);
|
|
39
|
+
|
|
106
40
|
/**
|
|
107
41
|
* Controller for handling product-related operations
|
|
108
42
|
* @class ProductController
|
|
109
43
|
* @desc Provides methods for product management including CRUD operations
|
|
110
44
|
*/
|
|
111
45
|
export default class ProductController {
|
|
46
|
+
// constructor(private readonly productService: ProductService) { }
|
|
47
|
+
|
|
48
|
+
@onError({
|
|
49
|
+
func: productNotFoundHandler,
|
|
50
|
+
})
|
|
51
|
+
/**
|
|
52
|
+
* Validates a string ID and converts it to a number.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} id - The ID to validate and convert.
|
|
55
|
+
* @returns {number} The numeric value of the provided ID.
|
|
56
|
+
*/
|
|
57
|
+
public async validateId(id: string): Promise<number> {
|
|
58
|
+
return stringToInteger(id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@onError({
|
|
62
|
+
func: invalidInputHandler,
|
|
63
|
+
})
|
|
64
|
+
/**
|
|
65
|
+
* Validates and creates a new Product from the given DTO.
|
|
66
|
+
*
|
|
67
|
+
* @param {ProductCreationDto} product - The incoming ProductCreationDto to validate and transform.
|
|
68
|
+
* @returns {Product} A fully formed Product object ready for persistence.
|
|
69
|
+
*/
|
|
70
|
+
public async validateProductCreationDto(
|
|
71
|
+
product: ProductCreationDto
|
|
72
|
+
): Promise<Product> {
|
|
73
|
+
const newProduct = await ZodProductCreationDto.parseAsync(product);
|
|
74
|
+
return { ...newProduct, id: products.length + 1 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@onError({
|
|
78
|
+
func: invalidInputHandler,
|
|
79
|
+
})
|
|
80
|
+
/**
|
|
81
|
+
* Validates and creates a new Product from the given DTO.
|
|
82
|
+
*
|
|
83
|
+
* @param {ProductUpdateDto} product - The incoming ProductCreationDto to validate and transform.
|
|
84
|
+
* @returns {Product} A fully formed Product object ready for persistence.
|
|
85
|
+
*/
|
|
86
|
+
public async validateProductUpdateDto(
|
|
87
|
+
product: ProductCreationDto
|
|
88
|
+
): Promise<Product> {
|
|
89
|
+
const productDto = await ZodProductUpdateDto.parseAsync(product);
|
|
90
|
+
let updatedProduct: Product = convert(productDto, ZodProduct);
|
|
91
|
+
return { ...updatedProduct, id: products.length + 1 };
|
|
92
|
+
}
|
|
93
|
+
|
|
112
94
|
@rateLimit({
|
|
113
|
-
timeSpanMs:
|
|
114
|
-
allowedCalls:
|
|
95
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
96
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
115
97
|
exceedHandler,
|
|
116
98
|
})
|
|
117
|
-
@Validate
|
|
118
99
|
/**
|
|
119
100
|
* Creates a new product with validated data
|
|
120
|
-
* @param product - Product creation data validated by Zod schema
|
|
121
|
-
* @returns
|
|
101
|
+
* @param {Product} product - Product creation data validated by Zod schema
|
|
102
|
+
* @returns {Promise<void>}
|
|
103
|
+
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
104
|
+
* @throws {ResponseError} 400 - Invalid input data
|
|
122
105
|
*/
|
|
123
|
-
public async create(
|
|
124
|
-
|
|
125
|
-
) {
|
|
126
|
-
products.push({
|
|
106
|
+
public async create(product: Product): Promise<void> {
|
|
107
|
+
const newProduct: Product = {
|
|
127
108
|
...product,
|
|
128
109
|
id: products.length + 1,
|
|
129
|
-
}
|
|
110
|
+
};
|
|
130
111
|
|
|
131
|
-
|
|
112
|
+
products.push(newProduct);
|
|
113
|
+
await productCache.set(newProduct.id.toString(), newProduct as ProductDto);
|
|
114
|
+
await productsCache.delete("key");
|
|
132
115
|
}
|
|
133
116
|
|
|
134
|
-
@
|
|
117
|
+
@memoizeAsync({
|
|
118
|
+
cache: productsCache,
|
|
119
|
+
keyResolver: () => "key",
|
|
120
|
+
expirationTimeMs: config.memoizeTime,
|
|
121
|
+
})
|
|
122
|
+
@timeout(config.timeout)
|
|
135
123
|
@rateLimit({
|
|
136
|
-
timeSpanMs:
|
|
137
|
-
allowedCalls:
|
|
124
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
125
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
138
126
|
exceedHandler,
|
|
139
127
|
})
|
|
140
128
|
/**
|
|
141
129
|
* Retrieves all products with truncated descriptions
|
|
142
130
|
* @returns List of products with summarized descriptions
|
|
143
131
|
*/
|
|
144
|
-
public async getAll(): Promise<
|
|
145
|
-
return products
|
|
146
|
-
(product) =>
|
|
147
|
-
({
|
|
148
|
-
...product,
|
|
149
|
-
description: product.description?.substring(0, 50) + "..." || "",
|
|
150
|
-
} satisfies Product)
|
|
151
|
-
);
|
|
132
|
+
public async getAll(): Promise<ProductDto[]> {
|
|
133
|
+
return products as ProductDto[];
|
|
152
134
|
}
|
|
153
135
|
|
|
136
|
+
@memoizeAsync({
|
|
137
|
+
cache: productCache,
|
|
138
|
+
keyResolver: (id: number) => id.toString(),
|
|
139
|
+
expirationTimeMs: config.memoizeTime,
|
|
140
|
+
})
|
|
154
141
|
@onError({
|
|
155
|
-
func:
|
|
142
|
+
func: productNotFoundHandler,
|
|
156
143
|
})
|
|
157
144
|
@rateLimit({
|
|
158
|
-
timeSpanMs:
|
|
159
|
-
allowedCalls:
|
|
145
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
146
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
160
147
|
exceedHandler,
|
|
161
148
|
})
|
|
162
149
|
/**
|
|
163
150
|
* Finds a product by its ID
|
|
164
|
-
* @param id - Product ID as string
|
|
151
|
+
* @param {number} id - Product ID as string
|
|
165
152
|
* @returns Product details or error object if not found
|
|
166
153
|
*/
|
|
167
|
-
public async get(id:
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
status: 404,
|
|
175
|
-
message: "Product dose not exist.",
|
|
176
|
-
} satisfies ResponseError;
|
|
177
|
-
|
|
178
|
-
return product satisfies Product;
|
|
154
|
+
public async get(id: number): Promise<ProductDto> {
|
|
155
|
+
const product = products.find((product) => product.id === id);
|
|
156
|
+
if (product == null) {
|
|
157
|
+
throw new ResponseError("Product not found");
|
|
158
|
+
}
|
|
159
|
+
return convert(product!, ZodProduct);
|
|
179
160
|
}
|
|
180
161
|
|
|
181
162
|
@rateLimit({
|
|
182
|
-
timeSpanMs:
|
|
183
|
-
allowedCalls:
|
|
163
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
164
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
184
165
|
exceedHandler,
|
|
185
166
|
})
|
|
186
|
-
@Validate
|
|
187
167
|
/**
|
|
188
168
|
* Updates an existing product
|
|
189
|
-
* @param {
|
|
169
|
+
* @param {number} id - Product ID to update
|
|
190
170
|
* @param {ProductUpdateDto} updateData - Partial product data to update
|
|
191
|
-
* @returns {Promise<Product
|
|
171
|
+
* @returns {Promise<Product>} Updated product or error object
|
|
192
172
|
* @throws {ResponseError} 404 - Product not found
|
|
193
173
|
* @throws {ResponseError} 400 - Invalid ID format or update data
|
|
194
174
|
*/
|
|
195
175
|
public async update(
|
|
196
|
-
id:
|
|
197
|
-
|
|
198
|
-
): Promise<
|
|
176
|
+
id: number,
|
|
177
|
+
updateData: ProductUpdateDto
|
|
178
|
+
): Promise<ProductDto> {
|
|
199
179
|
const product = await this.get(id);
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
} satisfies ResponseError;
|
|
180
|
+
if (product != null) {
|
|
181
|
+
Object.assign(product, updateData);
|
|
182
|
+
await productCache.set(id.toString(), product);
|
|
183
|
+
await productsCache.delete("key");
|
|
184
|
+
} else {
|
|
185
|
+
throw new ResponseError("Product dose not exist.", 404);
|
|
186
|
+
}
|
|
208
187
|
|
|
209
188
|
return product;
|
|
210
189
|
}
|
|
211
190
|
|
|
212
191
|
@rateLimit({
|
|
213
|
-
timeSpanMs:
|
|
214
|
-
allowedCalls:
|
|
192
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
193
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
215
194
|
exceedHandler,
|
|
216
195
|
})
|
|
217
196
|
/**
|
|
218
197
|
* Deletes a product by ID
|
|
219
|
-
* @param {
|
|
220
|
-
* @returns {Promise<Product
|
|
198
|
+
* @param {number} id - Product ID to delete
|
|
199
|
+
* @returns {Promise<Product>} Deleted product or error object
|
|
221
200
|
* @throws {ResponseError} 404 - Product not found
|
|
222
201
|
* @throws {ResponseError} 400 - Invalid ID format
|
|
223
202
|
*/
|
|
224
|
-
public async delete(id:
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
message: "Product dose not exist.",
|
|
233
|
-
} satisfies ResponseError;
|
|
234
|
-
|
|
235
|
-
return products.splice(index, 1)[0] satisfies Product;
|
|
203
|
+
public async delete(id: number): Promise<ProductDto> {
|
|
204
|
+
const index = products.findIndex((product) => product.id === id);
|
|
205
|
+
if (index == -1) {
|
|
206
|
+
throw new ResponseError("Product dose not exist.", 404);
|
|
207
|
+
}
|
|
208
|
+
await productCache.delete(id.toString());
|
|
209
|
+
await productsCache.delete("key");
|
|
210
|
+
return products.splice(index, 1)[0];
|
|
236
211
|
}
|
|
237
212
|
}
|
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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(),
|
|
13
|
-
username: z.string().min(3),
|
|
14
|
-
email: z.string().email(),
|
|
15
|
-
password: z.string().min(6),
|
|
16
|
-
});
|
|
1
|
+
import { ZodUser } from "@/entities/user.entity";
|
|
2
|
+
import z from "zod";
|
|
17
3
|
|
|
18
4
|
export const ZodUserCreationDto = ZodUser.omit({ id: true });
|
|
5
|
+
export const ZodUserDto = ZodUser.omit({ password: true });
|
|
6
|
+
|
|
7
|
+
export type UserCreationDto = {
|
|
8
|
+
username: string;
|
|
9
|
+
email: string;
|
|
10
|
+
password: string;
|
|
11
|
+
};
|
|
19
12
|
|
|
20
|
-
export type
|
|
21
|
-
|
|
13
|
+
export type UserDto = {
|
|
14
|
+
id: number;
|
|
15
|
+
username: string;
|
|
16
|
+
email: string;
|
|
17
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
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 { sendError } from "@/utilities";
|
|
5
4
|
|
|
6
5
|
const router = Router();
|
|
7
6
|
const userController = new UserController();
|
|
@@ -46,8 +45,9 @@ const userController = new UserController();
|
|
|
46
45
|
router.post(
|
|
47
46
|
"/",
|
|
48
47
|
asyncHandler(async (req: Request, res: Response) => {
|
|
49
|
-
const user = await userController.
|
|
50
|
-
|
|
48
|
+
const user = await userController.validateUserCreationDto(req.body);
|
|
49
|
+
await userController.create(user);
|
|
50
|
+
res.status(201).send();
|
|
51
51
|
})
|
|
52
52
|
);
|
|
53
53
|
|
|
@@ -72,10 +72,9 @@ router.post(
|
|
|
72
72
|
router.get(
|
|
73
73
|
"/:id",
|
|
74
74
|
asyncHandler(async (req: Request, res: Response) => {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
else res.json(user);
|
|
75
|
+
const id = await userController.validateId(req.params.id);
|
|
76
|
+
const user = await userController.get(id);
|
|
77
|
+
res.json(user);
|
|
79
78
|
})
|
|
80
79
|
);
|
|
81
80
|
|