@xylex-group/athena 2.14.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -1402
- package/dist/admin.d.cts +1 -1
- package/dist/admin.d.ts +1 -1
- package/dist/{athena-auth-url-oLux_57a.d.cts → athena-request-headers-C7Jy-c_D.d.ts} +85 -1
- package/dist/{athena-auth-url-oLux_57a.d.ts → athena-request-headers-DujFFyMU.d.cts} +85 -1
- package/dist/billing.cjs +1311 -0
- package/dist/billing.cjs.map +1 -0
- package/dist/billing.d.cts +270 -0
- package/dist/billing.d.ts +270 -0
- package/dist/billing.js +1306 -0
- package/dist/billing.js.map +1 -0
- package/dist/browser.cjs +9695 -9243
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +11 -10
- package/dist/browser.d.ts +11 -10
- package/dist/browser.js +9692 -9241
- package/dist/browser.js.map +1 -1
- package/dist/cli/index.cjs +8137 -7286
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +4 -4
- package/dist/cli/index.d.ts +4 -4
- package/dist/cli/index.js +8137 -7286
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +9718 -9260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.js +9711 -9258
- package/dist/index.js.map +1 -1
- package/dist/{model-form-DcH3R7WD.d.ts → model-form-Dh0sisdL.d.ts} +2 -2
- package/dist/{model-form-BE1_Bmdd.d.cts → model-form-oOAJUVd4.d.cts} +2 -2
- package/dist/{module-DFGZXCtt.d.cts → module-CB25egcO.d.cts} +18 -52
- package/dist/module-C_ab-MM1.d.cts +360 -0
- package/dist/module-C_ab-MM1.d.ts +360 -0
- package/dist/{module-Ca3_OQqR.d.ts → module-CkUM6v58.d.ts} +18 -52
- package/dist/next/client.cjs +9116 -8468
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.d.cts +24 -25
- package/dist/next/client.d.ts +24 -25
- package/dist/next/client.js +9088 -8469
- package/dist/next/client.js.map +1 -1
- package/dist/next/server.cjs +8932 -7776
- package/dist/next/server.cjs.map +1 -1
- package/dist/next/server.d.cts +62 -43
- package/dist/next/server.d.ts +62 -43
- package/dist/next/server.js +8873 -7777
- package/dist/next/server.js.map +1 -1
- package/dist/{payload-BbSCmILD.d.ts → payload-DWI1rw-e.d.cts} +1 -35
- package/dist/{payload-zXIwKTQf.d.cts → payload-DWI1rw-e.d.ts} +1 -35
- package/dist/{pipeline-DqwehoQV.d.ts → pipeline-B8aN2EHe.d.ts} +1 -1
- package/dist/{pipeline-CwQoD5G9.d.cts → pipeline-D4W-Cc-A.d.cts} +1 -1
- package/dist/proxy-request-headers-DGxdoyA0.d.cts +59 -0
- package/dist/proxy-request-headers-m4D2Tduy.d.ts +59 -0
- package/dist/react.cjs +118 -17
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +46 -16
- package/dist/react.d.ts +46 -16
- package/dist/react.js +117 -18
- package/dist/react.js.map +1 -1
- package/dist/social-providers.cjs +24 -20
- package/dist/social-providers.cjs.map +1 -1
- package/dist/social-providers.d.cts +96 -139
- package/dist/social-providers.d.ts +96 -139
- package/dist/social-providers.js +24 -20
- package/dist/social-providers.js.map +1 -1
- package/dist/{types-eAKAh71s.d.cts → types-AOY-GLu8.d.cts} +27 -54
- package/dist/{types-eAKAh71s.d.ts → types-AOY-GLu8.d.ts} +27 -54
- package/dist/{types-CD6K05JQ.d.ts → types-B75PdABb.d.ts} +3 -3
- package/dist/{types-dHnjluBC.d.cts → types-BBm-kEBL.d.cts} +2 -2
- package/dist/{types-C1swabT5.d.ts → types-BaAMXCqK.d.ts} +2 -2
- package/dist/{types-YQUR7wu0.d.cts → types-Bltx6iL6.d.cts} +3 -5
- package/dist/{types-YQUR7wu0.d.ts → types-Bltx6iL6.d.ts} +3 -5
- package/dist/{types-6H575ITc.d.cts → types-CgCv5o6R.d.cts} +3 -3
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +4 -145
- package/dist/utils.d.ts +4 -145
- package/dist/utils.js +7 -8
- package/dist/utils.js.map +1 -1
- package/dist/{client-wU2J9yqb.d.cts → v3-client-CWfZHcKL.d.cts} +402 -521
- package/dist/{client-uthENhaf.d.ts → v3-client-DZp2NEhK.d.ts} +402 -521
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -1,1464 +1,171 @@
|
|
|
1
|
-
# athena
|
|
1
|
+
# @xylex-group/athena
|
|
2
2
|
|
|
3
|
-
current version: `
|
|
4
|
-
|
|
3
|
+
current version: `3.0.2`
|
|
4
|
+
Athena JS 3 is the runtime-neutral TypeScript SDK for Athena database, authentication, chat, and storage services.
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
npm install @xylex-group/athena
|
|
10
|
-
# or
|
|
11
9
|
pnpm add @xylex-group/athena
|
|
12
|
-
# or
|
|
13
|
-
yarn add @xylex-group/athena
|
|
14
10
|
```
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install react # React >=17 required for the hook
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Quick start
|
|
12
|
+
## One constructor
|
|
23
13
|
|
|
24
14
|
```ts
|
|
25
|
-
import { createClient } from
|
|
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
|
-
});
|
|
15
|
+
import { createClient } from '@xylex-group/athena'
|
|
46
16
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
17
|
+
export const athena = createClient({
|
|
18
|
+
url: 'https://athena.example.com',
|
|
19
|
+
key: process.env.ATHENA_API_KEY,
|
|
20
|
+
client: 'formations-web',
|
|
21
|
+
})
|
|
52
22
|
```
|
|
53
23
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- DB: `${url}/db`
|
|
57
|
-
- Auth: `${url}/auth`
|
|
58
|
-
- Chat: `${url}/chat`
|
|
59
|
-
- Chat realtime: `${url}` converted to `ws:` / `wss:` plus `/chat/ws`
|
|
60
|
-
- Storage: `${url}/storage`
|
|
61
|
-
|
|
62
|
-
You can still override individual services when needed:
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
const athena = createClient({
|
|
66
|
-
key: ATHENA_API_KEY,
|
|
67
|
-
db: { url: process.env.ATHENA_DB_URL },
|
|
68
|
-
auth: { url: process.env.ATHENA_AUTH_URL },
|
|
69
|
-
chat: {
|
|
70
|
-
url: process.env.ATHENA_CHAT_URL,
|
|
71
|
-
wsUrl: process.env.ATHENA_CHAT_WS_URL,
|
|
72
|
-
},
|
|
73
|
-
storage: { url: process.env.ATHENA_STORAGE_URL },
|
|
74
|
-
});
|
|
75
|
-
```
|
|
24
|
+
The same root import works in browser and server bundles. The package export map selects the browser-safe implementation automatically.
|
|
76
25
|
|
|
77
|
-
|
|
26
|
+
Every client has stable namespaces:
|
|
78
27
|
|
|
79
28
|
```ts
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
chatWsUrl: process.env.ATHENA_CHAT_WS_URL,
|
|
86
|
-
storageUrl: process.env.ATHENA_STORAGE_URL,
|
|
87
|
-
});
|
|
29
|
+
athena.db
|
|
30
|
+
athena.auth
|
|
31
|
+
athena.chat
|
|
32
|
+
athena.storage
|
|
33
|
+
athena.billing
|
|
88
34
|
```
|
|
89
35
|
|
|
90
|
-
|
|
36
|
+
The root retains common database shortcuts:
|
|
91
37
|
|
|
92
38
|
```ts
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
experimental: {
|
|
99
|
-
retryReads: true,
|
|
100
|
-
},
|
|
101
|
-
});
|
|
39
|
+
const users = await athena
|
|
40
|
+
.from('users')
|
|
41
|
+
.eq('active', true)
|
|
42
|
+
.order('created_at', { ascending: false })
|
|
43
|
+
.select('id,email')
|
|
102
44
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
forceNoCache: true,
|
|
106
|
-
headers: { "X-Workspace-Id": "ws_123" },
|
|
107
|
-
});
|
|
45
|
+
const result = await athena.rpc('reserve_case_number', { organization_id: 'org_1' })
|
|
46
|
+
const raw = await athena.query('select now()')
|
|
108
47
|
```
|
|
109
48
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- it derives `userId`, `organizationId`, `bearerToken`, `sessionToken`, and request cookies for you
|
|
113
|
-
- it still lets you add `headers`, `forceNoCache`, or auth overrides for impersonation
|
|
114
|
-
- it keeps the base client immutable
|
|
115
|
-
|
|
116
|
-
Use `withContext(...)` when you want the same request-scoped behavior but already have raw values instead of a session object:
|
|
117
|
-
|
|
118
|
-
- it binds `userId`, `organizationId`, auth tokens/cookies, extra headers, and `forceNoCache`
|
|
119
|
-
- it can force `Cache-Control: no-cache` onto SDK-managed gateway, auth, and storage requests
|
|
120
|
-
- it keeps the base client immutable
|
|
121
|
-
|
|
122
|
-
Use `withOptions(...)` only when you intentionally need to re-target the client itself, such as overriding `url`, `key`, `client`, or service URLs.
|
|
123
|
-
|
|
124
|
-
If you already pass `client: "web-dashboard"`, the SDK sends `X-Athena-Client` for you. You do not need to duplicate that header manually.
|
|
125
|
-
|
|
126
|
-
### Low-level request hatch
|
|
127
|
-
|
|
128
|
-
If you need to hit a route the fluent SDK has not wrapped yet, use `client.request(...)`:
|
|
49
|
+
## Structured service configuration
|
|
129
50
|
|
|
130
51
|
```ts
|
|
131
|
-
const athena = createClient(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const response = await athena.request<{
|
|
139
|
-
ok: boolean;
|
|
140
|
-
}>({
|
|
141
|
-
service: "chat",
|
|
142
|
-
method: "POST",
|
|
143
|
-
path: "/rooms",
|
|
144
|
-
body: {
|
|
145
|
-
slug: "support",
|
|
146
|
-
name: "Support",
|
|
52
|
+
const athena = createClient({
|
|
53
|
+
key: process.env.ATHENA_API_KEY,
|
|
54
|
+
db: {
|
|
55
|
+
url: process.env.ATHENA_DB_URL,
|
|
56
|
+
pgUri: process.env.DATABASE_URL,
|
|
147
57
|
},
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
if (!response.ok) {
|
|
151
|
-
throw new Error(`chat route failed: ${response.status}`);
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Rules:
|
|
156
|
-
|
|
157
|
-
- use `service: "db" | "auth" | "chat" | "storage"` plus `path` for configured SDK services
|
|
158
|
-
- use `url` for absolute one-off targets
|
|
159
|
-
- `query`, `headers`, `body`, `signal`, `credentials`, and `responseType` are supported
|
|
160
|
-
- auth/session, API key, org/user context, and client headers are mirrored onto configured service calls automatically
|
|
161
|
-
|
|
162
|
-
### Next.js and React shortcuts
|
|
163
|
-
|
|
164
|
-
If you are in a Next.js app, use the higher-level adapters instead of rebuilding request context by hand:
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
import { createAthenaBrowserClient } from "@xylex-group/athena/next/client";
|
|
168
|
-
import { createAthenaServerClient, resolveAthenaServerContext } from "@xylex-group/athena/next/server";
|
|
169
|
-
import { useAthenaSessionClient } from "@xylex-group/athena/react";
|
|
170
|
-
|
|
171
|
-
const athena = createAthenaBrowserClient();
|
|
172
|
-
const requestAthena = await createAthenaServerClient();
|
|
173
|
-
const { client: scopedAthena, organizationId, session } = await resolveAthenaServerContext();
|
|
174
|
-
|
|
175
|
-
function Example() {
|
|
176
|
-
const { client, organizationId, refetch } = useAthenaSessionClient(athena);
|
|
177
|
-
void refetch;
|
|
178
|
-
return organizationId ? client : athena;
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
These helpers:
|
|
183
|
-
|
|
184
|
-
- resolve Athena URL, API key, client name, and auth defaults from environment aliases
|
|
185
|
-
- bind request `cookie` and bearer context automatically on the server
|
|
186
|
-
- derive the current organization from `session.session.activeOrganizationId`
|
|
187
|
-
- return a pre-scoped client so normal Athena queries inherit current user + org context without app-local header assembly
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
Current behavior:
|
|
205
|
-
|
|
206
|
-
- `headers.Cookie` containing an Athena auth session cookie keeps `Cookie` and also adds `X-Athena-Auth-Session-Token`
|
|
207
|
-
- `headers.Authorization: Bearer ...` keeps `Authorization` and also adds `X-Athena-Auth-Bearer-Token`
|
|
208
|
-
- `createClient(..., { auth: { cookie, sessionToken, bearerToken } })` binds the same defaults onto `client.auth.*` and mirrors the available token context onto gateway/query requests
|
|
209
|
-
|
|
210
|
-
Server-side request-scoped auth context example:
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
|
|
214
58
|
auth: {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
bearerToken: session?.session?.token,
|
|
218
|
-
sessionToken: session?.session?.token,
|
|
219
|
-
credentials: "include",
|
|
59
|
+
url: process.env.ATHENA_AUTH_URL,
|
|
60
|
+
credentials: 'include',
|
|
220
61
|
},
|
|
221
|
-
|
|
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
|
-
|
|
233
|
-
const athena = createClient(ATHENA_URL, ATHENA_API_KEY, {
|
|
234
|
-
client: "CLIENT_NAME",
|
|
235
|
-
auth: {
|
|
236
|
-
baseUrl: "http://localhost:3001/api/auth",
|
|
237
|
-
cookie: request.headers.get("cookie") ?? "",
|
|
238
|
-
// optional: bearer token or explicit session token if you are not relying only on cookies
|
|
239
|
-
bearerToken: process.env.AUTH_BEARER_TOKEN,
|
|
240
|
-
sessionToken: process.env.AUTH_SESSION_TOKEN,
|
|
241
|
-
credentials: "include",
|
|
62
|
+
chat: {
|
|
63
|
+
url: process.env.ATHENA_CHAT_URL,
|
|
64
|
+
wsUrl: process.env.ATHENA_CHAT_WS_URL,
|
|
242
65
|
},
|
|
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" });
|
|
265
|
-
await athena.auth.changePassword({ currentPassword: "old-secret", newPassword: "new-secret" });
|
|
266
|
-
await athena.auth.user.update({ name: "Demo User" });
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
If you need a one-off override such as impersonation, pass it per call:
|
|
270
|
-
|
|
271
|
-
```ts
|
|
272
|
-
await athena.auth.admin.hasPermission(
|
|
273
|
-
{ permissions: ["admin:read"] },
|
|
274
|
-
{ bearerToken: impersonationToken },
|
|
275
|
-
);
|
|
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
|
-
|
|
295
|
-
await athena.auth.admin.email.template.create({
|
|
296
|
-
template_key: "welcome",
|
|
297
|
-
subject_template: "Welcome",
|
|
298
|
-
react: {
|
|
299
|
-
component: WelcomeEmail,
|
|
300
|
-
props: { name: "Ava" },
|
|
66
|
+
storage: {
|
|
67
|
+
url: process.env.ATHENA_STORAGE_URL,
|
|
301
68
|
},
|
|
302
|
-
})
|
|
69
|
+
})
|
|
303
70
|
```
|
|
304
71
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
```ts
|
|
308
|
-
import { defineAuthEmailTemplate } from "@xylex-group/athena";
|
|
309
|
-
|
|
310
|
-
const welcomeTemplate = defineAuthEmailTemplate({
|
|
311
|
-
component: WelcomeEmail,
|
|
312
|
-
templateKey: "welcome",
|
|
313
|
-
subjectTemplate: "Welcome",
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
await athena.auth.admin.email.template.create(
|
|
317
|
-
welcomeTemplate.toTemplateCreate({
|
|
318
|
-
props: { name: "Ava" },
|
|
319
|
-
}),
|
|
320
|
-
);
|
|
321
|
-
```
|
|
72
|
+
An explicit service URL overrides unified-root routing. When a service has no route, its namespace stays present and throws `AthenaConfigurationError` with code `ATHENA_SERVICE_NOT_CONFIGURED` when invoked.
|
|
322
73
|
|
|
323
|
-
|
|
74
|
+
## Stable options
|
|
324
75
|
|
|
325
76
|
```ts
|
|
326
|
-
const athena = createClient(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
77
|
+
const athena = createClient({
|
|
78
|
+
url,
|
|
79
|
+
key,
|
|
80
|
+
retryReads: true,
|
|
81
|
+
traceQueries: true,
|
|
82
|
+
debugAst: true,
|
|
83
|
+
findManyAst: true,
|
|
84
|
+
storage: {
|
|
85
|
+
directUpload,
|
|
86
|
+
onError(error) {
|
|
87
|
+
reportStorageError(error)
|
|
333
88
|
},
|
|
334
89
|
},
|
|
335
|
-
})
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
Install support packages in your app:
|
|
339
|
-
|
|
340
|
-
```bash
|
|
341
|
-
pnpm add @react-email/components @react-email/render
|
|
90
|
+
})
|
|
342
91
|
```
|
|
343
92
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
#### Chat module
|
|
93
|
+
Athena JS 3 has no general-purpose `experimental` bag. Storage requires no enable flag, error normalization is unconditional, and model-derived typing requires no strictness flag.
|
|
347
94
|
|
|
348
|
-
|
|
95
|
+
## Models and type inference
|
|
349
96
|
|
|
350
97
|
```ts
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
const rooms = await athena.chat.room.list({ limit: 20 });
|
|
356
|
-
const created = await athena.chat.room.create({
|
|
357
|
-
slug: "engineering",
|
|
358
|
-
name: "Engineering",
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
await athena.chat.room.message.send(created.data?.id ?? "room_1", {
|
|
362
|
-
body: "Hello team",
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const socket = athena.chat.realtime.connect({
|
|
366
|
-
token: "chat_token",
|
|
367
|
-
onMessage(event) {
|
|
368
|
-
console.log(event);
|
|
369
|
-
},
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
socket.ping();
|
|
373
|
-
socket.close();
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
Available chat surfaces:
|
|
377
|
-
|
|
378
|
-
- `chat.room.list/create/get/update/archive`
|
|
379
|
-
- `chat.room.readCursor.upTo`
|
|
380
|
-
- `chat.room.member.list/add/remove`
|
|
381
|
-
- `chat.room.message.list/send/update/delete`
|
|
382
|
-
- `chat.message.reaction.add/remove`
|
|
383
|
-
- `chat.message.search`
|
|
384
|
-
- `chat.realtime.info/connect`
|
|
385
|
-
|
|
386
|
-
#### Native auth bootstrap helpers
|
|
98
|
+
import { createClient } from '@xylex-group/athena'
|
|
99
|
+
import { registry } from './athena/registry.generated'
|
|
387
100
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
an Athena-native `athenaAuth({...})` export that matches the Better Auth
|
|
391
|
-
top-level contract:
|
|
101
|
+
const athena = createClient({ url, key, models: registry })
|
|
102
|
+
const users = registry.app.schemas.public.models.users
|
|
392
103
|
|
|
393
|
-
|
|
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
|
-
}
|
|
104
|
+
await athena.from(users).select('id,email')
|
|
416
105
|
```
|
|
417
106
|
|
|
418
|
-
|
|
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)
|
|
107
|
+
Known model and explicit row types constrain tables, selections, filters, ordering, inserts, and updates. Dynamic callers without registry metadata can continue using `athena.from<Row>('runtime_table')`.
|
|
437
108
|
|
|
438
|
-
|
|
109
|
+
## Request context
|
|
439
110
|
|
|
440
111
|
```ts
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
}),
|
|
112
|
+
const athena = createClient({
|
|
113
|
+
url,
|
|
114
|
+
key,
|
|
115
|
+
context: async () => ({
|
|
116
|
+
cookie: await currentCookieHeader(),
|
|
117
|
+
bearerToken: await currentBearerToken(),
|
|
118
|
+
organizationId: await currentOrganizationId(),
|
|
469
119
|
}),
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
const typed = createTypedClient(registry, ATHENA_URL, ATHENA_API_KEY, {
|
|
473
|
-
tenantKeyMap: {
|
|
474
|
-
organizationId: "X-Organization-Id",
|
|
475
|
-
},
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
await typed
|
|
479
|
-
.withTenantContext({ organizationId: "org_1" })
|
|
480
|
-
.fromModel("primary", "public", "users")
|
|
481
|
-
.select("*");
|
|
482
|
-
|
|
483
|
-
const requestScoped = typed.withContext({
|
|
484
|
-
organizationId: "org_1",
|
|
485
|
-
userId: "user_1",
|
|
486
|
-
headers: {
|
|
487
|
-
"X-Request-Id": "req_1",
|
|
488
|
-
},
|
|
489
|
-
});
|
|
490
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
583
|
-
});
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
Important:
|
|
587
|
-
|
|
588
|
-
- `output.format = "table-builder"` is stable generator behavior, not an experimental flag.
|
|
589
|
-
- `experimental.findManyAst` is a separate runtime transport opt-in for `findMany(...)`; it does not enable generated table artifacts.
|
|
590
|
-
- the default generator mode is now safe direct `table-builder` output with `output.preset = "athena-direct"`
|
|
591
|
-
- the default registry target is `athena/registry.generated.ts`; opt into `output.preset = "legacy"` only when you intentionally need `athena/config.ts`
|
|
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
|
-
|
|
600
|
-
# direct postgres + Zero-style output + multiple schemas
|
|
601
|
-
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_db \
|
|
602
|
-
ATHENA_GENERATOR_SCHEMAS=public,analytics \
|
|
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
|
-
|
|
630
|
-
- PostgreSQL direct introspection (`provider.mode = "direct"`, `provider.connectionString` from your `PG_URL`/`DATABASE_URL`)
|
|
631
|
-
- PostgreSQL gateway-only introspection (`provider.mode = "gateway"` via Athena `POST /gateway/query`)
|
|
632
|
-
- Multiple schema syncs such as `public` plus `athena`, with schema-safe default output paths
|
|
633
|
-
- Default safe direct output via `output.preset = "athena-direct"` and `output.format = "table-builder"`
|
|
634
|
-
- Optional compatibility fallbacks to legacy `define-model` artifacts or `athena/config.ts` when explicitly requested
|
|
635
|
-
- Placeholder-driven output paths
|
|
636
|
-
- Feature flags (`features.emitRegistry`, `features.emitRelations`)
|
|
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" });
|
|
120
|
+
})
|
|
705
121
|
```
|
|
706
122
|
|
|
707
|
-
|
|
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)
|
|
123
|
+
The provider runs before every operation. Explicit views share the same core:
|
|
731
124
|
|
|
732
125
|
```ts
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
}
|
|
126
|
+
const organizationAthena = athena.withContext({
|
|
127
|
+
organizationId: 'org_1',
|
|
128
|
+
headers: { 'X-Company-Id': 'company_1' },
|
|
129
|
+
})
|
|
736
130
|
```
|
|
737
131
|
|
|
738
|
-
|
|
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`)
|
|
132
|
+
Header precedence is client headers, configured context, `withContext`, then operation headers.
|
|
745
133
|
|
|
746
|
-
|
|
134
|
+
## Next.js
|
|
747
135
|
|
|
748
|
-
|
|
136
|
+
Thin façades on `@xylex-group/athena/next/client` and `.../next/server` adapt
|
|
137
|
+
browser-safe config or request context, then call `createClient`. Full guide:
|
|
138
|
+
[docs/next-js.md](./docs/next-js.md).
|
|
749
139
|
|
|
750
140
|
```ts
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
},
|
|
758
|
-
},
|
|
759
|
-
},
|
|
760
|
-
});
|
|
761
|
-
```
|
|
141
|
+
// Client Components
|
|
142
|
+
import { createAthenaBrowserClient } from '@xylex-group/athena/next/client'
|
|
143
|
+
export const athena = createAthenaBrowserClient({
|
|
144
|
+
url: process.env.NEXT_PUBLIC_ATHENA_URL!,
|
|
145
|
+
key: process.env.NEXT_PUBLIC_ATHENA_PUBLISHABLE_KEY!,
|
|
146
|
+
})
|
|
762
147
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
913
|
-
### Retry helper (deprecated)
|
|
914
|
-
|
|
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.
|
|
916
|
-
|
|
917
|
-
```ts
|
|
918
|
-
import { withRetry } from "@xylex-group/athena";
|
|
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"),
|
|
928
|
-
);
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
By default, retries target transient/rate-limit failures; use `shouldRetry` for custom policies.
|
|
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;
|
|
148
|
+
// Server Components / Actions / Route Handlers
|
|
149
|
+
import { createAthenaServerClient } from '@xylex-group/athena/next/server'
|
|
150
|
+
export function createServerAthena() {
|
|
151
|
+
return createAthenaServerClient({
|
|
152
|
+
url: process.env.ATHENA_URL!,
|
|
153
|
+
key: process.env.ATHENA_API_KEY!,
|
|
154
|
+
})
|
|
1436
155
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
|
156
|
+
const athena = await createServerAthena()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Documentation
|
|
160
|
+
|
|
161
|
+
- [Docs index](./docs/index.md) — tracks for runtime, Next, auth, types, generator
|
|
162
|
+
- [Getting started](./docs/getting-started.md)
|
|
163
|
+
- [Next.js integration](./docs/next-js.md)
|
|
164
|
+
- [API reference](./docs/api-reference.md)
|
|
165
|
+
- [Complete generated method reference](./docs/complete-method-reference.md)
|
|
166
|
+
- [Typed schema registry](./docs/typed-schema-registry.md)
|
|
167
|
+
- [v2.16.0 to v3.0.0 migration](./docs/migration-v2-to-v3.md)
|
|
168
|
+
- [Client internal architecture](./docs/client-internal-architecture.md)
|
|
169
|
+
- [Site dual-publish (package → apps/docs)](./docs/site-publish.md)
|
|
170
|
+
- [Accepted ADR catalog](./docs/adr/README.md) (includes [ADR 0014](./docs/adr/0014-next-client-construction-facades.md))
|
|
171
|
+
- [3.0 release readiness](./docs/client-v3-release-readiness-report.md)
|