@luxdb/sdk 1.4.2 → 2.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/README.md CHANGED
@@ -35,8 +35,20 @@ if (error) throw error;
35
35
  Queries and mutations return a Supabase-style result object:
36
36
 
37
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
+
38
50
  const { data: users, error } = await lux
39
- .table<{ id: number; email: string; age: number }>("users")
51
+ .table<User[]>("users")
40
52
  .select()
41
53
  .gt("age", 25)
42
54
  .order("age", { ascending: false })
@@ -46,6 +58,28 @@ if (error) throw error;
46
58
  console.log(users);
47
59
  ```
48
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
+
49
83
  ```ts
50
84
  const { data: inserted, error: insertError } = await lux
51
85
  .table("messages")
@@ -62,6 +96,69 @@ const { data: deleted, error: deleteError } = await lux
62
96
  .eq("id", inserted?.id);
63
97
  ```
64
98
 
99
+ ### Filters and JSON
100
+
101
+ Beyond `.eq/.neq/.gt/.gte/.lt/.lte`, the query builder supports `IN` lists, JSON
102
+ dot-paths, and arrays:
103
+
104
+ ```ts
105
+ await lux.table("users").select().in("id", [1, 2, 3]);
106
+ await lux.table("users").select().notIn("status", ["banned", "deleted"]);
107
+
108
+ // JSON columns round-trip as native objects (no manual JSON.stringify)
109
+ await lux.table("events").insert({ metadata: { plan: { tier: "pro" }, count: 0 } });
110
+
111
+ // Query JSON by dot-path, like a JS object. A path that does not resolve is a
112
+ // non-match, never an error.
113
+ await lux.table("events").select().eq("metadata.plan.tier", "pro");
114
+
115
+ // IS VALID is existence, not truthiness: 0 / false / "" all count as valid.
116
+ await lux.table("events").select().isValid("metadata.count");
117
+ await lux.table("events").select().isNotValid("metadata.deleted_at");
118
+
119
+ // Array membership, and a declared JSON-path index for range queries at scale.
120
+ await lux.table("events").select().contains("tags", "urgent");
121
+ await lux.table("events").createIndex("metadata.plan.tier", "str");
122
+ ```
123
+
124
+ ## Live tables
125
+
126
+ 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.
127
+
128
+ `.live()` resolves once the server confirms the subscription, returning the same
129
+ `{ data, error }` shape as the rest of the SDK (here named `{ live, error }`). If
130
+ the query isn't permitted by a read grant, `error` is populated and `live` is
131
+ `null`. The subscription is async-iterable: the buffered snapshot arrives first,
132
+ then live changes.
133
+
134
+ ```ts
135
+ const { live, error } = await lux
136
+ .table<{ id: string; channel_id: string; body: string }>("messages")
137
+ .eq("channel_id", "general")
138
+ .live();
139
+
140
+ if (error) throw error;
141
+
142
+ for await (const event of live) {
143
+ if (event.type === "snapshot") console.log(event.rows);
144
+ else console.log(event.type, event.new ?? event.old);
145
+ }
146
+ ```
147
+
148
+ You can also attach callbacks instead of iterating:
149
+
150
+ ```ts
151
+ const { live, error } = await lux.table("messages").eq("channel_id", "general").live();
152
+ if (error) throw error;
153
+
154
+ live
155
+ .on("insert", (event) => console.log(event.new))
156
+ .on("update", (event) => console.log(event.old, event.new))
157
+ .on("delete", (event) => console.log(event.old));
158
+
159
+ await live.unsubscribe();
160
+ ```
161
+
65
162
  ## OAuth
66
163
 
67
164
  ```ts
@@ -82,6 +179,18 @@ if (error) throw error;
82
179
  console.log(data.user);
