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