@xylex-group/athena 1.2.0 → 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/{errors-DKNcLa5O.d.cts → errors-DHmpYG46.d.cts} +2 -1
- package/dist/{errors-DKNcLa5O.d.ts → errors-DHmpYG46.d.ts} +2 -1
- package/dist/index.cjs +331 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -3
- package/dist/index.d.ts +117 -3
- package/dist/index.js +322 -8
- package/dist/index.js.map +1 -1
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- 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
|
|
|
@@ -51,7 +51,8 @@ interface AthenaDeletePayload {
|
|
|
51
51
|
conditions?: AthenaGatewayCondition[];
|
|
52
52
|
}
|
|
53
53
|
interface AthenaUpdatePayload extends AthenaFetchPayload {
|
|
54
|
-
|
|
54
|
+
set?: Record<string, unknown>;
|
|
55
|
+
data?: Record<string, unknown>;
|
|
55
56
|
}
|
|
56
57
|
type AthenaRpcFilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike' | 'is' | 'in';
|
|
57
58
|
interface AthenaRpcFilter {
|
|
@@ -51,7 +51,8 @@ interface AthenaDeletePayload {
|
|
|
51
51
|
conditions?: AthenaGatewayCondition[];
|
|
52
52
|
}
|
|
53
53
|
interface AthenaUpdatePayload extends AthenaFetchPayload {
|
|
54
|
-
|
|
54
|
+
set?: Record<string, unknown>;
|
|
55
|
+
data?: Record<string, unknown>;
|
|
55
56
|
}
|
|
56
57
|
type AthenaRpcFilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike' | 'is' | 'in';
|
|
57
58
|
interface AthenaRpcFilter {
|