@luxdb/sdk 1.4.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/project.js +407 -1
- package/dist/cjs/table.js +131 -81
- package/dist/esm/index.js +1 -0
- package/dist/esm/project.js +405 -0
- package/dist/esm/table.js +131 -81
- package/dist/types/auth.d.ts +84 -0
- package/dist/types/index.d.ts +6 -5
- package/dist/types/project.d.ts +132 -14
- package/dist/types/table.d.ts +31 -21
- package/dist/types/types.d.ts +9 -1
- package/package.json +1 -1
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 = {
|
|
@@ -59,9 +94,10 @@ class TableSubscription {
|
|
|
59
94
|
try {
|
|
60
95
|
const initial = await this.fetchMatches();
|
|
61
96
|
for (const row of initial) {
|
|
62
|
-
|
|
97
|
+
const id = row.id;
|
|
98
|
+
if (id == null)
|
|
63
99
|
continue;
|
|
64
|
-
this.knownRows.set(String(
|
|
100
|
+
this.knownRows.set(String(id), row);
|
|
65
101
|
}
|
|
66
102
|
const pattern = `_t:${this.table}:row:*`;
|
|
67
103
|
this.unsubscribeFn = await this.client._subscribePattern(pattern, (raw) => {
|
|
@@ -111,7 +147,9 @@ class TableSubscription {
|
|
|
111
147
|
if (!previous || !next)
|
|
112
148
|
return;
|
|
113
149
|
this.knownRows.set(pk, next);
|
|
114
|
-
const
|
|
150
|
+
const previousRow = previous;
|
|
151
|
+
const nextRow = next;
|
|
152
|
+
const changed = Object.keys(nextRow).filter((key) => previousRow[key] !== nextRow[key]);
|
|
115
153
|
this.emitChange({
|
|
116
154
|
type: 'update',
|
|
117
155
|
table: this.table,
|
|
@@ -132,6 +170,8 @@ exports.TableSubscription = TableSubscription;
|
|
|
132
170
|
class TableQueryBuilder {
|
|
133
171
|
constructor(client, name, options) {
|
|
134
172
|
this.conditions = [];
|
|
173
|
+
this.groupFields = [];
|
|
174
|
+
this.havingConditions = [];
|
|
135
175
|
this.selectClause = '*';
|
|
136
176
|
this.expectSingle = false;
|
|
137
177
|
this.client = client;
|
|
@@ -157,18 +197,30 @@ class TableQueryBuilder {
|
|
|
157
197
|
const args = [this.selectClause, 'FROM', this.name];
|
|
158
198
|
const allConditions = extra ? [...this.conditions, ...extra] : this.conditions;
|
|
159
199
|
if (this.joinClause) {
|
|
160
|
-
args.push('JOIN', this.joinClause.table, this.joinClause.alias, 'ON', this.joinClause.onLeft, '=', this.joinClause.onRight);
|
|
200
|
+
args.push(...(this.joinClause.type === 'LEFT' ? ['LEFT', 'JOIN'] : ['JOIN']), this.joinClause.table, this.joinClause.alias, 'ON', this.joinClause.onLeft, '=', this.joinClause.onRight);
|
|
161
201
|
}
|
|
162
202
|
if (allConditions.length) {
|
|
163
|
-
args.push('WHERE');
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
args.push('WHERE', ...serializeConditions(allConditions));
|
|
204
|
+
}
|
|
205
|
+
if (this.groupFields.length) {
|
|
206
|
+
args.push('GROUP', 'BY', ...this.groupFields);
|
|
207
|
+
}
|
|
208
|
+
if (this.havingConditions.length) {
|
|
209
|
+
args.push('HAVING');
|
|
210
|
+
for (let i = 0; i < this.havingConditions.length; i++) {
|
|
211
|
+
const cond = this.havingConditions[i];
|
|
166
212
|
args.push(cond.field, cond.op, String(cond.value));
|
|
167
|
-
if (i <
|
|
213
|
+
if (i < this.havingConditions.length - 1) {
|
|
168
214
|
args.push('AND');
|
|
169
215
|
}
|
|
170
216
|
}
|
|
171
217
|
}
|
|
218
|
+
if (this.similarityClause) {
|
|
219
|
+
args.push('NEAR', this.similarityClause.field, `[${this.similarityClause.vector.join(',')}]`, 'K', String(this.similarityClause.k));
|
|
220
|
+
if (this.similarityClause.threshold != null) {
|
|
221
|
+
args.push('THRESHOLD', String(this.similarityClause.threshold));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
172
224
|
if (this.orderField) {
|
|
173
225
|
args.push('ORDER', 'BY', this.orderField, this.orderDir || 'ASC');
|
|
174
226
|
}
|
|
@@ -213,6 +265,29 @@ class TableQueryBuilder {
|
|
|
213
265
|
lte(field, value) {
|
|
214
266
|
return this.where(field, '<=', value);
|
|
215
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
|
+
}
|
|
216
291
|
orderBy(field, dir = 'asc') {
|
|
217
292
|
this.orderField = field;
|
|
218
293
|
this.orderDir = dir.toUpperCase();
|
|
@@ -230,68 +305,41 @@ class TableQueryBuilder {
|
|
|
230
305
|
return this;
|
|
231
306
|
}
|
|
232
307
|
join(table, alias, onLeft, onRight) {
|
|
233
|
-
this.joinClause = { table, alias, onLeft, onRight };
|
|
308
|
+
this.joinClause = { type: 'INNER', table, alias, onLeft, onRight };
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
leftJoin(table, alias, onLeft, onRight) {
|
|
312
|
+
this.joinClause = { type: 'LEFT', table, alias, onLeft, onRight };
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
group(fields) {
|
|
316
|
+
this.groupFields = Array.isArray(fields)
|
|
317
|
+
? fields
|
|
318
|
+
: fields.split(',').map((field) => field.trim()).filter(Boolean);
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
groupBy(fields) {
|
|
322
|
+
return this.group(fields);
|
|
323
|
+
}
|
|
324
|
+
having(field, op, value) {
|
|
325
|
+
this.havingConditions.push({ field, op, value });
|
|
234
326
|
return this;
|
|
235
327
|
}
|
|
236
|
-
|
|
328
|
+
near(field, vector, options = {}) {
|
|
237
329
|
this.similarityClause = {
|
|
238
330
|
field,
|
|
239
331
|
vector,
|
|
240
|
-
k: options.k,
|
|
241
|
-
|
|
332
|
+
k: options.k ?? 10,
|
|
333
|
+
threshold: options.threshold,
|
|
242
334
|
};
|
|
243
335
|
return this;
|
|
244
336
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (metadata && typeof metadata === 'object') {
|
|
248
|
-
for (const key of ['id', 'pk', 'row_id']) {
|
|
249
|
-
const value = metadata[key];
|
|
250
|
-
if (value != null)
|
|
251
|
-
return String(value);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
const expectedPrefix = `${this.name}:${field}:`;
|
|
255
|
-
if (result.key.startsWith(expectedPrefix)) {
|
|
256
|
-
return result.key.slice(expectedPrefix.length);
|
|
257
|
-
}
|
|
258
|
-
const segments = result.key.split(':');
|
|
259
|
-
if (segments.length > 0) {
|
|
260
|
-
return segments[segments.length - 1] || null;
|
|
261
|
-
}
|
|
262
|
-
return null;
|
|
337
|
+
similar(field, vector, options = {}) {
|
|
338
|
+
return this.near(field, vector, options);
|
|
263
339
|
}
|
|
264
340
|
async run() {
|
|
265
341
|
try {
|
|
266
|
-
|
|
267
|
-
if (this.similarityClause) {
|
|
268
|
-
if (this.joinClause) {
|
|
269
|
-
return (0, utils_1.err)('SIMILAR_JOIN_UNSUPPORTED', 'similar(...) cannot be combined with join(...) yet');
|
|
270
|
-
}
|
|
271
|
-
const similarResults = await this.client.vsearch(this.similarityClause.vector, {
|
|
272
|
-
k: this.similarityClause.k,
|
|
273
|
-
filter: this.similarityClause.filter,
|
|
274
|
-
meta: true,
|
|
275
|
-
});
|
|
276
|
-
for (const match of similarResults) {
|
|
277
|
-
const pk = this.parseSimilarityPk(match, this.similarityClause.field);
|
|
278
|
-
if (!pk)
|
|
279
|
-
continue;
|
|
280
|
-
const args = this.buildSelectArgs([{ field: 'id', op: '=', value: pk }]);
|
|
281
|
-
const one = await this.client._tselect(args);
|
|
282
|
-
if (one.length === 0)
|
|
283
|
-
continue;
|
|
284
|
-
rows.push({ ...one[0], _similarity: match.similarity });
|
|
285
|
-
}
|
|
286
|
-
if (this.offsetCount != null || this.limitCount != null) {
|
|
287
|
-
const start = this.offsetCount ?? 0;
|
|
288
|
-
const end = this.limitCount != null ? start + this.limitCount : undefined;
|
|
289
|
-
rows = rows.slice(start, end);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
rows = await this.client._tselect(this.buildSelectArgs());
|
|
294
|
-
}
|
|
342
|
+
const rows = await this.client._tselect(this.buildSelectArgs());
|
|
295
343
|
const validated = rows.map((row) => this.validateRow(row));
|
|
296
344
|
if (this.expectSingle) {
|
|
297
345
|
if (validated.length === 0) {
|
|
@@ -315,7 +363,7 @@ class TableQueryBuilder {
|
|
|
315
363
|
}
|
|
316
364
|
const args = [this.name];
|
|
317
365
|
for (const [k, v] of Object.entries(data)) {
|
|
318
|
-
args.push(k,
|
|
366
|
+
args.push(k, serializeFieldValue(v));
|
|
319
367
|
}
|
|
320
368
|
const result = await this.client.call('TINSERT', ...args);
|
|
321
369
|
return (0, utils_1.ok)(parseInt(result, 10) || 0);
|
|
@@ -324,6 +372,26 @@ class TableQueryBuilder {
|
|
|
324
372
|
return (0, utils_1.err)('TINSERT_ERROR', `Failed to insert into '${this.name}'`, (0, utils_1.toLuxError)(error));
|
|
325
373
|
}
|
|
326
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
|
+
}
|
|
327
395
|
async update(idOrData, data) {
|
|
328
396
|
try {
|
|
329
397
|
const hasExplicitId = data !== undefined;
|
|
@@ -333,20 +401,14 @@ class TableQueryBuilder {
|
|
|
333
401
|
}
|
|
334
402
|
const args = [this.name, 'SET'];
|
|
335
403
|
for (const [k, v] of Object.entries(patch)) {
|
|
336
|
-
args.push(k,
|
|
404
|
+
args.push(k, serializeFieldValue(v));
|
|
337
405
|
}
|
|
338
406
|
args.push('WHERE');
|
|
339
407
|
if (hasExplicitId) {
|
|
340
408
|
args.push('id', '=', String(idOrData));
|
|
341
409
|
}
|
|
342
410
|
else {
|
|
343
|
-
|
|
344
|
-
const cond = this.conditions[i];
|
|
345
|
-
args.push(cond.field, cond.op, String(cond.value));
|
|
346
|
-
if (i < this.conditions.length - 1) {
|
|
347
|
-
args.push('AND');
|
|
348
|
-
}
|
|
349
|
-
}
|
|
411
|
+
args.push(...serializeConditions(this.conditions));
|
|
350
412
|
}
|
|
351
413
|
const result = await this.client.call('TUPDATE', ...args);
|
|
352
414
|
return (0, utils_1.ok)(Number(result) || 0);
|
|
@@ -362,13 +424,7 @@ class TableQueryBuilder {
|
|
|
362
424
|
return (0, utils_1.err)('MISSING_WHERE', 'delete requires at least one filter');
|
|
363
425
|
}
|
|
364
426
|
const args = ['FROM', this.name, 'WHERE'];
|
|
365
|
-
|
|
366
|
-
const cond = this.conditions[i];
|
|
367
|
-
args.push(cond.field, cond.op, String(cond.value));
|
|
368
|
-
if (i < this.conditions.length - 1) {
|
|
369
|
-
args.push('AND');
|
|
370
|
-
}
|
|
371
|
-
}
|
|
427
|
+
args.push(...serializeConditions(this.conditions));
|
|
372
428
|
const result = await this.client.call('TDELETE', ...args);
|
|
373
429
|
return (0, utils_1.ok)(Number(result) || 0);
|
|
374
430
|
}
|
|
@@ -384,12 +440,6 @@ class TableQueryBuilder {
|
|
|
384
440
|
}
|
|
385
441
|
}
|
|
386
442
|
subscribe() {
|
|
387
|
-
if (this.similarityClause) {
|
|
388
|
-
return new TableSubscription(this.client, this.name, (extra) => this.buildSelectArgs(extra), {
|
|
389
|
-
code: 'SIMILAR_SUBSCRIBE_UNSUPPORTED',
|
|
390
|
-
message: 'subscribe() is not supported on similar(...) queries yet',
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
443
|
return new TableSubscription(this.client, this.name, (extra) => this.buildSelectArgs(extra));
|
|
394
444
|
}
|
|
395
445
|
}
|
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';
|