codeweaver 2.0.0 → 2.2.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 +11 -3
- 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 +19 -7
- package/src/routers/orders/dto/order.dto.ts +54 -30
- package/src/routers/orders/index.router.ts +12 -7
- package/src/routers/orders/order.controller.ts +87 -79
- package/src/routers/products/dto/product.dto.ts +86 -31
- package/src/routers/products/index.router.ts +13 -6
- package/src/routers/products/product.controller.ts +109 -120
- package/src/routers/users/dto/user.dto.ts +13 -19
- package/src/routers/users/index.router.ts +7 -4
- package/src/routers/users/user.controller.ts +72 -98
- 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/container.ts +159 -0
- package/src/utilities/conversion.ts +158 -0
- package/src/utilities/error-handling.ts +98 -26
- package/src/utilities/logger/base-logger.interface.ts +11 -0
- package/src/utilities/logger/logger.config.ts +95 -0
- package/src/utilities/logger/logger.service.ts +122 -0
- package/src/utilities/logger/winston-logger.service.ts +16 -0
- package/src/utilities/types.ts +0 -23
|
@@ -1,89 +1,32 @@
|
|
|
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";
|
|
14
|
+
import { Injectable } from "@/utilities/container";
|
|
76
15
|
|
|
77
16
|
function exceedHandler() {
|
|
78
17
|
const message = "Too much call in allowed window";
|
|
79
18
|
throw new ResponseError(message, 429);
|
|
80
19
|
}
|
|
81
20
|
|
|
82
|
-
function
|
|
83
|
-
const message = "
|
|
84
|
-
throw new ResponseError(message,
|
|
21
|
+
function invalidInputHandler(e: ResponseError) {
|
|
22
|
+
const message = "Invalid input";
|
|
23
|
+
throw new ResponseError(message, 400, e?.message);
|
|
85
24
|
}
|
|
86
25
|
|
|
26
|
+
const usersCache = new MapAsyncCache<UserDto[]>(config.cacheSize);
|
|
27
|
+
const userCache = new MapAsyncCache<UserDto>(config.cacheSize);
|
|
28
|
+
|
|
29
|
+
@Injectable()
|
|
87
30
|
/**
|
|
88
31
|
* Controller for handling user-related operations
|
|
89
32
|
* @class UserController
|
|
@@ -92,62 +35,93 @@ function getUserErrorHandler(e: Error) {
|
|
|
92
35
|
export default class UserController {
|
|
93
36
|
// constructor(private readonly userService: UserService) { }
|
|
94
37
|
|
|
38
|
+
@onError({
|
|
39
|
+
func: invalidInputHandler,
|
|
40
|
+
})
|
|
41
|
+
/**
|
|
42
|
+
* Validates a string ID and converts it to a number.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} id - The ID to validate and convert.
|
|
45
|
+
* @returns {number} The numeric value of the provided ID.
|
|
46
|
+
*/
|
|
47
|
+
public async validateId(id: string): Promise<number> {
|
|
48
|
+
return stringToInteger(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@onError({
|
|
52
|
+
func: invalidInputHandler,
|
|
53
|
+
})
|
|
54
|
+
/**
|
|
55
|
+
* Validates and creates a new User from the given DTO.
|
|
56
|
+
*
|
|
57
|
+
* @param {UserCreationDto} user - The incoming UserCreationDto to validate and transform.
|
|
58
|
+
* @returns {User} A fully formed User object ready for persistence.
|
|
59
|
+
*/
|
|
60
|
+
public async validateUserCreationDto(user: UserCreationDto): Promise<User> {
|
|
61
|
+
const newUser = await ZodUserCreationDto.parseAsync(user);
|
|
62
|
+
return { ...newUser, id: users.length + 1 };
|
|
63
|
+
}
|
|
64
|
+
|
|
95
65
|
@rateLimit({
|
|
96
66
|
timeSpanMs: config.rateLimitTimeSpan,
|
|
97
67
|
allowedCalls: config.rateLimitAllowedCalls,
|
|
98
68
|
exceedHandler,
|
|
99
69
|
})
|
|
100
|
-
@Validate
|
|
101
70
|
/**
|
|
102
71
|
* Create a new user
|
|
103
|
-
* @param {
|
|
72
|
+
* @param {User} user - User creation data validated by Zod schema
|
|
104
73
|
* @returns {Promise<void>}
|
|
105
74
|
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
106
75
|
* @throws {ResponseError} 400 - Invalid input data
|
|
107
76
|
*/
|
|
108
|
-
public async create(
|
|
109
|
-
users.push(
|
|
77
|
+
public async create(user: User): Promise<void> {
|
|
78
|
+
users.push(user);
|
|
79
|
+
await userCache.set(user.id.toString(), user as User);
|
|
80
|
+
await usersCache.delete("key");
|
|
110
81
|
}
|
|
111
82
|
|
|
112
|
-
@memoizeAsync(
|
|
113
|
-
|
|
114
|
-
|
|
83
|
+
@memoizeAsync({
|
|
84
|
+
cache: usersCache,
|
|
85
|
+
keyResolver: () => "key",
|
|
86
|
+
expirationTimeMs: config.memoizeTime,
|
|
115
87
|
})
|
|
88
|
+
@timeout(config.timeout)
|
|
116
89
|
@rateLimit({
|
|
117
90
|
timeSpanMs: config.rateLimitTimeSpan,
|
|
118
91
|
allowedCalls: config.rateLimitAllowedCalls,
|
|
119
92
|
exceedHandler,
|
|
120
93
|
})
|
|
121
94
|
/**
|
|
122
|
-
* Get
|
|
123
|
-
* @
|
|
124
|
-
* @
|
|
125
|
-
* @throws {ResponseError} 404 - User not found
|
|
126
|
-
* @throws {ResponseError} 400 - Invalid ID format
|
|
95
|
+
* Get all users
|
|
96
|
+
* @returns {Promise<UserDto[]>} List of users with hidden password fields
|
|
97
|
+
* @throws {ResponseError} 500 - When rate limit exceeded
|
|
127
98
|
*/
|
|
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;
|
|
99
|
+
public async getAll(): Promise<UserDto[]> {
|
|
100
|
+
return users as UserDto[];
|
|
133
101
|
}
|
|
134
102
|
|
|
135
|
-
@memoizeAsync(
|
|
136
|
-
|
|
103
|
+
@memoizeAsync({
|
|
104
|
+
cache: userCache,
|
|
105
|
+
keyResolver: (id: number) => id.toString(),
|
|
106
|
+
expirationTimeMs: config.memoizeTime,
|
|
107
|
+
})
|
|
137
108
|
@rateLimit({
|
|
138
109
|
timeSpanMs: config.rateLimitTimeSpan,
|
|
139
110
|
allowedCalls: config.rateLimitAllowedCalls,
|
|
140
111
|
exceedHandler,
|
|
141
112
|
})
|
|
142
113
|
/**
|
|
143
|
-
* Get
|
|
144
|
-
* @
|
|
145
|
-
* @
|
|
114
|
+
* Get user by ID
|
|
115
|
+
* @param {number} id - User ID as string
|
|
116
|
+
* @returns {Promise<UserDto>} User details or error object
|
|
117
|
+
* @throws {ResponseError} 404 - User not found
|
|
118
|
+
* @throws {ResponseError} 400 - Invalid ID format
|
|
146
119
|
*/
|
|
147
|
-
public async
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
120
|
+
public async get(id: number): Promise<UserDto> {
|
|
121
|
+
const user = users.find((user) => user.id === id);
|
|
122
|
+
if (user == null) {
|
|
123
|
+
throw new ResponseError("Product not found");
|
|
124
|
+
}
|
|
125
|
+
return convert(user!, ZodUserDto);
|
|
152
126
|
}
|
|
153
127
|
}
|
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
|
+
}
|