codeweaver 2.0.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.
@@ -1,37 +1,92 @@
1
- import { z } from "zod";
1
+ import { ZodProduct } from "@/entities/product.entity";
2
+ import z from "zod";
2
3
 
3
4
  /**
4
- * Zod schema for Product entity
5
- * @typedef {Object} ZodProduct
6
- * @property {number} id - Unique identifier (min 1)
7
- * @property {string} name - Product name (min 2 chars)
8
- * @property {number} price - Product price (min 1000)
9
- * @property {string} [description] - Optional description (min 10 chars)
10
- * @property {string} category - Product category from predefined enum
11
- * @property {number} stock - Available stock quantity (min 0)
5
+ * DTO for creating a Product.
6
+ * Derived from the full Product schema by omitting the system-generated id.
12
7
  */
13
- export const ZodProduct = z.object({
14
- id: z.number().min(1).int(),
15
- name: z.string().min(2),
16
- price: z.number().min(1000),
17
- description: z.string().min(10).optional(),
18
- category: z.enum([
19
- "Electronics",
20
- "Appliances",
21
- "Sports",
22
- "Kitchen",
23
- "Mobile Accessories",
24
- "Computer Accessories",
25
- "Home Appliances",
26
- "Books",
27
- ]),
28
- stock: z.number().min(0).int(),
29
- });
30
-
31
8
  export const ZodProductCreationDto = ZodProduct.omit({ id: true });
9
+
10
+ /**
11
+ * DTO for updating a Product.
12
+ * All fields are optional to support partial updates (PATCH semantics).
13
+ */
32
14
  export const ZodProductUpdateDto = ZodProductCreationDto.partial();
33
15
 
34
- export type Product = z.infer<typeof ZodProduct>;
35
- export type ProductCreationDto = z.infer<typeof ZodProductCreationDto>;
36
- export type ProductUpdateDto = z.infer<typeof ZodProductUpdateDto>;
37
- export type ProductDto = z.infer<typeof ZodProduct>;
16
+ /**
17
+ * Product categories supported by the system.
18
+ * Centralized to avoid repeating the union type in multiple places.
19
+ */
20
+ export type ProductCategory =
21
+ | "Electronics"
22
+ | "Appliances"
23
+ | "Sports"
24
+ | "Kitchen"
25
+ | "Mobile Accessories"
26
+ | "Computer Accessories"
27
+ | "Home Appliances"
28
+ | "Books";
29
+
30
+ /**
31
+ * Data required to create a Product.
32
+ */
33
+ export type ProductCreationDto = {
34
+ /** Product name. */
35
+ name: string;
36
+
37
+ /** Product price. */
38
+ price: number;
39
+
40
+ /** Category from the predefined list. */
41
+ category: ProductCategory;
42
+
43
+ /** Stock count in inventory. Non-negative. */
44
+ stock: number;
45
+
46
+ /** Optional product description. */
47
+ description?: string | undefined;
48
+ };
49
+
50
+ /**
51
+ * Data for updating a Product.
52
+ * All fields are optional to support partial updates.
53
+ */
54
+ export type ProductUpdateDto = {
55
+ /** Optional product name. */
56
+ name?: string | undefined;
57
+
58
+ /** Optional product price. */
59
+ price?: number | undefined;
60
+
61
+ /** Optional product description. */
62
+ description?: string | undefined;
63
+
64
+ /** Optional product category. Must be one of the predefined categories if provided. */
65
+ category?: ProductCategory | undefined;
66
+
67
+ /** Optional stock count in inventory. */
68
+ stock?: number | undefined;
69
+ };
70
+
71
+ /**
72
+ * Data Transfer Object representing a full Product (as returned by APIs, etc.).
73
+ */
74
+ export type ProductDto = {
75
+ /** Unique identifier for the product. */
76
+ id: number;
77
+
78
+ /** Product name. */
79
+ name: string;
80
+
81
+ /** Product price. */
82
+ price: number;
83
+
84
+ /** Category of the product. */
85
+ category: ProductCategory;
86
+
87
+ /** Stock count in inventory. */
88
+ stock: number;
89
+
90
+ /** Optional product description. */
91
+ description?: string | undefined;
92
+ };
@@ -63,8 +63,11 @@ const productController = new ProductController();
63
63
  router.post(
64
64
  "/",
65
65
  asyncHandler(async (req: Request, res: Response) => {
66
- const product = await productController.create(req.body);
67
- res.status(201).json(product);
66
+ const product = await productController.validateProductCreationDto(
67
+ req.body
68
+ );
69
+ await productController.create(product);
70
+ res.status(201).send();
68
71
  })
69
72
  );
