drizzle-redis 1.0.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/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "drizzle-redis",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": {
8
+ "types": "./build/index.d.mts",
9
+ "default": "./build/index.mjs"
10
+ },
11
+ "require": {
12
+ "types": "./build/index.d.cts",
13
+ "default": "./build/index.cjs"
14
+ }
15
+ },
16
+ "./bun": {
17
+ "import": {
18
+ "types": "./build/bun/index.d.mts",
19
+ "default": "./build/bun/index.mjs"
20
+ },
21
+ "require": {
22
+ "types": "./build/bun/index.d.cts",
23
+ "default": "./build/bun/index.cjs"
24
+ }
25
+ }
26
+ },
27
+ "scripts": {
28
+ "typecheck": "tsc --noEmit --incremental false",
29
+ "build": "tsdown",
30
+ "dev": "tsdown --watch ./src"
31
+ },
32
+ "dependencies": {},
33
+ "devDependencies": {},
34
+ "peerDependencies": {
35
+ "drizzle-orm": "^0.45.1",
36
+ "ioredis": "^5.4.2"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "ioredis": {
40
+ "optional": true
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,334 @@
1
+ import { RedisClient } from "bun";
2
+ import { Table, getTableName } from "drizzle-orm";
3
+ import type { MutationOption } from "drizzle-orm/cache/core";
4
+ import { Cache } from "drizzle-orm/cache/core";
5
+ import type { CacheConfig } from "drizzle-orm/cache/core/types";
6
+ import { entityKind, is } from "drizzle-orm/entity";
7
+ import type { BunCacheOptions } from "../types/main";
8
+
9
+ /**
10
+ * Lua script to atomically get a cached value by tag.
11
+ * Looks up the composite table name from the tags map, then retrieves the value.
12
+ */
13
+ const getByTagScript = `
14
+ local tagsMapKey = KEYS[1] -- tags map key
15
+ local tag = ARGV[1] -- tag
16
+
17
+ local compositeTableName = redis.call('HGET', tagsMapKey, tag)
18
+ if not compositeTableName then
19
+ return nil
20
+ end
21
+
22
+ local value = redis.call('HGET', compositeTableName, tag)
23
+ return value
24
+ `;
25
+
26
+ /**
27
+ * Lua script to atomically invalidate cache entries on mutation.
28
+ * Handles both tag-based and table-based invalidation.
29
+ */
30
+ const onMutateScript = `
31
+ local tagsMapKey = KEYS[1] -- tags map key
32
+ local tables = {} -- initialize tables array
33
+ local tags = ARGV -- tags array
34
+
35
+ for i = 2, #KEYS do
36
+ tables[#tables + 1] = KEYS[i] -- add all keys except the first one to tables
37
+ end
38
+
39
+ if #tags > 0 then
40
+ for _, tag in ipairs(tags) do
41
+ if tag ~= nil and tag ~= '' then
42
+ local compositeTableName = redis.call('HGET', tagsMapKey, tag)
43
+ if compositeTableName then
44
+ redis.call('HDEL', compositeTableName, tag)
45
+ end
46
+ end
47
+ end
48
+ redis.call('HDEL', tagsMapKey, unpack(tags))
49
+ end
50
+
51
+ local keysToDelete = {}
52
+
53
+ if #tables > 0 then
54
+ local compositeTableNames = redis.call('SUNION', unpack(tables))
55
+ for _, compositeTableName in ipairs(compositeTableNames) do
56
+ keysToDelete[#keysToDelete + 1] = compositeTableName
57
+ end
58
+ for _, table in ipairs(tables) do
59
+ keysToDelete[#keysToDelete + 1] = table
60
+ end
61
+ redis.call('DEL', unpack(keysToDelete))
62
+ end
63
+ `;
64
+
65
+ interface InternalConfig {
66
+ seconds: number;
67
+ hexOptions?: "NX" | "nx" | "XX" | "xx" | "GT" | "gt" | "LT" | "lt";
68
+ }
69
+
70
+ export class BunCache extends Cache {
71
+ static override readonly [entityKind]: string = "BunCache";
72
+
73
+ private readonly client: RedisClient;
74
+ private readonly prefix: string;
75
+ private readonly useGlobally: boolean;
76
+ private readonly internalConfig: InternalConfig;
77
+
78
+ /**
79
+ * Prefix for sets which denote the composite table names for each unique table.
80
+ *
81
+ * Example: In the composite table set of "table1", you may find
82
+ * `${compositeTableSetPrefix}table1,table2` and `${compositeTableSetPrefix}table1,table3`
83
+ */
84
+ private static readonly compositeTableSetPrefix = "__CTS__";
85
+
86
+ /**
87
+ * Prefix for hashes which map hash or tags to cache values.
88
+ */
89
+ private static readonly compositeTablePrefix = "__CT__";
90
+
91
+ /**
92
+ * Key which holds the mapping of tags to composite table names.
93
+ *
94
+ * Using this tagsMapKey, you can find the composite table name for a given tag
95
+ * and get the cache value for that tag.
96
+ */
97
+ private static readonly tagsMapKey = "__tagsMap__";
98
+
99
+ /**
100
+ * Queries whose auto invalidation is false aren't stored in their respective
101
+ * composite table hashes because those hashes are deleted when a mutation
102
+ * occurs on related tables.
103
+ *
104
+ * Instead, they are stored in a separate hash with this prefix
105
+ * to prevent them from being deleted when a mutation occurs.
106
+ */
107
+ private static readonly nonAutoInvalidateTablePrefix =
108
+ "__nonAutoInvalidate__";
109
+
110
+ constructor(options: BunCacheOptions) {
111
+ super();
112
+ this.prefix = options.prefix ?? "drizzle-redis";
113
+ this.useGlobally = options.global ?? false;
114
+ this.internalConfig = this.toInternalConfig(options.config);
115
+
116
+ if ("client" in options) {
117
+ this.client = options.client;
118
+ } else {
119
+ this.client = new RedisClient(options.url);
120
+ }
121
+ }
122
+
123
+ private toInternalConfig(config?: CacheConfig): InternalConfig {
124
+ return config
125
+ ? {
126
+ seconds: config.ex ?? 1,
127
+ hexOptions: config.hexOptions,
128
+ }
129
+ : {
130
+ seconds: 1,
131
+ };
132
+ }
133
+
134
+ override strategy(): "explicit" | "all" {
135
+ return this.useGlobally ? "all" : "explicit";
136
+ }
137
+
138
+ override async get(
139
+ key: string,
140
+ tables: string[],
141
+ isTag: boolean,
142
+ isAutoInvalidate?: boolean
143
+ ): Promise<any[] | undefined> {
144
+ // Handle non-auto-invalidate queries
145
+ if (!isAutoInvalidate) {
146
+ const result = await this.client.hget(
147
+ this.addPrefix(BunCache.nonAutoInvalidateTablePrefix),
148
+ key
149
+ );
150
+ return result === null ? undefined : JSON.parse(result);
151
+ }
152
+
153
+ // Handle tag-based lookup using Lua script
154
+ if (isTag) {
155
+ const result = await this.client.send("EVAL", [
156
+ getByTagScript,
157
+ "1",
158
+ this.addPrefix(BunCache.tagsMapKey),
159
+ key,
160
+ ]);
161
+ if (result === null) {
162
+ return undefined;
163
+ }
164
+ return JSON.parse(result as string);
165
+ }
166
+
167
+ // Handle normal table-based lookup
168
+ const compositeKey = this.getCompositeKey(tables);
169
+ const result = await this.client.hget(compositeKey, key);
170
+ return result === null ? undefined : JSON.parse(result);
171
+ }
172
+
173
+ override async put(
174
+ key: string,
175
+ response: any,
176
+ tables: string[],
177
+ isTag: boolean = false,
178
+ config?: CacheConfig
179
+ ): Promise<void> {
180
+ const isAutoInvalidate = tables.length !== 0;
181
+ const ttlSeconds =
182
+ config && config.ex ? config.ex : this.internalConfig.seconds;
183
+ const hexOptions =
184
+ config && config.hexOptions
185
+ ? config.hexOptions
186
+ : this.internalConfig?.hexOptions;
187
+
188
+ const serializedResponse = JSON.stringify(response);
189
+
190
+ // Handle non-auto-invalidate queries
191
+ if (!isAutoInvalidate) {
192
+ const nonAutoInvalidateKey = this.addPrefix(
193
+ BunCache.nonAutoInvalidateTablePrefix
194
+ );
195
+
196
+ const commands: Promise<any>[] = [];
197
+
198
+ if (isTag) {
199
+ const tagsMapKey = this.addPrefix(BunCache.tagsMapKey);
200
+ commands.push(
201
+ this.client.send("HSET", [tagsMapKey, key, nonAutoInvalidateKey])
202
+ );
203
+ commands.push(this.hexpire(tagsMapKey, key, ttlSeconds, hexOptions));
204
+ }
205
+
206
+ commands.push(
207
+ this.client.send("HSET", [
208
+ nonAutoInvalidateKey,
209
+ key,
210
+ serializedResponse,
211
+ ])
212
+ );
213
+ commands.push(
214
+ this.hexpire(nonAutoInvalidateKey, key, ttlSeconds, hexOptions)
215
+ );
216
+
217
+ await Promise.all(commands);
218
+ return;
219
+ }
220
+
221
+ // Handle auto-invalidate queries
222
+ const compositeKey = this.getCompositeKey(tables);
223
+ const commands: Promise<any>[] = [];
224
+
225
+ commands.push(
226
+ this.client.send("HSET", [compositeKey, key, serializedResponse])
227
+ );
228
+ commands.push(this.hexpire(compositeKey, key, ttlSeconds, hexOptions));
229
+
230
+ if (isTag) {
231
+ const tagsMapKey = this.addPrefix(BunCache.tagsMapKey);
232
+ commands.push(this.client.send("HSET", [tagsMapKey, key, compositeKey]));
233
+ commands.push(this.hexpire(tagsMapKey, key, ttlSeconds, hexOptions));
234
+ }
235
+
236
+ // Track composite keys for each table (for invalidation)
237
+ for (const table of tables) {
238
+ commands.push(this.client.sadd(this.addTablePrefix(table), compositeKey));
239
+ }
240
+
241
+ await Promise.all(commands);
242
+ }
243
+
244
+ override async onMutate(params: MutationOption): Promise<void> {
245
+ const tags = Array.isArray(params.tags)
246
+ ? params.tags
247
+ : params.tags
248
+ ? [params.tags]
249
+ : [];
250
+
251
+ const tables = Array.isArray(params.tables)
252
+ ? params.tables
253
+ : params.tables
254
+ ? [params.tables]
255
+ : [];
256
+
257
+ // Extract table names, handling Table objects via is() + getTableName
258
+ const tableNames = tables.map((table) =>
259
+ is(table, Table) ? getTableName(table) : (table as string)
260
+ );
261
+
262
+ const compositeTableSets = tableNames.map((table) =>
263
+ this.addTablePrefix(table)
264
+ );
265
+
266
+ const tagsMapKey = this.addPrefix(BunCache.tagsMapKey);
267
+
268
+ // Execute the Lua script for atomic invalidation
269
+ await this.client.send("EVAL", [
270
+ onMutateScript,
271
+ (1 + compositeTableSets.length).toString(),
272
+ tagsMapKey,
273
+ ...compositeTableSets,
274
+ ...tags,
275
+ ]);
276
+ }
277
+
278
+ /**
279
+ * Add the user-defined prefix to a key.
280
+ */
281
+ private addPrefix(key: string): string {
282
+ return `${this.prefix}:${key}`;
283
+ }
284
+
285
+ /**
286
+ * Add the composite table set prefix with user prefix.
287
+ */
288
+ private addTablePrefix(table: string): string {
289
+ return this.addPrefix(`${BunCache.compositeTableSetPrefix}${table}`);
290
+ }
291
+
292
+ /**
293
+ * Generate a composite key from sorted table names.
294
+ */
295
+ private getCompositeKey(tables: string[]): string {
296
+ return this.addPrefix(
297
+ `${BunCache.compositeTablePrefix}${tables.sort().join(",")}`
298
+ );
299
+ }
300
+
301
+ /**
302
+ * Execute HEXPIRE command using send().
303
+ * HEXPIRE requires Redis 7.4+
304
+ */
305
+ private hexpire(
306
+ key: string,
307
+ field: string,
308
+ seconds: number,
309
+ hexOptions?: "NX" | "nx" | "XX" | "xx" | "GT" | "gt" | "LT" | "lt"
310
+ ): Promise<any> {
311
+ if (hexOptions) {
312
+ return this.client.send("HEXPIRE", [
313
+ key,
314
+ seconds.toString(),
315
+ hexOptions,
316
+ "FIELDS",
317
+ "1",
318
+ field,
319
+ ]);
320
+ } else {
321
+ return this.client.send("HEXPIRE", [
322
+ key,
323
+ seconds.toString(),
324
+ "FIELDS",
325
+ "1",
326
+ field,
327
+ ]);
328
+ }
329
+ }
330
+ }
331
+
332
+ export function bunCache(options: BunCacheOptions): BunCache {
333
+ return new BunCache(options);
334
+ }
@@ -0,0 +1,2 @@
1
+ export { bunCache } from "./cache";
2
+ export type { BunCacheOptions } from "../types/main";
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { RedisCache, redisCache } from "./redis/cache";
2
+ export type { RedisCacheOptions } from "./types/main";
@@ -0,0 +1,305 @@
1
+ import type { MutationOption } from "drizzle-orm/cache/core";
2
+ import { Cache } from "drizzle-orm/cache/core";
3
+ import type { CacheConfig } from "drizzle-orm/cache/core/types";
4
+ import { is } from "drizzle-orm/entity";
5
+ import { Table, getTableName } from "drizzle-orm";
6
+ import Redis from "ioredis";
7
+ import type { RedisCacheOptions } from "../types/main";
8
+
9
+ /**
10
+ * Lua script to atomically get a cached value by tag.
11
+ * Looks up the composite table name from the tags map, then retrieves the value.
12
+ */
13
+ const getByTagScript = `
14
+ local tagsMapKey = KEYS[1] -- tags map key
15
+ local tag = ARGV[1] -- tag
16
+
17
+ local compositeTableName = redis.call('HGET', tagsMapKey, tag)
18
+ if not compositeTableName then
19
+ return nil
20
+ end
21
+
22
+ local value = redis.call('HGET', compositeTableName, tag)
23
+ return value
24
+ `;
25
+
26
+ /**
27
+ * Lua script to atomically invalidate cache entries on mutation.
28
+ * Handles both tag-based and table-based invalidation.
29
+ */
30
+ const onMutateScript = `
31
+ local tagsMapKey = KEYS[1] -- tags map key
32
+ local tables = {} -- initialize tables array
33
+ local tags = ARGV -- tags array
34
+
35
+ for i = 2, #KEYS do
36
+ tables[#tables + 1] = KEYS[i] -- add all keys except the first one to tables
37
+ end
38
+
39
+ if #tags > 0 then
40
+ for _, tag in ipairs(tags) do
41
+ if tag ~= nil and tag ~= '' then
42
+ local compositeTableName = redis.call('HGET', tagsMapKey, tag)
43
+ if compositeTableName then
44
+ redis.call('HDEL', compositeTableName, tag)
45
+ end
46
+ end
47
+ end
48
+ redis.call('HDEL', tagsMapKey, unpack(tags))
49
+ end
50
+
51
+ local keysToDelete = {}
52
+
53
+ if #tables > 0 then
54
+ local compositeTableNames = redis.call('SUNION', unpack(tables))
55
+ for _, compositeTableName in ipairs(compositeTableNames) do
56
+ keysToDelete[#keysToDelete + 1] = compositeTableName
57
+ end
58
+ for _, table in ipairs(tables) do
59
+ keysToDelete[#keysToDelete + 1] = table
60
+ end
61
+ redis.call('DEL', unpack(keysToDelete))
62
+ end
63
+ `;
64
+
65
+ interface InternalConfig {
66
+ seconds: number;
67
+ hexOptions?: "NX" | "nx" | "XX" | "xx" | "GT" | "gt" | "LT" | "lt";
68
+ }
69
+
70
+ export class RedisCache extends Cache {
71
+ private readonly client: Redis;
72
+ private readonly prefix: string;
73
+ private readonly useGlobally: boolean;
74
+ private readonly internalConfig: InternalConfig;
75
+
76
+ /**
77
+ * Prefix for sets which denote the composite table names for each unique table.
78
+ *
79
+ * Example: In the composite table set of "table1", you may find
80
+ * `${compositeTableSetPrefix}table1,table2` and `${compositeTableSetPrefix}table1,table3`
81
+ */
82
+ private static readonly compositeTableSetPrefix = "__CTS__";
83
+
84
+ /**
85
+ * Prefix for hashes which map hash or tags to cache values.
86
+ */
87
+ private static readonly compositeTablePrefix = "__CT__";
88
+
89
+ /**
90
+ * Key which holds the mapping of tags to composite table names.
91
+ *
92
+ * Using this tagsMapKey, you can find the composite table name for a given tag
93
+ * and get the cache value for that tag.
94
+ */
95
+ private static readonly tagsMapKey = "__tagsMap__";
96
+
97
+ /**
98
+ * Queries whose auto invalidation is false aren't stored in their respective
99
+ * composite table hashes because those hashes are deleted when a mutation
100
+ * occurs on related tables.
101
+ *
102
+ * Instead, they are stored in a separate hash with this prefix
103
+ * to prevent them from being deleted when a mutation occurs.
104
+ */
105
+ private static readonly nonAutoInvalidateTablePrefix =
106
+ "__nonAutoInvalidate__";
107
+
108
+ constructor(options: RedisCacheOptions) {
109
+ super();
110
+ this.prefix = options.prefix ?? "drizzle-redis";
111
+ this.useGlobally = options.global ?? false;
112
+ this.internalConfig = this.toInternalConfig(options.config);
113
+
114
+ if ("client" in options) {
115
+ this.client = options.client;
116
+ } else {
117
+ this.client = new Redis(options.url);
118
+ }
119
+ }
120
+
121
+ private toInternalConfig(config?: CacheConfig): InternalConfig {
122
+ return config
123
+ ? {
124
+ seconds: config.ex ?? 1,
125
+ hexOptions: config.hexOptions,
126
+ }
127
+ : {
128
+ seconds: 1,
129
+ };
130
+ }
131
+
132
+ override strategy(): "explicit" | "all" {
133
+ return this.useGlobally ? "all" : "explicit";
134
+ }
135
+
136
+ override async get(
137
+ key: string,
138
+ tables: string[],
139
+ isTag: boolean,
140
+ isAutoInvalidate?: boolean
141
+ ): Promise<any[] | undefined> {
142
+ // Handle non-auto-invalidate queries
143
+ if (!isAutoInvalidate) {
144
+ const result = await this.client.hget(
145
+ this.addPrefix(RedisCache.nonAutoInvalidateTablePrefix),
146
+ key
147
+ );
148
+ return result === null ? undefined : JSON.parse(result);
149
+ }
150
+
151
+ // Handle tag-based lookup using Lua script
152
+ if (isTag) {
153
+ const result = await this.client.eval(
154
+ getByTagScript,
155
+ 1,
156
+ this.addPrefix(RedisCache.tagsMapKey),
157
+ key
158
+ );
159
+ if (result === null) {
160
+ return undefined;
161
+ }
162
+ return JSON.parse(result as string);
163
+ }
164
+
165
+ // Handle normal table-based lookup
166
+ const compositeKey = this.getCompositeKey(tables);
167
+ const result = await this.client.hget(compositeKey, key);
168
+ return result === null ? undefined : JSON.parse(result);
169
+ }
170
+
171
+ override async put(
172
+ key: string,
173
+ response: any,
174
+ tables: string[],
175
+ isTag: boolean = false,
176
+ config?: CacheConfig
177
+ ): Promise<void> {
178
+ const isAutoInvalidate = tables.length !== 0;
179
+ const pipeline = this.client.pipeline();
180
+ const ttlSeconds =
181
+ config && config.ex ? config.ex : this.internalConfig.seconds;
182
+ const hexOptions =
183
+ config && config.hexOptions
184
+ ? config.hexOptions
185
+ : this.internalConfig?.hexOptions;
186
+
187
+ const serializedResponse = JSON.stringify(response);
188
+
189
+ // Handle non-auto-invalidate queries
190
+ if (!isAutoInvalidate) {
191
+ const nonAutoInvalidateKey = this.addPrefix(
192
+ RedisCache.nonAutoInvalidateTablePrefix
193
+ );
194
+
195
+ if (isTag) {
196
+ const tagsMapKey = this.addPrefix(RedisCache.tagsMapKey);
197
+ pipeline.hset(tagsMapKey, key, nonAutoInvalidateKey);
198
+ this.hexpire(pipeline, tagsMapKey, key, ttlSeconds, hexOptions);
199
+ }
200
+
201
+ pipeline.hset(nonAutoInvalidateKey, key, serializedResponse);
202
+ this.hexpire(pipeline, nonAutoInvalidateKey, key, ttlSeconds, hexOptions);
203
+ await pipeline.exec();
204
+ return;
205
+ }
206
+
207
+ // Handle auto-invalidate queries
208
+ const compositeKey = this.getCompositeKey(tables);
209
+
210
+ pipeline.hset(compositeKey, key, serializedResponse);
211
+ this.hexpire(pipeline, compositeKey, key, ttlSeconds, hexOptions);
212
+
213
+ if (isTag) {
214
+ const tagsMapKey = this.addPrefix(RedisCache.tagsMapKey);
215
+ pipeline.hset(tagsMapKey, key, compositeKey);
216
+ this.hexpire(pipeline, tagsMapKey, key, ttlSeconds, hexOptions);
217
+ }
218
+
219
+ // Track composite keys for each table (for invalidation)
220
+ for (const table of tables) {
221
+ pipeline.sadd(this.addTablePrefix(table), compositeKey);
222
+ }
223
+
224
+ await pipeline.exec();
225
+ }
226
+
227
+ override async onMutate(params: MutationOption): Promise<void> {
228
+ const tags = Array.isArray(params.tags)
229
+ ? params.tags
230
+ : params.tags
231
+ ? [params.tags]
232
+ : [];
233
+
234
+ const tables = Array.isArray(params.tables)
235
+ ? params.tables
236
+ : params.tables
237
+ ? [params.tables]
238
+ : [];
239
+
240
+ // Extract table names, handling Table objects via is() + getTableName
241
+ const tableNames = tables.map((table) =>
242
+ is(table, Table) ? getTableName(table) : (table as string)
243
+ );
244
+
245
+ const compositeTableSets = tableNames.map((table) =>
246
+ this.addTablePrefix(table)
247
+ );
248
+
249
+ const tagsMapKey = this.addPrefix(RedisCache.tagsMapKey);
250
+
251
+ // Execute the Lua script for atomic invalidation
252
+ await this.client.eval(
253
+ onMutateScript,
254
+ 1 + compositeTableSets.length,
255
+ tagsMapKey,
256
+ ...compositeTableSets,
257
+ ...tags
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Add the user-defined prefix to a key.
263
+ */
264
+ private addPrefix(key: string): string {
265
+ return `${this.prefix}:${key}`;
266
+ }
267
+
268
+ /**
269
+ * Add the composite table set prefix with user prefix.
270
+ */
271
+ private addTablePrefix(table: string): string {
272
+ return this.addPrefix(`${RedisCache.compositeTableSetPrefix}${table}`);
273
+ }
274
+
275
+ /**
276
+ * Generate a composite key from sorted table names.
277
+ */
278
+ private getCompositeKey(tables: string[]): string {
279
+ return this.addPrefix(
280
+ `${RedisCache.compositeTablePrefix}${tables.sort().join(",")}`
281
+ );
282
+ }
283
+
284
+ /**
285
+ * Execute HEXPIRE command using pipeline.call() since ioredis doesn't have a native method.
286
+ * HEXPIRE requires Redis 7.4+
287
+ */
288
+ private hexpire(
289
+ pipeline: ReturnType<Redis["pipeline"]>,
290
+ key: string,
291
+ field: string,
292
+ seconds: number,
293
+ hexOptions?: "NX" | "nx" | "XX" | "xx" | "GT" | "gt" | "LT" | "lt"
294
+ ): void {
295
+ if (hexOptions) {
296
+ pipeline.call("HEXPIRE", key, seconds, hexOptions, "FIELDS", 1, field);
297
+ } else {
298
+ pipeline.call("HEXPIRE", key, seconds, "FIELDS", 1, field);
299
+ }
300
+ }
301
+ }
302
+
303
+ export function redisCache(options: RedisCacheOptions): RedisCache {
304
+ return new RedisCache(options);
305
+ }
@@ -0,0 +1,74 @@
1
+ import type { RedisClient } from "bun";
2
+ import type { CacheConfig } from "drizzle-orm/cache/core/types";
3
+ import type { Redis } from "ioredis";
4
+
5
+ export interface RedisCacheWithClient {
6
+ /**
7
+ * The Redis client to use for the cache.
8
+ */
9
+ client: Redis;
10
+ }
11
+
12
+ export interface RedisCacheWithConnection {
13
+ /**
14
+ * The URL to use to connect to the Redis server.
15
+ */
16
+ url: string;
17
+ }
18
+
19
+ export type RedisCacheOptions = (
20
+ | RedisCacheWithClient
21
+ | RedisCacheWithConnection
22
+ ) & {
23
+ /**
24
+ * The prefix to use for the cache keys. Defaults to "drizzle-redis".
25
+ * @default "drizzle-redis"
26
+ */
27
+ prefix?: string;
28
+ /**
29
+ * Default cache configuration (TTL settings).
30
+ */
31
+ config?: CacheConfig;
32
+ /**
33
+ * Whether to enable global caching for all queries.
34
+ * When true, all queries will be cached automatically.
35
+ * When false (default), only queries with explicit .cache() will be cached.
36
+ * @default false
37
+ */
38
+ global?: boolean;
39
+ };
40
+
41
+ // Bun Redis Cache Types
42
+
43
+ export interface BunCacheWithClient {
44
+ /**
45
+ * The Bun RedisClient instance to use for the cache.
46
+ */
47
+ client: RedisClient;
48
+ }
49
+
50
+ export interface BunCacheWithConnection {
51
+ /**
52
+ * The URL to use to connect to the Redis server.
53
+ */
54
+ url: string;
55
+ }
56
+
57
+ export type BunCacheOptions = (BunCacheWithClient | BunCacheWithConnection) & {
58
+ /**
59
+ * The prefix to use for the cache keys. Defaults to "drizzle-redis".
60
+ * @default "drizzle-redis"
61
+ */
62
+ prefix?: string;
63
+ /**
64
+ * Default cache configuration (TTL settings).
65
+ */
66
+ config?: CacheConfig;
67
+ /**
68
+ * Whether to enable global caching for all queries.
69
+ * When true, all queries will be cached automatically.
70
+ * When false (default), only queries with explicit .cache() will be cached.
71
+ * @default false
72
+ */
73
+ global?: boolean;
74
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }
@@ -0,0 +1,35 @@
1
+ import { defineConfig, type UserConfig } from "tsdown";
2
+ import swc from "unplugin-swc";
3
+
4
+ const baseConfig: UserConfig = {
5
+ format: ["cjs", "esm"],
6
+ treeshake: false,
7
+ dts: true,
8
+ sourcemap: true,
9
+ clean: false,
10
+ plugins: [
11
+ //
12
+ swc.rolldown({
13
+ minify: true,
14
+ sourceMaps: true,
15
+ jsc: {
16
+ target: "es2015",
17
+ },
18
+ }),
19
+ ],
20
+ };
21
+
22
+ export default defineConfig([
23
+ {
24
+ entry: ["src/index.ts"],
25
+ ...baseConfig,
26
+ outDir: "build",
27
+ external: ["drizzle-orm", "ioredis"],
28
+ },
29
+ {
30
+ entry: ["src/bun/index.ts"],
31
+ ...baseConfig,
32
+ outDir: "build/bun",
33
+ external: ["drizzle-orm", "bun"],
34
+ },
35
+ ]);