get-db9 0.4.1 → 0.5.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/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 headers = {
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 response = await fetchFn(url, init);
111
- if (!response.ok) {
112
- throw await Db9Error.fromResponse(response);
113
- }
114
- if (response.status === 204) {
115
- return void 0;
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
- return response.json();
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 init = { method, headers };
136
- if (body !== void 0) {
137
- init.body = body;
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
- const response = await fetchFn(url, init);
140
- if (!response.ok) {
141
- throw await Db9Error.fromResponse(response);
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
 
@@ -251,7 +287,10 @@ function createDb9Client(options = {}) {
251
287
  const fetchFn = options.fetch ?? globalThis.fetch;
252
288
  const publicClient = createHttpClient({
253
289
  baseUrl,
254
- fetch: options.fetch
290
+ fetch: options.fetch,
291
+ timeout: options.timeout,
292
+ maxRetries: options.maxRetries,
293
+ retryDelay: options.retryDelay
255
294
  });
256
295
  async function getAuthClient() {
257
296
  if (!token && !tokenLoaded) {
@@ -274,35 +313,118 @@ function createDb9Client(options = {}) {
274
313
  return createHttpClient({
275
314
  baseUrl,
276
315
  fetch: options.fetch,
277
- headers: { Authorization: `Bearer ${token}` }
316
+ headers: { Authorization: `Bearer ${token}` },
317
+ timeout: options.timeout,
318
+ maxRetries: options.maxRetries,
319
+ retryDelay: options.retryDelay
278
320
  });
279
321
  }
322
+ let refreshPromise = null;
323
+ async function refreshAnonymousToken() {
324
+ const creds = await store.load();
325
+ if (!creds?.anonymous_id || !creds?.anonymous_secret) {
326
+ throw new Error("Not an anonymous session");
327
+ }
328
+ const resp = await publicClient.post(
329
+ "/customer/anonymous-refresh",
330
+ {
331
+ anonymous_id: creds.anonymous_id,
332
+ anonymous_secret: creds.anonymous_secret
333
+ }
334
+ );
335
+ token = resp.token;
336
+ await store.save({ ...creds, token: resp.token });
337
+ }
338
+ async function withAuthRetry(operation) {
339
+ const client = await getAuthClient();
340
+ try {
341
+ return await operation(client);
342
+ } catch (err) {
343
+ if (!(err instanceof Db9Error) || err.statusCode !== 401) {
344
+ throw err;
345
+ }
346
+ try {
347
+ if (!refreshPromise) {
348
+ refreshPromise = refreshAnonymousToken();
349
+ }
350
+ await refreshPromise;
351
+ } catch {
352
+ throw err;
353
+ } finally {
354
+ refreshPromise = null;
355
+ }
356
+ const newClient = await getAuthClient();
357
+ return operation(newClient);
358
+ }
359
+ }
280
360
  function deriveFs9Url(dbId) {
281
361
  const origin = baseUrl.replace(/\/api\/?$/, "");
282
362
  return `${origin}/fs9/${dbId}`;
283
363
  }
284
- async function fsRequest(method, dbId, fsPath, body, contentType) {
364
+ function getFsClient(dbId) {
365
+ const fs9Base = deriveFs9Url(dbId) + "/api/v1";
366
+ return createHttpClient({
367
+ baseUrl: fs9Base,
368
+ fetch: options.fetch,
369
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
370
+ timeout: options.timeout,
371
+ maxRetries: options.maxRetries,
372
+ retryDelay: options.retryDelay
373
+ });
374
+ }
375
+ async function withFsAuthRetry(dbId, operation) {
285
376
  if (!token && !tokenLoaded) {
286
377
  await getAuthClient();
287
378
  }
288
- const fs9Url = deriveFs9Url(dbId);
289
- const url = `${fs9Url}/api/v1${fsPath}`;
290
- const headers = {};
291
- if (token) {
292
- headers["Authorization"] = `Bearer ${token}`;
293
- }
294
- if (body !== void 0) {
295
- headers["Content-Type"] = contentType || "text/plain";
379
+ const client = getFsClient(dbId);
380
+ try {
381
+ return await operation(client);
382
+ } catch (err) {
383
+ if (!(err instanceof Db9Error) || err.statusCode !== 401) {
384
+ throw err;
385
+ }
386
+ try {
387
+ if (!refreshPromise) {
388
+ refreshPromise = refreshAnonymousToken();
389
+ }
390
+ await refreshPromise;
391
+ } catch {
392
+ throw err;
393
+ } finally {
394
+ refreshPromise = null;
395
+ }
396
+ const newClient = getFsClient(dbId);
397
+ return operation(newClient);
296
398
  }
297
- const init = { method, headers };
298
- if (body !== void 0) {
299
- init.body = body;
399
+ }
400
+ function parseSqlError(raw) {
401
+ try {
402
+ const parsed = JSON.parse(raw);
403
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.message === "string") {
404
+ return parsed;
405
+ }
406
+ } catch {
300
407
  }
301
- const response = await fetchFn(url, init);
302
- if (!response.ok) {
303
- throw await Db9Error.fromResponse(response);
408
+ const pgMatch = raw.match(/^(?:ERROR:\s*)?(.+?)(?:\s+DETAIL:\s+(.+?))?(?:\s+HINT:\s+(.+?))?(?:\s+\(SQLSTATE\s+(\w+)\))?$/s);
409
+ if (pgMatch && pgMatch[1]) {
410
+ const result = { message: pgMatch[1].trim() };
411
+ if (pgMatch[2]) result.detail = pgMatch[2].trim();
412
+ if (pgMatch[3]) result.hint = pgMatch[3].trim();
413
+ if (pgMatch[4]) result.code = pgMatch[4];
414
+ return result;
304
415
  }
305
- return response;
416
+ return { message: raw };
417
+ }
418
+ async function fsStat(dbId, path) {
419
+ return withFsAuthRetry(
420
+ dbId,
421
+ (client) => client.get("/stat", { path })
422
+ );
423
+ }
424
+ async function fetchAnonymousSecret() {
425
+ return withAuthRetry(
426
+ (client) => client.post("/customer/anonymous-secret", {})
427
+ );
306
428
  }
307
429
  return {
308
430
  auth: {
@@ -317,195 +439,206 @@ function createDb9Client(options = {}) {
317
439
  req
318
440
  ),
319
441
  // Authenticated endpoints
320
- me: async () => {
321
- const client = await getAuthClient();
322
- return client.get("/customer/me");
323
- },
324
- getAnonymousSecret: async () => {
325
- const client = await getAuthClient();
326
- return client.get(
327
- "/customer/anonymous-secret"
328
- );
442
+ me: async () => withAuthRetry(
443
+ (client) => client.get("/customer/me")
444
+ ),
445
+ getAnonymousSecret: () => {
446
+ return fetchAnonymousSecret();
329
447
  },
330
- claim: async (req) => {
331
- const client = await getAuthClient();
332
- return client.post("/customer/claim", req);
448
+ claim: async (req) => withAuthRetry(
449
+ (client) => client.post("/customer/claim", req)
450
+ ),
451
+ ensureAnonymousSecret: async () => {
452
+ const creds = await store.load();
453
+ if (!creds?.anonymous_id || creds.anonymous_secret) {
454
+ return;
455
+ }
456
+ const resp = await fetchAnonymousSecret();
457
+ await store.save({
458
+ ...creds,
459
+ anonymous_secret: resp.anonymous_secret
460
+ });
333
461
  }
334
462
  },
335
463
  tokens: {
336
- list: async () => {
337
- const client = await getAuthClient();
338
- return client.get("/customer/tokens");
339
- },
340
- revoke: async (tokenId) => {
341
- const client = await getAuthClient();
342
- return client.del(`/customer/tokens/${tokenId}`);
343
- }
464
+ list: async () => withAuthRetry(
465
+ (client) => client.get("/customer/tokens")
466
+ ),
467
+ revoke: async (tokenId) => withAuthRetry(
468
+ (client) => client.del(`/customer/tokens/${tokenId}`)
469
+ ),
470
+ create: async (req) => withAuthRetry(
471
+ (client) => client.post("/customer/tokens", req)
472
+ )
344
473
  },
345
474
  databases: {
346
475
  // ── CRUD ──────────────────────────────────────────────────
347
- create: async (req) => {
348
- const client = await getAuthClient();
349
- return client.post("/customer/databases", req);
350
- },
351
- list: async () => {
352
- const client = await getAuthClient();
353
- return client.get("/customer/databases");
354
- },
355
- get: async (databaseId) => {
356
- const client = await getAuthClient();
357
- return client.get(
476
+ create: async (req) => withAuthRetry(
477
+ (client) => client.post("/customer/databases", req)
478
+ ),
479
+ list: async () => withAuthRetry(
480
+ (client) => client.get("/customer/databases")
481
+ ),
482
+ get: async (databaseId) => withAuthRetry(
483
+ (client) => client.get(
358
484
  `/customer/databases/${databaseId}`
359
- );
360
- },
361
- delete: async (databaseId) => {
362
- const client = await getAuthClient();
363
- return client.del(
485
+ )
486
+ ),
487
+ delete: async (databaseId) => withAuthRetry(
488
+ (client) => client.del(
364
489
  `/customer/databases/${databaseId}`
365
- );
366
- },
367
- resetPassword: async (databaseId) => {
368
- const client = await getAuthClient();
369
- return client.post(
490
+ )
491
+ ),
492
+ resetPassword: async (databaseId) => withAuthRetry(
493
+ (client) => client.post(
370
494
  `/customer/databases/${databaseId}/reset-password`
371
- );
372
- },
373
- observability: async (databaseId) => {
374
- const client = await getAuthClient();
375
- return client.get(
495
+ )
496
+ ),
497
+ observability: async (databaseId) => withAuthRetry(
498
+ (client) => client.get(
376
499
  `/customer/databases/${databaseId}/observability`
377
- );
378
- },
500
+ )
501
+ ),
379
502
  // ── SQL Execution ─────────────────────────────────────────
380
503
  sql: async (databaseId, query) => {
381
- const client = await getAuthClient();
382
- return client.post(
383
- `/customer/databases/${databaseId}/sql`,
384
- { query }
504
+ const result = await withAuthRetry(
505
+ (client) => client.post(
506
+ `/customer/databases/${databaseId}/sql`,
507
+ { query }
508
+ )
385
509
  );
510
+ if (result.error && typeof result.error === "string") {
511
+ result.error = parseSqlError(result.error);
512
+ }
513
+ return result;
386
514
  },
387
515
  sqlFile: async (databaseId, fileContent) => {
388
- const client = await getAuthClient();
389
- return client.post(
390
- `/customer/databases/${databaseId}/sql`,
391
- { file_content: fileContent }
516
+ const result = await withAuthRetry(
517
+ (client) => client.post(
518
+ `/customer/databases/${databaseId}/sql`,
519
+ { file_content: fileContent }
520
+ )
392
521
  );
522
+ if (result.error && typeof result.error === "string") {
523
+ result.error = parseSqlError(result.error);
524
+ }
525
+ return result;
393
526
  },
394
527
  // ── Schema & Dump ─────────────────────────────────────────
395
- schema: async (databaseId) => {
396
- const client = await getAuthClient();
397
- return client.get(
528
+ schema: async (databaseId) => withAuthRetry(
529
+ (client) => client.get(
398
530
  `/customer/databases/${databaseId}/schema`
399
- );
400
- },
401
- dump: async (databaseId, req) => {
402
- const client = await getAuthClient();
403
- return client.post(
531
+ )
532
+ ),
533
+ dump: async (databaseId, req) => withAuthRetry(
534
+ (client) => client.post(
404
535
  `/customer/databases/${databaseId}/dump`,
405
536
  req
406
- );
407
- },
537
+ )
538
+ ),
408
539
  // ── Migrations ────────────────────────────────────────────
409
- applyMigration: async (databaseId, req) => {
410
- const client = await getAuthClient();
411
- return client.post(
540
+ applyMigration: async (databaseId, req) => withAuthRetry(
541
+ (client) => client.post(
412
542
  `/customer/databases/${databaseId}/migrations`,
413
543
  req
414
- );
415
- },
416
- listMigrations: async (databaseId) => {
417
- const client = await getAuthClient();
418
- return client.get(
544
+ )
545
+ ),
546
+ listMigrations: async (databaseId) => withAuthRetry(
547
+ (client) => client.get(
419
548
  `/customer/databases/${databaseId}/migrations`
420
- );
421
- },
549
+ )
550
+ ),
422
551
  // ── Branching ─────────────────────────────────────────────
423
- branch: async (databaseId, req) => {
424
- const client = await getAuthClient();
425
- return client.post(
552
+ branch: async (databaseId, req) => withAuthRetry(
553
+ (client) => client.post(
426
554
  `/customer/databases/${databaseId}/branch`,
427
555
  req
428
- );
429
- },
556
+ )
557
+ ),
430
558
  // ── User Management ───────────────────────────────────────
431
559
  users: {
432
- list: async (databaseId) => {
433
- const client = await getAuthClient();
434
- return client.get(
560
+ list: async (databaseId) => withAuthRetry(
561
+ (client) => client.get(
435
562
  `/customer/databases/${databaseId}/users`
436
- );
437
- },
438
- create: async (databaseId, req) => {
439
- const client = await getAuthClient();
440
- return client.post(
563
+ )
564
+ ),
565
+ create: async (databaseId, req) => withAuthRetry(
566
+ (client) => client.post(
441
567
  `/customer/databases/${databaseId}/users`,
442
568
  req
443
- );
444
- },
445
- delete: async (databaseId, username) => {
446
- const client = await getAuthClient();
447
- return client.del(
569
+ )
570
+ ),
571
+ delete: async (databaseId, username) => withAuthRetry(
572
+ (client) => client.del(
448
573
  `/customer/databases/${databaseId}/users/${username}`
449
- );
450
- }
574
+ )
575
+ )
451
576
  }
452
577
  },
453
578
  fs: {
454
579
  list: async (dbId, path, options2) => {
455
- const params = new URLSearchParams({ path });
456
- if (options2?.recursive) params.set("recursive", "true");
457
- const response = await fsRequest(
458
- "GET",
580
+ const params = { path };
581
+ if (options2?.recursive) params.recursive = "true";
582
+ return withFsAuthRetry(
459
583
  dbId,
460
- `/readdir?${params.toString()}`
584
+ (client) => client.get("/readdir", params)
461
585
  );
462
- return response.json();
463
586
  },
464
587
  read: async (dbId, path) => {
465
- const params = new URLSearchParams({ path });
466
- const response = await fsRequest(
467
- "GET",
468
- dbId,
469
- `/download?${params.toString()}`
470
- );
471
- return response.text();
588
+ return withFsAuthRetry(dbId, async (client) => {
589
+ const resp = await client.getRaw("/download", { path });
590
+ return resp.text();
591
+ });
472
592
  },
473
- write: async (dbId, path, content) => {
474
- const params = new URLSearchParams({ path });
475
- await fsRequest("PUT", dbId, `/upload?${params.toString()}`, content);
593
+ readBinary: async (dbId, path) => {
594
+ return withFsAuthRetry(dbId, async (client) => {
595
+ const resp = await client.getRaw("/download", { path });
596
+ return resp.arrayBuffer();
597
+ });
476
598
  },
477
- stat: async (dbId, path) => {
478
- const params = new URLSearchParams({ path });
479
- const response = await fsRequest(
480
- "GET",
599
+ write: async (dbId, path, content) => {
600
+ const contentType = typeof content === "string" ? "text/plain" : "application/octet-stream";
601
+ await withFsAuthRetry(
481
602
  dbId,
482
- `/stat?${params.toString()}`
603
+ (client) => client.putRaw(`/upload?${new URLSearchParams({ path })}`, content, { "Content-Type": contentType })
483
604
  );
484
- return response.json();
605
+ },
606
+ stat: (dbId, path) => {
607
+ return fsStat(dbId, path);
608
+ },
609
+ exists: async (dbId, path) => {
610
+ try {
611
+ await fsStat(dbId, path);
612
+ return true;
613
+ } catch (err) {
614
+ if (err instanceof Db9Error && err.statusCode === 404) {
615
+ return false;
616
+ }
617
+ throw err;
618
+ }
485
619
  },
486
620
  mkdir: async (dbId, path) => {
487
- const openResp = await fsRequest(
488
- "POST",
621
+ await withFsAuthRetry(
489
622
  dbId,
490
- "/open",
491
- JSON.stringify({
492
- path,
493
- flags: { create: true, directory: true }
494
- }),
495
- "application/json"
623
+ (client) => client.postRaw(`/mkdir?${new URLSearchParams({ path, recursive: "true" })}`)
496
624
  );
497
- const { handle_id } = await openResp.json();
498
- await fsRequest(
499
- "POST",
625
+ },
626
+ remove: async (dbId, path) => {
627
+ await withFsAuthRetry(
500
628
  dbId,
501
- "/close",
502
- JSON.stringify({ handle_id }),
503
- "application/json"
629
+ (client) => client.delRaw("/remove", { path })
504
630
  );
505
631
  },
506
- remove: async (dbId, path) => {
507
- const params = new URLSearchParams({ path });
508
- await fsRequest("DELETE", dbId, `/remove?${params.toString()}`);
632
+ events: async (dbId, options2) => {
633
+ const params = {};
634
+ if (options2?.limit !== void 0) params.limit = String(options2.limit);
635
+ if (options2?.offset !== void 0) params.offset = String(options2.offset);
636
+ if (options2?.path) params.path = options2.path;
637
+ if (options2?.type) params.type = options2.type;
638
+ return withFsAuthRetry(
639
+ dbId,
640
+ (client) => client.get("/events", params)
641
+ );
509
642
  }
510
643
  }
511
644
  };