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.
Files changed (37) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/README.md +114 -181
  3. package/package.json +12 -7
  4. package/src/app.ts +2 -112
  5. package/src/config.ts +33 -64
  6. package/src/constants.ts +7 -0
  7. package/src/db.ts +183 -0
  8. package/src/entities/order.entity.ts +68 -0
  9. package/src/entities/product.entity.ts +75 -0
  10. package/src/entities/user.entity.ts +38 -0
  11. package/src/main.ts +85 -0
  12. package/src/routers/orders/dto/order.dto.ts +54 -29
  13. package/src/routers/orders/{index.ts → index.router.ts} +13 -13
  14. package/src/routers/orders/order.controller.ts +118 -120
  15. package/src/routers/products/dto/product.dto.ts +86 -30
  16. package/src/routers/products/{index.ts → index.router.ts} +14 -15
  17. package/src/routers/products/product.controller.ts +136 -161
  18. package/src/routers/users/dto/user.dto.ts +14 -18
  19. package/src/routers/users/{index.ts → index.router.ts} +6 -7
  20. package/src/routers/users/user.controller.ts +87 -118
  21. package/src/swagger-options.ts +39 -0
  22. package/src/utilities/assign.ts +66 -0
  23. package/src/utilities/cache/memory-cache.ts +74 -0
  24. package/src/utilities/cache/redis-cache.ts +111 -0
  25. package/src/utilities/conversion.ts +158 -0
  26. package/src/utilities/error-handling.ts +156 -0
  27. package/src/utilities/router.ts +0 -0
  28. package/tsconfig.json +1 -4
  29. package/tsconfig.paths.json +8 -10
  30. package/src/packages/ts-zod-decorators/index.ts +0 -3
  31. package/src/packages/ts-zod-decorators/validate.decorator.ts +0 -20
  32. package/src/packages/ts-zod-decorators/validator.class.ts +0 -72
  33. package/src/packages/ts-zod-decorators/zod-input.decorator.ts +0 -12
  34. package/src/packages/ts-zod-decorators/zod-output.decorator.ts +0 -11
  35. package/src/types.ts +0 -16
  36. package/src/utilities.ts +0 -47
  37. /package/src/routers/{index.ts → index.router.ts} +0 -0
@@ -1,7 +1,6 @@
1
1
  import { Router, Request, Response } from "express";
2
2
  import asyncHandler from "express-async-handler";
3
3
  import OrderController from "./order.controller";
4
- import { sendError } from "@/utilities";
5
4
 
6
5
  const router = Router();
7
6
  const orderController = new OrderController();
@@ -11,7 +10,7 @@ const orderController = new OrderController();
11
10
  * /orders:
12
11
  * post:
13
12
  * summary: Create a new order
14
- * description: Creates an order with user details and products.
13
+ * description: Creates an order with order details and products.
15
14
  * consumes:
16
15
  * - application/json
17
16
  * produces:
@@ -59,8 +58,9 @@ const orderController = new OrderController();
59
58
  router.post(
60
59
  "/",
61
60
  asyncHandler(async (req: Request, res: Response) => {
62
- const order = await orderController.create(req.body);
63
- res.status(201).json(order);
61
+ const order = await orderController.validateOrderCreationDto(req.body);
62
+ await orderController.create(order);
63
+ res.status(201).send();;
64
64
  })
65
65
  );
66
66
 
