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
|
@@ -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();
|
|
@@ -105,9 +104,7 @@ router.get(
|
|
|
105
104
|
"/:id",
|
|
106
105
|
asyncHandler(async (req: Request, res: Response) => {
|
|
107
106
|
const product = await productController.get(req.params.id);
|
|
108
|
-
|
|
109
|
-
if ("id" in product == false) sendError(res, product);
|
|
110
|
-
else res.json(product);
|
|
107
|
+
res.json(product);
|
|
111
108
|
})
|
|
112
109
|
);
|
|
113
110
|
|
|
@@ -168,9 +165,7 @@ router.put(
|
|
|
168
165
|
"/:id",
|
|
169
166
|
asyncHandler(async (req: Request, res: Response) => {
|
|
170
167
|
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);
|
|
168
|
+
res.json(product);
|
|
174
169
|
})
|
|
175
170
|
);
|
|
176
171
|
|
|
@@ -198,9 +193,7 @@ router.delete(
|
|
|
198
193
|
"/:id",
|
|
199
194
|
asyncHandler(async (req: Request, res: Response) => {
|
|
200
195
|
const product = await productController.delete(req.params.id);
|
|
201
|
-
|
|
202
|
-
if ("id" in product == false) sendError(res, product);
|
|
203
|
-
else res.json(product);
|
|
196
|
+
res.json(product);
|
|
204
197
|
})
|
|
205
198
|
);
|
|
206
199
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { onError, rateLimit, timeout } from "utils-decorators";
|
|
1
|
+
import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
|
|
2
2
|
import {
|
|
3
3
|
Product,
|
|
4
4
|
ProductCreationDto,
|
|
5
|
+
ProductDto,
|
|
5
6
|
ProductUpdateDto,
|
|
6
7
|
ZodProductCreationDto,
|
|
7
8
|
ZodProductUpdateDto,
|
|
8
9
|
} from "./dto/product.dto";
|
|
9
|
-
import { Validate, ZodInput } from "
|
|
10
|
-
import { ResponseError } from "@/types";
|
|
11
|
-
import {
|
|
10
|
+
import { Validate, ZodInput } from "ts-zod4-decorators";
|
|
11
|
+
import { ResponseError } from "@/utilities/types";
|
|
12
|
+
import { parseId } from "@/utilities/error-handling";
|
|
13
|
+
import config from "@/config";
|
|
12
14
|
|
|
13
15
|
// Array to store products (as a mock database)
|
|
14
16
|
const products: Product[] = [
|
|
@@ -89,18 +91,12 @@ const products: Product[] = [
|
|
|
89
91
|
|
|
90
92
|
function exceedHandler() {
|
|
91
93
|
const message = "Too much call in allowed window";
|
|
92
|
-
|
|
93
|
-
throw new Error(message, {
|
|
94
|
-
cause: { status: 500, message } satisfies ResponseError,
|
|
95
|
-
});
|
|
94
|
+
throw new ResponseError(message, 429);
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
function getProductErrorHandler(e: Error) {
|
|
99
|
-
const message = "
|
|
100
|
-
|
|
101
|
-
throw new Error(message, {
|
|
102
|
-
cause: { status: 404, message, details: e.message } satisfies ResponseError,
|
|
103
|
-
});
|
|
98
|
+
const message = "Product not found.";
|
|
99
|
+
throw new ResponseError(message, 404, e.message);
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
/**
|
|
@@ -110,8 +106,8 @@ function getProductErrorHandler(e: Error) {
|
|
|
110
106
|
*/
|
|
111
107
|
export default class ProductController {
|
|
112
108
|
@rateLimit({
|
|
113
|
-
timeSpanMs:
|
|
114
|
-
allowedCalls:
|
|
109
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
110
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
115
111
|
exceedHandler,
|
|
116
112
|
})
|
|
117
113
|
@Validate
|
|
@@ -122,41 +118,40 @@ export default class ProductController {
|
|
|
122
118
|
*/
|
|
123
119
|
public async create(
|
|
124
120
|
@ZodInput(ZodProductCreationDto) product: ProductCreationDto
|
|
125
|
-
) {
|
|
121
|
+
): Promise<ProductDto> {
|
|
126
122
|
products.push({
|
|
127
123
|
...product,
|
|
128
124
|
id: products.length + 1,
|
|
129
125
|
} satisfies Product);
|
|
130
126
|
|
|
131
|
-
return product;
|
|
127
|
+
return product as ProductDto;
|
|
132
128
|
}
|
|
133
129
|
|
|
134
|
-
@
|
|
130
|
+
@memoizeAsync(config.memoizeTime)
|
|
131
|
+
@timeout(config.timeout)
|
|
135
132
|
@rateLimit({
|
|
136
|
-
timeSpanMs:
|
|
137
|
-
allowedCalls:
|
|
133
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
134
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
138
135
|
exceedHandler,
|
|
139
136
|
})
|
|
140
137
|
/**
|
|
141
138
|
* Retrieves all products with truncated descriptions
|
|
142
139
|
* @returns List of products with summarized descriptions
|
|
143
140
|
*/
|
|
144
|
-
public async getAll(): Promise<
|
|
145
|
-
return products.map(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
description: product.description?.substring(0, 50) + "..." || "",
|
|
150
|
-
} satisfies Product)
|
|
151
|
-
);
|
|
141
|
+
public async getAll(): Promise<ProductDto[]> {
|
|
142
|
+
return products.map((product) => ({
|
|
143
|
+
...product,
|
|
144
|
+
description: product.description?.substring(0, 50) + "..." || "",
|
|
145
|
+
}));
|
|
152
146
|
}
|
|
153
147
|
|
|
148
|
+
@memoizeAsync(config.memoizeTime)
|
|
154
149
|
@onError({
|
|
155
150
|
func: getProductErrorHandler,
|
|
156
151
|
})
|
|
157
152
|
@rateLimit({
|
|
158
|
-
timeSpanMs:
|
|
159
|
-
allowedCalls:
|
|
153
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
154
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
160
155
|
exceedHandler,
|
|
161
156
|
})
|
|
162
157
|
/**
|
|
@@ -164,23 +159,16 @@ export default class ProductController {
|
|
|
164
159
|
* @param id - Product ID as string
|
|
165
160
|
* @returns Product details or error object if not found
|
|
166
161
|
*/
|
|
167
|
-
public async get(id: string): Promise<
|
|
168
|
-
const productId =
|
|
169
|
-
if (typeof productId != "number") return productId satisfies ResponseError;
|
|
162
|
+
public async get(id: string): Promise<ProductDto> {
|
|
163
|
+
const productId = parseId(id);
|
|
170
164
|
const product = products.find((product) => product.id === productId);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
status: 404,
|
|
175
|
-
message: "Product dose not exist.",
|
|
176
|
-
} satisfies ResponseError;
|
|
177
|
-
|
|
178
|
-
return product satisfies Product;
|
|
165
|
+
if (product == null) throw new ResponseError("User dose not exist.", 404);
|
|
166
|
+
return product;
|
|
179
167
|
}
|
|
180
168
|
|
|
181
169
|
@rateLimit({
|
|
182
|
-
timeSpanMs:
|
|
183
|
-
allowedCalls:
|
|
170
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
171
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
184
172
|
exceedHandler,
|
|
185
173
|
})
|
|
186
174
|
@Validate
|
|
@@ -188,50 +176,42 @@ export default class ProductController {
|
|
|
188
176
|
* Updates an existing product
|
|
189
177
|
* @param {string} id - Product ID to update
|
|
190
178
|
* @param {ProductUpdateDto} updateData - Partial product data to update
|
|
191
|
-
* @returns {Promise<Product
|
|
179
|
+
* @returns {Promise<Product>} Updated product or error object
|
|
192
180
|
* @throws {ResponseError} 404 - Product not found
|
|
193
181
|
* @throws {ResponseError} 400 - Invalid ID format or update data
|
|
194
182
|
*/
|
|
195
183
|
public async update(
|
|
196
184
|
id: string,
|
|
197
185
|
@ZodInput(ZodProductUpdateDto) updateData: ProductUpdateDto
|
|
198
|
-
): Promise<
|
|
186
|
+
): Promise<ProductDto> {
|
|
199
187
|
const product = await this.get(id);
|
|
200
188
|
if ("id" in product == false) return product satisfies ResponseError;
|
|
201
189
|
|
|
202
|
-
if (product)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
} satisfies ResponseError;
|
|
190
|
+
if (product) {
|
|
191
|
+
Object.assign(product, updateData);
|
|
192
|
+
} else {
|
|
193
|
+
throw new ResponseError("Product dose not exist.", 404);
|
|
194
|
+
}
|
|
208
195
|
|
|
209
196
|
return product;
|
|
210
197
|
}
|
|
211
198
|
|
|
212
199
|
@rateLimit({
|
|
213
|
-
timeSpanMs:
|
|
214
|
-
allowedCalls:
|
|
200
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
201
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
215
202
|
exceedHandler,
|
|
216
203
|
})
|
|
217
204
|
/**
|
|
218
205
|
* Deletes a product by ID
|
|
219
206
|
* @param {string} id - Product ID to delete
|
|
220
|
-
* @returns {Promise<Product
|
|
207
|
+
* @returns {Promise<Product>} Deleted product or error object
|
|
221
208
|
* @throws {ResponseError} 404 - Product not found
|
|
222
209
|
* @throws {ResponseError} 400 - Invalid ID format
|
|
223
210
|
*/
|
|
224
|
-
public async delete(id: string): Promise<
|
|
225
|
-
const productId =
|
|
226
|
-
if (typeof productId != "number") return productId satisfies ResponseError;
|
|
211
|
+
public async delete(id: string): Promise<ProductDto> {
|
|
212
|
+
const productId = parseId(id);
|
|
227
213
|
const index = products.findIndex((product) => product.id === productId);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
status: 404,
|
|
232
|
-
message: "Product dose not exist.",
|
|
233
|
-
} satisfies ResponseError;
|
|
234
|
-
|
|
235
|
-
return products.splice(index, 1)[0] satisfies Product;
|
|
214
|
+
if (index == -1) throw new ResponseError("Product dose not exist.", 404);
|
|
215
|
+
return products.splice(index, 1)[0];
|
|
236
216
|
}
|
|
237
217
|
}
|
|
@@ -11,11 +11,13 @@ import { z } from "zod";
|
|
|
11
11
|
export const ZodUser = z.object({
|
|
12
12
|
id: z.number().min(1).int(),
|
|
13
13
|
username: z.string().min(3),
|
|
14
|
-
email: z.
|
|
14
|
+
email: z.email(),
|
|
15
15
|
password: z.string().min(6),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
export const ZodUserCreationDto = ZodUser.omit({ id: true });
|
|
19
|
+
export const ZodUserDto = ZodUser.omit({ password: true });
|
|
19
20
|
|
|
20
21
|
export type User = z.infer<typeof ZodUser>;
|
|
21
22
|
export type UserCreationDto = z.infer<typeof ZodUserCreationDto>;
|
|
23
|
+
export type UserDto = z.infer<typeof ZodUserDto>;
|
|
@@ -1,7 +1,6 @@
|
|
|
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 { sendError } from "@/utilities";
|
|
5
4
|
|
|
6
5
|
const router = Router();
|
|
7
6
|
const userController = new UserController();
|
|
@@ -73,9 +72,7 @@ router.get(
|
|
|
73
72
|
"/:id",
|
|
74
73
|
asyncHandler(async (req: Request, res: Response) => {
|
|
75
74
|
const user = await userController.get(req.params.id);
|
|
76
|
-
|
|
77
|
-
if ("id" in user == false) sendError(res, user);
|
|
78
|
-
else res.json(user);
|
|
75
|
+
res.json(user);
|
|
79
76
|
})
|
|
80
77
|
);
|
|
81
78
|
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
User,
|
|
3
|
+
ZodUserCreationDto,
|
|
4
|
+
UserCreationDto,
|
|
5
|
+
UserDto,
|
|
6
|
+
} from "./dto/user.dto";
|
|
7
|
+
import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
|
|
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";
|
|
6
12
|
|
|
7
13
|
// Array to store users (as a mock database)
|
|
8
14
|
const users = [
|
|
@@ -70,18 +76,12 @@ const users = [
|
|
|
70
76
|
|
|
71
77
|
function exceedHandler() {
|
|
72
78
|
const message = "Too much call in allowed window";
|
|
73
|
-
|
|
74
|
-
throw new Error(message, {
|
|
75
|
-
cause: { status: 500, message } satisfies ResponseError,
|
|
76
|
-
});
|
|
79
|
+
throw new ResponseError(message, 429);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
function getUserErrorHandler(e: Error) {
|
|
80
83
|
const message = "User not found.";
|
|
81
|
-
|
|
82
|
-
throw new Error(message, {
|
|
83
|
-
cause: { status: 404, message, details: e.message } satisfies ResponseError,
|
|
84
|
-
});
|
|
84
|
+
throw new ResponseError(message, 404, e.message);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
@@ -93,8 +93,8 @@ export default class UserController {
|
|
|
93
93
|
// constructor(private readonly userService: UserService) { }
|
|
94
94
|
|
|
95
95
|
@rateLimit({
|
|
96
|
-
timeSpanMs:
|
|
97
|
-
allowedCalls:
|
|
96
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
97
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
98
98
|
exceedHandler,
|
|
99
99
|
})
|
|
100
100
|
@Validate
|
|
@@ -106,42 +106,37 @@ export default class UserController {
|
|
|
106
106
|
* @throws {ResponseError} 400 - Invalid input data
|
|
107
107
|
*/
|
|
108
108
|
public async create(@ZodInput(ZodUserCreationDto) user: UserCreationDto) {
|
|
109
|
-
users.push({ ...user, id: users.length + 1 }
|
|
109
|
+
users.push({ ...user, id: users.length + 1 });
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
@memoizeAsync(config.memoizeTime)
|
|
112
113
|
@onError({
|
|
113
114
|
func: getUserErrorHandler,
|
|
114
115
|
})
|
|
115
116
|
@rateLimit({
|
|
116
|
-
timeSpanMs:
|
|
117
|
-
allowedCalls:
|
|
117
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
118
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
118
119
|
exceedHandler,
|
|
119
120
|
})
|
|
120
121
|
/**
|
|
121
122
|
* Get user by ID
|
|
122
123
|
* @param {string} id - User ID as string
|
|
123
|
-
* @returns {Promise<User
|
|
124
|
+
* @returns {Promise<User>} User details or error object
|
|
124
125
|
* @throws {ResponseError} 404 - User not found
|
|
125
126
|
* @throws {ResponseError} 400 - Invalid ID format
|
|
126
127
|
*/
|
|
127
|
-
public async get(id: string): Promise<
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!user)
|
|
133
|
-
return {
|
|
134
|
-
status: 404,
|
|
135
|
-
message: "User dose not exist.",
|
|
136
|
-
} satisfies ResponseError;
|
|
137
|
-
|
|
128
|
+
public async get(id: string): Promise<UserDto> {
|
|
129
|
+
const response = parseId(id);
|
|
130
|
+
const user = users.find((user) => user.id === response);
|
|
131
|
+
if (user == null) throw new ResponseError("User dose not exist.", 404);
|
|
138
132
|
return user satisfies User;
|
|
139
133
|
}
|
|
140
134
|
|
|
141
|
-
@
|
|
135
|
+
@memoizeAsync(config.memoizeTime)
|
|
136
|
+
@timeout(config.timeout)
|
|
142
137
|
@rateLimit({
|
|
143
|
-
timeSpanMs:
|
|
144
|
-
allowedCalls:
|
|
138
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
139
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
145
140
|
exceedHandler,
|
|
146
141
|
})
|
|
147
142
|
/**
|
|
@@ -149,13 +144,10 @@ export default class UserController {
|
|
|
149
144
|
* @returns {Promise<User[]>} List of users with hidden password fields
|
|
150
145
|
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
151
146
|
*/
|
|
152
|
-
public async getAll(): Promise<
|
|
153
|
-
return users.map(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
password: "?",
|
|
158
|
-
} satisfies User)
|
|
159
|
-
);
|
|
147
|
+
public async getAll(): Promise<UserDto[]> {
|
|
148
|
+
return users.map((user) => ({
|
|
149
|
+
...user,
|
|
150
|
+
password: "?",
|
|
151
|
+
}));
|
|
160
152
|
}
|
|
161
153
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memoizeTime,
|
|
3
|
+
productionEnvironment,
|
|
4
|
+
rateLimitTimeSpan,
|
|
5
|
+
rateLimitAllowedCalls,
|
|
6
|
+
timeout,
|
|
7
|
+
portNumber,
|
|
8
|
+
} from "./constants";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Server configuration interface
|
|
12
|
+
* @interface
|
|
13
|
+
* @property {string} url - Base server URL
|
|
14
|
+
*/
|
|
15
|
+
interface Server {
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* API information structure
|
|
21
|
+
* @interface
|
|
22
|
+
* @property {string} title - API title
|
|
23
|
+
* @property {string} version - API version
|
|
24
|
+
* @property {string} description - API description
|
|
25
|
+
*/
|
|
26
|
+
interface Info {
|
|
27
|
+
title: string;
|
|
28
|
+
version: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Swagger definition structure
|
|
34
|
+
* @interface
|
|
35
|
+
* @property {string} openApi - OpenAPI specification version
|
|
36
|
+
* @property {Info} info - API information
|
|
37
|
+
* @property {Server[]} servers - List of server configurations
|
|
38
|
+
*/
|
|
39
|
+
interface SwaggerDefinition {
|
|
40
|
+
openApi: string;
|
|
41
|
+
info: Info;
|
|
42
|
+
servers: Server[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Swagger configuration options
|
|
47
|
+
* @interface
|
|
48
|
+
* @property {SwaggerDefinition} swaggerDefinition - Swagger definition object
|
|
49
|
+
* @property {string[]} apis - Paths to API documentation files
|
|
50
|
+
*/
|
|
51
|
+
export interface SwaggerOptions {
|
|
52
|
+
swaggerDefinition: SwaggerDefinition;
|
|
53
|
+
apis: string[];
|
|
54
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z, ZodRawShape } from "zod";
|
|
2
|
+
import { ResponseError } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Strictly convert obj (type T1) to T2 using a Zod schema.
|
|
6
|
+
*
|
|
7
|
+
* - Throws if obj has extra fields beyond those defined in the schema.
|
|
8
|
+
* - Validates fields with the schema; on failure, throws with a descriptive message.
|
|
9
|
+
* - Returns an object typed as T2 (inferred from the schema).
|
|
10
|
+
*
|
|
11
|
+
* @param obj - Source object of type T1
|
|
12
|
+
* @param schema - Zod schema describing the target type T2
|
|
13
|
+
* @returns T2 inferred from the provided schema
|
|
14
|
+
*/
|
|
15
|
+
export function assignStrictlyFromSchema<T1 extends object, T2 extends object>(
|
|
16
|
+
obj: T1,
|
|
17
|
+
schema: z.ZodObject<any>
|
|
18
|
+
): T2 {
|
|
19
|
+
// 1) Derive the runtime keys from the schema's shape
|
|
20
|
+
const shape = (schema as any)._def?.shape as ZodRawShape | undefined;
|
|
21
|
+
if (!shape) {
|
|
22
|
+
throw new ResponseError(
|
|
23
|
+
"assignStrictlyFromSchema: provided schema has no shape.",
|
|
24
|
+
500
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const keysSchema = Object.keys(shape) as Array<keyof any>;
|
|
29
|
+
|
|
30
|
+
// 2) Extra keys check
|
|
31
|
+
const objKeys = Object.keys(obj) as Array<keyof T1>;
|
|
32
|
+
const extraKeys = objKeys.filter((k) => !keysSchema.includes(k as any));
|
|
33
|
+
if (extraKeys.length > 0) {
|
|
34
|
+
throw new ResponseError(
|
|
35
|
+
`assignStrictlyFromSchema: source object contains extra field(s) not present on target: ${extraKeys.join(
|
|
36
|
+
", "
|
|
37
|
+
)}`,
|
|
38
|
+
500
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3) Required-field check for T2 (all keys in schema must be present and non-undefined)
|
|
43
|
+
const missingOrUndefined = keysSchema.filter((k) => {
|
|
44
|
+
const v = (obj as any)[k];
|
|
45
|
+
return v === undefined || v === null;
|
|
46
|
+
});
|
|
47
|
+
if (missingOrUndefined.length > 0) {
|
|
48
|
+
throw new ResponseError(
|
|
49
|
+
`assignStrictlyFromSchema: missing required field(s): ${missingOrUndefined.join(
|
|
50
|
+
", "
|
|
51
|
+
)}`,
|
|
52
|
+
500
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4) Build a plain object to pass through Zod for validation
|
|
57
|
+
const candidate: any = {};
|
|
58
|
+
for (const k of keysSchema) {
|
|
59
|
+
if (k in (obj as any)) {
|
|
60
|
+
candidate[k] = (obj as any)[k];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 5) Validate against the schema
|
|
65
|
+
const result = schema.safeParse(candidate);
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
// Modern, non-format error reporting
|
|
68
|
+
const issues = result.error.issues.map((i) => ({
|
|
69
|
+
path: i.path, // where the issue occurred
|
|
70
|
+
message: i.message, // human-friendly message
|
|
71
|
+
code: i.code, // e.g., "too_small", "invalid_type"
|
|
72
|
+
}));
|
|
73
|
+
// You can log issues or throw a structured error
|
|
74
|
+
throw new Error(
|
|
75
|
+
`assignStrictlyFromSchema: validation failed: ${JSON.stringify(issues)}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 6) Return the validated data typed as T2
|
|
80
|
+
return result.data as T2;
|
|
81
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Response } from "express";
|
|
2
|
+
import { ReturnInfo, ResponseError } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sends a standardized HTTP error response.
|
|
6
|
+
*
|
|
7
|
+
* This function sets the response status from the provided error (defaulting to 500)
|
|
8
|
+
* and serializes the error object as JSON.
|
|
9
|
+
*
|
|
10
|
+
* @param res - Express Response object to send the error on
|
|
11
|
+
* @param error - Error details to return to the client (must include status or default will be 500)
|
|
12
|
+
*/
|
|
13
|
+
export function sendHttpError(res: Response, error: ResponseError): void {
|
|
14
|
+
res.status(error.status ?? 500).json(error);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Executes a function and captures a potential error as a ReturnInfo tuple.
|
|
19
|
+
*
|
|
20
|
+
* Returns a two-element tuple: [value, error]
|
|
21
|
+
* - value: the function result if it succeeds; null if an exception is thrown
|
|
22
|
+
* - error: the caught Error wrapped as a ResponseError (or the provided error) if the function throws; null if the function succeeds
|
|
23
|
+
*
|
|
24
|
+
* This utility helps avoid try/catch blocks at call sites by returning both the
|
|
25
|
+
* result and any error in a single value.
|
|
26
|
+
*
|
|
27
|
+
* @template T
|
|
28
|
+
* @param func - The function to execute
|
|
29
|
+
* @param error - The error object to return when an exception occurs (typically a ResponseError). If no error is provided, null is used.
|
|
30
|
+
* @returns ReturnInfo<T> A tuple: [value or null, error or null]
|
|
31
|
+
*/
|
|
32
|
+
export function tryCatch<T>(
|
|
33
|
+
func: () => T,
|
|
34
|
+
error: ResponseError | null
|
|
35
|
+
): ReturnInfo<T> {
|
|
36
|
+
try {
|
|
37
|
+
return [func(), null];
|
|
38
|
+
} catch {
|
|
39
|
+
return [null, error];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parses a string input into a number (ID) with basic validation.
|
|
45
|
+
*
|
|
46
|
+
* If parsing fails, this function throws a ResponseError describing the invalid input.
|
|
47
|
+
*
|
|
48
|
+
* @param input - The string to parse as an integer ID
|
|
49
|
+
* @returns The parsed number
|
|
50
|
+
* @throws {ResponseError} When the input cannot be parsed as an integer
|
|
51
|
+
*/
|
|
52
|
+
export function parseId(input: string): number {
|
|
53
|
+
try {
|
|
54
|
+
// parseInt may yield NaN for non-numeric input; this example mirrors the original behavior
|
|
55
|
+
// If you want stricter validation, you can check isNaN and throw a more explicit error.
|
|
56
|
+
return parseInt(input);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new ResponseError(
|
|
59
|
+
input,
|
|
60
|
+
400,
|
|
61
|
+
"Wrong input",
|
|
62
|
+
"The id parameter must be an integer number."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Creates a successful result from a ReturnInfo tuple.
|
|
69
|
+
*
|
|
70
|
+
* Given a ReturnInfo<T> of the form [value, error], this returns the value
|
|
71
|
+
* when the operation succeeded, or null when there was an error.
|
|
72
|
+
*
|
|
73
|
+
* @template T
|
|
74
|
+
* @param input - The ReturnInfo tuple
|
|
75
|
+
* @returns The successful value of type T, or null if there was an error
|
|
76
|
+
*/
|
|
77
|
+
export function successfulResult<T>(input: ReturnInfo<T>): T | null {
|
|
78
|
+
return input[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Normalizes and wraps an error into the common ReturnInfo shape.
|
|
83
|
+
*
|
|
84
|
+
* The function accepts a ReturnInfo-shaped input and extracts the error portion.
|
|
85
|
+
* If a non-Error value is provided, you should wrap it as a ResponseError beforehand.
|
|
86
|
+
* If the error is already a ResponseError, it is returned as-is.
|
|
87
|
+
*
|
|
88
|
+
* @template T
|
|
89
|
+
* @param responseError - The error to wrap, either as a ResponseError or as a ReturnInfo<T> where the error is at index 1
|
|
90
|
+
* @returns The extracted or wrapped ResponseError, or null if there is no error
|
|
91
|
+
*/
|
|
92
|
+
export function error<T>(responseError: ReturnInfo<T>): ResponseError | null {
|
|
93
|
+
return responseError[1];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Determines whether a ReturnInfo value represents a successful operation.
|
|
98
|
+
*
|
|
99
|
+
* A result is considered successful when there is no error (i.e., the error portion is null).
|
|
100
|
+
*
|
|
101
|
+
* @template T
|
|
102
|
+
* @param result - The ReturnInfo tuple [value | null, error | null]
|
|
103
|
+
* @returns true if there is no error; false otherwise
|
|
104
|
+
*/
|
|
105
|
+
export function isSuccessful<T>(result: ReturnInfo<T>): boolean {
|
|
106
|
+
return result[1] === null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Indicates whether a ReturnInfo value represents an error.
|
|
111
|
+
*
|
|
112
|
+
* This is the logical negation of isSuccess for a given ReturnInfo.
|
|
113
|
+
*
|
|
114
|
+
* @template T
|
|
115
|
+
* @param result - The ReturnInfo tuple [value | null, error | null]
|
|
116
|
+
* @returns true if an error is present (i.e., error is not null); false otherwise
|
|
117
|
+
*/
|
|
118
|
+
export function hasError<T>(result: ReturnInfo<T>): boolean {
|
|
119
|
+
return result[1] !== null;
|
|
120
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a standardized error response structure for API endpoints
|
|
3
|
+
* @class
|
|
4
|
+
* @property {number} [status] - HTTP status code
|
|
5
|
+
* @property {string} [name] - Error name/type
|
|
6
|
+
* @property {string} message - Human-readable error message
|
|
7
|
+
* @property {string} [stack] - Error stack trace (development only)
|
|
8
|
+
* @property {string} [details] - Additional error details
|
|
9
|
+
*/
|
|
10
|
+
export class ResponseError extends Error {
|
|
11
|
+
public constructor(
|
|
12
|
+
public message: string,
|
|
13
|
+
public status?: number,
|
|
14
|
+
public details?: string,
|
|
15
|
+
public code?: string,
|
|
16
|
+
public stack?: string
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ReturnInfo<T> = [T | null, ResponseError | null];
|
|
23
|
+
export type AsyncReturnInfo<T> = Promise<[T | null, ResponseError | null]>;
|