alepha 0.13.8 → 0.14.1

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.
Files changed (160) hide show
  1. package/dist/api/audits/index.d.ts +418 -338
  2. package/dist/api/audits/index.d.ts.map +1 -0
  3. package/dist/api/files/index.d.ts +81 -1
  4. package/dist/api/files/index.d.ts.map +1 -0
  5. package/dist/api/jobs/index.d.ts +107 -27
  6. package/dist/api/jobs/index.d.ts.map +1 -0
  7. package/dist/api/notifications/index.d.ts +21 -1
  8. package/dist/api/notifications/index.d.ts.map +1 -0
  9. package/dist/api/parameters/index.d.ts +455 -8
  10. package/dist/api/parameters/index.d.ts.map +1 -0
  11. package/dist/api/users/index.d.ts +844 -840
  12. package/dist/api/users/index.d.ts.map +1 -0
  13. package/dist/api/verifications/index.d.ts.map +1 -0
  14. package/dist/batch/index.d.ts.map +1 -0
  15. package/dist/bucket/index.d.ts.map +1 -0
  16. package/dist/cache/core/index.d.ts.map +1 -0
  17. package/dist/cache/redis/index.d.ts.map +1 -0
  18. package/dist/cli/index.d.ts +254 -59
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +499 -127
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/command/index.d.ts +217 -10
  23. package/dist/command/index.d.ts.map +1 -0
  24. package/dist/command/index.js +350 -74
  25. package/dist/command/index.js.map +1 -1
  26. package/dist/core/index.browser.js +1334 -1318
  27. package/dist/core/index.browser.js.map +1 -1
  28. package/dist/core/index.d.ts +76 -72
  29. package/dist/core/index.d.ts.map +1 -0
  30. package/dist/core/index.js +1337 -1321
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/index.native.js +1337 -1321
  33. package/dist/core/index.native.js.map +1 -1
  34. package/dist/datetime/index.d.ts.map +1 -0
  35. package/dist/email/index.d.ts.map +1 -0
  36. package/dist/fake/index.d.ts.map +1 -0
  37. package/dist/file/index.d.ts.map +1 -0
  38. package/dist/file/index.js.map +1 -1
  39. package/dist/lock/core/index.d.ts.map +1 -0
  40. package/dist/lock/redis/index.d.ts.map +1 -0
  41. package/dist/logger/index.d.ts +1 -0
  42. package/dist/logger/index.d.ts.map +1 -0
  43. package/dist/mcp/index.d.ts +820 -0
  44. package/dist/mcp/index.d.ts.map +1 -0
  45. package/dist/mcp/index.js +978 -0
  46. package/dist/mcp/index.js.map +1 -0
  47. package/dist/orm/index.d.ts +234 -107
  48. package/dist/orm/index.d.ts.map +1 -0
  49. package/dist/orm/index.js +376 -316
  50. package/dist/orm/index.js.map +1 -1
  51. package/dist/queue/core/index.d.ts +4 -4
  52. package/dist/queue/core/index.d.ts.map +1 -0
  53. package/dist/queue/redis/index.d.ts.map +1 -0
  54. package/dist/queue/redis/index.js +2 -4
  55. package/dist/queue/redis/index.js.map +1 -1
  56. package/dist/redis/index.d.ts +400 -29
  57. package/dist/redis/index.d.ts.map +1 -0
  58. package/dist/redis/index.js +412 -21
  59. package/dist/redis/index.js.map +1 -1
  60. package/dist/retry/index.d.ts.map +1 -0
  61. package/dist/router/index.d.ts.map +1 -0
  62. package/dist/scheduler/index.d.ts +6 -6
  63. package/dist/scheduler/index.d.ts.map +1 -0
  64. package/dist/security/index.d.ts +28 -28
  65. package/dist/security/index.d.ts.map +1 -0
  66. package/dist/server/auth/index.d.ts +155 -155
  67. package/dist/server/auth/index.d.ts.map +1 -0
  68. package/dist/server/cache/index.d.ts.map +1 -0
  69. package/dist/server/compress/index.d.ts.map +1 -0
  70. package/dist/server/cookies/index.d.ts.map +1 -0
  71. package/dist/server/core/index.d.ts +0 -1
  72. package/dist/server/core/index.d.ts.map +1 -0
  73. package/dist/server/core/index.js.map +1 -1
  74. package/dist/server/cors/index.d.ts.map +1 -0
  75. package/dist/server/health/index.d.ts +17 -17
  76. package/dist/server/health/index.d.ts.map +1 -0
  77. package/dist/server/helmet/index.d.ts +4 -1
  78. package/dist/server/helmet/index.d.ts.map +1 -0
  79. package/dist/server/links/index.d.ts +33 -33
  80. package/dist/server/links/index.d.ts.map +1 -0
  81. package/dist/server/metrics/index.d.ts.map +1 -0
  82. package/dist/server/multipart/index.d.ts.map +1 -0
  83. package/dist/server/multipart/index.js.map +1 -1
  84. package/dist/server/proxy/index.d.ts.map +1 -0
  85. package/dist/server/proxy/index.js.map +1 -1
  86. package/dist/server/rate-limit/index.d.ts.map +1 -0
  87. package/dist/server/security/index.d.ts +9 -9
  88. package/dist/server/security/index.d.ts.map +1 -0
  89. package/dist/server/static/index.d.ts.map +1 -0
  90. package/dist/server/swagger/index.d.ts.map +1 -0
  91. package/dist/sms/index.d.ts.map +1 -0
  92. package/dist/thread/index.d.ts.map +1 -0
  93. package/dist/topic/core/index.d.ts.map +1 -0
  94. package/dist/topic/redis/index.d.ts.map +1 -0
  95. package/dist/topic/redis/index.js +3 -3
  96. package/dist/topic/redis/index.js.map +1 -1
  97. package/dist/vite/index.d.ts +10 -2
  98. package/dist/vite/index.d.ts.map +1 -0
  99. package/dist/vite/index.js +45 -20
  100. package/dist/vite/index.js.map +1 -1
  101. package/dist/websocket/index.d.ts.map +1 -0
  102. package/package.json +9 -4
  103. package/src/cli/apps/AlephaCli.ts +10 -3
  104. package/src/cli/apps/AlephaPackageBuilderCli.ts +15 -8
  105. package/src/cli/assets/mainTs.ts +9 -10
  106. package/src/cli/atoms/changelogOptions.ts +45 -0
  107. package/src/cli/commands/ChangelogCommands.ts +259 -0
  108. package/src/cli/commands/DeployCommands.ts +118 -0
  109. package/src/cli/commands/DrizzleCommands.ts +230 -10
  110. package/src/cli/commands/ViteCommands.ts +47 -23
  111. package/src/cli/defineConfig.ts +15 -0
  112. package/src/cli/index.ts +3 -0
  113. package/src/cli/services/AlephaCliUtils.ts +10 -154
  114. package/src/cli/services/GitMessageParser.ts +77 -0
  115. package/src/command/helpers/EnvUtils.ts +37 -0
  116. package/src/command/index.ts +3 -1
  117. package/src/command/primitives/$command.ts +172 -6
  118. package/src/command/providers/CliProvider.ts +499 -95
  119. package/src/core/Alepha.ts +1 -1
  120. package/src/core/providers/SchemaValidator.ts +23 -1
  121. package/src/file/providers/NodeFileSystemProvider.ts +3 -1
  122. package/src/mcp/errors/McpError.ts +72 -0
  123. package/src/mcp/helpers/jsonrpc.ts +163 -0
  124. package/src/mcp/index.ts +132 -0
  125. package/src/mcp/interfaces/McpTypes.ts +248 -0
  126. package/src/mcp/primitives/$prompt.ts +188 -0
  127. package/src/mcp/primitives/$resource.ts +171 -0
  128. package/src/mcp/primitives/$tool.ts +285 -0
  129. package/src/mcp/providers/McpServerProvider.ts +382 -0
  130. package/src/mcp/transports/SseMcpTransport.ts +172 -0
  131. package/src/mcp/transports/StdioMcpTransport.ts +126 -0
  132. package/src/orm/index.ts +20 -4
  133. package/src/orm/interfaces/PgQueryWhere.ts +1 -26
  134. package/src/orm/providers/drivers/BunPostgresProvider.ts +225 -0
  135. package/src/orm/providers/drivers/BunSqliteProvider.ts +180 -0
  136. package/src/orm/providers/drivers/CloudflareD1Provider.ts +164 -0
  137. package/src/orm/providers/drivers/DatabaseProvider.ts +25 -0
  138. package/src/orm/providers/drivers/NodePostgresProvider.ts +0 -25
  139. package/src/orm/providers/drivers/NodeSqliteProvider.ts +3 -1
  140. package/src/orm/services/QueryManager.ts +10 -125
  141. package/src/queue/redis/providers/RedisQueueProvider.ts +2 -7
  142. package/src/redis/index.ts +65 -3
  143. package/src/redis/providers/BunRedisProvider.ts +304 -0
  144. package/src/redis/providers/BunRedisSubscriberProvider.ts +94 -0
  145. package/src/redis/providers/NodeRedisProvider.ts +280 -0
  146. package/src/redis/providers/NodeRedisSubscriberProvider.ts +94 -0
  147. package/src/redis/providers/RedisProvider.ts +134 -140
  148. package/src/redis/providers/RedisSubscriberProvider.ts +58 -49
  149. package/src/server/core/providers/BunHttpServerProvider.ts +0 -3
  150. package/src/server/core/providers/ServerBodyParserProvider.ts +3 -1
  151. package/src/server/core/providers/ServerProvider.ts +7 -4
  152. package/src/server/multipart/providers/ServerMultipartProvider.ts +3 -1
  153. package/src/server/proxy/providers/ServerProxyProvider.ts +1 -1
  154. package/src/topic/redis/providers/RedisTopicProvider.ts +3 -3
  155. package/src/vite/plugins/viteAlephaBuild.ts +8 -2
  156. package/src/vite/plugins/viteAlephaDev.ts +6 -2
  157. package/src/vite/tasks/buildServer.ts +2 -1
  158. package/src/vite/tasks/generateCloudflare.ts +43 -15
  159. package/src/vite/tasks/runAlepha.ts +1 -0
  160. package/src/orm/services/PgJsonQueryManager.ts +0 -511
