@luxdb/sdk 1.4.1 → 1.4.3

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/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # @luxdb/sdk
2
+
3
+ Official TypeScript SDK for Lux.
4
+
5
+ Use the project client for browser, server, and SSR app code. Use the direct client when you want low-level Redis-compatible access to a Lux instance.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun i @luxdb/sdk
11
+ ```
12
+
13
+ ## Browser app client
14
+
15
+ Use a publishable key in browser code. The browser client persists auth sessions in browser storage by default.
16
+
17
+ ```ts
18
+ import { createBrowserClient } from "@luxdb/sdk";
19
+
20
+ const lux = createBrowserClient(
21
+ "https://api.luxdb.dev/v1/my-project",
22
+ "lux_pub_..."
23
+ );
24
+
25
+ const { data: session, error } = await lux.auth.signInWithPassword({
26
+ email: "user@example.com",
27
+ password: "correct horse battery staple",
28
+ });
29
+
30
+ if (error) throw error;
31
+ ```
32
+
33
+ ## Tables
34
+
35
+ Queries and mutations return a Supabase-style result object:
36
+
37
+ ```ts
38
+ interface User {
39
+ id: number;
40
+ email: string;
41
+ age: number;
42
+ }
43
+
44
+ interface Message {
45
+ id: string;
46
+ body: string;
47
+ embedding: number[];
48
+ }
49
+
50
+ const { data: users, error } = await lux
51
+ .table<User[]>("users")
52
+ .select()
53
+ .gt("age", 25)
54
+ .order("age", { ascending: false })
55
+ .limit(10);
56
+
57
+ if (error) throw error;
58
+ console.log(users);
59
+ ```
60
+
61
+ `table<T>()` accepts either a row type or an array type. `table<User>("users")`
62
+ and `table<User[]>("users")` both infer `User` rows; the array form is useful
63
+ when you want the generic to read like the returned data.
64
+
65
+ For computed projections, pass the projection shape to `select<T>()`:
66
+
67
+ ```ts
68
+ import type { LuxAggregateRow, LuxNearRow } from "@luxdb/sdk";
69
+
70
+ type TeamStats = { team_id: number } & LuxAggregateRow<"member_count" | "avg_age">;
71
+
72
+ const { data: teamStats } = await lux
73
+ .table<User>("members")
74
+ .select<TeamStats>("team_id,COUNT(*) AS member_count,AVG(age) AS avg_age")
75
+ .group("team_id");
76
+
77
+ const { data: matches } = await lux
78
+ .table<Message>("messages")
79
+ .select<LuxNearRow<Message>>("id,body,_similarity")
80
+ .near("embedding", queryEmbedding, { k: 10, threshold: 0.8 });
81
+ ```
82
+
83
+ ```ts
84
+ const { data: inserted, error: insertError } = await lux
85
+ .table("messages")
86
+ .insert({ body: "hello", channel: "general" });
87
+
88
+ const { data: updated, error: updateError } = await lux
89
+ .table("messages")
90
+ .update({ body: "edited" })
91
+ .eq("id", inserted?.id);
92
+
93
+ const { data: deleted, error: deleteError } = await lux
94
+ .table("messages")
95
+ .delete()
96
+ .eq("id", inserted?.id);
97
+ ```
98
+
99
+ ## Live tables
100
+
101
+ Browser clients can subscribe to table queries over Lux Live. The SDK opens a WebSocket to the project live endpoint, and Lux core sends a snapshot followed by insert/update/delete events for rows matching the query.
102
+
103
+ ```ts
104
+ const sub = lux
105
+ .table<{ id: string; channel_id: string; body: string }>("messages")
106
+ .eq("channel_id", "general")
107
+ .live()
108
+ .on("snapshot", (event) => {
109
+ console.log(event.rows);
110
+ })
111
+ .on("insert", (event) => {
112
+ console.log(event.new);
113
+ })
114
+ .on("update", (event) => {
115
+ console.log(event.old, event.new);
116
+ })
117
+ .on("delete", (event) => {
118
+ console.log(event.old);
119
+ });
120
+
121
+ await sub.unsubscribe();
122
+ ```
123
+
124
+ ## OAuth
125
+
126
+ ```ts
127
+ const { data, error } = await lux.auth.signInWithOAuth({
128
+ provider: "google",
129
+ redirectTo: "https://app.example.com/auth/callback",
130
+ });
131
+
132
+ if (error) throw error;
133
+ ```
134
+
135
+ On your callback page:
136
+
137
+ ```ts
138
+ const { data, error } = await lux.auth.consumeOAuthRedirect();
139
+
140
+ if (error) throw error;
141
+ console.log(data.user);
142
+ ```
143
+
144
+ Auth types are exported for app code and system table reads:
145
+
146
+ ```ts
147
+ import type { LuxUser, LuxAuthTables } from "@luxdb/sdk";
148
+
149
+ type AuthUserRow = LuxAuthTables["auth.users"];
150
+
151
+ function renderUser(user: LuxUser, row: AuthUserRow) {
152
+ return row.email ?? user.email;
153
+ }
154
+ ```
155
+
156
+ ## Server client
157
+
158
+ Use a secret key only from trusted server code.
159
+
160
+ ```ts
161
+ import { createClient } from "@luxdb/sdk";
162
+
163
+ const admin = createClient(
164
+ "https://api.luxdb.dev/v1/my-project",
165
+ process.env.LUX_SECRET_KEY!
166
+ );
167
+
168
+ const { data: users, error } = await admin.auth.listUsers();
169
+ ```
170
+
171
+ ## SSR client
172
+
173
+ Use `createServerClient` with your framework's cookie methods to persist sessions on the server.
174
+
175
+ ```ts
176
+ import { createServerClient } from "@luxdb/sdk";
177
+
178
+ const lux = createServerClient(
179
+ "https://api.luxdb.dev/v1/my-project",
180
+ "lux_pub_...",
181
+ { cookies }
182
+ );
183
+ ```
184
+
185
+ ## Direct Lux/Redis-compatible access
186
+
187
+ Use direct access for trusted infrastructure that needs RESP commands, low-level primitives, or compatibility with Redis workflows. Do not ship database passwords to browsers.
188
+
189
+ ```ts
190
+ import Lux from "@luxdb/sdk";
191
+
192
+ const lux = new Lux("lux://:password@localhost:6379");
193
+
194
+ await lux.set("hello", "world");
195
+ const value = await lux.get("hello");
196
+ ```
197
+
198
+ ## Access model
199
+
200
+ - `lux_pub_...` keys are safe for browser app calls.
201
+ - `lux_sec_...` keys are server-only.
202
+ - User sessions issue JWT access tokens.
203
+ - Browser live subscriptions use the project publishable key plus the signed-in user's JWT.
204
+ - Table `select()` accepts Lux's constrained projection grammar, not arbitrary SQL.
205
+ - Direct `lux://` or `rediss://` database access uses the database password and is for trusted infrastructure.
package/dist/cjs/index.js CHANGED
@@ -44,10 +44,7 @@ class Lux extends ioredis_1.default {
44
44
  constructor(options) {
45
45
  let authOptions = {};
46
46
  if (typeof options === 'string') {
47
- if (options.startsWith('rediss://') || options.startsWith('luxs://')) {
48
- throw new Error('TLS is not yet supported');
49
- }
50
- options = options.replace(/^lux:\/\//, 'redis://');
47
+ options = options.replace(/^luxs:\/\//, 'rediss://').replace(/^lux:\/\//, 'redis://');
51
48
  }
52
49
  else if (options) {
53
50
  const { httpUrl, apiKey, authToken, fetch: fetchImpl, ...redisOptions } = options;
@@ -1,15 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LuxProjectMutationBuilder = exports.LuxProjectInsertBuilder = exports.LuxProjectSelectBuilder = exports.LuxProjectTable = exports.LuxProjectClient = void 0;
3
+ exports.LuxProjectMutationBuilder = exports.LuxProjectInsertBuilder = exports.LuxProjectLiveSubscription = exports.LuxProjectSelectBuilder = exports.LuxProjectTable = exports.LuxProjectClient = void 0;
4
4
  exports.createProjectClient = createProjectClient;
5
5
  exports.createClient = createClient;
6
6
  const auth_1 = require("./auth");
7
7
  const utils_1 = require("./utils");
8
8
  class LuxProjectClient {
9
9
  constructor(options) {
10
+ this.liveSocket = null;
11
+ this.liveSubscriptions = new Map();
12
+ this.livePending = [];
10
13
  this.url = options.url.replace(/\/+$/, '');
11
14
  this.key = options.key;
12
15
  this.fetchImpl = resolveFetch(options.fetch);
16
+ this.WebSocketImpl = options.websocket;
13
17
  this.auth = new auth_1.LuxAuthClient({
14
18
  ...options.auth,
15
19
  httpUrl: this.url,
@@ -84,6 +88,96 @@ class LuxProjectClient {
84
88
  return (0, utils_1.err)('LUX_PROJECT_REQUEST_ERROR', 'Lux request failed', (0, utils_1.toLuxError)(error));
85
89
  }
86
90
  }
91
+ async _subscribeLive(spec, handler, error) {
92
+ const id = `sub_${Math.random().toString(36).slice(2)}_${Date.now().toString(36)}`;
93
+ const record = { id, spec, handler, error };
94
+ this.liveSubscriptions.set(id, record);
95
+ await this.ensureLiveSocket();
96
+ this.sendLive({
97
+ type: 'live.subscribe',
98
+ id,
99
+ spec,
100
+ });
101
+ return () => {
102
+ this.liveSubscriptions.delete(id);
103
+ this.sendLive({ type: 'live.unsubscribe', id });
104
+ if (this.liveSubscriptions.size === 0) {
105
+ this.liveSocket?.close();
106
+ this.liveSocket = null;
107
+ }
108
+ };
109
+ }
110
+ async ensureLiveSocket() {
111
+ const WebSocketImpl = resolveWebSocket(this.WebSocketImpl);
112
+ this.WebSocketImpl = WebSocketImpl;
113
+ if (this.liveSocket &&
114
+ (this.liveSocket.readyState === WebSocketImpl.OPEN ||
115
+ this.liveSocket.readyState === WebSocketImpl.CONNECTING)) {
116
+ return;
117
+ }
118
+ const accessToken = await this.auth.getAccessToken();
119
+ const url = new URL(`${this.url}/live`);
120
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
121
+ url.searchParams.set('apikey', this.key);
122
+ if (accessToken)
123
+ url.searchParams.set('access_token', accessToken);
124
+ const socket = new WebSocketImpl(url.toString());
125
+ this.liveSocket = socket;
126
+ socket.onopen = () => {
127
+ for (const message of this.livePending.splice(0))
128
+ socket.send(message);
129
+ };
130
+ socket.onmessage = (event) => {
131
+ let message;
132
+ try {
133
+ message = JSON.parse(String(event.data));
134
+ }
135
+ catch {
136
+ return;
137
+ }
138
+ const subscription = typeof message.id === 'string' ? this.liveSubscriptions.get(message.id) : null;
139
+ if (message.type === 'live.event' && subscription) {
140
+ subscription.handler(message.event);
141
+ return;
142
+ }
143
+ if (message.type === 'live.error') {
144
+ const target = subscription ? [subscription] : [...this.liveSubscriptions.values()];
145
+ for (const sub of target) {
146
+ sub.error(message.error || { code: 'LIVE_ERROR', message: 'Live subscription failed' });
147
+ }
148
+ }
149
+ };
150
+ socket.onerror = () => {
151
+ for (const subscription of this.liveSubscriptions.values()) {
152
+ subscription.error({ code: 'LIVE_SOCKET_ERROR', message: 'Live socket failed' });
153
+ }
154
+ };
155
+ socket.onclose = () => {
156
+ if (this.liveSocket === socket)
157
+ this.liveSocket = null;
158
+ if (this.liveSubscriptions.size > 0) {
159
+ for (const subscription of this.liveSubscriptions.values()) {
160
+ this.livePending.push(JSON.stringify({
161
+ type: 'live.subscribe',
162
+ id: subscription.id,
163
+ spec: subscription.spec,
164
+ }));
165
+ }
166
+ setTimeout(() => {
167
+ void this.ensureLiveSocket();
168
+ }, 1000);
169
+ }
170
+ };
171
+ }
172
+ sendLive(message) {
173
+ const payload = JSON.stringify(message);
174
+ const WebSocketImpl = this.WebSocketImpl;
175
+ if (WebSocketImpl && this.liveSocket?.readyState === WebSocketImpl.OPEN) {
176
+ this.liveSocket.send(payload);
177
+ return;
178
+ }
179
+ this.livePending.push(payload);
180
+ }
87
181
  }
88
182
  exports.LuxProjectClient = LuxProjectClient;
89
183
  class LuxProjectTable {
@@ -94,6 +188,33 @@ class LuxProjectTable {
94
188
  select(columns = '*') {
95
189
  return new LuxProjectSelectBuilder(this.client, this.name, columns);
96
190
  }
191
+ eq(column, value) {
192
+ return this.select().eq(column, value);
193
+ }
194
+ neq(column, value) {
195
+ return this.select().neq(column, value);
196
+ }
197
+ gt(column, value) {
198
+ return this.select().gt(column, value);
199
+ }
200
+ gte(column, value) {
201
+ return this.select().gte(column, value);
202
+ }
203
+ lt(column, value) {
204
+ return this.select().lt(column, value);
205
+ }
206
+ lte(column, value) {
207
+ return this.select().lte(column, value);
208
+ }
209
+ near(column, vector, options = {}) {
210
+ return this.select().near(column, vector, options);
211
+ }
212
+ is(column, value) {
213
+ return this.select().is(column, value);
214
+ }
215
+ live() {
216
+ return this.select().live();
217
+ }
97
218
  insert(rowOrRows) {
98
219
  return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows);
99
220
  }
@@ -128,6 +249,9 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
128
249
  this.client = client;
129
250
  this.tableName = tableName;
130
251
  this.filters = [];
252
+ this.joins = [];
253
+ this.groupColumns = [];
254
+ this.havingFilters = [];
131
255
  }
132
256
  eq(column, value) {
133
257
  return this.addFilter(column, 'eq', value);
@@ -150,6 +274,24 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
150
274
  is(column, value) {
151
275
  return this.addFilter(column, 'is', value);
152
276
  }
277
+ join(table, alias, onLeft, onRight) {
278
+ this.joins.push({ type: 'inner', table, alias, onLeft, onRight });
279
+ return this;
280
+ }
281
+ leftJoin(table, alias, onLeft, onRight) {
282
+ this.joins.push({ type: 'left', table, alias, onLeft, onRight });
283
+ return this;
284
+ }
285
+ group(columns) {
286
+ this.groupColumns = Array.isArray(columns)
287
+ ? columns
288
+ : columns.split(',').map((column) => column.trim()).filter(Boolean);
289
+ return this;
290
+ }
291
+ having(column, operator, value) {
292
+ this.havingFilters.push({ column, operator, value });
293
+ return this;
294
+ }
153
295
  addFilter(column, operator, value) {
154
296
  this.filters.push({ column, operator, value });
155
297
  return this;
@@ -158,6 +300,22 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
158
300
  const params = new URLSearchParams();
159
301
  if (this.filters.length)
160
302
  params.set('where', filtersToWhere(this.filters));
303
+ for (const join of this.joins) {
304
+ const kind = join.type === 'left' ? ':left' : '';
305
+ params.append('join', `${join.table}:${join.alias}${kind}:on(${join.onLeft}=${join.onRight})`);
306
+ }
307
+ if (this.groupColumns.length)
308
+ params.set('group', this.groupColumns.join(','));
309
+ if (this.havingFilters.length)
310
+ params.set('having', havingToWhere(this.havingFilters));
311
+ if (this.nearQuery) {
312
+ params.set('near_field', this.nearQuery.field);
313
+ params.set('near_vector', `[${this.nearQuery.vector.join(',')}]`);
314
+ params.set('near_k', String(this.nearQuery.k));
315
+ if (this.nearQuery.threshold != null) {
316
+ params.set('near_threshold', String(this.nearQuery.threshold));
317
+ }
318
+ }
161
319
  if (this.orderBy) {
162
320
  params.set('order', `${this.orderBy.column} ${this.orderBy.ascending ? 'ASC' : 'DESC'}`);
163
321
  }
@@ -178,6 +336,15 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
178
336
  this.orderBy = { column, ascending: options.ascending ?? true };
179
337
  return this;
180
338
  }
339
+ near(column, vector, options = {}) {
340
+ this.nearQuery = {
341
+ field: column,
342
+ vector,
343
+ k: options.k ?? 10,
344
+ threshold: options.threshold,
345
+ };
346
+ return this;
347
+ }
181
348
  limit(count) {
182
349
  this.limitCount = count;
183
350
  return this;
@@ -210,8 +377,119 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
210
377
  }
211
378
  return (0, utils_1.ok)(rows[0]);
212
379
  }
380
+ live() {
381
+ return new LuxProjectLiveSubscription(this.client, this.tableName, this.columns, this.filters, this.nearQuery, this.orderBy, this.limitCount, this.offsetCount);
382
+ }
213
383
  }
214
384
  exports.LuxProjectSelectBuilder = LuxProjectSelectBuilder;
385
+ class LuxProjectLiveSubscription {
386
+ constructor(client, table, columns, filters, nearQuery, orderBy, limitCount, offsetCount) {
387
+ this.client = client;
388
+ this.table = table;
389
+ this.columns = columns;
390
+ this.filters = filters;
391
+ this.nearQuery = nearQuery;
392
+ this.orderBy = orderBy;
393
+ this.limitCount = limitCount;
394
+ this.offsetCount = offsetCount;
395
+ this.handlers = {
396
+ snapshot: [],
397
+ insert: [],
398
+ update: [],
399
+ delete: [],
400
+ error: [],
401
+ change: [],
402
+ };
403
+ this.unsubscribeFn = null;
404
+ void this.start();
405
+ }
406
+ on(type, handler) {
407
+ this.handlers[type].push(handler);
408
+ return this;
409
+ }
410
+ async unsubscribe() {
411
+ this.unsubscribeFn?.();
412
+ this.unsubscribeFn = null;
413
+ }
414
+ async start() {
415
+ this.unsubscribeFn = await this.client._subscribeLive(this.spec(), (event) => this.handleEvent(event), (error) => this.emit({
416
+ type: 'error',
417
+ table: this.table,
418
+ new: null,
419
+ old: null,
420
+ error,
421
+ }));
422
+ }
423
+ spec() {
424
+ const spec = {
425
+ kind: 'table',
426
+ table: this.table,
427
+ select: this.columns,
428
+ };
429
+ if (this.filters.length) {
430
+ spec.where = this.filters.map((filter) => ({
431
+ field: filter.column,
432
+ op: filterOperatorToWhere(filter.operator),
433
+ value: filter.value,
434
+ }));
435
+ }
436
+ if (this.nearQuery) {
437
+ spec.near = {
438
+ field: this.nearQuery.field,
439
+ vector: this.nearQuery.vector,
440
+ k: this.nearQuery.k,
441
+ threshold: this.nearQuery.threshold,
442
+ };
443
+ }
444
+ if (this.orderBy) {
445
+ spec.orderBy = {
446
+ field: this.orderBy.column,
447
+ dir: this.orderBy.ascending ? 'asc' : 'desc',
448
+ };
449
+ }
450
+ if (this.limitCount != null)
451
+ spec.limit = this.limitCount;
452
+ if (this.offsetCount != null)
453
+ spec.offset = this.offsetCount;
454
+ return spec;
455
+ }
456
+ handleEvent(raw) {
457
+ if (!raw || typeof raw !== 'object')
458
+ return;
459
+ const event = raw;
460
+ if (event.kind === 'snapshot') {
461
+ this.emit({
462
+ type: 'snapshot',
463
+ table: this.table,
464
+ new: null,
465
+ old: null,
466
+ rows: Array.isArray(event.rows) ? event.rows : [],
467
+ raw,
468
+ });
469
+ return;
470
+ }
471
+ if (event.kind === 'insert' || event.kind === 'update' || event.kind === 'delete') {
472
+ this.emit({
473
+ type: event.kind,
474
+ table: this.table,
475
+ pk: event.pk == null ? undefined : String(event.pk),
476
+ new: event.row ?? null,
477
+ old: event.previous ?? null,
478
+ changed: Array.isArray(event.changed) ? event.changed : undefined,
479
+ raw,
480
+ });
481
+ }
482
+ }
483
+ emit(event) {
484
+ for (const handler of this.handlers[event.type])
485
+ handler(event);
486
+ if (event.type !== 'snapshot' && event.type !== 'error') {
487
+ for (const handler of this.handlers.change)
488
+ handler(event);
489
+ }
490
+ }
491
+ }
492
+ exports.LuxProjectLiveSubscription = LuxProjectLiveSubscription;
215
493
  class LuxProjectInsertBuilder extends LuxProjectThenable {
216
494
  constructor(client, tableName, rowOrRows) {
217
495
  super();
@@ -273,6 +551,12 @@ function filtersToWhere(filters) {
273
551
  return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
274
552
  }).join(' AND ');
275
553
  }
554
+ function havingToWhere(filters) {
555
+ return filters.map((filter) => {
556
+ const op = filterOperatorToWhere(filter.operator);
557
+ return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
558
+ }).join(' AND ');
559
+ }
276
560
  function filterOperatorToWhere(operator) {
277
561
  switch (operator) {
278
562
  case 'eq':
@@ -311,3 +595,10 @@ function resolveFetch(fetchImpl) {
311
595
  }
312
596
  return candidate;
313
597
  }
598
+ function resolveWebSocket(websocketImpl) {
599
+ const candidate = websocketImpl ?? globalThis.WebSocket;
600
+ if (!candidate) {
601
+ throw new Error('Lux project live subscriptions require a WebSocket implementation');
602
+ }
603
+ return candidate;
604
+ }