@luxdb/sdk 1.4.2 → 1.4.3
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 +74 -1
- package/dist/cjs/project.js +292 -1
- package/dist/cjs/table.js +52 -61
- package/dist/esm/project.js +290 -0
- package/dist/esm/table.js +52 -61
- package/dist/types/auth.d.ts +84 -0
- package/dist/types/index.d.ts +5 -5
- package/dist/types/project.d.ts +102 -11
- package/dist/types/table.d.ts +17 -19
- package/dist/types/types.d.ts +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,8 +35,20 @@ if (error) throw error;
|
|
|
35
35
|
Queries and mutations return a Supabase-style result object:
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
|
+
interface User {
|
|
39
|
+
id: number;
|
|
40
|
+
email: string;
|
|
41
|
+
age: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Message {
|
|
45
|
+
id: string;
|
|
46
|
+
body: string;
|
|
47
|
+
embedding: number[];
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
const { data: users, error } = await lux
|
|
39
|
-
.table<
|
|
51
|
+
.table<User[]>("users")
|
|
40
52
|
.select()
|
|
41
53
|
.gt("age", 25)
|
|
42
54
|
.order("age", { ascending: false })
|
|
@@ -46,6 +58,28 @@ if (error) throw error;
|
|
|
46
58
|
console.log(users);
|
|
47
59
|
```
|
|
48
60
|
|
|
61
|
+
`table<T>()` accepts either a row type or an array type. `table<User>("users")`
|
|
62
|
+
and `table<User[]>("users")` both infer `User` rows; the array form is useful
|
|
63
|
+
when you want the generic to read like the returned data.
|
|
64
|
+
|
|
65
|
+
For computed projections, pass the projection shape to `select<T>()`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import type { LuxAggregateRow, LuxNearRow } from "@luxdb/sdk";
|
|
69
|
+
|
|
70
|
+
type TeamStats = { team_id: number } & LuxAggregateRow<"member_count" | "avg_age">;
|
|
71
|
+
|
|
72
|
+
const { data: teamStats } = await lux
|
|
73
|
+
.table<User>("members")
|
|
74
|
+
.select<TeamStats>("team_id,COUNT(*) AS member_count,AVG(age) AS avg_age")
|
|
75
|
+
.group("team_id");
|
|
76
|
+
|
|
77
|
+
const { data: matches } = await lux
|
|
78
|
+
.table<Message>("messages")
|
|
79
|
+
.select<LuxNearRow<Message>>("id,body,_similarity")
|
|
80
|
+
.near("embedding", queryEmbedding, { k: 10, threshold: 0.8 });
|
|
81
|
+
```
|
|
82
|
+
|
|
49
83
|
```ts
|
|
50
84
|
const { data: inserted, error: insertError } = await lux
|
|
51
85
|
.table("messages")
|
|
@@ -62,6 +96,31 @@ const { data: deleted, error: deleteError } = await lux
|
|
|
62
96
|
.eq("id", inserted?.id);
|
|
63
97
|
```
|
|
64
98
|
|
|
99
|
+
## Live tables
|
|
100
|
+
|
|
101
|
+
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
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const sub = lux
|
|
105
|
+
.table<{ id: string; channel_id: string; body: string }>("messages")
|
|
106
|
+
.eq("channel_id", "general")
|
|
107
|
+
.live()
|
|
108
|
+
.on("snapshot", (event) => {
|
|
109
|
+
console.log(event.rows);
|
|
110
|
+
})
|
|
111
|
+
.on("insert", (event) => {
|
|
112
|
+
console.log(event.new);
|
|
113
|
+
})
|
|
114
|
+
.on("update", (event) => {
|
|
115
|
+
console.log(event.old, event.new);
|
|
116
|
+
})
|
|
117
|
+
.on("delete", (event) => {
|
|
118
|
+
console.log(event.old);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await sub.unsubscribe();
|
|
122
|
+
```
|
|
123
|
+
|
|
65
124
|
## OAuth
|
|
66
125
|
|
|
67
126
|
```ts
|
|
@@ -82,6 +141,18 @@ if (error) throw error;
|
|
|
82
141
|
console.log(data.user);
|
|
83
142
|
```
|
|
84
143
|
|
|
144
|
+
Auth types are exported for app code and system table reads:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import type { LuxUser, LuxAuthTables } from "@luxdb/sdk";
|
|
148
|
+
|
|
149
|
+
type AuthUserRow = LuxAuthTables["auth.users"];
|
|
150
|
+
|
|
151
|
+
function renderUser(user: LuxUser, row: AuthUserRow) {
|
|
152
|
+
return row.email ?? user.email;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
85
156
|
## Server client
|
|
86
157
|
|
|
87
158
|
Use a secret key only from trusted server code.
|
|
@@ -129,4 +200,6 @@ const value = await lux.get("hello");
|
|
|
129
200
|
- `lux_pub_...` keys are safe for browser app calls.
|
|
130
201
|
- `lux_sec_...` keys are server-only.
|
|
131
202
|
- User sessions issue JWT access tokens.
|
|
203
|
+
- Browser live subscriptions use the project publishable key plus the signed-in user's JWT.
|
|
204
|
+
- Table `select()` accepts Lux's constrained projection grammar, not arbitrary SQL.
|
|
132
205
|
- Direct `lux://` or `rediss://` database access uses the database password and is for trusted infrastructure.
|
package/dist/cjs/project.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LuxProjectMutationBuilder = exports.LuxProjectInsertBuilder = exports.LuxProjectSelectBuilder = exports.LuxProjectTable = exports.LuxProjectClient = void 0;
|
|
3
|
+
exports.LuxProjectMutationBuilder = exports.LuxProjectInsertBuilder = exports.LuxProjectLiveSubscription = exports.LuxProjectSelectBuilder = exports.LuxProjectTable = exports.LuxProjectClient = void 0;
|
|
4
4
|
exports.createProjectClient = createProjectClient;
|
|
5
5
|
exports.createClient = createClient;
|
|
6
6
|
const auth_1 = require("./auth");
|
|
7
7
|
const utils_1 = require("./utils");
|
|
8
8
|
class LuxProjectClient {
|
|
9
9
|
constructor(options) {
|
|
10
|
+
this.liveSocket = null;
|
|
11
|
+
this.liveSubscriptions = new Map();
|
|
12
|
+
this.livePending = [];
|
|
10
13
|
this.url = options.url.replace(/\/+$/, '');
|
|
11
14
|
this.key = options.key;
|
|
12
15
|
this.fetchImpl = resolveFetch(options.fetch);
|
|
16
|
+
this.WebSocketImpl = options.websocket;
|
|
13
17
|
this.auth = new auth_1.LuxAuthClient({
|
|
14
18
|
...options.auth,
|
|
15
19
|
httpUrl: this.url,
|
|
@@ -84,6 +88,96 @@ class LuxProjectClient {
|
|
|
84
88
|
return (0, utils_1.err)('LUX_PROJECT_REQUEST_ERROR', 'Lux request failed', (0, utils_1.toLuxError)(error));
|
|
85
89
|
}
|
|
86
90
|
}
|
|
91
|
+
async _subscribeLive(spec, handler, error) {
|
|
92
|
+
const id = `sub_${Math.random().toString(36).slice(2)}_${Date.now().toString(36)}`;
|
|
93
|
+
const record = { id, spec, handler, error };
|
|
94
|
+
this.liveSubscriptions.set(id, record);
|
|
95
|
+
await this.ensureLiveSocket();
|
|
96
|
+
this.sendLive({
|
|
97
|
+
type: 'live.subscribe',
|
|
98
|
+
id,
|
|
99
|
+
spec,
|
|
100
|
+
});
|
|
101
|
+
return () => {
|
|
102
|
+
this.liveSubscriptions.delete(id);
|
|
103
|
+
this.sendLive({ type: 'live.unsubscribe', id });
|
|
104
|
+
if (this.liveSubscriptions.size === 0) {
|
|
105
|
+
this.liveSocket?.close();
|
|
106
|
+
this.liveSocket = null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async ensureLiveSocket() {
|
|
111
|
+
const WebSocketImpl = resolveWebSocket(this.WebSocketImpl);
|
|
112
|
+
this.WebSocketImpl = WebSocketImpl;
|
|
113
|
+
if (this.liveSocket &&
|
|
114
|
+
(this.liveSocket.readyState === WebSocketImpl.OPEN ||
|
|
115
|
+
this.liveSocket.readyState === WebSocketImpl.CONNECTING)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const accessToken = await this.auth.getAccessToken();
|
|
119
|
+
const url = new URL(`${this.url}/live`);
|
|
120
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
121
|
+
url.searchParams.set('apikey', this.key);
|
|
122
|
+
if (accessToken)
|
|
123
|
+
url.searchParams.set('access_token', accessToken);
|
|
124
|
+
const socket = new WebSocketImpl(url.toString());
|
|
125
|
+
this.liveSocket = socket;
|
|
126
|
+
socket.onopen = () => {
|
|
127
|
+
for (const message of this.livePending.splice(0))
|
|
128
|
+
socket.send(message);
|
|
129
|
+
};
|
|
130
|
+
socket.onmessage = (event) => {
|
|
131
|
+
let message;
|
|
132
|
+
try {
|
|
133
|
+
message = JSON.parse(String(event.data));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const subscription = typeof message.id === 'string' ? this.liveSubscriptions.get(message.id) : null;
|
|
139
|
+
if (message.type === 'live.event' && subscription) {
|
|
140
|
+
subscription.handler(message.event);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (message.type === 'live.error') {
|
|
144
|
+
const target = subscription ? [subscription] : [...this.liveSubscriptions.values()];
|
|
145
|
+
for (const sub of target) {
|
|
146
|
+
sub.error(message.error || { code: 'LIVE_ERROR', message: 'Live subscription failed' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
socket.onerror = () => {
|
|
151
|
+
for (const subscription of this.liveSubscriptions.values()) {
|
|
152
|
+
subscription.error({ code: 'LIVE_SOCKET_ERROR', message: 'Live socket failed' });
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
socket.onclose = () => {
|
|
156
|
+
if (this.liveSocket === socket)
|
|
157
|
+
this.liveSocket = null;
|
|
158
|
+
if (this.liveSubscriptions.size > 0) {
|
|
159
|
+
for (const subscription of this.liveSubscriptions.values()) {
|
|
160
|
+
this.livePending.push(JSON.stringify({
|
|
161
|
+
type: 'live.subscribe',
|
|
162
|
+
id: subscription.id,
|
|
163
|
+
spec: subscription.spec,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
void this.ensureLiveSocket();
|
|
168
|
+
}, 1000);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
sendLive(message) {
|
|
173
|
+
const payload = JSON.stringify(message);
|
|
174
|
+
const WebSocketImpl = this.WebSocketImpl;
|
|
175
|
+
if (WebSocketImpl && this.liveSocket?.readyState === WebSocketImpl.OPEN) {
|
|
176
|
+
this.liveSocket.send(payload);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.livePending.push(payload);
|
|
180
|
+
}
|
|
87
181
|
}
|
|
88
182
|
exports.LuxProjectClient = LuxProjectClient;
|
|
89
183
|
class LuxProjectTable {
|
|
@@ -94,6 +188,33 @@ class LuxProjectTable {
|
|
|
94
188
|
select(columns = '*') {
|
|
95
189
|
return new LuxProjectSelectBuilder(this.client, this.name, columns);
|
|
96
190
|
}
|
|
191
|
+
eq(column, value) {
|
|
192
|
+
return this.select().eq(column, value);
|
|
193
|
+
}
|
|
194
|
+
neq(column, value) {
|
|
195
|
+
return this.select().neq(column, value);
|
|
196
|
+
}
|
|
197
|
+
gt(column, value) {
|
|
198
|
+
return this.select().gt(column, value);
|
|
199
|
+
}
|
|
200
|
+
gte(column, value) {
|
|
201
|
+
return this.select().gte(column, value);
|
|
202
|
+
}
|
|
203
|
+
lt(column, value) {
|
|
204
|
+
return this.select().lt(column, value);
|
|
205
|
+
}
|
|
206
|
+
lte(column, value) {
|
|
207
|
+
return this.select().lte(column, value);
|
|
208
|
+
}
|
|
209
|
+
near(column, vector, options = {}) {
|
|
210
|
+
return this.select().near(column, vector, options);
|
|
211
|
+
}
|
|
212
|
+
is(column, value) {
|
|
213
|
+
return this.select().is(column, value);
|
|
214
|
+
}
|
|
215
|
+
live() {
|
|
216
|
+
return this.select().live();
|
|
217
|
+
}
|
|
97
218
|
insert(rowOrRows) {
|
|
98
219
|
return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows);
|
|
99
220
|
}
|
|
@@ -128,6 +249,9 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
|
|
|
128
249
|
this.client = client;
|
|
129
250
|
this.tableName = tableName;
|
|
130
251
|
this.filters = [];
|
|
252
|
+
this.joins = [];
|
|
253
|
+
this.groupColumns = [];
|
|
254
|
+
this.havingFilters = [];
|
|
131
255
|
}
|
|
132
256
|
eq(column, value) {
|
|
133
257
|
return this.addFilter(column, 'eq', value);
|
|
@@ -150,6 +274,24 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
|
|
|
150
274
|
is(column, value) {
|
|
151
275
|
return this.addFilter(column, 'is', value);
|
|
152
276
|
}
|
|
277
|
+
join(table, alias, onLeft, onRight) {
|
|
278
|
+
this.joins.push({ type: 'inner', table, alias, onLeft, onRight });
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
leftJoin(table, alias, onLeft, onRight) {
|
|
282
|
+
this.joins.push({ type: 'left', table, alias, onLeft, onRight });
|
|
283
|
+
return this;
|
|
284
|
+
}
|
|
285
|
+
group(columns) {
|
|
286
|
+
this.groupColumns = Array.isArray(columns)
|
|
287
|
+
? columns
|
|
288
|
+
: columns.split(',').map((column) => column.trim()).filter(Boolean);
|
|
289
|
+
return this;
|
|
290
|
+
}
|
|
291
|
+
having(column, operator, value) {
|
|
292
|
+
this.havingFilters.push({ column, operator, value });
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
153
295
|
addFilter(column, operator, value) {
|
|
154
296
|
this.filters.push({ column, operator, value });
|
|
155
297
|
return this;
|
|
@@ -158,6 +300,22 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
|
|
|
158
300
|
const params = new URLSearchParams();
|
|
159
301
|
if (this.filters.length)
|
|
160
302
|
params.set('where', filtersToWhere(this.filters));
|
|
303
|
+
for (const join of this.joins) {
|
|
304
|
+
const kind = join.type === 'left' ? ':left' : '';
|
|
305
|
+
params.append('join', `${join.table}:${join.alias}${kind}:on(${join.onLeft}=${join.onRight})`);
|
|
306
|
+
}
|
|
307
|
+
if (this.groupColumns.length)
|
|
308
|
+
params.set('group', this.groupColumns.join(','));
|
|
309
|
+
if (this.havingFilters.length)
|
|
310
|
+
params.set('having', havingToWhere(this.havingFilters));
|
|
311
|
+
if (this.nearQuery) {
|
|
312
|
+
params.set('near_field', this.nearQuery.field);
|
|
313
|
+
params.set('near_vector', `[${this.nearQuery.vector.join(',')}]`);
|
|
314
|
+
params.set('near_k', String(this.nearQuery.k));
|
|
315
|
+
if (this.nearQuery.threshold != null) {
|
|
316
|
+
params.set('near_threshold', String(this.nearQuery.threshold));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
161
319
|
if (this.orderBy) {
|
|
162
320
|
params.set('order', `${this.orderBy.column} ${this.orderBy.ascending ? 'ASC' : 'DESC'}`);
|
|
163
321
|
}
|
|
@@ -178,6 +336,15 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
|
|
|
178
336
|
this.orderBy = { column, ascending: options.ascending ?? true };
|
|
179
337
|
return this;
|
|
180
338
|
}
|
|
339
|
+
near(column, vector, options = {}) {
|
|
340
|
+
this.nearQuery = {
|
|
341
|
+
field: column,
|
|
342
|
+
vector,
|
|
343
|
+
k: options.k ?? 10,
|
|
344
|
+
threshold: options.threshold,
|
|
345
|
+
};
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
181
348
|
limit(count) {
|
|
182
349
|
this.limitCount = count;
|
|
183
350
|
return this;
|
|
@@ -210,8 +377,119 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
|
|
|
210
377
|
}
|
|
211
378
|
return (0, utils_1.ok)(rows[0]);
|
|
212
379
|
}
|
|
380
|
+
live() {
|
|
381
|
+
return new LuxProjectLiveSubscription(this.client, this.tableName, this.columns, this.filters, this.nearQuery, this.orderBy, this.limitCount, this.offsetCount);
|
|
382
|
+
}
|
|
213
383
|
}
|
|
214
384
|
exports.LuxProjectSelectBuilder = LuxProjectSelectBuilder;
|
|
385
|
+
class LuxProjectLiveSubscription {
|
|
386
|
+
constructor(client, table, columns, filters, nearQuery, orderBy, limitCount, offsetCount) {
|
|
387
|
+
this.client = client;
|
|
388
|
+
this.table = table;
|
|
389
|
+
this.columns = columns;
|
|
390
|
+
this.filters = filters;
|
|
391
|
+
this.nearQuery = nearQuery;
|
|
392
|
+
this.orderBy = orderBy;
|
|
393
|
+
this.limitCount = limitCount;
|
|
394
|
+
this.offsetCount = offsetCount;
|
|
395
|
+
this.handlers = {
|
|
396
|
+
snapshot: [],
|
|
397
|
+
insert: [],
|
|
398
|
+
update: [],
|
|
399
|
+
delete: [],
|
|
400
|
+
error: [],
|
|
401
|
+
change: [],
|
|
402
|
+
};
|
|
403
|
+
this.unsubscribeFn = null;
|
|
404
|
+
void this.start();
|
|
405
|
+
}
|
|
406
|
+
on(type, handler) {
|
|
407
|
+
this.handlers[type].push(handler);
|
|
408
|
+
return this;
|
|
409
|
+
}
|
|
410
|
+
async unsubscribe() {
|
|
411
|
+
this.unsubscribeFn?.();
|
|
412
|
+
this.unsubscribeFn = null;
|
|
413
|
+
}
|
|
414
|
+
async start() {
|
|
415
|
+
this.unsubscribeFn = await this.client._subscribeLive(this.spec(), (event) => this.handleEvent(event), (error) => this.emit({
|
|
416
|
+
type: 'error',
|
|
417
|
+
table: this.table,
|
|
418
|
+
new: null,
|
|
419
|
+
old: null,
|
|
420
|
+
error,
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
spec() {
|
|
424
|
+
const spec = {
|
|
425
|
+
kind: 'table',
|
|
426
|
+
table: this.table,
|
|
427
|
+
select: this.columns,
|
|
428
|
+
};
|
|
429
|
+
if (this.filters.length) {
|
|
430
|
+
spec.where = this.filters.map((filter) => ({
|
|
431
|
+
field: filter.column,
|
|
432
|
+
op: filterOperatorToWhere(filter.operator),
|
|
433
|
+
value: filter.value,
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
if (this.nearQuery) {
|
|
437
|
+
spec.near = {
|
|
438
|
+
field: this.nearQuery.field,
|
|
439
|
+
vector: this.nearQuery.vector,
|
|
440
|
+
k: this.nearQuery.k,
|
|
441
|
+
threshold: this.nearQuery.threshold,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (this.orderBy) {
|
|
445
|
+
spec.orderBy = {
|
|
446
|
+
field: this.orderBy.column,
|
|
447
|
+
dir: this.orderBy.ascending ? 'asc' : 'desc',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (this.limitCount != null)
|
|
451
|
+
spec.limit = this.limitCount;
|
|
452
|
+
if (this.offsetCount != null)
|
|
453
|
+
spec.offset = this.offsetCount;
|
|
454
|
+
return spec;
|
|
455
|
+
}
|
|
456
|
+
handleEvent(raw) {
|
|
457
|
+
if (!raw || typeof raw !== 'object')
|
|
458
|
+
return;
|
|
459
|
+
const event = raw;
|
|
460
|
+
if (event.kind === 'snapshot') {
|
|
461
|
+
this.emit({
|
|
462
|
+
type: 'snapshot',
|
|
463
|
+
table: this.table,
|
|
464
|
+
new: null,
|
|
465
|
+
old: null,
|
|
466
|
+
rows: Array.isArray(event.rows) ? event.rows : [],
|
|
467
|
+
raw,
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (event.kind === 'insert' || event.kind === 'update' || event.kind === 'delete') {
|
|
472
|
+
this.emit({
|
|
473
|
+
type: event.kind,
|
|
474
|
+
table: this.table,
|
|
475
|
+
pk: event.pk == null ? undefined : String(event.pk),
|
|
476
|
+
new: event.row ?? null,
|
|
477
|
+
old: event.previous ?? null,
|
|
478
|
+
changed: Array.isArray(event.changed) ? event.changed : undefined,
|
|
479
|
+
raw,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
emit(event) {
|
|
484
|
+
for (const handler of this.handlers[event.type])
|
|
485
|
+
handler(event);
|
|
486
|
+
if (event.type !== 'snapshot' && event.type !== 'error') {
|
|
487
|
+
for (const handler of this.handlers.change)
|
|
488
|
+
handler(event);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
exports.LuxProjectLiveSubscription = LuxProjectLiveSubscription;
|
|
215
493
|
class LuxProjectInsertBuilder extends LuxProjectThenable {
|
|
216
494
|
constructor(client, tableName, rowOrRows) {
|
|
217
495
|
super();
|
|
@@ -273,6 +551,12 @@ function filtersToWhere(filters) {
|
|
|
273
551
|
return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
|
|
274
552
|
}).join(' AND ');
|
|
275
553
|
}
|
|
554
|
+
function havingToWhere(filters) {
|
|
555
|
+
return filters.map((filter) => {
|
|
556
|
+
const op = filterOperatorToWhere(filter.operator);
|
|
557
|
+
return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
|
|
558
|
+
}).join(' AND ');
|
|
559
|
+
}
|
|
276
560
|
function filterOperatorToWhere(operator) {
|
|
277
561
|
switch (operator) {
|
|
278
562
|
case 'eq':
|
|
@@ -311,3 +595,10 @@ function resolveFetch(fetchImpl) {
|
|
|
311
595
|
}
|
|
312
596
|
return candidate;
|
|
313
597
|
}
|
|
598
|
+
function resolveWebSocket(websocketImpl) {
|
|
599
|
+
const candidate = websocketImpl ?? globalThis.WebSocket;
|
|
600
|
+
if (!candidate) {
|
|
601
|
+
throw new Error('Lux project live subscriptions require a WebSocket implementation');
|
|
602
|
+
}
|
|
603
|
+
return candidate;
|
|
604
|
+
}
|
package/dist/cjs/table.js
CHANGED
|
@@ -59,9 +59,10 @@ class TableSubscription {
|
|
|
59
59
|
try {
|
|
60
60
|
const initial = await this.fetchMatches();
|
|
61
61
|
for (const row of initial) {
|
|
62
|
-
|
|
62
|
+
const id = row.id;
|
|
63
|
+
if (id == null)
|
|
63
64
|
continue;
|
|
64
|
-
this.knownRows.set(String(
|
|
65
|
+
this.knownRows.set(String(id), row);
|
|
65
66
|
}
|
|
66
67
|
const pattern = `_t:${this.table}:row:*`;
|
|
67
68
|
this.unsubscribeFn = await this.client._subscribePattern(pattern, (raw) => {
|
|
@@ -111,7 +112,9 @@ class TableSubscription {
|
|
|
111
112
|
if (!previous || !next)
|
|
112
113
|
return;
|
|
113
114
|
this.knownRows.set(pk, next);
|
|
114
|
-
const
|
|
115
|
+
const previousRow = previous;
|
|
116
|
+
const nextRow = next;
|
|
117
|
+
const changed = Object.keys(nextRow).filter((key) => previousRow[key] !== nextRow[key]);
|
|
115
118
|
this.emitChange({
|
|
116
119
|
type: 'update',
|
|
117
120
|
table: this.table,
|
|
@@ -132,6 +135,8 @@ exports.TableSubscription = TableSubscription;
|
|
|
132
135
|
class TableQueryBuilder {
|
|
133
136
|
constructor(client, name, options) {
|
|
134
137
|
this.conditions = [];
|
|
138
|
+
this.groupFields = [];
|
|
139
|
+
this.havingConditions = [];
|
|
135
140
|
this.selectClause = '*';
|
|
136
141
|
this.expectSingle = false;
|
|
137
142
|
this.client = client;
|
|
@@ -157,7 +162,7 @@ class TableQueryBuilder {
|
|
|
157
162
|
const args = [this.selectClause, 'FROM', this.name];
|
|
158
163
|
const allConditions = extra ? [...this.conditions, ...extra] : this.conditions;
|
|
159
164
|
if (this.joinClause) {
|
|
160
|
-
args.push('JOIN', this.joinClause.table, this.joinClause.alias, 'ON', this.joinClause.onLeft, '=', this.joinClause.onRight);
|
|
165
|
+
args.push(...(this.joinClause.type === 'LEFT' ? ['LEFT', 'JOIN'] : ['JOIN']), this.joinClause.table, this.joinClause.alias, 'ON', this.joinClause.onLeft, '=', this.joinClause.onRight);
|
|
161
166
|
}
|
|
162
167
|
if (allConditions.length) {
|
|
163
168
|
args.push('WHERE');
|
|
@@ -169,6 +174,25 @@ class TableQueryBuilder {
|
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
}
|
|
177
|
+
if (this.groupFields.length) {
|
|
178
|
+
args.push('GROUP', 'BY', ...this.groupFields);
|
|
179
|
+
}
|
|
180
|
+
if (this.havingConditions.length) {
|
|
181
|
+
args.push('HAVING');
|
|
182
|
+
for (let i = 0; i < this.havingConditions.length; i++) {
|
|
183
|
+
const cond = this.havingConditions[i];
|
|
184
|
+
args.push(cond.field, cond.op, String(cond.value));
|
|
185
|
+
if (i < this.havingConditions.length - 1) {
|
|
186
|
+
args.push('AND');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (this.similarityClause) {
|
|
191
|
+
args.push('NEAR', this.similarityClause.field, `[${this.similarityClause.vector.join(',')}]`, 'K', String(this.similarityClause.k));
|
|
192
|
+
if (this.similarityClause.threshold != null) {
|
|
193
|
+
args.push('THRESHOLD', String(this.similarityClause.threshold));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
172
196
|
if (this.orderField) {
|
|
173
197
|
args.push('ORDER', 'BY', this.orderField, this.orderDir || 'ASC');
|
|
174
198
|
}
|
|
@@ -230,68 +254,41 @@ class TableQueryBuilder {
|
|
|
230
254
|
return this;
|
|
231
255
|
}
|
|
232
256
|
join(table, alias, onLeft, onRight) {
|
|
233
|
-
this.joinClause = { table, alias, onLeft, onRight };
|
|
257
|
+
this.joinClause = { type: 'INNER', table, alias, onLeft, onRight };
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
leftJoin(table, alias, onLeft, onRight) {
|
|
261
|
+
this.joinClause = { type: 'LEFT', table, alias, onLeft, onRight };
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
group(fields) {
|
|
265
|
+
this.groupFields = Array.isArray(fields)
|
|
266
|
+
? fields
|
|
267
|
+
: fields.split(',').map((field) => field.trim()).filter(Boolean);
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
groupBy(fields) {
|
|
271
|
+
return this.group(fields);
|
|
272
|
+
}
|
|
273
|
+
having(field, op, value) {
|
|
274
|
+
this.havingConditions.push({ field, op, value });
|
|
234
275
|
return this;
|
|
235
276
|
}
|
|
236
|
-
|
|
277
|
+
near(field, vector, options = {}) {
|
|
237
278
|
this.similarityClause = {
|
|
238
279
|
field,
|
|
239
280
|
vector,
|
|
240
|
-
k: options.k,
|
|
241
|
-
|
|
281
|
+
k: options.k ?? 10,
|
|
282
|
+
threshold: options.threshold,
|
|
242
283
|
};
|
|
243
284
|
return this;
|
|
244
285
|
}
|
|
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;
|
|
286
|
+
similar(field, vector, options = {}) {
|
|
287
|
+
return this.near(field, vector, options);
|
|
263
288
|
}
|
|
264
289
|
async run() {
|
|
265
290
|
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
|
-
}
|
|
291
|
+
const rows = await this.client._tselect(this.buildSelectArgs());
|
|
295
292
|
const validated = rows.map((row) => this.validateRow(row));
|
|
296
293
|
if (this.expectSingle) {
|
|
297
294
|
if (validated.length === 0) {
|
|
@@ -384,12 +381,6 @@ class TableQueryBuilder {
|
|
|
384
381
|
}
|
|
385
382
|
}
|
|
386
383
|
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
384
|
return new TableSubscription(this.client, this.name, (extra) => this.buildSelectArgs(extra));
|
|
394
385
|
}
|
|
395
386
|
}
|