70
73
 
@@ -103,7 +106,8 @@ router.get(
103
106
  router.get(
104
107
  "/:id",
105
108
  asyncHandler(async (req: Request, res: Response) => {
106
- const product = await productController.get(req.params.id);
109
+ const id = await productController.validateId(req.params.id);
110
+ const product = await productController.get(id);
107
111
  res.json(product);
108
112
  })
109
113
  );
@@ -164,7 +168,8 @@ router.get(
164
168
  router.put(
165
169
  "/:id",
166
170
  asyncHandler(async (req: Request, res: Response) => {
167
- const product = await productController.update(req.params.id, req.body);
171
+ const id = await productController.validateId(req.params.id);
172
+ const product = await productController.update(id, req.body);
168
173
  res.json(product);
169
174
  })
170
175
  );
@@ -192,7 +197,8 @@ router.put(
192
197
  router.delete(
193
198
  "/:id",
194
199
  asyncHandler(async (req: Request, res: Response) => {
195
- const product = await productController.delete(req.params.id);
200
+ const id = await productController.validateId(req.params.id);
201
+ const product = await productController.delete(id);
196
202
  res.json(product);
197
203
  })
198
204
  );
@@ -1,133 +1,124 @@
1
- import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
2
1
  import {
3
- Product,
2
+ memoizeAsync,
3
+ onError,
4
+ rateLimit,
5
+ timeout,
6
+ before,
7
+ } from "utils-decorators";
8
+ import {
4
9
  ProductCreationDto,
5
10
  ProductDto,
6
11
  ProductUpdateDto,
7
12
  ZodProductCreationDto,
8
13
  ZodProductUpdateDto,
9
14
  } 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";
15
+ import { MapAsyncCache } from "@/utilities/cache/memory-cache";
16
+ import { convert, stringToInteger } from "@/utilities/conversion";
13
17
  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
- ];
18
+ import { ResponseError } from "@/utilities/error-handling";
19
+ import { products } from "@/db";
20
+ import { Product, ZodProduct } from "@/entities/product.entity";
91
21
 
92
22
  function exceedHandler() {
93
23
  const message = "Too much call in allowed window";
94
24
  throw new ResponseError(message, 429);
95
25
  }
96
26
 
97
- function getProductErrorHandler(e: Error) {
27
+ function productNotFoundHandler(e: ResponseError) {
98
28
  const message = "Product not found.";
99
29
  throw new ResponseError(message, 404, e.message);
100
30
  }
101
31
 
32
+ function invalidInputHandler(e: ResponseError) {
33
+ const message = "Invalid input";
34
+ throw new ResponseError(message, 400, e.message);
35
+ }
36
+
37
+ const productsCache = new MapAsyncCache<ProductDto[]>(config.cacheSize);
38
+ const productCache = new MapAsyncCache<ProductDto>(config.cacheSize);
39
+
102
40
  /**
103
41
  * Controller for handling product-related operations
104
42
  * @class ProductController
105
43
  * @desc Provides methods for product management including CRUD operations
106
44
  */
107
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
+
108
94
  @rateLimit({
109
95
  timeSpanMs: config.rateLimitTimeSpan,
110
96
  allowedCalls: config.rateLimitAllowedCalls,
111
97
  exceedHandler,
112
98
  })
113
- @Validate
114
99
  /**
115
100
  * 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
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
118
105
  */
119
- public async create(
120
- @ZodInput(ZodProductCreationDto) product: ProductCreationDto
121
- ): Promise<ProductDto> {
122
- products.push({
106
+ public async create(product: Product): Promise<void> {
107
+ const newProduct: Product = {
123
108
  ...product,
124
109
  id: products.length + 1,
125
- } satisfies Product);
110
+ };
126
111
 
127
- return product as ProductDto;
112
+ products.push(newProduct);
113
+ await productCache.set(newProduct.id.toString(), newProduct as ProductDto);
114
+ await productsCache.delete("key");
128
115
  }
129
116
 
130
- @memoizeAsync(config.memoizeTime)
117
+ @memoizeAsync({
118
+ cache: productsCache,
119
+ keyResolver: () => "key",
120
+ expirationTimeMs: config.memoizeTime,
121
+ })
131
122
  @timeout(config.timeout)
132
123
  @rateLimit({
133
124
  timeSpanMs: config.rateLimitTimeSpan,
@@ -139,15 +130,16 @@ export default class ProductController {
139
130
  * @returns List of products with summarized descriptions
140
131
  */
141
132
  public async getAll(): Promise<ProductDto[]> {
142
- return products.map((product) => ({
143
- ...product,
144
- description: product.description?.substring(0, 50) + "..." || "",
145
- }));
133
+ return products as ProductDto[];
146
134
  }
147
135
 
148
- @memoizeAsync(config.memoizeTime)
136
+ @memoizeAsync({
137
+ cache: productCache,
138
+ keyResolver: (id: number) => id.toString(),
139
+ expirationTimeMs: config.memoizeTime,
140
+ })
149
141
  @onError({
150
- func: getProductErrorHandler,
142
+ func: productNotFoundHandler,
151
143
  })
152
144
  @rateLimit({
153
145
  timeSpanMs: config.rateLimitTimeSpan,
@@ -156,14 +148,15 @@ export default class ProductController {
156
148
  })
157
149
  /**
158
150
  * Finds a product by its ID
159
- * @param id - Product ID as string
151
+ * @param {number} id - Product ID as string
160
152
  * @returns Product details or error object if not found
161
153
  */
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;
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);
167
160
  }
168
161
 
169
162
  @rateLimit({
@@ -171,24 +164,23 @@ export default class ProductController {
171
164
  allowedCalls: config.rateLimitAllowedCalls,
172
165
  exceedHandler,
173
166
  })
174
- @Validate
175
167
  /**
176
168
  * Updates an existing product
177
- * @param {string} id - Product ID to update
169
+ * @param {number} id - Product ID to update
178
170
  * @param {ProductUpdateDto} updateData - Partial product data to update
179
171
  * @returns {Promise<Product>} Updated product or error object
180
172
  * @throws {ResponseError} 404 - Product not found
181
173
  * @throws {ResponseError} 400 - Invalid ID format or update data
182
174
  */
183
175
  public async update(
184
- id: string,
185
- @ZodInput(ZodProductUpdateDto) updateData: ProductUpdateDto
176
+ id: number,
177
+ updateData: ProductUpdateDto
186
178
  ): Promise<ProductDto> {
187
179
  const product = await this.get(id);
188
- if ("id" in product == false) return product satisfies ResponseError;
189
-
190
- if (product) {
180
+ if (product != null) {
191
181
  Object.assign(product, updateData);
182
+ await productCache.set(id.toString(), product);
183
+ await productsCache.delete("key");
192
184
  } else {
193
185
  throw new ResponseError("Product dose not exist.", 404);
194
186
  }
@@ -203,15 +195,18 @@ export default class ProductController {
203
195
  })
204
196
  /**
205
197
  * Deletes a product by ID
206
- * @param {string} id - Product ID to delete
198
+ * @param {number} id - Product ID to delete
207
199
  * @returns {Promise<Product>} Deleted product or error object
208
200
  * @throws {ResponseError} 404 - Product not found
209
201
  * @throws {ResponseError} 400 - Invalid ID format
210
202
  */
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);
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");
215
210
  return products.splice(index, 1)[0];
216
211
  }
217
212
  }
@@ -1,23 +1,17 @@
1
- import { z } from "zod";
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.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 });
19
5
  export const ZodUserDto = ZodUser.omit({ password: true });
20
6
 
21
- export type User = z.infer<typeof ZodUser>;
22
- export type UserCreationDto = z.infer<typeof ZodUserCreationDto>;
23
- export type UserDto = z.infer<typeof ZodUserDto>;
7
+ export type UserCreationDto = {
8
+ username: string;
9
+ email: string;
10
+ password: string;
11
+ };
12
+
13
+ export type UserDto = {
14
+ id: number;
15
+ username: string;
16
+ email: string;
17
+ };
@@ -45,8 +45,9 @@ const userController = new UserController();
45
45
  router.post(
46
46
  "/",
47
47
  asyncHandler(async (req: Request, res: Response) => {
48
- const user = await userController.create(req.body);
49
- res.status(201).json(user);
48
+ const user = await userController.validateUserCreationDto(req.body);
49
+ await userController.create(user);
50
+ res.status(201).send();
50
51
  })
51
52
  );
52
53
 
@@ -71,7 +72,8 @@ router.post(
71
72
  router.get(
72
73
  "/:id",
73
74
  asyncHandler(async (req: Request, res: Response) => {
74
- const user = await userController.get(req.params.id);
75
+ const id = await userController.validateId(req.params.id);
76
+ const user = await userController.get(id);
75
77
  res.json(user);
76
78
  })
77
79
  );