@@ -176,4 +176,29 @@ export abstract class DatabaseProvider {
176
176
  * MUST be implemented by each provider
177
177
  */
178
178
  protected abstract executeMigrations(migrationsFolder: string): Promise<void>;
179
+
180
+ // -------------------------------------------------------------------------------------------------------------------
181
+
182
+ /**
183
+ * For testing purposes, generate a unique schema name.
184
+ * The schema name will be generated based on the current date and time.
185
+ * It will be in the format of `test_YYYYMMDD_HHMMSS_randomSuffix`.
186
+ */
187
+ protected generateTestSchemaName(): string {
188
+ const pad = (n: number) => n.toString().padStart(2, "0");
189
+
190
+ const now = new Date();
191
+ const year = now.getUTCFullYear();
192
+ const month = pad(now.getUTCMonth() + 1);
193
+ const day = pad(now.getUTCDate());
194
+ const hours = pad(now.getUTCHours());
195
+ const minutes = pad(now.getUTCMinutes());
196
+ const seconds = pad(now.getUTCSeconds());
197
+
198
+ const timestamp = `${year}${month}${day}_${hours}${minutes}${seconds}`;
199
+
200
+ const randomSuffix = Math.random().toString(36).slice(2, 6); // 4 alphanumeric chars
201
+
202
+ return `test_${timestamp}_${randomSuffix}`;
203
+ }
179
204
  }
