@terreno/agent-plugin 57.21.0 → 58.0.0-beta.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +89 -0
- package/package.json +1 -1
- package/skills/building-admin-interfaces/SKILL.md +9 -6
- package/skills/building-admin-interfaces/references/nav-and-shell.md +1 -1
- package/skills/building-admin-interfaces/references/patterns-and-tools.md +4 -3
- package/skills/building-terreno-apps/SKILL.md +48 -34
- package/skills/building-terreno-apps/agents/openai.yaml +2 -2
- package/skills/building-terreno-apps/references/navigation.md +20 -14
- package/skills/building-terreno-apps/references/route-structure.md +26 -13
- package/skills/building-terreno-apps/references/screen-patterns.md +37 -54
- package/skills/generate-sdk/SKILL.md +87 -78
- package/skills/terreno-backend-api/references/model-router.md +0 -4
- package/skills/terreno-data-fetching/SKILL.md +83 -71
- package/skills/terreno-data-fetching/agents/openai.yaml +2 -2
- package/skills/terreno-data-fetching/references/auth-and-tokens.md +56 -60
- package/skills/terreno-data-fetching/references/realtime.md +26 -21
- package/skills/terreno-data-fetching/references/sdk-customization.md +31 -60
- package/skills/terreno-ui/SKILL.md +2 -2
- package/skills/terreno-ui/references/data-display.md +2 -0
- package/skills/upgrading-terreno/SKILL.md +13 -6
- package/skills/upgrading-terreno/references/ordering.md +3 -3
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
name: terreno-data-fetching
|
|
3
3
|
description: >-
|
|
4
4
|
Use when implementing or debugging ANY network request, API call, or data
|
|
5
|
-
fetching in a Terreno app. Covers syncdb collection CRUD,
|
|
6
|
-
hooks for non-synced routes,
|
|
7
|
-
and SDK regeneration. Replaces direct fetch, axios, React Query, and SWR in
|
|
5
|
+
fetching in a Terreno app. Covers syncdb collection CRUD, generated request
|
|
6
|
+
hooks (store/apiSdk.ts) for non-synced routes, session auth, socket pushes,
|
|
7
|
+
feature flags, admin's host-bound fetch client, and SDK regeneration. Replaces direct fetch, axios, React Query, and SWR in
|
|
8
8
|
application code.
|
|
9
9
|
Lifecycle composition: Grow for data-flow shape, Pick for implementation, Roast
|
|
10
10
|
for independent network/cache behavior proof.
|
|
11
11
|
---
|
|
12
12
|
# Terreno Data Fetching
|
|
13
13
|
|
|
14
|
-
**You MUST use this skill for ANY networking work in Terreno apps.**
|
|
15
|
-
for synced collection CRUD and
|
|
14
|
+
**You MUST use this skill for ANY networking work in Terreno apps.** Everything goes
|
|
15
|
+
through `@terreno/syncdb`: `useQuery` / `useMutate` for synced collection CRUD, and
|
|
16
|
+
generated hooks on the `@terreno/syncdb/api` request client for every other route.
|
|
16
17
|
Never call `axios`, raw `fetch`, React Query, or SWR from application screens.
|
|
18
|
+
`@terreno/rtk` and Redux were removed in Terreno 58; upgrade old code with
|
|
19
|
+
`docs/how-to/migrate-rtk-to-syncdb.md`.
|
|
17
20
|
|
|
18
21
|
**Related skills:** `generate-sdk` (regenerate hooks after backend changes), `terreno-backend-api` (create backend routes), `building-terreno-apps` (screen integration).
|
|
19
22
|
|
|
@@ -28,9 +31,9 @@ Never call `axios`, raw `fetch`, React Query, or SWR from application screens.
|
|
|
28
31
|
|
|
29
32
|
```
|
|
30
33
|
references/
|
|
31
|
-
auth-and-tokens.md
|
|
32
|
-
realtime.md
|
|
33
|
-
sdk-customization.md
|
|
34
|
+
auth-and-tokens.md SessionProvider/useSession, login/logout, token refresh, JWT
|
|
35
|
+
realtime.md Socket events, feature flags, sync status
|
|
36
|
+
sdk-customization.md Generated apiSdk.ts, ad-hoc useApiQuery/useApiMutation, cache tags
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
## When to Use
|
|
@@ -44,15 +47,19 @@ references/
|
|
|
44
47
|
|
|
45
48
|
## Core Rules
|
|
46
49
|
|
|
47
|
-
1. **Use syncdb hooks** (`useQuery`, `useEntity`, `useMutate`) for synced collection CRUD.
|
|
48
|
-
2. **Use generated hooks** from `store/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
5. **
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
1. **Use syncdb hooks** (`useQuery`, `useEntity`, `useWindowQuery`, `useMutate`) for synced collection CRUD.
|
|
51
|
+
2. **Use generated hooks** from `store/apiSdk.ts` (`terreno-syncdb-codegen --api-out`) for every
|
|
52
|
+
non-synced route: `/auth/me`, modelRouter actions, AI, consents, announcements.
|
|
53
|
+
3. **Never edit `store/apiSdk.ts` or `store/syncDbSdk.ts`** — regenerate with `generate-sdk`.
|
|
54
|
+
4. **Ad-hoc routes not in OpenAPI** use `useApiQuery` / `useLazyApiQuery` / `useApiMutation`
|
|
55
|
+
from `@terreno/syncdb/react`. Prefer documenting the route in OpenAPI and regenerating.
|
|
56
|
+
5. **Never use `axios` or raw `fetch`** in app code. Exceptions: SSE streams (`/gpt/prompt`)
|
|
57
|
+
authenticated with `authProvider.getToken()`, and framework-owned admin requests
|
|
58
|
+
(`@terreno/admin-frontend` binds `adminRequest` from `AdminProvider` `credentials` /
|
|
59
|
+
`getAuthHeaders` / `apiOrigin`).
|
|
60
|
+
6. **Admin uses its own fetch client.** String-`_id` models with `adminBroadcast` use
|
|
61
|
+
windowed syncdb; ObjectId/API-only CRUD uses `useAdminModelApi`; admin RPC uses
|
|
62
|
+
`useAdminRpc`. Admin components take no `api` prop (removed in Terreno 58).
|
|
56
63
|
|
|
57
64
|
## Quick Start
|
|
58
65
|
|
|
@@ -71,12 +78,13 @@ const TodosScreen: React.FC = () => {
|
|
|
71
78
|
### Synced collection write
|
|
72
79
|
|
|
73
80
|
```tsx
|
|
81
|
+
import {generateMutationId} from "@terreno/syncdb";
|
|
74
82
|
import {useMutate} from "@terreno/syncdb/react";
|
|
75
83
|
|
|
76
84
|
const {create, update} = useMutate("todos");
|
|
77
85
|
|
|
78
86
|
const handleCreate = useCallback((): void => {
|
|
79
|
-
create({data: {title: "New todo", completed: false}});
|
|
87
|
+
create({data: {_id: generateMutationId(), title: "New todo", completed: false}});
|
|
80
88
|
}, [create]);
|
|
81
89
|
|
|
82
90
|
const handleToggle = useCallback((id: string, completed: boolean): void => {
|
|
@@ -84,47 +92,48 @@ const handleToggle = useCallback((id: string, completed: boolean): void => {
|
|
|
84
92
|
}, [update]);
|
|
85
93
|
```
|
|
86
94
|
|
|
87
|
-
###
|
|
95
|
+
### Non-synced route
|
|
88
96
|
|
|
89
97
|
```tsx
|
|
90
|
-
import {
|
|
91
|
-
|
|
98
|
+
import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
|
|
99
|
+
|
|
100
|
+
const {data: me, isLoading} = useGetAuthMeQuery(undefined, {skip: !userId});
|
|
101
|
+
const [updateMe, {isLoading: isSaving}] = usePatchAuthMeMutation();
|
|
102
|
+
await updateMe({name}).unwrap();
|
|
103
|
+
```
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
### Auth
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
import {
|
|
97
|
-
import {useSelectCurrentUserId} from "@terreno/rtk";
|
|
107
|
+
```tsx
|
|
108
|
+
import {useSession} from "@terreno/syncdb/auth";
|
|
98
109
|
|
|
99
|
-
const
|
|
100
|
-
const userId = useSelectCurrentUserId();
|
|
110
|
+
const {userId, isAuthenticated, isLoading, logout} = useSession();
|
|
101
111
|
```
|
|
102
112
|
|
|
103
113
|
See `./references/auth-and-tokens.md` for the full auth flow.
|
|
104
114
|
|
|
105
|
-
##
|
|
115
|
+
## Client Setup
|
|
116
|
+
|
|
117
|
+
One `authProvider` feeds the sync client and the request client, so both send the same
|
|
118
|
+
token and refresh on a 401 the same way. Reference: `example-frontend/store/syncdb.ts`,
|
|
119
|
+
`store/apiClient.ts`, `app/_layout.tsx`.
|
|
106
120
|
|
|
107
121
|
```tsx
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
export const {logout} = authSlice;
|
|
122
|
+
// store/syncdb.ts
|
|
123
|
+
export const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient));
|
|
124
|
+
export const syncDb = createSyncDb({authProvider, baseUrl, collections: [...SYNC_COLLECTIONS], name: "myapp", wipeOnSignOut: true});
|
|
125
|
+
|
|
126
|
+
// store/apiClient.ts
|
|
127
|
+
export const apiClient = createApiClient({authProvider, baseUrl, credentials: "include", headers: appHeaders});
|
|
128
|
+
|
|
129
|
+
// app/_layout.tsx — no Redux store
|
|
130
|
+
<SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
|
|
131
|
+
<ApiClientProvider client={apiClient}>
|
|
132
|
+
<SyncDbProvider client={syncDb}>{children}</SyncDbProvider>
|
|
133
|
+
</ApiClientProvider>
|
|
134
|
+
</SessionProvider>
|
|
124
135
|
```
|
|
125
136
|
|
|
126
|
-
See `example-frontend/store/index.ts` for persist and Sentry integration.
|
|
127
|
-
|
|
128
137
|
## Environment Variables
|
|
129
138
|
|
|
130
139
|
| Variable | Purpose |
|
|
@@ -133,7 +142,7 @@ See `example-frontend/store/index.ts` for persist and Sentry integration.
|
|
|
133
142
|
| `EXPO_PUBLIC_DEV_API_PORT` | Dev API port when using Expo host URI (default `4000`) |
|
|
134
143
|
| `expo.extra.BASE_URL` | Production/staging API URL in `app.json` |
|
|
135
144
|
|
|
136
|
-
Base URL resolution priority (from `@terreno/
|
|
145
|
+
Base URL resolution priority (`baseUrl` from `@terreno/syncdb/config`):
|
|
137
146
|
|
|
138
147
|
1. `expo.extra.BASE_URL`
|
|
139
148
|
2. `EXPO_PUBLIC_API_URL`
|
|
@@ -144,13 +153,13 @@ Never put server secrets in `EXPO_PUBLIC_*` variables.
|
|
|
144
153
|
|
|
145
154
|
## Error Handling
|
|
146
155
|
|
|
147
|
-
|
|
156
|
+
Request hooks expose `ApiError` (`@terreno/syncdb/api`) on the result:
|
|
148
157
|
|
|
149
158
|
```tsx
|
|
150
|
-
const {error, isError} =
|
|
159
|
+
const {error, isError} = useGetAuthMeQuery();
|
|
151
160
|
|
|
152
161
|
if (isError) {
|
|
153
|
-
// error
|
|
162
|
+
// error.status, error.data.title, error.data.detail; status 0 = network error or timeout
|
|
154
163
|
console.error("Query failed", error);
|
|
155
164
|
}
|
|
156
165
|
```
|
|
@@ -160,27 +169,29 @@ For mutations, use `.unwrap()`:
|
|
|
160
169
|
```tsx
|
|
161
170
|
try {
|
|
162
171
|
await updateMe({name}).unwrap();
|
|
163
|
-
} catch (
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (isApiError(error)) {
|
|
174
|
+
console.error("Mutation failed", error.data.title);
|
|
175
|
+
}
|
|
166
176
|
}
|
|
167
177
|
```
|
|
168
178
|
|
|
169
|
-
|
|
179
|
+
Report every request failure once with `createApiClient({onError})` (skip Sentry when
|
|
180
|
+
`error.data.disableExternalErrorTracking` is `true`). Synced writes never throw: rejected
|
|
181
|
+
writes surface in `useConflicts()` and `useSyncStatus().failedCount`.
|
|
170
182
|
|
|
171
183
|
## SDK Regeneration
|
|
172
184
|
|
|
173
|
-
After a
|
|
174
|
-
collection CRUD does not belong in the OpenAPI SDK.
|
|
185
|
+
After a backend API change, run the `generate-sdk` skill.
|
|
175
186
|
|
|
176
187
|
```bash
|
|
177
188
|
# Backend running on :4000, then:
|
|
178
|
-
cd example-frontend && bun run sdk
|
|
189
|
+
cd example-frontend && bun run sdk # store/apiSdk.ts (non-synced routes)
|
|
190
|
+
cd example-frontend && bun run sync-sdk # store/syncDbSdk.ts (synced collections)
|
|
179
191
|
```
|
|
180
192
|
|
|
181
|
-
Triggers: custom routes, non-synced `modelRouter` routes, and OpenAPI
|
|
182
|
-
edits
|
|
183
|
-
schema changes.
|
|
193
|
+
Triggers: custom routes, modelRouter actions, non-synced `modelRouter` routes, and OpenAPI
|
|
194
|
+
edits (`sdk`); synced schema or `sync` config changes (`sync-sdk`).
|
|
184
195
|
|
|
185
196
|
## Decision Tree
|
|
186
197
|
|
|
@@ -190,22 +201,22 @@ Need data in a Terreno app?
|
|
|
190
201
|
| \-- @terreno/syncdb hooks
|
|
191
202
|
|
|
|
192
203
|
|-- Non-synced API route?
|
|
193
|
-
| \-- useGet*/usePost* from
|
|
204
|
+
| \-- useGet*/usePost* from store/apiSdk.ts
|
|
194
205
|
|
|
|
195
206
|
|-- Built-in admin?
|
|
196
207
|
| |-- String id + adminBroadcast -> windowed syncdb
|
|
197
|
-
| |-- Framework RPC ->
|
|
198
|
-
| \-- ObjectId
|
|
208
|
+
| |-- Framework RPC -> AdminProvider adminRequest (useAdminRpc)
|
|
209
|
+
| \-- ObjectId / API-only CRUD -> useAdminModelApi
|
|
199
210
|
|
|
|
200
211
|
|-- Login/logout?
|
|
201
212
|
| \-- references/auth-and-tokens.md
|
|
202
213
|
|
|
|
203
|
-
|--
|
|
204
|
-
| \-- references/realtime.md (
|
|
214
|
+
|-- Server push / feature flags?
|
|
215
|
+
| \-- references/realtime.md (useSocketEvent, @terreno/syncdb/flags)
|
|
205
216
|
|
|
|
206
217
|
|-- Custom endpoint not in SDK?
|
|
207
218
|
| |-- Backend exists? -> generate-sdk first
|
|
208
|
-
| \--
|
|
219
|
+
| \-- Route outside OpenAPI? -> references/sdk-customization.md (useApiQuery)
|
|
209
220
|
|
|
|
210
221
|
|-- Backend doesn't exist?
|
|
211
222
|
| \-- terreno-backend-api, then generate-sdk
|
|
@@ -237,10 +248,10 @@ fetch(url, {headers: {Authorization: `Bearer ${token}`}});
|
|
|
237
248
|
**Right: a generated hook handles auth for a non-synced route**
|
|
238
249
|
|
|
239
250
|
```tsx
|
|
240
|
-
const {data: profile} =
|
|
251
|
+
const {data: profile} = useGetAuthMeQuery();
|
|
241
252
|
```
|
|
242
253
|
|
|
243
|
-
**Wrong: editing
|
|
254
|
+
**Wrong: editing store/apiSdk.ts**
|
|
244
255
|
|
|
245
256
|
```tsx
|
|
246
257
|
// Adding a new endpoint by hand
|
|
@@ -269,8 +280,9 @@ throw new APIError({status: 422, title: "Not a member"});
|
|
|
269
280
|
## Example Invocations
|
|
270
281
|
|
|
271
282
|
- "How do I fetch todos?" → `useQuery("todos")` from `@terreno/syncdb/react`
|
|
272
|
-
- "How do I handle login?" → `
|
|
273
|
-
- "Should I use React Query?" → No — syncdb for collections, generated
|
|
274
|
-
- "API calls return 401" → Check
|
|
283
|
+
- "How do I handle login?" → `betterAuthClient.signIn.email` then `useSession().refresh()`
|
|
284
|
+
- "Should I use React Query?" → No — syncdb for collections, generated `store/apiSdk.ts` hooks for non-synced routes
|
|
285
|
+
- "API calls return 401" → Check the shared `authProvider`; see auth-and-tokens.md
|
|
286
|
+
- "Old code imports `@terreno/rtk`" → `docs/how-to/migrate-rtk-to-syncdb.md`
|
|
275
287
|
- "Hooks are missing after backend change" → Run `generate-sdk` skill
|
|
276
288
|
- "How do I add a custom endpoint?" → Backend route + OpenAPI, then regenerate the SDK
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Terreno Data Fetching"
|
|
3
|
-
short_description: "
|
|
4
|
-
default_prompt: "Use $terreno-data-fetching to implement API calls with
|
|
3
|
+
short_description: "syncdb collection hooks, generated request hooks, session auth, and socket events for Terreno apps — never axios or raw fetch"
|
|
4
|
+
default_prompt: "Use $terreno-data-fetching to implement API calls with @terreno/syncdb collection hooks, generated store/apiSdk.ts hooks, and session auth in a Terreno app."
|
|
@@ -1,93 +1,89 @@
|
|
|
1
1
|
# Auth and Tokens — Terreno
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Session state lives in `@terreno/syncdb/auth`. There is no Redux store. Reference:
|
|
4
|
+
`docs/reference/syncdb.md#auth`, `example-frontend/lib/betterAuth.ts`, `app/login.tsx`.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- `authReducer` — tracks `userId`, `error`, `lastTokenRefreshTimestamp`
|
|
8
|
-
- `logout` action — clears tokens from secure storage
|
|
9
|
-
- `middleware` — stores tokens on login, removes on logout
|
|
10
|
-
- Built-in endpoints: `emailLogin`, `emailSignUp`, `googleLogin`, `resetPassword`
|
|
11
|
-
|
|
12
|
-
## Login
|
|
6
|
+
## Better Auth client
|
|
13
7
|
|
|
14
8
|
```tsx
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
// lib/betterAuth.ts
|
|
10
|
+
import {createBetterAuthClient} from "@terreno/syncdb/auth";
|
|
11
|
+
import {baseUrl} from "@terreno/syncdb/config";
|
|
12
|
+
|
|
13
|
+
export const betterAuthClient = createBetterAuthClient({
|
|
14
|
+
baseURL: baseUrl,
|
|
15
|
+
scheme: "myapp",
|
|
16
|
+
storagePrefix: "myapp",
|
|
17
|
+
});
|
|
22
18
|
```
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
Storage is `expo-secure-store` on native and AsyncStorage on web. Never store tokens in
|
|
21
|
+
plain AsyncStorage on native or in `EXPO_PUBLIC_*` env vars.
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
import {useAppDispatch, logout} from "../store/index.ts";
|
|
23
|
+
## Session provider
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
```tsx
|
|
26
|
+
<SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
|
|
27
|
+
<App />
|
|
28
|
+
</SessionProvider>
|
|
32
29
|
```
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- **Native:** `expo-secure-store` (encrypted)
|
|
37
|
-
- **Web:** `@react-native-async-storage/async-storage` (SSR-safe wrapper)
|
|
38
|
-
|
|
39
|
-
Never store tokens in plain AsyncStorage on native or in `EXPO_PUBLIC_*` env vars.
|
|
31
|
+
```tsx
|
|
32
|
+
import {useSession} from "@terreno/syncdb/auth";
|
|
40
33
|
|
|
41
|
-
|
|
34
|
+
const {userId, user, isAuthenticated, isLoading, error, logout, refresh} = useSession();
|
|
35
|
+
```
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
## Login
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- Uses a mutex to prevent concurrent refresh storms
|
|
39
|
+
```tsx
|
|
40
|
+
const {refresh} = useSession();
|
|
48
41
|
|
|
49
|
-
|
|
42
|
+
const result = await betterAuthClient.signIn.email({email, password});
|
|
43
|
+
if (!result.error) {
|
|
44
|
+
await refresh(); // resolve userId now instead of waiting for the session atom
|
|
45
|
+
}
|
|
46
|
+
```
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
Use `SocialLoginButton` from `@terreno/ui` with `betterAuthClient.signIn.social({provider})`
|
|
49
|
+
for OAuth.
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
## Logout
|
|
54
52
|
|
|
55
53
|
```tsx
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
const {logout} = useSession();
|
|
55
|
+
await logout();
|
|
56
|
+
```
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
scheme: "myapp", // deep-link scheme, required
|
|
62
|
-
});
|
|
58
|
+
`logout()` signs out each `syncDbs` client first (wiping local rows when created with
|
|
59
|
+
`wipeOnSignOut: true`), then the auth server, then runs `onLogout`.
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
const betterAuth = generateBetterAuthSlice({authClient});
|
|
61
|
+
## Token refresh
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))` is the shared
|
|
64
|
+
`AuthProvider` for `createSyncDb` and `createApiClient`. The request client adds
|
|
65
|
+
`Authorization: Bearer`, and on a 401 calls `authProvider.refresh()` once and retries.
|
|
66
|
+
No manual refresh logic in app code. Code outside the client (SSE streams, `AdminProvider`
|
|
67
|
+
`getAuthHeaders`) reads `authProvider.getToken()`.
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
`authorization` header when there is none, and logs out only after a 401 survives one
|
|
74
|
-
`refresh()`. Do not fork the base query to read the session. Without `setTokenSource`, rtk
|
|
75
|
-
uses the JWT tokens in storage (`jwtTokenSource`).
|
|
69
|
+
## JWT backends (legacy)
|
|
76
70
|
|
|
77
|
-
|
|
71
|
+
Apps on `@terreno/api` JWT routes use `jwtAuthAdapter` as both the `authProvider` and the
|
|
72
|
+
`SessionProvider` `source`, plus `useJwtAuth({adapter})` for `login`, `signup`,
|
|
73
|
+
`googleLogin`, `resetPassword`. It refreshes when under 130 s remain and on a 401, with one
|
|
74
|
+
shared refresh in flight. See `docs/reference/syncdb.md#jwt-auth-legacy`.
|
|
78
75
|
|
|
79
76
|
## Current user
|
|
80
77
|
|
|
81
78
|
```tsx
|
|
82
|
-
import {
|
|
83
|
-
import {useReadProfile} from "@/hooks/useReadProfile"; // app-specific
|
|
79
|
+
import {useGetAuthMeQuery} from "@/store/apiSdk";
|
|
84
80
|
|
|
85
|
-
const userId =
|
|
86
|
-
const {data:
|
|
81
|
+
const {userId} = useSession();
|
|
82
|
+
const {data: me} = useGetAuthMeQuery(undefined, {skip: !userId});
|
|
87
83
|
```
|
|
88
84
|
|
|
89
85
|
## Protected routes
|
|
90
86
|
|
|
91
|
-
Gate navigation on `userId` in root `_layout.tsx`. Redirect unauthenticated
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
Gate navigation on `useSession().userId` in root `_layout.tsx`. Redirect unauthenticated
|
|
88
|
+
users to `/login`. For admin routes, check `canOpenAdminPage({admin: me?.admin, permissions:
|
|
89
|
+
me?.permissions})` from `@terreno/syncdb/app`.
|
|
@@ -1,45 +1,50 @@
|
|
|
1
1
|
# Realtime — Terreno
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The syncdb client owns the app's only socket. Do not open a second one.
|
|
4
|
+
|
|
5
|
+
## Server push events
|
|
4
6
|
|
|
5
7
|
```tsx
|
|
6
|
-
import {
|
|
8
|
+
import {useSocketEvent} from "@terreno/syncdb/react";
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
shouldConnect: !!userId,
|
|
11
|
-
getAuthToken: () => getAuthToken(),
|
|
10
|
+
useSocketEvent("announcementPublished", (payload) => {
|
|
11
|
+
console.info("announcement", payload);
|
|
12
12
|
});
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Inside `SyncDbProvider` the hook uses that client; above it, pass `{client: syncDb}`, or
|
|
16
|
+
`{client: null}` to listen to nothing. Outside React: `syncDb.onSocketEvent(event, handler)`
|
|
17
|
+
returns an unsubscribe.
|
|
16
18
|
|
|
17
19
|
## Feature flags
|
|
18
20
|
|
|
19
21
|
```tsx
|
|
20
|
-
import {
|
|
22
|
+
import {OpenFeatureProvider} from "@openfeature/react-sdk";
|
|
23
|
+
import {useTerrenoFeatureFlags} from "@terreno/syncdb/flags";
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
userId,
|
|
26
|
-
});
|
|
25
|
+
const {userId} = useSession();
|
|
26
|
+
useTerrenoFeatureFlags({client: apiClient, skip: !userId, socket: syncDb, userId});
|
|
27
|
+
return <OpenFeatureProvider domain="feature-flags">{children}</OpenFeatureProvider>;
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
Flags load through the request client and refetch when the server pushes
|
|
31
|
+
`featureFlagsChanged` on the sync socket. Screens can read flat accessors with
|
|
32
|
+
`useFeatureFlags({skip, socket, userId})`. See `example-frontend/app/_layout.tsx` and
|
|
33
|
+
`docs/reference/feature-flags.md`.
|
|
30
34
|
|
|
31
|
-
##
|
|
35
|
+
## Sync status
|
|
32
36
|
|
|
33
37
|
```tsx
|
|
34
|
-
import {
|
|
38
|
+
import {useSyncStatus} from "@terreno/syncdb/react";
|
|
35
39
|
|
|
36
|
-
const {
|
|
40
|
+
const {isOnline, queuedCount, conflictCount, failedCount, paused} = useSyncStatus();
|
|
37
41
|
```
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
Render `SyncStatusBanner` / `OfflineBanner` from `@terreno/ui` with these values.
|
|
40
44
|
|
|
41
45
|
## Realtime model updates
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
Live collection updates come from `sync` on the backend `modelRouter`; read them with
|
|
48
|
+
`useQuery` / `useEntity`. `RealtimeApp` hosts the socket for `sync:delta` and app events.
|
|
49
|
+
Terreno 58 removed `modelRouter` `realtime` and every `@terreno/rtk` socket helper
|
|
50
|
+
(`useSocketConnection`, `realtimeList`, `realtimeDocument`, `useRealtimeDebug`).
|
|
@@ -4,81 +4,52 @@
|
|
|
4
4
|
|
|
5
5
|
| File | Editable? | Purpose |
|
|
6
6
|
|------|-----------|---------|
|
|
7
|
-
| `store/
|
|
8
|
-
| `store/
|
|
9
|
-
| `
|
|
7
|
+
| `store/apiSdk.ts` | **Never** | Typed hooks for every non-synced OpenAPI operation (`--api-out`) |
|
|
8
|
+
| `store/syncDbSdk.ts` | **Never** | `SYNC_COLLECTIONS` + collection hooks (`--out`) |
|
|
9
|
+
| `store/apiClient.ts` | **Yes** | `createApiClient` (auth, headers, `onError`) |
|
|
10
|
+
| `syncdb-codegen.json` | **Yes** | Codegen config (`excludePaths`, per-collection `overrides`) |
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Generated hooks
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Names follow the old RTK codegen rule: camelCased `operationId`, otherwise method + path with
|
|
15
|
+
`{param}` as `By<Param>` (`PATCH /gpt/histories/{id}` → `usePatchGptHistoriesByIdMutation`).
|
|
16
|
+
Each operation also gets `<Name>Args`, `<Name>Res`, and `<name>Request(arg)` for
|
|
17
|
+
`apiClient.request(...)`. Tags come from the operation's OpenAPI tags.
|
|
14
18
|
|
|
15
19
|
```tsx
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
export const terrenoApi = openapi
|
|
19
|
-
.injectEndpoints({
|
|
20
|
-
endpoints: (build) => ({
|
|
21
|
-
getMe: build.query({
|
|
22
|
-
query: () => "/auth/me",
|
|
23
|
-
providesTags: ["profile"],
|
|
24
|
-
}),
|
|
25
|
-
patchMe: build.mutation({
|
|
26
|
-
query: (body) => ({
|
|
27
|
-
url: "/auth/me",
|
|
28
|
-
method: "PATCH",
|
|
29
|
-
body,
|
|
30
|
-
}),
|
|
31
|
-
invalidatesTags: ["profile"],
|
|
32
|
-
}),
|
|
33
|
-
}),
|
|
34
|
-
})
|
|
35
|
-
.enhanceEndpoints({
|
|
36
|
-
addTagTypes: ["todos", "users", "profile"],
|
|
37
|
-
});
|
|
20
|
+
import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
|
|
38
21
|
```
|
|
39
22
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## Cache tags
|
|
23
|
+
## Routes outside OpenAPI
|
|
43
24
|
|
|
44
|
-
|
|
25
|
+
Prefer documenting the route in OpenAPI and regenerating. When that is not possible, call it
|
|
26
|
+
directly through the request client:
|
|
45
27
|
|
|
46
28
|
```tsx
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
invalidatesTags:
|
|
57
|
-
|
|
29
|
+
import {useApiMutation, useApiQuery, useLazyApiQuery} from "@terreno/syncdb/react";
|
|
30
|
+
|
|
31
|
+
const {data, isLoading, refetch} = useApiQuery<Report>(
|
|
32
|
+
{url: "/reports/summary", params: {range}},
|
|
33
|
+
{providesTags: ["reports"], skip: !userId}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const [rebuild, {isLoading: isRebuilding}] = useApiMutation<RebuildBody, Report>(
|
|
37
|
+
(body) => ({body, method: "POST", url: "/reports/rebuild"}),
|
|
38
|
+
{invalidatesTags: ["reports"]}
|
|
39
|
+
);
|
|
40
|
+
await rebuild({range}).unwrap();
|
|
58
41
|
```
|
|
59
42
|
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
// openapi-config.ts
|
|
64
|
-
const config: ConfigFile = {
|
|
65
|
-
apiFile: "@terreno/rtk",
|
|
66
|
-
apiImport: "emptySplitApi",
|
|
67
|
-
outputFile: "./store/openApiSdk.ts",
|
|
68
|
-
schemaFile: "http://localhost:4000/openapi.json",
|
|
69
|
-
hooks: true,
|
|
70
|
-
tag: true,
|
|
71
|
-
flattenArg: true,
|
|
72
|
-
};
|
|
73
|
-
```
|
|
43
|
+
## Cache tags
|
|
74
44
|
|
|
75
|
-
|
|
45
|
+
Queries register `providesTags`; mutations `invalidatesTags`. Imperatively:
|
|
46
|
+
`apiClient.invalidateTags(["reports"])`. `apiClient.reset()` clears the cache (done on logout
|
|
47
|
+
via `SessionProvider` `onLogout`). Synced collections have no request cache; their data is local.
|
|
76
48
|
|
|
77
49
|
## Workflow after backend change
|
|
78
50
|
|
|
79
51
|
1. Start backend (`bun run backend:dev`)
|
|
80
|
-
2. Run `cd example-frontend && bun run sdk`
|
|
81
|
-
3. Import new hooks from `@/store/
|
|
82
|
-
4. If the endpoint needs custom cache behavior, update `sdk.ts`
|
|
52
|
+
2. Run `cd example-frontend && bun run sdk` (and `bun run sync-sdk` for synced schema changes)
|
|
53
|
+
3. Import new hooks from `@/store/apiSdk` (or `@/store/syncDbSdk`)
|
|
83
54
|
|
|
84
55
|
See the `generate-sdk` skill for the full procedure.
|
|
@@ -6,8 +6,8 @@ description: >-
|
|
|
6
6
|
layout, forms, tables, modals, feedback, or navigation chrome. Covers
|
|
7
7
|
component selection, Box/Page patterns, theme props, and when NOT to use raw
|
|
8
8
|
React Native primitives. Lifecycle composition: Grow for UI shape, Pick for
|
|
9
|
-
implementation, Roast for UI conformance review. Not for backend APIs,
|
|
10
|
-
|
|
9
|
+
implementation, Roast for UI conformance review. Not for backend APIs, data
|
|
10
|
+
fetching (syncdb), or Expo Router setup.
|
|
11
11
|
---
|
|
12
12
|
# @terreno/ui
|
|
13
13
|
|
|
@@ -42,6 +42,8 @@ Sizes: `sm`, `md`, `lg`, `xl`, `2xl` (responsive web/mobile values).
|
|
|
42
42
|
/>
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
Column `width` is optional. Omit it and the column sizes itself from its title and sampled content, then grows to fill the table; a set `width` is always kept. See `docs/reference/ui.md` (DataTable column widths).
|
|
46
|
+
|
|
45
47
|
Column types: `text`, `date`, `number`, `boolean`, `custom`.
|
|
46
48
|
|
|
47
49
|
For admin tables, prefer `AdminModelTable` from `@terreno/admin-frontend`.
|