@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/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 = {
@@ -56,9 +91,10 @@ export class TableSubscription {
56
91
  try {
57
92
  const initial = await this.fetchMatches();
58
93
  for (const row of initial) {
59
- if (row.id == null)
94
+ const id = row.id;
95
+ if (id == null)
60
96
  continue;
61
- this.knownRows.set(String(row.id), row);
97
+ this.knownRows.set(String(id), row);
62
98
  }
63
99
  const pattern = `_t:${this.table}:row:*`;
64
100
  this.unsubscribeFn = await this.client._subscribePattern(pattern, (raw) => {
@@ -108,7 +144,9 @@ export class TableSubscription {
108
144
  if (!previous || !next)
109
145
  return;
110
146
  this.knownRows.set(pk, next);
111
- const changed = Object.keys(next).filter((key) => previous[key] !== next[key]);
147
+ const previousRow = previous;
148
+ const nextRow = next;
149
+ const changed = Object.keys(nextRow).filter((key) => previousRow[key] !== nextRow[key]);
112
150
  this.emitChange({
113
151
  type: 'update',
114
152
  table: this.table,
@@ -128,6 +166,8 @@ export class TableSubscription {
128
166
  export class TableQueryBuilder {
129
167
  constructor(client, name, options) {
130
168
  this.conditions = [];
169
+ this.groupFields = [];
170
+ this.havingConditions = [];
131
171
  this.selectClause = '*';
132
172
  this.expectSingle = false;
133
173
  this.client = client;
@@ -153,18 +193,30 @@ export class TableQueryBuilder {
153
193
  const args = [this.selectClause, 'FROM', this.name];
154
194
  const allConditions = extra ? [...this.conditions, ...extra] : this.conditions;
155
195
  if (this.joinClause) {
156
- args.push('JOIN', this.joinClause.table, this.joinClause.alias, 'ON', this.joinClause.onLeft, '=', this.joinClause.onRight);
196
+ args.push(...(this.joinClause.type === 'LEFT' ? ['LEFT', 'JOIN'] : ['JOIN']), this.joinClause.table, this.joinClause.alias, 'ON', this.joinClause.onLeft, '=', this.joinClause.onRight);
157
197
  }
158
198
  if (allConditions.length) {
159
- args.push('WHERE');
160
- for (let i = 0; i < allConditions.length; i++) {
161
- const cond = allConditions[i];
199
+ args.push('WHERE', ...serializeConditions(allConditions));
200
+ }
201
+ if (this.groupFields.length) {
202
+ args.push('GROUP', 'BY', ...this.groupFields);
203
+ }
204
+ if (this.havingConditions.length) {
205
+ args.push('HAVING');
206
+ for (let i = 0; i < this.havingConditions.length; i++) {
207
+ const cond = this.havingConditions[i];
162
208
  args.push(cond.field, cond.op, String(cond.value));
163
- if (i < allConditions.length - 1) {
209
+ if (i < this.havingConditions.length - 1) {
164
210
  args.push('AND');
165
211
  }
166
212
  }
167
213
  }
214
+ if (this.similarityClause) {
215
+ args.push('NEAR', this.similarityClause.field, `[${this.similarityClause.vector.join(',')}]`, 'K', String(this.similarityClause.k));
216
+ if (this.similarityClause.threshold != null) {
217
+ args.push('THRESHOLD', String(this.similarityClause.threshold));
218
+ }
219
+ }
168
220
  if (this.orderField) {
169
221
  args.push('ORDER', 'BY', this.orderField, this.orderDir || 'ASC');
170
222
  }
@@ -209,6 +261,29 @@ export class TableQueryBuilder {
209
261
  lte(field, value) {
210
262
  return this.where(field, '<=', value);
211
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
+ }
212
287
  orderBy(field, dir = 'asc') {
213
288
  this.orderField = field;
214
289
  this.orderDir = dir.toUpperCase();
@@ -226,68 +301,41 @@ export class TableQueryBuilder {
226
301
  return this;
227
302
  }
228
303
  join(table, alias, onLeft, onRight) {
229
- this.joinClause = { table, alias, onLeft, onRight };
304
+ this.joinClause = { type: 'INNER', table, alias, onLeft, onRight };
305
+ return this;
306
+ }
307
+ leftJoin(table, alias, onLeft, onRight) {
308
+ this.joinClause = { type: 'LEFT', table, alias, onLeft, onRight };
309
+ return this;
310
+ }
311
+ group(fields) {
312
+ this.groupFields = Array.isArray(fields)
313
+ ? fields
314
+ : fields.split(',').map((field) => field.trim()).filter(Boolean);
315
+ return this;
316
+ }
317
+ groupBy(fields) {
318
+ return this.group(fields);
319
+ }
320
+ having(field, op, value) {
321
+ this.havingConditions.push({ field, op, value });
230
322
  return this;
231
323
  }
232
- similar(field, vector, options) {
324
+ near(field, vector, options = {}) {
233
325
  this.similarityClause = {
234
326
  field,
235
327
  vector,
236
- k: options.k,
237
- filter: options.filter,
328
+ k: options.k ?? 10,
329
+ threshold: options.threshold,
238
330
  };
239
331
  return this;
240
332
  }
241
- parseSimilarityPk(result, field) {
242
- const metadata = result.metadata;
243
- if (metadata && typeof metadata === 'object') {
244
- for (const key of ['id', 'pk', 'row_id']) {
245
- const value = metadata[key];
246
- if (value != null)
247
- return String(value);
248
- }
249
- }
250
- const expectedPrefix = `${this.name}:${field}:`;
251
- if (result.key.startsWith(expectedPrefix)) {
252
- return result.key.slice(expectedPrefix.length);
253
- }
254
- const segments = result.key.split(':');
255
- if (segments.length > 0) {
256
- return segments[segments.length - 1] || null;
257
- }
258
- return null;
333
+ similar(field, vector, options = {}) {
334
+ return this.near(field, vector, options);
259
335
  }
260
336
  async run() {
261
337
  try {
262
- let rows = [];
263
- if (this.similarityClause) {
264
- if (this.joinClause) {
265
- return err('SIMILAR_JOIN_UNSUPPORTED', 'similar(...) cannot be combined with join(...) yet');
266
- }
267
- const similarResults = await this.client.vsearch(this.similarityClause.vector, {
268
- k: this.similarityClause.k,
269
- filter: this.similarityClause.filter,
270
- meta: true,
271
- });
272
- for (const match of similarResults) {
273
- const pk = this.parseSimilarityPk(match, this.similarityClause.field);
274
- if (!pk)
275
- continue;
276
- const args = this.buildSelectArgs([{ field: 'id', op: '=', value: pk }]);
277
- const one = await this.client._tselect(args);
278
- if (one.length === 0)
279
- continue;
280
- rows.push({ ...one[0], _similarity: match.similarity });
281
- }
282
- if (this.offsetCount != null || this.limitCount != null) {
283
- const start = this.offsetCount ?? 0;
284
- const end = this.limitCount != null ? start + this.limitCount : undefined;
285
- rows = rows.slice(start, end);
286
- }
287
- }
288
- else {
289
- rows = await this.client._tselect(this.buildSelectArgs());
290
- }
338
+ const rows = await this.client._tselect(this.buildSelectArgs());
291
339
  const validated = rows.map((row) => this.validateRow(row));
292
340
  if (this.expectSingle) {
293
341
  if (validated.length === 0) {
@@ -311,7 +359,7 @@ export class TableQueryBuilder {
311
359
  }
312
360
  const args = [this.name];
313
361
  for (const [k, v] of Object.entries(data)) {
314
- args.push(k, String(v));
362
+ args.push(k, serializeFieldValue(v));
315
363
  }
316
364
  const result = await this.client.call('TINSERT', ...args);
317
365
  return ok(parseInt(result, 10) || 0);
@@ -320,6 +368,26 @@ export class TableQueryBuilder {
320
368
  return err('TINSERT_ERROR', `Failed to insert into '${this.name}'`, toLuxError(error));
321
369
  }
322
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
+ }
323
391
  async update(idOrData, data) {
324
392
  try {
325
393
  const hasExplicitId = data !== undefined;
@@ -329,20 +397,14 @@ export class TableQueryBuilder {
329
397
  }
330
398
  const args = [this.name, 'SET'];
331
399
  for (const [k, v] of Object.entries(patch)) {
332
- args.push(k, String(v));
400
+ args.push(k, serializeFieldValue(v));
333
401
  }
334
402
  args.push('WHERE');
335
403
  if (hasExplicitId) {
336
404
  args.push('id', '=', String(idOrData));
337
405
  }
338
406
  else {
339
- for (let i = 0; i < this.conditions.length; i++) {
340
- const cond = this.conditions[i];
341
- args.push(cond.field, cond.op, String(cond.value));
342
- if (i < this.conditions.length - 1) {
343
- args.push('AND');
344
- }
345
- }
407
+ args.push(...serializeConditions(this.conditions));
346
408
  }
347
409
  const result = await this.client.call('TUPDATE', ...args);
348
410
  return ok(Number(result) || 0);
@@ -358,13 +420,7 @@ export class TableQueryBuilder {
358
420
  return err('MISSING_WHERE', 'delete requires at least one filter');
359
421
  }
360
422
  const args = ['FROM', this.name, 'WHERE'];
361
- for (let i = 0; i < this.conditions.length; i++) {
362
- const cond = this.conditions[i];
363
- args.push(cond.field, cond.op, String(cond.value));
364
- if (i < this.conditions.length - 1) {
365
- args.push('AND');
366
- }
367
- }
423
+ args.push(...serializeConditions(this.conditions));
368
424
  const result = await this.client.call('TDELETE', ...args);
369
425
  return ok(Number(result) || 0);
370
426
  }
@@ -380,12 +436,6 @@ export class TableQueryBuilder {
380
436
  }
381
437
  }
382
438
  subscribe() {
383
- if (this.similarityClause) {
384
- return new TableSubscription(this.client, this.name, (extra) => this.buildSelectArgs(extra), {
385
- code: 'SIMILAR_SUBSCRIBE_UNSUPPORTED',
386
- message: 'subscribe() is not supported on similar(...) queries yet',
387
- });
388
- }
389
439
  return new TableSubscription(this.client, this.name, (extra) => this.buildSelectArgs(extra));
390
440
  }
391
441
  }
@@ -11,6 +11,90 @@ export interface LuxAuthUser {
11
11
  user_metadata?: Record<string, unknown>;
12
12
  app_metadata?: Record<string, unknown>;
13
13
  }
14
+ export type LuxUser = LuxAuthUser;
15
+ export interface LuxAuthUserRow {
16
+ id: string;
17
+ email?: string;
18
+ phone?: string;
19
+ encrypted_password?: string;
20
+ email_confirmed_at?: number | null;
21
+ phone_confirmed_at?: number | null;
22
+ raw_user_meta_data?: string;
23
+ raw_app_meta_data?: string;
24
+ created_at?: number | null;
25
+ updated_at?: number | null;
26
+ last_sign_in_at?: number | null;
27
+ banned_until?: number | null;
28
+ deleted_at?: number | null;
29
+ }
30
+ export interface LuxAuthIdentityRow {
31
+ id: string;
32
+ user_id: string;
33
+ provider: string;
34
+ provider_id: string;
35
+ identity_data?: string;
36
+ created_at?: number | null;
37
+ updated_at?: number | null;
38
+ }
39
+ export interface LuxAuthSessionRow {
40
+ id: string;
41
+ user_id: string;
42
+ refresh_token_hash: string;
43
+ refresh_token_family?: string;
44
+ user_agent?: string;
45
+ ip?: string;
46
+ expires_at?: number | null;
47
+ revoked_at?: number | null;
48
+ created_at?: number | null;
49
+ updated_at?: number | null;
50
+ }
51
+ export interface LuxAuthKeyRow {
52
+ id: string;
53
+ name?: string;
54
+ kind: 'publishable' | 'secret' | string;
55
+ prefix: string;
56
+ key_hash: string;
57
+ scopes?: string;
58
+ created_at?: number | null;
59
+ revoked_at?: number | null;
60
+ last_used_at?: number | null;
61
+ }
62
+ export interface LuxAuthSigningKeyRow {
63
+ id: string;
64
+ kid: string;
65
+ algorithm: string;
66
+ public_jwk?: string;
67
+ private_key_encrypted?: string;
68
+ active: boolean;
69
+ created_at?: number | null;
70
+ rotated_at?: number | null;
71
+ }
72
+ export interface LuxAuthGrantRow {
73
+ id: string;
74
+ user_id: string;
75
+ capability: string;
76
+ created_at?: number | null;
77
+ revoked_at?: number | null;
78
+ }
79
+ export interface LuxAuthProviderRow {
80
+ provider: LuxOAuthProvider | string;
81
+ enabled: boolean;
82
+ client_id?: string;
83
+ client_secret?: string;
84
+ redirect_uri?: string;
85
+ scopes?: string;
86
+ created_at?: number | null;
87
+ updated_at?: number | null;
88
+ }
89
+ export interface LuxAuthTables {
90
+ 'auth.users': LuxAuthUserRow;
91
+ 'auth.identities': LuxAuthIdentityRow;
92
+ 'auth.sessions': LuxAuthSessionRow;
93
+ 'auth.keys': LuxAuthKeyRow;
94
+ 'auth.signing_keys': LuxAuthSigningKeyRow;
95
+ 'auth.grants': LuxAuthGrantRow;
96
+ 'auth.providers': LuxAuthProviderRow;
97
+ }
14
98
  export interface LuxAuthSession {
15
99
  access_token: string;
16
100
  token_type: 'bearer';
@@ -3,15 +3,16 @@ import { LuxAuthClient, type LuxAuthOptions } from './auth';
3
3
  import { createProjectClient, LuxProjectClient, type LuxProjectOptions } from './project';
4
4
  import { TimeSeriesNamespace, VectorNamespace } from './namespaces';
5
5
  import { TableQueryBuilder, type TableQueryBuilderOptions } from './table';
6
- import type { KSubEvent, TableRow, TSAddOptions, TSMRangeResult, TSRangeOptions, TSSample, VSearchResult } from './types';
7
- export type { LuxAuthKey, LuxAuthChangeEvent, LuxAuthOptions, LuxAuthSession, LuxAuthStateChangeCallback, LuxAuthStorage, LuxAuthSubscription, LuxAuthUser, LuxOAuthProvider, LuxOAuthUrl, LuxSignInWithOAuthOptions, LuxCreateApiKeyOptions, LuxSignInOptions, LuxSignUpOptions, } from './auth';
6
+ import type { KSubEvent, LuxTypedRow, TableRow, TSAddOptions, TSMRangeResult, TSRangeOptions, TSSample, VSearchResult } from './types';
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 { LuxProjectOptions, LuxTableColumn, LuxVectorSearchOptions, } from './project';
14
- export type { KSubEvent, LuxError, LuxResult, TableChangeEvent, TableChangeType, TableErrorEvent, TableRow, TableSchema, TSAddOptions, TSMRangeResult, TSRangeOptions, TSSample, VSearchResult, } from './types';
14
+ export type { LuxLiveResult, LuxProjectLiveEvent, LuxProjectLiveEventType, LuxProjectOptions, LuxTableColumn, LuxVectorSearchOptions, } from './project';
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';
17
18
  export type LuxClientOptions = RedisOptions & LuxAuthOptions;
@@ -23,7 +24,7 @@ export declare class Lux extends Redis {
23
24
  authApi: LuxAuthClient;
24
25
  private realtimeManager?;
25
26
  constructor(options?: LuxClientOptions | RedisOptions | string);
26
- table<T extends TableRow = TableRow>(name: string, options?: TableQueryBuilderOptions<T>): TableQueryBuilder<T>;
27
+ table<T extends object | readonly object[] = TableRow>(name: string, options?: TableQueryBuilderOptions<LuxTypedRow<T>>): TableQueryBuilder<LuxTypedRow<T>>;
27
28
  _subscribePattern(pattern: string, handler: (event: KSubEvent) => void): Promise<() => void>;
28
29
  _tselect(args: string[]): Promise<TableRow[]>;
29
30
  vset(key: string, vector: number[], options?: {
@@ -1,14 +1,15 @@
1
1
  import { LuxAuthClient, type LuxAuthOptions } from './auth';
2
- import type { LuxResult } from './types';
2
+ import type { LuxError, LuxResult, LuxTypedRow } from './types';
3
3
  export interface LuxProjectOptions {
4
4
  url: string;
5
5
  key: string;
6
6
  fetch?: typeof fetch;
7
+ websocket?: typeof WebSocket;
7
8
  auth?: Omit<LuxAuthOptions, 'httpUrl' | 'apiKey' | 'fetch'>;
8
9
  }
9
10
  export interface LuxTableColumn {
10
11
  name: string;
11
- type: 'STR' | 'INT' | 'FLOAT' | 'BOOL' | 'TIMESTAMP' | 'UUID';
12
+ type: 'STR' | 'INT' | 'FLOAT' | 'BOOL' | 'TIMESTAMP' | 'UUID' | `VECTOR(${number})`;
12
13
  primaryKey?: boolean;
13
14
  unique?: boolean;
14
15
  notNull?: boolean;
@@ -21,24 +22,73 @@ export interface LuxVectorSearchOptions {
21
22
  filter?: string;
22
23
  filter_value?: string;
23
24
  }
24
- type QueryValue = string | number | boolean | null;
25
- type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'is';
25
+ type QueryValue = string | number | boolean | number[] | null;
26
+ type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'is' | 'in' | 'notIn' | 'isValid' | 'isNotValid' | 'contains';
27
+ type ProjectRowInput<T extends object> = Partial<T> & Record<string, QueryValue>;
28
+ type ProjectSelectSingle<TResult> = TResult extends readonly (infer Row)[] ? Row : TResult;
26
29
  interface QueryFilter {
27
30
  column: string;
28
31
  operator: FilterOperator;
29
- value: QueryValue;
32
+ value: QueryValue | QueryValue[];
30
33
  }
31
34
  interface QueryOrder {
32
35
  column: string;
33
36
  ascending: boolean;
34
37
  }
38
+ interface QueryJoin {
39
+ type: 'inner' | 'left';
40
+ table: string;
41
+ alias: string;
42
+ onLeft: string;
43
+ onRight: string;
44
+ }
45
+ interface QueryHaving {
46
+ column: string;
47
+ operator: FilterOperator;
48
+ value: QueryValue;
49
+ }
50
+ interface QueryNear {
51
+ field: string;
52
+ vector: number[];
53
+ k: number;
54
+ threshold?: number;
55
+ }
56
+ export type LuxProjectLiveEventType = 'snapshot' | 'insert' | 'update' | 'delete' | 'error';
57
+ export interface LuxProjectLiveEvent<T extends object = Record<string, unknown>> {
58
+ type: LuxProjectLiveEventType;
59
+ table: string;
60
+ pk?: string;
61
+ new: T | null;
62
+ old: T | null;
63
+ rows?: T[];
64
+ changed?: string[];
65
+ raw?: unknown;
66
+ error?: {
67
+ code?: string;
68
+ message?: string;
69
+ };
70
+ }
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
+ }
35
81
  export declare class LuxProjectClient {
36
82
  readonly url: string;
37
83
  readonly key: string;
38
84
  readonly auth: LuxAuthClient;
39
85
  private fetchImpl;
86
+ private WebSocketImpl?;
87
+ private liveSocket;
88
+ private liveSubscriptions;
89
+ private livePending;
40
90
  constructor(options: LuxProjectOptions);
41
- table<T extends Record<string, unknown> = Record<string, unknown>>(name: string): LuxProjectTable<T>;
91
+ table<T extends object | readonly object[] = Record<string, unknown>>(name: string): LuxProjectTable<LuxTypedRow<T>>;
42
92
  ping(): Promise<LuxResult<unknown>>;
43
93
  createTable(name: string, columns: Array<string | LuxTableColumn>): Promise<LuxResult<unknown>>;
44
94
  exec(command: string | string[]): Promise<LuxResult<unknown>>;
@@ -55,15 +105,33 @@ export declare class LuxProjectClient {
55
105
  count?: number;
56
106
  }): Promise<LuxResult<unknown>>;
57
107
  request<T = unknown>(method: string, path: string, body?: unknown): Promise<LuxResult<T>>;
108
+ _subscribeLive(spec: Record<string, unknown>, handler: (event: unknown) => void, error: (error: {
109
+ code?: string;
110
+ message?: string;
111
+ }) => void): Promise<() => void>;
112
+ private ensureLiveSocket;
113
+ private sendLive;
58
114
  }
59
- export declare class LuxProjectTable<T extends Record<string, unknown>> {
115
+ export declare class LuxProjectTable<T extends object> {
60
116
  private client;
61
117
  private name;
62
118
  constructor(client: LuxProjectClient, name: string);
63
- select(columns?: string): LuxProjectSelectBuilder<T, T[]>;
64
- insert(row: Partial<T> & Record<string, QueryValue>): LuxProjectInsertBuilder<unknown>;
65
- insert(rows: Array<Partial<T> & Record<string, QueryValue>>): LuxProjectInsertBuilder<unknown[]>;
66
- update(patch: Partial<T> & Record<string, QueryValue>): LuxProjectMutationBuilder<unknown>;
119
+ select<TResult extends object = T>(columns?: string): LuxProjectSelectBuilder<T, TResult[]>;
120
+ eq(column: string, value: QueryValue): LuxProjectSelectBuilder<T, T[]>;
121
+ neq(column: string, value: QueryValue): LuxProjectSelectBuilder<T, T[]>;
122
+ gt(column: string, value: QueryValue): LuxProjectSelectBuilder<T, T[]>;
123
+ gte(column: string, value: QueryValue): LuxProjectSelectBuilder<T, T[]>;
124
+ lt(column: string, value: QueryValue): LuxProjectSelectBuilder<T, T[]>;
125
+ lte(column: string, value: QueryValue): LuxProjectSelectBuilder<T, T[]>;
126
+ near(column: string, vector: number[], options?: {
127
+ k?: number;
128
+ threshold?: number;
129
+ }): LuxProjectSelectBuilder<T, T[]>;
130
+ is(column: string, value: QueryValue): LuxProjectSelectBuilder<T, T[]>;
131
+ live(): Promise<LuxLiveResult<T>>;
132
+ insert(row: ProjectRowInput<T>): LuxProjectInsertBuilder<unknown>;
133
+ insert(rows: Array<ProjectRowInput<T>>): LuxProjectInsertBuilder<unknown[]>;
134
+ update(patch: ProjectRowInput<T>): LuxProjectMutationBuilder<unknown>;
67
135
  delete(): LuxProjectMutationBuilder<unknown>;
68
136
  count(): Promise<LuxResult<number>>;
69
137
  }
@@ -78,6 +146,10 @@ declare abstract class LuxProjectFilterBuilder<TResult, TSelf> extends LuxProjec
78
146
  protected tableName: string;
79
147
  protected filters: QueryFilter[];
80
148
  protected orderBy?: QueryOrder;
149
+ protected joins: QueryJoin[];
150
+ protected groupColumns: string[];
151
+ protected havingFilters: QueryHaving[];
152
+ protected nearQuery?: QueryNear;
81
153
  protected limitCount?: number;
82
154
  protected offsetCount?: number;
83
155
  protected constructor(client: LuxProjectClient, tableName: string);
@@ -88,20 +160,66 @@ declare abstract class LuxProjectFilterBuilder<TResult, TSelf> extends LuxProjec
88
160
  lt(column: string, value: QueryValue): TSelf;
89
161
  lte(column: string, value: QueryValue): TSelf;
90
162
  is(column: string, value: QueryValue): TSelf;
91
- protected addFilter(column: string, operator: FilterOperator, 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;
168
+ join(table: string, alias: string, onLeft: string, onRight: string): TSelf;
169
+ leftJoin(table: string, alias: string, onLeft: string, onRight: string): TSelf;
170
+ group(columns: string | string[]): TSelf;
171
+ having(column: string, operator: FilterOperator, value: QueryValue): TSelf;
172
+ protected addFilter(column: string, operator: FilterOperator, value: QueryValue | QueryValue[]): TSelf;
92
173
  protected filteredQueryParams(): URLSearchParams;
93
174
  }
94
- export declare class LuxProjectSelectBuilder<T extends Record<string, unknown>, TResult> extends LuxProjectFilterBuilder<TResult, LuxProjectSelectBuilder<T, TResult>> {
175
+ export declare class LuxProjectSelectBuilder<T extends object, TResult> extends LuxProjectFilterBuilder<TResult, LuxProjectSelectBuilder<T, TResult>> {
95
176
  private columns;
96
177
  private expectSingle;
97
178
  constructor(client: LuxProjectClient, tableName: string, columns: string);
98
179
  order(column: string, options?: {
99
180
  ascending?: boolean;
100
181
  }): this;
182
+ near(column: string, vector: number[], options?: {
183
+ k?: number;
184
+ threshold?: number;
185
+ }): this;
101
186
  limit(count: number): this;
102
187
  range(from: number, to: number): this;
103
- single(): LuxProjectSelectBuilder<T, T>;
188
+ single(): LuxProjectSelectBuilder<T, ProjectSelectSingle<TResult>>;
104
189
  execute(): Promise<LuxResult<TResult>>;
190
+ live(): Promise<LuxLiveResult<LuxTypedRow<TResult>>>;
191
+ }
192
+ export declare class LuxProjectLiveSubscription<T extends object> {
193
+ private client;
194
+ private table;
195
+ private columns;
196
+ private filters;
197
+ private nearQuery?;
198
+ private orderBy?;
199
+ private limitCount?;
200
+ private offsetCount?;
201
+ private handlers;
202
+ private unsubscribeFn;
203
+ private queue;
204
+ private waiters;
205
+ private closed;
206
+ constructor(client: LuxProjectClient, table: string, columns: string, filters: QueryFilter[], nearQuery?: QueryNear | undefined, orderBy?: QueryOrder | undefined, limitCount?: number | undefined, offsetCount?: number | undefined);
207
+ on(type: LuxProjectLiveEventType | 'change', handler: LiveEventHandler<T>): this;
208
+ unsubscribe(): Promise<void>;
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;
219
+ private spec;
220
+ private handleEvent;
221
+ private emit;
222
+ private pushIterator;
105
223
  }
106
224
  export declare class LuxProjectInsertBuilder<TResult> extends LuxProjectThenable<TResult> {
107
225
  private client;