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