pgbeam 0.2.0 → 0.2.2

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/index.mjs CHANGED
@@ -324,8 +324,7 @@ const operationsByPath = {
324
324
 
325
325
  class ApiError extends Error {
326
326
  constructor(status, statusText, body){
327
- var _extractMessage;
328
- const message = typeof body === "object" && body !== null ? (_extractMessage = extractMessage(body)) != null ? _extractMessage : statusText : statusText;
327
+ const message = typeof body === "object" && body !== null ? extractMessage(body) ?? statusText : statusText;
329
328
  super(message);
330
329
  this.name = "ApiError";
331
330
  this.status = status;
@@ -344,6 +343,64 @@ class ApiError extends Error {
344
343
  if (typeof obj.message === "string") return obj.message;
345
344
  return undefined;
346
345
  }
346
+ const RETRYABLE_STATUS_CODES = new Set([
347
+ 408,
348
+ 429,
349
+ 502,
350
+ 503,
351
+ 504
352
+ ]);
353
+ const DEFAULT_RETRY = {
354
+ maxRetries: 5,
355
+ initialDelayMs: 500,
356
+ maxDelayMs: 30_000,
357
+ idempotencyKeys: true
358
+ };
359
+ function resolveRetry(config) {
360
+ if (!config) return {
361
+ ...DEFAULT_RETRY,
362
+ maxRetries: 0
363
+ };
364
+ return {
365
+ ...DEFAULT_RETRY,
366
+ ...config
367
+ };
368
+ }
369
+ /** Parse Retry-After header as milliseconds. Returns undefined if unparseable. */ function parseRetryAfter(response) {
370
+ const header = response.headers.get("Retry-After");
371
+ if (!header) return undefined;
372
+ // Integer seconds
373
+ const seconds = Number(header);
374
+ if (Number.isFinite(seconds) && seconds >= 0) {
375
+ return seconds * 1000;
376
+ }
377
+ // HTTP-date
378
+ const date = Date.parse(header);
379
+ if (Number.isFinite(date)) {
380
+ const ms = date - Date.now();
381
+ return ms > 0 ? ms : 0;
382
+ }
383
+ return undefined;
384
+ }
385
+ /** Compute exponential backoff with jitter. */ function backoff(attempt, initialDelayMs, maxDelayMs) {
386
+ const delay = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);
387
+ return delay * (0.5 + Math.random());
388
+ }
389
+ /** Generate a UUID v4 using crypto.randomUUID if available, otherwise fallback. */ function generateUUID() {
390
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
391
+ return crypto.randomUUID();
392
+ }
393
+ // Fallback for environments without crypto.randomUUID
394
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c)=>{
395
+ const r = Math.random() * 16 | 0;
396
+ const v = c === "x" ? r : r & 0x3 | 0x8;
397
+ return v.toString(16);
398
+ });
399
+ }
400
+ function isMutatingMethod(method) {
401
+ const upper = method.toUpperCase();
402
+ return upper === "POST" || upper === "PATCH";
403
+ }
347
404
  async function fetcher(options) {
348
405
  let url = options.path;
349
406
  if (options.pathParams) {
@@ -366,32 +423,83 @@ async function fetcher(options) {
366
423
  if (options.body !== undefined) {
367
424
  headers["Content-Type"] = "application/json";
368
425
  }
369
- var _options_fetchImpl;
370
- const fetchFn = (_options_fetchImpl = options.fetchImpl) != null ? _options_fetchImpl : globalThis.fetch;
371
- const response = await fetchFn(fullUrl.toString(), {
372
- method: options.method,
373
- headers,
374
- body: options.body !== undefined ? JSON.stringify(options.body) : undefined
375
- });
376
- if (options.onResponse) {
377
- await options.onResponse(response);
378
- }
379
- if (!response.ok) {
380
- const errorBody = await response.json().catch(()=>null);
381
- throw new ApiError(response.status, response.statusText, errorBody);
426
+ const fetchFn = options.fetchImpl ?? globalThis.fetch;
427
+ const retry = resolveRetry(options.retry);
428
+ const urlStr = fullUrl.toString();
429
+ const fetchBody = options.body !== undefined ? JSON.stringify(options.body) : undefined;
430
+ // Generate idempotency key once, reused across all retry attempts.
431
+ const idempotencyKey = retry.maxRetries > 0 && retry.idempotencyKeys && isMutatingMethod(options.method) ? generateUUID() : undefined;
432
+ for(let attempt = 0; attempt <= retry.maxRetries; attempt++){
433
+ const reqHeaders = {
434
+ ...headers
435
+ };
436
+ // Attach idempotency key on all attempts for mutating requests.
437
+ if (idempotencyKey) {
438
+ reqHeaders["Idempotency-Key"] = idempotencyKey;
439
+ }
440
+ try {
441
+ const response = await fetchFn(urlStr, {
442
+ method: options.method,
443
+ headers: reqHeaders,
444
+ body: fetchBody
445
+ });
446
+ if (options.onResponse) {
447
+ await options.onResponse(response);
448
+ }
449
+ if (response.ok) {
450
+ return parseResponseBody(response);
451
+ }
452
+ // Non-retryable error or last attempt — throw immediately.
453
+ if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === retry.maxRetries) {
454
+ const errorBody = await response.json().catch(()=>null);
455
+ throw new ApiError(response.status, response.statusText, errorBody);
456
+ }
457
+ // Retryable error — wait and retry.
458
+ const delay = parseRetryAfter(response) ?? backoff(attempt, retry.initialDelayMs, retry.maxDelayMs);
459
+ await sleep(delay);
460
+ } catch (err) {
461
+ // If it's already an ApiError, rethrow (we threw it above).
462
+ if (err instanceof ApiError) throw err;
463
+ // Network error — retry if we have attempts left.
464
+ if (attempt === retry.maxRetries) throw err;
465
+ const delay = backoff(attempt, retry.initialDelayMs, retry.maxDelayMs);
466
+ await sleep(delay);
467
+ }
382
468
  }
469
+ // Unreachable — the loop either returns or throws.
470
+ throw new Error("fetcher: exhausted all retry attempts");
471
+ }
472
+ async function parseResponseBody(response) {
383
473
  if (response.status === 204) return undefined;
384
474
  const contentType = response.headers.get("content-type");
385
- if (contentType == null ? void 0 : contentType.includes("application/json")) {
475
+ if (contentType?.includes("application/json")) {
386
476
  return response.json();
387
477
  }
388
478
  // Fallback: try JSON, return undefined if body is empty/non-JSON
389
479
  const text = await response.text();
390
480
  if (!text) return undefined;
391
- return JSON.parse(text);
481
+ try {
482
+ return JSON.parse(text);
483
+ } catch {
484
+ return undefined;
485
+ }
486
+ }
487
+ function sleep(ms) {
488
+ return new Promise((resolve)=>setTimeout(resolve, ms));
392
489
  }
393
490
 
394
491
  class PgBeamClient {
492
+ constructor(options){
493
+ this._baseUrl = options.baseUrl;
494
+ this._tokenOrFn = options.token;
495
+ this._fetchImpl = options.fetch;
496
+ this._onResponse = options.onResponse;
497
+ this._retry = options.retry === false ? {
498
+ maxRetries: 0
499
+ } : options.retry ?? {
500
+ maxRetries: 5
501
+ };
502
+ }
395
503
  async _resolveToken() {
396
504
  if (typeof this._tokenOrFn === "function") {
397
505
  return this._tokenOrFn();
@@ -410,7 +518,8 @@ class PgBeamClient {
410
518
  baseUrl: this._baseUrl,
411
519
  token,
412
520
  fetchImpl: this._fetchImpl,
413
- onResponse: this._onResponse
521
+ onResponse: this._onResponse,
522
+ retry: this._retry
414
523
  });
415
524
  };
416
525
  }
@@ -423,13 +532,14 @@ class PgBeamClient {
423
532
  return fetcher({
424
533
  method: meta.method,
425
534
  path: meta.path,
426
- pathParams: params == null ? void 0 : params.pathParams,
427
- queryParams: params == null ? void 0 : params.queryParams,
428
- body: params == null ? void 0 : params.body,
535
+ pathParams: params?.pathParams,
536
+ queryParams: params?.queryParams,
537
+ body: params?.body,
429
538
  baseUrl: this._baseUrl,
430
539
  token,
431
540
  fetchImpl: this._fetchImpl,
432
- onResponse: this._onResponse
541
+ onResponse: this._onResponse,
542
+ retry: this._retry
433
543
  });
434
544
  };
435
545
  this._api = new Proxy({}, {
@@ -449,12 +559,6 @@ class PgBeamClient {
449
559
  });
450
560
  return this._api;
451
561
  }
452
- constructor(options){
453
- this._baseUrl = options.baseUrl;
454
- this._tokenOrFn = options.token;
455
- this._fetchImpl = options.fetch;
456
- this._onResponse = options.onResponse;
457
- }
458
562
  }
459
563
 
460
564
  export { ApiError, PgBeamClient, extractMessage };
@@ -0,0 +1,328 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ /**
4
+ * Generated by scripts/generate-maps.ts — do not edit manually.
5
+ * Run: pnpm --filter pgbeam generate
6
+ */ const operationsByTag = {
7
+ account: {
8
+ exportAccountData: {
9
+ method: "GET",
10
+ path: "/v1/account/export"
11
+ },
12
+ getOnboardingProgress: {
13
+ method: "GET",
14
+ path: "/v1/organizations/{org_id}/onboarding"
15
+ },
16
+ updateOnboardingProgress: {
17
+ method: "PATCH",
18
+ path: "/v1/organizations/{org_id}/onboarding"
19
+ }
20
+ },
21
+ analytics: {
22
+ getOrganizationActivity: {
23
+ method: "GET",
24
+ path: "/v1/organizations/{org_id}/activity"
25
+ },
26
+ listPlans: {
27
+ method: "GET",
28
+ path: "/v1/plans"
29
+ },
30
+ getOrganizationPlan: {
31
+ method: "GET",
32
+ path: "/v1/organizations/{org_id}/plan"
33
+ },
34
+ updateSpendLimit: {
35
+ method: "PUT",
36
+ path: "/v1/organizations/{org_id}/spend-limit"
37
+ },
38
+ submitCancellationFeedback: {
39
+ method: "POST",
40
+ path: "/v1/organizations/{org_id}/cancellation-feedback"
41
+ },
42
+ getProjectInsights: {
43
+ method: "GET",
44
+ path: "/v1/projects/{project_id}/insights"
45
+ },
46
+ getOrganizationUsage: {
47
+ method: "GET",
48
+ path: "/v1/organizations/{org_id}/usage"
49
+ },
50
+ getProjectUsage: {
51
+ method: "GET",
52
+ path: "/v1/projects/{project_id}/usage"
53
+ }
54
+ },
55
+ projects: {
56
+ listCacheRules: {
57
+ method: "GET",
58
+ path: "/v1/projects/{project_id}/databases/{database_id}/cache-rules"
59
+ },
60
+ updateCacheRule: {
61
+ method: "PUT",
62
+ path: "/v1/projects/{project_id}/databases/{database_id}/cache-rules/{query_hash}"
63
+ },
64
+ listCustomDomains: {
65
+ method: "GET",
66
+ path: "/v1/projects/{project_id}/domains"
67
+ },
68
+ createCustomDomain: {
69
+ method: "POST",
70
+ path: "/v1/projects/{project_id}/domains"
71
+ },
72
+ deleteCustomDomain: {
73
+ method: "DELETE",
74
+ path: "/v1/projects/{project_id}/domains/{domain_id}"
75
+ },
76
+ verifyCustomDomain: {
77
+ method: "POST",
78
+ path: "/v1/projects/{project_id}/domains/{domain_id}/verify"
79
+ },
80
+ listProjects: {
81
+ method: "GET",
82
+ path: "/v1/projects"
83
+ },
84
+ createProject: {
85
+ method: "POST",
86
+ path: "/v1/projects"
87
+ },
88
+ getProject: {
89
+ method: "GET",
90
+ path: "/v1/projects/{project_id}"
91
+ },
92
+ updateProject: {
93
+ method: "PATCH",
94
+ path: "/v1/projects/{project_id}"
95
+ },
96
+ deleteProject: {
97
+ method: "DELETE",
98
+ path: "/v1/projects/{project_id}"
99
+ },
100
+ getProjectMetrics: {
101
+ method: "GET",
102
+ path: "/v1/projects/{project_id}/metrics"
103
+ },
104
+ listReplicas: {
105
+ method: "GET",
106
+ path: "/v1/databases/{database_id}/replicas"
107
+ },
108
+ createReplica: {
109
+ method: "POST",
110
+ path: "/v1/databases/{database_id}/replicas"
111
+ },
112
+ deleteReplica: {
113
+ method: "DELETE",
114
+ path: "/v1/databases/{database_id}/replicas/{replica_id}"
115
+ }
116
+ },
117
+ databases: {
118
+ listDatabases: {
119
+ method: "GET",
120
+ path: "/v1/projects/{project_id}/databases"
121
+ },
122
+ createDatabase: {
123
+ method: "POST",
124
+ path: "/v1/projects/{project_id}/databases"
125
+ },
126
+ getDatabase: {
127
+ method: "GET",
128
+ path: "/v1/projects/{project_id}/databases/{database_id}"
129
+ },
130
+ updateDatabase: {
131
+ method: "PATCH",
132
+ path: "/v1/projects/{project_id}/databases/{database_id}"
133
+ },
134
+ deleteDatabase: {
135
+ method: "DELETE",
136
+ path: "/v1/projects/{project_id}/databases/{database_id}"
137
+ },
138
+ testDatabaseConnection: {
139
+ method: "POST",
140
+ path: "/v1/projects/{project_id}/databases/{database_id}/test-connection"
141
+ }
142
+ },
143
+ platform: {
144
+ getHealth: {
145
+ method: "GET",
146
+ path: "/v1/health"
147
+ },
148
+ listRegions: {
149
+ method: "GET",
150
+ path: "/v1/regions"
151
+ }
152
+ }
153
+ };
154
+ const operationsByPath = {
155
+ "GET /v1/account/export": {
156
+ method: "GET",
157
+ path: "/v1/account/export",
158
+ operationId: "exportAccountData"
159
+ },
160
+ "GET /v1/organizations/{org_id}/activity": {
161
+ method: "GET",
162
+ path: "/v1/organizations/{org_id}/activity",
163
+ operationId: "getOrganizationActivity"
164
+ },
165
+ "GET /v1/plans": {
166
+ method: "GET",
167
+ path: "/v1/plans",
168
+ operationId: "listPlans"
169
+ },
170
+ "GET /v1/organizations/{org_id}/plan": {
171
+ method: "GET",
172
+ path: "/v1/organizations/{org_id}/plan",
173
+ operationId: "getOrganizationPlan"
174
+ },
175
+ "PUT /v1/organizations/{org_id}/spend-limit": {
176
+ method: "PUT",
177
+ path: "/v1/organizations/{org_id}/spend-limit",
178
+ operationId: "updateSpendLimit"
179
+ },
180
+ "POST /v1/organizations/{org_id}/cancellation-feedback": {
181
+ method: "POST",
182
+ path: "/v1/organizations/{org_id}/cancellation-feedback",
183
+ operationId: "submitCancellationFeedback"
184
+ },
185
+ "GET /v1/projects/{project_id}/databases/{database_id}/cache-rules": {
186
+ method: "GET",
187
+ path: "/v1/projects/{project_id}/databases/{database_id}/cache-rules",
188
+ operationId: "listCacheRules"
189
+ },
190
+ "PUT /v1/projects/{project_id}/databases/{database_id}/cache-rules/{query_hash}": {
191
+ method: "PUT",
192
+ path: "/v1/projects/{project_id}/databases/{database_id}/cache-rules/{query_hash}",
193
+ operationId: "updateCacheRule"
194
+ },
195
+ "GET /v1/projects/{project_id}/databases": {
196
+ method: "GET",
197
+ path: "/v1/projects/{project_id}/databases",
198
+ operationId: "listDatabases"
199
+ },
200
+ "POST /v1/projects/{project_id}/databases": {
201
+ method: "POST",
202
+ path: "/v1/projects/{project_id}/databases",
203
+ operationId: "createDatabase"
204
+ },
205
+ "GET /v1/projects/{project_id}/databases/{database_id}": {
206
+ method: "GET",
207
+ path: "/v1/projects/{project_id}/databases/{database_id}",
208
+ operationId: "getDatabase"
209
+ },
210
+ "PATCH /v1/projects/{project_id}/databases/{database_id}": {
211
+ method: "PATCH",
212
+ path: "/v1/projects/{project_id}/databases/{database_id}",
213
+ operationId: "updateDatabase"
214
+ },
215
+ "DELETE /v1/projects/{project_id}/databases/{database_id}": {
216
+ method: "DELETE",
217
+ path: "/v1/projects/{project_id}/databases/{database_id}",
218
+ operationId: "deleteDatabase"
219
+ },
220
+ "POST /v1/projects/{project_id}/databases/{database_id}/test-connection": {
221
+ method: "POST",
222
+ path: "/v1/projects/{project_id}/databases/{database_id}/test-connection",
223
+ operationId: "testDatabaseConnection"
224
+ },
225
+ "GET /v1/projects/{project_id}/domains": {
226
+ method: "GET",
227
+ path: "/v1/projects/{project_id}/domains",
228
+ operationId: "listCustomDomains"
229
+ },
230
+ "POST /v1/projects/{project_id}/domains": {
231
+ method: "POST",
232
+ path: "/v1/projects/{project_id}/domains",
233
+ operationId: "createCustomDomain"
234
+ },
235
+ "DELETE /v1/projects/{project_id}/domains/{domain_id}": {
236
+ method: "DELETE",
237
+ path: "/v1/projects/{project_id}/domains/{domain_id}",
238
+ operationId: "deleteCustomDomain"
239
+ },
240
+ "POST /v1/projects/{project_id}/domains/{domain_id}/verify": {
241
+ method: "POST",
242
+ path: "/v1/projects/{project_id}/domains/{domain_id}/verify",
243
+ operationId: "verifyCustomDomain"
244
+ },
245
+ "GET /v1/health": {
246
+ method: "GET",
247
+ path: "/v1/health",
248
+ operationId: "getHealth"
249
+ },
250
+ "GET /v1/projects/{project_id}/insights": {
251
+ method: "GET",
252
+ path: "/v1/projects/{project_id}/insights",
253
+ operationId: "getProjectInsights"
254
+ },
255
+ "GET /v1/organizations/{org_id}/onboarding": {
256
+ method: "GET",
257
+ path: "/v1/organizations/{org_id}/onboarding",
258
+ operationId: "getOnboardingProgress"
259
+ },
260
+ "PATCH /v1/organizations/{org_id}/onboarding": {
261
+ method: "PATCH",
262
+ path: "/v1/organizations/{org_id}/onboarding",
263
+ operationId: "updateOnboardingProgress"
264
+ },
265
+ "GET /v1/projects": {
266
+ method: "GET",
267
+ path: "/v1/projects",
268
+ operationId: "listProjects"
269
+ },
270
+ "POST /v1/projects": {
271
+ method: "POST",
272
+ path: "/v1/projects",
273
+ operationId: "createProject"
274
+ },
275
+ "GET /v1/projects/{project_id}": {
276
+ method: "GET",
277
+ path: "/v1/projects/{project_id}",
278
+ operationId: "getProject"
279
+ },
280
+ "PATCH /v1/projects/{project_id}": {
281
+ method: "PATCH",
282
+ path: "/v1/projects/{project_id}",
283
+ operationId: "updateProject"
284
+ },
285
+ "DELETE /v1/projects/{project_id}": {
286
+ method: "DELETE",
287
+ path: "/v1/projects/{project_id}",
288
+ operationId: "deleteProject"
289
+ },
290
+ "GET /v1/projects/{project_id}/metrics": {
291
+ method: "GET",
292
+ path: "/v1/projects/{project_id}/metrics",
293
+ operationId: "getProjectMetrics"
294
+ },
295
+ "GET /v1/regions": {
296
+ method: "GET",
297
+ path: "/v1/regions",
298
+ operationId: "listRegions"
299
+ },
300
+ "GET /v1/databases/{database_id}/replicas": {
301
+ method: "GET",
302
+ path: "/v1/databases/{database_id}/replicas",
303
+ operationId: "listReplicas"
304
+ },
305
+ "POST /v1/databases/{database_id}/replicas": {
306
+ method: "POST",
307
+ path: "/v1/databases/{database_id}/replicas",
308
+ operationId: "createReplica"
309
+ },
310
+ "DELETE /v1/databases/{database_id}/replicas/{replica_id}": {
311
+ method: "DELETE",
312
+ path: "/v1/databases/{database_id}/replicas/{replica_id}",
313
+ operationId: "deleteReplica"
314
+ },
315
+ "GET /v1/organizations/{org_id}/usage": {
316
+ method: "GET",
317
+ path: "/v1/organizations/{org_id}/usage",
318
+ operationId: "getOrganizationUsage"
319
+ },
320
+ "GET /v1/projects/{project_id}/usage": {
321
+ method: "GET",
322
+ path: "/v1/projects/{project_id}/usage",
323
+ operationId: "getProjectUsage"
324
+ }
325
+ };
326
+
327
+ exports.operationsByPath = operationsByPath;
328
+ exports.operationsByTag = operationsByTag;