@xylex-group/athena 2.12.0 → 2.13.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 (96) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1190 -1184
  3. package/bin/athena-js.js +104 -76
  4. package/dist/admin.cjs +45 -0
  5. package/dist/admin.cjs.map +1 -0
  6. package/dist/admin.d.cts +64 -0
  7. package/dist/admin.d.ts +64 -0
  8. package/dist/admin.js +41 -0
  9. package/dist/admin.js.map +1 -0
  10. package/dist/athena-auth-url-oLux_57a.d.cts +377 -0
  11. package/dist/athena-auth-url-oLux_57a.d.ts +377 -0
  12. package/dist/browser.cjs +33 -46
  13. package/dist/browser.cjs.map +1 -1
  14. package/dist/browser.d.cts +12 -10
  15. package/dist/browser.d.ts +12 -10
  16. package/dist/browser.js +33 -46
  17. package/dist/browser.js.map +1 -1
  18. package/dist/cli/index.cjs +150 -48
  19. package/dist/cli/index.cjs.map +1 -1
  20. package/dist/cli/index.d.cts +14 -5
  21. package/dist/cli/index.d.ts +14 -5
  22. package/dist/cli/index.js +150 -49
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/{client-QUbAs7E8.d.ts → client-BJjUwDSg.d.cts} +109 -1448
  25. package/dist/{client-C3x75Zgn.d.cts → client-DVOKSYDI.d.ts} +109 -1448
  26. package/dist/cookies.cjs +18 -0
  27. package/dist/cookies.cjs.map +1 -1
  28. package/dist/cookies.d.cts +2 -1
  29. package/dist/cookies.d.ts +2 -1
  30. package/dist/cookies.js +17 -1
  31. package/dist/cookies.js.map +1 -1
  32. package/dist/{index-CVcQCGyG.d.ts → index-CFL9xbFR.d.cts} +2 -0
  33. package/dist/{index-CVcQCGyG.d.cts → index-UKJpunc6.d.ts} +2 -0
  34. package/dist/index.cjs +101 -46
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +13 -10
  37. package/dist/index.d.ts +13 -10
  38. package/dist/index.js +101 -47
  39. package/dist/index.js.map +1 -1
  40. package/dist/{model-form-CD1uhWSh.d.cts → model-form-Dw4zEL5a.d.cts} +2 -2
  41. package/dist/{model-form-Dm69I-oO.d.ts → model-form-yRg71q3H.d.ts} +2 -2
  42. package/dist/{module-CW3tWJ10.d.ts → module-DBteUIob.d.ts} +12 -9
  43. package/dist/{module-Cxcurfes.d.cts → module-yoX49uQC.d.cts} +12 -9
  44. package/dist/next/client.cjs +452 -46
  45. package/dist/next/client.cjs.map +1 -1
  46. package/dist/next/client.d.cts +7 -4
  47. package/dist/next/client.d.ts +7 -4
  48. package/dist/next/client.js +430 -47
  49. package/dist/next/client.js.map +1 -1
  50. package/dist/next/server.cjs +292 -46
  51. package/dist/next/server.cjs.map +1 -1
  52. package/dist/next/server.d.cts +81 -5
  53. package/dist/next/server.d.ts +81 -5
  54. package/dist/next/server.js +280 -47
  55. package/dist/next/server.js.map +1 -1
  56. package/dist/organization.cjs +72 -0
  57. package/dist/organization.cjs.map +1 -0
  58. package/dist/organization.d.cts +43 -0
  59. package/dist/organization.d.ts +43 -0
  60. package/dist/organization.js +70 -0
  61. package/dist/organization.js.map +1 -0
  62. package/dist/payload-BzVbUgY_.d.cts +219 -0
  63. package/dist/payload-DEOWN_oo.d.ts +219 -0
  64. package/dist/{pipeline-BAwb6Vzm.d.ts → pipeline-Bj6RWt1A.d.ts} +1 -1
  65. package/dist/{pipeline-B14jVK7J.d.cts → pipeline-Dwck-wZO.d.cts} +1 -1
  66. package/dist/react.cjs +2 -10
  67. package/dist/react.cjs.map +1 -1
  68. package/dist/react.d.cts +26 -7
  69. package/dist/react.d.ts +26 -7
  70. package/dist/react.js +2 -10
  71. package/dist/react.js.map +1 -1
  72. package/dist/session-cookie-detection-BqOp9mO6.d.cts +46 -0
  73. package/dist/session-cookie-detection-BqOp9mO6.d.ts +46 -0
  74. package/dist/social-providers.cjs +4189 -0
  75. package/dist/social-providers.cjs.map +1 -0
  76. package/dist/social-providers.d.cts +4948 -0
  77. package/dist/social-providers.d.ts +4948 -0
  78. package/dist/social-providers.js +4117 -0
  79. package/dist/social-providers.js.map +1 -0
  80. package/dist/{types-Cq4-NoB4.d.ts → types-BRP7sfSF.d.ts} +50 -2
  81. package/dist/{types-Dr849HD6.d.cts → types-BU8uJH_b.d.cts} +50 -2
  82. package/dist/{types-CwJCPpLD.d.ts → types-CH78O90i.d.cts} +2 -2
  83. package/dist/{types-CwJCPpLD.d.cts → types-CH78O90i.d.ts} +2 -2
  84. package/dist/{types-DMOoYnPS.d.cts → types-CjC9_5ZQ.d.cts} +3 -3
  85. package/dist/{types-BcVmPBP-.d.ts → types-DPgejxYC.d.ts} +3 -3
  86. package/dist/types-eAKAh71s.d.cts +1476 -0
  87. package/dist/types-eAKAh71s.d.ts +1476 -0
  88. package/dist/utils.cjs +438 -12
  89. package/dist/utils.cjs.map +1 -1
  90. package/dist/utils.d.cts +76 -11
  91. package/dist/utils.d.ts +76 -11
  92. package/dist/utils.js +390 -13
  93. package/dist/utils.js.map +1 -1
  94. package/package.json +49 -22
  95. package/dist/shared-0Kdc74lu.d.ts +0 -35
  96. package/dist/shared-C0wVICRv.d.cts +0 -35
package/README.md CHANGED
@@ -1,56 +1,56 @@
1
- # athena-js
2
-
3
- current version: `2.12.0`
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
-
6
- ## Install
7
-
8
- ```bash
9
- npm install @xylex-group/athena
10
- # or
11
- pnpm add @xylex-group/athena
12
- # or
13
- yarn add @xylex-group/athena
14
- ```
15
-
16
- React peer dependency is optional — only needed if you use `@xylex-group/athena/react` hooks.
17
-
18
- ```bash
19
- npm install react # React >=17 required for the hook
20
- ```
21
-
22
- ## Quick start
23
-
24
- ```ts
25
- import { createClient } from "@xylex-group/athena";
26
-
27
- const athenaClient = createClient(
28
- ATHENA_URL,
29
- ATHENA_API_KEY,
30
- {
31
- client: "CLIENT_NAME",
32
- backend: { type: "athena" },
33
- },
34
- );
35
-
36
- const { data, error } = await athenaClient.from("orchestral_sections").findMany({
37
- select: {
38
- name: true,
39
- instruments: {
40
- select: {
41
- name: true,
42
- },
43
- },
44
- },
45
- });
46
-
47
- if (error) {
48
- console.error("gateway error", error);
49
- } else {
50
- console.table(data);
51
- }
52
- ```
53
-
1
+ # athena-js
2
+
3
+ current version: `2.13.0`
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
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @xylex-group/athena
10
+ # or
11
+ pnpm add @xylex-group/athena
12
+ # or
13
+ yarn add @xylex-group/athena
14
+ ```
15
+
16
+ React peer dependency is optional — only needed if you use `@xylex-group/athena/react` hooks.
17
+
18
+ ```bash
19
+ npm install react # React >=17 required for the hook
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ ```ts
25
+ import { createClient } from "@xylex-group/athena";
26
+
27
+ const athenaClient = createClient(
28
+ ATHENA_URL,
29
+ ATHENA_API_KEY,
30
+ {
31
+ client: "CLIENT_NAME",
32
+ backend: { type: "athena" },
33
+ },
34
+ );
35
+
36
+ const { data, error } = await athenaClient.from("orchestral_sections").findMany({
37
+ select: {
38
+ name: true,
39
+ instruments: {
40
+ select: {
41
+ name: true,
42
+ },
43
+ },
44
+ },
45
+ });
46
+
47
+ if (error) {
48
+ console.error("gateway error", error);
49
+ } else {
50
+ console.table(data);
51
+ }
52
+ ```
53
+
54
54
  `createClient(url, key)` is the canonical SDK shape. The SDK treats `url` as the public unified Athena base URL and derives:
55
55
 
56
56
  - DB: `${url}/db`
@@ -58,10 +58,10 @@ if (error) {
58
58
  - Chat: `${url}/chat`
59
59
  - Chat realtime: `${url}` converted to `ws:` / `wss:` plus `/chat/ws`
60
60
  - Storage: `${url}/storage`
61
-
62
- You can still override individual services when needed:
63
-
64
- ```ts
61
+
62
+ You can still override individual services when needed:
63
+
64
+ ```ts
65
65
  const athena = createClient({
66
66
  key: ATHENA_API_KEY,
67
67
  db: { url: process.env.ATHENA_DB_URL },
@@ -73,7 +73,7 @@ const athena = createClient({
73
73
  storage: { url: process.env.ATHENA_STORAGE_URL },
74
74
  });
75
75
  ```
76
-
76
+
77
77
  Legacy top-level aliases are also supported:
78
78
 
79
79
  ```ts
@@ -187,20 +187,20 @@ These helpers:
187
187
  - return a pre-scoped client so normal Athena queries inherit current user + org context without app-local header assembly
188
188
 
189
189
  Example version baseline: SDK `@xylex-group/athena` `2.4.0`, Athena server `3.12.3` verified on 2026-06-04.
190
-
191
- `.findMany({ select, where, orderBy, limit })` is the clean canonical read surface.
192
- The existing string-based `.select(...)` chain remains fully supported for compatibility,
193
- including alias/FK patterns like `from:sender_id(name)`.
194
- For the full AST model, route contract, error behavior, and Athena server implications,
195
- see [`docs/findmany-ast-and-server-contract.md`](docs/findmany-ast-and-server-contract.md).
196
- For method-by-method runtime AST/state/payload models across `select(...)`, mutations,
197
- `rpc(...)`, `query(...)`, and fluent builder filters, see
198
- [`docs/runtime-method-ast-models.md`](docs/runtime-method-ast-models.md).
199
-
200
- ### Gateway auth-session forwarding
201
-
190
+
191
+ `.findMany({ select, where, orderBy, limit })` is the clean canonical read surface.
192
+ The existing string-based `.select(...)` chain remains fully supported for compatibility,
193
+ including alias/FK patterns like `from:sender_id(name)`.
194
+ For the full AST model, route contract, error behavior, and Athena server implications,
195
+ see [`docs/findmany-ast-and-server-contract.md`](docs/findmany-ast-and-server-contract.md).
196
+ For method-by-method runtime AST/state/payload models across `select(...)`, mutations,
197
+ `rpc(...)`, `query(...)`, and fluent builder filters, see
198
+ [`docs/runtime-method-ast-models.md`](docs/runtime-method-ast-models.md).
199
+
200
+ ### Gateway auth-session forwarding
201
+
202
202
  If you need Athena server-side auth rollout to inspect auth context on normal query requests, the SDK now lets you bind auth state once and mirrors that context into gateway headers while still forwarding the original headers too.
203
-
203
+
204
204
  Current behavior:
205
205
 
206
206
  - `headers.Cookie` containing an Athena auth session cookie keeps `Cookie` and also adds `X-Athena-Auth-Session-Token`
@@ -220,16 +220,16 @@ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
220
220
  },
221
221
  });
