get-db9 0.4.1 → 0.6.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 +244 -2
- package/dist/client-B3NUYqOc.d.cts +643 -0
- package/dist/client-B3NUYqOc.d.ts +643 -0
- package/dist/client.cjs +567 -190
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +567 -190
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +575 -191
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +573 -191
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/dist/client-CXv2ZlbR.d.cts +0 -429
- package/dist/client-CXv2ZlbR.d.ts +0 -429
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,8 @@ __export(index_exports, {
|
|
|
35
35
|
Db9Error: () => Db9Error,
|
|
36
36
|
Db9NotFoundError: () => Db9NotFoundError,
|
|
37
37
|
FileCredentialStore: () => FileCredentialStore,
|
|
38
|
+
FsClient: () => FsClient,
|
|
39
|
+
FsError: () => FsError,
|
|
38
40
|
MemoryCredentialStore: () => MemoryCredentialStore,
|
|
39
41
|
createDb9Client: () => createDb9Client,
|
|
40
42
|
defaultCredentialStore: () => defaultCredentialStore,
|
|
@@ -92,6 +94,9 @@ var Db9ConflictError = class extends Db9Error {
|
|
|
92
94
|
};
|
|
93
95
|
|
|
94
96
|
// src/http.ts
|
|
97
|
+
function delay(ms) {
|
|
98
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
99
|
+
}
|
|
95
100
|
function createHttpClient(options) {
|
|
96
101
|
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
97
102
|
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
@@ -100,38 +105,61 @@ function createHttpClient(options) {
|
|
|
100
105
|
if (params) {
|
|
101
106
|
const searchParams = new URLSearchParams();
|
|
102
107
|
for (const [key, value] of Object.entries(params)) {
|
|
103
|
-
if (value !== void 0)
|
|
104
|
-
searchParams.set(key, value);
|
|
105
|
-
}
|
|
108
|
+
if (value !== void 0) searchParams.set(key, value);
|
|
106
109
|
}
|
|
107
110
|
const qs = searchParams.toString();
|
|
108
111
|
if (qs) url += `?${qs}`;
|
|
109
112
|
}
|
|
110
|
-
const
|
|
113
|
+
const reqHeaders = {
|
|
111
114
|
"Content-Type": "application/json",
|
|
112
115
|
...options.headers
|
|
113
116
|
};
|
|
114
|
-
const init = { method, headers };
|
|
117
|
+
const init = { method, headers: reqHeaders };
|
|
115
118
|
if (body !== void 0) {
|
|
116
119
|
init.body = JSON.stringify(body);
|
|
117
120
|
}
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const maxAttempts = Math.min(options.maxRetries ?? 0, 3) + 1;
|
|
122
|
+
const baseDelay = options.retryDelay ?? 1e3;
|
|
123
|
+
let lastError;
|
|
124
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
125
|
+
let timeoutId;
|
|
126
|
+
try {
|
|
127
|
+
const fetchInit = { ...init };
|
|
128
|
+
if (options.timeout) {
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
fetchInit.signal = controller.signal;
|
|
131
|
+
timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
132
|
+
}
|
|
133
|
+
const response = await fetchFn(url, fetchInit);
|
|
134
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
if (response.status >= 500 && attempt < maxAttempts - 1) {
|
|
137
|
+
lastError = await Db9Error.fromResponse(response);
|
|
138
|
+
await delay(baseDelay * Math.pow(2, attempt));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
throw await Db9Error.fromResponse(response);
|
|
142
|
+
}
|
|
143
|
+
if (response.status === 204) return void 0;
|
|
144
|
+
return response.json();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
147
|
+
if (err instanceof TypeError && attempt < maxAttempts - 1) {
|
|
148
|
+
lastError = err;
|
|
149
|
+
await delay(baseDelay * Math.pow(2, attempt));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
124
154
|
}
|
|
125
|
-
|
|
155
|
+
throw lastError;
|
|
126
156
|
}
|
|
127
157
|
async function requestRaw(method, path, body, params, customHeaders) {
|
|
128
158
|
let url = `${baseUrl}${path}`;
|
|
129
159
|
if (params) {
|
|
130
160
|
const searchParams = new URLSearchParams();
|
|
131
161
|
for (const [key, value] of Object.entries(params)) {
|
|
132
|
-
if (value !== void 0)
|
|
133
|
-
searchParams.set(key, value);
|
|
134
|
-
}
|
|
162
|
+
if (value !== void 0) searchParams.set(key, value);
|
|
135
163
|
}
|
|
136
164
|
const qs = searchParams.toString();
|
|
137
165
|
if (qs) url += `?${qs}`;
|
|
@@ -140,15 +168,23 @@ function createHttpClient(options) {
|
|
|
140
168
|
...options.headers,
|
|
141
169
|
...customHeaders
|
|
142
170
|
};
|
|
143
|
-
const
|
|
144
|
-
if (body !== void 0)
|
|
145
|
-
|
|
171
|
+
const fetchInit = { method, headers };
|
|
172
|
+
if (body !== void 0) fetchInit.body = body;
|
|
173
|
+
let timeoutId;
|
|
174
|
+
if (options.timeout) {
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
fetchInit.signal = controller.signal;
|
|
177
|
+
timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
146
178
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetchFn(url, fetchInit);
|
|
181
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
182
|
+
if (!response.ok) throw await Db9Error.fromResponse(response);
|
|
183
|
+
return response;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
186
|
+
throw err;
|
|
150
187
|
}
|
|
151
|
-
return response;
|
|
152
188
|
}
|
|
153
189
|
return {
|
|
154
190
|
get: (path, params) => request("GET", path, void 0, params),
|
|
@@ -156,7 +192,9 @@ function createHttpClient(options) {
|
|
|
156
192
|
put: (path, body) => request("PUT", path, body),
|
|
157
193
|
del: (path) => request("DELETE", path),
|
|
158
194
|
getRaw: (path, params) => requestRaw("GET", path, void 0, params),
|
|
159
|
-
putRaw: (path, body, headers) => requestRaw("PUT", path, body, void 0, headers)
|
|
195
|
+
putRaw: (path, body, headers) => requestRaw("PUT", path, body, void 0, headers),
|
|
196
|
+
postRaw: (path, body, headers) => requestRaw("POST", path, body, void 0, headers),
|
|
197
|
+
delRaw: (path, params) => requestRaw("DELETE", path, void 0, params)
|
|
160
198
|
};
|
|
161
199
|
}
|
|
162
200
|
|
|
@@ -267,6 +305,239 @@ function defaultCredentialStore() {
|
|
|
267
305
|
return new FileCredentialStore();
|
|
268
306
|
}
|
|
269
307
|
|
|
308
|
+
// src/ws.ts
|
|
309
|
+
var FsError = class extends Error {
|
|
310
|
+
code;
|
|
311
|
+
constructor(code, message) {
|
|
312
|
+
super(message);
|
|
313
|
+
this.name = "FsError";
|
|
314
|
+
this.code = code;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var nextId = 1;
|
|
318
|
+
function nextRequestId() {
|
|
319
|
+
return String(nextId++);
|
|
320
|
+
}
|
|
321
|
+
function toBase64(data) {
|
|
322
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
323
|
+
if (typeof Buffer !== "undefined") {
|
|
324
|
+
return Buffer.from(bytes).toString("base64");
|
|
325
|
+
}
|
|
326
|
+
let binary = "";
|
|
327
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
328
|
+
binary += String.fromCharCode(bytes[i]);
|
|
329
|
+
}
|
|
330
|
+
return btoa(binary);
|
|
331
|
+
}
|
|
332
|
+
function fromBase64(b64) {
|
|
333
|
+
if (typeof Buffer !== "undefined") {
|
|
334
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
335
|
+
}
|
|
336
|
+
const binary = atob(b64);
|
|
337
|
+
const bytes = new Uint8Array(binary.length);
|
|
338
|
+
for (let i = 0; i < binary.length; i++) {
|
|
339
|
+
bytes[i] = binary.charCodeAt(i);
|
|
340
|
+
}
|
|
341
|
+
return bytes;
|
|
342
|
+
}
|
|
343
|
+
var FsClient = class _FsClient {
|
|
344
|
+
ws;
|
|
345
|
+
pending = /* @__PURE__ */ new Map();
|
|
346
|
+
closed = false;
|
|
347
|
+
constructor(ws) {
|
|
348
|
+
this.ws = ws;
|
|
349
|
+
ws.onmessage = (ev) => {
|
|
350
|
+
const text = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
351
|
+
let resp;
|
|
352
|
+
try {
|
|
353
|
+
resp = JSON.parse(text);
|
|
354
|
+
} catch {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const p = this.pending.get(resp.id);
|
|
358
|
+
if (p) {
|
|
359
|
+
this.pending.delete(resp.id);
|
|
360
|
+
p.resolve(resp);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
ws.onclose = () => {
|
|
364
|
+
this.closed = true;
|
|
365
|
+
for (const [, p] of this.pending) {
|
|
366
|
+
p.reject(new FsError("CONNECTION_CLOSED", "WebSocket connection closed"));
|
|
367
|
+
}
|
|
368
|
+
this.pending.clear();
|
|
369
|
+
};
|
|
370
|
+
ws.onerror = () => {
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Connect to an fs9 WebSocket server.
|
|
375
|
+
*
|
|
376
|
+
* @param url WebSocket URL, e.g. `wss://host:5480`
|
|
377
|
+
* @param WS WebSocket constructor (native or from `ws` package)
|
|
378
|
+
*/
|
|
379
|
+
static connect(url, WS) {
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
const ws = new WS(url);
|
|
382
|
+
ws.onopen = () => {
|
|
383
|
+
ws.onopen = null;
|
|
384
|
+
ws.onerror = null;
|
|
385
|
+
resolve(new _FsClient(ws));
|
|
386
|
+
};
|
|
387
|
+
ws.onerror = (ev) => {
|
|
388
|
+
ws.onopen = null;
|
|
389
|
+
ws.onerror = null;
|
|
390
|
+
const msg = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket connection failed";
|
|
391
|
+
reject(new FsError("CONNECTION_ERROR", msg));
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/** Authenticate with the server. Must be called first after connect. */
|
|
396
|
+
async authenticate(username, password) {
|
|
397
|
+
const resp = await this.sendAndRecv({
|
|
398
|
+
id: nextRequestId(),
|
|
399
|
+
op: "auth",
|
|
400
|
+
username,
|
|
401
|
+
password
|
|
402
|
+
});
|
|
403
|
+
if (!resp.ok) {
|
|
404
|
+
throw new FsError("AUTH_FAILED", this.errorMessage(resp));
|
|
405
|
+
}
|
|
406
|
+
const data = resp.data;
|
|
407
|
+
return {
|
|
408
|
+
user: String(data?.user ?? ""),
|
|
409
|
+
tenant: String(data?.tenant ?? ""),
|
|
410
|
+
keyspace: String(data?.keyspace ?? "")
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** Get file or directory metadata. */
|
|
414
|
+
async stat(path) {
|
|
415
|
+
const resp = await this.sendAndRecv({
|
|
416
|
+
id: nextRequestId(),
|
|
417
|
+
op: "stat",
|
|
418
|
+
path
|
|
419
|
+
});
|
|
420
|
+
this.expectOk(resp);
|
|
421
|
+
return resp.data;
|
|
422
|
+
}
|
|
423
|
+
/** List directory contents. */
|
|
424
|
+
async readdir(path) {
|
|
425
|
+
const resp = await this.sendAndRecv({
|
|
426
|
+
id: nextRequestId(),
|
|
427
|
+
op: "readdir",
|
|
428
|
+
path
|
|
429
|
+
});
|
|
430
|
+
this.expectOk(resp);
|
|
431
|
+
const data = resp.data;
|
|
432
|
+
return data?.entries ?? [];
|
|
433
|
+
}
|
|
434
|
+
/** Create a directory. Always recursive (mkdir -p). */
|
|
435
|
+
async mkdir(path, recursive = true) {
|
|
436
|
+
const resp = await this.sendAndRecv({
|
|
437
|
+
id: nextRequestId(),
|
|
438
|
+
op: "mkdir",
|
|
439
|
+
path,
|
|
440
|
+
recursive
|
|
441
|
+
});
|
|
442
|
+
this.expectOk(resp);
|
|
443
|
+
}
|
|
444
|
+
/** Read an entire file, returning raw bytes. */
|
|
445
|
+
async readFile(path) {
|
|
446
|
+
const resp = await this.sendAndRecv({
|
|
447
|
+
id: nextRequestId(),
|
|
448
|
+
op: "read",
|
|
449
|
+
path
|
|
450
|
+
});
|
|
451
|
+
this.expectOk(resp);
|
|
452
|
+
const data = resp.data;
|
|
453
|
+
const content = data?.content;
|
|
454
|
+
if (typeof content !== "string") {
|
|
455
|
+
throw new FsError("PROTOCOL", "missing content field in read response");
|
|
456
|
+
}
|
|
457
|
+
return fromBase64(content);
|
|
458
|
+
}
|
|
459
|
+
/** Write (overwrite) a file. Returns bytes written. */
|
|
460
|
+
async writeFile(path, data) {
|
|
461
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
462
|
+
const resp = await this.sendAndRecv({
|
|
463
|
+
id: nextRequestId(),
|
|
464
|
+
op: "write",
|
|
465
|
+
path,
|
|
466
|
+
content: toBase64(bytes),
|
|
467
|
+
encoding: "base64"
|
|
468
|
+
});
|
|
469
|
+
this.expectOk(resp);
|
|
470
|
+
const result = resp.data;
|
|
471
|
+
return result?.written ?? bytes.byteLength;
|
|
472
|
+
}
|
|
473
|
+
/** Append to a file. Returns bytes written. */
|
|
474
|
+
async appendFile(path, data) {
|
|
475
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
476
|
+
const resp = await this.sendAndRecv({
|
|
477
|
+
id: nextRequestId(),
|
|
478
|
+
op: "append",
|
|
479
|
+
path,
|
|
480
|
+
content: toBase64(bytes),
|
|
481
|
+
encoding: "base64"
|
|
482
|
+
});
|
|
483
|
+
this.expectOk(resp);
|
|
484
|
+
const result = resp.data;
|
|
485
|
+
return result?.written ?? bytes.byteLength;
|
|
486
|
+
}
|
|
487
|
+
/** Remove a file (non-recursive) or directory (recursive). */
|
|
488
|
+
async rm(path, recursive = false) {
|
|
489
|
+
const resp = await this.sendAndRecv(
|
|
490
|
+
recursive ? { id: nextRequestId(), op: "rm", path, recursive: true } : { id: nextRequestId(), op: "unlink", path }
|
|
491
|
+
);
|
|
492
|
+
this.expectOk(resp);
|
|
493
|
+
}
|
|
494
|
+
/** Rename (move) a file or directory. */
|
|
495
|
+
async rename(oldPath, newPath) {
|
|
496
|
+
const resp = await this.sendAndRecv({
|
|
497
|
+
id: nextRequestId(),
|
|
498
|
+
op: "rename",
|
|
499
|
+
old_path: oldPath,
|
|
500
|
+
new_path: newPath
|
|
501
|
+
});
|
|
502
|
+
this.expectOk(resp);
|
|
503
|
+
}
|
|
504
|
+
/** Gracefully close the WebSocket connection. */
|
|
505
|
+
async close() {
|
|
506
|
+
if (!this.closed) {
|
|
507
|
+
this.ws.close();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// ── internals ──────────────────────────────────────────────────
|
|
511
|
+
sendAndRecv(request) {
|
|
512
|
+
if (this.closed) {
|
|
513
|
+
return Promise.reject(new FsError("CONNECTION_CLOSED", "WebSocket is closed"));
|
|
514
|
+
}
|
|
515
|
+
return new Promise((resolve, reject) => {
|
|
516
|
+
const id = request.id;
|
|
517
|
+
this.pending.set(id, { resolve, reject });
|
|
518
|
+
try {
|
|
519
|
+
this.ws.send(JSON.stringify(request));
|
|
520
|
+
} catch (err) {
|
|
521
|
+
this.pending.delete(id);
|
|
522
|
+
reject(new FsError("CONNECTION_ERROR", String(err)));
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
expectOk(resp) {
|
|
527
|
+
if (!resp.ok) {
|
|
528
|
+
const detail = resp.error;
|
|
529
|
+
throw new FsError(
|
|
530
|
+
detail?.code ?? "UNKNOWN",
|
|
531
|
+
detail?.message ?? "unknown error"
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
errorMessage(resp) {
|
|
536
|
+
const detail = resp.error;
|
|
537
|
+
return detail ? `${detail.code}: ${detail.message}` : "unknown error";
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
270
541
|
// src/client.ts
|
|
271
542
|
function createDb9Client(options = {}) {
|
|
272
543
|
const baseUrl = options.baseUrl ?? "https://db9.shared.aws.tidbcloud.com/api";
|
|
@@ -276,7 +547,10 @@ function createDb9Client(options = {}) {
|
|
|
276
547
|
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
277
548
|
const publicClient = createHttpClient({
|
|
278
549
|
baseUrl,
|
|
279
|
-
fetch: options.fetch
|
|
550
|
+
fetch: options.fetch,
|
|
551
|
+
timeout: options.timeout,
|
|
552
|
+
maxRetries: options.maxRetries,
|
|
553
|
+
retryDelay: options.retryDelay
|
|
280
554
|
});
|
|
281
555
|
async function getAuthClient() {
|
|
282
556
|
if (!token && !tokenLoaded) {
|
|
@@ -299,35 +573,110 @@ function createDb9Client(options = {}) {
|
|
|
299
573
|
return createHttpClient({
|
|
300
574
|
baseUrl,
|
|
301
575
|
fetch: options.fetch,
|
|
302
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
576
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
577
|
+
timeout: options.timeout,
|
|
578
|
+
maxRetries: options.maxRetries,
|
|
579
|
+
retryDelay: options.retryDelay
|
|
303
580
|
});
|
|
304
581
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
582
|
+
let refreshPromise = null;
|
|
583
|
+
async function refreshAnonymousToken() {
|
|
584
|
+
const creds = await store.load();
|
|
585
|
+
if (!creds?.anonymous_id || !creds?.anonymous_secret) {
|
|
586
|
+
throw new Error("Not an anonymous session");
|
|
587
|
+
}
|
|
588
|
+
const resp = await publicClient.post(
|
|
589
|
+
"/customer/anonymous-refresh",
|
|
590
|
+
{
|
|
591
|
+
anonymous_id: creds.anonymous_id,
|
|
592
|
+
anonymous_secret: creds.anonymous_secret
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
token = resp.token;
|
|
596
|
+
await store.save({ ...creds, token: resp.token });
|
|
308
597
|
}
|
|
309
|
-
async function
|
|
310
|
-
|
|
311
|
-
|
|
598
|
+
async function withAuthRetry(operation) {
|
|
599
|
+
const client = await getAuthClient();
|
|
600
|
+
try {
|
|
601
|
+
return await operation(client);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
if (!(err instanceof Db9Error) || err.statusCode !== 401) {
|
|
604
|
+
throw err;
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
if (!refreshPromise) {
|
|
608
|
+
refreshPromise = refreshAnonymousToken();
|
|
609
|
+
}
|
|
610
|
+
await refreshPromise;
|
|
611
|
+
} catch {
|
|
612
|
+
throw err;
|
|
613
|
+
} finally {
|
|
614
|
+
refreshPromise = null;
|
|
615
|
+
}
|
|
616
|
+
const newClient = await getAuthClient();
|
|
617
|
+
return operation(newClient);
|
|
312
618
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
619
|
+
}
|
|
620
|
+
const fsWsPort = options.wsPort ?? 5480;
|
|
621
|
+
async function resolveFsConn(dbId) {
|
|
622
|
+
const creds = await withAuthRetry(
|
|
623
|
+
(client) => client.get(
|
|
624
|
+
`/customer/databases/${dbId}/credentials`
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
const connStr = creds.connection_string;
|
|
628
|
+
const hostMatch = connStr.match(/@([^:/?]+)/);
|
|
629
|
+
const host = hostMatch?.[1];
|
|
630
|
+
if (!host) {
|
|
631
|
+
throw new Error(`Cannot parse host from connection string for database '${dbId}'`);
|
|
318
632
|
}
|
|
319
|
-
|
|
320
|
-
|
|
633
|
+
const userMatch = connStr.match(/:\/\/([^:@]+)/);
|
|
634
|
+
const username = userMatch?.[1] ?? creds.admin_user;
|
|
635
|
+
const protocol = host === "localhost" || host === "127.0.0.1" ? "ws" : "wss";
|
|
636
|
+
return {
|
|
637
|
+
wsUrl: `${protocol}://${host}:${fsWsPort}`,
|
|
638
|
+
username,
|
|
639
|
+
password: creds.admin_password
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
async function withFsClient(dbId, operation) {
|
|
643
|
+
const WS = options.WebSocket ?? (typeof globalThis !== "undefined" ? globalThis.WebSocket : void 0);
|
|
644
|
+
if (!WS) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
"WebSocket constructor not available. Pass `WebSocket` in Db9ClientOptions, or use Node 21+ / a browser environment with native WebSocket support, or install the `ws` package for Node 18\u201320."
|
|
647
|
+
);
|
|
321
648
|
}
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
649
|
+
const conn = await resolveFsConn(dbId);
|
|
650
|
+
const client = await FsClient.connect(conn.wsUrl, WS);
|
|
651
|
+
try {
|
|
652
|
+
await client.authenticate(conn.username, conn.password);
|
|
653
|
+
return await operation(client);
|
|
654
|
+
} finally {
|
|
655
|
+
await client.close();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function parseSqlError(raw) {
|
|
659
|
+
try {
|
|
660
|
+
const parsed = JSON.parse(raw);
|
|
661
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.message === "string") {
|
|
662
|
+
return parsed;
|
|
663
|
+
}
|
|
664
|
+
} catch {
|
|
325
665
|
}
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
328
|
-
|
|
666
|
+
const pgMatch = raw.match(/^(?:ERROR:\s*)?(.+?)(?:\s+DETAIL:\s+(.+?))?(?:\s+HINT:\s+(.+?))?(?:\s+\(SQLSTATE\s+(\w+)\))?$/s);
|
|
667
|
+
if (pgMatch && pgMatch[1]) {
|
|
668
|
+
const result = { message: pgMatch[1].trim() };
|
|
669
|
+
if (pgMatch[2]) result.detail = pgMatch[2].trim();
|
|
670
|
+
if (pgMatch[3]) result.hint = pgMatch[3].trim();
|
|
671
|
+
if (pgMatch[4]) result.code = pgMatch[4];
|
|
672
|
+
return result;
|
|
329
673
|
}
|
|
330
|
-
return
|
|
674
|
+
return { message: raw };
|
|
675
|
+
}
|
|
676
|
+
async function fetchAnonymousSecret() {
|
|
677
|
+
return withAuthRetry(
|
|
678
|
+
(client) => client.post("/customer/anonymous-secret", {})
|
|
679
|
+
);
|
|
331
680
|
}
|
|
332
681
|
return {
|
|
333
682
|
auth: {
|
|
@@ -342,196 +691,226 @@ function createDb9Client(options = {}) {
|
|
|
342
691
|
req
|
|
343
692
|
),
|
|
344
693
|
// Authenticated endpoints
|
|
345
|
-
me: async () =>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const client = await getAuthClient();
|
|
351
|
-
return client.get(
|
|
352
|
-
"/customer/anonymous-secret"
|
|
353
|
-
);
|
|
694
|
+
me: async () => withAuthRetry(
|
|
695
|
+
(client) => client.get("/customer/me")
|
|
696
|
+
),
|
|
697
|
+
getAnonymousSecret: () => {
|
|
698
|
+
return fetchAnonymousSecret();
|
|
354
699
|
},
|
|
355
|
-
claim: async (req) =>
|
|
356
|
-
|
|
357
|
-
|
|
700
|
+
claim: async (req) => withAuthRetry(
|
|
701
|
+
(client) => client.post("/customer/claim", req)
|
|
702
|
+
),
|
|
703
|
+
ensureAnonymousSecret: async () => {
|
|
704
|
+
const creds = await store.load();
|
|
705
|
+
if (!creds?.anonymous_id || creds.anonymous_secret) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const resp = await fetchAnonymousSecret();
|
|
709
|
+
await store.save({
|
|
710
|
+
...creds,
|
|
711
|
+
anonymous_secret: resp.anonymous_secret
|
|
712
|
+
});
|
|
358
713
|
}
|
|
359
714
|
},
|
|
360
715
|
tokens: {
|
|
361
|
-
list: async () =>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
716
|
+
list: async () => withAuthRetry(
|
|
717
|
+
(client) => client.get("/customer/tokens")
|
|
718
|
+
),
|
|
719
|
+
revoke: async (tokenId) => withAuthRetry(
|
|
720
|
+
(client) => client.del(`/customer/tokens/${tokenId}`)
|
|
721
|
+
),
|
|
722
|
+
create: async (req) => withAuthRetry(
|
|
723
|
+
(client) => client.post("/customer/tokens", req)
|
|
724
|
+
)
|
|
369
725
|
},
|
|
370
726
|
databases: {
|
|
371
727
|
// ── CRUD ──────────────────────────────────────────────────
|
|
372
|
-
create: async (req) =>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
get: async (databaseId) => {
|
|
381
|
-
const client = await getAuthClient();
|
|
382
|
-
return client.get(
|
|
728
|
+
create: async (req) => withAuthRetry(
|
|
729
|
+
(client) => client.post("/customer/databases", req)
|
|
730
|
+
),
|
|
731
|
+
list: async () => withAuthRetry(
|
|
732
|
+
(client) => client.get("/customer/databases")
|
|
733
|
+
),
|
|
734
|
+
get: async (databaseId) => withAuthRetry(
|
|
735
|
+
(client) => client.get(
|
|
383
736
|
`/customer/databases/${databaseId}`
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
delete: async (databaseId) =>
|
|
387
|
-
|
|
388
|
-
return client.del(
|
|
737
|
+
)
|
|
738
|
+
),
|
|
739
|
+
delete: async (databaseId) => withAuthRetry(
|
|
740
|
+
(client) => client.del(
|
|
389
741
|
`/customer/databases/${databaseId}`
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
resetPassword: async (databaseId) =>
|
|
393
|
-
|
|
394
|
-
return client.post(
|
|
742
|
+
)
|
|
743
|
+
),
|
|
744
|
+
resetPassword: async (databaseId) => withAuthRetry(
|
|
745
|
+
(client) => client.post(
|
|
395
746
|
`/customer/databases/${databaseId}/reset-password`
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
747
|
+
)
|
|
748
|
+
),
|
|
749
|
+
credentials: async (databaseId) => withAuthRetry(
|
|
750
|
+
(client) => client.get(
|
|
751
|
+
`/customer/databases/${databaseId}/credentials`
|
|
752
|
+
)
|
|
753
|
+
),
|
|
754
|
+
observability: async (databaseId) => withAuthRetry(
|
|
755
|
+
(client) => client.get(
|
|
401
756
|
`/customer/databases/${databaseId}/observability`
|
|
402
|
-
)
|
|
403
|
-
|
|
757
|
+
)
|
|
758
|
+
),
|
|
404
759
|
// ── SQL Execution ─────────────────────────────────────────
|
|
405
760
|
sql: async (databaseId, query) => {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
761
|
+
const result = await withAuthRetry(
|
|
762
|
+
(client) => client.post(
|
|
763
|
+
`/customer/databases/${databaseId}/sql`,
|
|
764
|
+
{ query }
|
|
765
|
+
)
|
|
410
766
|
);
|
|
767
|
+
if (result.error && typeof result.error === "string") {
|
|
768
|
+
result.error = parseSqlError(result.error);
|
|
769
|
+
}
|
|
770
|
+
return result;
|
|
411
771
|
},
|
|
412
772
|
sqlFile: async (databaseId, fileContent) => {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
773
|
+
const result = await withAuthRetry(
|
|
774
|
+
(client) => client.post(
|
|
775
|
+
`/customer/databases/${databaseId}/sql`,
|
|
776
|
+
{ file_content: fileContent }
|
|
777
|
+
)
|
|
417
778
|
);
|
|
779
|
+
if (result.error && typeof result.error === "string") {
|
|
780
|
+
result.error = parseSqlError(result.error);
|
|
781
|
+
}
|
|
782
|
+
return result;
|
|
418
783
|
},
|
|
419
784
|
// ── Schema & Dump ─────────────────────────────────────────
|
|
420
|
-
schema: async (databaseId) =>
|
|
421
|
-
|
|
422
|
-
return client.get(
|
|
785
|
+
schema: async (databaseId) => withAuthRetry(
|
|
786
|
+
(client) => client.get(
|
|
423
787
|
`/customer/databases/${databaseId}/schema`
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
dump: async (databaseId, req) =>
|
|
427
|
-
|
|
428
|
-
return client.post(
|
|
788
|
+
)
|
|
789
|
+
),
|
|
790
|
+
dump: async (databaseId, req) => withAuthRetry(
|
|
791
|
+
(client) => client.post(
|
|
429
792
|
`/customer/databases/${databaseId}/dump`,
|
|
430
793
|
req
|
|
431
|
-
)
|
|
432
|
-
|
|
794
|
+
)
|
|
795
|
+
),
|
|
433
796
|
// ── Migrations ────────────────────────────────────────────
|
|
434
|
-
applyMigration: async (databaseId, req) =>
|
|
435
|
-
|
|
436
|
-
return client.post(
|
|
797
|
+
applyMigration: async (databaseId, req) => withAuthRetry(
|
|
798
|
+
(client) => client.post(
|
|
437
799
|
`/customer/databases/${databaseId}/migrations`,
|
|
438
800
|
req
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
listMigrations: async (databaseId) =>
|
|
442
|
-
|
|
443
|
-
return client.get(
|
|
801
|
+
)
|
|
802
|
+
),
|
|
803
|
+
listMigrations: async (databaseId) => withAuthRetry(
|
|
804
|
+
(client) => client.get(
|
|
444
805
|
`/customer/databases/${databaseId}/migrations`
|
|
445
|
-
)
|
|
446
|
-
|
|
806
|
+
)
|
|
807
|
+
),
|
|
447
808
|
// ── Branching ─────────────────────────────────────────────
|
|
448
|
-
branch: async (databaseId, req) =>
|
|
449
|
-
|
|
450
|
-
return client.post(
|
|
809
|
+
branch: async (databaseId, req) => withAuthRetry(
|
|
810
|
+
(client) => client.post(
|
|
451
811
|
`/customer/databases/${databaseId}/branch`,
|
|
452
812
|
req
|
|
453
|
-
)
|
|
454
|
-
|
|
813
|
+
)
|
|
814
|
+
),
|
|
455
815
|
// ── User Management ───────────────────────────────────────
|
|
456
816
|
users: {
|
|
457
|
-
list: async (databaseId) =>
|
|
458
|
-
|
|
459
|
-
return client.get(
|
|
817
|
+
list: async (databaseId) => withAuthRetry(
|
|
818
|
+
(client) => client.get(
|
|
460
819
|
`/customer/databases/${databaseId}/users`
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
create: async (databaseId, req) =>
|
|
464
|
-
|
|
465
|
-
return client.post(
|
|
820
|
+
)
|
|
821
|
+
),
|
|
822
|
+
create: async (databaseId, req) => withAuthRetry(
|
|
823
|
+
(client) => client.post(
|
|
466
824
|
`/customer/databases/${databaseId}/users`,
|
|
467
825
|
req
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
delete: async (databaseId, username) =>
|
|
471
|
-
|
|
472
|
-
return client.del(
|
|
826
|
+
)
|
|
827
|
+
),
|
|
828
|
+
delete: async (databaseId, username) => withAuthRetry(
|
|
829
|
+
(client) => client.del(
|
|
473
830
|
`/customer/databases/${databaseId}/users/${username}`
|
|
474
|
-
)
|
|
475
|
-
|
|
831
|
+
)
|
|
832
|
+
)
|
|
476
833
|
}
|
|
477
834
|
},
|
|
478
835
|
fs: {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
`/download?${params.toString()}`
|
|
495
|
-
);
|
|
496
|
-
return response.text();
|
|
836
|
+
/**
|
|
837
|
+
* Open a persistent WebSocket connection for multiple fs operations.
|
|
838
|
+
* Caller is responsible for calling `client.close()` when done.
|
|
839
|
+
*/
|
|
840
|
+
connect: async (dbId) => {
|
|
841
|
+
const WS = options.WebSocket ?? (typeof globalThis !== "undefined" ? globalThis.WebSocket : void 0);
|
|
842
|
+
if (!WS) {
|
|
843
|
+
throw new Error(
|
|
844
|
+
"WebSocket constructor not available. Pass `WebSocket` in Db9ClientOptions, or use Node 21+ / a browser environment with native WebSocket support, or install the `ws` package for Node 18\u201320."
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const conn = await resolveFsConn(dbId);
|
|
848
|
+
const client = await FsClient.connect(conn.wsUrl, WS);
|
|
849
|
+
await client.authenticate(conn.username, conn.password);
|
|
850
|
+
return client;
|
|
497
851
|
},
|
|
852
|
+
/** List directory contents. */
|
|
853
|
+
list: async (dbId, path) => withFsClient(dbId, (client) => client.readdir(path)),
|
|
854
|
+
/** Read a file as text (UTF-8). */
|
|
855
|
+
read: async (dbId, path) => withFsClient(dbId, async (client) => {
|
|
856
|
+
const bytes = await client.readFile(path);
|
|
857
|
+
return new TextDecoder().decode(bytes);
|
|
858
|
+
}),
|
|
859
|
+
/** Read a file as raw bytes. */
|
|
860
|
+
readBinary: async (dbId, path) => withFsClient(dbId, (client) => client.readFile(path)),
|
|
861
|
+
/** Write (overwrite) a file. Accepts string, ArrayBuffer, or Uint8Array. */
|
|
498
862
|
write: async (dbId, path, content) => {
|
|
499
|
-
|
|
500
|
-
await fsRequest("PUT", dbId, `/upload?${params.toString()}`, content);
|
|
863
|
+
await withFsClient(dbId, (client) => client.writeFile(path, content));
|
|
501
864
|
},
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
865
|
+
/** Append to a file. Returns bytes written. */
|
|
866
|
+
append: async (dbId, path, content) => withFsClient(dbId, (client) => client.appendFile(path, content)),
|
|
867
|
+
/** Get file or directory metadata. */
|
|
868
|
+
stat: async (dbId, path) => withFsClient(dbId, (client) => client.stat(path)),
|
|
869
|
+
/** Check if a file or directory exists. */
|
|
870
|
+
exists: async (dbId, path) => {
|
|
871
|
+
try {
|
|
872
|
+
await withFsClient(dbId, (client) => client.stat(path));
|
|
873
|
+
return true;
|
|
874
|
+
} catch (err) {
|
|
875
|
+
if (err instanceof FsError && err.code === "ENOENT") {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
throw err;
|
|
879
|
+
}
|
|
510
880
|
},
|
|
881
|
+
/** Create a directory (recursive by default). */
|
|
511
882
|
mkdir: async (dbId, path) => {
|
|
512
|
-
|
|
513
|
-
"POST",
|
|
514
|
-
dbId,
|
|
515
|
-
"/open",
|
|
516
|
-
JSON.stringify({
|
|
517
|
-
path,
|
|
518
|
-
flags: { create: true, directory: true }
|
|
519
|
-
}),
|
|
520
|
-
"application/json"
|
|
521
|
-
);
|
|
522
|
-
const { handle_id } = await openResp.json();
|
|
523
|
-
await fsRequest(
|
|
524
|
-
"POST",
|
|
525
|
-
dbId,
|
|
526
|
-
"/close",
|
|
527
|
-
JSON.stringify({ handle_id }),
|
|
528
|
-
"application/json"
|
|
529
|
-
);
|
|
883
|
+
await withFsClient(dbId, (client) => client.mkdir(path, true));
|
|
530
884
|
},
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
await
|
|
885
|
+
/** Remove a file or directory. */
|
|
886
|
+
remove: async (dbId, path, opts) => {
|
|
887
|
+
await withFsClient(dbId, (client) => client.rm(path, opts?.recursive ?? false));
|
|
888
|
+
},
|
|
889
|
+
/** Rename (move) a file or directory. */
|
|
890
|
+
rename: async (dbId, oldPath, newPath) => {
|
|
891
|
+
await withFsClient(dbId, (client) => client.rename(oldPath, newPath));
|
|
534
892
|
}
|
|
893
|
+
},
|
|
894
|
+
// ── Device Auth Flow ─────────────────────────────────────────
|
|
895
|
+
deviceAuth: {
|
|
896
|
+
/** Start device code flow. Returns codes for user to authorize. */
|
|
897
|
+
createDeviceCode: () => publicClient.post("/customer/device-code"),
|
|
898
|
+
/** Poll for device token after user authorizes. Returns token or error status. */
|
|
899
|
+
pollDeviceToken: async (req) => {
|
|
900
|
+
try {
|
|
901
|
+
return await publicClient.post("/customer/device-token", req);
|
|
902
|
+
} catch (err) {
|
|
903
|
+
if (err instanceof Db9Error && err.statusCode === 400) {
|
|
904
|
+
const errorType = err.message;
|
|
905
|
+
if (errorType === "authorization_pending" || errorType === "expired_token" || errorType === "access_denied") {
|
|
906
|
+
return { error: errorType };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
throw err;
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
/** Submit device verification with user credentials. */
|
|
913
|
+
verifyDevice: (req) => publicClient.post("/customer/device-verify", req)
|
|
535
914
|
}
|
|
536
915
|
};
|
|
537
916
|
}
|
|
@@ -542,7 +921,10 @@ async function instantDatabase(options = {}) {
|
|
|
542
921
|
const client = createDb9Client({
|
|
543
922
|
baseUrl: options.baseUrl,
|
|
544
923
|
fetch: options.fetch,
|
|
545
|
-
credentialStore: options.credentialStore
|
|
924
|
+
credentialStore: options.credentialStore,
|
|
925
|
+
timeout: options.timeout,
|
|
926
|
+
maxRetries: options.maxRetries,
|
|
927
|
+
retryDelay: options.retryDelay
|
|
546
928
|
});
|
|
547
929
|
const existing = await client.databases.list();
|
|
548
930
|
const found = existing.find((db) => db.name === dbName);
|
|
@@ -574,6 +956,8 @@ function toResult(db) {
|
|
|
574
956
|
Db9Error,
|
|
575
957
|
Db9NotFoundError,
|
|
576
958
|
FileCredentialStore,
|
|
959
|
+
FsClient,
|
|
960
|
+
FsError,
|
|
577
961
|
MemoryCredentialStore,
|
|
578
962
|
createDb9Client,
|
|
579
963
|
defaultCredentialStore,
|