codeweaver 1.1.0 → 2.0.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 +54 -61
- package/package.json +8 -6
- package/src/config.ts +23 -15
- package/src/constants.ts +6 -0
- package/src/main.ts +82 -0
- package/src/routers/orders/dto/order.dto.ts +1 -0
- package/src/routers/orders/{index.ts → index.router.ts} +3 -7
- package/src/routers/orders/order.controller.ts +41 -54
- package/src/routers/products/dto/product.dto.ts +1 -0
- package/src/routers/products/{index.ts → index.router.ts} +3 -10
- package/src/routers/products/product.controller.ts +45 -65
- package/src/routers/users/dto/user.dto.ts +3 -1
- package/src/routers/users/{index.ts → index.router.ts} +1 -4
- package/src/routers/users/user.controller.ts +33 -41
- package/src/swagger-options.ts +54 -0
- package/src/utilities/assign.ts +81 -0
- package/src/utilities/error-handling.ts +120 -0
- package/src/utilities/types.ts +23 -0
- package/tsconfig.json +1 -4
- package/tsconfig.paths.json +8 -10
- package/src/app.ts +0 -113
- 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
package/README.md
CHANGED
|
@@ -53,34 +53,36 @@ To get started with the project, follow these steps:
|
|
|
53
53
|
|
|
54
54
|
**/src**
|
|
55
55
|
├── **/routers** `Directory containing all router files`
|
|
56
|
-
│ ├── **/
|
|
57
|
-
│ │ ├──
|
|
58
|
-
│ │ ├──
|
|
56
|
+
│ ├── **/users** `Routers for user-related endpoints`
|
|
57
|
+
│ │ ├── index.router.ts `/users`
|
|
58
|
+
│ │ ├── user-router2.router.ts `/users/user-router2`
|
|
59
59
|
│ │ ├── user.controller.ts
|
|
60
60
|
│ │ ├── user.service.ts
|
|
61
61
|
│ │ └── user.dto.ts
|
|
62
|
-
│ ├── **/
|
|
63
|
-
│ │ ├── index.ts `/
|
|
64
|
-
│ │ ├── product_test.spec.ts
|
|
62
|
+
│ ├── **/products** `Routers for product-related endpoints`
|
|
63
|
+
│ │ ├── index.router.ts `/products`
|
|
65
64
|
│ │ ├── product.controller.ts
|
|
66
65
|
│ │ ├── product.service.ts
|
|
67
|
-
│ │ ├── **/
|
|
66
|
+
│ │ ├── **/dtos**
|
|
68
67
|
│ │ │ └── product.dto.ts
|
|
69
|
-
|
|
70
|
-
│
|
|
71
|
-
│ │ ├──
|
|
68
|
+
| | │ └── product-types.dto.ts
|
|
69
|
+
│ ├── **/orders** `Routers for order-related endpoints`
|
|
70
|
+
│ │ ├── index.router.ts `/orders`
|
|
72
71
|
│ │ ├── order.controller.ts
|
|
73
|
-
│ │
|
|
74
|
-
│
|
|
72
|
+
│ │ ├── order.controller.spec.ts
|
|
73
|
+
│ │ ├── order.service.ts
|
|
74
|
+
│ │ └── order.service.spec.ts
|
|
75
|
+
│ └── index.router.ts `Home page`
|
|
76
|
+
│ └── app.controller.ts `Home page`
|
|
75
77
|
├── app.ts `Main application file`
|
|
76
78
|
├── config.ts `Application configuration file`
|
|
77
79
|
└── ... `Other files (middleware, models, etc.)`
|
|
78
80
|
|
|
79
81
|
### Router Directory
|
|
80
82
|
|
|
81
|
-
Each router file in the `/routers` directory is organized to handle related endpoints. The `app.ts` file automatically imports all routers and mounts them
|
|
83
|
+
Each router file in the `/routers` directory is organized to handle related endpoints. The `app.ts` file automatically imports all routers and mounts them on the main Express application, making it straightforward to add new routes without touching central code.
|
|
82
84
|
|
|
83
|
-
Files
|
|
85
|
+
Files ending with `.router.ts` or `.router.js` are automatically included in the router list and can be reused for various purposes within the application.
|
|
84
86
|
|
|
85
87
|
Example of a basic router:
|
|
86
88
|
|
|
@@ -88,7 +90,6 @@ Example of a basic router:
|
|
|
88
90
|
import { Router, Request, Response } from "express";
|
|
89
91
|
import asyncHandler from "express-async-handler";
|
|
90
92
|
import UserController from "./user.controller";
|
|
91
|
-
import { sendError } from "@src/utilities";
|
|
92
93
|
|
|
93
94
|
const router = Router();
|
|
94
95
|
const userController = new UserController();
|
|
@@ -160,9 +161,7 @@ router.get(
|
|
|
160
161
|
"/:id",
|
|
161
162
|
asyncHandler(async (req: Request, res: Response) => {
|
|
162
163
|
const user = await userController.get(req.params.id);
|
|
163
|
-
|
|
164
|
-
if ("id" in user == false) sendError(res, user);
|
|
165
|
-
else res.json(user);
|
|
164
|
+
res.json(user);
|
|
166
165
|
})
|
|
167
166
|
);
|
|
168
167
|
|
|
@@ -214,14 +213,16 @@ import { z } from "zod";
|
|
|
214
213
|
export const ZodUser = z.object({
|
|
215
214
|
id: z.number().min(1).int(),
|
|
216
215
|
username: z.string().min(3),
|
|
217
|
-
email: z.
|
|
216
|
+
email: z.email(),
|
|
218
217
|
password: z.string().min(6),
|
|
219
218
|
});
|
|
220
219
|
|
|
221
220
|
export const ZodUserCreationDto = ZodUser.omit({ id: true });
|
|
221
|
+
export const ZodUserDto = ZodUser.omit({ password: true });
|
|
222
222
|
|
|
223
223
|
export type User = z.infer<typeof ZodUser>;
|
|
224
224
|
export type UserCreationDto = z.infer<typeof ZodUserCreationDto>;
|
|
225
|
+
export type UserDto = z.infer<typeof ZodUserDto>;
|
|
225
226
|
```
|
|
226
227
|
|
|
227
228
|
- **Throttling and Rate Limiting**: The `@rateLimit` decorator is applied to safeguard the application's endpoints from abuse by limiting how frequently a particular method can be invoked.
|
|
@@ -233,11 +234,17 @@ By using a well-organized controller structure, this project makes it easier to
|
|
|
233
234
|
Here is a quick reference to the UserController in practice:
|
|
234
235
|
|
|
235
236
|
```typescript
|
|
236
|
-
import {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
import {
|
|
238
|
+
User,
|
|
239
|
+
ZodUserCreationDto,
|
|
240
|
+
UserCreationDto,
|
|
241
|
+
UserDto,
|
|
242
|
+
} from "./dto/user.dto";
|
|
243
|
+
import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
|
|
244
|
+
import { Validate, ZodInput } from "ts-zod4-decorators";
|
|
245
|
+
import { ResponseError } from "@/types";
|
|
246
|
+
import { parseId } from "@/utilities/error-handling";
|
|
247
|
+
import config from "@/config";
|
|
241
248
|
|
|
242
249
|
// Array to store users (as a mock database)
|
|
243
250
|
const users = [
|
|
@@ -305,18 +312,12 @@ const users = [
|
|
|
305
312
|
|
|
306
313
|
function exceedHandler() {
|
|
307
314
|
const message = "Too much call in allowed window";
|
|
308
|
-
|
|
309
|
-
throw new Error(message, {
|
|
310
|
-
cause: { status: 500, message } satisfies ResponseError,
|
|
311
|
-
});
|
|
315
|
+
throw new ResponseError(message, 429);
|
|
312
316
|
}
|
|
313
317
|
|
|
314
318
|
function getUserErrorHandler(e: Error) {
|
|
315
319
|
const message = "User not found.";
|
|
316
|
-
|
|
317
|
-
throw new Error(message, {
|
|
318
|
-
cause: { status: 404, message, details: e.message } satisfies ResponseError,
|
|
319
|
-
});
|
|
320
|
+
throw new ResponseError(message, 404, e.message);
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
/**
|
|
@@ -328,8 +329,8 @@ export default class UserController {
|
|
|
328
329
|
// constructor(private readonly userService: UserService) { }
|
|
329
330
|
|
|
330
331
|
@rateLimit({
|
|
331
|
-
timeSpanMs:
|
|
332
|
-
allowedCalls:
|
|
332
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
333
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
333
334
|
exceedHandler,
|
|
334
335
|
})
|
|
335
336
|
@Validate
|
|
@@ -341,57 +342,49 @@ export default class UserController {
|
|
|
341
342
|
* @throws {ResponseError} 400 - Invalid input data
|
|
342
343
|
*/
|
|
343
344
|
public async create(@ZodInput(ZodUserCreationDto) user: UserCreationDto) {
|
|
344
|
-
users.push({ ...user, id: users.length + 1 }
|
|
345
|
+
users.push({ ...user, id: users.length + 1 });
|
|
345
346
|
}
|
|
346
347
|
|
|
348
|
+
@memoizeAsync(config.memoizeTime)
|
|
347
349
|
@onError({
|
|
348
350
|
func: getUserErrorHandler,
|
|
349
351
|
})
|
|
350
352
|
@rateLimit({
|
|
351
|
-
timeSpanMs:
|
|
352
|
-
allowedCalls:
|
|
353
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
354
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
353
355
|
exceedHandler,
|
|
354
356
|
})
|
|
355
357
|
/**
|
|
356
358
|
* Get user by ID
|
|
357
359
|
* @param {string} id - User ID as string
|
|
358
|
-
* @returns {Promise<User
|
|
360
|
+
* @returns {Promise<User>} User details or error object
|
|
359
361
|
* @throws {ResponseError} 404 - User not found
|
|
360
362
|
* @throws {ResponseError} 400 - Invalid ID format
|
|
361
363
|
*/
|
|
362
|
-
public async get(id: string): Promise<
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (!user)
|
|
368
|
-
return {
|
|
369
|
-
status: 404,
|
|
370
|
-
message: "User dose not exist.",
|
|
371
|
-
} satisfies ResponseError;
|
|
372
|
-
|
|
364
|
+
public async get(id: string): Promise<UserDto> {
|
|
365
|
+
const response = parseId(id);
|
|
366
|
+
const user = users.find((user) => user.id === response);
|
|
367
|
+
if (user == null) throw new ResponseError("User dose not exist.", 404);
|
|
373
368
|
return user satisfies User;
|
|
374
369
|
}
|
|
375
370
|
|
|
376
|
-
@
|
|
371
|
+
@memoizeAsync(config.memoizeTime)
|
|
372
|
+
@timeout(config.timeout)
|
|
377
373
|
@rateLimit({
|
|
378
|
-
timeSpanMs:
|
|
379
|
-
allowedCalls:
|
|
374
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
375
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
380
376
|
exceedHandler,
|
|
381
377
|
})
|
|
382
378
|
/**
|
|
383
379
|
* Get all users with masked passwords
|
|
384
|
-
* @returns {Promise<
|
|
380
|
+
* @returns {Promise<UserDto[]>} List of users with hidden password fields
|
|
385
381
|
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
386
382
|
*/
|
|
387
|
-
public async getAll(): Promise<
|
|
388
|
-
return users.map(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
password: "?",
|
|
393
|
-
} satisfies User)
|
|
394
|
-
);
|
|
383
|
+
public async getAll(): Promise<UserDto[]> {
|
|
384
|
+
return users.map((user) => ({
|
|
385
|
+
...user,
|
|
386
|
+
password: "?",
|
|
387
|
+
}));
|
|
395
388
|
}
|
|
396
389
|
}
|
|
397
390
|
```
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeweaver",
|
|
3
|
-
"version": "
|
|
4
|
-
"main": "src/
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"main": "src/main.ts",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-codeweaver-app": "./command.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"start": "nodemon -r tsconfig-paths/register src/
|
|
10
|
-
"dev": "nodemon -r tsconfig-paths/register src/
|
|
9
|
+
"start": "nodemon -r tsconfig-paths/register src/main.ts",
|
|
10
|
+
"dev": "nodemon -r tsconfig-paths/register src/main.ts",
|
|
11
11
|
"build": "tsc && tsc-alias && copyfiles -u 1 \"src/**/*.{json,env,sqlite,db,sql,yaml,yml,xml,txt,md}\" dist",
|
|
12
|
-
"serve": "cross-env TSCONFIG_PATH=tsconfig.paths.json node -r tsconfig-paths/register dist/
|
|
12
|
+
"serve": "cross-env TSCONFIG_PATH=tsconfig.paths.json node -r tsconfig-paths/register dist/main.js"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"codeweaver",
|
|
@@ -32,8 +32,11 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"description": "A lightweight framework built on top of Express and TypeScript",
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"dotenv": "^17.2.3",
|
|
35
36
|
"express": "^5.1.0",
|
|
36
37
|
"express-async-handler": "^1.2.0",
|
|
38
|
+
"swagger-jsdoc": "^3.7.0",
|
|
39
|
+
"ts-zod4-decorators": "^1.0.0",
|
|
37
40
|
"utils-decorators": "^2.10.0",
|
|
38
41
|
"zod": "^4.0.14"
|
|
39
42
|
},
|
|
@@ -43,7 +46,6 @@
|
|
|
43
46
|
"copyfiles": "^2.4.1",
|
|
44
47
|
"cross-env": "^10.0.0",
|
|
45
48
|
"nodemon": "^3.1.10",
|
|
46
|
-
"swagger-jsdoc": "^6.2.8",
|
|
47
49
|
"swagger-ui-express": "^5.0.1",
|
|
48
50
|
"ts-node": "^10.9.2",
|
|
49
51
|
"tsc-alias": "^1.8.16",
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memoizeTime,
|
|
3
|
+
productionEnvironment,
|
|
4
|
+
rateLimitTimeSpan,
|
|
5
|
+
rateLimitAllowedCalls,
|
|
6
|
+
timeout,
|
|
7
|
+
portNumber,
|
|
8
|
+
} from "./constants";
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* Server configuration interface
|
|
3
12
|
* @interface
|
|
@@ -53,13 +62,18 @@ interface SwaggerOptions {
|
|
|
53
62
|
*/
|
|
54
63
|
interface Config {
|
|
55
64
|
devMode: boolean;
|
|
56
|
-
port:
|
|
65
|
+
port: number;
|
|
57
66
|
swaggerOptions: SwaggerOptions;
|
|
67
|
+
timeout: number;
|
|
68
|
+
rateLimitTimeSpan: number;
|
|
69
|
+
rateLimitAllowedCalls: number;
|
|
70
|
+
memoizeTime: number;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
const port = process.env.PORT ||
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
const port = Number(process.env.PORT) || portNumber;
|
|
74
|
+
|
|
75
|
+
let config: Config = {
|
|
76
|
+
devMode: process.env.NODE_ENV !== productionEnvironment,
|
|
63
77
|
port,
|
|
64
78
|
swaggerOptions: {
|
|
65
79
|
swaggerDefinition: {
|
|
@@ -82,17 +96,11 @@ const config: Config = {
|
|
|
82
96
|
"./src/routers/**/*.js",
|
|
83
97
|
], // Path to the API docs
|
|
84
98
|
},
|
|
99
|
+
timeout: Number(process.env.TIMEOUT) || timeout,
|
|
100
|
+
rateLimitTimeSpan: Number(process.env.RATE_LIMIT) || rateLimitTimeSpan,
|
|
101
|
+
rateLimitAllowedCalls:
|
|
102
|
+
Number(process.env.RATE_LIMIT) || rateLimitAllowedCalls,
|
|
103
|
+
memoizeTime: Number(process.env.MEMOIZE_TIME) || memoizeTime,
|
|
85
104
|
};
|
|
86
105
|
|
|
87
|
-
// Other configurations:
|
|
88
|
-
//
|
|
89
|
-
// config.jwt_key = config.devMode ? "" : "";
|
|
90
|
-
// config.jwt_expiration = config.devMode ? 360000 : 360000;
|
|
91
|
-
// config.dbConnectionString = config.devMode ? `mongoDb url` : `mongoDb url`;
|
|
92
|
-
// config.mongoDebug = config.devMode;
|
|
93
|
-
// config.port = config.devMode ? 3000 : 3000;
|
|
94
|
-
// config.host = config.devMode ? "localhost" : "localhost";
|
|
95
|
-
// config.env = config.devMode ? "development" : "production";
|
|
96
|
-
// config.mongoUrl = config.devMode ? "mongodb://localhost:27017/test" : "mongodb://localhost:27017/test";
|
|
97
|
-
|
|
98
106
|
export default config;
|
package/src/constants.ts
ADDED
package/src/main.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import express, { NextFunction, Request, Response } from "express";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import config from "./config";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Recursively loads Express routers from directory
|
|
8
|
+
* @param {string} routerPath - Directory path to scan
|
|
9
|
+
* @param {string} [basePath=""] - Base route path
|
|
10
|
+
*/
|
|
11
|
+
function loadRouters(routerPath: string, basePath: string = "") {
|
|
12
|
+
// Read entries with their type info
|
|
13
|
+
const entries = fs.readdirSync(routerPath, { withFileTypes: true });
|
|
14
|
+
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const fullPath = path.join(routerPath, entry.name);
|
|
17
|
+
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
// Recurse into subdirectories
|
|
20
|
+
const subRoutePath = path
|
|
21
|
+
.join(basePath, entry.name)
|
|
22
|
+
.replace(/\\/g, "/")
|
|
23
|
+
.replace(/\/?index$/g, "");
|
|
24
|
+
loadRouters(fullPath, subRoutePath);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Only handle router files
|
|
29
|
+
if (
|
|
30
|
+
!entry.name.endsWith(".router.ts") &&
|
|
31
|
+
!entry.name.endsWith(".router.js")
|
|
32
|
+
) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build route path safely
|
|
37
|
+
const routePath = path
|
|
38
|
+
.join(basePath, entry.name.replace(/\.router\.[tj]s$/, ""))
|
|
39
|
+
.replace(/\\/g, "/")
|
|
40
|
+
.replace(/\/?index$/g, "");
|
|
41
|
+
|
|
42
|
+
// Optional: skip if the target path would be empty (maps to /)
|
|
43
|
+
const mountPath = "/" + (routePath || "");
|
|
44
|
+
|
|
45
|
+
// Import and mount
|
|
46
|
+
const router = require(fullPath);
|
|
47
|
+
app.use(mountPath, router);
|
|
48
|
+
console.log(`Mounted ${entry.name} at ${mountPath}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
app.use(express.json());
|
|
54
|
+
app.use(express.urlencoded({ extended: true }));
|
|
55
|
+
|
|
56
|
+
//app.use(cors());
|
|
57
|
+
|
|
58
|
+
// Automatically import all routers from the /src/routers directory
|
|
59
|
+
const routersPath = path.join(__dirname, "/routers");
|
|
60
|
+
loadRouters(routersPath);
|
|
61
|
+
|
|
62
|
+
// Swagger setup
|
|
63
|
+
if (config.devMode) {
|
|
64
|
+
const swaggerJsDoc = require("swagger-jsdoc");
|
|
65
|
+
const swaggerUi = require("swagger-ui-express");
|
|
66
|
+
const swaggerDocs = swaggerJsDoc(config.swaggerOptions);
|
|
67
|
+
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// General error handler
|
|
71
|
+
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
|
|
72
|
+
res.status(500).json(err);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Start the server
|
|
76
|
+
app.listen(config.port, () => {
|
|
77
|
+
console.log(`Server is running on http://localhost:${config.port}`);
|
|
78
|
+
if (config.devMode)
|
|
79
|
+
console.log(
|
|
80
|
+
`Swagger UI is available at http://localhost:${config.port}/api-docs`
|
|
81
|
+
);
|
|
82
|
+
});
|
|
@@ -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();
|
|
@@ -101,8 +100,7 @@ router.get(
|
|
|
101
100
|
"/:id",
|
|
102
101
|
asyncHandler(async (req: Request, res: Response) => {
|
|
103
102
|
const order = await orderController.get(req.params.id);
|
|
104
|
-
|
|
105
|
-
else res.json(order);
|
|
103
|
+
res.json(order);
|
|
106
104
|
})
|
|
107
105
|
);
|
|
108
106
|
|
|
@@ -126,8 +124,7 @@ router.patch(
|
|
|
126
124
|
"/:id/cancel",
|
|
127
125
|
asyncHandler(async (req: Request, res: Response) => {
|
|
128
126
|
const order = await orderController.cancel(req.params.id);
|
|
129
|
-
|
|
130
|
-
else res.json(order);
|
|
127
|
+
res.json(order);
|
|
131
128
|
})
|
|
132
129
|
);
|
|
133
130
|
|
|
@@ -153,8 +150,7 @@ router.patch(
|
|
|
153
150
|
"/:id/deliver",
|
|
154
151
|
asyncHandler(async (req: Request, res: Response) => {
|
|
155
152
|
const order = await orderController.deliver(req.params.id);
|
|
156
|
-
|
|
157
|
-
else res.json(order);
|
|
153
|
+
res.json(order);
|
|
158
154
|
})
|
|
159
155
|
);
|
|
160
156
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { onError, rateLimit, timeout } from "utils-decorators";
|
|
1
|
+
import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
|
|
2
2
|
import {
|
|
3
3
|
Order,
|
|
4
|
+
OrderDto,
|
|
4
5
|
OrderCreationDto,
|
|
5
6
|
ZodOrderCreationDto,
|
|
6
7
|
} from "./dto/order.dto";
|
|
7
|
-
import { Validate, ZodInput } from "
|
|
8
|
-
import { ResponseError } from "@/types";
|
|
9
|
-
import {
|
|
8
|
+
import { Validate, ZodInput } from "ts-zod4-decorators";
|
|
9
|
+
import { ResponseError } from "@/utilities/types";
|
|
10
|
+
import { parseId } from "@/utilities/error-handling";
|
|
11
|
+
import config from "@/config";
|
|
10
12
|
|
|
11
13
|
// Array to store orders (as a mock database)
|
|
12
14
|
const orders: Order[] = [
|
|
@@ -49,18 +51,12 @@ const orders: Order[] = [
|
|
|
49
51
|
|
|
50
52
|
function exceedHandler() {
|
|
51
53
|
const message = "Too much call in allowed window";
|
|
52
|
-
|
|
53
|
-
throw new Error(message, {
|
|
54
|
-
cause: { status: 500, message } satisfies ResponseError,
|
|
55
|
-
});
|
|
54
|
+
throw new ResponseError(message, 429);
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
function getOrderErrorHandler(e: Error) {
|
|
59
58
|
const message = "Order not found.";
|
|
60
|
-
|
|
61
|
-
throw new Error(message, {
|
|
62
|
-
cause: { status: 404, message, details: e.message } satisfies ResponseError,
|
|
63
|
-
});
|
|
59
|
+
throw new ResponseError(message, 404, e.message);
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
/**
|
|
@@ -70,8 +66,8 @@ function getOrderErrorHandler(e: Error) {
|
|
|
70
66
|
*/
|
|
71
67
|
export default class OrderController {
|
|
72
68
|
@rateLimit({
|
|
73
|
-
timeSpanMs:
|
|
74
|
-
allowedCalls:
|
|
69
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
70
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
75
71
|
exceedHandler,
|
|
76
72
|
})
|
|
77
73
|
@Validate
|
|
@@ -92,23 +88,25 @@ export default class OrderController {
|
|
|
92
88
|
return newOrder;
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
@
|
|
91
|
+
@memoizeAsync(config.memoizeTime)
|
|
92
|
+
@timeout(config.timeout)
|
|
96
93
|
@rateLimit({
|
|
97
|
-
timeSpanMs:
|
|
98
|
-
allowedCalls:
|
|
94
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
95
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
99
96
|
exceedHandler,
|
|
100
97
|
})
|
|
101
98
|
/**
|
|
102
99
|
* Retrieves all orders
|
|
103
100
|
* @returns List of orders
|
|
104
101
|
*/
|
|
105
|
-
public async getAll(): Promise<
|
|
102
|
+
public async getAll(): Promise<OrderDto[]> {
|
|
106
103
|
return orders;
|
|
107
104
|
}
|
|
108
105
|
|
|
106
|
+
@memoizeAsync(config.memoizeTime)
|
|
109
107
|
@rateLimit({
|
|
110
|
-
timeSpanMs:
|
|
111
|
-
allowedCalls:
|
|
108
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
109
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
112
110
|
exceedHandler,
|
|
113
111
|
})
|
|
114
112
|
@onError({
|
|
@@ -119,42 +117,33 @@ export default class OrderController {
|
|
|
119
117
|
* @param id - Order ID as string
|
|
120
118
|
* @returns Order details or error object if not found
|
|
121
119
|
*/
|
|
122
|
-
public async get(id: string): Promise<
|
|
123
|
-
const orderId =
|
|
124
|
-
if (typeof orderId != "number") return orderId satisfies ResponseError;
|
|
120
|
+
public async get(id: string): Promise<OrderDto> {
|
|
121
|
+
const orderId = parseId(id);
|
|
125
122
|
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
|
-
|
|
123
|
+
if (order == null) throw new ResponseError("User dose not exist.", 404);
|
|
133
124
|
return order satisfies Order;
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
@rateLimit({
|
|
137
|
-
timeSpanMs:
|
|
138
|
-
allowedCalls:
|
|
128
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
129
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
139
130
|
exceedHandler,
|
|
140
131
|
})
|
|
141
132
|
/**
|
|
142
133
|
* Cancel an existing order
|
|
143
134
|
* @param {string} id - Order ID to cancel
|
|
144
|
-
* @returns {Promise<Order
|
|
135
|
+
* @returns {Promise<Order>} Updated order or error object
|
|
145
136
|
* @throws {ResponseError} 404 - Order not found
|
|
146
137
|
* @throws {ResponseError} 400 - Invalid ID format or invalid status for cancellation
|
|
147
138
|
*/
|
|
148
|
-
public async cancel(id: string): Promise<
|
|
139
|
+
public async cancel(id: string): Promise<OrderDto> {
|
|
149
140
|
let order = await this.get(id);
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
"Cancellation is not available unless the order is in processing status.",
|
|
157
|
-
} satisfies ResponseError;
|
|
141
|
+
if (order.status != "Processing") {
|
|
142
|
+
throw new ResponseError(
|
|
143
|
+
"Cancellation is not available unless the order is in processing status.",
|
|
144
|
+
400
|
|
145
|
+
);
|
|
146
|
+
}
|
|
158
147
|
|
|
159
148
|
order.status = "Canceled";
|
|
160
149
|
order.deliveredAt = new Date();
|
|
@@ -162,27 +151,25 @@ export default class OrderController {
|
|
|
162
151
|
}
|
|
163
152
|
|
|
164
153
|
@rateLimit({
|
|
165
|
-
timeSpanMs:
|
|
166
|
-
allowedCalls:
|
|
154
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
155
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
167
156
|
exceedHandler,
|
|
168
157
|
})
|
|
169
158
|
/**
|
|
170
159
|
* Mark an order as delivered
|
|
171
160
|
* @param {string} id - Order ID to mark as delivered
|
|
172
|
-
* @returns {Promise<Order
|
|
161
|
+
* @returns {Promise<Order>} Updated order or error object
|
|
173
162
|
* @throws {ResponseError} 404 - Order not found
|
|
174
163
|
* @throws {ResponseError} 400 - Invalid ID format or invalid status for delivery
|
|
175
164
|
*/
|
|
176
|
-
public async deliver(id: string): Promise<
|
|
165
|
+
public async deliver(id: string): Promise<OrderDto> {
|
|
177
166
|
let order = await this.get(id);
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"Delivery is only available when the order is in processing status.",
|
|
185
|
-
} satisfies ResponseError;
|
|
167
|
+
if (order.status != "Processing") {
|
|
168
|
+
throw new ResponseError(
|
|
169
|
+
"Delivery is only available when the order is in processing status.",
|
|
170
|
+
400
|
|
171
|
+
);
|
|
172
|
+
}
|
|
186
173
|
|
|
187
174
|
order.status = "Delivered";
|
|
188
175
|
order.deliveredAt = new Date();
|
|
@@ -34,3 +34,4 @@ export const ZodProductUpdateDto = ZodProductCreationDto.partial();
|
|
|
34
34
|
export type Product = z.infer<typeof ZodProduct>;
|
|
35
35
|
export type ProductCreationDto = z.infer<typeof ZodProductCreationDto>;
|
|
36
36
|
export type ProductUpdateDto = z.infer<typeof ZodProductUpdateDto>;
|
|
37
|
+
export type ProductDto = z.infer<typeof ZodProduct>;
|