@luxdb/sdk 1.3.0 → 1.4.2
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 +132 -0
- package/dist/cjs/auth.js +504 -0
- package/dist/cjs/browser.js +14 -0
- package/dist/{index.js → cjs/index.js} +46 -7
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/project.js +313 -0
- package/dist/cjs/ssr.js +40 -0
- package/dist/{table.js → cjs/table.js} +55 -43
- package/dist/esm/auth.js +500 -0
- package/dist/esm/browser.js +11 -0
- package/dist/esm/index.js +270 -0
- package/dist/esm/namespaces.js +41 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/project.js +303 -0
- package/dist/esm/realtime.js +84 -0
- package/dist/esm/ssr.js +37 -0
- package/dist/esm/table.js +391 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/utils.js +12 -0
- package/dist/types/auth.d.ts +163 -0
- package/dist/types/browser.d.ts +4 -0
- package/dist/{index.d.ts → types/index.d.ts} +17 -2
- package/dist/types/project.d.ts +121 -0
- package/dist/types/ssr.d.ts +22 -0
- package/dist/{table.d.ts → types/table.d.ts} +11 -2
- package/package.json +40 -6
- /package/dist/{namespaces.js → cjs/namespaces.js} +0 -0
- /package/dist/{realtime.js → cjs/realtime.js} +0 -0
- /package/dist/{types.js → cjs/types.js} +0 -0
- /package/dist/{utils.js → cjs/utils.js} +0 -0
- /package/dist/{namespaces.d.ts → types/namespaces.d.ts} +0 -0
- /package/dist/{realtime.d.ts → types/realtime.d.ts} +0 -0
- /package/dist/{types.d.ts → types/types.d.ts} +0 -0
- /package/dist/{utils.d.ts → types/utils.d.ts} +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import { LuxAuthClient } from './auth.js';
|
|
3
|
+
import { createProjectClient, LuxProjectClient } from './project.js';
|
|
4
|
+
import { TimeSeriesNamespace, VectorNamespace } from './namespaces.js';
|
|
5
|
+
import { LuxRealtimeManager } from './realtime.js';
|
|
6
|
+
import { TableQueryBuilder } from './table.js';
|
|
7
|
+
export { createProjectClient, LuxProjectClient, };
|
|
8
|
+
export { createBrowserClient } from './browser.js';
|
|
9
|
+
export { createServerClient } from './ssr.js';
|
|
10
|
+
export { TableQueryBuilder, TableSubscription } from './table.js';
|
|
11
|
+
function createAuthNamespace(redis, options) {
|
|
12
|
+
const client = new LuxAuthClient(options);
|
|
13
|
+
const redisAuth = ((...args) => {
|
|
14
|
+
return redis.call('AUTH', ...args);
|
|
15
|
+
});
|
|
16
|
+
return new Proxy(redisAuth, {
|
|
17
|
+
get(target, prop, receiver) {
|
|
18
|
+
if (prop in client) {
|
|
19
|
+
const value = client[prop];
|
|
20
|
+
return typeof value === 'function' ? value.bind(client) : value;
|
|
21
|
+
}
|
|
22
|
+
return Reflect.get(target, prop, receiver);
|
|
23
|
+
},
|
|
24
|
+
set(_target, prop, value) {
|
|
25
|
+
client[prop] = value;
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export class Lux extends Redis {
|
|
31
|
+
constructor(options) {
|
|
32
|
+
let authOptions = {};
|
|
33
|
+
if (typeof options === 'string') {
|
|
34
|
+
options = options.replace(/^luxs:\/\//, 'rediss://').replace(/^lux:\/\//, 'redis://');
|
|
35
|
+
}
|
|
36
|
+
else if (options) {
|
|
37
|
+
const { httpUrl, apiKey, authToken, fetch: fetchImpl, ...redisOptions } = options;
|
|
38
|
+
authOptions = { httpUrl, apiKey, authToken, fetch: fetchImpl };
|
|
39
|
+
options = redisOptions;
|
|
40
|
+
}
|
|
41
|
+
super(options);
|
|
42
|
+
this.vectors = new VectorNamespace(this);
|
|
43
|
+
this.timeseries = new TimeSeriesNamespace(this);
|
|
44
|
+
this.auth = createAuthNamespace(this, authOptions);
|
|
45
|
+
this.authApi = this.auth;
|
|
46
|
+
}
|
|
47
|
+
table(name, options) {
|
|
48
|
+
return new TableQueryBuilder(this, name, options);
|
|
49
|
+
}
|
|
50
|
+
async _subscribePattern(pattern, handler) {
|
|
51
|
+
if (!this.realtimeManager) {
|
|
52
|
+
this.realtimeManager = new LuxRealtimeManager(this);
|
|
53
|
+
}
|
|
54
|
+
return this.realtimeManager.subscribe(pattern, handler);
|
|
55
|
+
}
|
|
56
|
+
async _tselect(args) {
|
|
57
|
+
const result = await this.call('TSELECT', ...args);
|
|
58
|
+
if (!result || !Array.isArray(result))
|
|
59
|
+
return [];
|
|
60
|
+
const rows = [];
|
|
61
|
+
for (const item of result) {
|
|
62
|
+
if (Array.isArray(item)) {
|
|
63
|
+
const row = {};
|
|
64
|
+
for (let i = 0; i < item.length - 1; i += 2) {
|
|
65
|
+
const key = String(item[i]);
|
|
66
|
+
const val = item[i + 1];
|
|
67
|
+
row[key] = val;
|
|
68
|
+
}
|
|
69
|
+
if (row.id != null) {
|
|
70
|
+
const parsed = Number(row.id);
|
|
71
|
+
if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
|
|
72
|
+
row.id = parsed;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
rows.push(row);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return rows;
|
|
79
|
+
}
|
|
80
|
+
// Vector methods (keep for backward compat)
|
|
81
|
+
async vset(key, vector, options) {
|
|
82
|
+
const args = [key, vector.length, ...vector];
|
|
83
|
+
if (options?.metadata) {
|
|
84
|
+
args.push('META', JSON.stringify(options.metadata));
|
|
85
|
+
}
|
|
86
|
+
if (options?.ex) {
|
|
87
|
+
args.push('EX', options.ex);
|
|
88
|
+
}
|
|
89
|
+
else if (options?.px) {
|
|
90
|
+
args.push('PX', options.px);
|
|
91
|
+
}
|
|
92
|
+
return this.call('VSET', ...args);
|
|
93
|
+
}
|
|
94
|
+
async vget(key) {
|
|
95
|
+
const result = await this.call('VGET', key);
|
|
96
|
+
if (!result || !Array.isArray(result))
|
|
97
|
+
return null;
|
|
98
|
+
const dims = parseInt(result[0], 10);
|
|
99
|
+
const vector = [];
|
|
100
|
+
for (let i = 1; i <= dims; i++) {
|
|
101
|
+
vector.push(parseFloat(result[i]));
|
|
102
|
+
}
|
|
103
|
+
const metaRaw = result[dims + 1];
|
|
104
|
+
let metadata;
|
|
105
|
+
if (metaRaw) {
|
|
106
|
+
try {
|
|
107
|
+
metadata = JSON.parse(metaRaw);
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
}
|
|
111
|
+
return { dims, vector, metadata };
|
|
112
|
+
}
|
|
113
|
+
async vsearch(query, options) {
|
|
114
|
+
const args = [query.length, ...query, 'K', options.k];
|
|
115
|
+
if (options.filter) {
|
|
116
|
+
args.push('FILTER', options.filter.key, options.filter.value);
|
|
117
|
+
}
|
|
118
|
+
if (options.meta) {
|
|
119
|
+
args.push('META');
|
|
120
|
+
}
|
|
121
|
+
const result = await this.call('VSEARCH', ...args);
|
|
122
|
+
if (!result || !Array.isArray(result))
|
|
123
|
+
return [];
|
|
124
|
+
const results = [];
|
|
125
|
+
for (const item of result) {
|
|
126
|
+
if (Array.isArray(item)) {
|
|
127
|
+
const entry = { key: item[0], similarity: parseFloat(item[1]) };
|
|
128
|
+
if (options.meta && item[2]) {
|
|
129
|
+
try {
|
|
130
|
+
entry.metadata = JSON.parse(item[2]);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
entry.metadata = { _raw: item[2] };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
results.push(entry);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
141
|
+
async vcard() {
|
|
142
|
+
return this.call('VCARD');
|
|
143
|
+
}
|
|
144
|
+
// Time series methods (keep for backward compat)
|
|
145
|
+
async tsadd(key, timestamp, value, options) {
|
|
146
|
+
const args = [key, timestamp === '*' ? '*' : timestamp, value];
|
|
147
|
+
if (options?.retention != null) {
|
|
148
|
+
args.push('RETENTION', options.retention);
|
|
149
|
+
}
|
|
150
|
+
if (options?.labels) {
|
|
151
|
+
args.push('LABELS');
|
|
152
|
+
for (const [k, v] of Object.entries(options.labels)) {
|
|
153
|
+
args.push(k, v);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return this.call('TSADD', ...args);
|
|
157
|
+
}
|
|
158
|
+
async tsmadd(...entries) {
|
|
159
|
+
const args = [];
|
|
160
|
+
for (const [key, ts, val] of entries) {
|
|
161
|
+
args.push(key, ts === '*' ? '*' : ts, val);
|
|
162
|
+
}
|
|
163
|
+
return this.call('TSMADD', ...args);
|
|
164
|
+
}
|
|
165
|
+
async tsget(key) {
|
|
166
|
+
const result = await this.call('TSGET', key);
|
|
167
|
+
if (!result || !Array.isArray(result) || result.length < 2)
|
|
168
|
+
return null;
|
|
169
|
+
return { timestamp: parseInt(result[0], 10), value: parseFloat(result[1]) };
|
|
170
|
+
}
|
|
171
|
+
async tsrange(key, from, to, options) {
|
|
172
|
+
const args = [key, from === '-' ? '-' : from, to === '+' ? '+' : to];
|
|
173
|
+
if (options?.aggregation) {
|
|
174
|
+
args.push('AGGREGATION', options.aggregation.type, options.aggregation.bucketSize);
|
|
175
|
+
}
|
|
176
|
+
const result = await this.call('TSRANGE', ...args);
|
|
177
|
+
if (!result || !Array.isArray(result))
|
|
178
|
+
return [];
|
|
179
|
+
return result.map((pair) => ({ timestamp: parseInt(pair[0], 10), value: parseFloat(pair[1]) }));
|
|
180
|
+
}
|
|
181
|
+
async tsmrange(from, to, filter, options) {
|
|
182
|
+
const args = [from === '-' ? '-' : from, to === '+' ? '+' : to];
|
|
183
|
+
if (options?.aggregation) {
|
|
184
|
+
args.push('AGGREGATION', options.aggregation.type, options.aggregation.bucketSize);
|
|
185
|
+
}
|
|
186
|
+
args.push('FILTER', filter);
|
|
187
|
+
const result = await this.call('TSMRANGE', ...args);
|
|
188
|
+
if (!result || !Array.isArray(result))
|
|
189
|
+
return [];
|
|
190
|
+
return result.map((series) => {
|
|
191
|
+
const labels = {};
|
|
192
|
+
if (Array.isArray(series[1])) {
|
|
193
|
+
for (const pair of series[1]) {
|
|
194
|
+
if (Array.isArray(pair) && pair.length >= 2)
|
|
195
|
+
labels[pair[0]] = pair[1];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const samples = Array.isArray(series[2])
|
|
199
|
+
? series[2].map((s) => ({ timestamp: parseInt(s[0], 10), value: parseFloat(s[1]) }))
|
|
200
|
+
: [];
|
|
201
|
+
return { key: series[0], labels, samples };
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async tsinfo(key) {
|
|
205
|
+
const result = await this.call('TSINFO', key);
|
|
206
|
+
if (!result || !Array.isArray(result))
|
|
207
|
+
return {};
|
|
208
|
+
const info = {};
|
|
209
|
+
for (let i = 0; i < result.length - 1; i += 2) {
|
|
210
|
+
const k = result[i];
|
|
211
|
+
const v = result[i + 1];
|
|
212
|
+
if (k === 'labels' && Array.isArray(v)) {
|
|
213
|
+
const labels = {};
|
|
214
|
+
for (const pair of v) {
|
|
215
|
+
if (Array.isArray(pair) && pair.length >= 2)
|
|
216
|
+
labels[pair[0]] = pair[1];
|
|
217
|
+
}
|
|
218
|
+
info[k] = labels;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
info[k] = v;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return info;
|
|
225
|
+
}
|
|
226
|
+
// Realtime key subscriptions
|
|
227
|
+
ksub(patterns, handler) {
|
|
228
|
+
const sub = this.duplicate();
|
|
229
|
+
sub.on('error', () => { });
|
|
230
|
+
const dataHandler = sub._dataHandler || sub.dataHandler;
|
|
231
|
+
if (dataHandler && dataHandler.returnReply) {
|
|
232
|
+
const origReturn = dataHandler.returnReply.bind(dataHandler);
|
|
233
|
+
dataHandler.returnReply = (reply) => {
|
|
234
|
+
if (Array.isArray(reply) && reply.length === 4 && reply[0] === 'kmessage') {
|
|
235
|
+
handler({ pattern: reply[1], key: reply[2], operation: reply[3] });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
return origReturn(reply);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const origEmit = sub.emit.bind(sub);
|
|
243
|
+
sub.emit = (event, ...args) => {
|
|
244
|
+
if (event === 'error' && args[0]?.message?.includes('Command queue state error')) {
|
|
245
|
+
const match = args[0].message.match(/Last reply: kmessage,([^,]+),([^,]+),(.+)/);
|
|
246
|
+
if (match) {
|
|
247
|
+
handler({ pattern: match[1], key: match[2], operation: match[3] });
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return origEmit(event, ...args);
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
sub.call('KSUB', ...patterns);
|
|
255
|
+
return {
|
|
256
|
+
connection: sub,
|
|
257
|
+
unsubscribe() { sub.disconnect(); },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export function createClient(optionsOrUrl, key, projectOptions) {
|
|
262
|
+
if (typeof optionsOrUrl === 'string' && typeof key === 'string') {
|
|
263
|
+
return createProjectClient({ ...(projectOptions ?? {}), url: optionsOrUrl, key });
|
|
264
|
+
}
|
|
265
|
+
return new Lux(optionsOrUrl);
|
|
266
|
+
}
|
|
267
|
+
export function createAuthClient(options) {
|
|
268
|
+
return new LuxAuthClient(options);
|
|
269
|
+
}
|
|
270
|
+
export default Lux;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class VectorNamespace {
|
|
2
|
+
constructor(client) {
|
|
3
|
+
this.client = client;
|
|
4
|
+
}
|
|
5
|
+
async set(key, vector, metadata) {
|
|
6
|
+
const args = [key, vector.length, ...vector];
|
|
7
|
+
if (metadata) {
|
|
8
|
+
args.push('META', JSON.stringify(metadata));
|
|
9
|
+
}
|
|
10
|
+
return this.client.call('VSET', ...args);
|
|
11
|
+
}
|
|
12
|
+
async get(key) {
|
|
13
|
+
return this.client.vget(key);
|
|
14
|
+
}
|
|
15
|
+
async search(query, options) {
|
|
16
|
+
return this.client.vsearch(query, { k: options.topK, filter: options.filter, meta: options.meta ?? true });
|
|
17
|
+
}
|
|
18
|
+
async count() {
|
|
19
|
+
return this.client.vcard();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class TimeSeriesNamespace {
|
|
23
|
+
constructor(client) {
|
|
24
|
+
this.client = client;
|
|
25
|
+
}
|
|
26
|
+
async add(key, value, options) {
|
|
27
|
+
return this.client.tsadd(key, options?.timestamp ?? '*', value, { retention: options?.retention, labels: options?.labels });
|
|
28
|
+
}
|
|
29
|
+
async get(key) {
|
|
30
|
+
return this.client.tsget(key);
|
|
31
|
+
}
|
|
32
|
+
async range(key, from, to, options) {
|
|
33
|
+
return this.client.tsrange(key, from, to, options);
|
|
34
|
+
}
|
|
35
|
+
async mrange(from, to, filter, options) {
|
|
36
|
+
return this.client.tsmrange(from, to, filter, options);
|
|
37
|
+
}
|
|
38
|
+
async info(key) {
|
|
39
|
+
return this.client.tsinfo(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { LuxAuthClient } from './auth.js';
|
|
2
|
+
import { err, ok, toLuxError } from './utils.js';
|
|
3
|
+
export class LuxProjectClient {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.url = options.url.replace(/\/+$/, '');
|
|
6
|
+
this.key = options.key;
|
|
7
|
+
this.fetchImpl = resolveFetch(options.fetch);
|
|
8
|
+
this.auth = new LuxAuthClient({
|
|
9
|
+
...options.auth,
|
|
10
|
+
httpUrl: this.url,
|
|
11
|
+
apiKey: this.key,
|
|
12
|
+
fetch: this.fetchImpl,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
table(name) {
|
|
16
|
+
return new LuxProjectTable(this, name);
|
|
17
|
+
}
|
|
18
|
+
async ping() {
|
|
19
|
+
return this.request('GET', '/ping');
|
|
20
|
+
}
|
|
21
|
+
async createTable(name, columns) {
|
|
22
|
+
return this.request('POST', '/tables', { name, columns });
|
|
23
|
+
}
|
|
24
|
+
async exec(command) {
|
|
25
|
+
return this.request('POST', '/exec', { command });
|
|
26
|
+
}
|
|
27
|
+
async vectorSet(key, vector, metadata) {
|
|
28
|
+
return this.request('POST', `/vectors/${encodeURIComponent(key)}`, { vector, metadata });
|
|
29
|
+
}
|
|
30
|
+
async vectorSearch(options) {
|
|
31
|
+
return this.request('POST', '/vectors/search', {
|
|
32
|
+
vector: options.vector,
|
|
33
|
+
k: options.k ?? 10,
|
|
34
|
+
filter: options.filter,
|
|
35
|
+
filter_value: options.filter_value,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async tsAdd(key, value, options) {
|
|
39
|
+
return this.request('POST', `/ts/${encodeURIComponent(key)}`, {
|
|
40
|
+
timestamp: options?.timestamp ?? '*',
|
|
41
|
+
value,
|
|
42
|
+
labels: options?.labels,
|
|
43
|
+
retention: options?.retention,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async tsRange(key, options) {
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
if (options?.from != null)
|
|
49
|
+
params.set('from', String(options.from));
|
|
50
|
+
if (options?.to != null)
|
|
51
|
+
params.set('to', String(options.to));
|
|
52
|
+
if (options?.count != null)
|
|
53
|
+
params.set('count', String(options.count));
|
|
54
|
+
const query = params.toString();
|
|
55
|
+
return this.request('GET', `/ts/${encodeURIComponent(key)}${query ? `?${query}` : ''}`);
|
|
56
|
+
}
|
|
57
|
+
async request(method, path, body) {
|
|
58
|
+
try {
|
|
59
|
+
const accessToken = await this.auth.getAccessToken();
|
|
60
|
+
const headers = {
|
|
61
|
+
Accept: 'application/json',
|
|
62
|
+
apikey: this.key,
|
|
63
|
+
Authorization: `Bearer ${accessToken ?? this.key}`,
|
|
64
|
+
};
|
|
65
|
+
const init = { method, headers };
|
|
66
|
+
if (body !== undefined) {
|
|
67
|
+
headers['Content-Type'] = 'application/json';
|
|
68
|
+
init.body = JSON.stringify(body);
|
|
69
|
+
}
|
|
70
|
+
const response = await this.fetchImpl(`${this.url}${path}`, init);
|
|
71
|
+
const text = await response.text();
|
|
72
|
+
const payload = text ? JSON.parse(text) : {};
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
return err('LUX_PROJECT_REQUEST_ERROR', payload?.error || `Lux request failed with HTTP ${response.status}`, { status: response.status, payload });
|
|
75
|
+
}
|
|
76
|
+
return ok(payload);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return err('LUX_PROJECT_REQUEST_ERROR', 'Lux request failed', toLuxError(error));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export class LuxProjectTable {
|
|
84
|
+
constructor(client, name) {
|
|
85
|
+
this.client = client;
|
|
86
|
+
this.name = name;
|
|
87
|
+
}
|
|
88
|
+
select(columns = '*') {
|
|
89
|
+
return new LuxProjectSelectBuilder(this.client, this.name, columns);
|
|
90
|
+
}
|
|
91
|
+
insert(rowOrRows) {
|
|
92
|
+
return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows);
|
|
93
|
+
}
|
|
94
|
+
update(patch) {
|
|
95
|
+
return new LuxProjectMutationBuilder(this.client, this.name, 'PATCH', patch);
|
|
96
|
+
}
|
|
97
|
+
delete() {
|
|
98
|
+
return new LuxProjectMutationBuilder(this.client, this.name, 'DELETE');
|
|
99
|
+
}
|
|
100
|
+
async count() {
|
|
101
|
+
const result = await this.client.request('GET', `/tables/${encodeURIComponent(this.name)}/count`);
|
|
102
|
+
if (result.error)
|
|
103
|
+
return result;
|
|
104
|
+
return ok(unwrapResult(result.data) ?? 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
class LuxProjectThenable {
|
|
108
|
+
then(onfulfilled, onrejected) {
|
|
109
|
+
return this.execute().then(onfulfilled, onrejected);
|
|
110
|
+
}
|
|
111
|
+
catch(onrejected) {
|
|
112
|
+
return this.execute().catch(onrejected);
|
|
113
|
+
}
|
|
114
|
+
finally(onfinally) {
|
|
115
|
+
return this.execute().finally(onfinally ?? undefined);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
class LuxProjectFilterBuilder extends LuxProjectThenable {
|
|
119
|
+
constructor(client, tableName) {
|
|
120
|
+
super();
|
|
121
|
+
this.client = client;
|
|
122
|
+
this.tableName = tableName;
|
|
123
|
+
this.filters = [];
|
|
124
|
+
}
|
|
125
|
+
eq(column, value) {
|
|
126
|
+
return this.addFilter(column, 'eq', value);
|
|
127
|
+
}
|
|
128
|
+
neq(column, value) {
|
|
129
|
+
return this.addFilter(column, 'neq', value);
|
|
130
|
+
}
|
|
131
|
+
gt(column, value) {
|
|
132
|
+
return this.addFilter(column, 'gt', value);
|
|
133
|
+
}
|
|
134
|
+
gte(column, value) {
|
|
135
|
+
return this.addFilter(column, 'gte', value);
|
|
136
|
+
}
|
|
137
|
+
lt(column, value) {
|
|
138
|
+
return this.addFilter(column, 'lt', value);
|
|
139
|
+
}
|
|
140
|
+
lte(column, value) {
|
|
141
|
+
return this.addFilter(column, 'lte', value);
|
|
142
|
+
}
|
|
143
|
+
is(column, value) {
|
|
144
|
+
return this.addFilter(column, 'is', value);
|
|
145
|
+
}
|
|
146
|
+
addFilter(column, operator, value) {
|
|
147
|
+
this.filters.push({ column, operator, value });
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
filteredQueryParams() {
|
|
151
|
+
const params = new URLSearchParams();
|
|
152
|
+
if (this.filters.length)
|
|
153
|
+
params.set('where', filtersToWhere(this.filters));
|
|
154
|
+
if (this.orderBy) {
|
|
155
|
+
params.set('order', `${this.orderBy.column} ${this.orderBy.ascending ? 'ASC' : 'DESC'}`);
|
|
156
|
+
}
|
|
157
|
+
if (this.limitCount != null)
|
|
158
|
+
params.set('limit', String(this.limitCount));
|
|
159
|
+
if (this.offsetCount != null)
|
|
160
|
+
params.set('offset', String(this.offsetCount));
|
|
161
|
+
return params;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export class LuxProjectSelectBuilder extends LuxProjectFilterBuilder {
|
|
165
|
+
constructor(client, tableName, columns) {
|
|
166
|
+
super(client, tableName);
|
|
167
|
+
this.columns = columns;
|
|
168
|
+
this.expectSingle = false;
|
|
169
|
+
}
|
|
170
|
+
order(column, options = {}) {
|
|
171
|
+
this.orderBy = { column, ascending: options.ascending ?? true };
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
limit(count) {
|
|
175
|
+
this.limitCount = count;
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
range(from, to) {
|
|
179
|
+
this.offsetCount = from;
|
|
180
|
+
this.limitCount = Math.max(0, to - from + 1);
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
single() {
|
|
184
|
+
this.expectSingle = true;
|
|
185
|
+
if (this.limitCount == null)
|
|
186
|
+
this.limitCount = 1;
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
async execute() {
|
|
190
|
+
const params = this.filteredQueryParams();
|
|
191
|
+
if (this.columns && this.columns !== '*')
|
|
192
|
+
params.set('select', this.columns);
|
|
193
|
+
const query = params.toString();
|
|
194
|
+
const result = await this.client.request('GET', `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`);
|
|
195
|
+
if (result.error)
|
|
196
|
+
return result;
|
|
197
|
+
const rows = unwrapRows(result.data);
|
|
198
|
+
if (!this.expectSingle) {
|
|
199
|
+
return ok(rows);
|
|
200
|
+
}
|
|
201
|
+
if (rows.length === 0) {
|
|
202
|
+
return err('NOT_FOUND', `No rows found in table '${this.tableName}'`);
|
|
203
|
+
}
|
|
204
|
+
return ok(rows[0]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export class LuxProjectInsertBuilder extends LuxProjectThenable {
|
|
208
|
+
constructor(client, tableName, rowOrRows) {
|
|
209
|
+
super();
|
|
210
|
+
this.client = client;
|
|
211
|
+
this.tableName = tableName;
|
|
212
|
+
this.rowOrRows = rowOrRows;
|
|
213
|
+
}
|
|
214
|
+
async execute() {
|
|
215
|
+
if (!Array.isArray(this.rowOrRows)) {
|
|
216
|
+
return this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, this.rowOrRows);
|
|
217
|
+
}
|
|
218
|
+
const results = [];
|
|
219
|
+
for (const row of this.rowOrRows) {
|
|
220
|
+
const result = await this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, row);
|
|
221
|
+
if (result.error)
|
|
222
|
+
return result;
|
|
223
|
+
results.push(result.data);
|
|
224
|
+
}
|
|
225
|
+
return ok(results);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export class LuxProjectMutationBuilder extends LuxProjectFilterBuilder {
|
|
229
|
+
constructor(client, tableName, method, body) {
|
|
230
|
+
super(client, tableName);
|
|
231
|
+
this.method = method;
|
|
232
|
+
this.body = body;
|
|
233
|
+
}
|
|
234
|
+
async execute() {
|
|
235
|
+
if (this.filters.length === 0) {
|
|
236
|
+
return err('MISSING_FILTER', `${this.method === 'PATCH' ? 'update' : 'delete'}() requires at least one filter`);
|
|
237
|
+
}
|
|
238
|
+
const params = this.filteredQueryParams();
|
|
239
|
+
const query = params.toString();
|
|
240
|
+
return this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function unwrapRows(payload) {
|
|
244
|
+
if (Array.isArray(payload))
|
|
245
|
+
return payload;
|
|
246
|
+
if (payload && typeof payload === 'object' && Array.isArray(payload.result)) {
|
|
247
|
+
return payload.result;
|
|
248
|
+
}
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
function unwrapResult(payload) {
|
|
252
|
+
if (payload && typeof payload === 'object' && 'result' in payload) {
|
|
253
|
+
return payload.result;
|
|
254
|
+
}
|
|
255
|
+
return payload;
|
|
256
|
+
}
|
|
257
|
+
function normalizeWhere(where) {
|
|
258
|
+
return where.trim().replace(/\s*(>=|<=|!=|=|>|<)\s*/g, ' $1 ');
|
|
259
|
+
}
|
|
260
|
+
function filtersToWhere(filters) {
|
|
261
|
+
return filters.map((filter) => {
|
|
262
|
+
const op = filterOperatorToWhere(filter.operator);
|
|
263
|
+
return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
|
|
264
|
+
}).join(' AND ');
|
|
265
|
+
}
|
|
266
|
+
function filterOperatorToWhere(operator) {
|
|
267
|
+
switch (operator) {
|
|
268
|
+
case 'eq':
|
|
269
|
+
case 'is':
|
|
270
|
+
return '=';
|
|
271
|
+
case 'neq':
|
|
272
|
+
return '!=';
|
|
273
|
+
case 'gt':
|
|
274
|
+
return '>';
|
|
275
|
+
case 'gte':
|
|
276
|
+
return '>=';
|
|
277
|
+
case 'lt':
|
|
278
|
+
return '<';
|
|
279
|
+
case 'lte':
|
|
280
|
+
return '<=';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function formatWhereValue(value) {
|
|
284
|
+
if (value === null)
|
|
285
|
+
return '';
|
|
286
|
+
return String(value);
|
|
287
|
+
}
|
|
288
|
+
export function createProjectClient(options) {
|
|
289
|
+
return new LuxProjectClient(options);
|
|
290
|
+
}
|
|
291
|
+
export function createClient(url, key, options = {}) {
|
|
292
|
+
return new LuxProjectClient({ ...options, url, key });
|
|
293
|
+
}
|
|
294
|
+
function resolveFetch(fetchImpl) {
|
|
295
|
+
const candidate = fetchImpl ?? globalThis.fetch;
|
|
296
|
+
if (!candidate) {
|
|
297
|
+
throw new Error('Lux project client requires a fetch implementation');
|
|
298
|
+
}
|
|
299
|
+
if (typeof globalThis !== 'undefined' && candidate === globalThis.fetch) {
|
|
300
|
+
return candidate.bind(globalThis);
|
|
301
|
+
}
|
|
302
|
+
return candidate;
|
|
303
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export class LuxRealtimeManager {
|
|
2
|
+
constructor(client) {
|
|
3
|
+
this.connection = null;
|
|
4
|
+
this.initPromise = null;
|
|
5
|
+
this.nextHandlerId = 1;
|
|
6
|
+
this.handlersByPattern = new Map();
|
|
7
|
+
this.client = client;
|
|
8
|
+
}
|
|
9
|
+
async ensureConnection() {
|
|
10
|
+
if (this.connection)
|
|
11
|
+
return;
|
|
12
|
+
if (this.initPromise)
|
|
13
|
+
return this.initPromise;
|
|
14
|
+
this.initPromise = (async () => {
|
|
15
|
+
const sub = this.client.duplicate();
|
|
16
|
+
sub.on('error', () => { });
|
|
17
|
+
const dispatch = (event) => {
|
|
18
|
+
const handlers = this.handlersByPattern.get(event.pattern);
|
|
19
|
+
if (!handlers)
|
|
20
|
+
return;
|
|
21
|
+
for (const handler of handlers.values()) {
|
|
22
|
+
handler(event);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const dataHandler = sub._dataHandler || sub.dataHandler;
|
|
26
|
+
if (dataHandler && dataHandler.returnReply) {
|
|
27
|
+
const origReturn = dataHandler.returnReply.bind(dataHandler);
|
|
28
|
+
dataHandler.returnReply = (reply) => {
|
|
29
|
+
if (Array.isArray(reply) && reply.length === 4 && reply[0] === 'kmessage') {
|
|
30
|
+
dispatch({ pattern: reply[1], key: reply[2], operation: reply[3] });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
return origReturn(reply);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const origEmit = sub.emit.bind(sub);
|
|
38
|
+
sub.emit = (event, ...args) => {
|
|
39
|
+
if (event === 'error' && args[0]?.message?.includes('Command queue state error')) {
|
|
40
|
+
const match = args[0].message.match(/Last reply: kmessage,([^,]+),([^,]+),(.+)/);
|
|
41
|
+
if (match) {
|
|
42
|
+
dispatch({ pattern: match[1], key: match[2], operation: match[3] });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return origEmit(event, ...args);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
this.connection = sub;
|
|
50
|
+
})();
|
|
51
|
+
try {
|
|
52
|
+
await this.initPromise;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
this.initPromise = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async subscribe(pattern, handler) {
|
|
59
|
+
await this.ensureConnection();
|
|
60
|
+
const id = this.nextHandlerId++;
|
|
61
|
+
let handlers = this.handlersByPattern.get(pattern);
|
|
62
|
+
const firstForPattern = !handlers;
|
|
63
|
+
if (!handlers) {
|
|
64
|
+
handlers = new Map();
|
|
65
|
+
this.handlersByPattern.set(pattern, handlers);
|
|
66
|
+
}
|
|
67
|
+
handlers.set(id, handler);
|
|
68
|
+
if (firstForPattern && this.connection) {
|
|
69
|
+
await this.connection.call('KSUB', pattern);
|
|
70
|
+
}
|
|
71
|
+
return () => {
|
|
72
|
+
const map = this.handlersByPattern.get(pattern);
|
|
73
|
+
if (!map)
|
|
74
|
+
return;
|
|
75
|
+
map.delete(id);
|
|
76
|
+
if (map.size === 0) {
|
|
77
|
+
this.handlersByPattern.delete(pattern);
|
|
78
|
+
if (this.connection) {
|
|
79
|
+
this.connection.call('KUNSUB', pattern).catch(() => { });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|