@luxdb/sdk 1.4.3 → 2.1.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
@@ -80,45 +80,102 @@ const { data: matches } = await lux
80
80
  .near("embedding", queryEmbedding, { k: 10, threshold: 0.8 });
81
81
  ```
82
82
 
83
+ Writes return the affected row(s), including server-generated columns (`id`,
84
+ UUIDv7 primary keys, `DEFAULT now()` timestamps):
85
+
83
86
  ```ts
87
+ // insert -> the inserted row
84
88
  const { data: inserted, error: insertError } = await lux
85
89
  .table("messages")
86
90
  .insert({ body: "hello", channel: "general" });
87
91
 
88
- const { data: updated, error: updateError } = await lux
92
+ // bulk insert in a single request -> array of rows
93
+ const { data: many } = await lux
94
+ .table("messages")
95
+ .insert([{ body: "a" }, { body: "b" }]);
96
+
97
+ // upsert: insert, or update the row that conflicts on `onConflict` (default: PK)
98
+ const { data: user } = await lux
99
+ .table("users")
100
+ .upsert({ email: "a@x.com", name: "Bob" }, { onConflict: "email" });
101
+
102
+ // update / delete -> the affected rows
103
+ const { data: updated } = await lux
89
104
  .table("messages")
90
105
  .update({ body: "edited" })
91
106
  .eq("id", inserted?.id);
92
107
 
93
- const { data: deleted, error: deleteError } = await lux
108
+ const { data: deleted } = await lux
94
109
  .table("messages")
95
110
  .delete()
96
111
  .eq("id", inserted?.id);
97
112
  ```
98
113
 
114
+ ### Filters and JSON
115
+
116
+ Beyond `.eq/.neq/.gt/.gte/.lt/.lte`, the query builder supports `IN` lists, JSON
117
+ dot-paths, and arrays:
118
+
119
+ ```ts
120
+ await lux.table("users").select().in("id", [1, 2, 3]);
121
+ await lux.table("users").select().notIn("status", ["banned", "deleted"]);
122
+
123
+ // JSON columns round-trip as native objects (no manual JSON.stringify)
124
+ await lux.table("events").insert({ metadata: { plan: { tier: "pro" }, count: 0 } });
125
+
126
+ // Query JSON by dot-path, like a JS object. A path that does not resolve is a
127
+ // non-match, never an error.
128
+ await lux.table("events").select().eq("metadata.plan.tier", "pro");
129
+
130
+ // IS VALID is existence, not truthiness: 0 / false / "" all count as valid.
131
+ await lux.table("events").select().isValid("metadata.count");
132
+ await lux.table("events").select().isNotValid("metadata.deleted_at");
133
+
134
+ // IS NULL / IS NOT NULL on a regular column (NULL == the column is absent)
135
+ await lux.table("tasks").select().isNull("deleted_at");
136
+ await lux.table("tasks").select().isNotNull("archived_at");
137
+
138
+ // Array membership, and a declared JSON-path index for range queries at scale.
139
+ await lux.table("events").select().contains("tags", "urgent");
140
+ await lux.table("events").createIndex("metadata.plan.tier", "str");
141
+ ```
142
+
99
143
  ## Live tables
100
144
 
101
145
  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
146
 
147
+ `.live()` resolves once the server confirms the subscription, returning the same
148
+ `{ data, error }` shape as the rest of the SDK (here named `{ live, error }`). If
149
+ the query isn't permitted by a read grant, `error` is populated and `live` is
150
+ `null`. The subscription is async-iterable: the buffered snapshot arrives first,
151
+ then live changes.
152
+
103
153
  ```ts
104
- const sub = lux
154
+ const { live, error } = await lux
105
155
  .table<{ id: string; channel_id: string; body: string }>("messages")
106
156
  .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();
157
+ .live();
158
+
159
+ if (error) throw error;
160
+
161
+ for await (const event of live) {
162
+ if (event.type === "snapshot") console.log(event.rows);
163
+ else console.log(event.type, event.new ?? event.old);
164
+ }
165
+ ```
166
+
167
+ You can also attach callbacks instead of iterating:
168
+
169
+ ```ts
170
+ const { live, error } = await lux.table("messages").eq("channel_id", "general").live();
171
+ if (error) throw error;
172
+
173
+ live
174
+ .on("insert", (event) => console.log(event.new))
175
+ .on("update", (event) => console.log(event.old, event.new))
176
+ .on("delete", (event) => console.log(event.old));
177
+
178
+ await live.unsubscribe();
122
179
  ```
123
180
 
124
181
  ## OAuth
@@ -203,3 +260,4 @@ const value = await lux.get("hello");
203
260
  - Browser live subscriptions use the project publishable key plus the signed-in user's JWT.
204
261
  - Table `select()` accepts Lux's constrained projection grammar, not arbitrary SQL.
205
262
  - Direct `lux://` or `rediss://` database access uses the database password and is for trusted infrastructure.
263
+ - 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");
@@ -218,6 +218,12 @@ class LuxProjectTable {
218
218
  insert(rowOrRows) {
219
219
  return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows);
220
220
  }
221
+ upsert(rowOrRows, options) {
222
+ return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows, {
223
+ upsert: true,
224
+ onConflict: options?.onConflict,
225
+ });
226
+ }
221
227
  update(patch) {
222
228
  return new LuxProjectMutationBuilder(this.client, this.name, 'PATCH', patch);
223
229
  }
@@ -272,8 +278,32 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
272
278
  return this.addFilter(column, 'lte', value);
273
279
  }
274
280
  is(column, value) {
281
+ // `.is(col, null)` is the Supabase-style spelling of an IS NULL check.
282
+ if (value === null)
283
+ return this.addFilter(column, 'isNull', '');
275
284
  return this.addFilter(column, 'is', value);
276
285
  }
286
+ isNull(column) {
287
+ return this.addFilter(column, 'isNull', '');
288
+ }
289
+ isNotNull(column) {
290
+ return this.addFilter(column, 'isNotNull', '');
291
+ }
292
+ in(column, values) {
293
+ return this.addFilter(column, 'in', values);
294
+ }
295
+ notIn(column, values) {
296
+ return this.addFilter(column, 'notIn', values);
297
+ }
298
+ isValid(column) {
299
+ return this.addFilter(column, 'isValid', '');
300
+ }
301
+ isNotValid(column) {
302
+ return this.addFilter(column, 'isNotValid', '');
303
+ }
304
+ contains(column, value) {
305
+ return this.addFilter(column, 'contains', value);
306
+ }
277
307
  join(table, alias, onLeft, onRight) {
278
308
  this.joins.push({ type: 'inner', table, alias, onLeft, onRight });
279
309
  return this;
@@ -377,8 +407,14 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
377
407
  }
378
408
  return (0, utils_1.ok)(rows[0]);
379
409
  }