83
180
  ```
84
181
 
182
+ Auth types are exported for app code and system table reads:
183
+
184
+ ```ts
185
+ import type { LuxUser, LuxAuthTables } from "@luxdb/sdk";
186
+
187
+ type AuthUserRow = LuxAuthTables["auth.users"];
188
+
189
+ function renderUser(user: LuxUser, row: AuthUserRow) {
190
+ return row.email ?? user.email;
191
+ }
192
+ ```
193
+
85
194
  ## Server client
86
195
 
87
196
  Use a secret key only from trusted server code.
@@ -129,4 +238,7 @@ const value = await lux.get("hello");
129
238
  - `lux_pub_...` keys are safe for browser app calls.
130
239
  - `lux_sec_...` keys are server-only.
131
240
  - User sessions issue JWT access tokens.
241
+ - Browser live subscriptions use the project publishable key plus the signed-in user's JWT.
242
+ - Table `select()` accepts Lux's constrained projection grammar, not arbitrary SQL.
132
243
  - Direct `lux://` or `rediss://` database access uses the database password and is for trusted infrastructure.
244
+ - With auth enabled, signed-in users are denied by default and gated by per-table **grants** (`GRANT read, write ON t WHERE user_id = auth.uid()`). Reads, writes, and `.live()` are all checked against the grant: a query or subscription must satisfy the predicate or it is rejected (an unscoped `.live()` under a row-scoped grant fails at subscribe time). Grants are authored as migrations.
package/dist/cjs/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Lux = exports.TableSubscription = exports.TableQueryBuilder = exports.createServerClient = exports.createBrowserClient = exports.LuxProjectClient = exports.createProjectClient = void 0;
6
+ exports.Lux = exports.TableSubscription = exports.TableQueryBuilder = exports.createServerClient = exports.createBrowserClient = exports.LuxProjectLiveSubscription = exports.LuxProjectClient = exports.createProjectClient = void 0;
7
7
  exports.createClient = createClient;
8
8
  exports.createAuthClient = createAuthClient;
9
9
  const ioredis_1 = __importDefault(require("ioredis"));
