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.
@@ -0,0 +1,68 @@
1
+ import z from "zod";
2
+
3
+ /**
4
+ * Zod schema for the Order entity.
5
+ * This schema validates orders to ensure data integrity before processing.
6
+ */
7
+ export const ZodOrder = z.object({
8
+ /** Unique identifier for the order (positive integer). */
9
+ id: z.number().min(1).int(),
10
+
11
+ /** ID of the user who placed the order (positive integer). */
12
+ userId: z.number().min(1).int(),
13
+
14
+ /** Array of ordered products with their quantities. */
15
+ products: z.array(
16
+ z.object({
17
+ /** Product identifier referenced in the catalog */
18
+ productId: z.number().min(1).int(),
19
+
20
+ /** Quantity of the product ordered (at least 1) */
21
+ quantity: z.number().min(1).int(),
22
+ })
23
+ ),
24
+
25
+ /** Current status of the order */
26
+ status: z.enum(["Processing", "Delivered", "Canceled"]),
27
+
28
+ /** Total price of the order (minimum value constraint is 1000) */
29
+ total: z.number().min(1000),
30
+
31
+ /** Timestamp when the order was created */
32
+ createdAt: z.date(),
33
+
34
+ /** Optional timestamp when the order was canceled */
35
+ canceledAt: z.date().optional(),
36
+
37
+ /** Optional timestamp when the order was delivered */
38
+ deliveredAt: z.date().optional(),
39
+ });
40
+
41
+ export type Order = {
42
+ /** Unique identifier for the order */
43
+ id: number;
44
+
45
+ /** User ID who placed the order */
46
+ userId: number;
47
+
48
+ /** List of products in the order with quantities */
49
+ products: {
50
+ productId: number;
51
+ quantity: number;
52
+ }[];
53
+
54
+ /** Order status: Processing, Delivered, or Canceled */
55
+ status: "Processing" | "Delivered" | "Canceled";
56
+
57
+ /** Total price of the order */
58
+ total: number;
59
+
60
+ /** Order creation timestamp */
61
+ createdAt: Date;
62
+
63
+ /** Optional cancellation timestamp (if canceled) */
64
+ canceledAt?: Date | undefined;
65
+
66
+ /** Optional delivery timestamp (if delivered) */
67
+ deliveredAt?: Date | undefined;
68
+ };
@@ -0,0 +1,75 @@
1
+ import z from "zod";
2
+
3
+ /**
4
+ * Zod schema for the Product entity.
5
+ * This schema validates product data to ensure consistency
6
+ * before it is stored or processed.
7
+ */
8
+ export const ZodProduct = z.object({
9
+ /** Unique identifier for the product. Must be a positive integer. */
10
+ id: z.number().min(1).int(),
11
+
12
+ /** Product name. Minimum length of 2 characters. */
13
+ name: z.string().min(2),
14
+
15
+ /** Product price. Minimum value of 1000 (assuming your currency unit). */
16
+ price: z.number().min(1000),
17
+
18
+ /** Optional product description. Minimum length of 10 characters if provided. */
19
+ description: z.string().min(10).optional(),
20
+
21
+ /** Product category from a predefined list. */
22
+ category: z.enum([
23
+ "Electronics",
24
+ "Appliances",
25
+ "Sports",
26
+ "Kitchen",
27
+ "Mobile Accessories",
28
+ "Computer Accessories",
29
+ "Home Appliances",
30
+ "Books",
31
+ ]),
32
+
33
+ /** Stock count in inventory. Non-negative integer. */
34
+ stock: z.number().min(0).int(),
35
+ });
36
+
37
+ /**
38
+ * TypeScript type for a Product entity.
39
+ * Mirrors the Zod schema with an optional description.
40
+ */
41
+ export type Product = {
42
+ /** Unique identifier for the product. */
43
+ id: number;
44
+
45
+ /** Display name of the product. */
46
+ name: string;
47
+
48
+ /** Price of the product in the chosen currency. */
49
+ price: number;
50
+
51
+ /**
52
+ * Category of the product.
53
+ * Must be one of the predefined categories:
54
+ * - Electronics, Appliances, Sports, Kitchen, Mobile Accessories,
55
+ * Computer Accessories, Home Appliances, Books
56
+ */
57
+ category:
58
+ | "Electronics"
59
+ | "Appliances"
60
+ | "Sports"
61
+ | "Kitchen"
62
+ | "Mobile Accessories"
63
+ | "Computer Accessories"
64
+ | "Home Appliances"
65
+ | "Books";
66
+
67
+ /** Current stock level in inventory. Non-negative. */
68
+ stock: number;
69
+
70
+ /**
71
+ * Optional product description.
72
+ * Includes more details about the product when provided.
73
+ */
74
+ description?: string | undefined;
75
+ };
@@ -0,0 +1,38 @@
1
+ import z from "zod";
2
+
3
+ /**
4
+ * Zod schema for the User entity.
5
+ * This schema validates user data to ensure it meets basic integrity requirements
6
+ * before being stored or processed.
7
+ */
8
+ export const ZodUser = z.object({
9
+ /** Unique identifier for the user. Must be a positive integer. */
10
+ id: z.number().min(1).int(),
11
+
12
+ /** Username chosen by the user. Minimum length of 3 characters. */
13
+ username: z.string().min(3),
14
+
15
+ /** Email address of the user. Must be a valid email format. */
16
+ email: z.email(),
17
+
18
+ /** Password for the user. Minimum length of 6 characters. */
19
+ password: z.string().min(6),
20
+ });
21
+
22
+ /**
23
+ * TypeScript type for a User entity.
24
+ * Mirrors the Zod schema.
25
+ */
26
+ export type User = {
27
+ /** Unique identifier for the user. */
28
+ id: number;
29
+
30
+ /** Username chosen by the user. */
31
+ username: string;
32
+
33
+ /** Email address of the user. */
34
+ email: string;
35
+
36
+ /** Password for the user. */
37
+ password: string;
38
+ };
package/src/main.ts CHANGED
@@ -2,6 +2,9 @@ import express, { NextFunction, Request, Response } from "express";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import config from "./config";
5
+ import { ResponseError } from "./utilities/error-handling";
6
+ import { resolve } from "./utilities/container";
7
+ import { WinstonLoggerService } from "./utilities/logger/winston-logger.service";
5
8
 
