@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/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
- /** JSON.stringify replacer Prisma `BigInt` fields are not JSON-serializable by default. */
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
- return value;
9
- }
10
-
11
- /** JSON.parse reviver — restore tagged BigInt payloads from {@link redisJsonReplacer}. */
12
- export function redisJsonReviver(_key: string, value: unknown): unknown {
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 !== null &&
54
+ value &&
15
55
  typeof value === 'object' &&
16
- !Array.isArray(value) &&
17
- Object.keys(value).length === 1 &&
18
- BIGINT_TAG in value
56
+ (value as { type?: string }).type === 'Buffer' &&
57
+ Array.isArray((value as { data?: unknown }).data)
19
58
  ) {
20
- const raw = (value as Record<string, unknown>)[BIGINT_TAG];
21
- if (typeof raw === 'string') {
22
- return BigInt(raw);
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, redisJsonReplacer);
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>(raw: string): T {
33
- return JSON.parse(raw, redisJsonReviver) as T;
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
  }