codeweaver 2.0.0 → 2.2.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
+ };
@@ -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 = new ProductController();
7
+ const productController = resolve(ProductController);
7
8
 
8
9
  // CRUD Routes
9
10
 
@@ -63,8 +64,11 @@ const productController = new ProductController();
63
64
  router.post(
64
65
  "/",
65
66
  asyncHandler(async (req: Request, res: Response) => {
66
- const product = await productController.create(req.body);
67
- res.status(201).json(product);
67
+ const product = await productController.validateProductCreationDto(
68
+ req.body
69
+ );
70
+ await productController.create(product);
71
+ res.status(201).send();
68
72
  })
69
73
  );
70
74
 
@@ -103,7 +107,8 @@ router.get(
103
107
  router.get(
104
108
  "/:id",
105
109
  asyncHandler(async (req: Request, res: Response) => {
106
- const product = await productController.get(req.params.id);
110
+ const id = await productController.validateId(req.params.id);
111
+ const product = await productController.get(id);
107
112
  res.json(product);
108
113
  })
109
114
  );
@@ -164,7 +169,8 @@ router.get(
164
169
  router.put(
165
170
  "/:id",
166
171
  asyncHandler(async (req: Request, res: Response) => {
167
- const product = await productController.update(req.params.id, req.body);
172
+ const id = await productController.validateId(req.params.id);
173
+ const product = await productController.update(id, req.body);
168
174
  res.json(product);
169
175
  })
170
176
  );
@@ -192,7 +198,8 @@ router.put(
192
198
  router.delete(
193
199
  "/:id",
194
200
  asyncHandler(async (req: Request, res: Response) => {
195
- const product = await productController.delete(req.params.id);
201
+ const id = await productController.validateId(req.params.id);
202
+ const product = await productController.delete(id);
196
203
  res.json(product);
197
204
  })
198
205
  );
@@ -1,133 +1,121 @@
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";
21
+ import { Injectable } from "@/utilities/container";
91
22
 
92
23
  function exceedHandler() {
93
24
  const message = "Too much call in allowed window";
94
25
  throw new ResponseError(message, 429);
95
26
  }
96
27
 
97
- function getProductErrorHandler(e: Error) {
98
- const message = "Product not found.";
99
- throw new ResponseError(message, 404, e.message);
28
+ function invalidInputHandler(e: ResponseError) {
29
+ const message = "Invalid input";
30
+ throw new ResponseError(message, 400, e.message);
100
31
  }
101
32
 
33
+ const productsCache = new MapAsyncCache<ProductDto[]>(config.cacheSize);
34
+ const productCache = new MapAsyncCache<ProductDto>(config.cacheSize);
35
+
36
+ @Injectable()
102
37
  /**
103
38
  * Controller for handling product-related operations
104
39
  * @class ProductController
105
40
  * @desc Provides methods for product management including CRUD operations
106
41
  */
107
42
  export default class ProductController {
43
+ // constructor(private readonly productService: ProductService) { }
44
+
45
+ @onError({
46
+ func: invalidInputHandler,
47
+ })
48
+ /**
49
+ * Validates a string ID and converts it to a number.
50
+ *
51
+ * @param {string} id - The ID to validate and convert.
52
+ * @returns {number} The numeric value of the provided ID.
53
+ */
54
+ public async validateId(id: string): Promise<number> {
55
+ return stringToInteger(id);
56
+ }
57
+
58
+ @onError({
59
+ func: invalidInputHandler,
60
+ })
61
+ /**
62
+ * Validates and creates a new Product from the given DTO.
63
+ *
64
+ * @param {ProductCreationDto} product - The incoming ProductCreationDto to validate and transform.
65
+ * @returns {Product} A fully formed Product object ready for persistence.
66
+ */
67
+ public async validateProductCreationDto(
68
+ product: ProductCreationDto
69
+ ): Promise<Product> {
70
+ const newProduct = await ZodProductCreationDto.parseAsync(product);
71
+ return { ...newProduct, id: products.length + 1 };
72
+ }
73
+
74
+ @onError({
75
+ func: invalidInputHandler,
76
+ })
77
+ /**
78
+ * Validates and creates a new Product from the given DTO.
79
+ *
80
+ * @param {ProductUpdateDto} product - The incoming ProductCreationDto to validate and transform.
81
+ * @returns {Product} A fully formed Product object ready for persistence.
82
+ */
83
+ public async validateProductUpdateDto(
84
+ product: ProductCreationDto
85
+ ): Promise<Product> {
86
+ const productDto = await ZodProductUpdateDto.parseAsync(product);
87
+ let updatedProduct: Product = convert(productDto, ZodProduct);
88
+ return { ...updatedProduct, id: products.length + 1 };
89
+ }
90
+
108
91
  @rateLimit({
109
92
  timeSpanMs: config.rateLimitTimeSpan,
110
93
  allowedCalls: config.rateLimitAllowedCalls,
111
94
  exceedHandler,
112
95
  })
113
- @Validate
114
96
  /**
115
97
  * 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
98
+ * @param {Product} product - Product creation data validated by Zod schema
99
+ * @returns {Promise<void>}
100
+ * @throws {ResponseError} 500 - When rate limit exceeded
101
+ * @throws {ResponseError} 400 - Invalid input data
118
102
  */
119
- public async create(
120
- @ZodInput(ZodProductCreationDto) product: ProductCreationDto
121
- ): Promise<ProductDto> {
122
- products.push({
103
+ public async create(product: Product): Promise<void> {
104
+ const newProduct: Product = {
123
105
  ...product,
124
106
  id: products.length + 1,
125
- } satisfies Product);
107
+ };
126
108
 
127
- return product as ProductDto;
109
+ products.push(newProduct);
110
+ await productCache.set(newProduct.id.toString(), newProduct as ProductDto);
111
+ await productsCache.delete("key");
128
112
  }
129
113
 
130
- @memoizeAsync(config.memoizeTime)
114
+ @memoizeAsync({
115
+ cache: productsCache,
116
+ keyResolver: () => "key",
117
+ expirationTimeMs: config.memoizeTime,
118
+ })
131
119
  @timeout(config.timeout)
132
120
  @rateLimit({
133
121
  timeSpanMs: config.rateLimitTimeSpan,
@@ -139,15 +127,13 @@ export default class ProductController {
139
127
  * @returns List of products with summarized descriptions
140
128
  */
141
129
  public async getAll(): Promise<ProductDto[]> {
142
- return products.map((product) => ({
143
- ...product,
144
- description: product.description?.substring(0, 50) + "..." || "",
145
- }));
130
+ return products as ProductDto[];
146
131
  }
147
132
 
148
- @memoizeAsync(config.memoizeTime)
149
- @onError({
150
- func: getProductErrorHandler,
133
+ @memoizeAsync({
134
+ cache: productCache,
135
+ keyResolver: (id: number) => id.toString(),
136
+ expirationTimeMs: config.memoizeTime,
151
137
  })
152
138
  @rateLimit({
153
139
  timeSpanMs: config.rateLimitTimeSpan,
@@ -156,14 +142,15 @@ export default class ProductController {
156
142
  })
157
143
  /**
158
144
  * Finds a product by its ID
159
- * @param id - Product ID as string
145
+ * @param {number} id - Product ID as string
160
146
  * @returns Product details or error object if not found
161
147
  */
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;
148
+ public async get(id: number): Promise<ProductDto> {
149
+ const product = products.find((product) => product.id === id);
150
+ if (product == null) {
151
+ throw new ResponseError("Product not found");
152
+ }
153
+ return convert(product!, ZodProduct);
167
154
  }
168
155
 
169
156
  @rateLimit({
@@ -171,24 +158,23 @@ export default class ProductController {
171
158
  allowedCalls: config.rateLimitAllowedCalls,
172
159
  exceedHandler,
173
160
  })
174
- @Validate
175
161
  /**
176
162
  * Updates an existing product
177
- * @param {string} id - Product ID to update
163
+ * @param {number} id - Product ID to update
178
164
  * @param {ProductUpdateDto} updateData - Partial product data to update
179
165
  * @returns {Promise<Product>} Updated product or error object
180
166
  * @throws {ResponseError} 404 - Product not found
181
167
  * @throws {ResponseError} 400 - Invalid ID format or update data
182
168
  */
183
169
  public async update(
184
- id: string,
185
- @ZodInput(ZodProductUpdateDto) updateData: ProductUpdateDto
170
+ id: number,
171
+ updateData: ProductUpdateDto
186
172
  ): Promise<ProductDto> {
187
173
  const product = await this.get(id);
188
- if ("id" in product == false) return product satisfies ResponseError;
189
-
190
- if (product) {
174
+ if (product != null) {
191
175
  Object.assign(product, updateData);
176
+ await productCache.set(id.toString(), product);
177
+ await productsCache.delete("key");
192
178
  } else {
193
179
  throw new ResponseError("Product dose not exist.", 404);
194
180
  }
@@ -203,15 +189,18 @@ export default class ProductController {
203
189
  })
204
190
  /**
205
191
  * Deletes a product by ID
206
- * @param {string} id - Product ID to delete
192
+ * @param {number} id - Product ID to delete
207
193
  * @returns {Promise<Product>} Deleted product or error object
208
194
  * @throws {ResponseError} 404 - Product not found
209
195
  * @throws {ResponseError} 400 - Invalid ID format
210
196
  */
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);
197
+ public async delete(id: number): Promise<ProductDto> {
198
+ const index = products.findIndex((product) => product.id === id);
199
+ if (index == -1) {
200
+ throw new ResponseError("Product dose not exist.", 404);
201
+ }
202
+ await productCache.delete(id.toString());
203
+ await productsCache.delete("key");
215
204
  return products.splice(index, 1)[0];
216
205
  }
217
206
  }
@@ -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
+ };
@@ -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 = new UserController();
7
+ const userController = resolve(UserController);
7
8
 
8
9
  /**
9
10
  * @swagger
@@ -45,8 +46,9 @@ const userController = new UserController();
45
46
  router.post(
46
47
  "/",
47
48
  asyncHandler(async (req: Request, res: Response) => {
48
- const user = await userController.create(req.body);
49
- res.status(201).json(user);
49
+ const user = await userController.validateUserCreationDto(req.body);
50
+ await userController.create(user);
51
+ res.status(201).send();
50
52
  })
51
53
  );
52
54
 
@@ -71,7 +73,8 @@ router.post(
71
73
  router.get(
72
74
  "/:id",
73
75
  asyncHandler(async (req: Request, res: Response) => {
74
- const user = await userController.get(req.params.id);
76
+ const id = await userController.validateId(req.params.id);
77
+ const user = await userController.get(id);
75
78
  res.json(user);
76
79
  })
77
80
  );