6
9
  /**
7
10
  * Recursively loads Express routers from directory
@@ -53,30 +56,39 @@ const app = express();
53
56
  app.use(express.json());
54
57
  app.use(express.urlencoded({ extended: true }));
55
58
 
56
- //app.use(cors());
57
-
58
59
  // Automatically import all routers from the /src/routers directory
59
60
  const routersPath = path.join(__dirname, "/routers");
60
61
  loadRouters(routersPath);
61
62
 
62
63
  // Swagger setup
63
- if (config.devMode) {
64
+ if (config.swagger) {
64
65
  const swaggerJsDoc = require("swagger-jsdoc");
65
66
  const swaggerUi = require("swagger-ui-express");
66
67
  const swaggerDocs = swaggerJsDoc(config.swaggerOptions);
67
68
  app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs));
68
69
  }
69
70
 
71
+ const logger = resolve(WinstonLoggerService);
72
+
70
73
  // General error handler
71
- app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
72
- res.status(500).json(err);
73
- });
74
+ app.use(
75
+ (error: ResponseError, req: Request, res: Response, next: NextFunction) => {
76
+ const status = error.status ?? 500;
77
+ res.status(status).json(error);
78
+ if (status < 500) {
79
+ logger.warn(error.message, "Invalid request", error.details);
80
+ } else {
81
+ logger.error(error.message, "Server error", error.details);
82
+ }
83
+ }
84
+ );
74
85
 
75
86
  // Start the server
76
87
  app.listen(config.port, () => {
77
88
  console.log(`Server is running on http://localhost:${config.port}`);
78
- if (config.devMode)
89
+ if (config.devMode) {
79
90
  console.log(
80
91
  `Swagger UI is available at http://localhost:${config.port}/api-docs`
81
92
  );
93
+ }
82
94
  });
@@ -1,33 +1,10 @@
1
- import { z } from "zod";
1
+ import { ZodOrder } from "@/entities/order.entity";
2
2
 
3
3
  /**
4
- * Zod schema for Order entity
5
- * @typedef {Object} ZodOrder
6
- * @property {number} id - Unique identifier (min 1)
7
- * @property {number} userId - Associated user ID (min 1)
8
- * @property {{productId: number, quantity: number}[]} products - Array of ordered products
9
- * @property {"Processing"|"Delivered"|"Canceled"} status - Order status
10
- * @property {number} total - Total price (min 1000)
11
- * @property {Date} createdAt - Creation timestamp
12
- * @property {Date} [canceledAt] - Cancellation timestamp
13
- * @property {Date} [deliveredAt] - Delivery timestamp
4
+ * DTO for creating an order.
5
+ * This is derived from the full Order schema by omitting fields
6
+ * that are typically generated by the system (id, status, timestamps).
14
7
  */
