codeweaver 2.0.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/README.md +84 -144
- package/package.json +6 -3
- package/src/app.ts +3 -0
- package/src/config.ts +11 -50
- package/src/constants.ts +1 -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 +10 -7
- package/src/routers/orders/dto/order.dto.ts +54 -30
- package/src/routers/orders/index.router.ts +10 -6
- package/src/routers/orders/order.controller.ts +88 -77
- package/src/routers/products/dto/product.dto.ts +86 -31
- package/src/routers/products/index.router.ts +11 -5
- package/src/routers/products/product.controller.ts +112 -117
- package/src/routers/users/dto/user.dto.ts +13 -19
- package/src/routers/users/index.router.ts +5 -3
- package/src/routers/users/user.controller.ts +74 -97
- package/src/swagger-options.ts +7 -22
- package/src/utilities/assign.ts +44 -59
- 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 +62 -26
- package/src/utilities/router.ts +0 -0
- package/src/utilities/types.ts +0 -23
|
@@ -1,89 +1,35 @@
|
|
|
1
1
|
import {
|
|
2
|
-
User,
|
|
3
2
|
ZodUserCreationDto,
|
|
4
3
|
UserCreationDto,
|
|
5
4
|
UserDto,
|
|
5
|
+
ZodUserDto,
|
|
6
6
|
} from "./dto/user.dto";
|
|
7
7
|
import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { parseId } from "@/utilities/error-handling";
|
|
8
|
+
import { ResponseError } from "@/utilities/error-handling";
|
|
9
|
+
import { convert, stringToInteger } from "@/utilities/conversion";
|
|
11
10
|
import config from "@/config";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
id: 1,
|
|
17
|
-
username: "johndoe",
|
|
18
|
-
email: "johndoe@gmail.com",
|
|
19
|
-
password: "S3cur3P@ssw0rd",
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
id: 2,
|
|
23
|
-
username: "janesmith",
|
|
24
|
-
email: "janesmith@yahoo.com",
|
|
25
|
-
password: "P@ssw0rd2024",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: 3,
|
|
29
|
-
username: "michael89",
|
|
30
|
-
email: "michael89@hotmail.com",
|
|
31
|
-
password: "M1chael!2024",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
id: 4,
|
|
35
|
-
username: "lisa.wong",
|
|
36
|
-
email: "lisa.wong@example.com",
|
|
37
|
-
password: "L1saW0ng!2024",
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: 5,
|
|
41
|
-
username: "alex_k",
|
|
42
|
-
email: "alex.k@gmail.com",
|
|
43
|
-
password: "A1ex#Key2024",
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
id: 6,
|
|
47
|
-
username: "emilyj",
|
|
48
|
-
email: "emilyj@hotmail.com",
|
|
49
|
-
password: "Em!ly0101",
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: 7,
|
|
53
|
-
username: "davidparker",
|
|
54
|
-
email: "david.parker@yahoo.com",
|
|
55
|
-
password: "D@v!d2024",
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
id: 8,
|
|
59
|
-
username: "sophia_m",
|
|
60
|
-
email: "sophia.m@gmail.com",
|
|
61
|
-
password: "Sophi@2024",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: 9,
|
|
65
|
-
username: "chrisw",
|
|
66
|
-
email: "chrisw@outlook.com",
|
|
67
|
-
password: "Chri$Wong21",
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: 10,
|
|
71
|
-
username: "natalie_b",
|
|
72
|
-
email: "natalie_b@gmail.com",
|
|
73
|
-
password: "N@talie#B2024",
|
|
74
|
-
},
|
|
75
|
-
];
|
|
11
|
+
import { users } from "@/db";
|
|
12
|
+
import { User } from "@/entities/user.entity";
|
|
13
|
+
import { MapAsyncCache } from "@/utilities/cache/memory-cache";
|
|
76
14
|
|
|
77
15
|
function exceedHandler() {
|
|
78
16
|
const message = "Too much call in allowed window";
|
|
79
17
|
throw new ResponseError(message, 429);
|
|
80
18
|
}
|
|
81
19
|
|
|
82
|
-
function
|
|
20
|
+
function userNotFoundHandler(e: ResponseError) {
|
|
83
21
|
const message = "User not found.";
|
|
84
|
-
throw new ResponseError(message, 404, e
|
|
22
|
+
throw new ResponseError(message, 404, e?.message);
|
|
23
|
+
}
|
|
24
|
+
|
|
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,62 +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
69
|
timeSpanMs: config.rateLimitTimeSpan,
|
|
97
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
|
-
@memoizeAsync(
|
|
113
|
-
|
|
114
|
-
|
|
86
|
+
@memoizeAsync({
|
|
87
|
+
cache: usersCache,
|
|
88
|
+
keyResolver: () => "key",
|
|
89
|
+
expirationTimeMs: config.memoizeTime,
|
|
115
90
|
})
|
|
91
|
+
@timeout(config.timeout)
|
|
116
92
|
@rateLimit({
|
|
117
93
|
timeSpanMs: config.rateLimitTimeSpan,
|
|
118
94
|
allowedCalls: config.rateLimitAllowedCalls,
|
|
119
95
|
exceedHandler,
|
|
120
96
|
})
|
|
121
97
|
/**
|
|
122
|
-
* Get
|
|
123
|
-
* @
|
|
124
|
-
* @
|
|
125
|
-
* @throws {ResponseError} 404 - User not found
|
|
126
|
-
* @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
|
|
127
101
|
*/
|
|
128
|
-
public async
|
|
129
|
-
|
|
130
|
-
const user = users.find((user) => user.id === response);
|
|
131
|
-
if (user == null) throw new ResponseError("User dose not exist.", 404);
|
|
132
|
-
return user satisfies User;
|
|
102
|
+
public async getAll(): Promise<UserDto[]> {
|
|
103
|
+
return users as UserDto[];
|
|
133
104
|
}
|
|
134
105
|
|
|
135
|
-
@memoizeAsync(
|
|
136
|
-
|
|
106
|
+
@memoizeAsync({
|
|
107
|
+
cache: userCache,
|
|
108
|
+
keyResolver: (id: number) => id.toString(),
|
|
109
|
+
expirationTimeMs: config.memoizeTime,
|
|
110
|
+
})
|
|
137
111
|
@rateLimit({
|
|
138
112
|
timeSpanMs: config.rateLimitTimeSpan,
|
|
139
113
|
allowedCalls: config.rateLimitAllowedCalls,
|
|
140
114
|
exceedHandler,
|
|
141
115
|
})
|
|
142
116
|
/**
|
|
143
|
-
* Get
|
|
144
|
-
* @
|
|
145
|
-
* @
|
|
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
|
|
146
122
|
*/
|
|
147
|
-
public async
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
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);
|
|
152
129
|
}
|
|
153
130
|
}
|
package/src/swagger-options.ts
CHANGED
|
@@ -1,52 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
memoizeTime,
|
|
3
|
-
productionEnvironment,
|
|
4
|
-
rateLimitTimeSpan,
|
|
5
|
-
rateLimitAllowedCalls,
|
|
6
|
-
timeout,
|
|
7
|
-
portNumber,
|
|
8
|
-
} from "./constants";
|
|
9
|
-
|
|
10
1
|
/**
|
|
11
2
|
* Server configuration interface
|
|
12
|
-
* @interface
|
|
13
|
-
* @property {string} url - Base server URL
|
|
14
3
|
*/
|
|
15
4
|
interface Server {
|
|
5
|
+
/** URL of the base server */
|
|
16
6
|
url: string;
|
|
17
7
|
}
|
|
18
8
|
|
|
19
9
|
/**
|
|
20
10
|
* API information structure
|
|
21
|
-
* @interface
|
|
22
|
-
* @property {string} title - API title
|
|
23
|
-
* @property {string} version - API version
|
|
24
|
-
* @property {string} description - API description
|
|
25
11
|
*/
|
|
26
12
|
interface Info {
|
|
13
|
+
/** Title of the API */
|
|
27
14
|
title: string;
|
|
15
|
+
/** Version of the API */
|
|
28
16
|
version: string;
|
|
17
|
+
/** Description of the API */
|
|
29
18
|
description: string;
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
/**
|
|
33
22
|
* 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
23
|
*/
|
|
39
24
|
interface SwaggerDefinition {
|
|
25
|
+
/** OpenAPI specification version (e.g., "3.0.0") */
|
|
40
26
|
openApi: string;
|
|
27
|
+
/** API information object */
|
|
41
28
|
info: Info;
|
|
29
|
+
/** List of server configurations */
|
|
42
30
|
servers: Server[];
|
|
43
31
|
}
|
|
44
32
|
|
|
45
33
|
/**
|
|
46
34
|
* Swagger configuration options
|
|
47
|
-
* @interface
|
|
48
|
-
* @property {SwaggerDefinition} swaggerDefinition - Swagger definition object
|
|
49
|
-
* @property {string[]} apis - Paths to API documentation files
|
|
50
35
|
*/
|
|
51
36
|
export interface SwaggerOptions {
|
|
52
37
|
swaggerDefinition: SwaggerDefinition;
|
package/src/utilities/assign.ts
CHANGED
|
@@ -1,81 +1,66 @@
|
|
|
1
1
|
import { z, ZodRawShape } from "zod";
|
|
2
|
-
import { ResponseError } from "./
|
|
2
|
+
import { ResponseError } from "./error-handling";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Strictly
|
|
5
|
+
* Strictly assign obj (type T1) to T2 using a Zod schema.
|
|
6
6
|
*
|
|
7
|
-
* -
|
|
7
|
+
* - Extras in source are ignored (no throw).
|
|
8
8
|
* - Validates fields with the schema; on failure, throws with a descriptive message.
|
|
9
9
|
* - Returns an object typed as T2 (inferred from the schema).
|
|
10
10
|
*
|
|
11
|
-
* @param
|
|
11
|
+
* @param source - Source object of type T1
|
|
12
|
+
* @param destination - Destination object to be populated (typed as T2)
|
|
12
13
|
* @param schema - Zod schema describing the target type T2
|
|
13
|
-
* @returns T2
|
|
14
|
+
* @returns T2 representing the destination after assignment
|
|
14
15
|
*/
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
18
21
|
): T2 {
|
|
19
|
-
// 1)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
throw new ResponseError(
|
|
23
|
-
"assignStrictlyFromSchema: provided schema has no shape.",
|
|
24
|
-
500
|
|
25
|
-
);
|
|
26
|
-
}
|
|
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 = {};
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)}`,
|
|
38
|
-
500
|
|
39
|
-
);
|
|
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
|
+
}
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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";
|
|
48
44
|
throw new ResponseError(
|
|
49
|
-
`assignStrictlyFromSchema:
|
|
50
|
-
", "
|
|
51
|
-
)}`,
|
|
45
|
+
`assignStrictlyFromSchema: Validation failed for "${path}": ${message}`,
|
|
52
46
|
500
|
|
53
47
|
);
|
|
54
48
|
}
|
|
55
49
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (k in (obj as any)) {
|
|
60
|
-
candidate[k] = (obj as any)[k];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
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;
|
|
63
53
|
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
// You can log issues or throw a structured error
|
|
74
|
-
throw new Error(
|
|
75
|
-
`assignStrictlyFromSchema: validation failed: ${JSON.stringify(issues)}`
|
|
76
|
-
);
|
|
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
|
+
}
|
|
77
63
|
}
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
return result.data as T2;
|
|
65
|
+
return destination;
|
|
81
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
|
+
}
|