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,89 +1,35 @@
1
- import { User, ZodUserCreationDto, UserCreationDto } from "./dto/user.dto";
2
- import { onError, rateLimit, timeout } from "utils-decorators";
3
- import { Validate, ZodInput } from "@pkg/ts-zod-decorators";
4
- import { ResponseError } from "@/types";
5
- import { tryParseId } from "@/utilities";
6
-
7
- // Array to store users (as a mock database)
8
- const users = [
9
- {
10
- id: 1,
11
- username: "johndoe",
12
- email: "johndoe@gmail.com",
13
- password: "S3cur3P@ssw0rd",
14
- },
15
- {
16
- id: 2,
17
- username: "janesmith",
18
- email: "janesmith@yahoo.com",
19
- password: "P@ssw0rd2024",
20
- },
21
- {
22
- id: 3,
23
- username: "michael89",
24
- email: "michael89@hotmail.com",
25
- password: "M1chael!2024",
26
- },
27
- {
28
- id: 4,
29
- username: "lisa.wong",
30
- email: "lisa.wong@example.com",
31
- password: "L1saW0ng!2024",
32
- },
33
- {
34
- id: 5,
35
- username: "alex_k",
36
- email: "alex.k@gmail.com",
37
- password: "A1ex#Key2024",
38
- },
39
- {
40
- id: 6,
41
- username: "emilyj",
42
- email: "emilyj@hotmail.com",
43
- password: "Em!ly0101",
44
- },
45
- {
46
- id: 7,
47
- username: "davidparker",
48
- email: "david.parker@yahoo.com",
49
- password: "D@v!d2024",
50
- },
51
- {
52
- id: 8,
53
- username: "sophia_m",
54
- email: "sophia.m@gmail.com",
55
- password: "Sophi@2024",
56
- },
57
- {
58
- id: 9,
59
- username: "chrisw",
60
- email: "chrisw@outlook.com",
61
- password: "Chri$Wong21",
62
- },
63
- {
64
- id: 10,
65
- username: "natalie_b",
66
- email: "natalie_b@gmail.com",
67
- password: "N@talie#B2024",
68
- },
69
- ];
1
+ import {
2
+ ZodUserCreationDto,
3
+ UserCreationDto,
4
+ UserDto,
5
+ ZodUserDto,
6
+ } from "./dto/user.dto";
7
+ import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
8
+ import { ResponseError } from "@/utilities/error-handling";
9
+ import { convert, stringToInteger } from "@/utilities/conversion";
10
+ import config from "@/config";
11
+ import { users } from "@/db";
12
+ import { User } from "@/entities/user.entity";
13
+ import { MapAsyncCache } from "@/utilities/cache/memory-cache";
70
14
 
71
15
  function exceedHandler() {
72
16
  const message = "Too much call in allowed window";
73
-
74
- throw new Error(message, {
75
- cause: { status: 500, message } satisfies ResponseError,
76
- });
17
+ throw new ResponseError(message, 429);
77
18
  }
78
19
 
