@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/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,69 @@ const { data: deleted, error: deleteError } = await lux
|
|
|
62
96
|
.eq("id", inserted?.id);
|
|
63
97
|
```
|
|
64
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
|
+
|
|
124
|
+
## Live tables
|
|
125
|
+
|
|
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.
|
|
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
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const { live, error } = await lux
|
|
136
|
+
.table<{ id: string; channel_id: string; body: string }>("messages")
|
|
137
|
+
.eq("channel_id", "general")
|
|
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();
|
|
160
|
+
```
|
|
161
|
+
|
|
65
162
|
## OAuth
|
|
66
163
|
|
|
67
164
|
```ts
|
|
@@ -82,6 +179,18 @@ if (error) throw error;
|
|
|
82
179
|
console.log(data.user);
|
|
83
180
|
```
|
|
84
181
|
|
|
182
|
+
Auth types are exported for app code and system table reads:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import type { LuxUser, LuxAuthTables } from "@luxdb/sdk";
|
|
186
|
+
|
|
187
|
+
type AuthUserRow = LuxAuthTables["auth.users"];
|
|
188
|
+
|
|
189
|
+
function renderUser(user: LuxUser, row: AuthUserRow) {
|
|
190
|
+
return row.email ?? user.email;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
85
194
|
## Server client
|
|
86
195
|
|
|
87
196
|
Use a secret key only from trusted server code.
|
|
@@ -129,4 +238,7 @@ const value = await lux.get("hello");
|
|
|
129
238
|
- `lux_pub_...` keys are safe for browser app calls.
|
|
130
239
|
- `lux_sec_...` keys are server-only.
|
|
131
240
|
- User sessions issue JWT access tokens.
|
|
241
|
+
- Browser live subscriptions use the project publishable key plus the signed-in user's JWT.
|
|
242
|
+
- Table `select()` accepts Lux's constrained projection grammar, not arbitrary SQL.
|
|
132
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
|
@@ -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,39 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
|
|
|
150
274
|
is(column, value) {
|
|
151
275
|
return this.addFilter(column, 'is', value);
|
|
152
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
|
+
}
|
|
292
|
+
join(table, alias, onLeft, onRight) {
|
|
293
|
+
this.joins.push({ type: 'inner', table, alias, onLeft, onRight });
|
|
294
|
+
return this;
|
|
295
|
+
}
|
|
296
|
+
leftJoin(table, alias, onLeft, onRight) {
|
|
297
|
+
this.joins.push({ type: 'left', table, alias, onLeft, onRight });
|
|
298
|
+
return this;
|
|
299
|
+
}
|
|
300
|
+
group(columns) {
|
|
301
|
+
this.groupColumns = Array.isArray(columns)
|
|
302
|
+
? columns
|
|
303
|
+
: columns.split(',').map((column) => column.trim()).filter(Boolean);
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
having(column, operator, value) {
|
|
307
|
+
this.havingFilters.push({ column, operator, value });
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
153
310
|
addFilter(column, operator, value) {
|
|
154
311
|
this.filters.push({ column, operator, value });
|
|
155
312
|
return this;
|
|
@@ -158,6 +315,22 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
|
|
|
158
315
|
const params = new URLSearchParams();
|
|
159
316
|
if (this.filters.length)
|
|
160
317
|
params.set('where', filtersToWhere(this.filters));
|
|
318
|
+
for (const join of this.joins) {
|
|
319
|
+
const kind = join.type === 'left' ? ':left' : '';
|
|
320
|
+
params.append('join', `${join.table}:${join.alias}${kind}:on(${join.onLeft}=${join.onRight})`);
|
|
321
|
+
}
|
|
322
|
+
if (this.groupColumns.length)
|
|
323
|
+
params.set('group', this.groupColumns.join(','));
|
|
324
|
+
if (this.havingFilters.length)
|
|
325
|
+
params.set('having', havingToWhere(this.havingFilters));
|
|
326
|
+
if (this.nearQuery) {
|
|
327
|
+
params.set('near_field', this.nearQuery.field);
|
|
328
|
+
params.set('near_vector', `[${this.nearQuery.vector.join(',')}]`);
|
|
329
|
+
params.set('near_k', String(this.nearQuery.k));
|
|
330
|
+
if (this.nearQuery.threshold != null) {
|
|
331
|
+
params.set('near_threshold', String(this.nearQuery.threshold));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
161
334
|
if (this.orderBy) {
|
|
162
335
|
params.set('order', `${this.orderBy.column} ${this.orderBy.ascending ? 'ASC' : 'DESC'}`);
|
|
163
336
|
}
|
|
@@ -178,6 +351,15 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
|
|
|
178
351
|
this.orderBy = { column, ascending: options.ascending ?? true };
|
|
179
352
|
return this;
|
|
180
353
|
}
|
|
354
|
+
near(column, vector, options = {}) {
|
|
355
|
+
this.nearQuery = {
|
|
356
|
+
field: column,
|
|
357
|
+
vector,
|
|
358
|
+
k: options.k ?? 10,
|
|
359
|
+
threshold: options.threshold,
|
|
360
|
+
};
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
181
363
|
limit(count) {
|
|
182
364
|
this.limitCount = count;
|
|
183
365
|
return this;
|
|
@@ -210,8 +392,202 @@ class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
|
|
|
210
392
|
}
|
|
211
393
|
return (0, utils_1.ok)(rows[0]);
|
|
212
394
|
}
|
|
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 };
|
|
403
|
+
}
|
|
213
404
|
}
|
|
214
405
|
exports.LuxProjectSelectBuilder = LuxProjectSelectBuilder;
|
|
406
|
+
class LuxProjectLiveSubscription {
|
|
407
|
+
constructor(client, table, columns, filters, nearQuery, orderBy, limitCount, offsetCount) {
|
|
408
|
+
this.client = client;
|
|
409
|
+
this.table = table;
|
|
410
|
+
this.columns = columns;
|
|
411
|
+
this.filters = filters;
|
|
412
|
+
this.nearQuery = nearQuery;
|
|
413
|
+
this.orderBy = orderBy;
|
|
414
|
+
this.limitCount = limitCount;
|
|
415
|
+
this.offsetCount = offsetCount;
|
|
416
|
+
this.handlers = {
|
|
417
|
+
snapshot: [],
|
|
418
|
+
insert: [],
|
|
419
|
+
update: [],
|
|
420
|
+
delete: [],
|
|
421
|
+
error: [],
|
|
422
|
+
change: [],
|
|
423
|
+
};
|
|
424
|
+
this.unsubscribeFn = null;
|
|
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;
|
|
430
|
+
}
|
|
431
|
+
on(type, handler) {
|
|
432
|
+
this.handlers[type].push(handler);
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
async unsubscribe() {
|
|
436
|
+
this.unsubscribeFn?.();
|
|
437
|
+
this.unsubscribeFn = null;
|
|
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
|
+
*/
|
|
447
|
+
async start() {
|
|
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
|
+
}
|
|
508
|
+
}
|
|
509
|
+
spec() {
|
|
510
|
+
const spec = {
|
|
511
|
+
kind: 'table',
|
|
512
|
+
table: this.table,
|
|
513
|
+
select: this.columns,
|
|
514
|
+
};
|
|
515
|
+
if (this.filters.length) {
|
|
516
|
+
spec.where = this.filters.map((filter) => ({
|
|
517
|
+
field: filter.column,
|
|
518
|
+
op: filterOperatorToWhere(filter.operator),
|
|
519
|
+
value: filter.value,
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
if (this.nearQuery) {
|
|
523
|
+
spec.near = {
|
|
524
|
+
field: this.nearQuery.field,
|
|
525
|
+
vector: this.nearQuery.vector,
|
|
526
|
+
k: this.nearQuery.k,
|
|
527
|
+
threshold: this.nearQuery.threshold,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (this.orderBy) {
|
|
531
|
+
spec.orderBy = {
|
|
532
|
+
field: this.orderBy.column,
|
|
533
|
+
dir: this.orderBy.ascending ? 'asc' : 'desc',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (this.limitCount != null)
|
|
537
|
+
spec.limit = this.limitCount;
|
|
538
|
+
if (this.offsetCount != null)
|
|
539
|
+
spec.offset = this.offsetCount;
|
|
540
|
+
return spec;
|
|
541
|
+
}
|
|
542
|
+
handleEvent(raw) {
|
|
543
|
+
if (!raw || typeof raw !== 'object')
|
|
544
|
+
return;
|
|
545
|
+
const event = raw;
|
|
546
|
+
if (event.kind === 'snapshot') {
|
|
547
|
+
this.emit({
|
|
548
|
+
type: 'snapshot',
|
|
549
|
+
table: this.table,
|
|
550
|
+
new: null,
|
|
551
|
+
old: null,
|
|
552
|
+
rows: Array.isArray(event.rows) ? event.rows : [],
|
|
553
|
+
raw,
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (event.kind === 'insert' || event.kind === 'update' || event.kind === 'delete') {
|
|
558
|
+
this.emit({
|
|
559
|
+
type: event.kind,
|
|
560
|
+
table: this.table,
|
|
561
|
+
pk: event.pk == null ? undefined : String(event.pk),
|
|
562
|
+
new: event.row ?? null,
|
|
563
|
+
old: event.previous ?? null,
|
|
564
|
+
changed: Array.isArray(event.changed) ? event.changed : undefined,
|
|
565
|
+
raw,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
emit(event) {
|
|
570
|
+
for (const handler of this.handlers[event.type])
|
|
571
|
+
handler(event);
|
|
572
|
+
if (event.type !== 'snapshot' && event.type !== 'error') {
|
|
573
|
+
for (const handler of this.handlers.change)
|
|
574
|
+
handler(event);
|
|
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);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
exports.LuxProjectLiveSubscription = LuxProjectLiveSubscription;
|
|
215
591
|
class LuxProjectInsertBuilder extends LuxProjectThenable {
|
|
216
592
|
constructor(client, tableName, rowOrRows) {
|
|
217
593
|
super();
|
|
@@ -268,6 +644,19 @@ function normalizeWhere(where) {
|
|
|
268
644
|
return where.trim().replace(/\s*(>=|<=|!=|=|>|<)\s*/g, ' $1 ');
|
|
269
645
|
}
|
|
270
646
|
function filtersToWhere(filters) {
|
|
647
|
+
return filters.map((filter) => {
|
|
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
|
+
}
|
|
656
|
+
return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
|
|
657
|
+
}).join(' AND ');
|
|
658
|
+
}
|
|
659
|
+
function havingToWhere(filters) {
|
|
271
660
|
return filters.map((filter) => {
|
|
272
661
|
const op = filterOperatorToWhere(filter.operator);
|
|
273
662
|
return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
|
|
@@ -288,6 +677,16 @@ function filterOperatorToWhere(operator) {
|
|
|
288
677
|
return '<';
|
|
289
678
|
case 'lte':
|
|
290
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';
|
|
291
690
|
}
|
|
292
691
|
}
|
|
293
692
|
function formatWhereValue(value) {
|
|
@@ -311,3 +710,10 @@ function resolveFetch(fetchImpl) {
|
|
|
311
710
|
}
|
|
312
711
|
return candidate;
|
|
313
712
|
}
|
|
713
|
+
function resolveWebSocket(websocketImpl) {
|
|
714
|
+
const candidate = websocketImpl ?? globalThis.WebSocket;
|
|
715
|
+
if (!candidate) {
|
|
716
|
+
throw new Error('Lux project live subscriptions require a WebSocket implementation');
|
|
717
|
+
}
|
|
718
|
+
return candidate;
|
|
719
|
+
}
|