pgbeam 0.1.0 → 0.2.1

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