@basictech/react 0.8.0-beta.4 → 0.9.0-beta.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/changelog.md +31 -0
- package/dist/index.d.mts +1025 -294
- package/dist/index.d.ts +1025 -294
- package/dist/index.js +2256 -1444
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2230 -1448
- package/dist/index.mjs.map +1 -1
- package/package.json +27 -8
- package/readme.md +280 -217
- package/.turbo/turbo-build.log +0 -24
- package/AUTH_IMPLEMENTATION_GUIDE.md +0 -2011
- package/src/AuthContext.tsx +0 -591
- package/src/config.ts +0 -9
- package/src/context.tsx +0 -122
- package/src/core/auth/AuthManager.ts +0 -1371
- package/src/core/db/RemoteCollection.ts +0 -308
- package/src/core/db/RemoteDB.ts +0 -40
- package/src/core/db/index.ts +0 -7
- package/src/core/db/types.ts +0 -140
- package/src/dev/BasicDevToolbar.tsx +0 -665
- package/src/index.ts +0 -36
- package/src/sync/index.ts +0 -288
- package/src/sync/syncProtocol.js +0 -291
- package/src/sync/tokenRegistry.ts +0 -20
- package/src/updater/updateMigrations.ts +0 -22
- package/src/updater/versionUpdater.ts +0 -153
- package/src/utils/network.ts +0 -135
- package/src/utils/normalizeClientId.ts +0 -22
- package/src/utils/resolveDid.ts +0 -101
- package/src/utils/schema.ts +0 -119
- package/src/utils/storage.ts +0 -67
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -11
package/readme.md
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
# @basictech/react
|
|
2
2
|
|
|
3
|
-
React SDK for [Basic](https://basic.tech)
|
|
3
|
+
React SDK for [Basic](https://basic.tech) — authentication and an offline-first, synced database for your React app.
|
|
4
|
+
|
|
5
|
+
Basic gives every user their own personal datastore (a PDS). Your app authenticates with OAuth, gets its own sandboxed slice of the user's datastore, and this SDK keeps a local replica of that data in the browser — reads are instant and local, writes work offline, and everything syncs live across the user's devices over the Basic **Sync/2** protocol.
|
|
6
|
+
|
|
7
|
+
> **Breaking change (0.9):** this version is a ground-up rewrite for the new Basic auth system and the Sync/2 sync engine. The 0.8.x sync client speaks a protocol the server no longer supports. See [Migrating from 0.8](#migrating-from-08).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick start](#quick-start)
|
|
15
|
+
- [How it works](#how-it-works)
|
|
16
|
+
- [Authentication](#authentication)
|
|
17
|
+
- [Database](#database)
|
|
18
|
+
- [Live queries](#live-queries)
|
|
19
|
+
- [Sync status, offline behavior & errors](#sync-status-offline-behavior--errors)
|
|
20
|
+
- [Shares (multiplayer)](#shares-multiplayer)
|
|
21
|
+
- [Typed schemas](#typed-schemas)
|
|
22
|
+
- [REST mode](#rest-mode)
|
|
23
|
+
- [API reference](#api-reference)
|
|
24
|
+
- [Advanced usage](#advanced-usage)
|
|
25
|
+
- [Migrating from 0.8](#migrating-from-08)
|
|
26
|
+
|
|
27
|
+
---
|
|
4
28
|
|
|
5
29
|
## Installation
|
|
6
30
|
|
|
@@ -8,13 +32,16 @@ React SDK for [Basic](https://basic.tech) - add authentication and real-time dat
|
|
|
8
32
|
npm install @basictech/react
|
|
9
33
|
```
|
|
10
34
|
|
|
11
|
-
|
|
35
|
+
Requires React 16.8+ (hooks). React 17, 18, and 19 are supported.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
12
38
|
|
|
13
|
-
### 1.
|
|
39
|
+
### 1. Define your schema
|
|
14
40
|
|
|
15
|
-
Create a `basic.config.ts`
|
|
41
|
+
Create a `basic.config.ts` with your project id (from [app.basic.tech](https://app.basic.tech)) and your tables:
|
|
16
42
|
|
|
17
43
|
```typescript
|
|
44
|
+
// basic.config.ts
|
|
18
45
|
export const schema = {
|
|
19
46
|
project_id: "YOUR_PROJECT_ID",
|
|
20
47
|
version: 1,
|
|
@@ -22,7 +49,7 @@ export const schema = {
|
|
|
22
49
|
todos: {
|
|
23
50
|
type: "collection",
|
|
24
51
|
fields: {
|
|
25
|
-
title: { type: "string", indexed: true },
|
|
52
|
+
title: { type: "string", indexed: true, required: true },
|
|
26
53
|
completed: { type: "boolean", indexed: true },
|
|
27
54
|
},
|
|
28
55
|
},
|
|
@@ -30,318 +57,354 @@ export const schema = {
|
|
|
30
57
|
};
|
|
31
58
|
```
|
|
32
59
|
|
|
33
|
-
|
|
60
|
+
Field types: `string`, `number`, `boolean`, `json`. Publish the schema from the Basic dashboard — sync is enabled once the published version matches your local one.
|
|
34
61
|
|
|
35
|
-
Wrap your app
|
|
62
|
+
### 2. Wrap your app
|
|
36
63
|
|
|
37
64
|
```tsx
|
|
65
|
+
// main.tsx
|
|
38
66
|
import { BasicProvider } from "@basictech/react";
|
|
39
67
|
import { schema } from "./basic.config";
|
|
40
68
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
}
|
|
69
|
+
createRoot(document.getElementById("root")!).render(
|
|
70
|
+
<BasicProvider schema={schema} devToolbar>
|
|
71
|
+
<App />
|
|
72
|
+
</BasicProvider>,
|
|
73
|
+
);
|
|
48
74
|
```
|
|
49
75
|
|
|
50
|
-
### 3.
|
|
51
|
-
|
|
52
|
-
Access auth and database in any component:
|
|
76
|
+
### 3. Sign in and use the database
|
|
53
77
|
|
|
54
78
|
```tsx
|
|
55
79
|
import { useBasic, useQuery } from "@basictech/react";
|
|
56
80
|
|
|
57
|
-
function
|
|
58
|
-
const {
|
|
59
|
-
|
|
60
|
-
// Live query - automatically updates when data changes
|
|
61
|
-
const todos = useQuery(() => db.collection("todos").getAll());
|
|
62
|
-
|
|
63
|
-
const addTodo = async () => {
|
|
64
|
-
await db.collection("todos").add({
|
|
65
|
-
title: "New todo",
|
|
66
|
-
completed: false,
|
|
67
|
-
});
|
|
68
|
-
};
|
|
81
|
+
function Todos() {
|
|
82
|
+
const { isSignedIn, signIn, signOut, user, db } = useBasic();
|
|
83
|
+
const todos = useQuery(() => db.table("todos").getAll());
|
|
69
84
|
|
|
70
85
|
if (!isSignedIn) {
|
|
71
|
-
return <button onClick={signIn}>Sign
|
|
86
|
+
return <button onClick={() => signIn()}>Sign in with Basic</button>;
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
return (
|
|
75
90
|
<div>
|
|
76
|
-
<p>
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
<p>hi {user?.email} <button onClick={() => signOut()}>sign out</button></p>
|
|
92
|
+
|
|
93
|
+
<button onClick={() => db.table("todos").create({ title: "hello", completed: false })}>
|
|
94
|
+
add todo
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
{todos?.map((todo) => (
|
|
98
|
+
<label key={todo.id}>
|
|
99
|
+
<input
|
|
100
|
+
type="checkbox"
|
|
101
|
+
checked={!!todo.completed}
|
|
102
|
+
onChange={() => db.table("todos").patch(todo.id, { completed: !todo.completed })}
|
|
103
|
+
/>
|
|
104
|
+
{String(todo.title)}
|
|
105
|
+
<button onClick={() => db.table("todos").delete(todo.id)}>×</button>
|
|
106
|
+
</label>
|
|
107
|
+
))}
|
|
84
108
|
</div>
|
|
85
109
|
);
|
|
86
110
|
}
|
|
87
111
|
```
|
|
88
112
|
|
|
89
|
-
|
|
113
|
+
That's the whole loop: `signIn()` runs the OAuth flow, `db.table(...)` writes apply instantly to the local replica (and queue offline), and `useQuery` re-renders whenever local data changes — whether the change came from this tab, another tab, another device, or another user via a share.
|
|
90
114
|
|
|
91
|
-
##
|
|
115
|
+
## How it works
|
|
92
116
|
|
|
93
|
-
|
|
117
|
+
In the default `sync` mode, the SDK maintains a **local replica** of your app's slice of the user's datastore, in IndexedDB, and syncs it over one WebSocket:
|
|
94
118
|
|
|
95
|
-
|
|
119
|
+
1. **Bootstrap** — on first run the SDK fetches a snapshot of current state, then subscribes for live changes from that point.
|
|
120
|
+
2. **Writes are ops** — every write is a `put` (create/replace), `patch` (shallow field merge), or `delete` operation with a client-minted unique id. Ops apply to the local view immediately (optimistic), queue in IndexedDB, and push to the server when online. Retries are always safe: the server deduplicates by op id.
|
|
121
|
+
3. **Reads are local** — `get`/`getAll`/`find` read the local view: confirmed server state with your pending writes layered on top. No network round-trip.
|
|
122
|
+
4. **The server is the authority** — it orders all ops (last-writer-wins per record; `patch`es merge per field), validates them against your published schema, and echoes them to every connected device. If it rejects a write, the SDK rolls it back locally and surfaces it (see [rejected ops](#rejected-ops)).
|
|
123
|
+
5. **Sign-out is clean** — local data is deleted, the session is revoked server-side, and other tabs update in place. No page reloads.
|
|
124
|
+
|
|
125
|
+
## Authentication
|
|
126
|
+
|
|
127
|
+
Basic is the OAuth provider. `signIn()` redirects to the user's Basic sign-in page; after consent the user lands back on your app with a code the SDK exchanges automatically (PKCE, rotating refresh tokens — all handled for you).
|
|
96
128
|
|
|
97
129
|
```tsx
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
const auth = useAuth(); // or grab the same fields from useBasic()
|
|
131
|
+
|
|
132
|
+
auth.status; // 'bootstrapping' | 'authenticated' | 'recovering' | 'reauth_required' | 'signed_out'
|
|
133
|
+
auth.isSignedIn; // boolean (stays true during reauth_required so you can show user info)
|
|
134
|
+
auth.user; // { sub, email, name, picture } | null
|
|
135
|
+
auth.did; // the user's decentralized id
|
|
136
|
+
auth.scope; // scopes granted to your app
|
|
137
|
+
|
|
138
|
+
await auth.signIn(); // redirect flow (current URL is the redirect URI)
|
|
139
|
+
await auth.signIn("https://app.com/done"); // custom redirect URI
|
|
140
|
+
await auth.signInWithHandle("alice.basic.id"); // federated: resolve the user's own PDS first
|
|
141
|
+
await auth.signOut(); // revokes server-side + clears local data
|
|
104
142
|
```
|
|
105
143
|
|
|
106
|
-
|
|
144
|
+
### Auth status lifecycle
|
|
107
145
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
146
|
+
| Status | Meaning | What to render |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| `bootstrapping` | SDK is initializing | splash / nothing (provider hides children by default) |
|
|
149
|
+
| `authenticated` | Healthy session | your app |
|
|
150
|
+
| `recovering` | Session likely exists but unconfirmed (offline, mid-refresh) | your app (data still works locally) |
|
|
151
|
+
| `reauth_required` | Session definitively invalid — user must sign in again | user info + a sign-in prompt |
|
|
152
|
+
| `signed_out` | No session | sign-in screen |
|
|
114
153
|
|
|
115
|
-
|
|
154
|
+
### Calling your own API
|
|
116
155
|
|
|
117
|
-
|
|
118
|
-
- **`remote`** - Direct REST API calls (no local storage)
|
|
156
|
+
`getToken()` returns a valid access token, refreshing it automatically (tokens are short-lived — never cache them yourself):
|
|
119
157
|
|
|
120
|
-
|
|
158
|
+
```typescript
|
|
159
|
+
const token = await auth.getToken();
|
|
160
|
+
await fetch("https://your-api.example/thing", {
|
|
161
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
162
|
+
});
|
|
163
|
+
// after a 401 from your API:
|
|
164
|
+
await auth.getToken({ forceRefresh: true });
|
|
165
|
+
```
|
|
121
166
|
|
|
122
|
-
###
|
|
167
|
+
### Scopes
|
|
123
168
|
|
|
124
|
-
|
|
169
|
+
Default requested scopes are `profile,email,app:admin`. Check what was actually granted:
|
|
125
170
|
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
isReady, // boolean - SDK initialized
|
|
130
|
-
isSignedIn, // boolean - Session is valid
|
|
131
|
-
user, // { id, email, ... } | null (may be null briefly during profile refresh/recovery)
|
|
132
|
-
|
|
133
|
-
// Auth methods
|
|
134
|
-
signIn, // () => void - Redirect to login
|
|
135
|
-
signOut, // () => void - Clear session
|
|
136
|
-
signInWithCode, // (code, state?) => Promise - Manual OAuth
|
|
137
|
-
getSignInUrl, // (redirectUri?) => string - Get OAuth URL
|
|
138
|
-
getToken, // () => Promise<string> - Get access token
|
|
139
|
-
|
|
140
|
-
// Database
|
|
141
|
-
db, // Database instance
|
|
142
|
-
dbStatus, // DBStatus - see below
|
|
143
|
-
dbMode, // "sync" | "remote"
|
|
144
|
-
|
|
145
|
-
// Dev / schema snapshot (for custom tooling)
|
|
146
|
-
devInfo, // BasicSchemaDevInfo | null — local vs remote schema status
|
|
147
|
-
refreshSchemaStatus, // () => Promise<void> — re-fetch schema status from API
|
|
148
|
-
} = useBasic();
|
|
171
|
+
```typescript
|
|
172
|
+
auth.hasScope("app:db:read"); // note: app:admin implies all app:db:* scopes
|
|
173
|
+
auth.missingScopes(); // requested but not granted
|
|
149
174
|
```
|
|
150
175
|
|
|
151
|
-
|
|
176
|
+
## Database
|
|
152
177
|
|
|
153
|
-
|
|
178
|
+
`db.table(name)` returns the table API (name must exist in your schema):
|
|
154
179
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
| `LOADING` | SDK initializing |
|
|
158
|
-
| `OFFLINE` | Not connected |
|
|
159
|
-
| `CONNECTING` | Connecting to sync server |
|
|
160
|
-
| `ONLINE` | Connected and idle |
|
|
161
|
-
| `SYNCING` | Syncing data |
|
|
162
|
-
| `ERROR` | Sync error |
|
|
163
|
-
| `ERROR_WILL_RETRY` | Sync error but client will retry (e.g. expired token). Use this to show "Reconnecting…" or trigger token refresh. |
|
|
180
|
+
```typescript
|
|
181
|
+
const todos = db.table("todos");
|
|
164
182
|
|
|
165
|
-
|
|
183
|
+
const created = await todos.create({ title: "buy milk" });
|
|
184
|
+
// → { id: "0198…", title: "buy milk" } — id minted locally, works offline
|
|
166
185
|
|
|
167
|
-
|
|
186
|
+
await todos.put(id, { title: "replace", completed: false });
|
|
187
|
+
// create-or-replace the WHOLE record with these fields
|
|
188
|
+
|
|
189
|
+
await todos.patch(id, { completed: true });
|
|
190
|
+
// shallow-merge the given fields; returns null if the record doesn't exist
|
|
191
|
+
|
|
192
|
+
await todos.delete(id); // idempotent — deleting a missing record is fine
|
|
193
|
+
|
|
194
|
+
await todos.get(id); // record or null
|
|
195
|
+
await todos.getAll(); // all records
|
|
196
|
+
await todos.find((t) => !t.completed);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Semantics worth knowing:
|
|
168
200
|
|
|
169
|
-
|
|
201
|
+
- **`put` replaces, `patch` merges.** Two devices patching *different* fields of the same record both win; patching the *same* field, the last write wins.
|
|
202
|
+
- **`json` fields are atomic** — patching a `json` field replaces its whole value (no deep merge).
|
|
203
|
+
- Writes are **validated locally** against your schema before queueing (same rules the server enforces), so typos fail fast with a thrown error instead of a round-trip.
|
|
204
|
+
- In sync mode, `create` mints the id client-side — you get the full record back synchronously-ish, even offline.
|
|
170
205
|
|
|
171
|
-
|
|
206
|
+
## Live queries
|
|
172
207
|
|
|
173
|
-
|
|
208
|
+
`useQuery` re-runs your query and re-renders whenever the underlying data changes:
|
|
174
209
|
|
|
175
210
|
```tsx
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
211
|
+
const todos = useQuery(() => db.table("todos").getAll()); // undefined while loading
|
|
212
|
+
const open = useQuery(() => db.table("todos").find((t) => !t.completed));
|
|
213
|
+
const one = useQuery(() => db.table("todos").get(selectedId), [selectedId]); // deps like useEffect
|
|
179
214
|
```
|
|
180
215
|
|
|
181
|
-
|
|
216
|
+
For advanced queries, `table.ref` exposes the underlying [Dexie](https://dexie.org) table over the local view (sync mode only) — indexed fields from your schema are Dexie indexes:
|
|
182
217
|
|
|
183
218
|
```tsx
|
|
184
|
-
|
|
219
|
+
const recent = useQuery(() =>
|
|
220
|
+
db.table("todos").ref!.where("completed").equals(0).limit(10).toArray(),
|
|
221
|
+
);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Sync status, offline behavior & errors
|
|
185
225
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
226
|
+
```tsx
|
|
227
|
+
const sync = useSyncStatus();
|
|
228
|
+
|
|
229
|
+
sync.status; // 'idle' | 'connecting' | 'online' | 'offline' | 'auth_required' | 'revoked' | 'stopped'
|
|
230
|
+
sync.enabled; // false when the schema is invalid/unpublished
|
|
231
|
+
sync.pendingCount; // local writes not yet confirmed by the server
|
|
190
232
|
```
|
|
191
233
|
|
|
192
|
-
|
|
234
|
+
**Offline is not an error.** Writes keep working and queue in IndexedDB; reads serve the local view; everything pushes on reconnect (safe to retry — ops are idempotent). The queue survives page reloads.
|
|
193
235
|
|
|
194
|
-
|
|
236
|
+
`revoked` means the user disconnected your app from their account — data access is gone until they sign in and consent again.
|
|
195
237
|
|
|
196
|
-
###
|
|
238
|
+
### Rejected ops
|
|
197
239
|
|
|
198
|
-
|
|
240
|
+
If the server terminally rejects a write (schema validation, permissions, payload too large), the SDK **rolls the record back**, parks the op in a rejected store, and will never retry it:
|
|
199
241
|
|
|
200
242
|
```tsx
|
|
201
|
-
|
|
243
|
+
const sync = useSyncStatus();
|
|
244
|
+
const rejected = await sync.listRejected();
|
|
245
|
+
// [{ op_id, op: { type, table, record_id, data }, error: 'SCHEMA_VALIDATION_FAILED', message, rejected_at }]
|
|
246
|
+
await sync.clearRejected();
|
|
247
|
+
```
|
|
202
248
|
|
|
203
|
-
|
|
204
|
-
const todos = useQuery(() => db.collection("todos").getAll());
|
|
249
|
+
You can also listen live:
|
|
205
250
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
const todos = useQuery(() => db.collection<Todo>("todos").getAll());
|
|
251
|
+
```typescript
|
|
252
|
+
const { client } = useBasic();
|
|
253
|
+
useEffect(() => client.engine?.on("rejected", ({ rejection }) => {
|
|
254
|
+
toast.error(`write rejected: ${rejection.error}`);
|
|
255
|
+
}), [client]);
|
|
213
256
|
```
|
|
214
257
|
|
|
215
|
-
|
|
258
|
+
## Shares (multiplayer)
|
|
216
259
|
|
|
217
|
-
|
|
260
|
+
Users can grant other users access to a slice of their data — a whole table or specific records, read-only or read-write. Grants are created by the user on basic.id (apps can never share a user's data on their own); your app **lists** and **mounts** them:
|
|
218
261
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
262
|
+
```tsx
|
|
263
|
+
function SharedTodos() {
|
|
264
|
+
const { received } = useShares(); // shares other users granted to me
|
|
265
|
+
const share = received[0];
|
|
266
|
+
const { db: sharedDb, status } = useShare(share?.id); // 'mounting' | 'mounted' | 'error' | 'revoked'
|
|
267
|
+
const todos = useQuery(() => sharedDb?.table("todos").getAll(), [sharedDb]);
|
|
222
268
|
|
|
223
|
-
|
|
269
|
+
if (!share) return <p>nothing shared with you yet</p>;
|
|
270
|
+
if (status !== "mounted") return <p>{status}…</p>;
|
|
224
271
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
272
|
+
return (
|
|
273
|
+
<ul>
|
|
274
|
+
{todos?.map((t) => (
|
|
275
|
+
<li key={t.id} onClick={() => sharedDb!.table("todos").patch(t.id, { completed: true })}>
|
|
276
|
+
{String(t.title)}
|
|
277
|
+
</li>
|
|
278
|
+
))}
|
|
279
|
+
</ul>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
228
282
|
```
|
|
229
283
|
|
|
230
|
-
|
|
284
|
+
Mounted shares live in their **own local keyspace** — separate replica, separate offline queue — and are never merged with the user's own data. Writes work when the share permission is `write` (the owner sees your changes attributed to the guest). When the owner revokes the share, the mount status flips to `revoked` and the local copy is purged.
|
|
231
285
|
|
|
232
|
-
|
|
233
|
-
| ------------------ | -------------------- | ----------------------------------- |
|
|
234
|
-
| `getAll()` | `Promise<T[]>` | Get all records |
|
|
235
|
-
| `get(id)` | `Promise<T \| null>` | Get one record by ID |
|
|
236
|
-
| `add(data)` | `Promise<T>` | Create new record (returns with ID) |
|
|
237
|
-
| `put(data)` | `Promise<T>` | Upsert record (requires ID) |
|
|
238
|
-
| `update(id, data)` | `Promise<T \| null>` | Partial update |
|
|
239
|
-
| `delete(id)` | `Promise<boolean>` | Delete record |
|
|
240
|
-
| `filter(fn)` | `Promise<T[]>` | Filter with predicate |
|
|
286
|
+
## Typed schemas
|
|
241
287
|
|
|
242
|
-
|
|
288
|
+
`@basictech/schema` (a dependency of this package) can infer record types from your schema:
|
|
243
289
|
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
290
|
+
```typescript
|
|
291
|
+
import { defineSchema, type InferRecord } from "@basictech/schema";
|
|
292
|
+
|
|
293
|
+
export const schema = defineSchema({
|
|
294
|
+
project_id: "…",
|
|
295
|
+
version: 1,
|
|
296
|
+
tables: {
|
|
297
|
+
todos: {
|
|
298
|
+
fields: {
|
|
299
|
+
title: { type: "string", required: true },
|
|
300
|
+
completed: { type: "boolean" },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
249
304
|
});
|
|
250
|
-
console.log(todo.id); // Auto-generated ID
|
|
251
305
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
306
|
+
type Todo = InferRecord<typeof schema, "todos">;
|
|
307
|
+
// { id: string; title: string; completed?: boolean }
|
|
308
|
+
|
|
309
|
+
const todos = db.table<Todo>("todos"); // fully typed CRUD
|
|
310
|
+
```
|
|
255
311
|
|
|
256
|
-
|
|
257
|
-
await db.collection("todos").update("some-id", { completed: true });
|
|
312
|
+
## REST mode
|
|
258
313
|
|
|
259
|
-
|
|
260
|
-
await db.collection("todos").delete("some-id");
|
|
314
|
+
Don't want a local replica? `mode="rest"` makes every table call a direct API request — no IndexedDB, no WebSocket, no offline support:
|
|
261
315
|
|
|
262
|
-
|
|
263
|
-
|
|
316
|
+
```tsx
|
|
317
|
+
<BasicProvider schema={schema} mode="rest">
|
|
264
318
|
```
|
|
265
319
|
|
|
266
|
-
|
|
320
|
+
The table API is identical, with REST semantics: `create` gets its id from the server, `put`/`patch`/`delete` on missing records return null/no-op, reads hit the network. `useQuery` won't live-update in this mode (re-run queries with deps).
|
|
267
321
|
|
|
268
|
-
##
|
|
322
|
+
## API reference
|
|
269
323
|
|
|
270
|
-
###
|
|
324
|
+
### `<BasicProvider>` props
|
|
271
325
|
|
|
272
|
-
|
|
326
|
+
| Prop | Default | Description |
|
|
327
|
+
| --- | --- | --- |
|
|
328
|
+
| `schema` | — | Your Basic schema document (`project_id` is read from it) |
|
|
329
|
+
| `mode` | `"sync"` | `"sync"` (local replica) or `"rest"` (direct API) |
|
|
330
|
+
| `auth.scopes` | `"profile,email,app:admin"` | OAuth scopes to request |
|
|
331
|
+
| `auth.pds_url` | `https://pds.basic.id` | The Basic server (auth + data + sync) |
|
|
332
|
+
| `auth.sync_url` | `wss://<pds>/sync/` | Sync WebSocket URL (rarely needed) |
|
|
333
|
+
| `storage` | `localStorage` | Custom `BasicStorage` adapter for auth state |
|
|
334
|
+
| `debug` | `false` | Verbose console logging |
|
|
335
|
+
| `devToolbar` | `false` | Floating status toolbar (dev environments only) |
|
|
336
|
+
| `renderWhileLoading` | `false` | Render children before auth bootstrap finishes |
|
|
273
337
|
|
|
274
|
-
|
|
275
|
-
const { signInWithCode, getSignInUrl } = useBasic();
|
|
338
|
+
### Hooks
|
|
276
339
|
|
|
277
|
-
|
|
278
|
-
|
|
340
|
+
| Hook | Returns |
|
|
341
|
+
| --- | --- |
|
|
342
|
+
| `useBasic()` | Everything: auth fields + actions, `db`, `sync`, `devInfo`, `client` |
|
|
343
|
+
| `useAuth()` | `{ isReady, isSignedIn, status, user, did, scope, hasScope, missingScopes, signIn, signInWithHandle, signInWithCode, signOut, getToken, getSignInUrl }` |
|
|
344
|
+
| `useDb()` | The table API for the user's own data |
|
|
345
|
+
| `useQuery(fn, deps?)` | Live query result (`undefined` while loading) |
|
|
346
|
+
| `useSyncStatus()` | `{ status, enabled, pendingCount, listRejected, clearRejected }` |
|
|
347
|
+
| `useShares()` | `{ granted, received, isLoading, error, refresh }` |
|
|
348
|
+
| `useShare(shareId)` | `{ db, status, error }` — a mounted share |
|
|
279
349
|
|
|
280
|
-
|
|
281
|
-
const result = await signInWithCode(code, state);
|
|
282
|
-
if (result.success) {
|
|
283
|
-
console.log("Signed in!");
|
|
284
|
-
}
|
|
285
|
-
```
|
|
350
|
+
### Table API
|
|
286
351
|
|
|
287
|
-
|
|
352
|
+
| Method | Sync mode | REST mode |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| `create(data)` | client-minted id, offline-capable | server-minted id |
|
|
355
|
+
| `put(id, data)` | create **or replace** | replace; throws if missing |
|
|
356
|
+
| `patch(id, partial)` | merge; `null` if missing locally | merge; `null` if missing |
|
|
357
|
+
| `delete(id)` | idempotent | no-op if missing |
|
|
358
|
+
| `get(id)` / `getAll()` / `find(fn)` | local view | network |
|
|
359
|
+
| `ref` | Dexie table (live queries, indexes) | `undefined` |
|
|
288
360
|
|
|
289
|
-
|
|
361
|
+
## Advanced usage
|
|
290
362
|
|
|
291
|
-
|
|
292
|
-
<BasicProvider schema={schema} dbMode="remote">
|
|
293
|
-
<App />
|
|
294
|
-
</BasicProvider>
|
|
295
|
-
```
|
|
363
|
+
### Outside React
|
|
296
364
|
|
|
297
|
-
|
|
365
|
+
Everything is available without React — `BasicClient` is the framework-agnostic core (`AuthManager` + `SyncEngine` + `RestClient`):
|
|
298
366
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
- `useQuery` won't auto-update (use manual refresh)
|
|
302
|
-
- Requires authentication for all operations
|
|
303
|
-
- Auth request failures are surfaced to the caller; recoverable 401s do not automatically sign the user out
|
|
367
|
+
```typescript
|
|
368
|
+
import { createBasicClient } from "@basictech/react";
|
|
304
369
|
|
|
305
|
-
|
|
370
|
+
const client = createBasicClient({ schema, debug: true });
|
|
371
|
+
await client.start();
|
|
372
|
+
client.subscribe(() => console.log(client.getSnapshot()));
|
|
373
|
+
await client.db.table("todos").create({ title: "from anywhere" });
|
|
374
|
+
```
|
|
306
375
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
376
|
+
The Sync/2 protocol layer (`SyncEngine`, `SyncConnection`, `SyncStore`, and all wire types) is exported too, for building custom clients or other runtimes — pass a `WebSocketImpl` (e.g. the `ws` package) to run in Node.
|
|
377
|
+
|
|
378
|
+
### Events
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const { client } = useBasic();
|
|
382
|
+
client.engine?.on("status", (s) => {}); // sync status changes
|
|
383
|
+
client.engine?.on("change", ({ sub, tables }) => {}); // records changed
|
|
384
|
+
client.engine?.on("rejected", ({ sub, rejection }) => {}); // terminal write rejection
|
|
385
|
+
client.engine?.on("revoked", ({ code }) => {}); // app connection revoked
|
|
318
386
|
```
|
|
319
387
|
|
|
320
|
-
|
|
388
|
+
### Custom storage
|
|
321
389
|
|
|
322
|
-
|
|
390
|
+
Auth state lives in `localStorage` by default. Provide any `{ get, set, remove }` async adapter via the `storage` prop (see `BasicStorage`). `STORAGE_KEYS` lists every key the SDK uses.
|
|
323
391
|
|
|
324
|
-
|
|
392
|
+
## Migrating from 0.8
|
|
325
393
|
|
|
326
|
-
|
|
327
|
-
interface Todo {
|
|
328
|
-
id: string;
|
|
329
|
-
title: string;
|
|
330
|
-
completed: boolean;
|
|
331
|
-
createdAt: number;
|
|
332
|
-
}
|
|
394
|
+
0.9 is a breaking rewrite — the server's legacy sync endpoint was removed, so 0.8 sync no longer functions. Changes you'll make:
|
|
333
395
|
|
|
334
|
-
|
|
335
|
-
|
|
396
|
+
| 0.8 | 0.9 |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| `db.collection("todos")` | `db.table("todos")` |
|
|
399
|
+
| `.add(data)` | `.create(data)` |
|
|
400
|
+
| `.update(id, partial)` | `.patch(id, partial)` |
|
|
401
|
+
| `.put({ id, ...fields })` | `.put(id, fields)` |
|
|
402
|
+
| `dbStatus` / `DBStatus` enum | `useSyncStatus().status` |
|
|
403
|
+
| `dbMode="remote"` | `mode="rest"` |
|
|
404
|
+
| `auth.ws_url` | `auth.sync_url` (you almost never need it) |
|
|
405
|
+
| `signOut()` reloads the page | in-place teardown, no reload |
|
|
336
406
|
|
|
337
|
-
|
|
338
|
-
const todo = await todos.add({
|
|
339
|
-
title: "Test",
|
|
340
|
-
completed: false,
|
|
341
|
-
createdAt: Date.now(),
|
|
342
|
-
});
|
|
343
|
-
// todo is typed as Todo
|
|
344
|
-
```
|
|
407
|
+
Local data migrates by re-bootstrap: the old `basicdb` IndexedDB database is deleted automatically and 0.9 builds fresh replicas in `basic-sync:<project_id>` databases from the server (the server is the source of truth, so nothing is lost).
|
|
345
408
|
|
|
346
409
|
---
|
|
347
410
|
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
> @basictech/react@0.8.0-beta.1 build
|
|
4
|
-
> tsup
|
|
5
|
-
|
|
6
|
-
[34mCLI[39m Building entry: src/index.ts
|
|
7
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
8
|
-
[34mCLI[39m tsup v8.5.1
|
|
9
|
-
[34mCLI[39m Using tsup config: /Users/raz/codebook/basic/libs/client-ts/packages/react/tsup.config.ts
|
|
10
|
-
[34mCLI[39m Target: es2022
|
|
11
|
-
[34mCLI[39m Cleaning output folder
|
|
12
|
-
[34mCJS[39m Build start
|
|
13
|
-
[34mESM[39m Build start
|
|
14
|
-
[32mCJS[39m [1mdist/index.js [22m[32m97.02 KB[39m
|
|
15
|
-
[32mCJS[39m [1mdist/index.js.map [22m[32m187.81 KB[39m
|
|
16
|
-
[32mCJS[39m ⚡️ Build success in 36ms
|
|
17
|
-
[32mESM[39m [1mdist/index.mjs [22m[32m93.09 KB[39m
|
|
18
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[32m187.75 KB[39m
|
|
19
|
-
[32mESM[39m ⚡️ Build success in 37ms
|
|
20
|
-
DTS Build start
|
|
21
|
-
DTS ⚡️ Build success in 1091ms
|
|
22
|
-
DTS dist/index.d.ts 12.66 KB
|
|
23
|
-
DTS dist/index.d.mts 12.66 KB
|
|
24
|
-
⠙[1G[0K
|