@xylex-group/athena 1.2.1 → 1.3.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/README.md +188 -85
- package/dist/index.cjs +324 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -1
- package/dist/index.d.ts +115 -1
- package/dist/index.js +315 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# athena-js
|
|
2
2
|
|
|
3
|
-
current version: `1.
|
|
3
|
+
current version: `1.3.0`
|
|
4
4
|
`@xylex-group/athena` is a database driver and API gateway SDK that lets you interact with SQL backends over HTTP through a fluent builder API. It ships a typed query builder for Node.js / server environments and a React hook for client-side use.
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -43,9 +43,94 @@ if (error) {
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Every query resolves to `{ data, error, errorDetails?, status, count?, raw }`. `data` is `null` on error; `error` is `null` on success.
|
|
47
|
-
|
|
48
|
-
For richer handling, inspect `errorDetails` (`code`, `status`, `endpoint`, `method`, `requestId`, etc.) or use `AthenaGatewayError` / `isAthenaGatewayError` from the package exports.
|
|
46
|
+
Every query resolves to `{ data, error, errorDetails?, status, count?, raw }`. `data` is `null` on error; `error` is `null` on success.
|
|
47
|
+
|
|
48
|
+
For richer handling, inspect `errorDetails` (`code`, `status`, `endpoint`, `method`, `requestId`, etc.) or use `AthenaGatewayError` / `isAthenaGatewayError` from the package exports.
|
|
49
|
+
|
|
50
|
+
## Reliability helper APIs
|
|
51
|
+
|
|
52
|
+
The SDK exports composable helpers to reduce repetitive route-handler logic.
|
|
53
|
+
|
|
54
|
+
### Result unwrapping and success guards
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
isOk,
|
|
59
|
+
unwrap,
|
|
60
|
+
unwrapRows,
|
|
61
|
+
unwrapOne,
|
|
62
|
+
requireSuccess,
|
|
63
|
+
requireAffected,
|
|
64
|
+
} from "@xylex-group/athena";
|
|
65
|
+
|
|
66
|
+
const result = await athena.from("users").select("id,name");
|
|
67
|
+
|
|
68
|
+
if (isOk(result)) {
|
|
69
|
+
const rows = unwrapRows(result); // typed User[]
|
|
70
|
+
console.log(rows.length);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const one = await athena.from("users").eq("id", 1).single("id,name");
|
|
74
|
+
const user = unwrapOne(one, { allowNull: true });
|
|
75
|
+
|
|
76
|
+
const inserted = await athena
|
|
77
|
+
.from("users")
|
|
78
|
+
.insert({ name: "Alice" })
|
|
79
|
+
.select("id", { count: "exact" });
|
|
80
|
+
|
|
81
|
+
requireSuccess(inserted, { table: "users", operation: "insert" });
|
|
82
|
+
requireAffected(inserted, { min: 1 }, { table: "users", operation: "insert" });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`requireAffected` uses `result.count`; request it on writes with `{ count: "exact" }` when you need enforced postconditions.
|
|
86
|
+
|
|
87
|
+
### Error normalization
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { normalizeAthenaError } from "@xylex-group/athena";
|
|
91
|
+
|
|
92
|
+
const result = await athena.from("users").insert({ id: 1 }).select();
|
|
93
|
+
if (result.error) {
|
|
94
|
+
const err = normalizeAthenaError(result, {
|
|
95
|
+
table: "users",
|
|
96
|
+
operation: "insert",
|
|
97
|
+
});
|
|
98
|
+
if (err.kind === "unique_violation") {
|
|
99
|
+
// deterministic conflict handling
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Normalized errors expose stable `kind` values (`unique_violation`, `validation`, `auth`, `rate_limit`, `transient`, etc.) plus operation metadata.
|
|
105
|
+
|
|
106
|
+
### Numeric coercion
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { coerceInt, assertInt } from "@xylex-group/athena";
|
|
110
|
+
|
|
111
|
+
const maybeCaseId = coerceInt(req.query.case_id, { min: 1 });
|
|
112
|
+
if (maybeCaseId == null) throw new Error("Invalid case id");
|
|
113
|
+
|
|
114
|
+
const caseId = assertInt(req.query.case_id, "case_id", { min: 1 });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Retry helper
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { withRetry } from "@xylex-group/athena";
|
|
121
|
+
|
|
122
|
+
const result = await withRetry(
|
|
123
|
+
{
|
|
124
|
+
retries: 3,
|
|
125
|
+
backoff: "exponential",
|
|
126
|
+
baseDelayMs: 100,
|
|
127
|
+
jitter: true,
|
|
128
|
+
},
|
|
129
|
+
() => athena.from("users").select("id,name"),
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
By default, retries target transient/rate-limit failures; use `shouldRetry` for custom policies.
|
|
49
134
|
|
|
50
135
|
## Query builder
|
|
51
136
|
|
|
@@ -64,37 +149,37 @@ const { data } = await athena.from<User>("users").select("id, name");
|
|
|
64
149
|
|
|
65
150
|
### Filters
|
|
66
151
|
|
|
67
|
-
Filters accumulate on the builder and are sent together when the query executes.
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
const { data } = await athena
|
|
152
|
+
Filters accumulate on the builder and are sent together when the query executes.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const { data } = await athena
|
|
71
156
|
.from("characters")
|
|
72
157
|
.select("id, name")
|
|
73
|
-
.eq("active", true)
|
|
74
|
-
.neq("role", "guest")
|
|
75
|
-
.gt("level", 5)
|
|
76
|
-
.gte("score", 100)
|
|
77
|
-
.lt("age", 30)
|
|
158
|
+
.eq("active", true) // column = value
|
|
159
|
+
.neq("role", "guest") // column != value
|
|
160
|
+
.gt("level", 5) // column > value
|
|
161
|
+
.gte("score", 100) // column >= value
|
|
162
|
+
.lt("age", 30) // column < value
|
|
78
163
|
.lte("created_at", "2024-01-01") // column <= value
|
|
79
|
-
.like("name", "Ali%")
|
|
164
|
+
.like("name", "Ali%") // SQL LIKE (case-sensitive)
|
|
80
165
|
.ilike("email", "%@example%") // SQL ILIKE (case-insensitive)
|
|
81
|
-
.is("deleted_at", null)
|
|
82
|
-
.in("status", ["active", "pending"])
|
|
83
|
-
.contains("tags", ["hero"])
|
|
166
|
+
.is("deleted_at", null) // IS NULL / IS TRUE etc.
|
|
167
|
+
.in("status", ["active", "pending"]) // IN (…)
|
|
168
|
+
.contains("tags", ["hero"]) // array contains value
|
|
84
169
|
.containedBy("tags", ["hero", "villain"]) // array is subset of value
|
|
85
170
|
.match({ role: "admin", active: true }) // multiple eq filters at once
|
|
86
|
-
.not("role", "eq", "banned")
|
|
87
|
-
.or("status.eq.active,status.eq.pending"); // OR expression
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Canonical style for reads is to call `.select(...)` first, then apply filters:
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
const { data } = await athena
|
|
94
|
-
.from("instruments")
|
|
95
|
-
.select("name, section_id")
|
|
96
|
-
.eq("name", "violin");
|
|
97
|
-
```
|
|
171
|
+
.not("role", "eq", "banned") // NOT col op val
|
|
172
|
+
.or("status.eq.active,status.eq.pending"); // OR expression
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Canonical style for reads is to call `.select(...)` first, then apply filters:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const { data } = await athena
|
|
179
|
+
.from("instruments")
|
|
180
|
+
.select("name, section_id")
|
|
181
|
+
.eq("name", "violin");
|
|
182
|
+
```
|
|
98
183
|
|
|
99
184
|
### Pagination
|
|
100
185
|
|
|
@@ -119,40 +204,47 @@ const { data: user } = await athena
|
|
|
119
204
|
|
|
120
205
|
`maybeSingle` behaves identically — both return the first element of the result set.
|
|
121
206
|
|
|
122
|
-
### RPC
|
|
123
|
-
|
|
124
|
-
Use `athena.rpc(...)` for Postgres function calls. By default it calls `POST /gateway/rpc`, and with `{ get: true }` it uses the compatibility route `GET /rpc/{function_name}`.
|
|
125
|
-
|
|
126
|
-
```ts
|
|
127
|
-
const { data, count } = await athena
|
|
128
|
-
.rpc("list_users", { role: "admin" }, { count: "exact", schema: "public" })
|
|
129
|
-
.eq("active", true)
|
|
130
|
-
.order("created_at", { ascending: false })
|
|
131
|
-
.range(0, 24)
|
|
132
|
-
.select(["id", "email"]);
|
|
133
|
-
|
|
134
|
-
const { data: firstUser } = await athena
|
|
135
|
-
.rpc<{ id: number; email: string }>("list_users", { role: "admin" })
|
|
136
|
-
.single("id,email");
|
|
137
|
-
|
|
138
|
-
const { data: readOnlyUser } = await athena
|
|
139
|
-
.rpc<{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
207
|
+
### RPC
|
|
208
|
+
|
|
209
|
+
Use `athena.rpc(...)` for Postgres function calls. By default it calls `POST /gateway/rpc`, and with `{ get: true }` it uses the compatibility route `GET /rpc/{function_name}`.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const { data, count } = await athena
|
|
213
|
+
.rpc("list_users", { role: "admin" }, { count: "exact", schema: "public" })
|
|
214
|
+
.eq("active", true)
|
|
215
|
+
.order("created_at", { ascending: false })
|
|
216
|
+
.range(0, 24)
|
|
217
|
+
.select(["id", "email"]);
|
|
218
|
+
|
|
219
|
+
const { data: firstUser } = await athena
|
|
220
|
+
.rpc<{ id: number; email: string }>("list_users", { role: "admin" })
|
|
221
|
+
.single("id,email");
|
|
222
|
+
|
|
223
|
+
const { data: readOnlyUser } = await athena
|
|
224
|
+
.rpc<{
|
|
225
|
+
id: number;
|
|
226
|
+
email: string;
|
|
227
|
+
}>(
|
|
228
|
+
"list_users",
|
|
229
|
+
{ role: "admin" },
|
|
230
|
+
{ get: true, count: "planned", head: true },
|
|
231
|
+
)
|
|
232
|
+
.eq("id", 1)
|
|
233
|
+
.single("id,email");
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
RPC chain methods: `.eq()`, `.neq()`, `.gt()`, `.gte()`, `.lt()`, `.lte()`, `.like()`, `.ilike()`, `.is()`, `.in()`, `.order()`, `.limit()`, `.offset()`, `.range()`, `.select()`, `.single()`, `.maybeSingle()`.
|
|
237
|
+
RPC options: `schema`, `count` (`"exact" | "planned" | "estimated"`), `head`, `get`.
|
|
238
|
+
|
|
239
|
+
### Options
|
|
148
240
|
|
|
149
241
|
Pass options as the second argument to `.select()`:
|
|
150
242
|
|
|
151
|
-
| Option
|
|
152
|
-
|
|
153
|
-
| `count`
|
|
154
|
-
| `head`
|
|
155
|
-
| `stripNulls` | `boolean`
|
|
243
|
+
| Option | Type | Description |
|
|
244
|
+
| ------------ | ------------------------------------- | -------------------------------------------- |
|
|
245
|
+
| `count` | `"exact" \| "planned" \| "estimated"` | request a row count alongside the data |
|
|
246
|
+
| `head` | `boolean` | return response headers only (no rows) |
|
|
247
|
+
| `stripNulls` | `boolean` | strip null values from rows (default `true`) |
|
|
156
248
|
|
|
157
249
|
```ts
|
|
158
250
|
const { data } = await athena
|
|
@@ -178,9 +270,9 @@ const { data } = await athena
|
|
|
178
270
|
.insert([{ name: "Frodo" }, { name: "Sam" }])
|
|
179
271
|
.select();
|
|
180
272
|
|
|
181
|
-
// Type inference differs by payload shape:
|
|
182
|
-
// - insert(one) => AthenaResult<Row>
|
|
183
|
-
// - insert(many) => AthenaResult<Row[]>
|
|
273
|
+
// Type inference differs by payload shape:
|
|
274
|
+
// - insert(one) => AthenaResult<Row>
|
|
275
|
+
// - insert(many) => AthenaResult<Row[]>
|
|
184
276
|
```
|
|
185
277
|
|
|
186
278
|
### Update
|
|
@@ -206,18 +298,18 @@ const { data } = await athena
|
|
|
206
298
|
)
|
|
207
299
|
.select();
|
|
208
300
|
|
|
209
|
-
// Type inference differs by payload shape:
|
|
210
|
-
// - upsert(one) => AthenaResult<Row>
|
|
211
|
-
// - upsert(many) => AthenaResult<Row[]>
|
|
301
|
+
// Type inference differs by payload shape:
|
|
302
|
+
// - upsert(one) => AthenaResult<Row>
|
|
303
|
+
// - upsert(many) => AthenaResult<Row[]>
|
|
212
304
|
```
|
|
213
305
|
|
|
214
|
-
| Option
|
|
215
|
-
|
|
216
|
-
| `onConflict`
|
|
217
|
-
| `updateBody`
|
|
218
|
-
| `defaultToNull` | `boolean`
|
|
219
|
-
| `count`
|
|
220
|
-
| `head`
|
|
306
|
+
| Option | Type | Description |
|
|
307
|
+
| --------------- | ------------------------------------- | ---------------------------------------- |
|
|
308
|
+
| `onConflict` | `string \| string[]` | column(s) that determine a conflict |
|
|
309
|
+
| `updateBody` | `object` | fields to apply when a conflict occurs |
|
|
310
|
+
| `defaultToNull` | `boolean` | write explicit `null` for missing fields |
|
|
311
|
+
| `count` | `"exact" \| "planned" \| "estimated"` | request a row count |
|
|
312
|
+
| `head` | `boolean` | return headers only |
|
|
221
313
|
|
|
222
314
|
### Delete
|
|
223
315
|
|
|
@@ -286,7 +378,7 @@ export function UsersPanel() {
|
|
|
286
378
|
}
|
|
287
379
|
```
|
|
288
380
|
|
|
289
|
-
The hook returns `fetchGateway`, `insertGateway`, `updateGateway`, `deleteGateway`, `rpcGateway`, `isLoading`, `error`, `lastRequest`, `lastResponse`, and `baseUrl`.
|
|
381
|
+
The hook returns `fetchGateway`, `insertGateway`, `updateGateway`, `deleteGateway`, `rpcGateway`, `isLoading`, `error`, `lastRequest`, `lastResponse`, and `baseUrl`.
|
|
290
382
|
|
|
291
383
|
Hook config options mirror the client options: `baseUrl`, `apiKey`, `headers`, `userId`, `organizationId`, `publishEvent`.
|
|
292
384
|
|
|
@@ -295,24 +387,32 @@ Hook config options mirror the client options: `baseUrl`, `apiKey`, `headers`, `
|
|
|
295
387
|
Pass user and tenant context to every request without repeating it on each call:
|
|
296
388
|
|
|
297
389
|
```ts
|
|
298
|
-
const athena = createClient(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
390
|
+
const athena = createClient(
|
|
391
|
+
"https://athena-db.com",
|
|
392
|
+
process.env.ATHENA_API_KEY,
|
|
393
|
+
{
|
|
394
|
+
headers: {
|
|
395
|
+
"X-User-Id": currentUser.id,
|
|
396
|
+
"X-Organization-Id": currentUser.organizationId ?? "",
|
|
397
|
+
},
|
|
302
398
|
},
|
|
303
|
-
|
|
399
|
+
);
|
|
304
400
|
```
|
|
305
401
|
|
|
306
|
-
Or pass per-call via options. The Athena server interprets `url` and `key` based on the configured backend type.
|
|
402
|
+
Or pass per-call via options. The Athena server interprets `url` and `key` based on the configured backend type.
|
|
307
403
|
|
|
308
404
|
## Custom headers
|
|
309
405
|
|
|
310
406
|
```ts
|
|
311
|
-
const athena = createClient(
|
|
312
|
-
|
|
313
|
-
|
|
407
|
+
const athena = createClient(
|
|
408
|
+
"https://athena-db.com",
|
|
409
|
+
process.env.ATHENA_API_KEY,
|
|
410
|
+
{
|
|
411
|
+
headers: {
|
|
412
|
+
"X-Custom-Header": "value",
|
|
413
|
+
},
|
|
314
414
|
},
|
|
315
|
-
|
|
415
|
+
);
|
|
316
416
|
```
|
|
317
417
|
|
|
318
418
|
Per-call headers are merged with the client-level headers, with per-call values winning on conflict.
|
|
@@ -329,7 +429,10 @@ interface User {
|
|
|
329
429
|
active: boolean;
|
|
330
430
|
}
|
|
331
431
|
|
|
332
|
-
const { data } = await athena
|
|
432
|
+
const { data } = await athena
|
|
433
|
+
.from<User>("users")
|
|
434
|
+
.select("id, name")
|
|
435
|
+
.eq("active", true);
|
|
333
436
|
// data is User[] | null
|
|
334
437
|
```
|
|
335
438
|
|
package/dist/index.cjs
CHANGED
|
@@ -964,10 +964,334 @@ var Backend = {
|
|
|
964
964
|
ScyllaDB: { type: "scylladb" }
|
|
965
965
|
};
|
|
966
966
|
|
|
967
|
+
// src/auxiliaries.ts
|
|
968
|
+
function isRecord2(value) {
|
|
969
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
970
|
+
}
|
|
971
|
+
function safeStringify(value) {
|
|
972
|
+
try {
|
|
973
|
+
const serialized = JSON.stringify(value);
|
|
974
|
+
return serialized ?? String(value);
|
|
975
|
+
} catch {
|
|
976
|
+
return "[unserializable]";
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function contextHint(context) {
|
|
980
|
+
if (!context?.identity) return void 0;
|
|
981
|
+
const identity = typeof context.identity === "string" ? context.identity : safeStringify(context.identity);
|
|
982
|
+
return `Identity: ${identity}`;
|
|
983
|
+
}
|
|
984
|
+
function isAthenaResultLike(value) {
|
|
985
|
+
return isRecord2(value) && "status" in value && "error" in value && "data" in value && typeof value.status === "number";
|
|
986
|
+
}
|
|
987
|
+
function operationFromDetails(details) {
|
|
988
|
+
if (!details?.endpoint) return void 0;
|
|
989
|
+
if (details.endpoint === "/gateway/fetch" || details.endpoint === "/gateway/query") return "select";
|
|
990
|
+
if (details.endpoint === "/gateway/insert") return "insert";
|
|
991
|
+
if (details.endpoint === "/gateway/update") return "update";
|
|
992
|
+
if (details.endpoint === "/gateway/delete") return "delete";
|
|
993
|
+
if (details.endpoint === "/gateway/rpc" || details.endpoint.startsWith("/rpc/")) return "rpc";
|
|
994
|
+
return void 0;
|
|
995
|
+
}
|
|
996
|
+
function matchRegex(input, patterns) {
|
|
997
|
+
for (const pattern of patterns) {
|
|
998
|
+
const match = pattern.exec(input);
|
|
999
|
+
if (match?.[1]) return match[1];
|
|
1000
|
+
}
|
|
1001
|
+
return void 0;
|
|
1002
|
+
}
|
|
1003
|
+
function extractConstraint(message) {
|
|
1004
|
+
return matchRegex(message, [
|
|
1005
|
+
/unique constraint\s+["'`]([^"'`]+)["'`]/i,
|
|
1006
|
+
/constraint\s+["'`]([^"'`]+)["'`]/i
|
|
1007
|
+
]);
|
|
1008
|
+
}
|
|
1009
|
+
function extractTable(message) {
|
|
1010
|
+
return matchRegex(message, [
|
|
1011
|
+
/(?:table|relation)\s+["'`]([^"'`]+)["'`]/i,
|
|
1012
|
+
/on\s+table\s+([a-zA-Z0-9_.]+)/i
|
|
1013
|
+
]);
|
|
1014
|
+
}
|
|
1015
|
+
function classifyKind(status, code, message) {
|
|
1016
|
+
const lower = message.toLowerCase();
|
|
1017
|
+
const hasUniquePattern = lower.includes("unique constraint") || lower.includes("duplicate key") || lower.includes("already exists") || lower.includes("duplicate");
|
|
1018
|
+
const hasNotFoundPattern = lower.includes("not found") || lower.includes("no rows");
|
|
1019
|
+
const hasValidationPattern = lower.includes("validation") || lower.includes("invalid") || lower.includes("malformed");
|
|
1020
|
+
const hasAuthPattern = lower.includes("unauthorized") || lower.includes("forbidden") || lower.includes("auth");
|
|
1021
|
+
const hasRateLimitPattern = lower.includes("rate limit") || lower.includes("too many requests");
|
|
1022
|
+
const hasTransientPattern = lower.includes("timeout") || lower.includes("temporar") || lower.includes("connection reset") || lower.includes("socket") || lower.includes("deadlock");
|
|
1023
|
+
if (status === 409 || hasUniquePattern) return "unique_violation";
|
|
1024
|
+
if (status === 404 || hasNotFoundPattern) return "not_found";
|
|
1025
|
+
if (status === 401 || status === 403 || hasAuthPattern) return "auth";
|
|
1026
|
+
if (status === 429 || hasRateLimitPattern) return "rate_limit";
|
|
1027
|
+
if (status === 400 || status === 422 || hasValidationPattern) return "validation";
|
|
1028
|
+
if (code === "NETWORK_ERROR" || status === 0 || status !== void 0 && status >= 500 || hasTransientPattern) {
|
|
1029
|
+
return "transient";
|
|
1030
|
+
}
|
|
1031
|
+
return "unknown";
|
|
1032
|
+
}
|
|
1033
|
+
function toGatewayCode(kind, status) {
|
|
1034
|
+
if (kind === "transient" && (status === 0 || status === void 0)) return "NETWORK_ERROR";
|
|
1035
|
+
if (kind === "validation") return "INVALID_JSON";
|
|
1036
|
+
if (status !== void 0 && status >= 400) return "HTTP_ERROR";
|
|
1037
|
+
return "UNKNOWN_ERROR";
|
|
1038
|
+
}
|
|
1039
|
+
function toAthenaGatewayError(source, fallbackMessage, context) {
|
|
1040
|
+
if (isAthenaGatewayError(source)) {
|
|
1041
|
+
return source;
|
|
1042
|
+
}
|
|
1043
|
+
if (isAthenaResultLike(source) && source.errorDetails) {
|
|
1044
|
+
return new AthenaGatewayError({
|
|
1045
|
+
code: source.errorDetails.code,
|
|
1046
|
+
message: source.error ?? source.errorDetails.message,
|
|
1047
|
+
status: source.status,
|
|
1048
|
+
endpoint: source.errorDetails.endpoint,
|
|
1049
|
+
method: source.errorDetails.method,
|
|
1050
|
+
requestId: source.errorDetails.requestId,
|
|
1051
|
+
hint: source.errorDetails.hint,
|
|
1052
|
+
cause: source.errorDetails.cause
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
const normalized = normalizeAthenaError(source, context);
|
|
1056
|
+
const message = isAthenaResultLike(source) && source.error == null && source.errorDetails == null ? fallbackMessage : normalized.message || fallbackMessage;
|
|
1057
|
+
return new AthenaGatewayError({
|
|
1058
|
+
code: toGatewayCode(normalized.kind, normalized.status),
|
|
1059
|
+
message,
|
|
1060
|
+
status: normalized.status ?? 0,
|
|
1061
|
+
hint: normalized.constraint != null ? `Constraint: ${normalized.constraint}` : contextHint(context),
|
|
1062
|
+
cause: typeof normalized.raw === "string" ? normalized.raw : safeStringify(normalized.raw)
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
function isOk(result) {
|
|
1066
|
+
return result.error == null && result.status >= 200 && result.status < 300;
|
|
1067
|
+
}
|
|
1068
|
+
function normalizeAthenaError(resultOrError, context) {
|
|
1069
|
+
if (isAthenaResultLike(resultOrError)) {
|
|
1070
|
+
const details = resultOrError.errorDetails;
|
|
1071
|
+
const message2 = resultOrError.error ?? details?.message ?? `Athena ${context?.operation ?? operationFromDetails(details) ?? "request"} failed`;
|
|
1072
|
+
const operation = context?.operation ?? operationFromDetails(details);
|
|
1073
|
+
const table = context?.table ?? extractTable(message2);
|
|
1074
|
+
const constraint = extractConstraint(message2);
|
|
1075
|
+
const kind = classifyKind(resultOrError.status, details?.code, message2);
|
|
1076
|
+
return {
|
|
1077
|
+
kind,
|
|
1078
|
+
status: resultOrError.status,
|
|
1079
|
+
constraint,
|
|
1080
|
+
table,
|
|
1081
|
+
operation,
|
|
1082
|
+
message: message2,
|
|
1083
|
+
raw: resultOrError.raw
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
if (isAthenaGatewayError(resultOrError)) {
|
|
1087
|
+
const details = resultOrError.toDetails();
|
|
1088
|
+
const operation = context?.operation ?? operationFromDetails(details);
|
|
1089
|
+
const table = context?.table ?? extractTable(resultOrError.message);
|
|
1090
|
+
const constraint = extractConstraint(resultOrError.message);
|
|
1091
|
+
const kind = classifyKind(resultOrError.status, resultOrError.code, resultOrError.message);
|
|
1092
|
+
return {
|
|
1093
|
+
kind,
|
|
1094
|
+
status: resultOrError.status,
|
|
1095
|
+
constraint,
|
|
1096
|
+
table,
|
|
1097
|
+
operation,
|
|
1098
|
+
message: resultOrError.message,
|
|
1099
|
+
raw: resultOrError
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
if (resultOrError instanceof Error) {
|
|
1103
|
+
const maybeStatus = isRecord2(resultOrError) && typeof resultOrError.status === "number" ? resultOrError.status : void 0;
|
|
1104
|
+
return {
|
|
1105
|
+
kind: classifyKind(maybeStatus, void 0, resultOrError.message),
|
|
1106
|
+
status: maybeStatus,
|
|
1107
|
+
constraint: extractConstraint(resultOrError.message),
|
|
1108
|
+
table: context?.table ?? extractTable(resultOrError.message),
|
|
1109
|
+
operation: context?.operation,
|
|
1110
|
+
message: resultOrError.message,
|
|
1111
|
+
raw: resultOrError
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
const message = typeof resultOrError === "string" ? resultOrError : "Unknown Athena error";
|
|
1115
|
+
return {
|
|
1116
|
+
kind: classifyKind(void 0, void 0, message),
|
|
1117
|
+
status: void 0,
|
|
1118
|
+
constraint: extractConstraint(message),
|
|
1119
|
+
table: context?.table ?? extractTable(message),
|
|
1120
|
+
operation: context?.operation,
|
|
1121
|
+
message,
|
|
1122
|
+
raw: resultOrError
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function unwrapRows(result, options) {
|
|
1126
|
+
if (!isOk(result)) {
|
|
1127
|
+
throw toAthenaGatewayError(result, "Athena request failed", options?.context);
|
|
1128
|
+
}
|
|
1129
|
+
if (result.data == null) return [];
|
|
1130
|
+
return Array.isArray(result.data) ? result.data : [result.data];
|
|
1131
|
+
}
|
|
1132
|
+
function unwrap(result, options) {
|
|
1133
|
+
if (!isOk(result)) {
|
|
1134
|
+
throw toAthenaGatewayError(result, "Athena request failed", options?.context);
|
|
1135
|
+
}
|
|
1136
|
+
if (result.data == null && !options?.allowNull) {
|
|
1137
|
+
throw toAthenaGatewayError(result, "Expected data but received null", options?.context);
|
|
1138
|
+
}
|
|
1139
|
+
return result.data;
|
|
1140
|
+
}
|
|
1141
|
+
function unwrapOne(result, options) {
|
|
1142
|
+
const rows = unwrapRows(result, options);
|
|
1143
|
+
if (!rows.length) {
|
|
1144
|
+
if (options?.allowNull) return null;
|
|
1145
|
+
throw toAthenaGatewayError(result, "Expected one row but received none", options?.context);
|
|
1146
|
+
}
|
|
1147
|
+
if (options?.requireExactlyOne && rows.length !== 1) {
|
|
1148
|
+
throw toAthenaGatewayError(
|
|
1149
|
+
result,
|
|
1150
|
+
`Expected exactly one row but received ${rows.length}`,
|
|
1151
|
+
options.context
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
return rows[0];
|
|
1155
|
+
}
|
|
1156
|
+
function requireSuccess(result, context) {
|
|
1157
|
+
if (!isOk(result)) {
|
|
1158
|
+
throw toAthenaGatewayError(
|
|
1159
|
+
result,
|
|
1160
|
+
`Athena ${context?.operation ?? "request"} failed`,
|
|
1161
|
+
context
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
function requireAffected(result, options, context) {
|
|
1167
|
+
requireSuccess(result, context);
|
|
1168
|
+
const minimum = options?.min ?? 1;
|
|
1169
|
+
const count = result.count;
|
|
1170
|
+
if (count == null) {
|
|
1171
|
+
throw new AthenaGatewayError({
|
|
1172
|
+
code: "UNKNOWN_ERROR",
|
|
1173
|
+
status: result.status,
|
|
1174
|
+
message: "Expected affected row count but response.count is missing",
|
|
1175
|
+
hint: 'Set call option { count: "exact" } for mutation postcondition checks.',
|
|
1176
|
+
cause: safeStringify(result.raw)
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
if (count < minimum) {
|
|
1180
|
+
throw new AthenaGatewayError({
|
|
1181
|
+
code: "UNKNOWN_ERROR",
|
|
1182
|
+
status: result.status,
|
|
1183
|
+
message: `Expected at least ${minimum} affected rows but received ${count}`,
|
|
1184
|
+
hint: contextHint(context),
|
|
1185
|
+
cause: safeStringify(result.raw)
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
return count;
|
|
1189
|
+
}
|
|
1190
|
+
function applyBounds(value, options) {
|
|
1191
|
+
if (options?.min !== void 0 && value < options.min) return null;
|
|
1192
|
+
if (options?.max !== void 0 && value > options.max) return null;
|
|
1193
|
+
return value;
|
|
1194
|
+
}
|
|
1195
|
+
function parseIntegerString(value) {
|
|
1196
|
+
const normalized = value.trim();
|
|
1197
|
+
if (!/^[-+]?\d+$/.test(normalized)) return null;
|
|
1198
|
+
const parsed = Number(normalized);
|
|
1199
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) return null;
|
|
1200
|
+
return parsed;
|
|
1201
|
+
}
|
|
1202
|
+
function coerceInt(value, options) {
|
|
1203
|
+
if (value == null) return null;
|
|
1204
|
+
if (typeof value === "number") {
|
|
1205
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) return null;
|
|
1206
|
+
return applyBounds(value, options);
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof value === "string") {
|
|
1209
|
+
const parsed = parseIntegerString(value);
|
|
1210
|
+
if (parsed == null) return null;
|
|
1211
|
+
return applyBounds(parsed, options);
|
|
1212
|
+
}
|
|
1213
|
+
if (typeof value === "bigint") {
|
|
1214
|
+
if (options?.strictBigInt) {
|
|
1215
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const parsed = Number(value);
|
|
1220
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) return null;
|
|
1221
|
+
return applyBounds(parsed, options);
|
|
1222
|
+
}
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
function assertInt(value, label = "value", options) {
|
|
1226
|
+
const parsed = coerceInt(value, options);
|
|
1227
|
+
if (parsed == null) {
|
|
1228
|
+
throw new TypeError(`${label} must be a finite integer`);
|
|
1229
|
+
}
|
|
1230
|
+
return parsed;
|
|
1231
|
+
}
|
|
1232
|
+
function defaultShouldRetry(error) {
|
|
1233
|
+
const normalized = normalizeAthenaError(error);
|
|
1234
|
+
return normalized.kind === "transient" || normalized.kind === "rate_limit";
|
|
1235
|
+
}
|
|
1236
|
+
function computeDelayMs(attempt, error, config) {
|
|
1237
|
+
const baseDelay = config.baseDelayMs;
|
|
1238
|
+
const rawDelay = typeof config.backoff === "function" ? config.backoff(attempt, error) : config.backoff === "linear" ? baseDelay * attempt : baseDelay * Math.pow(2, attempt - 1);
|
|
1239
|
+
const safeDelay = Number.isFinite(rawDelay) ? Math.max(0, rawDelay) : 0;
|
|
1240
|
+
const clamped = Math.min(config.maxDelayMs, safeDelay);
|
|
1241
|
+
const jitterFactor = typeof config.jitter === "number" ? Math.max(0, Math.min(1, config.jitter)) : config.jitter ? 0.2 : 0;
|
|
1242
|
+
if (!jitterFactor) return clamped;
|
|
1243
|
+
const deviation = clamped * jitterFactor;
|
|
1244
|
+
const offset = (Math.random() * 2 - 1) * deviation;
|
|
1245
|
+
return Math.max(0, clamped + offset);
|
|
1246
|
+
}
|
|
1247
|
+
function sleep(ms) {
|
|
1248
|
+
if (ms <= 0) return Promise.resolve();
|
|
1249
|
+
return new Promise((resolve) => {
|
|
1250
|
+
setTimeout(resolve, ms);
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
async function withRetry(config, fn) {
|
|
1254
|
+
const retries = Math.max(0, Math.trunc(config.retries));
|
|
1255
|
+
const shouldRetry = config.shouldRetry ?? defaultShouldRetry;
|
|
1256
|
+
const resolvedConfig = {
|
|
1257
|
+
baseDelayMs: config.baseDelayMs ?? 100,
|
|
1258
|
+
maxDelayMs: config.maxDelayMs ?? 1e4,
|
|
1259
|
+
backoff: config.backoff ?? "exponential",
|
|
1260
|
+
jitter: config.jitter ?? false
|
|
1261
|
+
};
|
|
1262
|
+
for (let attempts = 0; attempts <= retries; attempts += 1) {
|
|
1263
|
+
try {
|
|
1264
|
+
return await fn();
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
if (attempts >= retries) {
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
const currentAttempt = attempts + 1;
|
|
1270
|
+
const retry = await shouldRetry(error, currentAttempt);
|
|
1271
|
+
if (!retry) {
|
|
1272
|
+
throw error;
|
|
1273
|
+
}
|
|
1274
|
+
const delay = computeDelayMs(currentAttempt, error, resolvedConfig);
|
|
1275
|
+
await sleep(delay);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
throw new Error("withRetry reached an unexpected state");
|
|
1279
|
+
}
|
|
1280
|
+
|
|
967
1281
|
exports.AthenaClient = AthenaClient;
|
|
968
1282
|
exports.AthenaGatewayError = AthenaGatewayError;
|
|
969
1283
|
exports.Backend = Backend;
|
|
1284
|
+
exports.assertInt = assertInt;
|
|
1285
|
+
exports.coerceInt = coerceInt;
|
|
970
1286
|
exports.createClient = createClient;
|
|
971
1287
|
exports.isAthenaGatewayError = isAthenaGatewayError;
|
|
1288
|
+
exports.isOk = isOk;
|
|
1289
|
+
exports.normalizeAthenaError = normalizeAthenaError;
|
|
1290
|
+
exports.requireAffected = requireAffected;
|
|
1291
|
+
exports.requireSuccess = requireSuccess;
|
|
1292
|
+
exports.unwrap = unwrap;
|
|
1293
|
+
exports.unwrapOne = unwrapOne;
|
|
1294
|
+
exports.unwrapRows = unwrapRows;
|
|
1295
|
+
exports.withRetry = withRetry;
|
|
972
1296
|
//# sourceMappingURL=index.cjs.map
|
|
973
1297
|
//# sourceMappingURL=index.cjs.map
|