@xylex-group/athena 2.1.2 → 2.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.
Files changed (42) hide show
  1. package/README.md +287 -116
  2. package/dist/browser.cjs +1628 -156
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.cts +14 -14
  5. package/dist/browser.d.ts +14 -14
  6. package/dist/browser.js +1625 -156
  7. package/dist/browser.js.map +1 -1
  8. package/dist/cli/index.cjs +1532 -145
  9. package/dist/cli/index.cjs.map +1 -1
  10. package/dist/cli/index.d.cts +3 -3
  11. package/dist/cli/index.d.ts +3 -3
  12. package/dist/cli/index.js +1533 -146
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +1658 -167
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +7 -7
  17. package/dist/index.d.ts +7 -7
  18. package/dist/index.js +1656 -168
  19. package/dist/index.js.map +1 -1
  20. package/dist/{model-form-2hqmoOUX.d.ts → model-form-BpDXlbxb.d.ts} +97 -2
  21. package/dist/{model-form-Cy-zaO0u.d.cts → model-form-hoE2jHIi.d.cts} +97 -2
  22. package/dist/{pipeline-BOPszLsL.d.ts → pipeline-BNIw8pDQ.d.ts} +1 -1
  23. package/dist/{pipeline-E3FDbs4W.d.cts → pipeline-DNIpEsN8.d.cts} +1 -1
  24. package/dist/{client-dpAp-NZK.d.cts → react-email-BvyCZnfW.d.cts} +349 -174
  25. package/dist/{client-BX0NQqOn.d.ts → react-email-qPA1wjFV.d.ts} +349 -174
  26. package/dist/react.cjs +30 -9
  27. package/dist/react.cjs.map +1 -1
  28. package/dist/react.d.cts +4 -4
  29. package/dist/react.d.ts +4 -4
  30. package/dist/react.js +30 -9
  31. package/dist/react.js.map +1 -1
  32. package/dist/{types-BaBzjwXr.d.cts → types-A5e97acl.d.cts} +2 -1
  33. package/dist/{types-BaBzjwXr.d.ts → types-A5e97acl.d.ts} +2 -1
  34. package/dist/{types-CeBPrnGj.d.ts → types-BnD22-vb.d.ts} +1 -1
  35. package/dist/{types-CpqL-pZx.d.cts → types-bDlr4u7p.d.cts} +1 -1
  36. package/dist/utils.cjs +153 -0
  37. package/dist/utils.cjs.map +1 -0
  38. package/dist/utils.d.cts +23 -0
  39. package/dist/utils.d.ts +23 -0
  40. package/dist/utils.js +146 -0
  41. package/dist/utils.js.map +1 -0
  42. package/package.json +75 -17
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # athena-js
2
2
 
3
- current version: `2.1.2`
3
+ current version: `2.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 plus Athena-native React hooks for client-side use.
5
5
 
6
6
  ## Install
@@ -24,66 +24,144 @@ npm install react # React >=17 required for the hook
24
24
  ```ts
25
25
  import { createClient } from "@xylex-group/athena";
26
26
 
27
- const athenaClient = createClient(ATHENA_URL, ATHENA_API_KEY, {
28
- client: "CLIENT_NAME",
29
- backend: { type: "athena" },
30
- });
31
-
32
- const { data, error } = await athenaClient.from("characters").select(`
33
- id,
34
- name,
35
- from:sender_id(name),
36
- to:receiver_id(name)
37
- `);
38
-
39
- if (error) {
40
- console.error("gateway error", error);
41
- } else {
42
- console.table(data);
43
- }
44
- ```
45
-
46
- ### Auth client (Athena Auth server)
47
-
48
- If your auth backend is now Athena Auth, you can keep core login/session flows in this SDK:
49
-
50
- ```ts
51
- import { createClient } from "@xylex-group/athena";
52
-
53
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
27
+ const athenaClient = createClient(ATHENA_URL, ATHENA_API_KEY, {
54
28
  client: "CLIENT_NAME",
55
- auth: {
56
- baseUrl: "http://localhost:3001/api/auth",
57
- // optional: bearer token if you are not using cookie-based sessions
58
- bearerToken: process.env.AUTH_BEARER_TOKEN,
59
- },
29
+ backend: { type: "athena" },
60
30
  });
61
31
 
