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.
- package/.vscode/settings.json +3 -0
- package/README.md +114 -181
- package/package.json +12 -7
- package/src/app.ts +2 -112
- package/src/config.ts +33 -64
- package/src/constants.ts +7 -0
- package/src/db.ts +183 -0
- package/src/entities/order.entity.ts +68 -0
- package/src/entities/product.entity.ts +75 -0
- package/src/entities/user.entity.ts +38 -0
- package/src/main.ts +85 -0
- package/src/routers/orders/dto/order.dto.ts +54 -29
- package/src/routers/orders/{index.ts → index.router.ts} +13 -13
- package/src/routers/orders/order.controller.ts +118 -120
- package/src/routers/products/dto/product.dto.ts +86 -30
- package/src/routers/products/{index.ts → index.router.ts} +14 -15
- package/src/routers/products/product.controller.ts +136 -161
- package/src/routers/users/dto/user.dto.ts +14 -18
- package/src/routers/users/{index.ts → index.router.ts} +6 -7
- package/src/routers/users/user.controller.ts +87 -118
- package/src/swagger-options.ts +39 -0
- package/src/utilities/assign.ts +66 -0
- package/src/utilities/cache/memory-cache.ts +74 -0
- package/src/utilities/cache/redis-cache.ts +111 -0
- package/src/utilities/conversion.ts +158 -0
- package/src/utilities/error-handling.ts +156 -0
- package/src/utilities/router.ts +0 -0
- package/tsconfig.json +1 -4
- package/tsconfig.paths.json +8 -10
- 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,89 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
20
|
+
function userNotFoundHandler(e: ResponseError) {
|
|
80
21
|
const message = "User not found.";
|
|
22
|
+
throw new ResponseError(message, 404, e?.message);
|
|
23
|
+
}
|
|
81
24
|
|
|
82
|
-
|
|
83
|
-
|
|
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:
|
|
97
|
-
allowedCalls:
|
|
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 {
|
|
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(
|
|
109
|
-
users.push(
|
|
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
|
-
@
|
|
113
|
-
|
|
86
|
+
@memoizeAsync({
|
|
87
|
+
cache: usersCache,
|
|
88
|
+
keyResolver: () => "key",
|
|
89
|
+
expirationTimeMs: config.memoizeTime,
|
|
114
90
|
})
|
|
91
|
+
@timeout(config.timeout)
|
|
115
92
|
@rateLimit({
|
|
116
|
-
timeSpanMs:
|
|
117
|
-
allowedCalls:
|
|
93
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
94
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
118
95
|
exceedHandler,
|
|
119
96
|
})
|
|
120
97
|
/**
|
|
121
|
-
* Get
|
|
122
|
-
* @
|
|
123
|
-
* @
|
|
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
|
|
128
|
-
|
|
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
|
-
@
|
|
106
|
+
@memoizeAsync({
|
|
107
|
+
cache: userCache,
|
|
108
|
+
keyResolver: (id: number) => id.toString(),
|
|
109
|
+
expirationTimeMs: config.memoizeTime,
|
|
110
|
+
})
|
|
142
111
|
@rateLimit({
|
|
143
|
-
timeSpanMs:
|
|
144
|
-
allowedCalls:
|
|
112
|
+
timeSpanMs: config.rateLimitTimeSpan,
|
|
113
|
+
allowedCalls: config.rateLimitAllowedCalls,
|
|
145
114
|
exceedHandler,
|
|
146
115
|
})
|
|
147
116
|
/**
|
|
148
|
-
* Get
|
|
149
|
-
* @
|
|
150
|
-
* @
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
}
|