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