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.
- package/dist/api/audits/index.d.ts +418 -338
- package/dist/api/audits/index.d.ts.map +1 -0
- package/dist/api/files/index.d.ts +81 -1
- package/dist/api/files/index.d.ts.map +1 -0
- package/dist/api/jobs/index.d.ts +107 -27
- package/dist/api/jobs/index.d.ts.map +1 -0
- package/dist/api/notifications/index.d.ts +21 -1
- package/dist/api/notifications/index.d.ts.map +1 -0
- package/dist/api/parameters/index.d.ts +455 -8
- package/dist/api/parameters/index.d.ts.map +1 -0
- package/dist/api/users/index.d.ts +844 -840
- package/dist/api/users/index.d.ts.map +1 -0
- package/dist/api/verifications/index.d.ts.map +1 -0
- package/dist/batch/index.d.ts.map +1 -0
- package/dist/bucket/index.d.ts.map +1 -0
- package/dist/cache/core/index.d.ts.map +1 -0
- package/dist/cache/redis/index.d.ts.map +1 -0
- package/dist/cli/index.d.ts +254 -59
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +499 -127
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +217 -10
- package/dist/command/index.d.ts.map +1 -0
- package/dist/command/index.js +350 -74
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +1334 -1318
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +76 -72
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +1337 -1321
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1337 -1321
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts.map +1 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/fake/index.d.ts.map +1 -0
- package/dist/file/index.d.ts.map +1 -0
- package/dist/file/index.js.map +1 -1
- package/dist/lock/core/index.d.ts.map +1 -0
- package/dist/lock/redis/index.d.ts.map +1 -0
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +820 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +978 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/orm/index.d.ts +234 -107
- package/dist/orm/index.d.ts.map +1 -0
- package/dist/orm/index.js +376 -316
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +4 -4
- package/dist/queue/core/index.d.ts.map +1 -0
- package/dist/queue/redis/index.d.ts.map +1 -0
- package/dist/queue/redis/index.js +2 -4
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/redis/index.d.ts +400 -29
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +412 -21
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts.map +1 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/scheduler/index.d.ts +6 -6
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/security/index.d.ts +28 -28
- package/dist/security/index.d.ts.map +1 -0
- package/dist/server/auth/index.d.ts +155 -155
- package/dist/server/auth/index.d.ts.map +1 -0
- package/dist/server/cache/index.d.ts.map +1 -0
- package/dist/server/compress/index.d.ts.map +1 -0
- package/dist/server/cookies/index.d.ts.map +1 -0
- package/dist/server/core/index.d.ts +0 -1
- package/dist/server/core/index.d.ts.map +1 -0
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts.map +1 -0
- package/dist/server/health/index.d.ts +17 -17
- package/dist/server/health/index.d.ts.map +1 -0
- package/dist/server/helmet/index.d.ts +4 -1
- package/dist/server/helmet/index.d.ts.map +1 -0
- package/dist/server/links/index.d.ts +33 -33
- package/dist/server/links/index.d.ts.map +1 -0
- package/dist/server/metrics/index.d.ts.map +1 -0
- package/dist/server/multipart/index.d.ts.map +1 -0
- package/dist/server/multipart/index.js.map +1 -1
- package/dist/server/proxy/index.d.ts.map +1 -0
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts.map +1 -0
- package/dist/server/security/index.d.ts +9 -9
- package/dist/server/security/index.d.ts.map +1 -0
- package/dist/server/static/index.d.ts.map +1 -0
- package/dist/server/swagger/index.d.ts.map +1 -0
- package/dist/sms/index.d.ts.map +1 -0
- package/dist/thread/index.d.ts.map +1 -0
- package/dist/topic/core/index.d.ts.map +1 -0
- package/dist/topic/redis/index.d.ts.map +1 -0
- package/dist/topic/redis/index.js +3 -3
- package/dist/topic/redis/index.js.map +1 -1
- package/dist/vite/index.d.ts +10 -2
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +45 -20
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts.map +1 -0
- package/package.json +9 -4
- package/src/cli/apps/AlephaCli.ts +10 -3
- package/src/cli/apps/AlephaPackageBuilderCli.ts +15 -8
- package/src/cli/assets/mainTs.ts +9 -10
- package/src/cli/atoms/changelogOptions.ts +45 -0
- package/src/cli/commands/ChangelogCommands.ts +259 -0
- package/src/cli/commands/DeployCommands.ts +118 -0
- package/src/cli/commands/DrizzleCommands.ts +230 -10
- package/src/cli/commands/ViteCommands.ts +47 -23
- package/src/cli/defineConfig.ts +15 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/services/AlephaCliUtils.ts +10 -154
- package/src/cli/services/GitMessageParser.ts +77 -0
- package/src/command/helpers/EnvUtils.ts +37 -0
- package/src/command/index.ts +3 -1
- package/src/command/primitives/$command.ts +172 -6
- package/src/command/providers/CliProvider.ts +499 -95
- package/src/core/Alepha.ts +1 -1
- package/src/core/providers/SchemaValidator.ts +23 -1
- package/src/file/providers/NodeFileSystemProvider.ts +3 -1
- package/src/mcp/errors/McpError.ts +72 -0
- package/src/mcp/helpers/jsonrpc.ts +163 -0
- package/src/mcp/index.ts +132 -0
- package/src/mcp/interfaces/McpTypes.ts +248 -0
- package/src/mcp/primitives/$prompt.ts +188 -0
- package/src/mcp/primitives/$resource.ts +171 -0
- package/src/mcp/primitives/$tool.ts +285 -0
- package/src/mcp/providers/McpServerProvider.ts +382 -0
- package/src/mcp/transports/SseMcpTransport.ts +172 -0
- package/src/mcp/transports/StdioMcpTransport.ts +126 -0
- package/src/orm/index.ts +20 -4
- package/src/orm/interfaces/PgQueryWhere.ts +1 -26
- package/src/orm/providers/drivers/BunPostgresProvider.ts +225 -0
- package/src/orm/providers/drivers/BunSqliteProvider.ts +180 -0
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +164 -0
- package/src/orm/providers/drivers/DatabaseProvider.ts +25 -0
- package/src/orm/providers/drivers/NodePostgresProvider.ts +0 -25
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +3 -1
- package/src/orm/services/QueryManager.ts +10 -125
- package/src/queue/redis/providers/RedisQueueProvider.ts +2 -7
- package/src/redis/index.ts +65 -3
- package/src/redis/providers/BunRedisProvider.ts +304 -0
- package/src/redis/providers/BunRedisSubscriberProvider.ts +94 -0
- package/src/redis/providers/NodeRedisProvider.ts +280 -0
- package/src/redis/providers/NodeRedisSubscriberProvider.ts +94 -0
- package/src/redis/providers/RedisProvider.ts +134 -140
- package/src/redis/providers/RedisSubscriberProvider.ts +58 -49
- package/src/server/core/providers/BunHttpServerProvider.ts +0 -3
- package/src/server/core/providers/ServerBodyParserProvider.ts +3 -1
- package/src/server/core/providers/ServerProvider.ts +7 -4
- package/src/server/multipart/providers/ServerMultipartProvider.ts +3 -1
- package/src/server/proxy/providers/ServerProxyProvider.ts +1 -1
- package/src/topic/redis/providers/RedisTopicProvider.ts +3 -3
- package/src/vite/plugins/viteAlephaBuild.ts +8 -2
- package/src/vite/plugins/viteAlephaDev.ts +6 -2
- package/src/vite/tasks/buildServer.ts +2 -1
- package/src/vite/tasks/generateCloudflare.ts +43 -15
- package/src/vite/tasks/runAlepha.ts +1 -0
- 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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
) {
|
|
169
|
-
|
|
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.
|
|
20
|
+
await this.redisProvider.lpush(this.prefix(queue), message);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
public async pop(queue: string): Promise<string | undefined> {
|
|
24
|
-
|
|
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
|
}
|
package/src/redis/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
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: [
|
|
21
|
-
|
|
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
|
+
}
|