15
- export const ZodOrder = z.object({
16
- id: z.number().min(1).int(),
17
- userId: z.number().min(1).int(),
18
- products: z.array(
19
- z.object({
20
- productId: z.number().min(1).int(),
21
- quantity: z.number().min(1).int(),
22
- })
23
- ),
24
- status: z.enum(["Processing", "Delivered", "Canceled"]),
25
- total: z.number().min(1000),
26
- createdAt: z.date(),
27
- canceledAt: z.date().optional(),
28
- deliveredAt: z.date().optional(),
29
- });
30
-
31
8
  export const ZodOrderCreationDto = ZodOrder.omit({
32
9
  id: true,
33
10
  status: true,
@@ -36,6 +13,53 @@ export const ZodOrderCreationDto = ZodOrder.omit({
36
13
  deliveredAt: true,
37
14
  });
38
15
 
39
- export type Order = z.infer<typeof ZodOrder>;
40
- export type OrderCreationDto = z.infer<typeof ZodOrderCreationDto>;
41
- export type OrderDto = z.infer<typeof ZodOrder>;
16
+ /**
17
+ * Data Transfer Object for creating an order.
18
+ * Represents the minimal data required to create an order.
19
+ */
20
+ export type OrderCreationDto = {
21
+ /** ID of the user placing the order. */
22
+ userId: number;
23
+
24
+ /** List of products included in the order with their quantities. */
25
+ products: {
26
+ productId: number;
27
+ quantity: number;
28
+ }[];
29
+
30
+ /** Total price for the order (validated by the server/persistence layer). */
31
+ total: number;
32
+ };
33
+
34
+ /**
35
+ * Data Transfer Object for a full Order view.
36
+ * Represents the complete order data as returned by the API/service.
37
+ */
38
+ export type OrderDto = {
39
+ /** Unique identifier for the order. */
40
+ id: number;
41
+
42
+ /** User ID who placed the order. */
43
+ userId: number;
44
+
45
+ /** List of products in the order with their quantities. */
46
+ products: {
47
+ productId: number;
48
+ quantity: number;
49
+ }[];
50
+
51
+ /** Current status of the order: Processing, Delivered, or Canceled. */
52
+ status: "Processing" | "Delivered" | "Canceled";
53
+
54
+ /** Total price for the order. */
55
+ total: number;
56
+
57
+ /** Timestamp when the order was created. */
58
+ createdAt: Date;
59
+
60
+ /** Optional timestamp when the order was canceled (if canceled). */
61
+ canceledAt?: Date | undefined;
62
+
63
+ /** Optional timestamp when the order was delivered (if delivered). */
64
+ deliveredAt?: Date | undefined;
65
+ };
@@ -1,16 +1,17 @@
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 { resolve } from "@/utilities/container";
4
5
 
5
6
  const router = Router();
6
- const orderController = new OrderController();
7
+ const orderController = resolve(OrderController);
7
8
 
8
9
  /**
9
10
  * @swagger
10
11
  * /orders:
11
12
  * post:
12
13
  * summary: Create a new order
13
- * description: Creates an order with user details and products.
14
+ * description: Creates an order with order details and products.
14
15
  * consumes:
15
16
  * - application/json
16
17
  * produces:
@@ -58,8 +59,9 @@ const orderController = new OrderController();
58
59
  router.post(
59
60
  "/",
60
61
  asyncHandler(async (req: Request, res: Response) => {
61
- const order = await orderController.create(req.body);
62
- res.status(201).json(order);
62
+ const order = await orderController.validateOrderCreationDto(req.body);
63
+ await orderController.create(order);
64
+ res.status(201).send();
63
65
  })
64
66
  );
65
67
 
@@ -99,7 +101,8 @@ router.get(
99
101
  router.get(
100
102
  "/:id",
101
103
  asyncHandler(async (req: Request, res: Response) => {
102
- const order = await orderController.get(req.params.id);
104
+ const id = await orderController.validateId(req.params.id);
105
+ const order = await orderController.get(id);
103
106
  res.json(order);
104
107
  })
105
108
  );
@@ -123,7 +126,8 @@ router.get(
123
126
  router.patch(
124
127
  "/:id/cancel",
125
128
  asyncHandler(async (req: Request, res: Response) => {
126
- const order = await orderController.cancel(req.params.id);
129
+ const id = await orderController.validateId(req.params.id);
130
+ const order = await orderController.cancel(id);
127
131
  res.json(order);
128
132
  })
129
133
  );
@@ -149,7 +153,8 @@ router.patch(
149
153
  router.patch(
150
154
  "/:id/deliver",
151
155
  asyncHandler(async (req: Request, res: Response) => {
152
- const order = await orderController.deliver(req.params.id);
156
+ const id = await orderController.validateId(req.params.id);
157
+ const order = await orderController.deliver(id);
153
158
  res.json(order);
154
159
  })
155
160
  );
@@ -1,94 +1,96 @@
1
1
  import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
2
2
  import {
3
- Order,
4
3
  OrderDto,
5
4
  OrderCreationDto,
6
5
  ZodOrderCreationDto,
7
6
  } from "./dto/order.dto";
8
- import { Validate, ZodInput } from "ts-zod4-decorators";
9
- import { ResponseError } from "@/utilities/types";
10
- import { parseId } from "@/utilities/error-handling";
7
+ import { ResponseError } from "@/utilities/error-handling";
8
+ import { convert, stringToInteger } from "@/utilities/conversion";
11
9
  import config from "@/config";
12
-
13
- // Array to store orders (as a mock database)
14
- const orders: Order[] = [
15
- {
16
- id: 1,
17
- userId: 1,
18
- products: [
19
- { productId: 2, quantity: 1 },
20
- { productId: 6, quantity: 2 },
21
- ],
22
- status: "Delivered",
23
- total: 14400,
24
- createdAt: new Date("2024-01-15"),
25
- deliveredAt: new Date("2024-02-10"),
26
- },
27
- {
28
- id: 2,
29
- userId: 3,
30
- products: [
31
- { productId: 9, quantity: 1 },
32
- { productId: 7, quantity: 1 },
33
- ],
34
- status: "Processing",
35
- total: 36500,
36
- createdAt: new Date("2024-03-20"),
37
- },
38
- {
39
- id: 3,
40
- userId: 2,
41
- products: [
42
- { productId: 1, quantity: 1 },
43
- { productId: 4, quantity: 2 },
44
- ],
45
- status: "Canceled",
46
- total: 81000,
47
- createdAt: new Date("2024-05-01"),
48
- canceledAt: new Date("2024-05-03"),
49
- },
50
- ];
10
+ import { orders } from "@/db";
11
+ import { Order, ZodOrder } from "@/entities/order.entity";
12
+ import { MapAsyncCache } from "@/utilities/cache/memory-cache";
13
+ import { Injectable } from "@/utilities/container";
51
14
 
52
15
  function exceedHandler() {
53
16
  const message = "Too much call in allowed window";
54
17
  throw new ResponseError(message, 429);
55
18
  }
56
19
 
57
- function getOrderErrorHandler(e: Error) {
58
- const message = "Order not found.";
59
- throw new ResponseError(message, 404, e.message);
20
+ function invalidInputHandler(e: ResponseError) {
21
+ const message = "Invalid input";
22
+ throw new ResponseError(message, 400, e.message);
60
23
  }
61
24
 
25
+ const ordersCache = new MapAsyncCache<OrderDto[]>(config.cacheSize);
26
+ const orderCache = new MapAsyncCache<OrderDto>(config.cacheSize);
27
+
28
+ @Injectable()
62
29
  /**
63
30
  * Controller for handling order-related operations
64
31
  * @class OrderController
65
32
  * @desc Provides methods for order management including creation, status updates and retrieval
66
33
  */
67
34
  export default class OrderController {
35
+ // constructor(private readonly orderService: OrderService) { }
36
+
37
+ @onError({
38
+ func: invalidInputHandler,
39
+ })
40
+ /**
41
+ * Validates a string ID and converts it to a number.
42
+ *
43
+ * @param {string} id - The ID to validate and convert.
44
+ * @returns {number} The numeric value of the provided ID.
45
+ */
46
+ public async validateId(id: string): Promise<number> {
47
+ return stringToInteger(id);
48
+ }
49
+
50
+ @onError({
51
+ func: invalidInputHandler,
52
+ })
53
+ /**
54
+ * Validates and creates a new Order from the given DTO.
55
+ *
56
+ * @param {OrderCreationDto} order - The incoming OrderCreationDto to validate and transform.
57
+ * @returns {Order} A fully formed Order object ready for persistence.
58
+ */
59
+ public async validateOrderCreationDto(
60
+ order: OrderCreationDto
61
+ ): Promise<Order> {
62
+ const newOrder = await ZodOrderCreationDto.parseAsync(order);
63
+ return {
64
+ ...newOrder,
65
+ id: orders.length + 1,
66
+ status: "Processing",
67
+ createdAt: new Date(),
68
+ };
69
+ }
70
+
68
71
  @rateLimit({
69
72
  timeSpanMs: config.rateLimitTimeSpan,
70
73
  allowedCalls: config.rateLimitAllowedCalls,
71
74
  exceedHandler,
72
75
  })
73
- @Validate
74
76
  /**
75
77
  * Create a new order
76
- * @param {OrderCreationDto} order - Order creation data
77
- * @returns {Promise<Order>} Newly created order
78
+ * @param {Order} order - Order creation data
79
+ * @returns {Promise<void>}
80
+ * @throws {ResponseError} 500 - When rate limit exceeded
81
+ * @throws {ResponseError} 400 - Invalid input data
78
82
  */
79
- public async create(@ZodInput(ZodOrderCreationDto) order: OrderCreationDto) {
80
- const newOrder = {
81
- ...order,
82
- id: orders.length + 1,
83
- createdAt: new Date(),
84
- status: "Processing",
85
- } satisfies Order;
86
-
87
- orders.push(newOrder);
88
- return newOrder;
83
+ public async create(order: Order): Promise<void> {
84
+ orders.push(order);
85
+ await orderCache.set(order.id.toString(), order as OrderDto);
86
+ await ordersCache.delete("key");
89
87
  }
90
88
 
91
- @memoizeAsync(config.memoizeTime)
89
+ @memoizeAsync({
90
+ cache: ordersCache,
91
+ keyResolver: () => "key",
92
+ expirationTimeMs: config.memoizeTime,
93
+ })
92
94
  @timeout(config.timeout)
93
95
  @rateLimit({
94
96
  timeSpanMs: config.rateLimitTimeSpan,
@@ -100,28 +102,30 @@ export default class OrderController {
100
102
  * @returns List of orders
101
103
  */
102
104
  public async getAll(): Promise<OrderDto[]> {
103
- return orders;
105
+ return orders as OrderDto[];
104
106
  }
105
107
 
106
- @memoizeAsync(config.memoizeTime)
108
+ @memoizeAsync({
109
+ cache: orderCache,
110
+ keyResolver: (id: number) => id.toString(),
111
+ expirationTimeMs: config.memoizeTime,
112
+ })
107
113
  @rateLimit({
108
114
  timeSpanMs: config.rateLimitTimeSpan,
109
115
  allowedCalls: config.rateLimitAllowedCalls,
110
116
  exceedHandler,
111
117
  })
112
- @onError({
113
- func: getOrderErrorHandler,
114
- })
115
118
  /**
116
119
  * Finds an order by its ID
117
- * @param id - Order ID as string
120
+ * @param {number} id - Order ID as string
118
121
  * @returns Order details or error object if not found
119
122
  */
120
- public async get(id: string): Promise<OrderDto> {
121
- const orderId = parseId(id);
122
- const order = orders.find((order) => order.id === orderId);
123
- if (order == null) throw new ResponseError("User dose not exist.", 404);
124
- return order satisfies Order;
123
+ public async get(id: number): Promise<OrderDto> {
124
+ const order = orders.find((order) => order.id === id);
125
+ if (order == null) {
126
+ throw new ResponseError("Order not found");
127
+ }
128
+ return convert(order!, ZodOrder);
125
129
  }
126
130
 
127
131
  @rateLimit({
@@ -131,12 +135,12 @@ export default class OrderController {
131
135
  })
132
136
  /**
133
137
  * Cancel an existing order
134
- * @param {string} id - Order ID to cancel
138
+ * @param {number} id - Order ID to cancel
135
139
  * @returns {Promise<Order>} Updated order or error object
136
140
  * @throws {ResponseError} 404 - Order not found
137
141
  * @throws {ResponseError} 400 - Invalid ID format or invalid status for cancellation
138
142
  */
139
- public async cancel(id: string): Promise<OrderDto> {
143
+ public async cancel(id: number): Promise<OrderDto> {
140
144
  let order = await this.get(id);
141
145
  if (order.status != "Processing") {
142
146
  throw new ResponseError(
@@ -144,10 +148,12 @@ export default class OrderController {
144
148
  400
145
149
  );
146
150
  }
147
-
148
151
  order.status = "Canceled";
149
152
  order.deliveredAt = new Date();
150
- return order satisfies Order;
153
+
154
+ await orderCache.set(id.toString(), order);
155
+ await ordersCache.delete("key");
156
+ return order;
151
157
  }
152
158
 
153
159
  @rateLimit({
@@ -157,12 +163,12 @@ export default class OrderController {
157
163
  })
158
164
  /**
159
165
  * Mark an order as delivered
160
- * @param {string} id - Order ID to mark as delivered
166
+ * @param {number} id - Order ID to mark as delivered
161
167
  * @returns {Promise<Order>} Updated order or error object
162
168
  * @throws {ResponseError} 404 - Order not found
163
169
  * @throws {ResponseError} 400 - Invalid ID format or invalid status for delivery
164
170
  */
165
- public async deliver(id: string): Promise<OrderDto> {
171
+ public async deliver(id: number): Promise<OrderDto> {
166
172
  let order = await this.get(id);
167
173
  if (order.status != "Processing") {
168
174
  throw new ResponseError(
@@ -170,9 +176,11 @@ export default class OrderController {
170
176
  400
171
177
  );
172
178
  }
173
-
174
179
  order.status = "Delivered";
175
180
  order.deliveredAt = new Date();
176
- return order satisfies Order;
181
+
182
+ await orderCache.set(id.toString(), order);
183
+ await ordersCache.delete("key");
184
+ return order;
177
185
  }
178
186
  }