@prismakit/redis 3.0.2 → 3.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/dist/index.cjs +99 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -15
- package/dist/index.d.ts +37 -15
- package/dist/index.js +97 -58
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/redis-cache-adapter.integration.spec.ts +35 -0
- package/src/__tests__/redis-cache-adapter.spec.ts +14 -0
- package/src/index.ts +4 -0
- package/src/redis-cache-adapter.ts +42 -50
- package/src/redis-json.ts +115 -17
package/src/redis-json.ts
CHANGED
|
@@ -1,34 +1,132 @@
|
|
|
1
1
|
const BIGINT_TAG = '__bigint';
|
|
2
|
+
const DATE_TAG = '__date';
|
|
3
|
+
const BYTES_TAG = '__bytes';
|
|
4
|
+
const DECIMAL_TAG = '__decimal';
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
export type DecimalFactory = (value: string) => unknown;
|
|
7
|
+
|
|
8
|
+
export type RedisJsonOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Reconstruct Prisma `Decimal` (or equivalent) from the tagged string.
|
|
11
|
+
* Default keeps the precision-preserving string so values are not silently
|
|
12
|
+
* coerced to numbers.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* decimalFactory: (s) => new Prisma.Decimal(s)
|
|
16
|
+
*/
|
|
17
|
+
decimalFactory?: DecimalFactory;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return (
|
|
22
|
+
value !== null &&
|
|
23
|
+
typeof value === 'object' &&
|
|
24
|
+
!Array.isArray(value) &&
|
|
25
|
+
!(value instanceof Date) &&
|
|
26
|
+
!Buffer.isBuffer(value)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isDecimalLike(value: unknown): value is { toFixed: () => string } {
|
|
31
|
+
return (
|
|
32
|
+
isPlainObject(value) &&
|
|
33
|
+
typeof (value as { constructor?: { name?: string } }).constructor?.name ===
|
|
34
|
+
'string' &&
|
|
35
|
+
((value as { constructor: { name: string } }).constructor.name ===
|
|
36
|
+
'Decimal' ||
|
|
37
|
+
(typeof (value as { toFixed?: unknown }).toFixed === 'function' &&
|
|
38
|
+
typeof (value as { d?: unknown }).d === 'object'))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** JSON.stringify replacer — Prisma BigInt / Date / Bytes / Decimal. */
|
|
4
43
|
export function redisJsonReplacer(_key: string, value: unknown): unknown {
|
|
5
44
|
if (typeof value === 'bigint') {
|
|
6
45
|
return { [BIGINT_TAG]: value.toString() };
|
|
7
46
|
}
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
47
|
+
if (value instanceof Date) {
|
|
48
|
+
return { [DATE_TAG]: value.toISOString() };
|
|
49
|
+
}
|
|
50
|
+
if (Buffer.isBuffer(value)) {
|
|
51
|
+
return { [BYTES_TAG]: value.toString('base64') };
|
|
52
|
+
}
|
|
13
53
|
if (
|
|
14
|
-
value
|
|
54
|
+
value &&
|
|
15
55
|
typeof value === 'object' &&
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
BIGINT_TAG in value
|
|
56
|
+
(value as { type?: string }).type === 'Buffer' &&
|
|
57
|
+
Array.isArray((value as { data?: unknown }).data)
|
|
19
58
|
) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
59
|
+
return {
|
|
60
|
+
[BYTES_TAG]: Buffer.from(
|
|
61
|
+
(value as { data: number[] }).data,
|
|
62
|
+
).toString('base64'),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (isDecimalLike(value)) {
|
|
66
|
+
return { [DECIMAL_TAG]: value.toFixed() };
|
|
24
67
|
}
|
|
25
68
|
return value;
|
|
26
69
|
}
|
|
27
70
|
|
|
71
|
+
/** JSON.parse reviver — restore tagged payloads from {@link redisJsonReplacer}. */
|
|
72
|
+
export function createRedisJsonReviver(options: RedisJsonOptions = {}) {
|
|
73
|
+
const decimalFactory = options.decimalFactory ?? ((s: string) => s);
|
|
74
|
+
return (_key: string, value: unknown): unknown => {
|
|
75
|
+
if (!isPlainObject(value)) return value;
|
|
76
|
+
const keys = Object.keys(value);
|
|
77
|
+
if (keys.length !== 1) return value;
|
|
78
|
+
|
|
79
|
+
if (BIGINT_TAG in value && typeof value[BIGINT_TAG] === 'string') {
|
|
80
|
+
return BigInt(value[BIGINT_TAG] as string);
|
|
81
|
+
}
|
|
82
|
+
if (DATE_TAG in value && typeof value[DATE_TAG] === 'string') {
|
|
83
|
+
return new Date(value[DATE_TAG] as string);
|
|
84
|
+
}
|
|
85
|
+
if (BYTES_TAG in value && typeof value[BYTES_TAG] === 'string') {
|
|
86
|
+
return Buffer.from(value[BYTES_TAG] as string, 'base64');
|
|
87
|
+
}
|
|
88
|
+
if (DECIMAL_TAG in value && typeof value[DECIMAL_TAG] === 'string') {
|
|
89
|
+
return decimalFactory(value[DECIMAL_TAG] as string);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @deprecated Prefer {@link createRedisJsonReviver} when a decimalFactory is needed. */
|
|
96
|
+
export function redisJsonReviver(_key: string, value: unknown): unknown {
|
|
97
|
+
return createRedisJsonReviver()(_key, value);
|
|
98
|
+
}
|
|
99
|
+
|
|
28
100
|
export function redisJsonStringify(value: unknown): string {
|
|
29
|
-
return JSON.stringify(value,
|
|
101
|
+
return JSON.stringify(value, function (key, val) {
|
|
102
|
+
const holder = this as Record<string, unknown>;
|
|
103
|
+
const raw = key === '' ? value : holder[key];
|
|
104
|
+
if (raw instanceof Date) {
|
|
105
|
+
return redisJsonReplacer(key, raw);
|
|
106
|
+
}
|
|
107
|
+
return redisJsonReplacer(key, val);
|
|
108
|
+
});
|
|
30
109
|
}
|
|
31
110
|
|
|
32
|
-
export function redisJsonParse<T>(
|
|
33
|
-
|
|
111
|
+
export function redisJsonParse<T>(
|
|
112
|
+
raw: string,
|
|
113
|
+
options?: RedisJsonOptions,
|
|
114
|
+
): T {
|
|
115
|
+
return JSON.parse(raw, createRedisJsonReviver(options)) as T;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Deep clone via structuredClone, falling back to the tagged JSON codec so
|
|
120
|
+
* Date / BigInt / Buffer / Decimal survive (unlike plain JSON.stringify).
|
|
121
|
+
*/
|
|
122
|
+
export function cloneWithCodec<T>(value: T, options?: RedisJsonOptions): T {
|
|
123
|
+
if (value === null || value === undefined) return value;
|
|
124
|
+
if (typeof structuredClone === 'function') {
|
|
125
|
+
try {
|
|
126
|
+
return structuredClone(value);
|
|
127
|
+
} catch {
|
|
128
|
+
// Fall through for non-cloneable values (e.g. Decimal)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return redisJsonParse(redisJsonStringify(value), options);
|
|
34
132
|
}
|