222
222
  ```
223
-
224
- For the full contract, precedence rules, browser/server caveats, and rollout guidance, see [`docs/auth-session-forwarding.md`](docs/auth-session-forwarding.md).
225
-
226
- ### Auth client (Athena Auth server)
227
-
228
- If your auth backend is now Athena Auth, you can keep core login/session flows in this SDK:
229
-
230
- ```ts
231
- import { createClient } from "@xylex-group/athena";
232
-
223
+
224
+ For the full contract, precedence rules, browser/server caveats, and rollout guidance, see [`docs/auth-session-forwarding.md`](docs/auth-session-forwarding.md).
225
+
226
+ ### Auth client (Athena Auth server)
227
+
228
+ If your auth backend is now Athena Auth, you can keep core login/session flows in this SDK:
229
+
230
+ ```ts
231
+ import { createClient } from "@xylex-group/athena";
232
+
233
233
  const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
234
234
  client: "CLIENT_NAME",
235
235
  auth: {
@@ -241,27 +241,27 @@ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
241
241
  credentials: "include",
242
242
  },
243
243
  });
244
-
245
- const login = await athena.auth.signIn.email({
246
- email: "demo@example.com",
247
- password: "super-secret",
248
- rememberMe: true,
249
- });
250
-
251
- const session = await athena.auth.getSession();
252
- const sessions = await athena.auth.session.list();
253
-
254
- // clear one session
255
- await athena.auth.session.revoke({ token: "session_token_here" });
256
- // or clear all sessions
257
- await athena.auth.session.revoke([{ token: "session_token_here" }, { token: "session_token_2" }]);
258
-
259
- await athena.auth.signOut();
260
-
261
- // additional core flows
262
- await athena.auth.forgetPassword({ email: "demo@example.com", redirectTo: "https://app/reset-password" });
263
- await athena.auth.resetPassword({ newPassword: "new-secret", token: "reset_token" });
264
- await athena.auth.verifyEmail({ token: "verify_token", callbackURL: "https://app/verified" });
244
+
245
+ const login = await athena.auth.signIn.email({
246
+ email: "demo@example.com",
247
+ password: "super-secret",
248
+ rememberMe: true,
249
+ });
250
+
251
+ const session = await athena.auth.getSession();
252
+ const sessions = await athena.auth.session.list();
253
+
254
+ // clear one session
255
+ await athena.auth.session.revoke({ token: "session_token_here" });
256
+ // or clear all sessions
257
+ await athena.auth.session.revoke([{ token: "session_token_here" }, { token: "session_token_2" }]);
258
+
259
+ await athena.auth.signOut();
260
+
261
+ // additional core flows
262
+ await athena.auth.forgetPassword({ email: "demo@example.com", redirectTo: "https://app/reset-password" });
263
+ await athena.auth.resetPassword({ newPassword: "new-secret", token: "reset_token" });
264
+ await athena.auth.verifyEmail({ token: "verify_token", callbackURL: "https://app/verified" });
265
265
  await athena.auth.changePassword({ currentPassword: "old-secret", newPassword: "new-secret" });
266
266
  await athena.auth.user.update({ name: "Demo User" });
267
267
  ```
@@ -274,73 +274,73 @@ await athena.auth.admin.hasPermission(
274
274
  { bearerToken: impersonationToken },
275
275
  );