380
- live() {
381
- return new LuxProjectLiveSubscription(this.client, this.tableName, this.columns, this.filters, this.nearQuery, this.orderBy, this.limitCount, this.offsetCount);
410
+ async live() {
411
+ const live = new LuxProjectLiveSubscription(this.client, this.tableName, this.columns, this.filters, this.nearQuery, this.orderBy, this.limitCount, this.offsetCount);
412
+ const error = await live.start();
413
+ if (error) {
414
+ await live.unsubscribe();
415
+ return { live: null, error };
416
+ }
417
+ return { live, error: null };
382
418
  }
383
419
  }
384
420
  exports.LuxProjectSelectBuilder = LuxProjectSelectBuilder;
@@ -401,7 +437,11 @@ class LuxProjectLiveSubscription {
401
437
  change: [],
402
438
  };
403
439
  this.unsubscribeFn = null;
404
- void this.start();
440
+ // Async-iterator plumbing: events buffer in `queue` until a `for await`
441
+ // consumer pulls them; pending `next()` calls park in `waiters`.
442
+ this.queue = [];
443
+ this.waiters = [];
444
+ this.closed = false;
405
445
  }
406
446
  on(type, handler) {
407
447
  this.handlers[type].push(handler);
@@ -410,15 +450,76 @@ class LuxProjectLiveSubscription {
410
450
  async unsubscribe() {
411
451
  this.unsubscribeFn?.();
412
452
  this.unsubscribeFn = null;
413
- }
453
+ this.close();
454
+ }
455
+ /**
456
+ * Open the subscription and wait for the server to confirm it. Resolves
457
+ * `null` once the initial snapshot arrives, or a `LuxError` if the
458
+ * subscription is rejected (e.g. a grant `FORBIDDEN`) or the socket fails.
459
+ * Subsequent errors after a successful start surface via `on('error')` and
460
+ * end the async iterator.
461
+ */
414
462
  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
- }));
463
+ let settled = false;
464
+ let settle;
465
+ const ready = new Promise((resolve) => {
466
+ settle = (error) => {
467
+ if (settled)
468
+ return;
469
+ settled = true;
470
+ resolve(error);
471
+ };
472
+ });
473
+ // Safety net: a server that never answers shouldn't hang the caller.
474
+ const timeout = setTimeout(() => {
475
+ settle({ code: 'LIVE_TIMEOUT', message: 'Timed out establishing live subscription' });
476
+ }, 15000);
477
+ this.unsubscribeFn = await this.client._subscribeLive(this.spec(), (event) => {
478
+ const kind = event?.kind;
479
+ this.handleEvent(event);
480
+ if (kind === 'snapshot')
481
+ settle(null);
482
+ }, (error) => {
483
+ const luxError = {
484
+ code: error.code ?? 'LIVE_ERROR',
485
+ message: error.message ?? 'Live subscription failed',
486
+ };
487
+ if (settled) {
488
+ // Post-start failure: notify handlers and end the stream.
489
+ this.emit({ type: 'error', table: this.table, new: null, old: null, error });
490
+ this.close();
491
+ }
492
+ else {
493
+ settle(luxError);
494
+ }
495
+ });
496
+ const result = await ready;
497
+ clearTimeout(timeout);
498
+ return result;
499
+ }
500
+ [Symbol.asyncIterator]() {
501
+ return {
502
+ next: () => {
503
+ const buffered = this.queue.shift();
504
+ if (buffered)
505
+ return Promise.resolve({ value: buffered, done: false });
506
+ if (this.closed)
507
+ return Promise.resolve({ value: undefined, done: true });
508
+ return new Promise((resolve) => this.waiters.push(resolve));
509
+ },
510
+ return: () => {
511
+ void this.unsubscribe();
512
+ return Promise.resolve({ value: undefined, done: true });
513
+ },
514
+ };
515
+ }
516
+ close() {
517
+ if (this.closed)
518
+ return;
519
+ this.closed = true;
520
+ for (const waiter of this.waiters.splice(0)) {
521
+ waiter({ value: undefined, done: true });
522
+ }
422
523
  }
423
524
  spec() {
424
525
  const spec = {
@@ -487,28 +588,46 @@ class LuxProjectLiveSubscription {
487
588
  for (const handler of this.handlers.change)
488
589
  handler(event);
489
590
  }
591
+ // Feed `for await` consumers the data events (errors end the stream via close()).
592
+ if (event.type !== 'error')
593
+ this.pushIterator(event);
594
+ }
595
+ pushIterator(event) {
596
+ if (this.closed)
597
+ return;
598
+ const waiter = this.waiters.shift();
599
+ if (waiter)
600
+ waiter({ value: event, done: false });
601
+ else
602
+ this.queue.push(event);
490
603
  }
491
604
  }
492
605
  exports.LuxProjectLiveSubscription = LuxProjectLiveSubscription;
493
606
  class LuxProjectInsertBuilder extends LuxProjectThenable {
494
- constructor(client, tableName, rowOrRows) {
607
+ constructor(client, tableName, rowOrRows, upsertOptions) {
495
608
  super();
496
609
  this.client = client;
497
610
  this.tableName = tableName;
498
611
  this.rowOrRows = rowOrRows;
612
+ this.upsertOptions = upsertOptions;
499
613
  }
500
614
  async execute() {
501
- if (!Array.isArray(this.rowOrRows)) {
502
- return this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, this.rowOrRows);
615
+ // One request for both shapes: an array body inserts all rows server-side
616
+ // in a single round-trip. The server returns the affected row(s)
617
+ // ({result: row} for a single row, {result: [rows]} for an array).
618
+ let path = `/tables/${encodeURIComponent(this.tableName)}`;
619
+ if (this.upsertOptions?.upsert) {
620
+ const params = new URLSearchParams();
621
+ if (this.upsertOptions.onConflict)
622
+ params.set('on_conflict', this.upsertOptions.onConflict);
623
+ else
624
+ params.set('upsert', 'true');
625
+ path += `?${params.toString()}`;
503
626
  }
504
- const results = [];
505
- for (const row of this.rowOrRows) {
506
- const result = await this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, row);
507
- if (result.error)
508
- return result;
509
- results.push(result.data);
510
- }
511
- return (0, utils_1.ok)(results);
627
+ const res = await this.client.request('POST', path, this.rowOrRows);
628
+ if (res.error)
629
+ return res;
630
+ return (0, utils_1.ok)(unwrapResult(res.data));
512
631
  }
513
632
  }
514
633
  exports.LuxProjectInsertBuilder = LuxProjectInsertBuilder;
@@ -524,7 +643,11 @@ class LuxProjectMutationBuilder extends LuxProjectFilterBuilder {
524
643
  }
525
644
  const params = this.filteredQueryParams();
526
645
  const query = params.toString();