62
- const login = await athena.auth.signIn.email({
63
- email: "demo@example.com",
64
- password: "super-secret",
65
- rememberMe: true,
32
+ const { data, error } = await athenaClient.from("orchestral_sections").findMany({
33
+ select: {
34
+ name: true,
35
+ instruments: {
36
+ select: {
37
+ name: true,
38
+ },
39
+ },
40
+ },
66
41
  });
67
42
 
68
- const session = await athena.auth.getSession();
69
- const sessions = await athena.auth.session.list();
70
-
71
- // clear one session
72
- await athena.auth.session.revoke({ token: "session_token_here" });
73
- // or clear all sessions
74
- await athena.auth.session.revoke([{ token: "session_token_here" }, { token: "session_token_2" }]);
75
-
76
- await athena.auth.signOut();
77
-
78
- // additional core flows
79
- await athena.auth.forgetPassword({ email: "demo@example.com", redirectTo: "https://app/reset-password" });
80
- await athena.auth.resetPassword({ newPassword: "new-secret", token: "reset_token" });
81
- await athena.auth.verifyEmail({ token: "verify_token", callbackURL: "https://app/verified" });
82
- await athena.auth.changePassword({ currentPassword: "old-secret", newPassword: "new-secret" });
83
- await athena.auth.user.update({ name: "Demo User" });
43
+ if (error) {
44
+ console.error("gateway error", error);
45
+ } else {
46
+ console.table(data);
47
+ }
84
48
  ```
85
49
 
86
- Auth responses follow the same envelope style: `{ ok, status, data, error, errorDetails, raw }`.
50
+ Example version baseline: SDK `@xylex-group/athena` `2.3.0`, Athena server `3.12.3` verified on 2026-06-04.
51
+
52
+ `.findMany({ select, where, orderBy, limit })` is the clean canonical read surface.
53
+ The existing string-based `.select(...)` chain remains fully supported for compatibility,
54
+ including alias/FK patterns like `from:sender_id(name)`.
55
+ For the full AST model, route contract, error behavior, and Athena server implications,
56
+ see [`docs/findmany-ast-and-server-contract.md`](docs/findmany-ast-and-server-contract.md).
57
+
58
+ ### Auth client (Athena Auth server)
59
+
60
+ If your auth backend is now Athena Auth, you can keep core login/session flows in this SDK:
61
+
62
+ ```ts
63
+ import { createClient } from "@xylex-group/athena";
64
+
65
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
66
+ client: "CLIENT_NAME",
67
+ auth: {
68
+ baseUrl: "http://localhost:3001/api/auth",
69
+ // optional: bearer token if you are not using cookie-based sessions
70
+ bearerToken: process.env.AUTH_BEARER_TOKEN,
71
+ },
72
+ });
73
+
74
+ const login = await athena.auth.signIn.email({
75
+ email: "demo@example.com",
76
+ password: "super-secret",
77
+ rememberMe: true,
78
+ });
79
+
80
+ const session = await athena.auth.getSession();
81
+ const sessions = await athena.auth.session.list();
82
+
83
+ // clear one session
84
+ await athena.auth.session.revoke({ token: "session_token_here" });
85
+ // or clear all sessions
86
+ await athena.auth.session.revoke([{ token: "session_token_here" }, { token: "session_token_2" }]);
87
+
88
+ await athena.auth.signOut();
89
+
90
+ // additional core flows
91
+ await athena.auth.forgetPassword({ email: "demo@example.com", redirectTo: "https://app/reset-password" });
92
+ await athena.auth.resetPassword({ newPassword: "new-secret", token: "reset_token" });
93
+ await athena.auth.verifyEmail({ token: "verify_token", callbackURL: "https://app/verified" });
94
+ await athena.auth.changePassword({ currentPassword: "old-secret", newPassword: "new-secret" });
95
+ await athena.auth.user.update({ name: "Demo User" });
96
+ ```
97
+
98
+ #### React Email templates for admin HTML routes
99
+
100
+ If you use `@react-email/components`, you can pass component+props payloads directly on admin email/template routes:
101
+
102
+ ```ts
103
+ import { Body, Html, Text } from "@react-email/components";
104
+
105
+ function WelcomeEmail(props: { name: string }) {
106
+ return (
107
+ <Html lang="en">
108
+ <Body>
109
+ <Text>Welcome {props.name}</Text>
110
+ </Body>
111
+ </Html>
112
+ );
113
+ }
114
+
115
+ await athena.auth.admin.email.template.create({
116
+ templateKey: "welcome",
117
+ subjectTemplate: "Welcome",
118
+ react: {
119
+ component: WelcomeEmail,
120
+ props: { name: "Ava" },
121
+ },
122
+ });
123
+ ```
124
+
125
+ For reusable templates, use `defineAuthEmailTemplate(...)`:
126
+
127
+ ```ts
128
+ import { defineAuthEmailTemplate } from "@xylex-group/athena";
129
+
130
+ const welcomeTemplate = defineAuthEmailTemplate({
131
+ component: WelcomeEmail,
132
+ templateKey: "welcome",
133
+ subjectTemplate: "Welcome",
134
+ });
135
+
136
+ await athena.auth.admin.email.template.create(
137
+ welcomeTemplate.toTemplateCreate({
138
+ props: { name: "Ava" },
139
+ }),
140
+ );
141
+ ```
142
+
143
+ You can also observe render timing/errors:
144
+
145
+ ```ts
146
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
147
+ auth: {
148
+ baseUrl: AUTH_BASE_URL,
149
+ reactEmail: {
150
+ observe: (event) => {
151
+ console.log(JSON.stringify(event));
152
+ },
153
+ },
154
+ },
155
+ });
156
+ ```
157
+
158
+ Install support packages in your app:
159
+
160
+ ```bash
161
+ pnpm add @react-email/components @react-email/render
162
+ ```
163
+
164
+ Auth responses follow the same envelope style: `{ ok, status, data, error, errorDetails, raw }`.
87
165
 
88
166
  ### Typed schema registry (model-first)
89
167
 
@@ -125,6 +203,8 @@ await typed
125
203
 
126
204
  For full details, see [`docs/typed-schema-registry.md`](./docs/typed-schema-registry.md).
127
205
 
206
+ For exhaustive method-by-method documentation with usage snippets (root client, runtime builders, auth bindings, react runtime, cookies, and utils), see [`docs/complete-method-reference.md`](./docs/complete-method-reference.md).
207
+
128
208
  ### Typed schema generator
129
209
 
130
210
  Schema generation is additive. Existing `createClient(...).from<T>(...)` usage remains valid while teams migrate to generated registry files.
@@ -165,9 +245,19 @@ For prompt-ready documentation handoff text, see [`docs/generator-codex-handoff-
165
245
  - `athena-rs` for Rust backend throughput