276
276
  ```
277
-
278
- #### React Email templates for admin HTML routes
279
-
280
- If you use `@react-email/components`, you can pass component+props payloads directly on admin email/template routes:
281
-
282
- ```ts
283
- import { Body, Html, Text } from "@react-email/components";
284
-
285
- function WelcomeEmail(props: { name: string }) {
286
- return (
287
- <Html lang="en">
288
- <Body>
289
- <Text>Welcome {props.name}</Text>
290
- </Body>
291
- </Html>
292
- );
293
- }
294
-
277
+
278
+ #### React Email templates for admin HTML routes
279
+
280
+ If you use `@react-email/components`, you can pass component+props payloads directly on admin email/template routes:
281
+
282
+ ```ts
283
+ import { Body, Html, Text } from "@react-email/components";
284
+
285
+ function WelcomeEmail(props: { name: string }) {
286
+ return (
287
+ <Html lang="en">
288
+ <Body>
289
+ <Text>Welcome {props.name}</Text>
290
+ </Body>
291
+ </Html>
292
+ );
293
+ }
294
+
295
295
  await athena.auth.admin.email.template.create({
296
296
  template_key: "welcome",
297
297
  subject_template: "Welcome",
298
298
  react: {
299
299
  component: WelcomeEmail,
300
300
  props: { name: "Ava" },
301
- },
302
- });
303
- ```
304
-
305
- For reusable templates, use `defineAuthEmailTemplate(...)`:
306
-
307
- ```ts
308
- import { defineAuthEmailTemplate } from "@xylex-group/athena";
309
-
301
+ },
302
+ });
303
+ ```
304
+
305
+ For reusable templates, use `defineAuthEmailTemplate(...)`:
306
+
307
+ ```ts
308
+ import { defineAuthEmailTemplate } from "@xylex-group/athena";
309
+
310
310
  const welcomeTemplate = defineAuthEmailTemplate({
311
311
  component: WelcomeEmail,
312
312
  templateKey: "welcome",
313
313
  subjectTemplate: "Welcome",
314
314
  });
315
-
316
- await athena.auth.admin.email.template.create(
317
- welcomeTemplate.toTemplateCreate({
318
- props: { name: "Ava" },
319
- }),
320
- );
321
- ```
322
-
323
- You can also observe render timing/errors:
324
-
325
- ```ts
326
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
327
- auth: {
328
- baseUrl: AUTH_BASE_URL,
329
- reactEmail: {
330
- observe: (event) => {
331
- console.log(JSON.stringify(event));
332
- },
333
- },
334
- },
335
- });
336
- ```
337
-
338
- Install support packages in your app:
339
-
340
- ```bash
341
- pnpm add @react-email/components @react-email/render
342
- ```
343
-
315
+
316
+ await athena.auth.admin.email.template.create(
317
+ welcomeTemplate.toTemplateCreate({
318
+ props: { name: "Ava" },
319
+ }),
320
+ );
321
+ ```
322
+
323
+ You can also observe render timing/errors:
324
+
325
+ ```ts
326
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
327
+ auth: {
328
+ baseUrl: AUTH_BASE_URL,
329
+ reactEmail: {
330
+ observe: (event) => {
331
+ console.log(JSON.stringify(event));
332
+ },
333
+ },
334
+ },
335
+ });
336
+ ```
337
+
338
+ Install support packages in your app:
339
+
340
+ ```bash
341
+ pnpm add @react-email/components @react-email/render
342
+ ```
343
+
344
344
  Auth responses follow the same envelope style: `{ ok, status, data, error, errorDetails, raw }`.
345
345
 
346
346
  #### Chat module
@@ -382,99 +382,99 @@ Available chat surfaces:
382
382
  - `chat.message.reaction.add/remove`
383
383
  - `chat.message.search`
384
384
  - `chat.realtime.info/connect`
385
-
386
- #### Native auth bootstrap helpers
387
-
388
- If you want to remove `better-auth` from an app that is already aligned to
389
- Athena Auth session semantics, the SDK now ships a native bootstrap layer with
390
- an Athena-native `athenaAuth({...})` export that matches the Better Auth
391
- top-level contract:
392
-
393
- ```ts
394
- import { athenaAuth } from "@xylex-group/athena";
395
-
396
- export function getAuth(env: {
397
- DB: unknown;
398
- ATHENA_AUTH_URL: string;
399
- ATHENA_AUTH_SECRET: string;
400
- GITHUB_CLIENT_ID: string;
401
- GITHUB_CLIENT_SECRET: string;
402
- }) {
403
- return athenaAuth({
404
- baseURL: env.ATHENA_AUTH_URL,
405
- secret: env.ATHENA_AUTH_SECRET,
406
- database: env.DB,
407
- socialProviders: {
408
- github: {
409
- clientId: env.GITHUB_CLIENT_ID,
410
- clientSecret: env.GITHUB_CLIENT_SECRET,
411
- scope: ["repo", "read:org", "user:email"],
412
- },
413
- },
414
- });
415
- }
416
- ```
417
-
418
- The returned auth object now carries the Better Auth-style top-level contract:
419
-
420
- - `handler(request)`
421
- - `api`
422
- - `options`
423
- - `$context`
424
- - `$ERROR_CODES`
425
-
426
- This native layer currently covers:
427
-
428
- - typed auth bootstrap config
429
- - session cookie set/clear helpers via the SDK cookie primitives
430
-
431
- It also supports dynamic `baseURL` host resolution plus static/dynamic
432
- `trustedOrigins` and `trustedProviders` on the native server bootstrap.
433
-
434
- For the full details and current scope, see [`docs/auth/server-bootstrap.mdx`](docs/auth/server-bootstrap.mdx).
435
-
436
- ### Typed schema registry (table-first)
437
-
438
- You can keep `createClient(...).from<T>(...)` as-is, or opt into a typed registry with the new Zero-style table DSL:
439
-
440
- ```ts
441
- import {
442
- boolean,
443
- createTypedClient,
444
- defineDatabase,
445
- defineRegistry,
446
- defineSchema,
447
- enumeration,
448
- json,
449
- string,
450
- table,
451
- } from "@xylex-group/athena";
452
-
453
- const users = table("users")
454
- .schema("public")
455
- .columns({
456
- id: string().generated(),
457
- email: string(),
458
- active: boolean().defaulted(),
459
- mood: enumeration(["happy", "sad"] as const).optional(),
460
- settings: json<{ theme: "light" | "dark" }>(),
461
- })
462
- .primaryKey("id");
463
-
464
- const registry = defineRegistry({
465
- primary: defineDatabase({
466
- public: defineSchema({
467
- users,
468
- }),
469
- }),
470
- });
471
-
472
- const typed = createTypedClient(registry, ATHENA_URL, ATHENA_API_KEY, {
473
- tenantKeyMap: {
474
- organizationId: "X-Organization-Id",
475
- },
476
- });
477
-
385
+
386
+ #### Native auth bootstrap helpers
387
+
388
+ If you want to remove `better-auth` from an app that is already aligned to
389
+ Athena Auth session semantics, the SDK now ships a native bootstrap layer with
390
+ an Athena-native `athenaAuth({...})` export that matches the Better Auth
391
+ top-level contract:
392
+
393
+ ```ts
394
+ import { athenaAuth } from "@xylex-group/athena";
395
+
396
+ export function getAuth(env: {
397
+ DB: unknown;
398
+ ATHENA_AUTH_URL: string;
399
+ ATHENA_AUTH_SECRET: string;
400
+ GITHUB_CLIENT_ID: string;
401
+ GITHUB_CLIENT_SECRET: string;
402
+ }) {
403
+ return athenaAuth({
404
+ baseURL: env.ATHENA_AUTH_URL,
405
+ secret: env.ATHENA_AUTH_SECRET,
406
+ database: env.DB,
407
+ socialProviders: {
408
+ github: {
409
+ clientId: env.GITHUB_CLIENT_ID,
410
+ clientSecret: env.GITHUB_CLIENT_SECRET,
411
+ scope: ["repo", "read:org", "user:email"],
412
+ },
413
+ },
414
+ });
415
+ }
416
+ ```
417
+
418
+ The returned auth object now carries the Better Auth-style top-level contract:
419
+
420
+ - `handler(request)`
421
+ - `api`
422
+ - `options`
423
+ - `$context`
424
+ - `$ERROR_CODES`
425
+
426
+ This native layer currently covers:
427
+
428
+ - typed auth bootstrap config
429
+ - session cookie set/clear helpers via the SDK cookie primitives
430
+
431
+ It also supports dynamic `baseURL` host resolution plus static/dynamic
432
+ `trustedOrigins` and `trustedProviders` on the native server bootstrap.
433
+
434
+ For the full details and current scope, see [`docs/auth/server-bootstrap.mdx`](docs/auth/server-bootstrap.mdx).
435
+
436
+ ### Typed schema registry (table-first)
437
+
438
+ You can keep `createClient(...).from<T>(...)` as-is, or opt into a typed registry with the new Zero-style table DSL:
439
+
440
+ ```ts
441
+ import {
442
+ boolean,
443
+ createTypedClient,
444
+ defineDatabase,
445
+ defineRegistry,
446
+ defineSchema,
447
+ enumeration,
448
+ json,
449
+ string,
450
+ table,
451
+ } from "@xylex-group/athena";
452
+
453
+ const users = table("users")
454
+ .schema("public")
455
+ .columns({
456
+ id: string().generated(),
457
+ email: string(),
458
+ active: boolean().defaulted(),
459
+ mood: enumeration(["happy", "sad"] as const).optional(),
460
+ settings: json<{ theme: "light" | "dark" }>(),
461
+ })
462
+ .primaryKey("id");
463
+
464
+ const registry = defineRegistry({
465
+ primary: defineDatabase({
466
+ public: defineSchema({
467
+ users,
468
+ }),
469
+ }),
470
+ });
471
+
472
+ const typed = createTypedClient(registry, ATHENA_URL, ATHENA_API_KEY, {
473
+ tenantKeyMap: {
474
+ organizationId: "X-Organization-Id",
475
+ },
476
+ });
477
+
478
478
  await typed
479
479
  .withTenantContext({ organizationId: "org_1" })
480
480
  .fromModel("primary", "public", "users")
@@ -489,97 +489,97 @@ const requestScoped = typed.withContext({
489
489
  });
490
490
 
491
491
  const insert = users.schemas.form.parse({
492
- email: "ada@example.com",
493
- mood: "",
494
- settings: { theme: "light" },
495
- });
496
- ```
497
-
498
- You can also pass that native Athena table/model value directly into the root client to avoid repeating the string table target:
499
-
500
- ```ts
501
- const result = await athena.from(users)
502
- .select("id, email, active")
503
- .eq("active", true)
504
- .order("created_at", { ascending: false })
505
- .limit(25);
506
- ```
507
-
508
- This is the viable opt-in short form because the `users` value carries runtime target metadata. A generic-only call like `from<UserPublicSchema>()` cannot resolve a table at runtime after TypeScript erases types.
509
-
510
- If you want compile-time validation for simple string selects and RPC column names, enable the experimental strict mode:
511
-
512
- ```ts
513
- const strictAthena = createClient(ATHENA_URL, ATHENA_API_KEY, {
514
- experimental: {
515
- typecheckColumns: true,
516
- },
517
- });
518
-
519
- await strictAthena.from(users).select("id, email").order("created_at");
520
-
521
- // compile-time error
522
- strictAthena.from(users).select("id, missing_column");
523
- ```
524
-
525
- For the DB helper surface, use `strictAthena.db.from<UserRow>("users").select("id, email")` when you want inline typed column validation. `strictAthena.db.select<UserRow>("users")` still gives a typed row-aware chain, but it does not accept inline typed column arguments.
526
-
492
+ email: "ada@example.com",
493
+ mood: "",
494
+ settings: { theme: "light" },
495
+ });
496
+ ```
497
+
498
+ You can also pass that native Athena table/model value directly into the root client to avoid repeating the string table target:
499
+
500
+ ```ts
501
+ const result = await athena.from(users)
502
+ .select("id, email, active")
503
+ .eq("active", true)
504
+ .order("created_at", { ascending: false })
505
+ .limit(25);
506
+ ```
507
+
508
+ This is the viable opt-in short form because the `users` value carries runtime target metadata. A generic-only call like `from<UserPublicSchema>()` cannot resolve a table at runtime after TypeScript erases types.
509
+
510
+ If you want compile-time validation for simple string selects and RPC column names, enable the experimental strict mode:
511
+
512
+ ```ts
513
+ const strictAthena = createClient(ATHENA_URL, ATHENA_API_KEY, {
514
+ experimental: {
515
+ typecheckColumns: true,
516
+ },
517
+ });
518
+
519
+ await strictAthena.from(users).select("id, email").order("created_at");
520
+
521
+ // compile-time error
522
+ strictAthena.from(users).select("id, missing_column");
523
+ ```
524
+
525
+ For the DB helper surface, use `strictAthena.db.from<UserRow>("users").select("id, email")` when you want inline typed column validation. `strictAthena.db.select<UserRow>("users")` still gives a typed row-aware chain, but it does not accept inline typed column arguments.
526
+
527
527
  `defineModel(...)` is deprecated and retained for compatibility and manual low-level contracts. Prefer `table(...).schema(...).columns(...).primaryKey(...)` for new model authoring.
528
-
529
- For full details, see [`docs/typed-schema-registry.md`](./docs/typed-schema-registry.md).
530
-
531
- 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).
532
-
533
- ### Typed schema generator
534
-
535
- Schema generation is additive. Existing `createClient(...).from<T>(...)` usage remains valid while teams migrate to generated registry files.
536
-
537
- CLI:
538
-
539
- ```bash
540
- athena-js generate
541
- athena-js generate --dry-run
542
- athena-js generate --config ./athena.config.ts
543
- athena-js generate --help
544
- athena-js help generate
545
- ```
546
-
547
- Out of the box, `athena-js generate` now works without an `athena.config.*` file in the common cases:
548
-
549
- ```bash
550
- DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_db athena-js generate --dry-run
551
- ```
552
-
553
- ```bash
554
- ATHENA_URL=https://athena-db.com ATHENA_API_KEY=secret ATHENA_GENERATOR_DB=app_db athena-js generate --dry-run
555
- ```
556
-
557
- Smallest direct-mode config file:
558
-
559
- ```ts
560
- import { defineGeneratorConfig } from "@xylex-group/athena";
561
-
562
- export default defineGeneratorConfig({
563
- provider: {
564
- kind: "postgres",
565
- mode: "direct",
566
- },
567
- });
568
- ```
569
-
528
+
529
+ For full details, see [`docs/typed-schema-registry.md`](./docs/typed-schema-registry.md).
530
+
531
+ 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).
532
+
533
+ ### Typed schema generator
534
+
535
+ Schema generation is additive. Existing `createClient(...).from<T>(...)` usage remains valid while teams migrate to generated registry files.
536
+
537
+ CLI:
538
+
539
+ ```bash
540
+ athena-js generate
541
+ athena-js generate --dry-run
542
+ athena-js generate --config ./athena.config.ts
543
+ athena-js generate --help
544
+ athena-js help generate
545
+ ```
546
+
547
+ Out of the box, `athena-js generate` now works without an `athena.config.*` file in the common cases:
548
+
549
+ ```bash
550
+ DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_db athena-js generate --dry-run
551
+ ```
552
+
553
+ ```bash
554
+ ATHENA_URL=https://athena-db.com ATHENA_API_KEY=secret ATHENA_GENERATOR_DB=app_db athena-js generate --dry-run
555
+ ```
556
+
557
+ Smallest direct-mode config file:
558
+
559
+ ```ts
560
+ import { defineGeneratorConfig } from "@xylex-group/athena";
561
+
562
+ export default defineGeneratorConfig({
563
+ provider: {
564
+ kind: "postgres",
565
+ mode: "direct",
566
+ },
567
+ });
568
+ ```
569
+
570
570
  Smallest table-builder config:
571
-
572
- ```ts
573
- import { defineGeneratorConfig } from "@xylex-group/athena";
574
-
575
- export default defineGeneratorConfig({
576
- provider: {
577
- kind: "postgres",
578
- mode: "direct",
579
- },
580
- output: {
581
- format: "table-builder",
582
- },
571
+
572
+ ```ts
573
+ import { defineGeneratorConfig } from "@xylex-group/athena";
574
+
575
+ export default defineGeneratorConfig({
576
+ provider: {
577
+ kind: "postgres",
578
+ mode: "direct",
579
+ },
580
+ output: {
581
+ format: "table-builder",
582
+ },
583
583
  });
584
584
  ```
585
585
 
@@ -590,43 +590,43 @@ Important:
590
590
  - the default generator mode is now safe direct `table-builder` output with `output.preset = "athena-direct"`
591
591
  - the default registry target is `athena/registry.generated.ts`; opt into `output.preset = "legacy"` only when you intentionally need `athena/config.ts`
592
592
  - if you want flat `athena/models/*.ts` files, set `output.targets.model = "athena/models/{model_kebab}.ts"` (or `ATHENA_GENERATOR_MODEL_TARGET=athena/models/{model_kebab}.ts`); multi-schema collisions are still auto-scoped by schema when needed
593
-
594
- Common copy-paste starts:
595
-
596
- ```bash
597
- # direct postgres, no config file
598
- DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_db athena-js generate --dry-run
599
-
593
+
594
+ Common copy-paste starts:
595
+
596
+ ```bash
597
+ # direct postgres, no config file
598
+ DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_db athena-js generate --dry-run
599
+
600
600
  # direct postgres + Zero-style output + multiple schemas
601
601
  DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_db \
602
602
  ATHENA_GENERATOR_SCHEMAS=public,analytics \
603
603
  athena-js generate --dry-run
604
-
605
- # gateway-only CI job, no config file
606
- ATHENA_URL=https://athena-db.com \
607
- ATHENA_API_KEY=secret \
608
- ATHENA_GENERATOR_DB=app_db \
609
- athena-js generate --dry-run
610
- ```
611
-
612
- Smallest gateway table-builder config:
613
-
614
- ```ts
615
- import { defineGeneratorConfig } from "@xylex-group/athena";
616
-
617
- export default defineGeneratorConfig({
618
- provider: {
619
- kind: "postgres",
620
- mode: "gateway",
621
- },
622
- output: {
623
- format: "table-builder",
624
- },
625
- });
626
- ```
627
-
628
- Generator supports:
629
-
604
+
605
+ # gateway-only CI job, no config file
606
+ ATHENA_URL=https://athena-db.com \
607
+ ATHENA_API_KEY=secret \
608
+ ATHENA_GENERATOR_DB=app_db \
609
+ athena-js generate --dry-run
610
+ ```
611
+
612
+ Smallest gateway table-builder config:
613
+
614
+ ```ts
615
+ import { defineGeneratorConfig } from "@xylex-group/athena";
616
+
617
+ export default defineGeneratorConfig({
618
+ provider: {
619
+ kind: "postgres",
620
+ mode: "gateway",
621
+ },
622
+ output: {
623
+ format: "table-builder",
624
+ },
625
+ });
626
+ ```
627
+
628
+ Generator supports:
629
+
630
630
  - PostgreSQL direct introspection (`provider.mode = "direct"`, `provider.connectionString` from your `PG_URL`/`DATABASE_URL`)
631
631
  - PostgreSQL gateway-only introspection (`provider.mode = "gateway"` via Athena `POST /gateway/query`)
632
632
  - Multiple schema syncs such as `public` plus `athena`, with schema-safe default output paths
@@ -635,824 +635,830 @@ Generator supports:
635
635
  - Placeholder-driven output paths
636
636
  - Feature flags (`features.emitRegistry`, `features.emitRelations`)
637
637
  - Typed env-backed config fields via `generatorEnv(...)` for connection strings, schema lists, naming styles, flags, and placeholder maps
638
-
639
- For copy-paste quickstarts and more example profiles, see [`docs/generator-quickstart.md`](./docs/generator-quickstart.md).
640
- For full generator configuration and troubleshooting, see [`docs/generator-config.md`](./docs/generator-config.md).
641
- For full CLI commands, help behavior, and troubleshooting, see [`docs/cli-command-reference.md`](./docs/cli-command-reference.md).
642
- For CI/CD pipelines and generated-file branch policy, see [`docs/generator-cicd.md`](./docs/generator-cicd.md).
643
- For prompt-ready documentation handoff text, see [`docs/generator-codex-handoff-prompt-pack.md`](./docs/generator-codex-handoff-prompt-pack.md).
644
-
645
- ### Athena JS and Athena RS
646
-
647
- `athena-js` is designed to be standalone for TypeScript/Node and React-native workflows:
648
-
649
- - query builder + hooks
650
- - typed registry and generator pipeline
651
- - CLI-driven codegen in JS/TS projects
652
-
653
- `athena-rs` remains the faster fit for Rust service execution paths. Teams can run both in parallel:
654
-
655
- - `athena-rs` for Rust backend throughput
656
- - `athena-js` for app/tooling layers that need TypeScript contracts and frontend-facing ergonomics
657
-
658
- Every query resolves to `{ data, error, errorDetails?, status, statusText?, count?, raw }`. `data` is `null` on error; `error` is `null` on success.
659
-
660
- Failed results now include a structured `error` object with the useful fields inline:
661
-
662
- - `message`
663
- - `code`
664
- - `details`
665
- - `hint`
666
- - `status`
667
- - `statusText`
668
- - normalized metadata such as `kind`, `table`, `operation`, and `retryable`
669
-
670
- `errorDetails` is still present as a compatibility alias for low-level gateway metadata (`gatewayCode`, `endpoint`, `method`, `requestId`, etc.).
671
-
672
- ## Reliability helper APIs
673
-
674
- The SDK exports composable helpers to reduce repetitive route-handler logic.
675
-
676
- ### Result unwrapping and success guards
677
-
678
- ```ts
679
- import {
680
- isOk,
681
- unwrap,
682
- unwrapRows,
683
- unwrapOne,
684
- requireSuccess,
685
- requireAffected,
686
- } from "@xylex-group/athena";
687
-
688
- const result = await athena.from("users").select("id,name");
689
-
690
- if (isOk(result)) {
691
- const rows = unwrapRows(result); // typed User[]
692
- console.log(rows.length);
693
- }
694
-
695
- const one = await athena.from("users").eq("id", 1).single("id,name");
696
- const user = unwrapOne(one, { allowNull: true });
697
-
698
- const inserted = await athena
699
- .from("users")
700
- .insert({ name: "Alice" })
701
- .select("id", { count: "exact" });
702
-
703
- requireSuccess(inserted, { table: "users", operation: "insert" });
704
- requireAffected(inserted, { min: 1 }, { table: "users", operation: "insert" });
705
- ```
706
-
707
- `requireAffected` uses `result.count`; request it on writes with `{ count: "exact" }` when you need enforced postconditions.
708
-
709
- ### Structured errors by default
710
-
711
- ```ts
712
- import { createClient } from "@xylex-group/athena";
713
-
714
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY);
715
-
716
- const { data, error, status, statusText } = await athena.from("users").insert({ id: 1 }).select();
717
- if (error) {
718
- console.error(error);
719
- console.error(error.hint ?? error.message, status, statusText);
720
- if (error.kind === "unique_violation") {
721
- // deterministic conflict handling
722
- }
723
- }
724
- ```
725
-
726
- `result.error` already carries normalized `kind` values (`unique_violation`, `validation`, `auth`, `rate_limit`, `transient`, etc.) plus operation metadata.
727
-
728
- `normalizeAthenaError(...)` is deprecated. Prefer `result.error` on failed results and the structured fields already attached to thrown SDK errors.
729
-
730
- ### Query tracing (experimental)
731
-
732
- ```ts
733
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
734
- experimental: { traceQueries: true, debugAst: true },
735
- });
736
- ```
737
-
738
- With `traceQueries: true`, the SDK logs every runtime execution (`select`, `insert`, `upsert`, `update`, `delete`, `rpc`, `query`) and includes:
739
-
740
- - the gateway endpoint used
741
- - synthesized SQL (or raw SQL for `query(...)` and SQL fallback reads)
742
- - payload and call options
743
- - full outcome (`status`, `error`, `count`, `data`, `raw`)
744
- - callsite metadata (`filePath`, `fileName`, `line`, `column`)
745
-
746
- 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.
747
-
748
- Use a custom sink:
749
-
750
- ```ts
751
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
752
- experimental: {
753
- traceQueries: {
754
- logger(event) {
755
- // Forward into your logger/observability sink
756
- console.log(event.operation, event.endpoint, event.sql, event.callsite);
757
- },
758
- },
759
- },
760
- });
761
- ```
762
-
763
- If you also enable `debugAst: true`, every traced operation includes a normalized AST, and successful results expose the same AST through `getAthenaDebugAst(...)`:
764
-
765
- ```ts
766
- import { createClient, getAthenaDebugAst } from "@xylex-group/athena";
767
-
768
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
769
- experimental: {
770
- debugAst: true,
771
- },
772
- });
773
-
774
- const result = await athena.from("users").eq("id", 1).select("id");
775
- const ast = getAthenaDebugAst(result);
776
- ```
777
-
778
- This works across the runtime operation families too:
779
-
780
- ```ts
781
- const inserted = await athena
782
- .from("users")
783
- .insert({ email: "ada@example.com" })
784
- .select("id,email");
785
-
786
- const insertedAst = getAthenaDebugAst(inserted);
787
-
788
- const rpcResult = await athena
789
- .rpc("list_users", { role: "admin" })
790
- .eq("active", true)
791
- .select("id,email");
792
-
793
- const rpcAst = getAthenaDebugAst(rpcResult);
794
-
795
- const sqlResult = await athena.query<{ id: number }>("select id from users where active = true");
796
- const sqlAst = getAthenaDebugAst(sqlResult);
797
- ```
798
-
799
- If `traceQueries` is enabled too, the same normalized AST is emitted on each `AthenaQueryTraceEvent.ast`.
800
-
801
- ### Read retries (experimental)
802
-
803
- ```ts
804
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
805
- experimental: {
806
- retryReads: true,
807
- },
808
- });
809
- ```
810
-
811
- With `retryReads: true`, the SDK automatically retries retryable read failures for `select`, `findMany(...)`, and `query(...)`.
812
-
813
- - two additional attempts are applied internally
814
- - retry classification follows the SDK's normalized `retryable` signal
815
- - writes (`insert`, `upsert`, `update`, `delete`) are not retried by this flag
816
-
817
- ### findMany AST transport (experimental)
818
-
819
- ```ts
820
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
821
- experimental: {
822
- findManyAst: true,
823
- },
824
- });
825
- ```
826
-
827
- With `findManyAst: true`, clean `findMany(...)` calls can send an AST-style body to `/gateway/fetch` instead of compiling the select tree down to `columns` and `conditions` first.
828
-
829
- - this is opt-in and meant for gateways that explicitly support direct AST bodies
830
- - existing compiled `findMany(...)` transport remains the default
831
- - shorthand `where` filters are normalized to explicit operator objects before the AST body is sent
832
- - UUID-like equality filters that need the SDK's `::text` comparison still fall back to the legacy query/compiled path
833
- - nested relation select strings stay off the SQL query fallback path and continue through `/gateway/fetch`
834
- - chained builder filters or pagination state that the AST body cannot represent losslessly yet continue to use the legacy compiled path
835
- - trace output still includes synthesized SQL so diagnostics stay readable
836
-
837
- ### Numeric coercion
838
-
839
- ```ts
840
- import { coerceInt, assertInt } from "@xylex-group/athena";
841
-
842
- const maybeCaseId = coerceInt(req.query.case_id, { min: 1 });
843
- if (maybeCaseId == null) throw new Error("Invalid case id");
844
-
845
- const caseId = assertInt(req.query.case_id, "case_id", { min: 1 });
846
- ```
847
-
848
- ### Utilities subpath
849
-
850
- Utilities that are intentionally not exported from the root package are available from `@xylex-group/athena/utils`.
851
-
852
- ```ts
853
- import {
854
- asString,
855
- asBoolean,
856
- asBooleanOrNull,
857
- asRecord,
858
- asIdentifier,
859
- firstString,
860
- readTrimmedString,
861
- asNumber,
862
- asStringArray,
863
- slugify,
864
- trimTrailingSlashes,
865
- parseBooleanFlag,
866
- isLocalHostname,
867
- clearAuthCookies,
868
- proxyRequestHeaders,
869
- sqlText,
870
- escapeLikePatternValue,
871
- quoteSqlStringLiteral,
872
- sqlNullableText,
873
- sqlJsonbLiteral,
874
- sqlBigInt,
875
- } from "@xylex-group/athena/utils";
876
- ```
877
-
878
- Examples:
879
-
880
- ```ts
881
- const slug = slugify("Customer Success / Q4 Report"); // customer-success-q4-report
882
- const local = isLocalHostname("api.localhost"); // true
883
- const normalized = trimTrailingSlashes("https://example.com///"); // https://example.com
884
- const enabled = parseBooleanFlag(process.env.FEATURE_FLAG, false);
885
- const count = asNumber("42"); // 42
886
- const label = asString(" ready "); // ready
887
- const active = asBooleanOrNull("yes"); // true
888
- const tags = asStringArray([" alpha ", "", "beta"]); // ["alpha", "beta"]
889
- const likePattern = escapeLikePatternValue("%admin_"); // \%admin\_
890
-
891
- // Browser-only helper (safe no-op on server runtimes)
892
- clearAuthCookies();
893
-
894
- // Preserve forwarded headers when proxying auth requests
895
- const upstreamHeaders = proxyRequestHeaders(request);
896
-
897
- // Safely embed raw SQL values when using athena.query(...)
898
- const emailLiteral = sqlText("floris@example.com");
899
- const metadataLiteral = sqlJsonbLiteral({ role: "admin" });
900
- const actorIdLiteral = sqlBigInt(42);
901
- const exactLiteral = quoteSqlStringLiteral("Athena's SDK");
902
- ```
903
-
904
- `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.
905
- For SQL identifiers, keep using `identifier(...)`; `sqlText(...)`-style helpers are for literal values only.
906
-
638
+
639
+ For copy-paste quickstarts and more example profiles, see [`docs/generator-quickstart.md`](./docs/generator-quickstart.md).
640
+ For full generator configuration and troubleshooting, see [`docs/generator-config.md`](./docs/generator-config.md).
641
+ For full CLI commands, help behavior, and troubleshooting, see [`docs/cli-command-reference.md`](./docs/cli-command-reference.md).
642
+ For CI/CD pipelines and generated-file branch policy, see [`docs/generator-cicd.md`](./docs/generator-cicd.md).
643
+ For prompt-ready documentation handoff text, see [`docs/generator-codex-handoff-prompt-pack.md`](./docs/generator-codex-handoff-prompt-pack.md).
644
+
645
+ ### Athena JS and Athena RS
646
+
647
+ `athena-js` is designed to be standalone for TypeScript/Node and React-native workflows:
648
+
649
+ - query builder + hooks
650
+ - typed registry and generator pipeline
651
+ - CLI-driven codegen in JS/TS projects
652
+
653
+ `athena-rs` remains the faster fit for Rust service execution paths. Teams can run both in parallel:
654
+
655
+ - `athena-rs` for Rust backend throughput
656
+ - `athena-js` for app/tooling layers that need TypeScript contracts and frontend-facing ergonomics
657
+
658
+ Every query resolves to `{ data, error, errorDetails?, status, statusText?, count?, raw }`. `data` is `null` on error; `error` is `null` on success.
659
+
660
+ Failed results now include a structured `error` object with the useful fields inline:
661
+
662
+ - `message`
663
+ - `code`
664
+ - `details`
665
+ - `hint`
666
+ - `status`
667
+ - `statusText`
668
+ - normalized metadata such as `kind`, `table`, `operation`, and `retryable`
669
+
670
+ `errorDetails` is still present as a compatibility alias for low-level gateway metadata (`gatewayCode`, `endpoint`, `method`, `requestId`, etc.).
671
+
672
+ ## Reliability helper APIs
673
+
674
+ The SDK exports composable helpers to reduce repetitive route-handler logic.
675
+
676
+ ### Result unwrapping and success guards
677
+
678
+ ```ts
679
+ import {
680
+ isOk,
681
+ unwrap,
682
+ unwrapRows,
683
+ unwrapOne,
684
+ requireSuccess,
685
+ requireAffected,
686
+ } from "@xylex-group/athena";
687
+
688
+ const result = await athena.from("users").select("id,name");
689
+
690
+ if (isOk(result)) {
691
+ const rows = unwrapRows(result); // typed User[]
692
+ console.log(rows.length);
693
+ }
694
+
695
+ const one = await athena.from("users").eq("id", 1).single("id,name");
696
+ const user = unwrapOne(one, { allowNull: true });
697
+
698
+ const inserted = await athena
699
+ .from("users")
700
+ .insert({ name: "Alice" })
701
+ .select("id", { count: "exact" });
702
+
703
+ requireSuccess(inserted, { table: "users", operation: "insert" });
704
+ requireAffected(inserted, { min: 1 }, { table: "users", operation: "insert" });
705
+ ```
706
+
707
+ `requireAffected` uses `result.count`; request it on writes with `{ count: "exact" }` when you need enforced postconditions.
708
+
709
+ ### Structured errors by default
710
+
711
+ ```ts
712
+ import { createClient } from "@xylex-group/athena";
713
+
714
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY);
715
+
716
+ const { data, error, status, statusText } = await athena.from("users").insert({ id: 1 }).select();
717
+ if (error) {
718
+ console.error(error);
719
+ console.error(error.hint ?? error.message, status, statusText);
720
+ if (error.kind === "unique_violation") {
721
+ // deterministic conflict handling
722
+ }
723
+ }
724
+ ```
725
+
726
+ `result.error` already carries normalized `kind` values (`unique_violation`, `validation`, `auth`, `rate_limit`, `transient`, etc.) plus operation metadata.
727
+
728
+ `normalizeAthenaError(...)` is deprecated. Prefer `result.error` on failed results and the structured fields already attached to thrown SDK errors.
729
+
730
+ ### Query tracing (experimental)
731
+
732
+ ```ts
733
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
734
+ experimental: { traceQueries: true, debugAst: true },
735
+ });
736
+ ```
737
+
738
+ With `traceQueries: true`, the SDK logs every runtime execution (`select`, `insert`, `upsert`, `update`, `delete`, `rpc`, `query`) and includes:
739
+
740
+ - the gateway endpoint used
741
+ - synthesized SQL (or raw SQL for `query(...)` and SQL fallback reads)
742
+ - payload and call options
743
+ - full outcome (`status`, `error`, `count`, `data`, `raw`)
744
+ - callsite metadata (`filePath`, `fileName`, `line`, `column`)
745
+
746
+ 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.
747
+
748
+ Use a custom sink:
749
+
750
+ ```ts
751
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
752
+ experimental: {
753
+ traceQueries: {
754
+ logger(event) {
755
+ // Forward into your logger/observability sink
756
+ console.log(event.operation, event.endpoint, event.sql, event.callsite);
757
+ },
758
+ },
759
+ },
760
+ });
761
+ ```
762
+
763
+ If you also enable `debugAst: true`, every traced operation includes a normalized AST, and successful results expose the same AST through `getAthenaDebugAst(...)`:
764
+
765
+ ```ts
766
+ import { createClient, getAthenaDebugAst } from "@xylex-group/athena";
767
+
768
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
769
+ experimental: {
770
+ debugAst: true,
771
+ },
772
+ });
773
+
774
+ const result = await athena.from("users").eq("id", 1).select("id");
775
+ const ast = getAthenaDebugAst(result);
776
+ ```
777
+
778
+ This works across the runtime operation families too:
779
+
780
+ ```ts
781
+ const inserted = await athena
782
+ .from("users")
783
+ .insert({ email: "ada@example.com" })
784
+ .select("id,email");
785
+
786
+ const insertedAst = getAthenaDebugAst(inserted);
787
+
788
+ const rpcResult = await athena
789
+ .rpc("list_users", { role: "admin" })
790
+ .eq("active", true)
791
+ .select("id,email");
792
+
793
+ const rpcAst = getAthenaDebugAst(rpcResult);
794
+
795
+ const sqlResult = await athena.query<{ id: number }>("select id from users where active = true");
796
+ const sqlAst = getAthenaDebugAst(sqlResult);
797
+ ```
798
+
799
+ If `traceQueries` is enabled too, the same normalized AST is emitted on each `AthenaQueryTraceEvent.ast`.
800
+
801
+ ### Read retries (experimental)
802
+
803
+ ```ts
804
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
805
+ experimental: {
806
+ retryReads: true,
807
+ },
808
+ });
809
+ ```
810
+
811
+ With `retryReads: true`, the SDK automatically retries retryable read failures for `select`, `findMany(...)`, and `query(...)`.
812
+
813
+ - two additional attempts are applied internally
814
+ - retry classification follows the SDK's normalized `retryable` signal
815
+ - writes (`insert`, `upsert`, `update`, `delete`) are not retried by this flag
816
+
817
+ ### findMany AST transport (experimental)
818
+
819
+ ```ts
820
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
821
+ experimental: {
822
+ findManyAst: true,
823
+ },
824
+ });
825
+ ```
826
+
827
+ With `findManyAst: true`, clean `findMany(...)` calls can send an AST-style body to `/gateway/fetch` instead of compiling the select tree down to `columns` and `conditions` first.
828
+
829
+ - this is opt-in and meant for gateways that explicitly support direct AST bodies
830
+ - existing compiled `findMany(...)` transport remains the default
831
+ - shorthand `where` filters are normalized to explicit operator objects before the AST body is sent
832
+ - UUID-like equality filters that need the SDK's `::text` comparison still fall back to the legacy query/compiled path
833
+ - nested relation select strings stay off the SQL query fallback path and continue through `/gateway/fetch`
834
+ - chained builder filters or pagination state that the AST body cannot represent losslessly yet continue to use the legacy compiled path
835
+ - trace output still includes synthesized SQL so diagnostics stay readable
836
+
837
+ ### Numeric coercion
838
+
839
+ ```ts
840
+ import { coerceInt, assertInt } from "@xylex-group/athena";
841
+
842
+ const maybeCaseId = coerceInt(req.query.case_id, { min: 1 });
843
+ if (maybeCaseId == null) throw new Error("Invalid case id");
844
+
845
+ const caseId = assertInt(req.query.case_id, "case_id", { min: 1 });
846
+ ```
847
+
848
+ ### Utilities subpath
849
+
850
+ Utilities that are intentionally not exported from the root package are available from `@xylex-group/athena/utils`.
851
+
852
+ ```ts
853
+ import {
854
+ asString,
855
+ asBoolean,
856
+ asBooleanOrNull,
857
+ asRecord,
858
+ asIdentifier,
859
+ firstString,
860
+ readTrimmedString,
861
+ asNumber,
862
+ asStringArray,
863
+ slugify,
864
+ trimTrailingSlashes,
865
+ parseBooleanFlag,
866
+ isLocalHostname,
867
+ clearAuthCookies,
868
+ proxyRequestHeaders,
869
+ sqlText,
870
+ escapeLikePatternValue,
871
+ quoteSqlStringLiteral,
872
+ sqlNullableText,
873
+ sqlJsonbLiteral,
874
+ sqlBigInt,
875
+ } from "@xylex-group/athena/utils";
876
+ ```
877
+
878
+ Examples:
879
+
880
+ ```ts
881
+ const slug = slugify("Customer Success / Q4 Report"); // customer-success-q4-report
882
+ const local = isLocalHostname("api.localhost"); // true
883
+ const normalized = trimTrailingSlashes("https://example.com///"); // https://example.com
884
+ const enabled = parseBooleanFlag(process.env.FEATURE_FLAG, false);
885
+ const count = asNumber("42"); // 42
886
+ const label = asString(" ready "); // ready
887
+ const active = asBooleanOrNull("yes"); // true
888
+ const tags = asStringArray([" alpha ", "", "beta"]); // ["alpha", "beta"]
889
+ const likePattern = escapeLikePatternValue("%admin_"); // \%admin\_
890
+
891
+ // Browser-only helper (safe no-op on server runtimes)
892
+ clearAuthCookies();
893
+
894
+ // Preserve forwarded headers when proxying auth requests
895
+ const upstreamHeaders = proxyRequestHeaders(request);
896
+
897
+ // Safely embed raw SQL values when using athena.query(...)
898
+ const emailLiteral = sqlText("floris@example.com");
899
+ const metadataLiteral = sqlJsonbLiteral({ role: "admin" });
900
+ const actorIdLiteral = sqlBigInt(42);
901
+ const exactLiteral = quoteSqlStringLiteral("Athena's SDK");
902
+ ```
903
+
904
+ `clearAuthCookies()` clears cookies matching Athena/Better Auth prefixes
905
+ (`athena-auth`, `__Secure-athena-auth`, `better-auth`, `__Secure-better-auth`)
906
+ and attempts parent-domain cleanup for subdomain deployments. Prefer this
907
+ export over a local `document.cookie` wipe. After session-bridge login, also
908
+ call `clearAthenaAuthSessionOnAppHost()` for the httpOnly app-host cookie.
909
+ See `docs/auth-cookies.md`.
910
+
911
+ For SQL identifiers, keep using `identifier(...)`; `sqlText(...)`-style helpers are for literal values only.
912
+
907
913
  ### Retry helper (deprecated)
908
914
 
909
915
  Prefer `experimental.retryReads` for ordinary SDK-managed `select`, `findMany(...)`, and `query(...)` retries. `withRetry(...)` remains available for compatibility and for cases where you need explicit call-site retry control.
910
916
 
911
917
  ```ts