527
- return this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
646
+ // Update/delete return the affected rows ({result: [rows]}); unwrap them.
647
+ const res = await this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
648
+ if (res.error)
649
+ return res;
650
+ return (0, utils_1.ok)(unwrapResult(res.data));
528
651
  }
529
652
  }
530
653
  exports.LuxProjectMutationBuilder = LuxProjectMutationBuilder;
@@ -548,6 +671,16 @@ function normalizeWhere(where) {
548
671
  function filtersToWhere(filters) {
549
672
  return filters.map((filter) => {
550
673
  const op = filterOperatorToWhere(filter.operator);
674
+ if (filter.operator === 'in' || filter.operator === 'notIn') {
675
+ const values = Array.isArray(filter.value) ? filter.value : [filter.value];
676
+ return normalizeWhere(`${filter.column} ${op} ( ${values.map(formatWhereValue).join(' ')} )`);
677
+ }
678
+ if (filter.operator === 'isValid' ||
679
+ filter.operator === 'isNotValid' ||
680
+ filter.operator === 'isNull' ||
681
+ filter.operator === 'isNotNull') {
682
+ return normalizeWhere(`${filter.column} ${op}`);
683
+ }
551
684
  return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
552
685
  }).join(' AND ');
553
686
  }
@@ -572,6 +705,20 @@ function filterOperatorToWhere(operator) {
572
705
  return '<';
573
706
  case 'lte':
574
707
  return '<=';
708
+ case 'in':
709
+ return 'IN';
710
+ case 'notIn':
711
+ return 'NOT IN';
712
+ case 'isValid':
713
+ return 'IS VALID';
714
+ case 'isNotValid':
715
+ return 'IS NOT VALID';
716
+ case 'isNull':
717
+ return 'IS NULL';
718
+ case 'isNotNull':
719
+ return 'IS NOT NULL';
720
+ case 'contains':
721
+ return 'CONTAINS';
575
722
  }
576
723
  }
