@luxdb/sdk 1.4.1 → 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 +205 -0
- package/dist/cjs/index.js +1 -4
- package/dist/cjs/project.js +292 -1
- package/dist/cjs/table.js +52 -61
- package/dist/esm/index.js +1 -4
- 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 +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# @luxdb/sdk
|
|
2
|
+
|
|
3
|
+
Official TypeScript SDK for Lux.
|
|
4
|
+
|
|
5
|
+
Use the project client for browser, server, and SSR app code. Use the direct client when you want low-level Redis-compatible access to a Lux instance.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun i @luxdb/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Browser app client
|
|
14
|
+
|
|
15
|
+
Use a publishable key in browser code. The browser client persists auth sessions in browser storage by default.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createBrowserClient } from "@luxdb/sdk";
|
|
19
|
+
|
|
20
|
+
const lux = createBrowserClient(
|
|
21
|
+
"https://api.luxdb.dev/v1/my-project",
|
|
22
|
+
"lux_pub_..."
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const { data: session, error } = await lux.auth.signInWithPassword({
|
|
26
|
+
email: "user@example.com",
|
|
27
|
+
password: "correct horse battery staple",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (error) throw error;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Tables
|
|
34
|
+
|
|
35
|
+
Queries and mutations return a Supabase-style result object:
|
|
36
|
+
|
|
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
|
+
|
|
50
|
+
const { data: users, error } = await lux
|
|
51
|
+
.table<User[]>("users")
|
|
52
|
+
.select()
|
|
53
|
+
.gt("age", 25)
|
|
54
|
+
.order("age", { ascending: false })
|
|
55
|
+
.limit(10);
|
|
56
|
+
|
|
57
|
+
if (error) throw error;
|
|
58
|
+
console.log(users);
|
|
59
|
+
```
|
|
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
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const { data: inserted, error: insertError } = await lux
|
|
85
|
+
.table("messages")
|
|
86
|
+
.insert({ body: "hello", channel: "general" });
|
|
87
|
+
|
|
88
|
+
const { data: updated, error: updateError } = await lux
|
|
89
|
+
.table("messages")
|
|
90
|
+
.update({ body: "edited" })
|
|
91
|
+
.eq("id", inserted?.id);
|
|
92
|
+
|
|
93
|
+
const { data: deleted, error: deleteError } = await lux
|
|
94
|
+
.table("messages")
|
|
95
|
+
.delete()
|
|
96
|
+
.eq("id", inserted?.id);
|
|
97
|
+
```
|
|
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
|
+
|
|
124
|
+
## OAuth
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const { data, error } = await lux.auth.signInWithOAuth({
|
|
128
|
+
provider: "google",
|
|
129
|
+
redirectTo: "https://app.example.com/auth/callback",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (error) throw error;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
On your callback page:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const { data, error } = await lux.auth.consumeOAuthRedirect();
|
|
139
|
+
|
|
140
|
+
if (error) throw error;
|
|
141
|
+
console.log(data.user);
|
|
142
|
+
```
|
|
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
|
+
|
|
156
|
+
## Server client
|
|
157
|
+
|
|
158
|
+
Use a secret key only from trusted server code.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { createClient } from "@luxdb/sdk";
|
|
162
|
+
|
|
163
|
+
const admin = createClient(
|
|
164
|
+
"https://api.luxdb.dev/v1/my-project",
|
|
165
|
+
process.env.LUX_SECRET_KEY!
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const { data: users, error } = await admin.auth.listUsers();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## SSR client
|
|
172
|
+
|
|
173
|
+
Use `createServerClient` with your framework's cookie methods to persist sessions on the server.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { createServerClient } from "@luxdb/sdk";
|
|
177
|
+
|
|
178
|
+
const lux = createServerClient(
|
|
179
|
+
"https://api.luxdb.dev/v1/my-project",
|
|
180
|
+
"lux_pub_...",
|
|
181
|
+
{ cookies }
|
|
182
|
+
);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Direct Lux/Redis-compatible access
|
|
186
|
+
|
|
187
|
+
Use direct access for trusted infrastructure that needs RESP commands, low-level primitives, or compatibility with Redis workflows. Do not ship database passwords to browsers.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import Lux from "@luxdb/sdk";
|
|
191
|
+
|
|
192
|
+
const lux = new Lux("lux://:password@localhost:6379");
|
|
193
|
+
|
|
194
|
+
await lux.set("hello", "world");
|
|
195
|
+
const value = await lux.get("hello");
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Access model
|
|
199
|
+
|
|
200
|
+
- `lux_pub_...` keys are safe for browser app calls.
|
|
201
|
+
- `lux_sec_...` keys are server-only.
|
|
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.
|
|
205
|
+
- Direct `lux://` or `rediss://` database access uses the database password and is for trusted infrastructure.
|
package/dist/cjs/index.js
CHANGED
|
@@ -44,10 +44,7 @@ class Lux extends ioredis_1.default {
|
|
|
44
44
|
constructor(options) {
|
|
45
45
|
let authOptions = {};
|
|
46
46
|
if (typeof options === 'string') {
|
|
47
|
-
|
|
48
|
-
throw new Error('TLS is not yet supported');
|
|
49
|
-
}
|
|
50
|
-
options = options.replace(/^lux:\/\//, 'redis://');
|
|
47
|
+
options = options.replace(/^luxs:\/\//, 'rediss://').replace(/^lux:\/\//, 'redis://');
|
|
51
48
|
}
|
|
52
49
|
else if (options) {
|
|
53
50
|
const { httpUrl, apiKey, authToken, fetch: fetchImpl, ...redisOptions } = options;
|
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
|
+
}
|