@@ -14,6 +14,8 @@ Object.defineProperty(exports, "LuxProjectClient", { enumerable: true, get: func
14
14
  const namespaces_1 = require("./namespaces");
15
15
  const realtime_1 = require("./realtime");
16
16
  const table_1 = require("./table");
17
+ var project_2 = require("./project");
18
+ Object.defineProperty(exports, "LuxProjectLiveSubscription", { enumerable: true, get: function () { return project_2.LuxProjectLiveSubscription; } });
17
19
  var browser_1 = require("./browser");
18
20
  Object.defineProperty(exports, "createBrowserClient", { enumerable: true, get: function () { return browser_1.createBrowserClient; } });
19
21
  var ssr_1 = require("./ssr");
@@ -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,39 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
150
274
  is(column, value) {
151
275
  return this.addFilter(column, 'is', value);
152
276
  }
277
+ in(column, values) {
278
+ return this.addFilter(column, 'in', values);
279
+ }
280
+ notIn(column, values) {
281
+ return this.addFilter(column, 'notIn', values);
282
+ }
283
+ isValid(column) {
284
+ return this.addFilter(column, 'isValid', '');
285
+ }
286
+ isNotValid(column) {
287
+ return this.addFilter(column, 'isNotValid', '');
288
+ }
289
+ contains(column, value) {
290
+ return this.addFilter(column, 'contains', value);
291
+ }
292
+ join(table, alias, onLeft, onRight) {
293
+ this.joins.push({ type: 'inner', table, alias, onLeft, onRight });
294
+ return this;
295
+ }
296
+ leftJoin(table, alias, onLeft, onRight) {
297
+ this.joins.push({ type: 'left', table, alias, onLeft, onRight });
298
+ return this;
299
+ }
300
+ group(columns) {
301
+ this.groupColumns = Array.isArray(columns)
302
+ ? columns
303
+ : columns.split(',').map((column) => column.trim()).filter(Boolean);
304
+ return this;
305
+ }
306
+ having(column, operator, value) {
307
+ this.havingFilters.push({ column, operator, value });
308
+ return this;
309
+ }
153
310
  addFilter(column, operator, value) {
154
311
  this.filters.push({ column, operator, value });
155
312
  return this;
@@ -158,6 +315,22 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
158
315
  const params = new URLSearchParams();
159
316
  if (this.filters.length)
160
317
  params.set('where', filtersToWhere(this.filters));
318
+ for (const join of this.joins) {
319
+ const kind = join.type === 'left' ? ':left' : '';
320
+ params.append('join', `${join.table}:${join.alias}${kind}:on(${join.onLeft}=${join.onRight})`);
321
+ }
322
+ if (this.groupColumns.length)
323
+ params.set('group', this.groupColumns.join(','));
324
+ if (this.havingFilters.length)
325
+ params.set('having', havingToWhere(this.havingFilters));
326
+ if (this.nearQuery) {
327
+ params.set('near_field', this.nearQuery.field);
328
+ params.set('near_vector', `[${this.nearQuery.vector.join(',')}]`);
329
+ params.set('near_k', String(this.nearQuery.k));
330
+ if (this.nearQuery.threshold != null) {
331
+ params.set('near_threshold', String(this.nearQuery.threshold));
332
+ }
333
+ }
161
334
  if (this.orderBy) {
162
335
  params.set('order', `${this.orderBy.column} ${this.orderBy.ascending ? 'ASC' : 'DESC'}`);
163
336
  }
@@ -178,6 +351,15 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
178
351
  this.orderBy = { column, ascending: options.ascending ?? true };
179
352
  return this;
180
353
  }
354
+ near(column, vector, options = {}) {
355
+ this.nearQuery = {
356
+ field: column,
357
+ vector,
358
+ k: options.k ?? 10,
359
+ threshold: options.threshold,
360
+ };
361
+ return this;
362
+ }
181
363
  limit(count) {
182
364
  this.limitCount = count;
183
365
  return this;
@@ -210,8 +392,202 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
210
392
  }
211
393
  return (0, utils_1.ok)(rows[0]);
212
394
  }
395
+ async live() {
396
+ const live = new LuxProjectLiveSubscription(this.client, this.tableName, this.columns, this.filters, this.nearQuery, this.orderBy, this.limitCount, this.offsetCount);
397
+ const error = await live.start();
398
+ if (error) {
399
+ await live.unsubscribe();
400
+ return { live: null, error };
401
+ }
402
+ return { live, error: null };
403
+ }
213
404
  }
214
405
  exports.LuxProjectSelectBuilder = LuxProjectSelectBuilder;