@@ -100,9 +100,9 @@ router.get(
100
100
  router.get(
101
101
  "/:id",
102
102
  asyncHandler(async (req: Request, res: Response) => {
103
- const order = await orderController.get(req.params.id);
104
- if ("id" in order == false) sendError(res, order);
105
- else res.json(order);
103
+ const id = await orderController.validateId(req.params.id);
104
+ const order = await orderController.get(id);
105
+ res.json(order);
106
106
  })
107
107
  );
108
108
 
@@ -125,9 +125,9 @@ router.get(
125
125
  router.patch(
126
126
  "/:id/cancel",
127
127
  asyncHandler(async (req: Request, res: Response) => {
128
- const order = await orderController.cancel(req.params.id);
129
- if ("id" in order == false) sendError(res, order);
130
- else res.json(order);
128
+ const id = await orderController.validateId(req.params.id);
129
+ const order = await orderController.cancel(id);
130
+ res.json(order);
131
131
  })
132
132
  );
133
133
 
@@ -152,9 +152,9 @@ router.patch(
152
152
  router.patch(
153
153
  "/:id/deliver",
154
154
  asyncHandler(async (req: Request, res: Response) => {
155
- const order = await orderController.deliver(req.params.id);
156
- if ("id" in order == false) sendError(res, order);
157
- else res.json(order);
155
+ const id = await orderController.validateId(req.params.id);
156
+ const order = await orderController.deliver(id);
157
+ res.json(order);
158
158
  })
159
159
  );
160
160
 
@@ -1,191 +1,189 @@
1
- import { onError, rateLimit, timeout } from "utils-decorators";
1
+ import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
2
2
  import {
3
- Order,
3
+ OrderDto,
4
4
  OrderCreationDto,
5
5
  ZodOrderCreationDto,
6
6
  } from "./dto/order.dto";
7
- import { Validate, ZodInput } from "@pkg/ts-zod-decorators";
8
- import { ResponseError } from "@/types";
9
- import { tryParseId } from "@/utilities";
10
-
11
- // Array to store orders (as a mock database)
12
- const orders: Order[] = [
13
- {
14
- id: 1,
15
- userId: 1,
16
- products: [
17
- { productId: 2, quantity: 1 },
18
- { productId: 6, quantity: 2 },
19
- ],
20
- status: "Delivered",
21
- total: 14400,
22
- createdAt: new Date("2024-01-15"),
23
- deliveredAt: new Date("2024-02-10"),
24
- },
25
- {
26
- id: 2,
27
- userId: 3,
28
- products: [
29
- { productId: 9, quantity: 1 },
30
- { productId: 7, quantity: 1 },
31
- ],
32
- status: "Processing",
33
- total: 36500,
34
- createdAt: new Date("2024-03-20"),
35
- },
36
- {
37
- id: 3,
38
- userId: 2,
39
- products: [
40
- { productId: 1, quantity: 1 },
41
- { productId: 4, quantity: 2 },
42
- ],
43
- status: "Canceled",
44
- total: 81000,
45
- createdAt: new Date("2024-05-01"),
46
- canceledAt: new Date("2024-05-03"),
47
- },
48
- ];
7
+ import { ResponseError } from "@/utilities/error-handling";
8
+ import { convert, stringToInteger } from "@/utilities/conversion";
9
+ import config from "@/config";
10
+ import { orders } from "@/db";
11
+ import { Order, ZodOrder } from "@/entities/order.entity";
12
+ import { MapAsyncCache } from "@/utilities/cache/memory-cache";
49
13
 
50
14
  function exceedHandler() {
51
15
  const message = "Too much call in allowed window";
52
-
53
- throw new Error(message, {
54
- cause: { status: 500, message } satisfies ResponseError,
55
- });
16
+ throw new ResponseError(message, 429);
56
17
  }
57
18
 
58
- function getOrderErrorHandler(e: Error) {
19
+ function orderNotFoundHandler(e: ResponseError) {
59
20
  const message = "Order not found.";
21
+ throw new ResponseError(message, 404, e.message);
22
+ }
60
23
 
61
- throw new Error(message, {
62
- cause: { status: 404, message, details: e.message } satisfies ResponseError,
63
- });
24
+ function invalidInputHandler(e: ResponseError) {
25
+ const message = "Invalid input";
26
+ throw new ResponseError(message, 400, e.message);
64
27
  }
65
28
 
29
+ const ordersCache = new MapAsyncCache<OrderDto[]>(config.cacheSize);
30
+ const orderCache = new MapAsyncCache<OrderDto>(config.cacheSize);
31
+
66
32
  /**
67
33
  * Controller for handling order-related operations
68
34
  * @class OrderController
69
35
  * @desc Provides methods for order management including creation, status updates and retrieval
70
36
  */
71
37
  export default class OrderController {
72
- @rateLimit({
73
- timeSpanMs: 60000,
74
- allowedCalls: 300,
75
- exceedHandler,
38
+ // constructor(private readonly orderService: OrderService) { }
39
+
40
+ @onError({
41
+ func: orderNotFoundHandler,
76
42
  })
77
- @Validate
78
43
  /**
79
- * Create a new order
80
- * @param {OrderCreationDto} order - Order creation data
81
- * @returns {Promise<Order>} Newly created order
44
+ * Validates a string ID and converts it to a number.
45
+ *
46
+ * @param {string} id - The ID to validate and convert.
47
+ * @returns {number} The numeric value of the provided ID.
82
48
  */
83
- public async create(@ZodInput(ZodOrderCreationDto) order: OrderCreationDto) {
84
- const newOrder = {
85
- ...order,
49
+ public async validateId(id: string): Promise<number> {
50
+ return stringToInteger(id);
51
+ }
52
+
53
+ @onError({
54
+ func: invalidInputHandler,
55
+ })
56
+ /**
57
+ * Validates and creates a new Order from the given DTO.
58
+ *
59
+ * @param {OrderCreationDto} order - The incoming OrderCreationDto to validate and transform.
60
+ * @returns {Order} A fully formed Order object ready for persistence.
61
+ */
62
+ public async validateOrderCreationDto(
63
+ order: OrderCreationDto
64
+ ): Promise<Order> {
65
+ const newOrder = await ZodOrderCreationDto.parseAsync(order);
66
+ return {
67
+ ...newOrder,
86
68
  id: orders.length + 1,
87
- createdAt: new Date(),
88
69
  status: "Processing",
89
- } satisfies Order;
70
+ createdAt: new Date(),
71
+ };
72
+ }
90
73
 
91
- orders.push(newOrder);
92
- return newOrder;
74
+ @rateLimit({
75
+ timeSpanMs: config.rateLimitTimeSpan,
76
+ allowedCalls: config.rateLimitAllowedCalls,
77
+ exceedHandler,
78
+ })
79
+ /**
80
+ * Create a new order
81
+ * @param {Order} order - Order creation data
82
+ * @returns {Promise<void>}
83
+ * @throws {ResponseError} 500 - When rate limit exceeded
84
+ * @throws {ResponseError} 400 - Invalid input data
85
+ */
86
+ public async create(order: Order): Promise<void> {
87
+ orders.push(order);
88
+ await orderCache.set(order.id.toString(), order as OrderDto);
89
+ await ordersCache.delete("key");
93
90
  }
94
91
 
95
- @timeout(20000)
92
+ @memoizeAsync({
93
+ cache: ordersCache,
94
+ keyResolver: () => "key",
95
+ expirationTimeMs: config.memoizeTime,
96
+ })
97
+ @timeout(config.timeout)
96
98
  @rateLimit({
97
- timeSpanMs: 60000,
98
- allowedCalls: 300,
99
+ timeSpanMs: config.rateLimitTimeSpan,
100
+ allowedCalls: config.rateLimitAllowedCalls,
99
101
  exceedHandler,
100
102
  })
101
103
  /**
102
104
  * Retrieves all orders
103
105
  * @returns List of orders
104
106
  */
105
- public async getAll(): Promise<Order[]> {
106
- return orders;
107
+ public async getAll(): Promise<OrderDto[]> {
108
+ return orders as OrderDto[];
107
109
  }
108
110
 
111
+ @memoizeAsync({
112
+ cache: orderCache,
113
+ keyResolver: (id: number) => id.toString(),
114
+ expirationTimeMs: config.memoizeTime,
115
+ })
109
116
  @rateLimit({
110
- timeSpanMs: 60000,
111
- allowedCalls: 300,
117
+ timeSpanMs: config.rateLimitTimeSpan,
118
+ allowedCalls: config.rateLimitAllowedCalls,
112
119
  exceedHandler,
113
120
  })
114
- @onError({
115
- func: getOrderErrorHandler,
116
- })
117
121
  /**
118
122
  * Finds an order by its ID
119
- * @param id - Order ID as string
123
+ * @param {number} id - Order ID as string
120
124
  * @returns Order details or error object if not found
121
125
  */
122
- public async get(id: string): Promise<Order | ResponseError> {
123
- const orderId = tryParseId(id);
124
- if (typeof orderId != "number") return orderId satisfies ResponseError;
125
- const order = orders.find((order) => order.id === orderId);
126
-
127
- if (!order)
128
- return {
129
- status: 404,
130
- message: "Order dose not exist.",
131
- } satisfies ResponseError;
132
-
133
- return order satisfies Order;
126
+ public async get(id: number): Promise<OrderDto> {
127
+ const order = orders.find((order) => order.id === id);
128
+ if (order == null) {
129
+ throw new ResponseError("Order not found");
130
+ }
131
+ return convert(order!, ZodOrder);
134
132
  }
135
133
 
136
134
  @rateLimit({
137
- timeSpanMs: 60000,
138
- allowedCalls: 300,
135
+ timeSpanMs: config.rateLimitTimeSpan,
136
+ allowedCalls: config.rateLimitAllowedCalls,
139
137
  exceedHandler,
140
138
  })
141
139
  /**
142
140
  * Cancel an existing order
143
- * @param {string} id - Order ID to cancel
144
- * @returns {Promise<Order | ResponseError>} Updated order or error object
141
+ * @param {number} id - Order ID to cancel
142
+ * @returns {Promise<Order>} Updated order or error object
145
143
  * @throws {ResponseError} 404 - Order not found
146
144
  * @throws {ResponseError} 400 - Invalid ID format or invalid status for cancellation
147
145
  */
148
- public async cancel(id: string): Promise<Order | ResponseError> {
146
+ public async cancel(id: number): Promise<OrderDto> {
149
147
  let order = await this.get(id);
150
- if ("id" in order == false) return order satisfies ResponseError;
151
-
152
- if (order.status != "Processing")
153
- return {
154
- status: 400,
155
- message:
156
- "Cancellation is not available unless the order is in processing status.",
157
- } satisfies ResponseError;
158
-
148
+ if (order.status != "Processing") {
149
+ throw new ResponseError(
150
+ "Cancellation is not available unless the order is in processing status.",
151
+ 400
152
+ );
153
+ }
159
154
  order.status = "Canceled";
160
155
  order.deliveredAt = new Date();
161
- return order satisfies Order;
156
+
157
+ await orderCache.set(id.toString(), order);
158
+ await ordersCache.delete("key");
159
+ return order;
162
160
  }
163
161
 
164
162
  @rateLimit({
165
- timeSpanMs: 60000,
166
- allowedCalls: 300,
163
+ timeSpanMs: config.rateLimitTimeSpan,
164
+ allowedCalls: config.rateLimitAllowedCalls,
167
165
  exceedHandler,
168
166
  })
169
167
  /**
170
168
  * Mark an order as delivered
171
- * @param {string} id - Order ID to mark as delivered
172
- * @returns {Promise<Order | ResponseError>} Updated order or error object
169
+ * @param {number} id - Order ID to mark as delivered
170
+ * @returns {Promise<Order>} Updated order or error object
173
171
  * @throws {ResponseError} 404 - Order not found
174
172
  * @throws {ResponseError} 400 - Invalid ID format or invalid status for delivery
175
173
  */
176
- public async deliver(id: string): Promise<Order | ResponseError> {
174
+ public async deliver(id: number): Promise<OrderDto> {
177
175
  let order = await this.get(id);
178
- if ("id" in order == false) return order satisfies ResponseError;
179
-
180
- if (order.status != "Processing")
181
- return {
182
- status: 400,
183
- message:
184
- "Delivery is only available when the order is in processing status.",
185
- } satisfies ResponseError;
186
-
176
+ if (order.status != "Processing") {
177
+ throw new ResponseError(
178
+ "Delivery is only available when the order is in processing status.",
179
+ 400
180
+ );
181
+ }
187
182
  order.status = "Delivered";
188
183
  order.deliveredAt = new Date();
189
- return order satisfies Order;
184
+
185
+ await orderCache.set(id.toString(), order);
186
+ await ordersCache.delete("key");
187
+ return order;
190
188
  }
191
189
  }
@@ -1,36 +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>;
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,7 +1,6 @@
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 { sendError } from "@/utilities";
5
4
 
6
5
  const router = Router();
7
6
  const productController = new ProductController();
@@ -64,8 +63,11 @@ const productController = new ProductController();
64
63
  router.post(
65
64
  "/",
66
65
  asyncHandler(async (req: Request, res: Response) => {
67
- const product = await productController.create(req.body);
68
- 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();
69
71
  })
70
72
  );
71
73
 
@@ -104,10 +106,9 @@ router.get(
104
106
  router.get(
105
107
  "/:id",
106
108
  asyncHandler(async (req: Request, res: Response) => {
107
- const product = await productController.get(req.params.id);
108
-
109
- if ("id" in product == false) sendError(res, product);
110
- else res.json(product);
109
+ const id = await productController.validateId(req.params.id);
110
+ const product = await productController.get(id);
111
+ res.json(product);
111
112
  })
112
113
  );
113
114
 
@@ -167,10 +168,9 @@ router.get(
167
168
  router.put(
168
169
  "/:id",
169
170
  asyncHandler(async (req: Request, res: Response) => {
170
- const product = await productController.update(req.params.id, req.body);
171
-
172
- if ("id" in product == false) sendError(res, product);
173
- else res.json(product);
171
+ const id = await productController.validateId(req.params.id);
172
+ const product = await productController.update(id, req.body);
173
+ res.json(product);
174
174
  })
175
175
  );
176
176
 
@@ -197,10 +197,9 @@ router.put(
197
197
  router.delete(
198
198
  "/:id",
199
199
  asyncHandler(async (req: Request, res: Response) => {
200
- const product = await productController.delete(req.params.id);
201
-
202
- if ("id" in product == false) sendError(res, product);
203
- else res.json(product);
200
+ const id = await productController.validateId(req.params.id);
201
+ const product = await productController.delete(id);
202
+ res.json(product);
204
203
  })
205
204
  );
206
205