912
918
  import { withRetry } from "@xylex-group/athena";
913
-
914
- const result = await withRetry(
915
- {
916
- retries: 3,
917
- backoff: "exponential",
918
- baseDelayMs: 100,
919
- jitter: true,
920
- },
921
- () => athena.from("users").select("id,name"),
919
+
920
+ const result = await withRetry(
921
+ {
922
+ retries: 3,
923
+ backoff: "exponential",
924
+ baseDelayMs: 100,
925
+ jitter: true,
926
+ },
927
+ () => athena.from("users").select("id,name"),
922
928
  );
923
929
  ```
924
930
 
925
931
  By default, retries target transient/rate-limit failures; use `shouldRetry` for custom policies.
926
-
927
- ## Query builder
928
-
929
- ### DB module namespace
930
-
931
- `createClient()` keeps root methods (`from`, `rpc`, `query`) and now also exposes `db` as an additive namespace.
932
-
933
- ```ts
934
- const athena = createClient(ATHENA_URL, ATHENA_API_KEY);
935
-
936
- await athena.db.from("users").select("id,name").eq("active", true).limit(20);
937
-
938
- await athena.db.select("users", "id,name").eq("id", 1).single();
939
-
940
- await athena.db.insert("users", { id: 1, name: "Alice" }).select("id");
941
- await athena.db.update("users", { name: "Updated" }).eq("id", 1).select("id,name");
942
- await athena.db.delete("users", { resourceId: "r-1" }).select("id");
943
- ```
944
-
945
- `db` mirrors the existing query builder semantics while providing a module seam for future database-surface expansion.
946
-
947
- ### Reading rows
948
-
949
- ```ts
950
- // select all columns
951
- const { data } = await athena.from("users").select();
952
-
953
- // select specific columns
954
- const { data } = await athena.from("users").select("id, name, email");
955
-
956
- // select with type annotation
957
- const { data } = await athena.from<User>("users").select("id, name");
958
- ```
959
-
960
- ### Filters
961
-
962
- Filters accumulate on the builder and are sent together when the query executes.
963
-
964
- ```ts
965
- const { data } = await athena
966
- .from("characters")
967
- .select("id, name")
968
- .eq("active", true) // column = value
969
- .eqUuid("session_id", "550e8400-e29b-41d4-a716-446655440000") // explicit UUID cast
970
- .eqCast("session_id", "550e8400-e29b-41d4-a716-446655440000", "uuid") // explicit cast type
971
- .neq("role", "guest") // column != value
972
- .gt("level", 5) // column > value
973
- .gte("score", 100) // column >= value
974
- .lt("age", 30) // column < value
975
- .lte("created_at", "2024-01-01") // column <= value
976
- .like("name", "Ali%") // SQL LIKE (case-sensitive)
977
- .ilike("email", "%@example%") // SQL ILIKE (case-insensitive)
978
- .is("deleted_at", null) // IS NULL / IS TRUE etc.
979
- .in("status", ["active", "pending"]) // IN (…)
980
- .contains("tags", ["hero"]) // array contains value
981
- .containedBy("tags", ["hero", "villain"]) // array is subset of value
982
- .match({ role: "admin", active: true }) // multiple eq filters at once
983
- .not("role", "eq", "banned") // NOT col op val
984
- .or("status.eq.active,status.eq.pending"); // OR expression
985
- ```
986
-
987
- `eq()` now auto-detects UUID-like values on identifier columns (for example `id`, `*_id`, `*uuid*`) and uses a safe typed comparison path so UUID columns no longer require app-side manual `::uuid` / `::text` casts.
988
-
989
- Canonical style for reads is to call `.select(...)` first, then apply filters:
990
-
991
- ```ts
992
- const { data } = await athena
993
- .from("instruments")
994
- .select("name, section_id")
995
- .eq("name", "violin");
996
- ```
997
-
998
- ### Pagination
999
-
1000
- Two styles, pick whichever matches your UI / backend. Both live on the shared `FilterChain`, so they work before or after `.select()`.
1001
-
1002
- ```ts
1003
- // 1. offset / limit — contiguous windows
1004
- const { data } = await athena.from("users").select().limit(25).offset(50);
1005
-
1006
- // range shorthand: offset = from, limit = to - from + 1
1007
- const { data: firstTwentyFive } = await athena.from("users").select().range(0, 24);
1008
-
1009
- // 2. page based — maps to current_page / page_size / total_pages
1010
- const { data: page2 } = await athena
1011
- .from("orders")
1012
- .select("id, total")
1013
- .currentPage(2)
1014
- .pageSize(25);
1015
-
1016
- // .totalPages() is an optional hint some backends use in the response envelope
1017
- const { data: hinted } = await athena
1018
- .from("orders")
1019
- .select("id, total")
1020
- .currentPage(1)
1021
- .pageSize(25)
1022
- .totalPages(10);
1023
- ```
1024
-
1025
- | Method | Body field |
1026
- |--------|------------|
1027
- | `.limit(n)` | `limit` |
1028
- | `.offset(n)` | `offset` |
1029
- | `.range(from, to)` | `offset` + `limit` |
1030
- | `.currentPage(n)` | `current_page` |
1031
- | `.pageSize(n)` | `page_size` |
1032
- | `.totalPages(n)` | `total_pages` |
1033
-
1034
- ### Ordering
1035
-
1036
- `.order(column, { ascending? })` is available on the table builder, select chain, update chain, and delete — before or after the operation terminator. It serializes to `sort_by: { field, direction }` on the gateway payload and defaults to ascending.
1037
-
1038
- ```ts
1039
- // descending + limit
1040
- // SELECT * FROM rsf_messages WHERE room_id = $1 ORDER BY created_at DESC LIMIT 100
1041
- const { data } = await athena
1042
- .from("rsf_messages")
1043
- .eq("room_id", roomId)
1044
- .select("*", { stripNulls: false })
1045
- .order("created_at", { ascending: false })
1046
- .limit(100);
1047
-
1048
- // ascending (default) + page-based pagination
1049
- const { data: page } = await athena
1050
- .from("orders")
1051
- .select("id, total, created_at")
1052
- .order("created_at")
1053
- .currentPage(1)
1054
- .pageSize(25);
1055
-
1056
- // combine with .single() to grab the newest / oldest row
1057
- const { data: latest } = await athena
1058
- .from("messages")
1059
- .eq("room_id", roomId)
1060
- .select("*")
1061
- .order("created_at", { ascending: false })
1062
- .single();
1063
- ```
1064
-
1065
- Only the last `.order()` wins — the SDK does not support multi-column ordering on the table builder. Use `.rpc()` or `.query()` for that.
1066
-
1067
- ### Single row
1068
-
1069
- ```ts
1070
- // returns the first row or null instead of an array
1071
- const { data: user } = await athena
1072
- .from("users")
1073
- .select("id, name")
1074
- .eq("id", 42)
1075
- .single();
1076
- ```
1077
-
1078
- `maybeSingle` behaves identically — both return the first element of the result set.
1079
-
1080
- ### Table schema targeting
1081
-
1082
- Use `schema` either on `from(...)` itself or in table call options to qualify unqualified table names:
1083
-
1084
- ```ts
1085
- const { data } = await athena
1086
- .from("users", { schema: "public" })
1087
- .select("id,email");
1088
-
1089
- const { data: sameTarget } = await athena
1090
- .from("users")
1091
- .select("id,email", { schema: "public" });
1092
-
1093
- const { data: crossSchema } = await athena
1094
- .from("chat_subscriptions", { schema: "private" })
1095
- .findMany({
1096
- select: {
1097
- user_id: true,
1098
- user: {
1099
- schema: "athena",
1100
- select: {
1101
- id: true,
1102
- },
1103
- },
1104
- },
1105
- });
1106
- ```
1107
-
1108
- Both resolve the table target to `public.users`.
1109
-
1110
- ### RPC
1111
-
1112
- 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}`.
1113
-
1114
- ```ts
1115
- const { data, count } = await athena
1116
- .rpc("list_users", { role: "admin" }, { count: "exact", schema: "public" })
1117
- .eq("active", true)
1118
- .order("created_at", { ascending: false })
1119
- .range(0, 24)
1120
- .select(["id", "email"]);
1121
-
1122
- const { data: firstUser } = await athena
1123
- .rpc<{ id: number; email: string }>("list_users", { role: "admin" })
1124
- .single("id,email");
1125
-
1126
- const { data: readOnlyUser } = await athena
1127
- .rpc<{
1128
- id: number;
1129
- email: string;
1130
- }>(
1131
- "list_users",
1132
- { role: "admin" },
1133
- { get: true, count: "planned", head: true },
1134
- )
1135
- .eq("id", 1)
1136
- .single("id,email");
1137
- ```
1138
-
1139
- RPC chain methods: `.eq()`, `.neq()`, `.gt()`, `.gte()`, `.lt()`, `.lte()`, `.like()`, `.ilike()`, `.is()`, `.in()`, `.order()`, `.limit()`, `.offset()`, `.range()`, `.select()`, `.single()`, `.maybeSingle()`.
1140
- RPC options: `schema`, `count` (`"exact" | "planned" | "estimated"`), `head`, `get`.
1141
-
1142
- ### Options
1143
-
1144
- Pass options as the second argument to `.select()`:
1145
-
1146
- | Option | Type | Description |
1147
- | ------------ | ------------------------------------- | -------------------------------------------- |
1148
- | `schema` | `string` | qualify unqualified table names for table calls |
1149
- | `count` | `"exact" \| "planned" \| "estimated"` | request a row count alongside the data |
1150
- | `head` | `boolean` | return response headers only (no rows) |
1151
- | `stripNulls` | `boolean` | strip null values from rows (default `true`) |
1152
-
1153
- ```ts
1154
- const { data } = await athena
1155
- .from("orders")
1156
- .select("id", { count: "exact", stripNulls: false });
1157
- ```
1158
-
1159
- ## Mutations
1160
-
1161
- Insert, update, upsert, and delete all return a `MutationQuery` that you can await directly or chain further calls onto before the request fires.
1162
-
1163
- ### Insert
1164
-
1165
- ```ts
1166
- const { data: inserted } = await athena
1167
- .from("countries")
1168
- .insert({ name: "Mordor" })
1169
- .select("id, name");
1170
-
1171
- // insert multiple rows
1172
- const { data } = await athena
1173
- .from("characters")
1174
- .insert([{ name: "Frodo" }, { name: "Sam" }])
1175
- .select();
1176
-
1177
- // Type inference differs by payload shape:
1178
- // - insert(one) => AthenaResult<Row>
1179
- // - insert(many) => AthenaResult<Row[]>
1180
- ```
1181
-
1182
- ### Update
1183
-
1184
- ```ts
1185
- const { data: updated } = await athena
1186
- .from("countries")
1187
- .update({ name: "Gondor" })
1188
- .eq("id", 1)
1189
- .select();
1190
- ```
1191
-
1192
- Filters (`.eq()`, `.match()`, etc.) applied before `.update()` are used as `WHERE` conditions.
1193
-
1194
- ### Upsert
1195
-
1196
- ```ts
1197
- const { data } = await athena
1198
- .from("countries")
1199
- .upsert(
1200
- { id: 2, name: "Rohan" },
1201
- { updateBody: { name: "Rohan" }, onConflict: "id" },
1202
- )
1203
- .select();
1204
-
1205
- // Type inference differs by payload shape:
1206
- // - upsert(one) => AthenaResult<Row>
1207
- // - upsert(many) => AthenaResult<Row[]>
1208
- ```
1209
-
1210
- | Option | Type | Description |
1211
- | --------------- | ------------------------------------- | ---------------------------------------- |
1212
- | `onConflict` | `string \| string[]` | column(s) that determine a conflict |
1213
- | `updateBody` | `object` | fields to apply when a conflict occurs |
1214
- | `defaultToNull` | `boolean` | write explicit `null` for missing fields |
1215
- | `count` | `"exact" \| "planned" \| "estimated"` | request a row count |
1216
- | `head` | `boolean` | return headers only |
1217
-
1218
- ### Delete
1219
-
1220
- ```ts
1221
- // delete by id filter
1222
- await athena.from("countries").eq("id", 1).delete();
1223
-
1224
- // delete with explicit resourceId option
1225
- await athena.from("countries").delete({ resourceId: "abc-123" });
1226
-
1227
- // chain .select() to get the deleted row back
1228
- const { data: deleted } = await athena
1229
- .from("countries")
1230
- .eq("resource_id", "abc-123")
1231
- .delete()
1232
- .select("id, name");
1233
- ```
1234
-
1235
- Delete requires either `.eq("resource_id", …)`, `.eq("id", …)`, or `options.resourceId` — calling `.delete()` without any of these throws an error.
1236
-
1237
- ### MutationQuery chaining
1238
-
1239
- All mutation methods return a `MutationQuery` which supports:
1240
-
1241
- ```ts
1242
- const mutation = athena.from("users").insert({ name: "Alice" });
1243
-
1244
- await mutation.select("id, name"); // fire request, return rows
1245
- await mutation.returning("id"); // alias for .select()
1246
- await mutation.single("id"); // return first row or null
1247
- await mutation.maybeSingle("id"); // same as .single()
1248
- await mutation; // fire request, return default columns
1249
- mutation.then(({ data }) => …); // thenable
1250
- mutation.catch(err => …);
1251
- mutation.finally(() => …);
1252
- ```
1253
-
1254
- The request fires only once regardless of how many times you call `.then()` or await the object.
1255
-
1256
- ## React hooks
1257
-
1258
- ```tsx
1259
- "use client";
1260
-
1261
- import {
1262
- AthenaQueryClientProvider,
1263
- createAthenaQueryClient,
1264
- useAthenaGateway,
1265
- useMutation,
1266
- useQuery,
1267
- } from "@xylex-group/athena/react";
1268
- import { createClient } from "@xylex-group/athena";
1269
-
1270
- const queryClient = createAthenaQueryClient({
1271
- cache: { mode: "none" }, // default: no persistent data cache, inflight dedupe only
1272
- });
1273
-
1274
- const athena = createClient(
1275
- process.env.NEXT_PUBLIC_ATHENA_URL!,
1276
- process.env.NEXT_PUBLIC_ATHENA_API_KEY!,
1277
- );
1278
-
1279
- type Product = {
1280
- id: string;
1281
- name: string;
1282
- price: number;
1283
- };
1284
-
1285
- type CreateProductInput = {
1286
- name: string;
1287
- price: number;
1288
- };
1289
-
1290
- function ProductsInner() {
1291
- const products = useQuery<Product[]>({
1292
- queryKey: ["products"],
1293
- queryFn: () =>
1294
- athena.from("products").select("id,name,price").limit(50),
1295
- });
1296
-
1297
- const createProduct = useMutation<CreateProductInput, Product>({
1298
- mutationFn: (input) =>
1299
- athena.from("products").insert(input).select("id,name,price").single(),
1300
- onSuccess: () => {
1301
- void products.refetch();
1302
- },
1303
- });
1304
-
1305
- if (products.isLoading) return <div>Loading...</div>;
1306
- if (products.error) return <div>{products.error.message}</div>;
1307
-
1308
- return (
1309
- <div>
1310
- <button
1311
- onClick={() => {
1312
- createProduct.mutate({ name: "New product", price: 99 });
1313
- }}
1314
- >
1315
- Add Product
1316
- </button>
1317
- {products.data?.map((product) => (
1318
- <div key={product.id}>
1319
- {product.name} - {product.price}
1320
- </div>
1321
- ))}
1322
- </div>
1323
- );
1324
- }
1325
-
1326
- export function Products() {
1327
- return (
1328
- <AthenaQueryClientProvider client={queryClient}>
1329
- <ProductsInner />
1330
- </AthenaQueryClientProvider>
1331
- );
1332
- }
1333
- ```
1334
-
1335
- Available React APIs:
1336
-
1337
- - `useAthenaGateway`: low-level gateway hook (`fetchGateway`, `insertGateway`, `updateGateway`, `deleteGateway`, `rpcGateway`) with request/response logging.
1338
- - `createAthenaQueryClient` + `AthenaQueryClientProvider`: Athena query runtime boundary for scoped state and subscriptions.
1339
- - `useQuery`: lightweight read lifecycle hook (`status`, `isFetching`, `refetch`, `reset`) with normalized Athena error/result handling.
1340
- - `useMutation`: lightweight write lifecycle hook (`mutate`, `mutateAsync`, `reset`) with manual refetch/invalidation flow.
1341
-
1342
- By design this is not a cache-heavy React Query clone:
1343
-
1344
- - No TanStack/React Query dependency.
1345
- - No persistent data cache by default (`cache.mode = "none"`).
1346
- - Inflight dedupe for identical query keys is enabled.
1347
- - Manual `refetch()` after mutations is the default invalidation strategy.
1348
-
1349
- `test-sdk` includes runnable local examples for these hooks in
1350
- `test-sdk/examples/react-hooks`, where `queryFn`/`mutationFn` call Athena directly via `createClient(...).from(...).select()/insert()/eq()`.
1351
-
1352
- `useAthenaGateway` example:
1353
-
1354
- ```tsx
1355
- import { useAthenaGateway } from "@xylex-group/athena/react";
1356
- import { useEffect } from "react";
1357
-
1358
- export function UsersPanel() {
1359
- const { fetchGateway, lastResponse, isLoading, error } = useAthenaGateway({
1360
- baseUrl: "https://athena-db.com",
1361
- apiKey: process.env.NEXT_PUBLIC_ATHENA_API_KEY,
1362
- });
1363
-
1364
- useEffect(() => {
1365
- void fetchGateway({
1366
- table_name: "users",
1367
- columns: ["id", "email"],
1368
- limit: 25,
1369
- });
1370
- }, [fetchGateway]);
1371
-
1372
- if (error) return <div>Error: {error}</div>;
1373
- if (isLoading) return <div>Loading…</div>;
1374
-
1375
- return <pre>{JSON.stringify(lastResponse?.data, null, 2)}</pre>;
1376
- }
1377
- ```
1378
-
1379
- `useAthenaGateway` config options mirror the client options: `baseUrl`, `apiKey`, `headers`, `userId`, `organizationId`, `publishEvent`.
1380
-
1381
- ## User context headers
1382
-
1383
- Pass user and tenant context to every request without repeating it on each call:
1384
-
1385
- ```ts
1386
- const athena = createClient(
1387
- "https://athena-db.com",
1388
- process.env.ATHENA_API_KEY,
1389
- {
1390
- headers: {
1391
- "X-User-Id": currentUser.id,
1392
- "X-Organization-Id": currentUser.organizationId ?? "",
1393
- },
1394
- },
1395
- );
1396
- ```
1397
-
1398
- Or pass per-call via options. The Athena server interprets `url` and `key` based on the configured backend type.
1399
-
1400
- ## Custom headers
1401
-
1402
- ```ts
1403
- const athena = createClient(
1404
- "https://athena-db.com",
1405
- process.env.ATHENA_API_KEY,
1406
- {
1407
- headers: {
1408
- "X-Custom-Header": "value",
1409
- },
1410
- },
1411
- );
1412
- ```
1413
-
1414
- Per-call headers are merged with the client-level headers, with per-call values winning on conflict.
1415
-
1416
- The SDK also sends a standard identification header on every request:
1417
-
1418
- - `X-Athena-Sdk: xylex-group/athena <version>`
1419
-
1420
- ## TypeScript
1421
-
1422
- The package is written in TypeScript and ships declaration files. Pass a row type to `.from()` for fully-typed builder methods and results:
1423
-
1424
- ```ts
1425
- interface User {
1426
- id: number;
1427
- name: string;
1428
- email: string;
1429
- active: boolean;
1430
- }
1431
-
1432
- const { data } = await athena
1433
- .from<User>("users")
1434
- .select("id, name")
1435
- .eq("active", true);
1436
- // data is User[] | null
1437
- ```
1438
-
1439
- ## Development Validation
1440
-
1441
- For local quality checks:
1442
-
1443
- ```bash
1444
- pnpm typecheck # compile-time type compatibility checks
1445
- pnpm check:all # lint + typecheck + test + build
1446
- ```
1447
-
1448
- CI and publish workflows run `typecheck` before build/publish.
1449
-
1450
- ## Learn more
1451
-
1452
- - [Documentation index](docs/index.md) — complete documentation map
1453
- - [Getting started](docs/getting-started.md) — step-by-step walkthrough
1454
- - [CLI command reference](docs/cli-command-reference.md) — all `athena-js` commands, help flows, and troubleshooting
1455
- - [Typed schema registry](docs/typed-schema-registry.md) — typed contracts and migration path
1456
- - [Generator config](docs/generator-config.md) — generator provider and output pipeline
1457
- - [Generator CI/CD](docs/generator-cicd.md) — pipeline patterns, secret mapping, retries, and branch policy
1458
- - [API reference](docs/api-reference.md) — complete method and type reference
932
+
933
+ ## Query builder
934
+
935
+ ### DB module namespace
936
+
937
+ `createClient()` keeps root methods (`from`, `rpc`, `query`) and now also exposes `db` as an additive namespace.
938
+
939
+ ```ts
940
+ const athena = createClient(ATHENA_URL, ATHENA_API_KEY);
941
+
942
+ await athena.db.from("users").select("id,name").eq("active", true).limit(20);
943
+
944
+ await athena.db.select("users", "id,name").eq("id", 1).single();
945
+
946
+ await athena.db.insert("users", { id: 1, name: "Alice" }).select("id");
947
+ await athena.db.update("users", { name: "Updated" }).eq("id", 1).select("id,name");
948
+ await athena.db.delete("users", { resourceId: "r-1" }).select("id");
949
+ ```
950
+
951
+ `db` mirrors the existing query builder semantics while providing a module seam for future database-surface expansion.
952
+
953
+ ### Reading rows
954
+
955
+ ```ts
956
+ // select all columns
957
+ const { data } = await athena.from("users").select();
958
+
959
+ // select specific columns
960
+ const { data } = await athena.from("users").select("id, name, email");
961
+
962
+ // select with type annotation
963
+ const { data } = await athena.from<User>("users").select("id, name");
964
+ ```
965
+
966
+ ### Filters
967
+
968
+ Filters accumulate on the builder and are sent together when the query executes.
969
+
970
+ ```ts
971
+ const { data } = await athena
972
+ .from("characters")
973
+ .select("id, name")
974
+ .eq("active", true) // column = value
975
+ .eqUuid("session_id", "550e8400-e29b-41d4-a716-446655440000") // explicit UUID cast
976
+ .eqCast("session_id", "550e8400-e29b-41d4-a716-446655440000", "uuid") // explicit cast type
977
+ .neq("role", "guest") // column != value
978
+ .gt("level", 5) // column > value
979
+ .gte("score", 100) // column >= value
980
+ .lt("age", 30) // column < value
981
+ .lte("created_at", "2024-01-01") // column <= value
982
+ .like("name", "Ali%") // SQL LIKE (case-sensitive)
983
+ .ilike("email", "%@example%") // SQL ILIKE (case-insensitive)
984
+ .is("deleted_at", null) // IS NULL / IS TRUE etc.
985
+ .in("status", ["active", "pending"]) // IN (…)
986
+ .contains("tags", ["hero"]) // array contains value
987
+ .containedBy("tags", ["hero", "villain"]) // array is subset of value
988
+ .match({ role: "admin", active: true }) // multiple eq filters at once
989
+ .not("role", "eq", "banned") // NOT col op val
990
+ .or("status.eq.active,status.eq.pending"); // OR expression
991
+ ```
992
+
993
+ `eq()` now auto-detects UUID-like values on identifier columns (for example `id`, `*_id`, `*uuid*`) and uses a safe typed comparison path so UUID columns no longer require app-side manual `::uuid` / `::text` casts.
994
+
995
+ Canonical style for reads is to call `.select(...)` first, then apply filters:
996
+
997
+ ```ts
998
+ const { data } = await athena
999
+ .from("instruments")
1000
+ .select("name, section_id")
1001
+ .eq("name", "violin");
1002
+ ```
1003
+
1004
+ ### Pagination
1005
+
1006
+ Two styles, pick whichever matches your UI / backend. Both live on the shared `FilterChain`, so they work before or after `.select()`.
1007
+
1008
+ ```ts
1009
+ // 1. offset / limit — contiguous windows
1010
+ const { data } = await athena.from("users").select().limit(25).offset(50);
1011
+
1012
+ // range shorthand: offset = from, limit = to - from + 1
1013
+ const { data: firstTwentyFive } = await athena.from("users").select().range(0, 24);
1014
+
1015
+ // 2. page based — maps to current_page / page_size / total_pages
1016
+ const { data: page2 } = await athena
1017
+ .from("orders")
1018
+ .select("id, total")
1019
+ .currentPage(2)
1020
+ .pageSize(25);
1021
+
1022
+ // .totalPages() is an optional hint some backends use in the response envelope
1023
+ const { data: hinted } = await athena
1024
+ .from("orders")
1025
+ .select("id, total")
1026
+ .currentPage(1)
1027
+ .pageSize(25)
1028
+ .totalPages(10);
1029
+ ```
1030
+
1031
+ | Method | Body field |
1032
+ |--------|------------|
1033
+ | `.limit(n)` | `limit` |
1034
+ | `.offset(n)` | `offset` |
1035
+ | `.range(from, to)` | `offset` + `limit` |
1036
+ | `.currentPage(n)` | `current_page` |
1037
+ | `.pageSize(n)` | `page_size` |
1038
+ | `.totalPages(n)` | `total_pages` |
1039
+
1040
+ ### Ordering
1041
+
1042
+ `.order(column, { ascending? })` is available on the table builder, select chain, update chain, and delete — before or after the operation terminator. It serializes to `sort_by: { field, direction }` on the gateway payload and defaults to ascending.
1043
+
1044
+ ```ts
1045
+ // descending + limit
1046
+ // SELECT * FROM rsf_messages WHERE room_id = $1 ORDER BY created_at DESC LIMIT 100
1047
+ const { data } = await athena
1048
+ .from("rsf_messages")
1049
+ .eq("room_id", roomId)
1050
+ .select("*", { stripNulls: false })
1051
+ .order("created_at", { ascending: false })
1052
+ .limit(100);
1053
+
1054
+ // ascending (default) + page-based pagination
1055
+ const { data: page } = await athena
1056
+ .from("orders")
1057
+ .select("id, total, created_at")
1058
+ .order("created_at")
1059
+ .currentPage(1)
1060
+ .pageSize(25);
1061
+
1062
+ // combine with .single() to grab the newest / oldest row
1063
+ const { data: latest } = await athena
1064
+ .from("messages")
1065
+ .eq("room_id", roomId)
1066
+ .select("*")
1067
+ .order("created_at", { ascending: false })
1068
+ .single();
1069
+ ```
1070
+
1071
+ Only the last `.order()` wins — the SDK does not support multi-column ordering on the table builder. Use `.rpc()` or `.query()` for that.
1072
+
1073
+ ### Single row
1074
+
1075
+ ```ts
1076
+ // returns the first row or null instead of an array
1077
+ const { data: user } = await athena
1078
+ .from("users")
1079
+ .select("id, name")
1080
+ .eq("id", 42)
1081
+ .single();
1082
+ ```
1083
+
1084
+ `maybeSingle` behaves identically — both return the first element of the result set.
1085
+
1086
+ ### Table schema targeting
1087
+
1088
+ Use `schema` either on `from(...)` itself or in table call options to qualify unqualified table names:
1089
+
1090
+ ```ts
1091
+ const { data } = await athena
1092
+ .from("users", { schema: "public" })
1093
+ .select("id,email");
1094
+
1095
+ const { data: sameTarget } = await athena
1096
+ .from("users")
1097
+ .select("id,email", { schema: "public" });
1098
+
1099
+ const { data: crossSchema } = await athena
1100
+ .from("chat_subscriptions", { schema: "private" })
1101
+ .findMany({
1102
+ select: {
1103
+ user_id: true,
1104
+ user: {
1105
+ schema: "athena",
1106
+ select: {
1107
+ id: true,
1108
+ },
1109
+ },
1110
+ },
1111
+ });
1112
+ ```
1113
+
1114
+ Both resolve the table target to `public.users`.
1115
+
1116
+ ### RPC
1117
+
1118
+ 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}`.
1119
+
1120
+ ```ts
1121
+ const { data, count } = await athena
1122
+ .rpc("list_users", { role: "admin" }, { count: "exact", schema: "public" })
1123
+ .eq("active", true)
1124
+ .order("created_at", { ascending: false })
1125
+ .range(0, 24)
1126
+ .select(["id", "email"]);
1127
+
1128
+ const { data: firstUser } = await athena
1129
+ .rpc<{ id: number; email: string }>("list_users", { role: "admin" })
1130
+ .single("id,email");
1131
+
1132
+ const { data: readOnlyUser } = await athena
1133
+ .rpc<{
1134
+ id: number;
1135
+ email: string;
1136
+ }>(
1137
+ "list_users",
1138
+ { role: "admin" },
1139
+ { get: true, count: "planned", head: true },
1140
+ )
1141
+ .eq("id", 1)
1142
+ .single("id,email");
1143
+ ```
1144
+
1145
+ RPC chain methods: `.eq()`, `.neq()`, `.gt()`, `.gte()`, `.lt()`, `.lte()`, `.like()`, `.ilike()`, `.is()`, `.in()`, `.order()`, `.limit()`, `.offset()`, `.range()`, `.select()`, `.single()`, `.maybeSingle()`.
1146
+ RPC options: `schema`, `count` (`"exact" | "planned" | "estimated"`), `head`, `get`.
1147
+
1148
+ ### Options
1149
+
1150
+ Pass options as the second argument to `.select()`:
1151
+
1152
+ | Option | Type | Description |
1153
+ | ------------ | ------------------------------------- | -------------------------------------------- |
1154
+ | `schema` | `string` | qualify unqualified table names for table calls |
1155
+ | `count` | `"exact" \| "planned" \| "estimated"` | request a row count alongside the data |
1156
+ | `head` | `boolean` | return response headers only (no rows) |
1157
+ | `stripNulls` | `boolean` | strip null values from rows (default `true`) |
1158
+
1159
+ ```ts
1160
+ const { data } = await athena
1161
+ .from("orders")
1162
+ .select("id", { count: "exact", stripNulls: false });
1163
+ ```
1164
+
1165
+ ## Mutations
1166
+
1167
+ Insert, update, upsert, and delete all return a `MutationQuery` that you can await directly or chain further calls onto before the request fires.
1168
+
1169
+ ### Insert
1170
+
1171
+ ```ts
1172
+ const { data: inserted } = await athena
1173
+ .from("countries")
1174
+ .insert({ name: "Mordor" })
1175
+ .select("id, name");
1176
+
1177
+ // insert multiple rows
1178
+ const { data } = await athena
1179
+ .from("characters")
1180
+ .insert([{ name: "Frodo" }, { name: "Sam" }])
1181
+ .select();
1182
+
1183
+ // Type inference differs by payload shape:
1184
+ // - insert(one) => AthenaResult<Row>
1185
+ // - insert(many) => AthenaResult<Row[]>
1186
+ ```
1187
+
1188
+ ### Update
1189
+
1190
+ ```ts
1191
+ const { data: updated } = await athena
1192
+ .from("countries")
1193
+ .update({ name: "Gondor" })
1194
+ .eq("id", 1)
1195
+ .select();
1196
+ ```
1197
+
1198
+ Filters (`.eq()`, `.match()`, etc.) applied before `.update()` are used as `WHERE` conditions.
1199
+
1200
+ ### Upsert
1201
+
1202
+ ```ts
1203
+ const { data } = await athena
1204
+ .from("countries")
1205
+ .upsert(
1206
+ { id: 2, name: "Rohan" },
1207
+ { updateBody: { name: "Rohan" }, onConflict: "id" },
1208
+ )
1209
+ .select();
1210
+
1211
+ // Type inference differs by payload shape:
1212
+ // - upsert(one) => AthenaResult<Row>
1213
+ // - upsert(many) => AthenaResult<Row[]>
1214
+ ```
1215
+
1216
+ | Option | Type | Description |
1217
+ | --------------- | ------------------------------------- | ---------------------------------------- |
1218
+ | `onConflict` | `string \| string[]` | column(s) that determine a conflict |
1219
+ | `updateBody` | `object` | fields to apply when a conflict occurs |
1220
+ | `defaultToNull` | `boolean` | write explicit `null` for missing fields |
1221
+ | `count` | `"exact" \| "planned" \| "estimated"` | request a row count |
1222
+ | `head` | `boolean` | return headers only |
1223
+
1224
+ ### Delete
1225
+
1226
+ ```ts
1227
+ // delete by id filter
1228
+ await athena.from("countries").eq("id", 1).delete();
1229
+
1230
+ // delete with explicit resourceId option
1231
+ await athena.from("countries").delete({ resourceId: "abc-123" });
1232
+
1233
+ // chain .select() to get the deleted row back
1234
+ const { data: deleted } = await athena
1235
+ .from("countries")
1236
+ .eq("resource_id", "abc-123")
1237
+ .delete()
1238
+ .select("id, name");
1239
+ ```
1240
+
1241
+ Delete requires either `.eq("resource_id", …)`, `.eq("id", …)`, or `options.resourceId` — calling `.delete()` without any of these throws an error.
1242
+
1243
+ ### MutationQuery chaining
1244
+
1245
+ All mutation methods return a `MutationQuery` which supports:
1246
+
1247
+ ```ts
1248
+ const mutation = athena.from("users").insert({ name: "Alice" });
1249
+
1250
+ await mutation.select("id, name"); // fire request, return rows
1251
+ await mutation.returning("id"); // alias for .select()
1252
+ await mutation.single("id"); // return first row or null
1253
+ await mutation.maybeSingle("id"); // same as .single()
1254
+ await mutation; // fire request, return default columns
1255
+ mutation.then(({ data }) => …); // thenable
1256
+ mutation.catch(err => …);
1257
+ mutation.finally(() => …);
1258
+ ```
1259
+
1260
+ The request fires only once regardless of how many times you call `.then()` or await the object.
1261
+
1262
+ ## React hooks
1263
+
1264
+ ```tsx
1265
+ "use client";
1266
+
1267
+ import {
1268
+ AthenaQueryClientProvider,
1269
+ createAthenaQueryClient,
1270
+ useAthenaGateway,
1271
+ useMutation,
1272
+ useQuery,
1273
+ } from "@xylex-group/athena/react";
1274
+ import { createClient } from "@xylex-group/athena";
1275
+
1276
+ const queryClient = createAthenaQueryClient({
1277
+ cache: { mode: "none" }, // default: no persistent data cache, inflight dedupe only
1278
+ });
1279
+
1280
+ const athena = createClient(
1281
+ process.env.NEXT_PUBLIC_ATHENA_URL!,
1282
+ process.env.NEXT_PUBLIC_ATHENA_API_KEY!,
1283
+ );
1284
+
1285
+ type Product = {
1286
+ id: string;
1287
+ name: string;
1288
+ price: number;
1289
+ };
1290
+
1291
+ type CreateProductInput = {
1292
+ name: string;
1293
+ price: number;
1294
+ };
1295
+
1296
+ function ProductsInner() {
1297
+ const products = useQuery<Product[]>({
1298
+ queryKey: ["products"],
1299
+ queryFn: () =>
1300
+ athena.from("products").select("id,name,price").limit(50),
1301
+ });
1302
+
1303
+ const createProduct = useMutation<CreateProductInput, Product>({
1304
+ mutationFn: (input) =>
1305
+ athena.from("products").insert(input).select("id,name,price").single(),
1306
+ onSuccess: () => {
1307
+ void products.refetch();
1308
+ },
1309
+ });
1310
+
1311
+ if (products.isLoading) return <div>Loading...</div>;
1312
+ if (products.error) return <div>{products.error.message}</div>;
1313
+
1314
+ return (
1315
+ <div>
1316
+ <button
1317
+ onClick={() => {
1318
+ createProduct.mutate({ name: "New product", price: 99 });
1319
+ }}
1320
+ >
1321
+ Add Product
1322
+ </button>
1323
+ {products.data?.map((product) => (
1324
+ <div key={product.id}>
1325
+ {product.name} - {product.price}
1326
+ </div>
1327
+ ))}
1328
+ </div>
1329
+ );
1330
+ }
1331
+
1332
+ export function Products() {
1333
+ return (
1334
+ <AthenaQueryClientProvider client={queryClient}>
1335
+ <ProductsInner />
1336
+ </AthenaQueryClientProvider>
1337
+ );
1338
+ }
1339
+ ```
1340
+
1341
+ Available React APIs:
1342
+
1343
+ - `useAthenaGateway`: low-level gateway hook (`fetchGateway`, `insertGateway`, `updateGateway`, `deleteGateway`, `rpcGateway`) with request/response logging.
1344
+ - `createAthenaQueryClient` + `AthenaQueryClientProvider`: Athena query runtime boundary for scoped state and subscriptions.
1345
+ - `useQuery`: lightweight read lifecycle hook (`status`, `isFetching`, `refetch`, `reset`) with normalized Athena error/result handling.
1346
+ - `useMutation`: lightweight write lifecycle hook (`mutate`, `mutateAsync`, `reset`) with manual refetch/invalidation flow.
1347
+
1348
+ By design this is not a cache-heavy React Query clone:
1349
+
1350
+ - No TanStack/React Query dependency.
1351
+ - No persistent data cache by default (`cache.mode = "none"`).
1352
+ - Inflight dedupe for identical query keys is enabled.
1353
+ - Manual `refetch()` after mutations is the default invalidation strategy.
1354
+
1355
+ `test-sdk` includes runnable local examples for these hooks in
1356
+ `test-sdk/examples/react-hooks`, where `queryFn`/`mutationFn` call Athena directly via `createClient(...).from(...).select()/insert()/eq()`.
1357
+
1358
+ `useAthenaGateway` example:
1359
+
1360
+ ```tsx
1361
+ import { useAthenaGateway } from "@xylex-group/athena/react";
1362
+ import { useEffect } from "react";
1363
+
1364
+ export function UsersPanel() {
1365
+ const { fetchGateway, lastResponse, isLoading, error } = useAthenaGateway({
1366
+ baseUrl: "https://athena-db.com",
1367
+ apiKey: process.env.NEXT_PUBLIC_ATHENA_API_KEY,
1368
+ });
1369
+
1370
+ useEffect(() => {
1371
+ void fetchGateway({
1372
+ table_name: "users",
1373
+ columns: ["id", "email"],
1374
+ limit: 25,
1375
+ });
1376
+ }, [fetchGateway]);
1377
+
1378
+ if (error) return <div>Error: {error}</div>;
1379
+ if (isLoading) return <div>Loading…</div>;
1380
+
1381
+ return <pre>{JSON.stringify(lastResponse?.data, null, 2)}</pre>;
1382
+ }
1383
+ ```
1384
+
1385
+ `useAthenaGateway` config options mirror the client options: `baseUrl`, `apiKey`, `headers`, `userId`, `organizationId`, `publishEvent`.
1386
+
1387
+ ## User context headers
1388
+
1389
+ Pass user and tenant context to every request without repeating it on each call:
1390
+
1391
+ ```ts
1392
+ const athena = createClient(
1393
+ "https://athena-db.com",
1394
+ process.env.ATHENA_API_KEY,
1395
+ {
1396
+ headers: {
1397
+ "X-User-Id": currentUser.id,
1398
+ "X-Organization-Id": currentUser.organizationId ?? "",
1399
+ },
1400
+ },
1401
+ );
1402
+ ```
1403
+
1404
+ Or pass per-call via options. The Athena server interprets `url` and `key` based on the configured backend type.
1405
+
1406
+ ## Custom headers
1407
+
1408
+ ```ts
1409
+ const athena = createClient(
1410
+ "https://athena-db.com",
1411
+ process.env.ATHENA_API_KEY,
1412
+ {
1413
+ headers: {
1414
+ "X-Custom-Header": "value",
1415
+ },
1416
+ },
1417
+ );
1418
+ ```
1419
+
1420
+ Per-call headers are merged with the client-level headers, with per-call values winning on conflict.
1421
+
1422
+ The SDK also sends a standard identification header on every request:
1423
+
1424
+ - `X-Athena-Sdk: xylex-group/athena <version>`
1425
+
1426
+ ## TypeScript
1427
+
1428
+ The package is written in TypeScript and ships declaration files. Pass a row type to `.from()` for fully-typed builder methods and results:
1429
+
1430
+ ```ts
1431
+ interface User {
1432
+ id: number;
1433
+ name: string;
1434
+ email: string;
1435
+ active: boolean;
1436
+ }
1437
+
1438
+ const { data } = await athena
1439
+ .from<User>("users")
1440
+ .select("id, name")
1441
+ .eq("active", true);
1442
+ // data is User[] | null
1443
+ ```
1444
+
1445
+ ## Development Validation
1446
+
1447
+ For local quality checks:
1448
+
1449
+ ```bash
1450
+ pnpm typecheck # compile-time type compatibility checks
1451
+ pnpm check:all # lint + typecheck + test + build
1452
+ ```
1453
+
1454
+ CI and publish workflows run `typecheck` before build/publish.
1455
+
1456
+ ## Learn more
1457
+
1458
+ - [Documentation index](docs/index.md) — complete documentation map
1459
+ - [Getting started](docs/getting-started.md) — step-by-step walkthrough
1460
+ - [CLI command reference](docs/cli-command-reference.md) — all `athena-js` commands, help flows, and troubleshooting
1461
+ - [Typed schema registry](docs/typed-schema-registry.md) — typed contracts and migration path
1462
+ - [Generator config](docs/generator-config.md) — generator provider and output pipeline
1463
+ - [Generator CI/CD](docs/generator-cicd.md) — pipeline patterns, secret mapping, retries, and branch policy
1464
+ - [API reference](docs/api-reference.md) — complete method and type reference