406
+ class LuxProjectLiveSubscription {
407
+ constructor(client, table, columns, filters, nearQuery, orderBy, limitCount, offsetCount) {
408
+ this.client = client;
409
+ this.table = table;
410
+ this.columns = columns;
411
+ this.filters = filters;
412
+ this.nearQuery = nearQuery;
413
+ this.orderBy = orderBy;
414
+ this.limitCount = limitCount;
415
+ this.offsetCount = offsetCount;
416
+ this.handlers = {
417
+ snapshot: [],
418
+ insert: [],
419
+ update: [],
420
+ delete: [],
421
+ error: [],
422
+ change: [],
423
+ };
424
+ this.unsubscribeFn = null;
425
+ // Async-iterator plumbing: events buffer in `queue` until a `for await`
426
+ // consumer pulls them; pending `next()` calls park in `waiters`.
427
+ this.queue = [];
428
+ this.waiters = [];
429
+ this.closed = false;
430
+ }
431
+ on(type, handler) {
432
+ this.handlers[type].push(handler);
433
+ return this;
434
+ }
435
+ async unsubscribe() {
436
+ this.unsubscribeFn?.();
437
+ this.unsubscribeFn = null;
438
+ this.close();
439
+ }
440
+ /**
441
+ * Open the subscription and wait for the server to confirm it. Resolves
442
+ * `null` once the initial snapshot arrives, or a `LuxError` if the
443
+ * subscription is rejected (e.g. a grant `FORBIDDEN`) or the socket fails.
444
+ * Subsequent errors after a successful start surface via `on('error')` and
445
+ * end the async iterator.
446
+ */
447
+ async start() {
448
+ let settled = false;
449
+ let settle;
450
+ const ready = new Promise((resolve) => {
451
+ settle = (error) => {
452
+ if (settled)
453
+ return;
454
+ settled = true;
455
+ resolve(error);
456
+ };
457
+ });
458
+ // Safety net: a server that never answers shouldn't hang the caller.
459
+ const timeout = setTimeout(() => {
460
+ settle({ code: 'LIVE_TIMEOUT', message: 'Timed out establishing live subscription' });
461
+ }, 15000);
462
+ this.unsubscribeFn = await this.client._subscribeLive(this.spec(), (event) => {
463
+ const kind = event?.kind;
464
+ this.handleEvent(event);
465
+ if (kind === 'snapshot')
466
+ settle(null);
467
+ }, (error) => {
468
+ const luxError = {
469
+ code: error.code ?? 'LIVE_ERROR',
470
+ message: error.message ?? 'Live subscription failed',
471
+ };
472
+ if (settled) {
473
+ // Post-start failure: notify handlers and end the stream.
474
+ this.emit({ type: 'error', table: this.table, new: null, old: null, error });
475
+ this.close();
476
+ }
477
+ else {
478
+ settle(luxError);
479
+ }
480
+ });
481
+ const result = await ready;
482
+ clearTimeout(timeout);
483
+ return result;
484
+ }
485
+ [Symbol.asyncIterator]() {
486
+ return {
487
+ next: () => {
488
+ const buffered = this.queue.shift();
489
+ if (buffered)
490
+ return Promise.resolve({ value: buffered, done: false });
491
+ if (this.closed)
492
+ return Promise.resolve({ value: undefined, done: true });
493
+ return new Promise((resolve) => this.waiters.push(resolve));
494
+ },
495
+ return: () => {
496
+ void this.unsubscribe();
497
+ return Promise.resolve({ value: undefined, done: true });
498
+ },
499
+ };
500
+ }
501
+ close() {
502
+ if (this.closed)
503
+ return;
504
+ this.closed = true;
505
+ for (const waiter of this.waiters.splice(0)) {
506
+ waiter({ value: undefined, done: true });
507
+ }
508
+ }
509
+ spec() {
510
+ const spec = {
511
+ kind: 'table',
512
+ table: this.table,
513
+ select: this.columns,
514
+ };
515
+ if (this.filters.length) {
516
+ spec.where = this.filters.map((filter) => ({
517
+ field: filter.column,
518
+ op: filterOperatorToWhere(filter.operator),
519
+ value: filter.value,
520
+ }));
521
+ }
522
+ if (this.nearQuery) {
523
+ spec.near = {
524
+ field: this.nearQuery.field,
525
+ vector: this.nearQuery.vector,
526
+ k: this.nearQuery.k,
527
+ threshold: this.nearQuery.threshold,
528
+ };
529
+ }
530
+ if (this.orderBy) {
531
+ spec.orderBy = {
532
+ field: this.orderBy.column,
533
+ dir: this.orderBy.ascending ? 'asc' : 'desc',
534
+ };
535
+ }
536
+ if (this.limitCount != null)
537
+ spec.limit = this.limitCount;
538
+ if (this.offsetCount != null)
539
+ spec.offset = this.offsetCount;
540
+ return spec;
541
+ }
542
+ handleEvent(raw) {
543
+ if (!raw || typeof raw !== 'object')
544
+ return;
545
+ const event = raw;
546
+ if (event.kind === 'snapshot') {
547
+ this.emit({
548
+ type: 'snapshot',
549
+ table: this.table,
550
+ new: null,
551
+ old: null,
552
+ rows: Array.isArray(event.rows) ? event.rows : [],
553
+ raw,
554
+ });
555
+ return;
556
+ }
557
+ if (event.kind === 'insert' || event.kind === 'update' || event.kind === 'delete') {
558
+ this.emit({
559
+ type: event.kind,
560
+ table: this.table,
561
+ pk: event.pk == null ? undefined : String(event.pk),
562
+ new: event.row ?? null,
563
+ old: event.previous ?? null,
564
+ changed: Array.isArray(event.changed) ? event.changed : undefined,
565
+ raw,
566
+ });
567
+ }
568
+ }
569
+ emit(event) {
570
+ for (const handler of this.handlers[event.type])
571
+ handler(event);
572
+ if (event.type !== 'snapshot' && event.type !== 'error') {
573
+ for (const handler of this.handlers.change)
574
+ handler(event);
575
+ }
576
+ // Feed `for await` consumers the data events (errors end the stream via close()).
577
+ if (event.type !== 'error')
578
+ this.pushIterator(event);
579
+ }
580
+ pushIterator(event) {
581
+ if (this.closed)
582
+ return;
583
+ const waiter = this.waiters.shift();
584
+ if (waiter)
585
+ waiter({ value: event, done: false });
586
+ else
587
+ this.queue.push(event);
588
+ }
589
+ }
590
+ exports.LuxProjectLiveSubscription = LuxProjectLiveSubscription;
215
591
  class LuxProjectInsertBuilder extends LuxProjectThenable {
216
592
  constructor(client, tableName, rowOrRows) {
217
593
  super();
@@ -268,6 +644,19 @@ function normalizeWhere(where) {
268
644
  return where.trim().replace(/\s*(>=|<=|!=|=|>|<)\s*/g, ' $1 ');
269
645
  }
270
646
  function filtersToWhere(filters) {
647
+ return filters.map((filter) => {
648
+ const op = filterOperatorToWhere(filter.operator);
649
+ if (filter.operator === 'in' || filter.operator === 'notIn') {
650
+ const values = Array.isArray(filter.value) ? filter.value : [filter.value];
651
+ return normalizeWhere(`${filter.column} ${op} ( ${values.map(formatWhereValue).join(' ')} )`);
652
+ }
653
+ if (filter.operator === 'isValid' || filter.operator === 'isNotValid') {
654
+ return normalizeWhere(`${filter.column} ${op}`);
655
+ }
656
+ return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
657
+ }).join(' AND ');
658
+ }
659
+ function havingToWhere(filters) {
271
660
  return filters.map((filter) => {
272
661
  const op = filterOperatorToWhere(filter.operator);
273
662
  return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
@@ -288,6 +677,16 @@ function filterOperatorToWhere(operator) {
288
677
  return '<';
289
678
  case 'lte':
290
679
  return '<=';
680
+ case 'in':
681
+ return 'IN';
682
+ case 'notIn':
683
+ return 'NOT IN';
684
+ case 'isValid':
685
+ return 'IS VALID';
686
+ case 'isNotValid':
687
+ return 'IS NOT VALID';
688
+ case 'contains':
689
+ return 'CONTAINS';
291
690
  }
292
691
  }
293
692
  function formatWhereValue(value) {
@@ -311,3 +710,10 @@ function resolveFetch(fetchImpl) {
311
710
  }
312
711
  return candidate;
313
712
  }
713
+ function resolveWebSocket(websocketImpl) {
714
+ const candidate = websocketImpl ?? globalThis.WebSocket;
715
+ if (!candidate) {
716
+ throw new Error('Lux project live subscriptions require a WebSocket implementation');
717
+ }
718
+ return candidate;
719
+ }