166
246
  - `athena-js` for app/tooling layers that need TypeScript contracts and frontend-facing ergonomics
167
247
 
168
- Every query resolves to `{ data, error, errorDetails?, status, count?, raw }`. `data` is `null` on error; `error` is `null` on success.
169
-
170
- For richer handling, inspect `errorDetails` (`code`, `status`, `endpoint`, `method`, `requestId`, etc.) or use `AthenaGatewayError` / `isAthenaGatewayError` from the package exports.
248
+ Every query resolves to `{ data, error, errorDetails?, status, statusText?, count?, raw }`. `data` is `null` on error; `error` is `null` on success.
249
+
250
+ Failed results now include a structured `error` object with the useful fields inline:
251
+
252
+ - `message`
253
+ - `code`
254
+ - `details`
255
+ - `hint`
256
+ - `status`
257
+ - `statusText`
258
+ - normalized metadata such as `kind`, `table`, `operation`, and `retryable`
259
+
260
+ `errorDetails` is still present as a compatibility alias for low-level gateway metadata (`gatewayCode`, `endpoint`, `method`, `requestId`, etc.).
171
261
 
172
262
  ## Reliability helper APIs
173
263
 
@@ -206,29 +296,78 @@ requireAffected(inserted, { min: 1 }, { table: "users", operation: "insert" });
206
296
 
207
297
  `requireAffected` uses `result.count`; request it on writes with `{ count: "exact" }` when you need enforced postconditions.
208
298
 
209
- ### Error normalization
299
+ ### Structured errors by default
210
300
 
211
301
  ```ts
212
302
  import { createClient, normalizeAthenaError } from "@xylex-group/athena";
213
303
 
214
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
215
- experimental: { enableErrorNormalization: true },
216
- });
304
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY);
217
305
 
218
- const result = await athena.from("users").insert({ id: 1 }).select();
219
- if (result.error) {
220
- const err = normalizeAthenaError(result);
221
- if (err.kind === "unique_violation") {
306
+ const { data, error, status, statusText } = await athena.from("users").insert({ id: 1 }).select();
307
+ if (error) {
308
+ console.error(error);
309
+ console.error(error.hint ?? error.message, status, statusText);
310
+ if (error.kind === "unique_violation") {
222
311
  // deterministic conflict handling
223
312
  }
224
313
  }
225
314
  ```
226
315
 
