@archlast/client 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +783 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,50 +1,579 @@
|
|
|
1
1
|
# @archlast/client
|
|
2
2
|
|
|
3
|
-
Archlast client SDK for React hooks,
|
|
3
|
+
The Archlast client SDK provides a complete solution for building reactive frontend applications with Archlast backends. It includes WebSocket-based real-time queries, React hooks with automatic cache management, authentication helpers, file storage utilities, admin APIs, and optional tRPC integration.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Quick Start](#quick-start)
|
|
9
|
+
- [Client Configuration](#client-configuration)
|
|
10
|
+
- [React Hooks](#react-hooks)
|
|
11
|
+
- [useQuery](#usequery)
|
|
12
|
+
- [useMutation](#usemutation)
|
|
13
|
+
- [usePagination](#usepagination)
|
|
14
|
+
- [useUpload](#useupload)
|
|
15
|
+
- [useAuth](#useauth)
|
|
16
|
+
- [Direct Client Usage](#direct-client-usage)
|
|
17
|
+
- [Authentication](#authentication)
|
|
18
|
+
- [File Storage](#file-storage)
|
|
19
|
+
- [Admin APIs](#admin-apis)
|
|
20
|
+
- [tRPC Integration](#trpc-integration)
|
|
21
|
+
- [Subpath Exports](#subpath-exports)
|
|
22
|
+
- [TypeScript Support](#typescript-support)
|
|
23
|
+
- [Error Handling](#error-handling)
|
|
24
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
6
27
|
|
|
7
28
|
```bash
|
|
8
29
|
npm install @archlast/client
|
|
30
|
+
|
|
31
|
+
# Or with other package managers
|
|
32
|
+
pnpm add @archlast/client
|
|
33
|
+
yarn add @archlast/client
|
|
34
|
+
bun add @archlast/client
|
|
9
35
|
```
|
|
10
36
|
|
|
11
|
-
Peer
|
|
37
|
+
### Peer Dependencies
|
|
38
|
+
|
|
39
|
+
Install required peer dependencies in your application:
|
|
12
40
|
|
|
13
41
|
```bash
|
|
14
42
|
npm install react react-dom @tanstack/react-query
|
|
15
43
|
```
|
|
16
44
|
|
|
17
|
-
|
|
45
|
+
### Optional Dependencies
|
|
46
|
+
|
|
47
|
+
For tRPC support:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install @trpc/client @trpc/react-query
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### 1. Create the Client
|
|
18
56
|
|
|
19
57
|
```ts
|
|
20
|
-
import { ArchlastClient
|
|
21
|
-
|
|
58
|
+
import { ArchlastClient } from "@archlast/client";
|
|
59
|
+
|
|
60
|
+
const client = new ArchlastClient(
|
|
61
|
+
"ws://localhost:4000/ws", // WebSocket URL
|
|
62
|
+
"http://localhost:4000", // HTTP URL
|
|
63
|
+
"web" // App ID
|
|
64
|
+
);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Set Up the Provider
|
|
22
68
|
|
|
23
|
-
|
|
69
|
+
```tsx
|
|
70
|
+
import { ArchlastProvider } from "@archlast/client/react";
|
|
24
71
|
|
|
25
72
|
function App() {
|
|
26
73
|
return (
|
|
27
74
|
<ArchlastProvider client={client}>
|
|
28
|
-
<
|
|
75
|
+
<YourApp />
|
|
29
76
|
</ArchlastProvider>
|
|
30
77
|
);
|
|
31
78
|
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Use Hooks in Components
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useQuery, useMutation } from "@archlast/client/react";
|
|
85
|
+
import { api } from "./_generated/api";
|
|
32
86
|
|
|
33
87
|
function TodoList() {
|
|
88
|
+
// Subscribe to live updates
|
|
89
|
+
const tasks = useQuery(api.tasks.list, {});
|
|
90
|
+
|
|
91
|
+
// Mutation with automatic cache invalidation
|
|
92
|
+
const createTask = useMutation(api.tasks.create);
|
|
93
|
+
|
|
94
|
+
const handleAdd = async () => {
|
|
95
|
+
await createTask({ text: "New task" });
|
|
96
|
+
// Cache automatically invalidated, UI updates reactively
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (!tasks) return <div>Loading...</div>;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div>
|
|
103
|
+
<ul>
|
|
104
|
+
{tasks.map(task => (
|
|
105
|
+
<li key={task._id}>{task.text}</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
<button onClick={handleAdd}>Add Task</button>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
> **Note:** The `api` object is auto-generated by the CLI in `_generated/api.ts`. Run `archlast dev` or `archlast build` to generate it.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Client Configuration
|
|
119
|
+
|
|
120
|
+
### Constructor Signature
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
new ArchlastClient(wsUrl, httpUrl, appId, authUrl?, options?)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Parameters
|
|
127
|
+
|
|
128
|
+
| Parameter | Type | Description |
|
|
129
|
+
|-----------|------|-------------|
|
|
130
|
+
| `wsUrl` | `string` | WebSocket endpoint (e.g., `ws://localhost:4000/ws`) |
|
|
131
|
+
| `httpUrl` | `string` | HTTP base URL (e.g., `http://localhost:4000`) |
|
|
132
|
+
| `appId` | `string` | Application identifier (e.g., `"web"`, `"mobile"`) |
|
|
133
|
+
| `authUrl` | `string?` | Optional custom auth URL (defaults to `httpUrl`) |
|
|
134
|
+
| `options` | `object?` | Additional configuration options |
|
|
135
|
+
|
|
136
|
+
### Options
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
interface ClientOptions {
|
|
140
|
+
// Auto-connect WebSocket on instantiation (default: true)
|
|
141
|
+
autoConnect?: boolean;
|
|
142
|
+
|
|
143
|
+
// API key for authentication (arch_ prefix)
|
|
144
|
+
apiKey?: string;
|
|
145
|
+
|
|
146
|
+
// Enable admin WebSocket subscriptions
|
|
147
|
+
isAdmin?: boolean;
|
|
148
|
+
|
|
149
|
+
// Better Auth session cookies for SSR
|
|
150
|
+
betterAuthCookies?: Record<string, string>;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Full Example
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const client = new ArchlastClient(
|
|
158
|
+
"wss://api.myapp.com/ws",
|
|
159
|
+
"https://api.myapp.com",
|
|
160
|
+
"web",
|
|
161
|
+
"https://auth.myapp.com", // Custom auth URL
|
|
162
|
+
{
|
|
163
|
+
autoConnect: true,
|
|
164
|
+
apiKey: "arch_your_api_key",
|
|
165
|
+
isAdmin: false,
|
|
166
|
+
betterAuthCookies: {
|
|
167
|
+
"better-auth.session": "session_token_here"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Runtime Credential Updates
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// Rotate API key
|
|
177
|
+
client.setApiKey("arch_new_api_key");
|
|
178
|
+
|
|
179
|
+
// Update session cookies (for SSR)
|
|
180
|
+
client.setBetterAuthCookies({
|
|
181
|
+
"better-auth.session": "new_session_token"
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## React Hooks
|
|
188
|
+
|
|
189
|
+
### useQuery
|
|
190
|
+
|
|
191
|
+
Subscribe to real-time query updates with automatic caching via TanStack Query.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { useQuery } from "@archlast/client/react";
|
|
195
|
+
import { api } from "./_generated/api";
|
|
196
|
+
|
|
197
|
+
function TaskList() {
|
|
198
|
+
// Basic usage
|
|
34
199
|
const tasks = useQuery(api.tasks.list, {});
|
|
35
|
-
|
|
200
|
+
|
|
201
|
+
// With arguments
|
|
202
|
+
const task = useQuery(api.tasks.get, { id: "123" });
|
|
203
|
+
|
|
204
|
+
// With options
|
|
205
|
+
const { data, isLoading, error, refetch } = useQuery(
|
|
206
|
+
api.tasks.list,
|
|
207
|
+
{ completed: false },
|
|
208
|
+
{
|
|
209
|
+
// TanStack Query options
|
|
210
|
+
staleTime: 5000,
|
|
211
|
+
refetchOnWindowFocus: true,
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (isLoading) return <Spinner />;
|
|
216
|
+
if (error) return <Error message={error.message} />;
|
|
217
|
+
|
|
218
|
+
return <TaskGrid tasks={data} />;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### useMutation
|
|
223
|
+
|
|
224
|
+
Execute mutations with automatic cache invalidation.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { useMutation } from "@archlast/client/react";
|
|
228
|
+
|
|
229
|
+
function CreateTaskForm() {
|
|
230
|
+
const createTask = useMutation(api.tasks.create);
|
|
231
|
+
|
|
232
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
const formData = new FormData(e.target as HTMLFormElement);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const task = await createTask({
|
|
238
|
+
text: formData.get("text") as string,
|
|
239
|
+
priority: "medium"
|
|
240
|
+
});
|
|
241
|
+
console.log("Created:", task._id);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error("Failed:", error);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<form onSubmit={handleSubmit}>
|
|
249
|
+
<input name="text" required />
|
|
250
|
+
<button type="submit">Create</button>
|
|
251
|
+
</form>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### usePagination
|
|
257
|
+
|
|
258
|
+
Handle cursor-based pagination with infinite scroll support.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { usePagination } from "@archlast/client/react";
|
|
262
|
+
|
|
263
|
+
function InfiniteTaskList() {
|
|
264
|
+
const {
|
|
265
|
+
items,
|
|
266
|
+
isLoading,
|
|
267
|
+
hasMore,
|
|
268
|
+
loadMore,
|
|
269
|
+
error
|
|
270
|
+
} = usePagination(api.tasks.listPaginated, {
|
|
271
|
+
limit: 20,
|
|
272
|
+
filter: { completed: false }
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div>
|
|
277
|
+
<ul>
|
|
278
|
+
{items.map(task => (
|
|
279
|
+
<li key={task._id}>{task.text}</li>
|
|
280
|
+
))}
|
|
281
|
+
</ul>
|
|
282
|
+
|
|
283
|
+
{hasMore && (
|
|
284
|
+
<button
|
|
285
|
+
onClick={loadMore}
|
|
286
|
+
disabled={isLoading}
|
|
287
|
+
>
|
|
288
|
+
{isLoading ? "Loading..." : "Load More"}
|
|
289
|
+
</button>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Server-side query must return:**
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
interface PaginatedResult<T> {
|
|
300
|
+
items: T[];
|
|
301
|
+
continueCursor: string | null;
|
|
302
|
+
isDone: boolean;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### useUpload
|
|
307
|
+
|
|
308
|
+
Upload files with progress tracking.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { useUpload } from "@archlast/client/react";
|
|
312
|
+
|
|
313
|
+
function FileUploader() {
|
|
314
|
+
const {
|
|
315
|
+
upload,
|
|
316
|
+
progress,
|
|
317
|
+
isUploading,
|
|
318
|
+
storageId,
|
|
319
|
+
error
|
|
320
|
+
} = useUpload();
|
|
321
|
+
|
|
322
|
+
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
|
323
|
+
const file = e.target.files?.[0];
|
|
324
|
+
if (!file) return;
|
|
325
|
+
|
|
326
|
+
const id = await upload(file, "images");
|
|
327
|
+
console.log("Uploaded with ID:", id);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div>
|
|
332
|
+
<input type="file" onChange={handleFileChange} disabled={isUploading} />
|
|
333
|
+
|
|
334
|
+
{isUploading && (
|
|
335
|
+
<div>
|
|
336
|
+
<progress value={progress} max={100} />
|
|
337
|
+
<span>{progress}%</span>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
{storageId && <p>Uploaded: {storageId}</p>}
|
|
342
|
+
{error && <p className="error">{error.message}</p>}
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### useAuth
|
|
349
|
+
|
|
350
|
+
Authentication state and actions.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { useAuth } from "@archlast/client/react";
|
|
354
|
+
|
|
355
|
+
function AuthButton() {
|
|
356
|
+
const {
|
|
357
|
+
signIn,
|
|
358
|
+
signOut,
|
|
359
|
+
isAuthenticated,
|
|
360
|
+
user,
|
|
361
|
+
isLoading
|
|
362
|
+
} = useAuth();
|
|
363
|
+
|
|
364
|
+
if (isLoading) return <Spinner />;
|
|
365
|
+
|
|
366
|
+
if (isAuthenticated) {
|
|
367
|
+
return (
|
|
368
|
+
<div>
|
|
369
|
+
<span>Welcome, {user?.name}</span>
|
|
370
|
+
<button onClick={signOut}>Sign Out</button>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<button onClick={() => signIn({ provider: "google" })}>
|
|
377
|
+
Sign in with Google
|
|
378
|
+
</button>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Direct Client Usage
|
|
386
|
+
|
|
387
|
+
Use the client directly without React for Node.js, scripts, or non-React frameworks.
|
|
388
|
+
|
|
389
|
+
### Fetch (Query)
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
// Fetch a single query
|
|
393
|
+
const tasks = await client.fetch("tasks.list", {});
|
|
394
|
+
|
|
395
|
+
// With arguments
|
|
396
|
+
const task = await client.fetch("tasks.get", { id: "123" });
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Mutate
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
// Execute a mutation
|
|
403
|
+
const created = await client.mutate("tasks.create", {
|
|
404
|
+
text: "New task",
|
|
405
|
+
priority: "high"
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Update
|
|
409
|
+
await client.mutate("tasks.update", {
|
|
410
|
+
id: "123",
|
|
411
|
+
completed: true
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Delete
|
|
415
|
+
await client.mutate("tasks.delete", { id: "123" });
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Subscribe
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
// Manual WebSocket subscription
|
|
422
|
+
const unsubscribe = client.subscribe("tasks.list", {}, (data) => {
|
|
423
|
+
console.log("Tasks updated:", data);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Later: unsubscribe
|
|
427
|
+
unsubscribe();
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Authentication
|
|
433
|
+
|
|
434
|
+
### Using the Auth Client
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { ArchlastAuthClient } from "@archlast/client/auth";
|
|
438
|
+
|
|
439
|
+
const auth = new ArchlastAuthClient({
|
|
440
|
+
baseUrl: "http://localhost:4000"
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Email/password sign up
|
|
444
|
+
await auth.signUp({
|
|
445
|
+
email: "user@example.com",
|
|
446
|
+
password: "secure_password",
|
|
447
|
+
name: "John Doe"
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Email/password sign in
|
|
451
|
+
await auth.signIn({
|
|
452
|
+
email: "user@example.com",
|
|
453
|
+
password: "secure_password"
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// OAuth sign in
|
|
457
|
+
await auth.signIn({ provider: "google" });
|
|
458
|
+
await auth.signIn({ provider: "github" });
|
|
459
|
+
|
|
460
|
+
// Get current session
|
|
461
|
+
const session = await auth.getSession();
|
|
462
|
+
|
|
463
|
+
// Sign out
|
|
464
|
+
await auth.signOut();
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Auth State in Components
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
import { useAuth } from "@archlast/client/react";
|
|
471
|
+
|
|
472
|
+
function ProtectedRoute({ children }) {
|
|
473
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
474
|
+
|
|
475
|
+
if (isLoading) return <LoadingScreen />;
|
|
476
|
+
if (!isAuthenticated) return <Navigate to="/login" />;
|
|
477
|
+
|
|
478
|
+
return children;
|
|
36
479
|
}
|
|
37
480
|
```
|
|
38
481
|
|
|
39
|
-
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## File Storage
|
|
485
|
+
|
|
486
|
+
### Storage Client
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
// Access via main client
|
|
490
|
+
const storage = client.storage;
|
|
491
|
+
|
|
492
|
+
// Or import directly
|
|
493
|
+
import { StorageClient } from "@archlast/client/storage";
|
|
494
|
+
const storage = new StorageClient(httpUrl, getAuthHeaders);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Upload Files
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
const file = new File(["Hello, World!"], "hello.txt", { type: "text/plain" });
|
|
501
|
+
|
|
502
|
+
// Basic upload
|
|
503
|
+
const result = await client.storage.upload(file);
|
|
504
|
+
console.log("Storage ID:", result.id);
|
|
505
|
+
console.log("URL:", result.url);
|
|
506
|
+
|
|
507
|
+
// Upload to specific bucket
|
|
508
|
+
const imageResult = await client.storage.upload(imageFile, "images");
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### List Files
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
// List all files
|
|
515
|
+
const files = await client.storage.list();
|
|
516
|
+
|
|
517
|
+
// List files in bucket
|
|
518
|
+
const images = await client.storage.list("images");
|
|
519
|
+
|
|
520
|
+
// With pagination
|
|
521
|
+
const page1 = await client.storage.list("images", { limit: 20 });
|
|
522
|
+
const page2 = await client.storage.list("images", {
|
|
523
|
+
limit: 20,
|
|
524
|
+
cursor: page1.nextCursor
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Get Presigned URLs
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
// Get temporary signed URL for download
|
|
532
|
+
const { url, expiresAt } = await client.storage.presign(storageId);
|
|
533
|
+
|
|
534
|
+
// Use in img tag
|
|
535
|
+
<img src={url} alt="Uploaded file" />
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Get Metadata
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
const metadata = await client.storage.getMetadata(storageId);
|
|
542
|
+
console.log(metadata.filename); // "hello.txt"
|
|
543
|
+
console.log(metadata.contentType); // "text/plain"
|
|
544
|
+
console.log(metadata.size); // 13
|
|
545
|
+
console.log(metadata.createdAt); // Date
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Admin APIs
|
|
551
|
+
|
|
552
|
+
Access admin functionality (requires admin privileges).
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
// Auth operations
|
|
556
|
+
const profile = await client.admin.auth.getProfile();
|
|
557
|
+
const users = await client.admin.auth.listUsers();
|
|
558
|
+
await client.admin.auth.updateUser(userId, { role: "admin" });
|
|
559
|
+
|
|
560
|
+
// API key management
|
|
561
|
+
const keys = await client.admin.apiKeys.list();
|
|
562
|
+
const newKey = await client.admin.apiKeys.create({ name: "CI/CD" });
|
|
563
|
+
await client.admin.apiKeys.revoke(keyId);
|
|
564
|
+
|
|
565
|
+
// Data operations
|
|
566
|
+
const collections = await client.admin.data.listCollections();
|
|
567
|
+
const docs = await client.admin.data.query("tasks", { limit: 100 });
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## tRPC Integration
|
|
40
573
|
|
|
41
|
-
-
|
|
42
|
-
- `@archlast/client/trpc`
|
|
43
|
-
- `@archlast/client/auth`
|
|
44
|
-
- `@archlast/client/storage`
|
|
45
|
-
- `@archlast/client/admin`
|
|
574
|
+
For type-safe RPC with full TypeScript inference.
|
|
46
575
|
|
|
47
|
-
|
|
576
|
+
### Setup
|
|
48
577
|
|
|
49
578
|
```ts
|
|
50
579
|
import { createArchlastTRPCClient } from "@archlast/client/trpc";
|
|
@@ -52,9 +581,246 @@ import type { AppRouter } from "./_generated/rpc";
|
|
|
52
581
|
|
|
53
582
|
const trpc = createArchlastTRPCClient<AppRouter>({
|
|
54
583
|
baseUrl: "http://localhost:4000",
|
|
584
|
+
apiKey: "arch_your_api_key"
|
|
585
|
+
});
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Usage
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
// Queries
|
|
592
|
+
const tasks = await trpc.tasks.list.query({});
|
|
593
|
+
const task = await trpc.tasks.get.query({ id: "123" });
|
|
594
|
+
|
|
595
|
+
// Mutations
|
|
596
|
+
const created = await trpc.tasks.create.mutate({ text: "New task" });
|
|
597
|
+
await trpc.tasks.update.mutate({ id: "123", completed: true });
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### React Query Integration
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
import { createTRPCReact } from "@trpc/react-query";
|
|
604
|
+
import type { AppRouter } from "./_generated/rpc";
|
|
605
|
+
|
|
606
|
+
export const trpc = createTRPCReact<AppRouter>();
|
|
607
|
+
|
|
608
|
+
// In component
|
|
609
|
+
function TaskList() {
|
|
610
|
+
const { data, isLoading } = trpc.tasks.list.useQuery({});
|
|
611
|
+
const createTask = trpc.tasks.create.useMutation();
|
|
612
|
+
|
|
613
|
+
// ...
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## Subpath Exports
|
|
620
|
+
|
|
621
|
+
Import specific functionality to reduce bundle size:
|
|
622
|
+
|
|
623
|
+
| Import | Description |
|
|
624
|
+
|--------|-------------|
|
|
625
|
+
| `@archlast/client` | Core `ArchlastClient` class |
|
|
626
|
+
| `@archlast/client/react` | React hooks and provider |
|
|
627
|
+
| `@archlast/client/auth` | Authentication client |
|
|
628
|
+
| `@archlast/client/storage` | File storage client |
|
|
629
|
+
| `@archlast/client/admin` | Admin API client |
|
|
630
|
+
| `@archlast/client/trpc` | tRPC client factory |
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
// Minimal import for core functionality
|
|
634
|
+
import { ArchlastClient } from "@archlast/client";
|
|
635
|
+
|
|
636
|
+
// React-specific imports
|
|
637
|
+
import {
|
|
638
|
+
ArchlastProvider,
|
|
639
|
+
useQuery,
|
|
640
|
+
useMutation,
|
|
641
|
+
usePagination,
|
|
642
|
+
useUpload,
|
|
643
|
+
useAuth
|
|
644
|
+
} from "@archlast/client/react";
|
|
645
|
+
|
|
646
|
+
// Auth-only import
|
|
647
|
+
import { ArchlastAuthClient } from "@archlast/client/auth";
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## TypeScript Support
|
|
653
|
+
|
|
654
|
+
The client is fully typed. Types are generated by the CLI based on your schema.
|
|
655
|
+
|
|
656
|
+
### Generated Types
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
// _generated/api.ts
|
|
660
|
+
export const api = {
|
|
661
|
+
tasks: {
|
|
662
|
+
list: { _path: "tasks.list", _returnType: {} as Task[] },
|
|
663
|
+
get: { _path: "tasks.get", _returnType: {} as Task | null },
|
|
664
|
+
create: { _path: "tasks.create", _returnType: {} as Task },
|
|
665
|
+
// ...
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Usage with Types
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
import { useQuery } from "@archlast/client/react";
|
|
674
|
+
import { api } from "./_generated/api";
|
|
675
|
+
|
|
676
|
+
// TypeScript knows `tasks` is Task[]
|
|
677
|
+
const tasks = useQuery(api.tasks.list, {});
|
|
678
|
+
|
|
679
|
+
// TypeScript knows `task` is Task | null
|
|
680
|
+
const task = useQuery(api.tasks.get, { id: "123" });
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## Error Handling
|
|
686
|
+
|
|
687
|
+
### Query Errors
|
|
688
|
+
|
|
689
|
+
```tsx
|
|
690
|
+
function TaskList() {
|
|
691
|
+
const { data, error, isError } = useQuery(api.tasks.list, {});
|
|
692
|
+
|
|
693
|
+
if (isError) {
|
|
694
|
+
return (
|
|
695
|
+
<div className="error">
|
|
696
|
+
<h3>Failed to load tasks</h3>
|
|
697
|
+
<p>{error.message}</p>
|
|
698
|
+
<button onClick={() => refetch()}>Retry</button>
|
|
699
|
+
</div>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return <TaskGrid tasks={data} />;
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Mutation Errors
|
|
708
|
+
|
|
709
|
+
```ts
|
|
710
|
+
const createTask = useMutation(api.tasks.create);
|
|
711
|
+
|
|
712
|
+
try {
|
|
713
|
+
await createTask({ text: "" }); // Invalid
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (error instanceof ValidationError) {
|
|
716
|
+
console.log("Validation failed:", error.issues);
|
|
717
|
+
} else if (error instanceof AuthenticationError) {
|
|
718
|
+
console.log("Not authenticated");
|
|
719
|
+
} else {
|
|
720
|
+
console.log("Unknown error:", error);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### Global Error Boundary
|
|
726
|
+
|
|
727
|
+
```tsx
|
|
728
|
+
import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
|
729
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
730
|
+
|
|
731
|
+
function App() {
|
|
732
|
+
return (
|
|
733
|
+
<QueryErrorResetBoundary>
|
|
734
|
+
{({ reset }) => (
|
|
735
|
+
<ErrorBoundary
|
|
736
|
+
onReset={reset}
|
|
737
|
+
fallbackRender={({ error, resetErrorBoundary }) => (
|
|
738
|
+
<div>
|
|
739
|
+
<p>Something went wrong: {error.message}</p>
|
|
740
|
+
<button onClick={resetErrorBoundary}>Try again</button>
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
>
|
|
744
|
+
<ArchlastProvider client={client}>
|
|
745
|
+
<YourApp />
|
|
746
|
+
</ArchlastProvider>
|
|
747
|
+
</ErrorBoundary>
|
|
748
|
+
)}
|
|
749
|
+
</QueryErrorResetBoundary>
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## Advanced Patterns
|
|
757
|
+
|
|
758
|
+
### Optimistic Updates
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
const queryClient = useQueryClient();
|
|
762
|
+
|
|
763
|
+
const updateTask = useMutation(api.tasks.update, {
|
|
764
|
+
onMutate: async (newData) => {
|
|
765
|
+
// Cancel outgoing refetches
|
|
766
|
+
await queryClient.cancelQueries(["tasks.list"]);
|
|
767
|
+
|
|
768
|
+
// Snapshot previous value
|
|
769
|
+
const previous = queryClient.getQueryData(["tasks.list"]);
|
|
770
|
+
|
|
771
|
+
// Optimistically update
|
|
772
|
+
queryClient.setQueryData(["tasks.list"], (old: Task[]) =>
|
|
773
|
+
old.map(t => t._id === newData.id ? { ...t, ...newData } : t)
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
return { previous };
|
|
777
|
+
},
|
|
778
|
+
onError: (err, newData, context) => {
|
|
779
|
+
// Rollback on error
|
|
780
|
+
queryClient.setQueryData(["tasks.list"], context.previous);
|
|
781
|
+
}
|
|
55
782
|
});
|
|
56
783
|
```
|
|
57
784
|
|
|
58
|
-
|
|
785
|
+
### SSR / Server Components
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
// For Next.js SSR
|
|
789
|
+
const client = new ArchlastClient(
|
|
790
|
+
wsUrl,
|
|
791
|
+
httpUrl,
|
|
792
|
+
"web",
|
|
793
|
+
undefined,
|
|
794
|
+
{
|
|
795
|
+
autoConnect: false, // Don't connect on server
|
|
796
|
+
betterAuthCookies: cookies() // Pass request cookies
|
|
797
|
+
}
|
|
798
|
+
);
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### Multiple Clients
|
|
802
|
+
|
|
803
|
+
```tsx
|
|
804
|
+
const mainClient = new ArchlastClient(mainWsUrl, mainHttpUrl, "main");
|
|
805
|
+
const analyticsClient = new ArchlastClient(analyticsWsUrl, analyticsHttpUrl, "analytics");
|
|
806
|
+
|
|
807
|
+
function App() {
|
|
808
|
+
return (
|
|
809
|
+
<ArchlastProvider client={mainClient}>
|
|
810
|
+
<ArchlastProvider client={analyticsClient} contextKey="analytics">
|
|
811
|
+
<YourApp />
|
|
812
|
+
</ArchlastProvider>
|
|
813
|
+
</ArchlastProvider>
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## Publishing (Maintainers)
|
|
59
821
|
|
|
60
822
|
See `docs/npm-publishing.md` for release and publish steps.
|
|
823
|
+
|
|
824
|
+
## License
|
|
825
|
+
|
|
826
|
+
MIT
|