577
724
  function formatWhereValue(value) {
package/dist/cjs/table.js CHANGED
@@ -2,6 +2,41 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TableQueryBuilder = exports.TableSubscription = void 0;
4
4
  const utils_1 = require("./utils");
5
+ /** Serialize a field value: JSON objects/arrays round-trip as JSON text. */
6
+ function serializeFieldValue(v) {
7
+ if (v !== null && typeof v === 'object') {
8
+ return JSON.stringify(v);
9
+ }
10
+ return String(v);
11
+ }
12
+ /** Serialize one WHERE condition into RESP tokens. */
13
+ function serializeCondition(cond) {
14
+ if (cond.op === 'IN' || cond.op === 'NOT IN') {
15
+ const values = Array.isArray(cond.value) ? cond.value : [cond.value];
16
+ return [
17
+ cond.field,
18
+ ...(cond.op === 'NOT IN' ? ['NOT', 'IN'] : ['IN']),
19
+ '(',
20
+ ...values.map(String),
21
+ ')',
22
+ ];
23
+ }
24
+ if (cond.op === 'IS VALID')
25
+ return [cond.field, 'IS', 'VALID'];
26
+ if (cond.op === 'IS NOT VALID')
27
+ return [cond.field, 'IS', 'NOT', 'VALID'];
28
+ return [cond.field, cond.op, String(cond.value)];
29
+ }
30
+ /** Join serialized conditions with AND separators. */
31
+ function serializeConditions(conditions) {
32
+ const out = [];
33
+ for (let i = 0; i < conditions.length; i++) {
34
+ out.push(...serializeCondition(conditions[i]));
35
+ if (i < conditions.length - 1)
36
+ out.push('AND');
37
+ }
38
+ return out;
39
+ }
5
40
  class TableSubscription {
6
41
  constructor(client, table, selectArgsBuilder, initError = null) {
7
42
  this.handlers = {
@@ -165,14 +200,7 @@ class TableQueryBuilder {
165
200
  args.push(...(this.joinClause.type === 'LEFT' ? ['LEFT', 'JOIN'] : ['JOIN']), this.joinClause.table, this.joinClause.alias, 'ON', this.joinClause.onLeft, '=', this.joinClause.onRight);
166
201
  }
167
202
  if (allConditions.length) {
168
- args.push('WHERE');
169
- for (let i = 0; i < allConditions.length; i++) {
170
- const cond = allConditions[i];
171
- args.push(cond.field, cond.op, String(cond.value));
172
- if (i < allConditions.length - 1) {
173
- args.push('AND');
174
- }
175
- }
203
+ args.push('WHERE', ...serializeConditions(allConditions));
176
204
  }
177
205
  if (this.groupFields.length) {
178
206
  args.push('GROUP', 'BY', ...this.groupFields);
@@ -237,6 +265,29 @@ class TableQueryBuilder {
237
265
  lte(field, value) {
238
266
  return this.where(field, '<=', value);
239
267
  }
268
+ in(field, values) {
269
+ this.conditions.push({ field, op: 'IN', value: values });
270
+ return this;
271
+ }
272
+ notIn(field, values) {
273
+ this.conditions.push({ field, op: 'NOT IN', value: values });
274
+ return this;
275
+ }
276
+ /** Match rows where a JSON dot-path resolves to a present, non-null value. */
277
+ isValid(field) {
278
+ this.conditions.push({ field, op: 'IS VALID', value: '' });
279
+ return this;
280
+ }
281
+ /** Match rows where a JSON dot-path is absent or resolves to null. */
282
+ isNotValid(field) {
283
+ this.conditions.push({ field, op: 'IS NOT VALID', value: '' });
284
+ return this;
285
+ }
286
+ /** Match rows where an ARRAY column (or array-valued path) contains a value. */
287
+ contains(field, value) {
288
+ this.conditions.push({ field, op: 'CONTAINS', value });
289
+ return this;
290
+ }
240
291
  orderBy(field, dir = 'asc') {
241
292
  this.orderField = field;
242
293
  this.orderDir = dir.toUpperCase();
@@ -312,7 +363,7 @@ class TableQueryBuilder {
312
363
  }
313
364
  const args = [this.name];
314
365
  for (const [k, v] of Object.entries(data)) {
315
- args.push(k, String(v));
366
+ args.push(k, serializeFieldValue(v));
316
367
  }
317
368
  const result = await this.client.call('TINSERT', ...args);
318
369
  return (0, utils_1.ok)(parseInt(result, 10) || 0);
@@ -321,6 +372,26 @@ class TableQueryBuilder {
321
372
  return (0, utils_1.err)('TINSERT_ERROR', `Failed to insert into '${this.name}'`, (0, utils_1.toLuxError)(error));
322
373
  }
323
374
  }
375
+ /** Declare a typed index on a JSON dot-path, e.g. ('meta.reactions.count', 'int'). */
376
+ async createIndex(path, type) {
377
+ try {
378
+ await this.client.call('TINDEX', this.name, path, type.toUpperCase());
379
+ return (0, utils_1.ok)(true);
380
+ }
381
+ catch (error) {
382
+ return (0, utils_1.err)('TINDEX_ERROR', `Failed to index '${path}'`, (0, utils_1.toLuxError)(error));
383
+ }
384
+ }
385
+ /** Drop a previously declared JSON path index. */
386
+ async dropIndex(path) {
387
+ try {
388
+ await this.client.call('TDROPINDEX', this.name, path);
389
+ return (0, utils_1.ok)(true);
390
+ }
391
+ catch (error) {
392
+ return (0, utils_1.err)('TDROPINDEX_ERROR', `Failed to drop index '${path}'`, (0, utils_1.toLuxError)(error));
393
+ }
394
+ }
324
395
  async update(idOrData, data) {
325
396
  try {
326
397
  const hasExplicitId = data !== undefined;
@@ -330,20 +401,14 @@ class TableQueryBuilder {
330
401
  }
331
402
  const args = [this.name, 'SET'];
332
403
  for (const [k, v] of Object.entries(patch)) {
333
- args.push(k, String(v));
404
+ args.push(k, serializeFieldValue(v));
334
405
  }
335
406
  args.push('WHERE');
336
407
  if (hasExplicitId) {
337
408
  args.push('id', '=', String(idOrData));
338
409
  }
339
410
  else {
340
- for (let i = 0; i < this.conditions.length; i++) {
341
- const cond = this.conditions[i];
342
- args.push(cond.field, cond.op, String(cond.value));
343
- if (i < this.conditions.length - 1) {
344
- args.push('AND');
345
- }
346
- }
411
+ args.push(...serializeConditions(this.conditions));
347
412
  }
348
413
  const result = await this.client.call('TUPDATE', ...args);
349
414
  return (0, utils_1.ok)(Number(result) || 0);
@@ -359,13 +424,7 @@ class TableQueryBuilder {
359
424
  return (0, utils_1.err)('MISSING_WHERE', 'delete requires at least one filter');
360
425
  }
361
426
  const args = ['FROM', this.name, 'WHERE'];
362
- for (let i = 0; i < this.conditions.length; i++) {
363
- const cond = this.conditions[i];
364
- args.push(cond.field, cond.op, String(cond.value));
365
- if (i < this.conditions.length - 1) {
366
- args.push('AND');
367
- }
368
- }
427
+ args.push(...serializeConditions(this.conditions));
369
428
  const result = await this.client.call('TDELETE', ...args);
370
429
  return (0, utils_1.ok)(Number(result) || 0);
371
430
  }
package/dist/esm/index.js CHANGED
@@ -5,6 +5,7 @@ import { TimeSeriesNamespace, VectorNamespace } from './namespaces.js';
5
5
  import { LuxRealtimeManager } from './realtime.js';
6
6
  import { TableQueryBuilder } from './table.js';
7
7
  export { createProjectClient, LuxProjectClient, };
8
+ export { LuxProjectLiveSubscription } from './project.js';
8
9
  export { createBrowserClient } from './browser.js';
9
10
  export { createServerClient } from './ssr.js';
10
11
  export { TableQueryBuilder, TableSubscription } from './table.js';
@@ -212,6 +212,12 @@ export class LuxProjectTable {
212
212
  insert(rowOrRows) {
213
213
  return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows);
214
214
  }
215
+ upsert(rowOrRows, options) {
216
+ return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows, {
217
+ upsert: true,
218
+ onConflict: options?.onConflict,
219
+ });
220
+ }
215
221
  update(patch) {
216
222
  return new LuxProjectMutationBuilder(this.client, this.name, 'PATCH', patch);
217
223
  }
@@ -265,8 +271,32 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
265
271
  return this.addFilter(column, 'lte', value);
266
272
  }
267
273
  is(column, value) {
274
+ // `.is(col, null)` is the Supabase-style spelling of an IS NULL check.
275
+ if (value === null)
276
+ return this.addFilter(column, 'isNull', '');
268
277
  return this.addFilter(column, 'is', value);
269
278
  }
279
+ isNull(column) {
280
+ return this.addFilter(column, 'isNull', '');
281
+ }
282
+ isNotNull(column) {
283
+ return this.addFilter(column, 'isNotNull', '');
284
+ }
285
+ in(column, values) {
286
+ return this.addFilter(column, 'in', values);
287
+ }
288
+ notIn(column, values) {
289
+ return this.addFilter(column, 'notIn', values);
290
+ }
291
+ isValid(column) {
292
+ return this.addFilter(column, 'isValid', '');
293
+ }
294
+ isNotValid(column) {
295
+ return this.addFilter(column, 'isNotValid', '');
296
+ }
297
+ contains(column, value) {
298
+ return this.addFilter(column, 'contains', value);
299
+ }
270
300
  join(table, alias, onLeft, onRight) {
271
301
  this.joins.push({ type: 'inner', table, alias, onLeft, onRight });
272
302
  return this;
@@ -370,8 +400,14 @@ export class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
370
400
  }
371
401
  return ok(rows[0]);
372
402
  }
373
- live() {
374
- return new LuxProjectLiveSubscription(this.client, this.tableName, this.columns, this.filters, this.nearQuery, this.orderBy, this.limitCount, this.offsetCount);
403
+ async live() {
404
+ const live = new LuxProjectLiveSubscription(this.client, this.tableName, this.columns, this.filters, this.nearQuery, this.orderBy, this.limitCount, this.offsetCount);
405
+ const error = await live.start();
406
+ if (error) {
407
+ await live.unsubscribe();
408
+ return { live: null, error };
409
+ }
410
+ return { live, error: null };
375
411
  }
376
412
  }
377
413
  export class LuxProjectLiveSubscription {
@@ -393,7 +429,11 @@ export class LuxProjectLiveSubscription {
393
429
  change: [],
394
430
  };
395
431
  this.unsubscribeFn = null;
396
- void this.start();
432
+ // Async-iterator plumbing: events buffer in `queue` until a `for await`
433
+ // consumer pulls them; pending `next()` calls park in `waiters`.
434
+ this.queue = [];
435
+ this.waiters = [];
436
+ this.closed = false;
397
437
  }
398
438
  on(type, handler) {
399
439
  this.handlers[type].push(handler);
@@ -402,15 +442,76 @@ export class LuxProjectLiveSubscription {
402
442
  async unsubscribe() {
403
443
  this.unsubscribeFn?.();
404
444
  this.unsubscribeFn = null;
405
- }
445
+ this.close();
446
+ }
447
+ /**
448
+ * Open the subscription and wait for the server to confirm it. Resolves
449
+ * `null` once the initial snapshot arrives, or a `LuxError` if the
450
+ * subscription is rejected (e.g. a grant `FORBIDDEN`) or the socket fails.
451
+ * Subsequent errors after a successful start surface via `on('error')` and
452
+ * end the async iterator.
453
+ */
406
454
  async start() {
407
- this.unsubscribeFn = await this.client._subscribeLive(this.spec(), (event) => this.handleEvent(event), (error) => this.emit({
408
- type: 'error',
409
- table: this.table,
410
- new: null,
411
- old: null,
412
- error,
413
- }));
455
+ let settled = false;
456
+ let settle;
457
+ const ready = new Promise((resolve) => {
458
+ settle = (error) => {
459
+ if (settled)
460
+ return;
461
+ settled = true;
462
+ resolve(error);
463
+ };
464
+ });
465
+ // Safety net: a server that never answers shouldn't hang the caller.
466
+ const timeout = setTimeout(() => {
467
+ settle({ code: 'LIVE_TIMEOUT', message: 'Timed out establishing live subscription' });
468
+ }, 15000);
469
+ this.unsubscribeFn = await this.client._subscribeLive(this.spec(), (event) => {
470
+ const kind = event?.kind;
471
+ this.handleEvent(event);
472
+ if (kind === 'snapshot')
473
+ settle(null);
474
+ }, (error) => {
475
+ const luxError = {
476
+ code: error.code ?? 'LIVE_ERROR',
477
+ message: error.message ?? 'Live subscription failed',
478
+ };
479
+ if (settled) {
480
+ // Post-start failure: notify handlers and end the stream.
481
+ this.emit({ type: 'error', table: this.table, new: null, old: null, error });
482
+ this.close();
483
+ }
484
+ else {
485
+ settle(luxError);
486
+ }
487
+ });
488
+ const result = await ready;
489
+ clearTimeout(timeout);
490
+ return result;
491
+ }
492
+ [Symbol.asyncIterator]() {
493
+ return {
494
+ next: () => {
495
+ const buffered = this.queue.shift();
496
+ if (buffered)
497
+ return Promise.resolve({ value: buffered, done: false });
498
+ if (this.closed)
499
+ return Promise.resolve({ value: undefined, done: true });
500
+ return new Promise((resolve) => this.waiters.push(resolve));
501
+ },
502
+ return: () => {
503
+ void this.unsubscribe();
504
+ return Promise.resolve({ value: undefined, done: true });
505
+ },
506
+ };
507
+ }
508
+ close() {
509
+ if (this.closed)
510
+ return;
511
+ this.closed = true;
512
+ for (const waiter of this.waiters.splice(0)) {
513
+ waiter({ value: undefined, done: true });
514
+ }
414
515
  }
415
516
  spec() {
416
517
  const spec = {
@@ -479,27 +580,45 @@ export class LuxProjectLiveSubscription {
479
580
  for (const handler of this.handlers.change)
480
581
  handler(event);
481
582
  }
583
+ // Feed `for await` consumers the data events (errors end the stream via close()).
584
+ if (event.type !== 'error')
585
+ this.pushIterator(event);
586
+ }
587
+ pushIterator(event) {
588
+ if (this.closed)
589
+ return;
590
+ const waiter = this.waiters.shift();
591
+ if (waiter)
592
+ waiter({ value: event, done: false });
593
+ else
594
+ this.queue.push(event);
482
595
  }
483
596
  }
484
597
  export class LuxProjectInsertBuilder extends LuxProjectThenable {
485
- constructor(client, tableName, rowOrRows) {
598
+ constructor(client, tableName, rowOrRows, upsertOptions) {
486
599
  super();
487
600
  this.client = client;
488
601
  this.tableName = tableName;
489
602
  this.rowOrRows = rowOrRows;
603
+ this.upsertOptions = upsertOptions;
490
604
  }
491
605
  async execute() {
492
- if (!Array.isArray(this.rowOrRows)) {
493
- return this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, this.rowOrRows);
606
+ // One request for both shapes: an array body inserts all rows server-side
607
+ // in a single round-trip. The server returns the affected row(s)
608
+ // ({result: row} for a single row, {result: [rows]} for an array).
609
+ let path = `/tables/${encodeURIComponent(this.tableName)}`;
610
+ if (this.upsertOptions?.upsert) {
611
+ const params = new URLSearchParams();
612
+ if (this.upsertOptions.onConflict)
613
+ params.set('on_conflict', this.upsertOptions.onConflict);
614
+ else
615
+ params.set('upsert', 'true');
616
+ path += `?${params.toString()}`;
494
617
  }
495
- const results = [];
496
- for (const row of this.rowOrRows) {
497
- const result = await this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, row);
498
- if (result.error)
499
- return result;
500
- results.push(result.data);
501
- }
502
- return ok(results);
618
+ const res = await this.client.request('POST', path, this.rowOrRows);
619
+ if (res.error)
620
+ return res;
621
+ return ok(unwrapResult(res.data));
503
622
  }
504
623
  }
505
624
  export class LuxProjectMutationBuilder extends LuxProjectFilterBuilder {
@@ -514,7 +633,11 @@ export class LuxProjectMutationBuilder extends LuxProjectFilterBuilder {
514
633
  }
515
634
  const params = this.filteredQueryParams();
516
635
  const query = params.toString();
517
- return this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
636
+ // Update/delete return the affected rows ({result: [rows]}); unwrap them.
637
+ const res = await this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
638
+ if (res.error)
639
+ return res;
640
+ return ok(unwrapResult(res.data));
518
641
  }
519
642
  }
520
643
  function unwrapRows(payload) {
@@ -537,6 +660,16 @@ function normalizeWhere(where) {
537
660
  function filtersToWhere(filters) {
538
661
  return filters.map((filter) => {
539
662
  const op = filterOperatorToWhere(filter.operator);
663
+ if (filter.operator === 'in' || filter.operator === 'notIn') {
664
+ const values = Array.isArray(filter.value) ? filter.value : [filter.value];
665
+ return normalizeWhere(`${filter.column} ${op} ( ${values.map(formatWhereValue).join(' ')} )`);
666
+ }
667
+ if (filter.operator === 'isValid' ||
668
+ filter.operator === 'isNotValid' ||
669
+ filter.operator === 'isNull' ||
670
+ filter.operator === 'isNotNull') {
671
+ return normalizeWhere(`${filter.column} ${op}`);
672
+ }
540
673
  return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
541
674
  }).join(' AND ');
542
675
  }
@@ -561,6 +694,20 @@ function filterOperatorToWhere(operator) {
561
694
  return '<';
562
695
  case 'lte':
563
696
  return '<=';
697
+ case 'in':
698
+ return 'IN';
699
+ case 'notIn':
700
+ return 'NOT IN';
701
+ case 'isValid':
702
+ return 'IS VALID';
703
+ case 'isNotValid':
704
+ return 'IS NOT VALID';
705
+ case 'isNull':
706
+ return 'IS NULL';
707
+ case 'isNotNull':
708
+ return 'IS NOT NULL';
709
+ case 'contains':
710
+ return 'CONTAINS';
564
711
  }
565
712
  }
566
713
  function formatWhereValue(value) {
package/dist/esm/table.js CHANGED
@@ -1,4 +1,39 @@
1
1
  import { err, ok, toLuxError } from './utils.js';
2
+ /** Serialize a field value: JSON objects/arrays round-trip as JSON text. */
3
+ function serializeFieldValue(v) {
4
+ if (v !== null && typeof v === 'object') {
5
+ return JSON.stringify(v);
6
+ }
7
+ return String(v);
8
+ }
9
+ /** Serialize one WHERE condition into RESP tokens. */
10
+ function serializeCondition(cond) {
11
+ if (cond.op === 'IN' || cond.op === 'NOT IN') {
12
+ const values = Array.isArray(cond.value) ? cond.value : [cond.value];
13
+ return [
14
+ cond.field,
15
+ ...(cond.op === 'NOT IN' ? ['NOT', 'IN'] : ['IN']),
16
+ '(',
17
+ ...values.map(String),
18
+ ')',
19
+ ];
20
+ }
21
+ if (cond.op === 'IS VALID')
22
+ return [cond.field, 'IS', 'VALID'];
23
+ if (cond.op === 'IS NOT VALID')
24
+ return [cond.field, 'IS', 'NOT', 'VALID'];
25
+ return [cond.field, cond.op, String(cond.value)];
26
+ }
27
+ /** Join serialized conditions with AND separators. */
28
+ function serializeConditions(conditions) {
29
+ const out = [];
30
+ for (let i = 0; i < conditions.length; i++) {
31
+ out.push(...serializeCondition(conditions[i]));
32
+ if (i < conditions.length - 1)
33
+ out.push('AND');
34
+ }
35
+ return out;
36
+ }
2
37
  export class TableSubscription {
3
38
  constructor(client, table, selectArgsBuilder, initError = null) {
4
39
  this.handlers = {
@@ -161,14 +196,7 @@ export class TableQueryBuilder {
161
196
  args.push(...(this.joinClause.type === 'LEFT' ? ['LEFT', 'JOIN'] : ['JOIN']), this.joinClause.table, this.joinClause.alias, 'ON', this.joinClause.onLeft, '=', this.joinClause.onRight);
162
197
  }
163
198
  if (allConditions.length) {
164
- args.push('WHERE');
165
- for (let i = 0; i < allConditions.length; i++) {
166
- const cond = allConditions[i];
167
- args.push(cond.field, cond.op, String(cond.value));
168
- if (i < allConditions.length - 1) {
169
- args.push('AND');
170
- }
171
- }
199
+ args.push('WHERE', ...serializeConditions(allConditions));
172
200
  }
173
201
  if (this.groupFields.length) {
174
202
  args.push('GROUP', 'BY', ...this.groupFields);
@@ -233,6 +261,29 @@ export class TableQueryBuilder {
233
261
  lte(field, value) {
234
262
  return this.where(field, '<=', value);
235
263
  }
264
+ in(field, values) {
265
+ this.conditions.push({ field, op: 'IN', value: values });
266
+ return this;
267
+ }
268
+ notIn(field, values) {
269
+ this.conditions.push({ field, op: 'NOT IN', value: values });
270
+ return this;
271
+ }
272
+ /** Match rows where a JSON dot-path resolves to a present, non-null value. */
273
+ isValid(field) {
274
+ this.conditions.push({ field, op: 'IS VALID', value: '' });
275
+ return this;
276
+ }
277
+ /** Match rows where a JSON dot-path is absent or resolves to null. */
278
+ isNotValid(field) {
279
+ this.conditions.push({ field, op: 'IS NOT VALID', value: '' });
280
+ return this;
281
+ }
282
+ /** Match rows where an ARRAY column (or array-valued path) contains a value. */
283
+ contains(field, value) {
284
+ this.conditions.push({ field, op: 'CONTAINS', value });
285
+ return this;
286
+ }
236
287
  orderBy(field, dir = 'asc') {
237
288
  this.orderField = field;
238
289
  this.orderDir = dir.toUpperCase();
@@ -308,7 +359,7 @@ export class TableQueryBuilder {
308
359
  }
309
360
  const args = [this.name];
310
361
  for (const [k, v] of Object.entries(data)) {
311
- args.push(k, String(v));
362
+ args.push(k, serializeFieldValue(v));
312
363
  }
313
364
  const result = await this.client.call('TINSERT', ...args);
314
365
  return ok(parseInt(result, 10) || 0);
@@ -317,6 +368,26 @@ export class TableQueryBuilder {
317
368
  return err('TINSERT_ERROR', `Failed to insert into '${this.name}'`, toLuxError(error));
318
369
  }
319
370
  }
371
+ /** Declare a typed index on a JSON dot-path, e.g. ('meta.reactions.count', 'int'). */
372
+ async createIndex(path, type) {
373
+ try {
374
+ await this.client.call('TINDEX', this.name, path, type.toUpperCase());
375
+ return ok(true);
376
+ }
377
+ catch (error) {
378
+ return err('TINDEX_ERROR', `Failed to index '${path}'`, toLuxError(error));
379
+ }
380
+ }
381
+ /** Drop a previously declared JSON path index. */
382
+ async dropIndex(path) {
383
+ try {
384
+ await this.client.call('TDROPINDEX', this.name, path);
385
+ return ok(true);
386
+ }
387
+ catch (error) {
388
+ return err('TDROPINDEX_ERROR', `Failed to drop index '${path}'`, toLuxError(error));
389
+ }
390
+ }
320
391
  async update(idOrData, data) {
321
392
  try {
322
393
  const hasExplicitId = data !== undefined;
@@ -326,20 +397,14 @@ export class TableQueryBuilder {
326
397
  }
327
398
  const args = [this.name, 'SET'];
328
399
  for (const [k, v] of Object.entries(patch)) {
329
- args.push(k, String(v));
400
+ args.push(k, serializeFieldValue(v));
330
401
  }
331
402
  args.push('WHERE');
332
403
  if (hasExplicitId) {
333
404
  args.push('id', '=', String(idOrData));
334
405
  }
335
406
  else {
336
- for (let i = 0; i < this.conditions.length; i++) {
337
- const cond = this.conditions[i];
338
- args.push(cond.field, cond.op, String(cond.value));
339
- if (i < this.conditions.length - 1) {
340
- args.push('AND');
341
- }
342
- }
407
+ args.push(...serializeConditions(this.conditions));
343
408
  }
344
409
  const result = await this.client.call('TUPDATE', ...args);
345
410
  return ok(Number(result) || 0);
@@ -355,13 +420,7 @@ export class TableQueryBuilder {
355
420
  return err('MISSING_WHERE', 'delete requires at least one filter');
356
421
  }
357
422
  const args = ['FROM', this.name, 'WHERE'];
358
- for (let i = 0; i < this.conditions.length; i++) {
359
- const cond = this.conditions[i];
360
- args.push(cond.field, cond.op, String(cond.value));
361
- if (i < this.conditions.length - 1) {
362
- args.push('AND');
363
- }
364
- }
423
+ args.push(...serializeConditions(this.conditions));
365
424
  const result = await this.client.call('TDELETE', ...args);
366
425
  return ok(Number(result) || 0);
367
426
  }
@@ -6,11 +6,12 @@ import { TableQueryBuilder, type TableQueryBuilderOptions } from './table';
6
6
  import type { KSubEvent, LuxTypedRow, TableRow, TSAddOptions, TSMRangeResult, TSRangeOptions, TSSample, VSearchResult } from './types';
7
7
  export type { LuxAuthKey, LuxAuthGrantRow, LuxAuthIdentityRow, LuxAuthChangeEvent, LuxAuthOptions, LuxAuthKeyRow, LuxAuthProviderRow, LuxAuthSession, LuxAuthSessionRow, LuxAuthSigningKeyRow, LuxAuthStateChangeCallback, LuxAuthStorage, LuxAuthSubscription, LuxAuthTables, LuxAuthUserRow, LuxAuthUser, LuxUser, LuxOAuthProvider, LuxOAuthUrl, LuxSignInWithOAuthOptions, LuxCreateApiKeyOptions, LuxSignInOptions, LuxSignUpOptions, } from './auth';
8
8
  export { createProjectClient, LuxProjectClient, };
9
+ export { LuxProjectLiveSubscription } from './project';
9
10
  export { createBrowserClient } from './browser';
10
11
  export type { LuxBrowserClientOptions } from './browser';
11
12
  export { createServerClient } from './ssr';
12
13
  export type { LuxCookieMethods, LuxCookieOptions, LuxServerClientOptions } from './ssr';
13
- export type { LuxProjectLiveEvent, LuxProjectLiveEventType, LuxProjectOptions, LuxTableColumn, LuxVectorSearchOptions, } from './project';
14
+ export type { LuxLiveResult, LuxProjectLiveEvent, LuxProjectLiveEventType, LuxProjectOptions, LuxTableColumn, LuxVectorSearchOptions, } from './project';
14
15
  export type { KSubEvent, LuxAggregateRow, LuxAggregateValue, LuxError, LuxInferRow, LuxNearRow, LuxResult, LuxSimilarity, LuxTypedRow, TableChangeEvent, TableChangeType, TableErrorEvent, TableRow, TableSchema, TSAddOptions, TSMRangeResult, TSRangeOptions, TSSample, VSearchResult, } from './types';
15
16
  export { TableQueryBuilder, TableSubscription } from './table';
16
17
  export type { TableQueryBuilderOptions } from './table';
@@ -1,5 +1,5 @@
1
1
  import { LuxAuthClient, type LuxAuthOptions } from './auth';
2
- import type { LuxResult, LuxTypedRow } from './types';
2
+ import type { LuxError, LuxResult, LuxTypedRow } from './types';
3
3
  export interface LuxProjectOptions {
4
4
  url: string;
5
5
  key: string;
@@ -23,13 +23,13 @@ export interface LuxVectorSearchOptions {
23
23
  filter_value?: string;
24
24
  }
25
25
  type QueryValue = string | number | boolean | number[] | null;
26
- type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'is';
26
+ type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'is' | 'in' | 'notIn' | 'isValid' | 'isNotValid' | 'isNull' | 'isNotNull' | 'contains';
27
27
  type ProjectRowInput<T extends object> = Partial<T> & Record<string, QueryValue>;
28
28
  type ProjectSelectSingle<TResult> = TResult extends readonly (infer Row)[] ? Row : TResult;
29
29
  interface QueryFilter {
30
30
  column: string;
31
31
  operator: FilterOperator;
32
- value: QueryValue;
32
+ value: QueryValue | QueryValue[];
33
33
  }
34
34
  interface QueryOrder {
35
35
  column: string;
@@ -69,6 +69,15 @@ export interface LuxProjectLiveEvent<T extends object = Record<string, unknown>>
69
69
  };
70
70
  }
71
71
  type LiveEventHandler<T extends object> = (event: LuxProjectLiveEvent<T>) => void;
72
+ /**
73
+ * Result of opening a live subscription, in the same `{ data, error }` spirit as
74
+ * the rest of the SDK: `live` is the established subscription (or `null` if the
75
+ * server rejected it), `error` carries the rejection (e.g. a grant `FORBIDDEN`).
76
+ */
77
+ export interface LuxLiveResult<T extends object> {
78
+ live: LuxProjectLiveSubscription<T> | null;
79
+ error: LuxError | null;
80
+ }
72
81
  export declare class LuxProjectClient {
73
82
  readonly url: string;
74
83
  readonly key: string;
@@ -119,9 +128,15 @@ export declare class LuxProjectTable<T extends object> {
119
128
  threshold?: number;
120
129
  }): LuxProjectSelectBuilder<T, T[]>;
121
130
  is(column: string, value: QueryValue): LuxProjectSelectBuilder<T, T[]>;
122
- live(): LuxProjectLiveSubscription<T>;
131
+ live(): Promise<LuxLiveResult<T>>;
123
132
  insert(row: ProjectRowInput<T>): LuxProjectInsertBuilder<unknown>;
124
133
  insert(rows: Array<ProjectRowInput<T>>): LuxProjectInsertBuilder<unknown[]>;
134
+ upsert(row: ProjectRowInput<T>, options?: {
135
+ onConflict?: string;
136
+ }): LuxProjectInsertBuilder<unknown>;
137
+ upsert(rows: Array<ProjectRowInput<T>>, options?: {
138
+ onConflict?: string;
139
+ }): LuxProjectInsertBuilder<unknown[]>;
125
140
  update(patch: ProjectRowInput<T>): LuxProjectMutationBuilder<unknown>;
126
141
  delete(): LuxProjectMutationBuilder<unknown>;
127
142
  count(): Promise<LuxResult<number>>;
@@ -151,11 +166,18 @@ declare abstract class LuxProjectFilterBuilder<TResult, TSelf> extends LuxProjec
151
166
  lt(column: string, value: QueryValue): TSelf;
152
167
  lte(column: string, value: QueryValue): TSelf;
153
168
  is(column: string, value: QueryValue): TSelf;
169
+ isNull(column: string): TSelf;
170
+ isNotNull(column: string): TSelf;
171
+ in(column: string, values: QueryValue[]): TSelf;
172
+ notIn(column: string, values: QueryValue[]): TSelf;
173
+ isValid(column: string): TSelf;
174
+ isNotValid(column: string): TSelf;
175
+ contains(column: string, value: QueryValue): TSelf;
154
176
  join(table: string, alias: string, onLeft: string, onRight: string): TSelf;
155
177
  leftJoin(table: string, alias: string, onLeft: string, onRight: string): TSelf;
156
178
  group(columns: string | string[]): TSelf;
157
179
  having(column: string, operator: FilterOperator, value: QueryValue): TSelf;
158
- protected addFilter(column: string, operator: FilterOperator, value: QueryValue): TSelf;
180
+ protected addFilter(column: string, operator: FilterOperator, value: QueryValue | QueryValue[]): TSelf;
159
181
  protected filteredQueryParams(): URLSearchParams;
160
182
  }
161
183
  export declare class LuxProjectSelectBuilder<T extends object, TResult> extends LuxProjectFilterBuilder<TResult, LuxProjectSelectBuilder<T, TResult>> {
@@ -173,7 +195,7 @@ export declare class LuxProjectSelectBuilder<T extends object, TResult> extends
173
195
  range(from: number, to: number): this;
174
196
  single(): LuxProjectSelectBuilder<T, ProjectSelectSingle<TResult>>;
175
197
  execute(): Promise<LuxResult<TResult>>;
176
- live(): LuxProjectLiveSubscription<LuxTypedRow<TResult>>;
198
+ live(): Promise<LuxLiveResult<LuxTypedRow<TResult>>>;
177
199
  }
178
200
  export declare class LuxProjectLiveSubscription<T extends object> {
179
201
  private client;
@@ -186,19 +208,36 @@ export declare class LuxProjectLiveSubscription<T extends object> {
186
208
  private offsetCount?;
187
209
  private handlers;
188
210
  private unsubscribeFn;
211
+ private queue;
212
+ private waiters;
213
+ private closed;
189
214
  constructor(client: LuxProjectClient, table: string, columns: string, filters: QueryFilter[], nearQuery?: QueryNear | undefined, orderBy?: QueryOrder | undefined, limitCount?: number | undefined, offsetCount?: number | undefined);
190
215
  on(type: LuxProjectLiveEventType | 'change', handler: LiveEventHandler<T>): this;
191
216
  unsubscribe(): Promise<void>;
192
- private start;
217
+ /**
218
+ * Open the subscription and wait for the server to confirm it. Resolves
219
+ * `null` once the initial snapshot arrives, or a `LuxError` if the
220
+ * subscription is rejected (e.g. a grant `FORBIDDEN`) or the socket fails.
221
+ * Subsequent errors after a successful start surface via `on('error')` and
222
+ * end the async iterator.
223
+ */
224
+ start(): Promise<LuxError | null>;
225
+ [Symbol.asyncIterator](): AsyncIterator<LuxProjectLiveEvent<T>>;
226
+ private close;
193
227
  private spec;
194
228
  private handleEvent;
195
229
  private emit;
230
+ private pushIterator;
196
231
  }
197
232
  export declare class LuxProjectInsertBuilder<TResult> extends LuxProjectThenable<TResult> {
198
233
  private client;
199
234
  private tableName;
200
235
  private rowOrRows;
201
- constructor(client: LuxProjectClient, tableName: string, rowOrRows: Record<string, QueryValue> | Array<Record<string, QueryValue>>);
236
+ private upsertOptions?;
237
+ constructor(client: LuxProjectClient, tableName: string, rowOrRows: Record<string, QueryValue> | Array<Record<string, QueryValue>>, upsertOptions?: {
238
+ upsert: boolean;
239
+ onConflict?: string;
240
+ } | undefined);
202
241
  execute(): Promise<LuxResult<TResult>>;
203
242
  }
204
243
  export declare class LuxProjectMutationBuilder<TResult> extends LuxProjectFilterBuilder<TResult, LuxProjectMutationBuilder<TResult>> {
@@ -1,10 +1,10 @@
1
1
  import type { KSubEvent, LuxError, LuxResult, TableChangeEvent, TableErrorEvent, TableRow, TableSchema } from './types';
2
- type TableWhereOp = '=' | '!=' | '>' | '<' | '>=' | '<=';
2
+ type TableWhereOp = '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'IS VALID' | 'IS NOT VALID' | 'CONTAINS';
3
3
  type TableWhereValue = string | number | boolean;
4
4
  interface TableWhereCondition {
5
5
  field: string;
6
6
  op: TableWhereOp;
7
- value: TableWhereValue;
7
+ value: TableWhereValue | TableWhereValue[];
8
8
  }
9
9
  interface TableClient {
10
10
  call(command: string, ...args: Array<string | number>): Promise<unknown>;
@@ -60,6 +60,14 @@ export declare class TableQueryBuilder<T extends object = TableRow> {
60
60
  gte(field: string, value: TableWhereValue): this;
61
61
  lt(field: string, value: TableWhereValue): this;
62
62
  lte(field: string, value: TableWhereValue): this;
63
+ in(field: string, values: TableWhereValue[]): this;
64
+ notIn(field: string, values: TableWhereValue[]): this;
65
+ /** Match rows where a JSON dot-path resolves to a present, non-null value. */
66
+ isValid(field: string): this;
67
+ /** Match rows where a JSON dot-path is absent or resolves to null. */
68
+ isNotValid(field: string): this;
69
+ /** Match rows where an ARRAY column (or array-valued path) contains a value. */
70
+ contains(field: string, value: TableWhereValue): this;
63
71
  orderBy(field: string, dir?: 'asc' | 'desc'): this;
64
72
  order(field: string, options?: {
65
73
  ascending?: boolean;
@@ -82,6 +90,10 @@ export declare class TableQueryBuilder<T extends object = TableRow> {
82
90
  run(): Promise<LuxResult<T[] | T>>;
83
91
  then<TFulfilled = LuxResult<T[] | T>, TRejected = never>(onfulfilled?: ((value: LuxResult<T[] | T>) => TFulfilled | PromiseLike<TFulfilled>) | null, onrejected?: ((reason: unknown) => TRejected | PromiseLike<TRejected>) | null): Promise<TFulfilled | TRejected>;
84
92
  insert(data: Record<string, unknown>): Promise<LuxResult<number>>;
93
+ /** Declare a typed index on a JSON dot-path, e.g. ('meta.reactions.count', 'int'). */
94
+ createIndex(path: string, type: 'int' | 'float' | 'bool' | 'timestamp' | 'str'): Promise<LuxResult<true>>;
95
+ /** Drop a previously declared JSON path index. */
96
+ dropIndex(path: string): Promise<LuxResult<true>>;
85
97
  update(id: number | string, data: Record<string, unknown>): Promise<LuxResult<number>>;
86
98
  update(data: Record<string, unknown>): Promise<LuxResult<number>>;
87
99
  delete(...ids: Array<number | string>): Promise<LuxResult<number>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxdb/sdk",
3
- "version": "1.4.3",
3
+ "version": "2.1.0",
4
4
  "description": "Official Lux TypeScript SDK for app data, auth, tables, vectors, realtime, and Redis-compatible direct access",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",