227
- Normalized errors expose stable `kind` values (`unique_violation`, `validation`, `auth`, `rate_limit`, `transient`, etc.) plus operation metadata.
316
+ `result.error` already carries normalized `kind` values (`unique_violation`, `validation`, `auth`, `rate_limit`, `transient`, etc.) plus operation metadata.
228
317
 
229
- `experimental.enableErrorNormalization` keeps the existing `AthenaResult<T>` shape intact and pre-attaches context-aware metadata so `normalizeAthenaError(result)` can resolve table/operation without extra per-call context objects.
318
+ `normalizeAthenaError(result)` still exists when you need the normalized envelope from an arbitrary thrown value or mixed unknown input.
230
319
 
231
- ### Numeric coercion
320
+ ### Query tracing (experimental)
321
+
322
+ ```ts
323
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
324
+ experimental: { traceQueries: true },
325
+ });
326
+ ```
327
+
328
+ With `traceQueries: true`, the SDK logs every runtime execution (`select`, `insert`, `upsert`, `update`, `delete`, `rpc`, `query`) and includes:
329
+
330
+ - the gateway endpoint used
331
+ - synthesized SQL (or raw SQL for `query(...)` and SQL fallback reads)
332
+ - payload and call options
333
+ - full outcome (`status`, `error`, `count`, `data`, `raw`)
334
+ - callsite metadata (`filePath`, `fileName`, `line`, `column`)
335
+
336
+ For deferred chains, Athena captures that callsite from the public SDK seam that declared or finalized the operation and reuses it for the eventual network execution. That keeps traces pinned to user code instead of drifting into SDK internals when async stack shapes differ between local runs and CI.
337
+
338
+ Use a custom sink:
339
+
340
+ ```ts
341
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
342
+ experimental: {
343
+ traceQueries: {
344
+ logger(event) {
345
+ // Forward into your logger/observability sink
346
+ console.log(event.operation, event.endpoint, event.sql, event.callsite);
347
+ },
348
+ },
349
+ },
350
+ });
351
+ ```
352
+
353
+ ### findMany AST transport (experimental)
354
+
355
+ ```ts
356
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
357
+ experimental: {
358
+ findManyAst: true,
359
+ },
360
+ });
361
+ ```
362
+
363
+ With `findManyAst: true`, clean `findMany(...)` calls send the original object AST body to `/gateway/fetch` instead of compiling the select tree down to `columns` and `conditions` first.
364
+
365
+ - this is opt-in and meant for gateways that explicitly support direct AST bodies
366
+ - existing compiled `findMany(...)` transport remains the default
367
+ - chained builder filters or pagination state that the AST body cannot represent losslessly yet continue to use the legacy compiled path
368
+ - trace output still includes synthesized SQL so diagnostics stay readable
369
+
370
+ ### Numeric coercion
232
371
 
233
372
  ```ts
234
373
  import { coerceInt, assertInt } from "@xylex-group/athena";
@@ -239,6 +378,38 @@ if (maybeCaseId == null) throw new Error("Invalid case id");
239
378
  const caseId = assertInt(req.query.case_id, "case_id", { min: 1 });
240
379
  ```
241
380
 
381
+ ### Utilities subpath
382
+
383
+ Utilities that are intentionally not exported from the root package are available from `@xylex-group/athena/utils`.
384
+
385
+ ```ts
386
+ import {
387
+ slugify,
388
+ trimTrailingSlashes,
389
+ parseBooleanFlag,
390
+ isLocalHostname,
391
+ clearAuthCookies,
392
+ proxyRequestHeaders,
393
+ } from "@xylex-group/athena/utils";
394
+ ```
395
+
396
+ Examples:
397
+
398
+ ```ts
399
+ const slug = slugify("Customer Success / Q4 Report"); // customer-success-q4-report
400
+ const local = isLocalHostname("api.localhost"); // true
401
+ const normalized = trimTrailingSlashes("https://example.com///"); // https://example.com
402
+ const enabled = parseBooleanFlag(process.env.FEATURE_FLAG, false);
403
+
404
+ // Browser-only helper (safe no-op on server runtimes)
405
+ clearAuthCookies();
406
+
407
+ // Preserve forwarded headers when proxying auth requests
408
+ const upstreamHeaders = proxyRequestHeaders(request);
409
+ ```
410
+
411
+ `clearAuthCookies()` clears cookies matching Athena/Better Auth prefixes (`athena-auth`, `__Secure-athena-auth`, `better-auth`, `__Secure-better-auth`) and also attempts parent-domain cleanup for subdomain deployments.
412
+
242
413
  ### Retry helper
243
414
 
