@luxdb/sdk 1.4.3 → 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 +55 -16
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/project.js +126 -11
- package/dist/cjs/table.js +83 -24
- package/dist/esm/index.js +1 -0
- package/dist/esm/project.js +126 -11
- package/dist/esm/table.js +83 -24
- package/dist/types/index.d.ts +2 -1
- package/dist/types/project.d.ts +34 -7
- package/dist/types/table.d.ts +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,29 +96,67 @@ const { data: deleted, error: deleteError } = await lux
|
|
|
96
96
|
.eq("id", inserted?.id);
|
|
97
97
|
```
|
|
98
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
|
+
|
|
99
124
|
## Live tables
|
|
100
125
|
|
|
101
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.
|
|
102
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
|
+
|
|
103
134
|
```ts
|
|
104
|
-
const
|
|
135
|
+
const { live, error } = await lux
|
|
105
136
|
.table<{ id: string; channel_id: string; body: string }>("messages")
|
|
106
137
|
.eq("channel_id", "general")
|
|
107
|
-
.live()
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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();
|
|
122
160
|
```
|
|
123
161
|
|
|
124
162
|
## OAuth
|
|
@@ -203,3 +241,4 @@ const value = await lux.get("hello");
|
|
|
203
241
|
- Browser live subscriptions use the project publishable key plus the signed-in user's JWT.
|
|
204
242
|
- Table `select()` accepts Lux's constrained projection grammar, not arbitrary SQL.
|
|
205
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");
|
package/dist/cjs/project.js
CHANGED
|
@@ -274,6 +274,21 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
|
|
|
274
274
|
is(column, value) {
|
|
275
275
|
return this.addFilter(column, 'is', value);
|
|
276
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
|
+
}
|
|
277
292
|
join(table, alias, onLeft, onRight) {
|
|
278
293
|
this.joins.push({ type: 'inner', table, alias, onLeft, onRight });
|
|
279
294
|
return this;
|
|
@@ -377,8 +392,14 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
|
|
|
377
392
|
}
|
|
378
393
|
return (0, utils_1.ok)(rows[0]);
|
|
379
394
|
}
|
|
380
|
-
live() {
|
|
381
|
-
|
|
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 };
|
|
382
403
|
}
|
|
383
404
|
}
|
|
384
405
|
exports.LuxProjectSelectBuilder = LuxProjectSelectBuilder;
|
|
@@ -401,7 +422,11 @@ class LuxProjectLiveSubscription {
|
|
|
401
422
|
change: [],
|
|
402
423
|
};
|
|
403
424
|
this.unsubscribeFn = null;
|
|
404
|
-
|
|
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;
|
|
405
430
|
}
|
|
406
431
|
on(type, handler) {
|
|
407
432
|
this.handlers[type].push(handler);
|
|
@@ -410,15 +435,76 @@ class LuxProjectLiveSubscription {
|
|
|
410
435
|
async unsubscribe() {
|
|
411
436
|
this.unsubscribeFn?.();
|
|
412
437
|
this.unsubscribeFn = null;
|
|
413
|
-
|
|
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
|
+
*/
|
|
414
447
|
async start() {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
}
|
|
422
508
|
}
|
|
423
509
|
spec() {
|
|
424
510
|
const spec = {
|
|
@@ -487,6 +573,18 @@ class LuxProjectLiveSubscription {
|
|
|
487
573
|
for (const handler of this.handlers.change)
|
|
488
574
|
handler(event);
|
|
489
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);
|
|
490
588
|
}
|
|
491
589
|
}
|
|
492
590
|
exports.LuxProjectLiveSubscription = LuxProjectLiveSubscription;
|
|
@@ -548,6 +646,13 @@ function normalizeWhere(where) {
|
|
|
548
646
|
function filtersToWhere(filters) {
|
|
549
647
|
return filters.map((filter) => {
|
|
550
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
|
+
}
|
|
551
656
|
return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
|
|
552
657
|
}).join(' AND ');
|
|
553
658
|
}
|
|
@@ -572,6 +677,16 @@ function filterOperatorToWhere(operator) {
|
|
|
572
677
|
return '<';
|
|
573
678
|
case 'lte':
|
|
574
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';
|
|
575
690
|
}
|
|
576
691
|
}
|
|
577
692
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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';
|
package/dist/esm/project.js
CHANGED
|
@@ -267,6 +267,21 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
|
|
|
267
267
|
is(column, value) {
|
|
268
268
|
return this.addFilter(column, 'is', value);
|
|
269
269
|
}
|
|
270
|
+
in(column, values) {
|
|
271
|
+
return this.addFilter(column, 'in', values);
|
|
272
|
+
}
|
|
273
|
+
notIn(column, values) {
|
|
274
|
+
return this.addFilter(column, 'notIn', values);
|
|
275
|
+
}
|
|
276
|
+
isValid(column) {
|
|
277
|
+
return this.addFilter(column, 'isValid', '');
|
|
278
|
+
}
|
|
279
|
+
isNotValid(column) {
|
|
280
|
+
return this.addFilter(column, 'isNotValid', '');
|
|
281
|
+
}
|
|
282
|
+
contains(column, value) {
|
|
283
|
+
return this.addFilter(column, 'contains', value);
|
|
284
|
+
}
|
|
270
285
|
join(table, alias, onLeft, onRight) {
|
|
271
286
|
this.joins.push({ type: 'inner', table, alias, onLeft, onRight });
|
|
272
287
|
return this;
|
|
@@ -370,8 +385,14 @@ export class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
|
|
|
370
385
|
}
|
|
371
386
|
return ok(rows[0]);
|
|
372
387
|
}
|
|
373
|
-
live() {
|
|
374
|
-
|
|
388
|
+
async live() {
|
|
389
|
+
const live = new LuxProjectLiveSubscription(this.client, this.tableName, this.columns, this.filters, this.nearQuery, this.orderBy, this.limitCount, this.offsetCount);
|
|
390
|
+
const error = await live.start();
|
|
391
|
+
if (error) {
|
|
392
|
+
await live.unsubscribe();
|
|
393
|
+
return { live: null, error };
|
|
394
|
+
}
|
|
395
|
+
return { live, error: null };
|
|
375
396
|
}
|
|
376
397
|
}
|
|
377
398
|
export class LuxProjectLiveSubscription {
|
|
@@ -393,7 +414,11 @@ export class LuxProjectLiveSubscription {
|
|
|
393
414
|
change: [],
|
|
394
415
|
};
|
|
395
416
|
this.unsubscribeFn = null;
|
|
396
|
-
|
|
417
|
+
// Async-iterator plumbing: events buffer in `queue` until a `for await`
|
|
418
|
+
// consumer pulls them; pending `next()` calls park in `waiters`.
|
|
419
|
+
this.queue = [];
|
|
420
|
+
this.waiters = [];
|
|
421
|
+
this.closed = false;
|
|
397
422
|
}
|
|
398
423
|
on(type, handler) {
|
|
399
424
|
this.handlers[type].push(handler);
|
|
@@ -402,15 +427,76 @@ export class LuxProjectLiveSubscription {
|
|
|
402
427
|
async unsubscribe() {
|
|
403
428
|
this.unsubscribeFn?.();
|
|
404
429
|
this.unsubscribeFn = null;
|
|
405
|
-
|
|
430
|
+
this.close();
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Open the subscription and wait for the server to confirm it. Resolves
|
|
434
|
+
* `null` once the initial snapshot arrives, or a `LuxError` if the
|
|
435
|
+
* subscription is rejected (e.g. a grant `FORBIDDEN`) or the socket fails.
|
|
436
|
+
* Subsequent errors after a successful start surface via `on('error')` and
|
|
437
|
+
* end the async iterator.
|
|
438
|
+
*/
|
|
406
439
|
async start() {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
440
|
+
let settled = false;
|
|
441
|
+
let settle;
|
|
442
|
+
const ready = new Promise((resolve) => {
|
|
443
|
+
settle = (error) => {
|
|
444
|
+
if (settled)
|
|
445
|
+
return;
|
|
446
|
+
settled = true;
|
|
447
|
+
resolve(error);
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
// Safety net: a server that never answers shouldn't hang the caller.
|
|
451
|
+
const timeout = setTimeout(() => {
|
|
452
|
+
settle({ code: 'LIVE_TIMEOUT', message: 'Timed out establishing live subscription' });
|
|
453
|
+
}, 15000);
|
|
454
|
+
this.unsubscribeFn = await this.client._subscribeLive(this.spec(), (event) => {
|
|
455
|
+
const kind = event?.kind;
|
|
456
|
+
this.handleEvent(event);
|
|
457
|
+
if (kind === 'snapshot')
|
|
458
|
+
settle(null);
|
|
459
|
+
}, (error) => {
|
|
460
|
+
const luxError = {
|
|
461
|
+
code: error.code ?? 'LIVE_ERROR',
|
|
462
|
+
message: error.message ?? 'Live subscription failed',
|
|
463
|
+
};
|
|
464
|
+
if (settled) {
|
|
465
|
+
// Post-start failure: notify handlers and end the stream.
|
|
466
|
+
this.emit({ type: 'error', table: this.table, new: null, old: null, error });
|
|
467
|
+
this.close();
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
settle(luxError);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
const result = await ready;
|
|
474
|
+
clearTimeout(timeout);
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
[Symbol.asyncIterator]() {
|
|
478
|
+
return {
|
|
479
|
+
next: () => {
|
|
480
|
+
const buffered = this.queue.shift();
|
|
481
|
+
if (buffered)
|
|
482
|
+
return Promise.resolve({ value: buffered, done: false });
|
|
483
|
+
if (this.closed)
|
|
484
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
485
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
486
|
+
},
|
|
487
|
+
return: () => {
|
|
488
|
+
void this.unsubscribe();
|
|
489
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
close() {
|
|
494
|
+
if (this.closed)
|
|
495
|
+
return;
|
|
496
|
+
this.closed = true;
|
|
497
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
498
|
+
waiter({ value: undefined, done: true });
|
|
499
|
+
}
|
|
414
500
|
}
|
|
415
501
|
spec() {
|
|
416
502
|
const spec = {
|
|
@@ -479,6 +565,18 @@ export class LuxProjectLiveSubscription {
|
|
|
479
565
|
for (const handler of this.handlers.change)
|
|
480
566
|
handler(event);
|
|
481
567
|
}
|
|
568
|
+
// Feed `for await` consumers the data events (errors end the stream via close()).
|
|
569
|
+
if (event.type !== 'error')
|
|
570
|
+
this.pushIterator(event);
|
|
571
|
+
}
|
|
572
|
+
pushIterator(event) {
|
|
573
|
+
if (this.closed)
|
|
574
|
+
return;
|
|
575
|
+
const waiter = this.waiters.shift();
|
|
576
|
+
if (waiter)
|
|
577
|
+
waiter({ value: event, done: false });
|
|
578
|
+
else
|
|
579
|
+
this.queue.push(event);
|
|
482
580
|
}
|
|
483
581
|
}
|
|
484
582
|
export class LuxProjectInsertBuilder extends LuxProjectThenable {
|
|
@@ -537,6 +635,13 @@ function normalizeWhere(where) {
|
|
|
537
635
|
function filtersToWhere(filters) {
|
|
538
636
|
return filters.map((filter) => {
|
|
539
637
|
const op = filterOperatorToWhere(filter.operator);
|
|
638
|
+
if (filter.operator === 'in' || filter.operator === 'notIn') {
|
|
639
|
+
const values = Array.isArray(filter.value) ? filter.value : [filter.value];
|
|
640
|
+
return normalizeWhere(`${filter.column} ${op} ( ${values.map(formatWhereValue).join(' ')} )`);
|
|
641
|
+
}
|
|
642
|
+
if (filter.operator === 'isValid' || filter.operator === 'isNotValid') {
|
|
643
|
+
return normalizeWhere(`${filter.column} ${op}`);
|
|
644
|
+
}
|
|
540
645
|
return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
|
|
541
646
|
}).join(' AND ');
|
|
542
647
|
}
|
|
@@ -561,6 +666,16 @@ function filterOperatorToWhere(operator) {
|
|
|
561
666
|
return '<';
|
|
562
667
|
case 'lte':
|
|
563
668
|
return '<=';
|
|
669
|
+
case 'in':
|
|
670
|
+
return 'IN';
|
|
671
|
+
case 'notIn':
|
|
672
|
+
return 'NOT IN';
|
|
673
|
+
case 'isValid':
|
|
674
|
+
return 'IS VALID';
|
|
675
|
+
case 'isNotValid':
|
|
676
|
+
return 'IS NOT VALID';
|
|
677
|
+
case 'contains':
|
|
678
|
+
return 'CONTAINS';
|
|
564
679
|
}
|
|
565
680
|
}
|
|
566
681
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/types/project.d.ts
CHANGED
|
@@ -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' | '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,7 +128,7 @@ 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():
|
|
131
|
+
live(): Promise<LuxLiveResult<T>>;
|
|
123
132
|
insert(row: ProjectRowInput<T>): LuxProjectInsertBuilder<unknown>;
|
|
124
133
|
insert(rows: Array<ProjectRowInput<T>>): LuxProjectInsertBuilder<unknown[]>;
|
|
125
134
|
update(patch: ProjectRowInput<T>): LuxProjectMutationBuilder<unknown>;
|
|
@@ -151,11 +160,16 @@ declare abstract class LuxProjectFilterBuilder<TResult, TSelf> extends LuxProjec
|
|
|
151
160
|
lt(column: string, value: QueryValue): TSelf;
|
|
152
161
|
lte(column: string, value: QueryValue): TSelf;
|
|
153
162
|
is(column: string, value: QueryValue): TSelf;
|
|
163
|
+
in(column: string, values: QueryValue[]): TSelf;
|
|
164
|
+
notIn(column: string, values: QueryValue[]): TSelf;
|
|
165
|
+
isValid(column: string): TSelf;
|
|
166
|
+
isNotValid(column: string): TSelf;
|
|
167
|
+
contains(column: string, value: QueryValue): TSelf;
|
|
154
168
|
join(table: string, alias: string, onLeft: string, onRight: string): TSelf;
|
|
155
169
|
leftJoin(table: string, alias: string, onLeft: string, onRight: string): TSelf;
|
|
156
170
|
group(columns: string | string[]): TSelf;
|
|
157
171
|
having(column: string, operator: FilterOperator, value: QueryValue): TSelf;
|
|
158
|
-
protected addFilter(column: string, operator: FilterOperator, value: QueryValue): TSelf;
|
|
172
|
+
protected addFilter(column: string, operator: FilterOperator, value: QueryValue | QueryValue[]): TSelf;
|
|
159
173
|
protected filteredQueryParams(): URLSearchParams;
|
|
160
174
|
}
|
|
161
175
|
export declare class LuxProjectSelectBuilder<T extends object, TResult> extends LuxProjectFilterBuilder<TResult, LuxProjectSelectBuilder<T, TResult>> {
|
|
@@ -173,7 +187,7 @@ export declare class LuxProjectSelectBuilder<T extends object, TResult> extends
|
|
|
173
187
|
range(from: number, to: number): this;
|
|
174
188
|
single(): LuxProjectSelectBuilder<T, ProjectSelectSingle<TResult>>;
|
|
175
189
|
execute(): Promise<LuxResult<TResult>>;
|
|
176
|
-
live():
|
|
190
|
+
live(): Promise<LuxLiveResult<LuxTypedRow<TResult>>>;
|
|
177
191
|
}
|
|
178
192
|
export declare class LuxProjectLiveSubscription<T extends object> {
|
|
179
193
|
private client;
|
|
@@ -186,13 +200,26 @@ export declare class LuxProjectLiveSubscription<T extends object> {
|
|
|
186
200
|
private offsetCount?;
|
|
187
201
|
private handlers;
|
|
188
202
|
private unsubscribeFn;
|
|
203
|
+
private queue;
|
|
204
|
+
private waiters;
|
|
205
|
+
private closed;
|
|
189
206
|
constructor(client: LuxProjectClient, table: string, columns: string, filters: QueryFilter[], nearQuery?: QueryNear | undefined, orderBy?: QueryOrder | undefined, limitCount?: number | undefined, offsetCount?: number | undefined);
|
|
190
207
|
on(type: LuxProjectLiveEventType | 'change', handler: LiveEventHandler<T>): this;
|
|
191
208
|
unsubscribe(): Promise<void>;
|
|
192
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Open the subscription and wait for the server to confirm it. Resolves
|
|
211
|
+
* `null` once the initial snapshot arrives, or a `LuxError` if the
|
|
212
|
+
* subscription is rejected (e.g. a grant `FORBIDDEN`) or the socket fails.
|
|
213
|
+
* Subsequent errors after a successful start surface via `on('error')` and
|
|
214
|
+
* end the async iterator.
|
|
215
|
+
*/
|
|
216
|
+
start(): Promise<LuxError | null>;
|
|
217
|
+
[Symbol.asyncIterator](): AsyncIterator<LuxProjectLiveEvent<T>>;
|
|
218
|
+
private close;
|
|
193
219
|
private spec;
|
|
194
220
|
private handleEvent;
|
|
195
221
|
private emit;
|
|
222
|
+
private pushIterator;
|
|
196
223
|
}
|
|
197
224
|
export declare class LuxProjectInsertBuilder<TResult> extends LuxProjectThenable<TResult> {
|
|
198
225
|
private client;
|
package/dist/types/table.d.ts
CHANGED
|
@@ -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