@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/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
- if (row.id == null)
97
+ const id = row.id;
98
+ if (id == null)
63
99
  continue;
64
- this.knownRows.set(String(row.id), row);
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 changed = Object.keys(next).filter((key) => previous[key] !== next[key]);
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
- for (let i = 0; i < allConditions.length; i++) {
165
- const cond = allConditions[i];
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 < allConditions.length - 1) {
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
- similar(field, vector, options) {
328
+ near(field, vector, options = {}) {
237
329
  this.similarityClause = {
238
330
  field,
239
331
  vector,
240
- k: options.k,
241
- filter: options.filter,
332
+ k: options.k ?? 10,
333
+ threshold: options.threshold,
242
334
  };
243
335
  return this;
244
336
  }
245
- parseSimilarityPk(result, field) {
246
- const metadata = result.metadata;
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
- let rows = [];
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, String(v));
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, String(v));
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
- for (let i = 0; i < this.conditions.length; i++) {
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
- for (let i = 0; i < this.conditions.length; i++) {
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';