244
415
  ```ts
@@ -257,31 +428,31 @@ const result = await withRetry(
257
428
 
258
429
  By default, retries target transient/rate-limit failures; use `shouldRetry` for custom policies.
259
430
 
260
- ## Query builder
261
-
262
- ### DB module namespace
263
-
264
- `createClient()` keeps root methods (`from`, `rpc`, `query`) and now also exposes `db` as an additive namespace.
265
-
266
- ```ts
267
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY);
268
-
269
- await athena.db.from("users").select("id,name").eq("active", true).limit(20);
270
-
271
- await athena.db.select("users", "id,name").eq("id", 1).single();
272
-
273
- await athena.db.insert("users", { id: 1, name: "Alice" }).select("id");
274
- await athena.db.update("users", { name: "Updated" }).eq("id", 1).select("id,name");
275
- await athena.db.delete("users", { resourceId: "r-1" }).select("id");
276
- ```
277
-
278
- `db` mirrors the existing query builder semantics while providing a module seam for future database-surface expansion.
279
-
280
- ### Reading rows
281
-
282
- ```ts
283
- // select all columns
284
- const { data } = await athena.from("users").select();
431
+ ## Query builder
432
+
433
+ ### DB module namespace
434
+
435
+ `createClient()` keeps root methods (`from`, `rpc`, `query`) and now also exposes `db` as an additive namespace.
436
+
437
+ ```ts
438
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY);
439
+
440
+ await athena.db.from("users").select("id,name").eq("active", true).limit(20);
441
+
442
+ await athena.db.select("users", "id,name").eq("id", 1).single();
443
+
444
+ await athena.db.insert("users", { id: 1, name: "Alice" }).select("id");
445
+ await athena.db.update("users", { name: "Updated" }).eq("id", 1).select("id,name");
446
+ await athena.db.delete("users", { resourceId: "r-1" }).select("id");
447
+ ```
448
+
449
+ `db` mirrors the existing query builder semantics while providing a module seam for future database-surface expansion.
450
+
451
+ ### Reading rows
452
+
453
+ ```ts
454
+ // select all columns
455
+ const { data } = await athena.from("users").select();
285
456
 
286
457
  // select specific columns
287
458
  const { data } = await athena.from("users").select("id, name, email");
@@ -408,23 +579,23 @@ const { data: user } = await athena
408
579
  .single();
409
580
  ```
410
581
 
411
- `maybeSingle` behaves identically — both return the first element of the result set.
412
-
413
- ### Table schema targeting
414
-
415
- Use `schema` in table call options to qualify unqualified table names:
416
-
417
- ```ts
418
- const { data } = await athena
419
- .from("users")
420
- .select("id,email", { schema: "public" });
421
- ```
422
-
423
- This resolves the table target to `public.users`.
424
-
425
- ### RPC
426
-
427
- 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}`.
582
+ `maybeSingle` behaves identically — both return the first element of the result set.
583
+
584
+ ### Table schema targeting
585
+
586
+ Use `schema` in table call options to qualify unqualified table names:
587
+
588
+ ```ts
589
+ const { data } = await athena
590
+ .from("users")
591
+ .select("id,email", { schema: "public" });
592
+ ```
593
+
594
+ This resolves the table target to `public.users`.
595
+
596
+ ### RPC
597
+
598
+ 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}`.
428
599
 
429
600
  ```ts
430
601
  const { data, count } = await athena
@@ -458,12 +629,12 @@ RPC options: `schema`, `count` (`"exact" | "planned" | "estimated"`), `head`, `g
458
629
 
459
630
  Pass options as the second argument to `.select()`:
460
631
 
461
- | Option | Type | Description |
462
- | ------------ | ------------------------------------- | -------------------------------------------- |
463
- | `schema` | `string` | qualify unqualified table names for table calls |
464
- | `count` | `"exact" \| "planned" \| "estimated"` | request a row count alongside the data |
465
- | `head` | `boolean` | return response headers only (no rows) |
466
- | `stripNulls` | `boolean` | strip null values from rows (default `true`) |
632
+ | Option | Type | Description |
633
+ | ------------ | ------------------------------------- | -------------------------------------------- |
634
+ | `schema` | `string` | qualify unqualified table names for table calls |
635
+ | `count` | `"exact" \| "planned" \| "estimated"` | request a row count alongside the data |
636
+ | `head` | `boolean` | return response headers only (no rows) |
637
+ | `stripNulls` | `boolean` | strip null values from rows (default `true`) |
467
638
 
468
639
  ```ts
469
640
  const { data } = await athena