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.cjs ADDED
@@ -0,0 +1,568 @@
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
+ class ApiError extends Error {
328
+ constructor(status, statusText, body){
329
+ const message = typeof body === "object" && body !== null ? extractMessage(body) ?? statusText : statusText;
330
+ super(message);
331
+ this.name = "ApiError";
332
+ this.status = status;
333
+ this.statusText = statusText;
334
+ this.body = body;
335
+ }
336
+ }
337
+ /** Extract a human-readable message from an API error body. */ function extractMessage(body) {
338
+ if (typeof body !== "object" || body === null) return undefined;
339
+ const obj = body;
340
+ // API returns { error: { code, message } }
341
+ if (typeof obj.error === "object" && obj.error !== null) {
342
+ const inner = obj.error;
343
+ if (typeof inner.message === "string") return inner.message;
344
+ }
345
+ if (typeof obj.message === "string") return obj.message;
346
+ return undefined;
347
+ }
348
+ const RETRYABLE_STATUS_CODES = new Set([
349
+ 408,
350
+ 429,
351
+ 502,
352
+ 503,
353
+ 504
354
+ ]);
355
+ const DEFAULT_RETRY = {
356
+ maxRetries: 5,
357
+ initialDelayMs: 500,
358
+ maxDelayMs: 30_000,
359
+ idempotencyKeys: true
360
+ };
361
+ function resolveRetry(config) {
362
+ if (!config) return {
363
+ ...DEFAULT_RETRY,
364
+ maxRetries: 0
365
+ };
366
+ return {
367
+ ...DEFAULT_RETRY,
368
+ ...config
369
+ };
370
+ }
371
+ /** Parse Retry-After header as milliseconds. Returns undefined if unparseable. */ function parseRetryAfter(response) {
372
+ const header = response.headers.get("Retry-After");
373
+ if (!header) return undefined;
374
+ // Integer seconds
375
+ const seconds = Number(header);
376
+ if (Number.isFinite(seconds) && seconds >= 0) {
377
+ return seconds * 1000;
378
+ }
379
+ // HTTP-date
380
+ const date = Date.parse(header);
381
+ if (Number.isFinite(date)) {
382
+ const ms = date - Date.now();
383
+ return ms > 0 ? ms : 0;
384
+ }
385
+ return undefined;
386
+ }
387
+ /** Compute exponential backoff with jitter. */ function backoff(attempt, initialDelayMs, maxDelayMs) {
388
+ const delay = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);
389
+ return delay * (0.5 + Math.random());
390
+ }
391
+ /** Generate a UUID v4 using crypto.randomUUID if available, otherwise fallback. */ function generateUUID() {
392
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
393
+ return crypto.randomUUID();
394
+ }
395
+ // Fallback for environments without crypto.randomUUID
396
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c)=>{
397
+ const r = Math.random() * 16 | 0;
398
+ const v = c === "x" ? r : r & 0x3 | 0x8;
399
+ return v.toString(16);
400
+ });
401
+ }
402
+ function isMutatingMethod(method) {
403
+ const upper = method.toUpperCase();
404
+ return upper === "POST" || upper === "PATCH";
405
+ }
406
+ async function fetcher(options) {
407
+ let url = options.path;
408
+ if (options.pathParams) {
409
+ for (const [key, value] of Object.entries(options.pathParams)){
410
+ url = url.replace(`{${key}}`, encodeURIComponent(value));
411
+ }
412
+ }
413
+ const fullUrl = new URL(url, options.baseUrl);
414
+ if (options.queryParams) {
415
+ for (const [key, value] of Object.entries(options.queryParams)){
416
+ if (value !== undefined && value !== null) {
417
+ fullUrl.searchParams.set(key, String(value));
418
+ }
419
+ }
420
+ }
421
+ const headers = {};
422
+ if (options.token) {
423
+ headers.Authorization = `Bearer ${options.token}`;
424
+ }
425
+ if (options.body !== undefined) {
426
+ headers["Content-Type"] = "application/json";
427
+ }
428
+ const fetchFn = options.fetchImpl ?? globalThis.fetch;
429
+ const retry = resolveRetry(options.retry);
430
+ const urlStr = fullUrl.toString();
431
+ const fetchBody = options.body !== undefined ? JSON.stringify(options.body) : undefined;
432
+ // Generate idempotency key once, reused across all retry attempts.
433
+ const idempotencyKey = retry.maxRetries > 0 && retry.idempotencyKeys && isMutatingMethod(options.method) ? generateUUID() : undefined;
434
+ for(let attempt = 0; attempt <= retry.maxRetries; attempt++){
435
+ const reqHeaders = {
436
+ ...headers
437
+ };
438
+ // Attach idempotency key on all attempts for mutating requests.
439
+ if (idempotencyKey) {
440
+ reqHeaders["Idempotency-Key"] = idempotencyKey;
441
+ }
442
+ try {
443
+ const response = await fetchFn(urlStr, {
444
+ method: options.method,
445
+ headers: reqHeaders,
446
+ body: fetchBody
447
+ });
448
+ if (options.onResponse) {
449
+ await options.onResponse(response);
450
+ }
451
+ if (response.ok) {
452
+ return parseResponseBody(response);
453
+ }
454
+ // Non-retryable error or last attempt — throw immediately.
455
+ if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === retry.maxRetries) {
456
+ const errorBody = await response.json().catch(()=>null);
457
+ throw new ApiError(response.status, response.statusText, errorBody);
458
+ }
459
+ // Retryable error — wait and retry.
460
+ const delay = parseRetryAfter(response) ?? backoff(attempt, retry.initialDelayMs, retry.maxDelayMs);
461
+ await sleep(delay);
462
+ } catch (err) {
463
+ // If it's already an ApiError, rethrow (we threw it above).
464
+ if (err instanceof ApiError) throw err;
465
+ // Network error — retry if we have attempts left.
466
+ if (attempt === retry.maxRetries) throw err;
467
+ const delay = backoff(attempt, retry.initialDelayMs, retry.maxDelayMs);
468
+ await sleep(delay);
469
+ }
470
+ }
471
+ // Unreachable — the loop either returns or throws.
472
+ throw new Error("fetcher: exhausted all retry attempts");
473
+ }
474
+ async function parseResponseBody(response) {
475
+ if (response.status === 204) return undefined;
476
+ const contentType = response.headers.get("content-type");
477
+ if (contentType?.includes("application/json")) {
478
+ return response.json();
479
+ }
480
+ // Fallback: try JSON, return undefined if body is empty/non-JSON
481
+ const text = await response.text();
482
+ if (!text) return undefined;
483
+ try {
484
+ return JSON.parse(text);
485
+ } catch {
486
+ return undefined;
487
+ }
488
+ }
489
+ function sleep(ms) {
490
+ return new Promise((resolve)=>setTimeout(resolve, ms));
491
+ }
492
+
493
+ class PgBeamClient {
494
+ constructor(options){
495
+ this._baseUrl = options.baseUrl;
496
+ this._tokenOrFn = options.token;
497
+ this._fetchImpl = options.fetch;
498
+ this._onResponse = options.onResponse;
499
+ this._retry = options.retry === false ? {
500
+ maxRetries: 0
501
+ } : options.retry ?? {
502
+ maxRetries: 5
503
+ };
504
+ }
505
+ async _resolveToken() {
506
+ if (typeof this._tokenOrFn === "function") {
507
+ return this._tokenOrFn();
508
+ }
509
+ return this._tokenOrFn;
510
+ }
511
+ _call(method, path) {
512
+ return async (params = {})=>{
513
+ const token = await this._resolveToken();
514
+ return fetcher({
515
+ method,
516
+ path,
517
+ pathParams: params.pathParams,
518
+ queryParams: params.queryParams,
519
+ body: params.body,
520
+ baseUrl: this._baseUrl,
521
+ token,
522
+ fetchImpl: this._fetchImpl,
523
+ onResponse: this._onResponse,
524
+ retry: this._retry
525
+ });
526
+ };
527
+ }
528
+ /** Access API operations via tag-based namespaces or `.request()`. */ get api() {
529
+ if (this._api) return this._api;
530
+ const request = async (route, params)=>{
531
+ const meta = operationsByPath[route];
532
+ if (!meta) throw new Error(`Unknown route: ${String(route)}`);
533
+ const token = await this._resolveToken();
534
+ return fetcher({
535
+ method: meta.method,
536
+ path: meta.path,
537
+ pathParams: params?.pathParams,
538
+ queryParams: params?.queryParams,
539
+ body: params?.body,
540
+ baseUrl: this._baseUrl,
541
+ token,
542
+ fetchImpl: this._fetchImpl,
543
+ onResponse: this._onResponse,
544
+ retry: this._retry
545
+ });
546
+ };
547
+ this._api = new Proxy({}, {
548
+ get: (_, prop)=>{
549
+ if (prop === "request") return request;
550
+ const tagOps = operationsByTag[prop];
551
+ if (!tagOps) return undefined;
552
+ return new Proxy({}, {
553
+ get: (_, method)=>{
554
+ const ops = tagOps;
555
+ const opMeta = ops[method];
556
+ if (!opMeta) return undefined;
557
+ return this._call(opMeta.method, opMeta.path);
558
+ }
559
+ });
560
+ }
561
+ });
562
+ return this._api;
563
+ }
564
+ }
565
+
566
+ exports.ApiError = ApiError;
567
+ exports.PgBeamClient = PgBeamClient;
568
+ exports.extractMessage = extractMessage;