79
- function getUserErrorHandler(e: Error) {
20
+ function userNotFoundHandler(e: ResponseError) {
80
21
  const message = "User not found.";
22
+ throw new ResponseError(message, 404, e?.message);
23
+ }
81
24
 
82
- throw new Error(message, {
83
- cause: { status: 404, message, details: e.message } satisfies ResponseError,
84
- });
25
+ function invalidInputHandler(e: ResponseError) {
26
+ const message = "Invalid input";
27
+ throw new ResponseError(message, 400, e?.message);
85
28
  }
86
29
 
30
+ const usersCache = new MapAsyncCache<UserDto[]>(config.cacheSize);
31
+ const userCache = new MapAsyncCache<UserDto>(config.cacheSize);
32
+
87
33
  /**
88
34
  * Controller for handling user-related operations
89
35
  * @class UserController
@@ -92,70 +38,93 @@ function getUserErrorHandler(e: Error) {
92
38
  export default class UserController {
93
39
  // constructor(private readonly userService: UserService) { }
94
40
 
41
+ @onError({
42
+ func: invalidInputHandler,
43
+ })
44
+ /**
45
+ * Validates a string ID and converts it to a number.
46
+ *
47
+ * @param {string} id - The ID to validate and convert.
48
+ * @returns {number} The numeric value of the provided ID.
49
+ */
50
+ public async validateId(id: string): Promise<number> {
51
+ return stringToInteger(id);
52
+ }
53
+
54
+ @onError({
55
+ func: invalidInputHandler,
56
+ })
57
+ /**
58
+ * Validates and creates a new User from the given DTO.
59
+ *
60
+ * @param {UserCreationDto} user - The incoming UserCreationDto to validate and transform.
61
+ * @returns {User} A fully formed User object ready for persistence.
62
+ */
63
+ public async validateUserCreationDto(user: UserCreationDto): Promise<User> {
64
+ const newUser = await ZodUserCreationDto.parseAsync(user);
65
+ return { ...newUser, id: users.length + 1 };
66
+ }
67
+
95
68
  @rateLimit({
96
- timeSpanMs: 60000,
97
- allowedCalls: 300,
69
+ timeSpanMs: config.rateLimitTimeSpan,
70
+ allowedCalls: config.rateLimitAllowedCalls,
98
71
  exceedHandler,
99
72
  })
100
- @Validate
101
73
  /**
102
74
  * Create a new user
103
- * @param {UserCreationDto} user - User creation data validated by Zod schema
75
+ * @param {User} user - User creation data validated by Zod schema
104
76
  * @returns {Promise<void>}
105
77
  * @throws {ResponseError} 500 - When rate limit exceeded
106
78
  * @throws {ResponseError} 400 - Invalid input data
107
79
  */
108
- public async create(@ZodInput(ZodUserCreationDto) user: UserCreationDto) {
109
- users.push({ ...user, id: users.length + 1 } satisfies User);
80
+ public async create(user: User): Promise<void> {
81
+ users.push(user);
82
+ await userCache.set(user.id.toString(), user as User);
83
+ await usersCache.delete("key");
110
84
  }
111
85
 
112
- @onError({
113
- func: getUserErrorHandler,
86
+ @memoizeAsync({
87
+ cache: usersCache,
88
+ keyResolver: () => "key",
89
+ expirationTimeMs: config.memoizeTime,
114
90
  })
91
+ @timeout(config.timeout)
115
92
  @rateLimit({
116
- timeSpanMs: 60000,
117
- allowedCalls: 300,
93
+ timeSpanMs: config.rateLimitTimeSpan,
94
+ allowedCalls: config.rateLimitAllowedCalls,
118
95
  exceedHandler,
119
96
  })
120
97
  /**
121
- * Get user by ID
122
- * @param {string} id - User ID as string
123
- * @returns {Promise<User | ResponseError>} User details or error object
124
- * @throws {ResponseError} 404 - User not found
125
- * @throws {ResponseError} 400 - Invalid ID format
98
+ * Get all users
99
+ * @returns {Promise<UserDto[]>} List of users with hidden password fields
100
+ * @throws {ResponseError} 500 - When rate limit exceeded
126
101
  */
127
- public async get(id: string): Promise<User | ResponseError> {
128
- const userId = tryParseId(id);
129
- if (typeof userId != "number") return userId satisfies ResponseError;
130
- const user = users.find((user) => user.id === userId);
131
-
132
- if (!user)
133
- return {
134
- status: 404,
135
- message: "User dose not exist.",
136
- } satisfies ResponseError;
137
-
138
- return user satisfies User;
102
+ public async getAll(): Promise<UserDto[]> {
103
+ return users as UserDto[];
139
104
  }
140
105
 
141
- @timeout(20000)
106
+ @memoizeAsync({
107
+ cache: userCache,
108
+ keyResolver: (id: number) => id.toString(),
109
+ expirationTimeMs: config.memoizeTime,
110
+ })
142
111
  @rateLimit({
143
- timeSpanMs: 60000,
144
- allowedCalls: 300,
112
+ timeSpanMs: config.rateLimitTimeSpan,
113
+ allowedCalls: config.rateLimitAllowedCalls,
145
114
  exceedHandler,
146
115
  })
147
116
  /**
148
- * Get all users with masked passwords
149
- * @returns {Promise<User[]>} List of users with hidden password fields
150
- * @throws {ResponseError} 500 - When rate limit exceeded
117
+ * Get user by ID
118
+ * @param {number} id - User ID as string
119
+ * @returns {Promise<UserDto>} User details or error object
120
+ * @throws {ResponseError} 404 - User not found
121
+ * @throws {ResponseError} 400 - Invalid ID format
151
122
  */
152
- public async getAll(): Promise<User[]> {
153
- return users.map(
154
- (user) =>
155
- ({
156
- ...user,
157
- password: "?",
158
- } satisfies User)
159
- );
123
+ public async get(id: number): Promise<UserDto> {
124
+ const user = users.find((user) => user.id === id);
125
+ if (user == null) {
126
+ throw new ResponseError("Product not found");
127
+ }
128
+ return convert(user!, ZodUserDto);
160
129
  }
161
130
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Server configuration interface
3
+ */
4
+ interface Server {
5
+ /** URL of the base server */
6
+ url: string;
7
+ }
8
+
9
+ /**
10
+ * API information structure
11
+ */
12
+ interface Info {
13
+ /** Title of the API */
14
+ title: string;
15
+ /** Version of the API */
16
+ version: string;
17
+ /** Description of the API */
18
+ description: string;
19
+ }
20
+
21
+ /**
22
+ * Swagger definition structure
23
+ */
24
+ interface SwaggerDefinition {
25
+ /** OpenAPI specification version (e.g., "3.0.0") */
26
+ openApi: string;
27
+ /** API information object */
28
+ info: Info;
29
+ /** List of server configurations */
30
+ servers: Server[];
31
+ }
32
+
33
+ /**
34
+ * Swagger configuration options
35
+ */
36
+ export interface SwaggerOptions {
37
+ swaggerDefinition: SwaggerDefinition;
38
+ apis: string[];
39
+ }
@@ -0,0 +1,66 @@
1
+ import { z, ZodRawShape } from "zod";
2
+ import { ResponseError } from "./error-handling";
3
+
4
+ /**
5
+ * Strictly assign obj (type T1) to T2 using a Zod schema.
6
+ *
7
+ * - Extras in source are ignored (no throw).
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 source - Source object of type T1
12
+ * @param destination - Destination object to be populated (typed as T2)
13
+ * @param schema - Zod schema describing the target type T2
14
+ * @returns T2 representing the destination after assignment
15
+ */
16
+ export default function assign<T1 extends object, T2 extends object>(
17
+ source: T1,
18
+ destination: T2,
19
+ schema: z.ZodObject<any>,
20
+ ignoreNullAndUndefined: false
21
+ ): T2 {
22
+ // 1) Validate the subset of keys defined in the schema
23
+ // Build an object that contains only the keys present in source but are part of the schema
24
+ const subsetForSchema: any = {};
25
+
26
+ // Iterate schema keys
27
+ for (const key of Object.keys(schema.shape)) {
28
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
29
+ if (ignoreNullAndUndefined) {
30
+ subsetForSchema[key] = (source as any)[key] ?? subsetForSchema[key];
31
+ } else {
32
+ subsetForSchema[key] = (source as any)[key];
33
+ }
34
+ }
35
+ }
36
+
37
+ // Validate using the schema on the subset (this will also coerce if the schema has transforms)
38
+ const parseResult = schema.safeParse(subsetForSchema);
39
+ if (!parseResult.success) {
40
+ // Build a descriptive error message from the first issue
41
+ const issue = parseResult.error.issues?.[0];
42
+ const path = issue?.path?.length ? issue.path.join(".") : "value";
43
+ const message = issue?.message ?? "Schema validation failed";
44
+ throw new ResponseError(
45
+ `assignStrictlyFromSchema: Validation failed for "${path}": ${message}`,
46
+ 500
47
+ );
48
+ }
49
+
50
+ // 2) Assign validated values to destination strictly
51
+ // Use the parsed result to ensure types align with the schema
52
+ const validated = parseResult.data as any;
53
+
54
+ // Copy only keys that are in the schema
55
+ for (const key of Object.keys(schema.shape)) {
56
+ if (Object.prototype.hasOwnProperty.call(validated, key)) {
57
+ if (ignoreNullAndUndefined) {
58
+ subsetForSchema[key] = (source as any)[key] ?? subsetForSchema[key];
59
+ } else {
60
+ subsetForSchema[key] = (source as any)[key];
61
+ }
62
+ }
63
+ }
64
+
65
+ return destination;
66
+ }
@@ -0,0 +1,74 @@
1
+ import { AsyncCache } from "utils-decorators";
2
+
3
+ /**
4
+ * A simple in-memory map-based cache implementing AsyncCache<T>.
5
+ * This is a straightforward wrapper around a Map<string, T>.
6
+ */
7
+ export class MapAsyncCache<T> implements AsyncCache<T> {
8
+ public constructor(
9
+ private capacity?: number,
10
+ private cache?: Map<string, T>
11
+ ) {
12
+ this.capacity = this.capacity ?? Number.POSITIVE_INFINITY;
13
+ this.cache = this.cache ?? new Map<string, T>();
14
+ }
15
+
16
+ /**
17
+ * Asynchronously set a value by key.
18
+ * @param key The cache key
19
+ * @param value The value to cache
20
+ */
21
+ async set(key: string, value: T): Promise<void> {
22
+ if (value != null) {
23
+ if (this.cache?.has(key)) {
24
+ this.cache?.set(key, value);
25
+ } else {
26
+ if (
27
+ (this.capacity ?? Number.POSITIVE_INFINITY) > (this.cache?.size ?? 0)
28
+ ) {
29
+ this.cache?.set(key, value);
30
+ }
31
+ }
32
+ } else {
33
+ this.cache?.delete(key);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Asynchronously get a value by key.
39
+ * - If the key exists, returns the value.
40
+ * - If the key does not exist, returns undefined cast to T to satisfy the Promise<T> return type.
41
+ *
42
+ * Note: Returning undefined may be surprising for callers expecting a strict A/B.
43
+ * Consider returning `T | undefined` from AsyncCache if you can adjust the interface,
44
+ * or throw a ResponseError for "not found" depending on your usage.
45
+ */
46
+ async get(key: string): Promise<T> {
47
+ if (this.cache?.has(key)) {
48
+ return this.cache?.get(key) ?? (null as T);
49
+ }
50
+
51
+ return null as T;
52
+ }
53
+
54
+ /**
55
+ * Asynchronously delete a value by key.
56
+ */
57
+ async delete(key: string): Promise<void> {
58
+ this.cache?.delete(key);
59
+ }
60
+
61
+ /**
62
+ * Asynchronously check if a key exists in the cache.
63
+ */
64
+ async has(key: string): Promise<boolean> {
65
+ return this.cache?.has(key) ?? false;
66
+ }
67
+
68
+ /**
69
+ * Asynchronously clear the cache.
70
+ */
71
+ async clear(key: string): Promise<void> {
72
+ this.cache?.clear();
73
+ }
74
+ }
@@ -0,0 +1,111 @@
1
+ import { AsyncCache } from "utils-decorators";
2
+ import { Redis } from "ioredis";
3
+
4
+ /**
5
+ * A Redis-backed cache adapter implementing AsyncCache<T | null>.
6
+ * This wraps a Redis instance and provides a similar API to MapAsyncCache.
7
+ */
8
+ export class RedisCache<T> implements AsyncCache<T> {
9
+ public constructor(
10
+ private cache?: Redis,
11
+ private capacity?: number,
12
+ private namespace?: string
13
+ ) {
14
+ this.cache = this.cache ?? new Redis();
15
+ this.capacity = this.capacity ?? Number.POSITIVE_INFINITY;
16
+ this.namespace = this.namespace ?? "redis";
17
+ }
18
+
19
+ private keyFor(key: string): string {
20
+ return `${this.namespace}:${key}`;
21
+ }
22
+
23
+ /**
24
+ * Asynchronously set a value by key.
25
+ * If value is null, delete the key to represent "not present".
26
+ * Capacity is not strictly enforced in Redis on a per-key basis here; you
27
+ * could leverage Redis max memory and eviction policies for that.
28
+ */
29
+ async set(key: string, value: T): Promise<void> {
30
+ const k = this.keyFor(key);
31
+
32
+ if (value != null) {
33
+ if (await this.has(key)) {
34
+ await this.cache?.set(k, JSON.stringify(value));
35
+ } else {
36
+ if (
37
+ (this.capacity ?? Number.POSITIVE_INFINITY) >
38
+ ((await this.cache?.dbsize()) ?? 0)
39
+ ) {
40
+ await this.cache?.set(k, JSON.stringify(value));
41
+ }
42
+ }
43
+ } else {
44
+ await this.cache?.del(k);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Asynchronously get a value by key.
50
+ * - If the key exists, returns the parsed value.
51
+ * - If the key does not exist, returns null to satisfy Promise<T | null>.
52
+ */
53
+ async get(key: string): Promise<T> {
54
+ const k = this.keyFor(key);
55
+ const raw = (await this.cache?.get(k)) ?? null;
56
+ if (raw == null) {
57
+ return null as T;
58
+ }
59
+ try {
60
+ return JSON.parse(raw) as T;
61
+ } catch {
62
+ return raw as T;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Asynchronously delete a value by key.
68
+ */
69
+ async delete(key: string): Promise<void> {
70
+ await this.cache?.del(this.keyFor(key));
71
+ }
72
+
73
+ /**
74
+ * Asynchronously check if a key exists in the cache.
75
+ */
76
+ async has(key: string): Promise<boolean> {
77
+ const exists = await this.cache?.exists(this.keyFor(key));
78
+ return exists === 1;
79
+ }
80
+
81
+ /**
82
+ * Asynchronously clear the cache (namespace-scoped).
83
+ * Use with caution in a shared Redis instance.
84
+ */
85
+ async clear(key: string): Promise<void> {
86
+ // Optional: implement namespace-wide clear if needed
87
+ // This simple example uses a pattern-based approach for a full clear.
88
+ const pattern = `${this.namespace}:*${key ? "" : ""}`;
89
+ const stream = this.cache?.scanStream({ match: pattern });
90
+ const pipeline = this.cache?.pipeline();
91
+ stream?.on("data", (keys: string[]) => {
92
+ if (keys.length) {
93
+ pipeline?.del(keys);
94
+ }
95
+ });
96
+ await new Promise<void>((resolve, reject) => {
97
+ stream?.on("end", async () => {
98
+ await pipeline?.exec();
99
+ resolve();
100
+ });
101
+ stream?.on("error", (err) => reject(err));
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Optional: gracefully close the Redis connection.
107
+ */
108
+ async close(): Promise<void> {
109
+ await this.cache?.quit();
110
+ }
111
+ }
@@ -0,0 +1,158 @@
1
+ import { z, ZodRawShape } from "zod";
2
+ import { ResponseError } from "./error-handling";
3
+
4
+ /**
5
+ * Helper: normalize and validate a numeric string for integer parsing.
6
+ * This ensures we reject non-integer strings, empty input, or inputs with extra chars.
7
+ */
8
+ function parseIntegerStrict(input: string): number {
9
+ // Trim whitespace
10
+ const s = input.trim();
11
+
12
+ // Empty or just sign is invalid
13
+ if (s.length === 0 || s === "+" || s === "-") {
14
+ throw new Error("Invalid integer");
15
+ }
16
+
17
+ // Use a regex to ensure the entire string is an optional sign followed by digits
18
+ if (!/^[+-]?\d+$/.test(s)) {
19
+ throw new Error("Invalid integer");
20
+ }
21
+
22
+ // Safe parse
23
+ const n = Number(s);
24
+ if (!Number.isSafeInteger(n)) {
25
+ throw new Error("Integer out of safe range");
26
+ }
27
+
28
+ return n;
29
+ }
30
+
31
+ /**
32
+ * Parses a string input into an integer number with strict validation.
33
+ *
34
+ * If parsing fails, this function throws a ResponseError describing the invalid input.
35
+ *
36
+ * @param input - The string to parse as an integer
37
+ * @returns The parsed integer
38
+ * @throws {ResponseError} When the input cannot be parsed as an integer
39
+ */
40
+ export function stringToInteger(input: string): number {
41
+ try {
42
+ return parseIntegerStrict(input);
43
+ } catch {
44
+ throw new ResponseError(
45
+ "The input parameter must be a valid integer.",
46
+ 400
47
+ );
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Parses a string input into a boolean with explicit validation.
53
+ *
54
+ * Accepted true values: "true", "1", "yes", case-insensitive
55
+ * Accepted false values: "false", "0", "no", case-insensitive
56
+ * Any other input is invalid.
57
+ *
58
+ * If parsing fails, this function throws a ResponseError describing the invalid input.
59
+ *
60
+ * @param input - The string to parse as a boolean
61
+ * @returns The parsed boolean
62
+ * @throws {ResponseError} When the input cannot be parsed as a boolean
63
+ */
64
+ export function stringToBoolean(input: string): boolean {
65
+ const s = input.trim().toLowerCase();
66
+
67
+ if (["true", "1", "yes", "y"].includes(s)) {
68
+ return true;
69
+ }
70
+ if (["false", "0", "no", "n"].includes(s)) {
71
+ return false;
72
+ }
73
+
74
+ throw new ResponseError(
75
+ "The input parameter must be a boolean (e.g., true/false, 1/0).",
76
+ 400
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Parses a string input into a number with basic validation.
82
+ *
83
+ * If parsing fails, this function throws a ResponseError describing the invalid input.
84
+ *
85
+ * @param input - The string to parse as a number
86
+ * @returns The parsed number
87
+ * @throws {ResponseError} When the input cannot be parsed as a number
88
+ */
89
+ export function stringToNumber(input: string): number {
90
+ try {
91
+ // Trim and convert
92
+ const n = Number(input.trim());
93
+
94
+ // Allow finite numbers only
95
+ if (!Number.isFinite(n)) {
96
+ throw new Error("Invalid number");
97
+ }
98
+
99
+ return n;
100
+ } catch {
101
+ throw new ResponseError("The input parameter must be a valid number.", 400);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Strictly convert obj (type T1) to T2 using a Zod schema.
107
+ *
108
+ * - Extras in obj are ignored (no throw).
109
+ * - Validates fields with the schema; on failure, throws with a descriptive message.
110
+ * - Returns an object typed as T2 (inferred from the schema).
111
+ *
112
+ * @param obj - Source object of type T1
113
+ * @param schema - Zod schema describing the target type T2
114
+ * @returns T2 inferred from the provided schema
115
+ */
116
+ export function convert<T1 extends object, T2 extends object>(
117
+ obj: T1,
118
+ schema: z.ZodObject<any>
119
+ ): T2 {
120
+ // 1) Derive the runtime keys from the schema's shape
121
+ const shape = (schema as any)._def?.shape as ZodRawShape | undefined;
122
+ if (!shape) {
123
+ throw new ResponseError(
124
+ "convertStrictlyFromSchema: provided schema has no shape.",
125
+ 500
126
+ );
127
+ }
128
+
129
+ const keysSchema = Object.keys(shape) as Array<keyof any>;
130
+
131
+ // 2) Build a plain object to pass through Zod for validation
132
+ // Include only keys that exist on the schema (ignore extras in obj)
133
+ const candidate: any = {};
134
+ for (const k of keysSchema) {
135
+ if ((obj as any).hasOwnProperty(k)) {
136
+ candidate[k] = (obj as any)[k];
137
+ }
138
+ }
139
+
140
+ // 3) Validate against the schema
141
+ const result = schema.safeParse(candidate);
142
+ if (!result.success) {
143
+ // Modern, non-format error reporting
144
+ const issues = result.error.issues.map((i) => ({
145
+ path: i.path, // where the issue occurred
146
+ message: i.message, // human-friendly message
147
+ code: i.code, // e.g., "too_small", "invalid_type"
148
+ }));
149
+ // You can log issues or throw a structured error
150
+ throw new ResponseError(
151
+ `convertStrictlyFromSchema: validation failed: ${JSON.stringify(issues)}`,
152
+ 500
153
+ );
154
+ }
155
+
156
+ // 4) Return the validated data typed as T2
157
+ return result.data as T2;
158
+ }