@@ -231,29 +231,4 @@ export class NodePostgresProvider extends DatabaseProvider {
231
231
  }
232
232
  }
233
233
  }
234
-
235
- // -------------------------------------------------------------------------------------------------------------------
236
-
237
- /**
238
- * For testing purposes, generate a unique schema name.
239
- * The schema name will be generated based on the current date and time.
240
- * It will be in the format of `test_YYYYMMDD_HHMMSS_randomSuffix`.
241
- */
242
- protected generateTestSchemaName(): string {
243
- const pad = (n: number) => n.toString().padStart(2, "0");
244
-
245
- const now = new Date();
246
- const year = now.getUTCFullYear();
247
- const month = pad(now.getUTCMonth() + 1);
248
- const day = pad(now.getUTCDate());
249
- const hours = pad(now.getUTCHours());
250
- const minutes = pad(now.getUTCMinutes());
251
- const seconds = pad(now.getUTCSeconds());
252
-
253
- const timestamp = `${year}${month}${day}_${hours}${minutes}${seconds}`;
254
-
255
- const randomSuffix = Math.random().toString(36).slice(2, 6); // 4 alphanumeric chars
256
-
257
- return `test_${timestamp}_${randomSuffix}`;
258
- }
259
234
  }
@@ -145,7 +145,9 @@ export class NodeSqliteProvider extends DatabaseProvider {
145
145
  on: "start",
146
146
  handler: async () => {
147
147
  const { DatabaseSync } = await import("node:sqlite");
148
- const filepath = this.url.replace("sqlite://", "");
148
+
149
+ const filepath = this.url.replace("sqlite://", "").replace("sqlite:", "");
150
+
149
151
  if (filepath !== ":memory:" && filepath !== "") {
150
152
  const dirname = filepath.split("/").slice(0, -1).join("/");
151
153
  if (dirname) {
@@ -38,10 +38,8 @@ import type {
38
38
  PgQueryWhere,
39
39
  PgQueryWhereOrSQL,
40
40
  } from "../interfaces/PgQueryWhere.ts";
41
- import { PgJsonQueryManager } from "./PgJsonQueryManager.ts";
42
41
 
43
42
  export class QueryManager {
44
- protected readonly jsonQueryManager = $inject(PgJsonQueryManager);
45
43
  protected readonly alepha = $inject(Alepha);
46
44
 
47
45
  /**
@@ -157,40 +155,16 @@ export class QueryManager {
157
155
  }
158
156
 
159
157
  if (operator) {
160
- // Check if this is a JSONB column with nested query
161
- // BUT skip primitive arrays - they should use native Drizzle operators
162
- if (
163
- this.jsonQueryManager.isJsonbColumn(schema, key) &&
164
- !this.jsonQueryManager.isPrimitiveArray(schema, key) &&
165
- typeof operator === "object" &&
166
- !Array.isArray(operator) &&
167
- this.jsonQueryManager.hasNestedQuery({ [key]: operator })
168
- ) {
169
- // Handle JSONB nested queries for objects and arrays of objects
170
- const column = col(key);
171
- const jsonbSql = this.buildJsonbQuery(
172
- column,
173
- operator,
174
- schema,
175
- key,
176
- options.dialect,
177
- );
178
- if (jsonbSql) {
179
- conditions.push(jsonbSql);
180
- }
181
- } else {
182
- // Regular column query (including primitive arrays)
183
- const column = col(key);
184
- const sql = this.mapOperatorToSql(
185
- operator,
186
- column,
187
- schema,
188
- key,
189
- options.dialect,
190
- );
191
- if (sql) {
192
- conditions.push(sql);
193
- }
158
+ const column = col(key);
159
+ const sql = this.mapOperatorToSql(
160
+ operator,
161
+ column,
162
+ schema,
163
+ key,
164
+ options.dialect,
165
+ );
166
+ if (sql) {
167
+ conditions.push(sql);
194
168
  }
195
169
  }
196
170
  }
@@ -203,95 +177,6 @@ export class QueryManager {
203
177
  return and(...conditions);
204
178
  }
205
179
 
206
- /**
207
- * Build a JSONB query for nested object/array queries.
208
- */
209
- protected buildJsonbQuery(
210
- column: PgColumn,
211
- nestedQuery: any,
212
- schema: TObject,
213
- columnName: string,
214
- dialect: "postgresql" | "sqlite",
215
- ): SQL | undefined {
216
- // Parse the nested query to extract paths and operators
217
- const queries = this.jsonQueryManager.parseNestedQuery(nestedQuery);
218
-
219
- if (queries.length === 0) {
220
- return undefined;
221
- }
222
-
223
- // Get the column schema for type inference
224
- const columnSchema = schema.properties[columnName];
225
-
226
- // Build conditions for each parsed query
227
- const conditions: SQL[] = [];
228
-
229
- for (const { path, operator } of queries) {
230
- // Check if the operator is an array operator (arrayContains, arrayContained, arrayOverlaps)
231
- const isArrayOperator =
232
- operator.arrayContains !== undefined ||
233
- operator.arrayContained !== undefined ||
234
- operator.arrayOverlaps !== undefined;
235
-
236
- // Check if this is an array property
237
- const isArrayProp = this.jsonQueryManager.isArrayProperty(schema, [
238
- columnName,
239
- ...path,
240
- ]);
241
-
242
- if (isArrayProp && isArrayOperator) {
243
- // Array operators on JSONB arrays should use buildJsonbCondition
244
- // This handles cases like: { metadata: { permissions: { arrayContains: [...] } } }
245
- const condition = this.jsonQueryManager.buildJsonbCondition(
246
- column,
247
- path,
248
- operator,
249
- dialect,
250
- columnSchema,
251
- );
252
- if (condition) {
253
- conditions.push(condition);
254
- }
255
- } else if (isArrayProp && !isArrayOperator) {
256
- // Non-array operators on array properties use buildJsonbArrayCondition
257
- // This handles cases like: { addresses: { city: { eq: "Wonderland" } } }
258
- const condition = this.jsonQueryManager.buildJsonbArrayCondition(
259
- column,
260
- path,
261
- "",
262
- operator,
263
- dialect,
264
- );
265
- if (condition) {
266
- conditions.push(condition);
267
- }
268
- } else {
269
- // Handle object queries
270
- const condition = this.jsonQueryManager.buildJsonbCondition(
271
- column,
272
- path,
273
- operator,
274
- dialect,
275
- columnSchema,
276
- );
277
- if (condition) {
278
- conditions.push(condition);
279
- }
280
- }
281
- }
282
-
283
- if (conditions.length === 0) {
284
- return undefined;
285
- }
286
-
287
- if (conditions.length === 1) {
288
- return conditions[0];
289
- }
290
-
291
- // Multiple conditions - AND them together
292
- return and(...conditions);
293
- }
294
-
295
180
  /**
296
181
  * Check if an object has any filter operator properties.
297
182
  */
@@ -17,15 +17,10 @@ export class RedisQueueProvider implements QueueProvider {
17
17
  }
18
18
 
19
19
  public async push(queue: string, message: string): Promise<void> {
20
- await this.redisProvider.publisher.LPUSH(this.prefix(queue), message);
20
+ await this.redisProvider.lpush(this.prefix(queue), message);
21
21
  }
22
22
 
23
23
  public async pop(queue: string): Promise<string | undefined> {
24
- const value = await this.redisProvider.publisher.RPOP(this.prefix(queue));
25
- if (value == null) {
26
- return undefined;
27
- }
28
-
29
- return String(value);
24
+ return this.redisProvider.rpop(this.prefix(queue));
30
25
  }
31
26
  }
@@ -1,9 +1,17 @@
1
1
  import { $module, type Alepha } from "alepha";
2
+ import { BunRedisProvider } from "./providers/BunRedisProvider.ts";
3
+ import { BunRedisSubscriberProvider } from "./providers/BunRedisSubscriberProvider.ts";
4
+ import { NodeRedisProvider } from "./providers/NodeRedisProvider.ts";
5
+ import { NodeRedisSubscriberProvider } from "./providers/NodeRedisSubscriberProvider.ts";
2
6
  import { RedisProvider } from "./providers/RedisProvider.ts";
3
7
  import { RedisSubscriberProvider } from "./providers/RedisSubscriberProvider.ts";
4
8
 
5
9
  // ---------------------------------------------------------------------------------------------------------------------
6
10
 
11
+ export * from "./providers/BunRedisProvider.ts";
12
+ export * from "./providers/BunRedisSubscriberProvider.ts";
13
+ export * from "./providers/NodeRedisProvider.ts";
14
+ export * from "./providers/NodeRedisSubscriberProvider.ts";
7
15
  export * from "./providers/RedisProvider.ts";
8
16
  export * from "./providers/RedisSubscriberProvider.ts";
9
17
 
@@ -12,11 +20,65 @@ export * from "./providers/RedisSubscriberProvider.ts";
12
20
  /**
13
21
  * Redis client provider for Alepha applications.
14
22
  *
15
- * @see {@link RedisProvider}
23
+ * Automatically selects the appropriate provider based on runtime:
24
+ * - Bun: Uses `BunRedisProvider` with Bun's native Redis client (7.9x faster than ioredis)
25
+ * - Node.js: Uses `NodeRedisProvider` with `@redis/client`
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * // Inject the abstract provider - runtime selects the implementation
30
+ * const redis = alepha.inject(RedisProvider);
31
+ *
32
+ * // Use common operations
33
+ * await redis.set("key", "value");
34
+ * const value = await redis.get("key");
35
+ *
36
+ * // For pub/sub
37
+ * const subscriber = alepha.inject(RedisSubscriberProvider);
38
+ * await subscriber.subscribe("channel", (message, channel) => {
39
+ * console.log(`Received: ${message} on ${channel}`);
40
+ * });
41
+ * ```
42
+ *
43
+ * @see {@link RedisProvider} - Abstract base class
44
+ * @see {@link NodeRedisProvider} - Node.js implementation
45
+ * @see {@link BunRedisProvider} - Bun implementation
46
+ * @see {@link RedisSubscriberProvider} - Abstract subscriber base class
47
+ * @see {@link NodeRedisSubscriberProvider} - Node.js subscriber implementation
48
+ * @see {@link BunRedisSubscriberProvider} - Bun subscriber implementation
16
49
  * @module alepha.redis
17
50
  */
18
51
  export const AlephaRedis = $module({
19
52
  name: "alepha.redis",
20
- services: [RedisProvider, RedisSubscriberProvider],
21
- register: (alepha: Alepha) => alepha.with(RedisProvider),
53
+ services: [
54
+ NodeRedisProvider,
55
+ NodeRedisSubscriberProvider,
56
+ BunRedisProvider,
57
+ BunRedisSubscriberProvider,
58
+ RedisProvider,
59
+ RedisSubscriberProvider,
60
+ ],
61
+ register: (alepha: Alepha) => {
62
+ if (alepha.isBun()) {
63
+ alepha
64
+ .with({
65
+ provide: RedisProvider,
66
+ use: BunRedisProvider,
67
+ })
68
+ .with({
69
+ provide: RedisSubscriberProvider,
70
+ use: BunRedisSubscriberProvider,
71
+ });
72
+ } else {
73
+ alepha
74
+ .with({
75
+ provide: RedisProvider,
76
+ use: NodeRedisProvider,
77
+ })
78
+ .with({
79
+ provide: RedisSubscriberProvider,
80
+ use: NodeRedisSubscriberProvider,
81
+ });
82
+ }
83
+ },
22
84
  });
@@ -0,0 +1,304 @@
1
+ import {
2
+ $env,
3
+ $hook,
4
+ $inject,
5
+ Alepha,
6
+ AlephaError,
7
+ type Static,
8
+ t,
9
+ } from "alepha";
10
+ import { $logger } from "alepha/logger";
11
+ import type { RedisClient as BunRedisClient } from "bun";
12
+ import { RedisProvider, type RedisSetOptions } from "./RedisProvider.ts";
13
+
14
+ const envSchema = t.object({
15
+ REDIS_URL: t.optional(t.text()),
16
+ REDIS_PORT: t.integer({
17
+ default: "6379",
18
+ }),
19
+ REDIS_HOST: t.text({
20
+ default: "localhost",
21
+ }),
22
+ REDIS_PASSWORD: t.optional(t.text()),
23
+ });
24
+
25
+ declare module "alepha" {
26
+ interface Env extends Partial<Static<typeof envSchema>> {}
27
+ }
28
+
29
+ /**
30
+ * Bun Redis client provider using Bun's native Redis client.
31
+ *
32
+ * This provider uses Bun's built-in `RedisClient` class for Redis connections,
33
+ * which provides excellent performance (7.9x faster than ioredis) on the Bun runtime.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * // Set REDIS_URL environment variable
38
+ * // REDIS_URL=redis://localhost:6379
39
+ *
40
+ * // Or configure via REDIS_HOST, REDIS_PORT, REDIS_PASSWORD
41
+ *
42
+ * // Or configure programmatically
43
+ * alepha.with({
44
+ * provide: RedisProvider,
45
+ * use: BunRedisProvider,
46
+ * });
47
+ * ```
48
+ */
49
+ export class BunRedisProvider extends RedisProvider {
50
+ protected readonly log = $logger();
51
+ protected readonly alepha = $inject(Alepha);
52
+ protected readonly env = $env(envSchema);
53
+ protected client?: BunRedisClient;
54
+
55
+ public get publisher(): BunRedisClient {
56
+ if (!this.client?.connected) {
57
+ throw new AlephaError("Redis client is not ready");
58
+ }
59
+
60
+ return this.client;
61
+ }
62
+
63
+ public override get isReady(): boolean {
64
+ return this.client?.connected ?? false;
65
+ }
66
+
67
+ protected readonly start = $hook({
68
+ on: "start",
69
+ handler: () => this.connect(),
70
+ });
71
+
72
+ protected readonly stop = $hook({
73
+ on: "stop",
74
+ handler: () => this.close(),
75
+ });
76
+
77
+ /**
78
+ * Connect to the Redis server.
79
+ */
80
+ public override async connect(): Promise<void> {
81
+ // Check if we're running in Bun
82
+ if (typeof Bun === "undefined") {
83
+ throw new AlephaError(
84
+ "BunRedisProvider requires the Bun runtime. Use NodeRedisProvider for Node.js.",
85
+ );
86
+ }
87
+
88
+ this.log.debug("Connecting...");
89
+
90
+ const { RedisClient } = await import("bun");
91
+
92
+ this.client = new RedisClient(this.getUrl(), {
93
+ autoReconnect: true,
94
+ enableAutoPipelining: true,
95
+ });
96
+
97
+ this.client.onconnect = () => {
98
+ this.log.trace("Redis connected");
99
+ };
100
+
101
+ this.client.onclose = (error) => {
102
+ if (this.alepha.isStarted() && error) {
103
+ this.log.error("Redis connection closed", error);
104
+ }
105
+ };
106
+
107
+ await this.client.connect();
108
+
109
+ this.log.info("Connection OK");
110
+ }
111
+
112
+ /**
113
+ * Close the connection to the Redis server.
114
+ */
115
+ public override async close(): Promise<void> {
116
+ if (this.client) {
117
+ this.log.debug("Closing connection...");
118
+ this.client.close();
119
+ this.client = undefined;
120
+ this.log.info("Connection closed");
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Create a duplicate connection for pub/sub or other isolated operations.
126
+ */
127
+ public async duplicate(): Promise<BunRedisClient> {
128
+ if (typeof Bun === "undefined") {
129
+ throw new AlephaError("BunRedisProvider requires the Bun runtime.");
130
+ }
131
+
132
+ const { RedisClient } = await import("bun");
133
+
134
+ const client = new RedisClient(this.getUrl(), {
135
+ autoReconnect: true,
136
+ enableAutoPipelining: true,
137
+ });
138
+
139
+ client.onclose = (error) => {
140
+ if (this.alepha.isStarted() && error) {
141
+ this.log.error("Redis duplicate connection closed", error);
142
+ }
143
+ };
144
+
145
+ await client.connect();
146
+
147
+ return client;
148
+ }
149
+
150
+ public override async get(key: string): Promise<Buffer | undefined> {
151
+ this.log.trace(`Getting key ${key}`);
152
+ const resp = await this.publisher.getBuffer(key);
153
+
154
+ if (resp === null) {
155
+ return undefined;
156
+ }
157
+
158
+ return Buffer.from(resp);
159
+ }
160
+
161
+ public override async set(
162
+ key: string,
163
+ value: Buffer | string,
164
+ options?: RedisSetOptions,
165
+ ): Promise<Buffer> {
166
+ const buf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf-8");
167
+
168
+ // Build SET command arguments
169
+ const args: string[] = [key, buf.toString("binary")];
170
+
171
+ // Handle expiration object format (from alepha/cache-redis, alepha/lock-redis)
172
+ if (options?.expiration) {
173
+ if (options.expiration.type === "KEEPTTL") {
174
+ args.push("KEEPTTL");
175
+ } else {
176
+ args.push(options.expiration.type, String(options.expiration.value));
177
+ }
178
+ }
179
+
180
+ // Handle direct expiration properties
181
+ if (options?.EX !== undefined) {
182
+ args.push("EX", String(options.EX));
183
+ }
184
+ if (options?.PX !== undefined) {
185
+ args.push("PX", String(options.PX));
186
+ }
187
+ if (options?.EXAT !== undefined) {
188
+ args.push("EXAT", String(options.EXAT));
189
+ }
190
+ if (options?.PXAT !== undefined) {
191
+ args.push("PXAT", String(options.PXAT));
192
+ }
193
+ if (options?.KEEPTTL) {
194
+ args.push("KEEPTTL");
195
+ }
196
+
197
+ // Handle condition object format
198
+ if (options?.condition === "NX") {
199
+ args.push("NX");
200
+ } else if (options?.condition === "XX") {
201
+ args.push("XX");
202
+ }
203
+
204
+ // Handle direct condition properties
205
+ if (options?.NX) {
206
+ args.push("NX");
207
+ }
208
+ if (options?.XX) {
209
+ args.push("XX");
210
+ }
211
+ if (options?.GET) {
212
+ args.push("GET");
213
+ }
214
+
215
+ if (args.length === 2) {
216
+ // Simple set without options
217
+ await this.publisher.set(key, buf);
218
+ } else {
219
+ // Set with options via raw command
220
+ await this.publisher.send("SET", args);
221
+ }
222
+
223
+ return buf;
224
+ }
225
+
226
+ public override async has(key: string): Promise<boolean> {
227
+ return this.publisher.exists(key);
228
+ }
229
+
230
+ public override async keys(pattern: string): Promise<string[]> {
231
+ const keys = await this.publisher.send("KEYS", [pattern]);
232
+ if (!Array.isArray(keys)) {
233
+ return [];
234
+ }
235
+ return keys.map((key) =>
236
+ key instanceof Uint8Array ? Buffer.from(key).toString() : String(key),
237
+ );
238
+ }
239
+
240
+ public override async del(keys: string[]): Promise<void> {
241
+ if (keys.length === 0) {
242
+ return;
243
+ }
244
+
245
+ await this.publisher.send("DEL", keys);
246
+ }
247
+
248
+ // ---------------------------------------------------------
249
+ // Queue operations
250
+ // ---------------------------------------------------------
251
+
252
+ public override async lpush(key: string, value: string): Promise<void> {
253
+ await this.publisher.send("LPUSH", [key, value]);
254
+ }
255
+
256
+ public override async rpop(key: string): Promise<string | undefined> {
257
+ const value = await this.publisher.send("RPOP", [key]);
258
+ if (value == null) {
259
+ return undefined;
260
+ }
261
+ if (value instanceof Uint8Array) {
262
+ return Buffer.from(value).toString();
263
+ }
264
+ return String(value);
265
+ }
266
+
267
+ // ---------------------------------------------------------
268
+ // Pub/Sub operations
269
+ // ---------------------------------------------------------
270
+
271
+ public override async publish(
272
+ channel: string,
273
+ message: string,
274
+ ): Promise<void> {
275
+ await this.publisher.publish(channel, message);
276
+ }
277
+
278
+ /**
279
+ * Get the Redis connection URL.
280
+ */
281
+ protected getUrl(): string {
282
+ // Prefer REDIS_URL if set
283
+ if (this.env.REDIS_URL) {
284
+ return this.env.REDIS_URL;
285
+ }
286
+
287
+ // Build URL from components
288
+ const url = new URL("redis://127.0.0.1:6379");
289
+
290
+ if (this.env.REDIS_PASSWORD) {
291
+ url.password = this.env.REDIS_PASSWORD;
292
+ }
293
+
294
+ if (this.env.REDIS_HOST) {
295
+ url.hostname = this.env.REDIS_HOST;
296
+ }
297
+
298
+ if (this.env.REDIS_PORT) {
299
+ url.port = String(this.env.REDIS_PORT);
300
+ }
301
+
302
+ return url.toString();
303
+ }
304
+ }