@archlast/client 0.0.1 → 0.1.1
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 +918 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,50 +1,713 @@
|
|
|
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
|
-
import { api } from "./_generated/api";
|
|
58
|
+
import { ArchlastClient } from "@archlast/client";
|
|
22
59
|
|
|
23
|
-
const client = new ArchlastClient(
|
|
60
|
+
const client = new ArchlastClient(
|
|
61
|
+
"ws://localhost:4000/ws", // WebSocket URL
|
|
62
|
+
"http://localhost:4000", // HTTP URL
|
|
63
|
+
"web", // App ID
|
|
64
|
+
undefined, // Optional: auth URL (for same-origin proxy)
|
|
65
|
+
{
|
|
66
|
+
autoConnect: true, // Auto-connect WebSocket (default: true)
|
|
67
|
+
isAdmin: false, // Admin mode (default: false)
|
|
68
|
+
apiKey: "arch_key", // Optional: Better-Auth API key
|
|
69
|
+
betterAuthCookies: {}, // Optional: Better-Auth session cookies
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. Set Up the Provider
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { ArchlastProvider } from "@archlast/client/react";
|
|
24
78
|
|
|
25
79
|
function App() {
|
|
26
80
|
return (
|
|
27
81
|
<ArchlastProvider client={client}>
|
|
28
|
-
<
|
|
82
|
+
<YourApp />
|
|
29
83
|
</ArchlastProvider>
|
|
30
84
|
);
|
|
31
85
|
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. Use Hooks in Components
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { useQuery, useMutation } from "@archlast/client/react";
|
|
92
|
+
import { api } from "./_generated/api";
|
|
32
93
|
|
|
33
94
|
function TodoList() {
|
|
95
|
+
// Subscribe to live updates
|
|
96
|
+
const tasks = useQuery(api.tasks.list, {});
|
|
97
|
+
|
|
98
|
+
// Mutation with automatic cache invalidation
|
|
99
|
+
const createTask = useMutation(api.tasks.create);
|
|
100
|
+
|
|
101
|
+
const handleAdd = async () => {
|
|
102
|
+
await createTask({ text: "New task" });
|
|
103
|
+
// Cache automatically invalidated, UI updates reactively
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (!tasks) return <div>Loading...</div>;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<ul>
|
|
111
|
+
{tasks.map(task => (
|
|
112
|
+
<li key={task._id}>{task.text}</li>
|
|
113
|
+
))}
|
|
114
|
+
</ul>
|
|
115
|
+
<button onClick={handleAdd}>Add Task</button>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
> **Note:** The `api` object is auto-generated by the CLI in `_generated/api.ts`. Run `archlast dev` or `archlast build` to generate it.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Client Configuration
|
|
126
|
+
|
|
127
|
+
### Constructor Signature
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
new ArchlastClient(wsUrl, httpUrl, appId, authUrl?, options?)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Parameters
|
|
134
|
+
|
|
135
|
+
| Parameter | Type | Description |
|
|
136
|
+
|-----------|------|-------------|
|
|
137
|
+
| `wsUrl` | `string` | WebSocket endpoint (e.g., `ws://localhost:4000/ws`) |
|
|
138
|
+
| `httpUrl` | `string` | HTTP base URL (e.g., `http://localhost:4000`) |
|
|
139
|
+
| `appId` | `string` | Application identifier (e.g., `"web"`, `"mobile"`) |
|
|
140
|
+
| `authUrl` | `string?` | Optional custom auth URL (defaults to `httpUrl`) |
|
|
141
|
+
| `options` | `object?` | Additional configuration options |
|
|
142
|
+
|
|
143
|
+
### Options
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
interface ClientOptions {
|
|
147
|
+
// Auto-connect WebSocket on instantiation (default: true)
|
|
148
|
+
autoConnect?: boolean;
|
|
149
|
+
|
|
150
|
+
// API key for authentication (arch_ prefix)
|
|
151
|
+
apiKey?: string;
|
|
152
|
+
|
|
153
|
+
// Enable admin WebSocket subscriptions
|
|
154
|
+
isAdmin?: boolean;
|
|
155
|
+
|
|
156
|
+
// Better Auth session cookies for SSR
|
|
157
|
+
betterAuthCookies?: Record<string, string>;
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Full Example
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const client = new ArchlastClient(
|
|
165
|
+
"wss://api.myapp.com/ws",
|
|
166
|
+
"https://api.myapp.com",
|
|
167
|
+
"web",
|
|
168
|
+
"https://auth.myapp.com", // Custom auth URL
|
|
169
|
+
{
|
|
170
|
+
autoConnect: true,
|
|
171
|
+
apiKey: "arch_your_api_key",
|
|
172
|
+
isAdmin: false,
|
|
173
|
+
betterAuthCookies: {
|
|
174
|
+
"better-auth.session": "session_token_here"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Runtime Credential Updates
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// Rotate API key
|
|
184
|
+
client.setApiKey("arch_new_api_key");
|
|
185
|
+
|
|
186
|
+
// Update session cookies (for SSR)
|
|
187
|
+
client.setBetterAuthCookies({
|
|
188
|
+
"better-auth.session": "new_session_token"
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## React Hooks
|
|
195
|
+
|
|
196
|
+
### useQuery
|
|
197
|
+
|
|
198
|
+
Subscribe to real-time query updates with automatic caching via TanStack Query.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { useQuery } from "@archlast/client/react";
|
|
202
|
+
import { api } from "./_generated/api";
|
|
203
|
+
|
|
204
|
+
function TaskList() {
|
|
205
|
+
// Basic usage
|
|
34
206
|
const tasks = useQuery(api.tasks.list, {});
|
|
35
|
-
|
|
207
|
+
|
|
208
|
+
// With arguments
|
|
209
|
+
const task = useQuery(api.tasks.get, { id: "123" });
|
|
210
|
+
|
|
211
|
+
// With options
|
|
212
|
+
const { data, isLoading, error, refetch } = useQuery(
|
|
213
|
+
api.tasks.list,
|
|
214
|
+
{ completed: false },
|
|
215
|
+
{
|
|
216
|
+
// TanStack Query options
|
|
217
|
+
staleTime: 5000,
|
|
218
|
+
refetchOnWindowFocus: true,
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (isLoading) return <Spinner />;
|
|
223
|
+
if (error) return <Error message={error.message} />;
|
|
224
|
+
|
|
225
|
+
return <TaskGrid tasks={data} />;
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### useMutation
|
|
230
|
+
|
|
231
|
+
Execute mutations with automatic cache invalidation.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { useMutation } from "@archlast/client/react";
|
|
235
|
+
|
|
236
|
+
function CreateTaskForm() {
|
|
237
|
+
const createTask = useMutation(api.tasks.create);
|
|
238
|
+
|
|
239
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
const formData = new FormData(e.target as HTMLFormElement);
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const task = await createTask({
|
|
245
|
+
text: formData.get("text") as string,
|
|
246
|
+
priority: "medium"
|
|
247
|
+
});
|
|
248
|
+
console.log("Created:", task._id);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error("Failed:", error);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<form onSubmit={handleSubmit}>
|
|
256
|
+
<input name="text" required />
|
|
257
|
+
<button type="submit">Create</button>
|
|
258
|
+
</form>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### usePagination
|
|
264
|
+
|
|
265
|
+
Handle cursor-based pagination with infinite scroll support.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { usePagination } from "@archlast/client/react";
|
|
269
|
+
|
|
270
|
+
function InfiniteTaskList() {
|
|
271
|
+
const {
|
|
272
|
+
items, // Accumulated items across all pages
|
|
273
|
+
cursor, // Current cursor for next page
|
|
274
|
+
isDone, // Whether pagination is complete
|
|
275
|
+
hasMore, // Convenience flag: !isDone && cursor exists
|
|
276
|
+
isLoading, // Loading state
|
|
277
|
+
loadMore, // Function to load next page
|
|
278
|
+
refresh, // Function to reset and reload from first page
|
|
279
|
+
page, // Optional: current page number (if server provides)
|
|
280
|
+
pageSize, // Optional: page size (if server provides)
|
|
281
|
+
total, // Optional: total count (if server provides)
|
|
282
|
+
} = usePagination(
|
|
283
|
+
api.tasks.listPaginated,
|
|
284
|
+
{ completed: false }, // Base args (no cursor/limit)
|
|
285
|
+
{ limit: 20 } // Options
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div>
|
|
290
|
+
<ul>
|
|
291
|
+
{items.map(task => (
|
|
292
|
+
<li key={task._id}>{task.text}</li>
|
|
293
|
+
))}
|
|
294
|
+
</ul>
|
|
295
|
+
|
|
296
|
+
{hasMore && (
|
|
297
|
+
<button
|
|
298
|
+
onClick={loadMore}
|
|
299
|
+
disabled={isLoading}
|
|
300
|
+
>
|
|
301
|
+
{isLoading ? "Loading..." : "Load More"}
|
|
302
|
+
</button>
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
<button onClick={refresh}>Refresh</button>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Server-side query must return:**
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
interface PaginatedResponse<T> {
|
|
315
|
+
items: T[];
|
|
316
|
+
continueCursor: string | null;
|
|
317
|
+
isDone: boolean;
|
|
318
|
+
page?: number; // Optional
|
|
319
|
+
pageSize?: number; // Optional
|
|
320
|
+
total?: number; // Optional
|
|
321
|
+
}
|
|
322
|
+
continueCursor: string | null;
|
|
323
|
+
isDone: boolean;
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### useUpload
|
|
328
|
+
|
|
329
|
+
Upload files with progress tracking.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import { useUpload } from "@archlast/client/react";
|
|
333
|
+
|
|
334
|
+
function FileUploader() {
|
|
335
|
+
const {
|
|
336
|
+
upload,
|
|
337
|
+
progress,
|
|
338
|
+
isUploading,
|
|
339
|
+
storageId,
|
|
340
|
+
error
|
|
341
|
+
} = useUpload();
|
|
342
|
+
|
|
343
|
+
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
|
344
|
+
const file = e.target.files?.[0];
|
|
345
|
+
if (!file) return;
|
|
346
|
+
|
|
347
|
+
const id = await upload(file, "images");
|
|
348
|
+
console.log("Uploaded with ID:", id);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div>
|
|
353
|
+
<input type="file" onChange={handleFileChange} disabled={isUploading} />
|
|
354
|
+
|
|
355
|
+
{isUploading && (
|
|
356
|
+
<div>
|
|
357
|
+
<progress value={progress} max={100} />
|
|
358
|
+
<span>{progress}%</span>
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
{storageId && <p>Uploaded: {storageId}</p>}
|
|
363
|
+
{error && <p className="error">{error.message}</p>}
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### useAuth
|
|
370
|
+
|
|
371
|
+
Authentication state and actions via the `ArchlastAuthClient` wrapper.
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { useAuth } from "@archlast/client/react";
|
|
375
|
+
|
|
376
|
+
function AuthButton() {
|
|
377
|
+
const {
|
|
378
|
+
isAuthenticated,
|
|
379
|
+
user,
|
|
380
|
+
isLoading,
|
|
381
|
+
signIn,
|
|
382
|
+
signOut
|
|
383
|
+
} = useAuth();
|
|
384
|
+
|
|
385
|
+
if (isLoading) return <Spinner />;
|
|
386
|
+
|
|
387
|
+
if (isAuthenticated) {
|
|
388
|
+
return (
|
|
389
|
+
<div>
|
|
390
|
+
<span>Welcome, {user?.name}</span>
|
|
391
|
+
<button onClick={() => signOut()}>
|
|
392
|
+
Sign Out
|
|
393
|
+
</button>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<button onClick={() => signIn({ email: "user@example.com", password: "..." })}>
|
|
400
|
+
Sign In
|
|
401
|
+
</button>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Using Better-Auth React Hooks
|
|
407
|
+
|
|
408
|
+
For full-featured auth in React, use Better-Auth's `useSession` hook:
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
import { authClient } from "./lib/better-auth"; // Your Better-Auth client
|
|
412
|
+
|
|
413
|
+
function AuthButton() {
|
|
414
|
+
const { data: session, isPending } = authClient.useSession();
|
|
415
|
+
|
|
416
|
+
if (isPending) return <Spinner />;
|
|
417
|
+
|
|
418
|
+
if (session?.user) {
|
|
419
|
+
return (
|
|
420
|
+
<div>
|
|
421
|
+
<span>Welcome, {session.user.name}</span>
|
|
422
|
+
<button onClick={() => authClient.signOut()}>
|
|
423
|
+
Sign Out
|
|
424
|
+
</button>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<button onClick={() => authClient.signIn.email({ email, password })}>
|
|
431
|
+
Sign In
|
|
432
|
+
</button>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Direct Client Usage
|
|
440
|
+
|
|
441
|
+
Use the client directly without React for Node.js, scripts, or non-React frameworks.
|
|
442
|
+
|
|
443
|
+
### Fetch (Query)
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
// Fetch a single query
|
|
447
|
+
const tasks = await client.fetch("tasks.list", {});
|
|
448
|
+
|
|
449
|
+
// With arguments
|
|
450
|
+
const task = await client.fetch("tasks.get", { id: "123" });
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Mutate
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
// Execute a mutation
|
|
457
|
+
const created = await client.mutate("tasks.create", {
|
|
458
|
+
text: "New task",
|
|
459
|
+
priority: "high"
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Update
|
|
463
|
+
await client.mutate("tasks.update", {
|
|
464
|
+
id: "123",
|
|
465
|
+
completed: true
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Delete
|
|
469
|
+
await client.mutate("tasks.delete", { id: "123" });
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Subscribe
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
// Manual WebSocket subscription
|
|
476
|
+
const unsubscribe = client.subscribe("tasks.list", {}, (data) => {
|
|
477
|
+
console.log("Tasks updated:", data);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Later: unsubscribe
|
|
481
|
+
unsubscribe();
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Authentication
|
|
487
|
+
|
|
488
|
+
Archlast uses [Better-Auth](https://www.better-auth.com/) for authentication. The server exposes Better-Auth endpoints at `/api/auth/*`.
|
|
489
|
+
|
|
490
|
+
### Using the Auth Client
|
|
491
|
+
|
|
492
|
+
The `ArchlastAuthClient` provides a wrapper around Better-Auth endpoints:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
import { ArchlastAuthClient } from "@archlast/client/auth";
|
|
496
|
+
|
|
497
|
+
const auth = new ArchlastAuthClient({
|
|
498
|
+
baseUrl: "http://localhost:4000",
|
|
499
|
+
apiKey: "arch_your_api_key" // Optional: for programmatic access
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Sign up with email/password
|
|
503
|
+
// POST /api/auth/sign-up/email
|
|
504
|
+
await auth.signUp({
|
|
505
|
+
email: "user@example.com",
|
|
506
|
+
password: "secure_password",
|
|
507
|
+
name: "John Doe"
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Sign in with email/password
|
|
511
|
+
// POST /api/auth/sign-in/email
|
|
512
|
+
await auth.signIn({
|
|
513
|
+
email: "user@example.com",
|
|
514
|
+
password: "secure_password"
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Sign in with username (if username plugin enabled)
|
|
518
|
+
await auth.signIn({
|
|
519
|
+
username: "johndoe",
|
|
520
|
+
password: "secure_password"
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Get current session
|
|
524
|
+
// GET /api/auth/get-session
|
|
525
|
+
const state = await auth.getState();
|
|
526
|
+
console.log(state.isAuthenticated, state.user);
|
|
527
|
+
|
|
528
|
+
// Sign out
|
|
529
|
+
await auth.signOut();
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Using Better-Auth Client Directly
|
|
533
|
+
|
|
534
|
+
For full Better-Auth features (OAuth, organization, admin), use the Better-Auth client directly:
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
import { createAuthClient } from "better-auth/react";
|
|
538
|
+
import {
|
|
539
|
+
adminClient,
|
|
540
|
+
usernameClient,
|
|
541
|
+
organizationClient,
|
|
542
|
+
// Optional plugins (add as needed):
|
|
543
|
+
// anonymousClient, // For guest users
|
|
544
|
+
// apiKeyClient, // For API key management
|
|
545
|
+
} from "better-auth/client/plugins";
|
|
546
|
+
|
|
547
|
+
const authClient = createAuthClient({
|
|
548
|
+
baseURL: "http://localhost:4000/api/auth",
|
|
549
|
+
fetchOptions: { credentials: "include" },
|
|
550
|
+
plugins: [
|
|
551
|
+
adminClient(),
|
|
552
|
+
usernameClient(),
|
|
553
|
+
organizationClient(),
|
|
554
|
+
],
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Email sign in (typical for user-facing apps)
|
|
558
|
+
await authClient.signIn.email({
|
|
559
|
+
email: "user@example.com",
|
|
560
|
+
password: "secure_password",
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Username sign in (used by dashboard)
|
|
564
|
+
await authClient.signIn.username({
|
|
565
|
+
username: "admin",
|
|
566
|
+
password: "secure_password",
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Session management
|
|
570
|
+
const { data: session } = authClient.useSession();
|
|
571
|
+
|
|
572
|
+
// Organization management
|
|
573
|
+
await authClient.organization.create({ name: "My Team" });
|
|
574
|
+
|
|
575
|
+
// Admin operations (requires admin role)
|
|
576
|
+
await authClient.admin.listUsers({ limit: 10 });
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Auth State in Components
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
582
|
+
import { useAuth } from "@archlast/client/react";
|
|
583
|
+
|
|
584
|
+
function ProtectedRoute({ children }) {
|
|
585
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
586
|
+
|
|
587
|
+
if (isLoading) return <LoadingScreen />;
|
|
588
|
+
if (!isAuthenticated) return <Navigate to="/login" />;
|
|
589
|
+
|
|
590
|
+
return children;
|
|
36
591
|
}
|
|
37
592
|
```
|
|
38
593
|
|
|
39
|
-
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## File Storage
|
|
597
|
+
|
|
598
|
+
### Storage Client
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
// Access via main client
|
|
602
|
+
const storage = client.storage;
|
|
603
|
+
|
|
604
|
+
// Or import directly
|
|
605
|
+
import { StorageClient } from "@archlast/client/storage";
|
|
606
|
+
const storage = new StorageClient(httpUrl, getAuthHeaders);
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Upload Files
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
const file = new File(["Hello, World!"], "hello.txt", { type: "text/plain" });
|
|
613
|
+
|
|
614
|
+
// Basic upload
|
|
615
|
+
const result = await client.storage.upload(file);
|
|
616
|
+
console.log("Storage ID:", result.id);
|
|
617
|
+
console.log("URL:", result.url);
|
|
618
|
+
|
|
619
|
+
// Upload with explicit content type
|
|
620
|
+
const blob = new Blob(["data"], { type: "application/octet-stream" });
|
|
621
|
+
const binaryResult = await client.storage.upload(blob, "application/octet-stream");
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### List Files
|
|
625
|
+
|
|
626
|
+
### List Files
|
|
40
627
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
628
|
+
```ts
|
|
629
|
+
// List files with pagination
|
|
630
|
+
const { items } = await client.storage.list(20, 0); // limit, offset
|
|
631
|
+
|
|
632
|
+
// Next page
|
|
633
|
+
const page2 = await client.storage.list(20, 20);
|
|
634
|
+
|
|
635
|
+
// Items structure
|
|
636
|
+
items.forEach(file => {
|
|
637
|
+
console.log(file.id); // Storage ID
|
|
638
|
+
console.log(file.fileName); // Original filename
|
|
639
|
+
console.log(file.contentType); // MIME type
|
|
640
|
+
console.log(file.size); // Size in bytes
|
|
641
|
+
console.log(file.url); // Public URL
|
|
642
|
+
});
|
|
643
|
+
```
|
|
46
644
|
|
|
47
|
-
|
|
645
|
+
### Get Presigned URLs
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
// Get temporary signed URL for download
|
|
649
|
+
const { url, expiresAt } = await client.storage.presign(storageId);
|
|
650
|
+
|
|
651
|
+
// Use in img tag
|
|
652
|
+
<img src={url} alt="Uploaded file" />
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Get Metadata
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
const metadata = await client.storage.getMetadata(storageId);
|
|
659
|
+
console.log(metadata.filename); // "hello.txt"
|
|
660
|
+
console.log(metadata.contentType); // "text/plain"
|
|
661
|
+
console.log(metadata.size); // 13
|
|
662
|
+
console.log(metadata.createdAt); // Date
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## Admin APIs
|
|
668
|
+
|
|
669
|
+
Access admin functionality (requires admin privileges).
|
|
670
|
+
|
|
671
|
+
### Admin Auth Client
|
|
672
|
+
|
|
673
|
+
The `AdminClient` currently provides authentication management:
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
import { AdminClient } from "@archlast/client/admin";
|
|
677
|
+
|
|
678
|
+
const admin = new AdminClient("http://localhost:4000", {
|
|
679
|
+
apiKey: "arch_your_api_key" // Optional: Better-Auth API key
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Admin authentication
|
|
683
|
+
await admin.auth.signIn(email, password);
|
|
684
|
+
const { user } = await admin.auth.getProfile();
|
|
685
|
+
await admin.auth.signOut();
|
|
686
|
+
|
|
687
|
+
// Session management
|
|
688
|
+
const { sessions } = await admin.auth.getSessions();
|
|
689
|
+
await admin.auth.revokeSession(sessionId);
|
|
690
|
+
await admin.auth.signOutAll();
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**Available Methods:**
|
|
694
|
+
|
|
695
|
+
- `admin.auth.signIn(email, password)` - Admin sign in
|
|
696
|
+
- `admin.auth.signOut()` - Sign out current session
|
|
697
|
+
- `admin.auth.getProfile()` - Get admin profile
|
|
698
|
+
- `admin.auth.getSessions()` - List all sessions
|
|
699
|
+
- `admin.auth.revokeSession(sessionId)` - Revoke specific session
|
|
700
|
+
- `admin.auth.signOutAll()` - Sign out from all sessions
|
|
701
|
+
|
|
702
|
+
**Note:** Additional admin clients (users, data, API keys) may be added in future releases.
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## tRPC Integration
|
|
707
|
+
|
|
708
|
+
For type-safe RPC with full TypeScript inference.
|
|
709
|
+
|
|
710
|
+
### Setup
|
|
48
711
|
|
|
49
712
|
```ts
|
|
50
713
|
import { createArchlastTRPCClient } from "@archlast/client/trpc";
|
|
@@ -52,9 +715,246 @@ import type { AppRouter } from "./_generated/rpc";
|
|
|
52
715
|
|
|
53
716
|
const trpc = createArchlastTRPCClient<AppRouter>({
|
|
54
717
|
baseUrl: "http://localhost:4000",
|
|
718
|
+
apiKey: "arch_your_api_key"
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Usage
|
|
723
|
+
|
|
724
|
+
```ts
|
|
725
|
+
// Queries
|
|
726
|
+
const tasks = await trpc.tasks.list.query({});
|
|
727
|
+
const task = await trpc.tasks.get.query({ id: "123" });
|
|
728
|
+
|
|
729
|
+
// Mutations
|
|
730
|
+
const created = await trpc.tasks.create.mutate({ text: "New task" });
|
|
731
|
+
await trpc.tasks.update.mutate({ id: "123", completed: true });
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### React Query Integration
|
|
735
|
+
|
|
736
|
+
```tsx
|
|
737
|
+
import { createTRPCReact } from "@trpc/react-query";
|
|
738
|
+
import type { AppRouter } from "./_generated/rpc";
|
|
739
|
+
|
|
740
|
+
export const trpc = createTRPCReact<AppRouter>();
|
|
741
|
+
|
|
742
|
+
// In component
|
|
743
|
+
function TaskList() {
|
|
744
|
+
const { data, isLoading } = trpc.tasks.list.useQuery({});
|
|
745
|
+
const createTask = trpc.tasks.create.useMutation();
|
|
746
|
+
|
|
747
|
+
// ...
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## Subpath Exports
|
|
754
|
+
|
|
755
|
+
Import specific functionality to reduce bundle size:
|
|
756
|
+
|
|
757
|
+
| Import | Description |
|
|
758
|
+
|--------|-------------|
|
|
759
|
+
| `@archlast/client` | Core `ArchlastClient` class |
|
|
760
|
+
| `@archlast/client/react` | React hooks and provider |
|
|
761
|
+
| `@archlast/client/auth` | Authentication client |
|
|
762
|
+
| `@archlast/client/storage` | File storage client |
|
|
763
|
+
| `@archlast/client/admin` | Admin API client |
|
|
764
|
+
| `@archlast/client/trpc` | tRPC client factory |
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
// Minimal import for core functionality
|
|
768
|
+
import { ArchlastClient } from "@archlast/client";
|
|
769
|
+
|
|
770
|
+
// React-specific imports
|
|
771
|
+
import {
|
|
772
|
+
ArchlastProvider,
|
|
773
|
+
useQuery,
|
|
774
|
+
useMutation,
|
|
775
|
+
usePagination,
|
|
776
|
+
useUpload,
|
|
777
|
+
useAuth
|
|
778
|
+
} from "@archlast/client/react";
|
|
779
|
+
|
|
780
|
+
// Auth-only import
|
|
781
|
+
import { ArchlastAuthClient } from "@archlast/client/auth";
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
## TypeScript Support
|
|
787
|
+
|
|
788
|
+
The client is fully typed. Types are generated by the CLI based on your schema.
|
|
789
|
+
|
|
790
|
+
### Generated Types
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
// _generated/api.ts
|
|
794
|
+
export const api = {
|
|
795
|
+
tasks: {
|
|
796
|
+
list: { _path: "tasks.list", _returnType: {} as Task[] },
|
|
797
|
+
get: { _path: "tasks.get", _returnType: {} as Task | null },
|
|
798
|
+
create: { _path: "tasks.create", _returnType: {} as Task },
|
|
799
|
+
// ...
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Usage with Types
|
|
805
|
+
|
|
806
|
+
```ts
|
|
807
|
+
import { useQuery } from "@archlast/client/react";
|
|
808
|
+
import { api } from "./_generated/api";
|
|
809
|
+
|
|
810
|
+
// TypeScript knows `tasks` is Task[]
|
|
811
|
+
const tasks = useQuery(api.tasks.list, {});
|
|
812
|
+
|
|
813
|
+
// TypeScript knows `task` is Task | null
|
|
814
|
+
const task = useQuery(api.tasks.get, { id: "123" });
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
## Error Handling
|
|
820
|
+
|
|
821
|
+
### Query Errors
|
|
822
|
+
|
|
823
|
+
```tsx
|
|
824
|
+
function TaskList() {
|
|
825
|
+
const { data, error, isError } = useQuery(api.tasks.list, {});
|
|
826
|
+
|
|
827
|
+
if (isError) {
|
|
828
|
+
return (
|
|
829
|
+
<div className="error">
|
|
830
|
+
<h3>Failed to load tasks</h3>
|
|
831
|
+
<p>{error.message}</p>
|
|
832
|
+
<button onClick={() => refetch()}>Retry</button>
|
|
833
|
+
</div>
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return <TaskGrid tasks={data} />;
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### Mutation Errors
|
|
842
|
+
|
|
843
|
+
```ts
|
|
844
|
+
const createTask = useMutation(api.tasks.create);
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
await createTask({ text: "" }); // Invalid
|
|
848
|
+
} catch (error) {
|
|
849
|
+
if (error instanceof ValidationError) {
|
|
850
|
+
console.log("Validation failed:", error.issues);
|
|
851
|
+
} else if (error instanceof AuthenticationError) {
|
|
852
|
+
console.log("Not authenticated");
|
|
853
|
+
} else {
|
|
854
|
+
console.log("Unknown error:", error);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### Global Error Boundary
|
|
860
|
+
|
|
861
|
+
```tsx
|
|
862
|
+
import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
|
863
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
864
|
+
|
|
865
|
+
function App() {
|
|
866
|
+
return (
|
|
867
|
+
<QueryErrorResetBoundary>
|
|
868
|
+
{({ reset }) => (
|
|
869
|
+
<ErrorBoundary
|
|
870
|
+
onReset={reset}
|
|
871
|
+
fallbackRender={({ error, resetErrorBoundary }) => (
|
|
872
|
+
<div>
|
|
873
|
+
<p>Something went wrong: {error.message}</p>
|
|
874
|
+
<button onClick={resetErrorBoundary}>Try again</button>
|
|
875
|
+
</div>
|
|
876
|
+
)}
|
|
877
|
+
>
|
|
878
|
+
<ArchlastProvider client={client}>
|
|
879
|
+
<YourApp />
|
|
880
|
+
</ArchlastProvider>
|
|
881
|
+
</ErrorBoundary>
|
|
882
|
+
)}
|
|
883
|
+
</QueryErrorResetBoundary>
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## Advanced Patterns
|
|
891
|
+
|
|
892
|
+
### Optimistic Updates
|
|
893
|
+
|
|
894
|
+
```ts
|
|
895
|
+
const queryClient = useQueryClient();
|
|
896
|
+
|
|
897
|
+
const updateTask = useMutation(api.tasks.update, {
|
|
898
|
+
onMutate: async (newData) => {
|
|
899
|
+
// Cancel outgoing refetches
|
|
900
|
+
await queryClient.cancelQueries(["tasks.list"]);
|
|
901
|
+
|
|
902
|
+
// Snapshot previous value
|
|
903
|
+
const previous = queryClient.getQueryData(["tasks.list"]);
|
|
904
|
+
|
|
905
|
+
// Optimistically update
|
|
906
|
+
queryClient.setQueryData(["tasks.list"], (old: Task[]) =>
|
|
907
|
+
old.map(t => t._id === newData.id ? { ...t, ...newData } : t)
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
return { previous };
|
|
911
|
+
},
|
|
912
|
+
onError: (err, newData, context) => {
|
|
913
|
+
// Rollback on error
|
|
914
|
+
queryClient.setQueryData(["tasks.list"], context.previous);
|
|
915
|
+
}
|
|
55
916
|
});
|
|
56
917
|
```
|
|
57
918
|
|
|
58
|
-
|
|
919
|
+
### SSR / Server Components
|
|
920
|
+
|
|
921
|
+
```ts
|
|
922
|
+
// For Next.js SSR
|
|
923
|
+
const client = new ArchlastClient(
|
|
924
|
+
wsUrl,
|
|
925
|
+
httpUrl,
|
|
926
|
+
"web",
|
|
927
|
+
undefined,
|
|
928
|
+
{
|
|
929
|
+
autoConnect: false, // Don't connect on server
|
|
930
|
+
betterAuthCookies: cookies() // Pass request cookies
|
|
931
|
+
}
|
|
932
|
+
);
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### Multiple Clients
|
|
936
|
+
|
|
937
|
+
```tsx
|
|
938
|
+
const mainClient = new ArchlastClient(mainWsUrl, mainHttpUrl, "main");
|
|
939
|
+
const analyticsClient = new ArchlastClient(analyticsWsUrl, analyticsHttpUrl, "analytics");
|
|
940
|
+
|
|
941
|
+
function App() {
|
|
942
|
+
return (
|
|
943
|
+
<ArchlastProvider client={mainClient}>
|
|
944
|
+
<ArchlastProvider client={analyticsClient} contextKey="analytics">
|
|
945
|
+
<YourApp />
|
|
946
|
+
</ArchlastProvider>
|
|
947
|
+
</ArchlastProvider>
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
## Keywords
|
|
955
|
+
|
|
956
|
+
archlast, client, sdk, react, hooks, real-time, websocket, typescript, api
|
|
957
|
+
|
|
958
|
+
## License
|
|
59
959
|
|
|
60
|
-
|
|
960
|
+
MIT
|