@weirdscience/based-client 0.4.0 → 0.6.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 +267 -49
- package/dist/client.d.ts.map +1 -1
- package/dist/core.cjs +376 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +334 -0
- package/dist/hooks/use-mutation.d.ts +1 -3
- package/dist/hooks/use-mutation.d.ts.map +1 -1
- package/dist/hooks/use-query.d.ts.map +1 -1
- package/dist/hooks/use-record.d.ts.map +1 -1
- package/dist/hooks/use-user.d.ts.map +1 -1
- package/dist/index.cjs +593 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -42
- package/dist/provider.d.ts +4 -4
- package/dist/provider.d.ts.map +1 -1
- package/dist/types.d.ts +44 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -6
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @weirdscience/based-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SDK for [Based](https://based.weirdscience.dev) — a minimal self-hosted Backend-as-a-Service.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A typed data builder and auth helpers for any JavaScript app, plus React hooks for queries and mutations. Type-safe end-to-end when paired with `based typegen`.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -12,10 +12,55 @@ bun add @weirdscience/based-client
|
|
|
12
12
|
# or: pnpm add @weirdscience/based-client
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
React (`>=18`) is an optional peer dependency: only the root entry needs it. Projects without React import from `@weirdscience/based-client/core` (next section).
|
|
16
|
+
|
|
17
|
+
## Without React — `@weirdscience/based-client/core`
|
|
18
|
+
|
|
19
|
+
The `core` entry point is the whole client minus the provider and hooks: `createClient`, `from()`, `auth`, `BasedError` and the types. It has no dependency on React and no `"use client"` directive, so it works in Vue, Svelte, Node, Bun, Deno, workers, or plain scripts.
|
|
20
|
+
|
|
21
|
+
<!-- @typecheck -->
|
|
22
|
+
```ts
|
|
23
|
+
import { createClient, BasedError } from "@weirdscience/based-client/core";
|
|
24
|
+
import type { Tables } from "./based";
|
|
25
|
+
|
|
26
|
+
const based = createClient<Tables>({
|
|
27
|
+
url: "https://my-app.based.example.com",
|
|
28
|
+
anonKey: "public-anon-key",
|
|
29
|
+
storage: false, // no localStorage outside the browser
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await based.auth.signIn("you@example.com", "password123");
|
|
33
|
+
|
|
34
|
+
const { data: posts } = await based.from("posts").select({ order: "createdAt.desc", limit: 10 });
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await based.from("posts").insert({ title: "Hello" });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err instanceof BasedError && err.status === 403) {
|
|
40
|
+
// not allowed by the table's access policy
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Everything in the [Data](#data--clientfromtable), [Auth](#auth), [Session persistence](#session-persistence) and [Type safety](#type-safety) sections applies to `core` unchanged. Only the [Hooks](#hooks) need React.
|
|
46
|
+
|
|
47
|
+
## Breaking changes in 0.5.0
|
|
48
|
+
|
|
49
|
+
- `mutate()` resolves to `Row | null` and no longer throws — check the returned
|
|
50
|
+
row, or the hook's `error`, instead of wrapping the call in `try`/`catch`.
|
|
51
|
+
- A hook's `error` is a `HookError` object (`{ code, message, status?, details? }`),
|
|
52
|
+
not an `Error`: read `error.message`, and branch on `error.code`.
|
|
53
|
+
- `AuthUser` has a `role` (`"owner" | "member"`).
|
|
54
|
+
- `signIn` / `signUp` throw a `BasedError` instead of resolving with an error.
|
|
16
55
|
|
|
17
56
|
## Quick start
|
|
18
57
|
|
|
58
|
+
Both bundles (ESM and CJS) ship with the `"use client"` directive, so
|
|
59
|
+
`BasedProvider` can be rendered straight from `app/layout.tsx` — no wrapper
|
|
60
|
+
component needed. The hooks are still client-side code: the file that *calls* a
|
|
61
|
+
hook needs its own `"use client"`, and a Server Component cannot call one.
|
|
62
|
+
|
|
63
|
+
<!-- @typecheck -->
|
|
19
64
|
```tsx
|
|
20
65
|
import { createClient, BasedProvider } from "@weirdscience/based-client";
|
|
21
66
|
|
|
@@ -29,12 +74,70 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|
|
29
74
|
}
|
|
30
75
|
```
|
|
31
76
|
|
|
77
|
+
## Data — `client.from(table)`
|
|
78
|
+
|
|
79
|
+
The imperative counterpart to the hooks, for event handlers, server scripts, and
|
|
80
|
+
anything outside React. Every method throws a `BasedError` on failure.
|
|
81
|
+
|
|
82
|
+
<!-- @typecheck -->
|
|
83
|
+
```ts
|
|
84
|
+
import { createClient } from "@weirdscience/based-client";
|
|
85
|
+
import type { Tables } from "./based";
|
|
86
|
+
|
|
87
|
+
const based = createClient<Tables>({ url, anonKey });
|
|
88
|
+
const posts = based.from("posts");
|
|
89
|
+
|
|
90
|
+
// List rows: filter, order, limit (default 20, max 100), offset
|
|
91
|
+
const { data, total } = await posts.select({
|
|
92
|
+
filter: { status: "published" },
|
|
93
|
+
order: "createdAt.desc",
|
|
94
|
+
limit: 20,
|
|
95
|
+
offset: 0,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// One row by id — resolves to null on 404 instead of throwing
|
|
99
|
+
const post = await posts.get("V1StGXR8_Z5jdHi6B");
|
|
100
|
+
|
|
101
|
+
// Create. Omit id and the server generates a nanoid
|
|
102
|
+
const created = await posts.insert({ title: "Hello" });
|
|
103
|
+
|
|
104
|
+
// Upsert at an id you choose: creates if missing, updates if present
|
|
105
|
+
await based.from("preferences").upsert({ id: "alice--theme", value: "dark" });
|
|
106
|
+
|
|
107
|
+
// Same request, id passed separately
|
|
108
|
+
await posts.update("V1StGXR8_Z5jdHi6B", { title: "Renamed" });
|
|
109
|
+
|
|
110
|
+
// Delete
|
|
111
|
+
await posts.delete("V1StGXR8_Z5jdHi6B");
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`select` returns `{ data, total }` where `total` counts the matching rows before
|
|
115
|
+
pagination. `order` is `"<column>.asc"` or `"<column>.desc"`, checked against the
|
|
116
|
+
row type; omit it and the server sorts by `id` ascending so pagination is stable.
|
|
117
|
+
|
|
118
|
+
### `client.fetch`
|
|
119
|
+
|
|
120
|
+
The request everything else is built on: it prefixes the project URL, attaches
|
|
121
|
+
the anon key or the bearer token, and retries once after refreshing an expired
|
|
122
|
+
access token. Use it for anything the builder does not cover.
|
|
123
|
+
|
|
124
|
+
<!-- @typecheck -->
|
|
125
|
+
```ts
|
|
126
|
+
import { createClient } from "@weirdscience/based-client";
|
|
127
|
+
|
|
128
|
+
const based = createClient({ url, anonKey });
|
|
129
|
+
|
|
130
|
+
const res = await based.fetch("/api/posts?limit=5&order=createdAt.desc");
|
|
131
|
+
const body = (await res.json()) as { data: unknown[]; total: number };
|
|
132
|
+
```
|
|
133
|
+
|
|
32
134
|
## Hooks
|
|
33
135
|
|
|
34
136
|
### `useUser()`
|
|
35
137
|
|
|
36
138
|
Current authenticated user.
|
|
37
139
|
|
|
140
|
+
<!-- @typecheck -->
|
|
38
141
|
```tsx
|
|
39
142
|
import { useUser } from "@weirdscience/based-client";
|
|
40
143
|
|
|
@@ -42,36 +145,64 @@ function Profile() {
|
|
|
42
145
|
const { user, isLoading } = useUser();
|
|
43
146
|
if (isLoading) return <p>...</p>;
|
|
44
147
|
if (!user) return <p>Not logged in</p>;
|
|
45
|
-
return <p>{user.email}</p>;
|
|
148
|
+
return <p>{user.email} ({user.role})</p>;
|
|
46
149
|
}
|
|
47
150
|
```
|
|
48
151
|
|
|
152
|
+
`user` is `{ id, email, role }`, where `role` is `"owner"` for the first account
|
|
153
|
+
created on the project and `"member"` for everyone after. Only an owner can
|
|
154
|
+
manage schema. `error` is part of the return shape for symmetry with the other
|
|
155
|
+
hooks, and is always `null` today — the hook reads client state, it never fetches.
|
|
156
|
+
|
|
49
157
|
### `useQuery(table, options?)`
|
|
50
158
|
|
|
51
159
|
Read rows from a table. Returns `{ data, total, isLoading, error, refetch }`.
|
|
52
160
|
|
|
161
|
+
<!-- @typecheck -->
|
|
53
162
|
```tsx
|
|
54
163
|
import { useQuery } from "@weirdscience/based-client";
|
|
55
164
|
|
|
56
165
|
const { data, total, isLoading } = useQuery("posts", {
|
|
57
166
|
filter: { status: "published" },
|
|
167
|
+
order: "createdAt.desc",
|
|
58
168
|
limit: 20,
|
|
59
169
|
offset: 0,
|
|
60
170
|
});
|
|
61
171
|
```
|
|
62
172
|
|
|
173
|
+
`limit` defaults to 20 and the server caps it at 100 — a larger value is clamped,
|
|
174
|
+
not rejected. `order` is `"<column>.asc"` or `"<column>.desc"`, and the column is
|
|
175
|
+
checked against the table's type. Omit it and rows come back ordered by `id`
|
|
176
|
+
ascending, so pagination stays stable.
|
|
177
|
+
|
|
178
|
+
`error` is a `HookError` — `{ code, message, status?, details? }`, the server's
|
|
179
|
+
error code and HTTP status, never an `Error` instance.
|
|
180
|
+
|
|
63
181
|
Gate the query behind auth or any boolean with `enabled`:
|
|
64
182
|
|
|
183
|
+
<!-- @typecheck -->
|
|
65
184
|
```tsx
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
185
|
+
import { useQuery, useUser } from "@weirdscience/based-client";
|
|
186
|
+
|
|
187
|
+
function Notes() {
|
|
188
|
+
const { user } = useUser();
|
|
189
|
+
const { data, isLoading } = useQuery("notes", { enabled: !!user });
|
|
190
|
+
// Won't fire until `user` is truthy — avoids a 403 when signed out.
|
|
191
|
+
// While disabled: `data` is null and `isLoading` is false.
|
|
192
|
+
return <p>{isLoading ? "..." : `${data?.length ?? 0} notes`}</p>;
|
|
193
|
+
}
|
|
69
194
|
```
|
|
70
195
|
|
|
196
|
+
Queries refetch on their own when the signed-in user changes, so signing out
|
|
197
|
+
clears the previous user's rows instead of leaving them on screen. Responses
|
|
198
|
+
that arrive out of order are discarded — only the newest request can write to
|
|
199
|
+
`data`.
|
|
200
|
+
|
|
71
201
|
### `useRecord(table, id, options?)`
|
|
72
202
|
|
|
73
203
|
Fetch a single record by id via `GET /api/:table/:id`. Returns `{ data, isLoading, error, refetch }` — `data` is a single row, not an array.
|
|
74
204
|
|
|
205
|
+
<!-- @typecheck -->
|
|
75
206
|
```tsx
|
|
76
207
|
import { useRecord } from "@weirdscience/based-client";
|
|
77
208
|
|
|
@@ -79,7 +210,7 @@ function Post({ id }: { id: string }) {
|
|
|
79
210
|
const { data: post, isLoading } = useRecord("posts", id);
|
|
80
211
|
if (isLoading) return <Spinner />;
|
|
81
212
|
if (!post) return <NotFound />;
|
|
82
|
-
return <h1>{post.title}</h1>;
|
|
213
|
+
return <h1>{String(post.title)}</h1>;
|
|
83
214
|
}
|
|
84
215
|
```
|
|
85
216
|
|
|
@@ -97,36 +228,64 @@ const { data: prefs } = useRecord("preferences", `${user.id}:theme`);
|
|
|
97
228
|
|
|
98
229
|
### `useMutation(table, operation)`
|
|
99
230
|
|
|
100
|
-
Write rows. `operation` is `"create" | "update" | "delete"`. Returns `{ mutate, isLoading, error }`.
|
|
231
|
+
Write rows. `operation` is `"create" | "update" | "upsert" | "delete"`. Returns `{ mutate, isLoading, error }`.
|
|
232
|
+
|
|
233
|
+
`mutate` resolves to the row on success and to `null` on failure — it never
|
|
234
|
+
throws, so `onClick={() => mutate(...)}` can't produce an unhandled rejection.
|
|
235
|
+
Check the returned value or `error` (`{ code, message, status?, details? }`)
|
|
236
|
+
instead of reaching for `try/catch`.
|
|
101
237
|
|
|
238
|
+
| Operation | `mutate(data)` | Request |
|
|
239
|
+
| --- | --- | --- |
|
|
240
|
+
| `create` | `{ ...fields }` | `POST /api/:table` — 201, unknown keys rejected |
|
|
241
|
+
| `update` | `{ id, ...fields }` | `PUT /api/:table/:id` — upsert: 201 on create, 200 on update |
|
|
242
|
+
| `upsert` | `{ id, ...fields }` | Same request as `update`; reads better when you pick the id |
|
|
243
|
+
| `delete` | `{ id }` | `DELETE /api/:table/:id` |
|
|
244
|
+
|
|
245
|
+
<!-- @typecheck -->
|
|
102
246
|
```tsx
|
|
103
247
|
import { useMutation } from "@weirdscience/based-client";
|
|
104
248
|
|
|
105
249
|
function NewPost() {
|
|
106
|
-
const { mutate, isLoading } = useMutation("posts", "create");
|
|
250
|
+
const { mutate, isLoading, error } = useMutation("posts", "create");
|
|
251
|
+
|
|
252
|
+
async function onCreate() {
|
|
253
|
+
const created = await mutate({ title: "Hello", content: "World" });
|
|
254
|
+
if (!created) return; // `error` holds { code, message, status?, details? }
|
|
255
|
+
}
|
|
256
|
+
|
|
107
257
|
return (
|
|
108
|
-
|
|
109
|
-
onClick={
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Create
|
|
113
|
-
</button>
|
|
258
|
+
<>
|
|
259
|
+
<button onClick={onCreate} disabled={isLoading}>Create</button>
|
|
260
|
+
{error && <p>{error.message}</p>}
|
|
261
|
+
</>
|
|
114
262
|
);
|
|
115
263
|
}
|
|
116
264
|
```
|
|
117
265
|
|
|
118
|
-
`update` and `delete` require an `id` field:
|
|
266
|
+
`update`, `upsert`, and `delete` all require an `id` field:
|
|
119
267
|
|
|
268
|
+
<!-- @typecheck -->
|
|
120
269
|
```tsx
|
|
121
|
-
|
|
122
|
-
await update({ id: "abc", title: "Renamed" });
|
|
270
|
+
import { useMutation } from "@weirdscience/based-client";
|
|
123
271
|
|
|
124
|
-
|
|
125
|
-
|
|
272
|
+
function PostActions({ id }: { id: string }) {
|
|
273
|
+
const { mutate: update, error } = useMutation("posts", "update");
|
|
274
|
+
const { mutate: remove } = useMutation("posts", "delete");
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<>
|
|
278
|
+
<button onClick={() => update({ id, title: "Renamed" })}>Rename</button>
|
|
279
|
+
<button onClick={() => remove({ id })}>Delete</button>
|
|
280
|
+
{error && <p>{error.message}</p>}
|
|
281
|
+
</>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
126
284
|
```
|
|
127
285
|
|
|
128
286
|
## Auth
|
|
129
287
|
|
|
288
|
+
<!-- @typecheck -->
|
|
130
289
|
```tsx
|
|
131
290
|
import { useBasedClient } from "@weirdscience/based-client";
|
|
132
291
|
|
|
@@ -156,27 +315,77 @@ Methods on `client.auth`:
|
|
|
156
315
|
|
|
157
316
|
- `signUp(email, password)` → creates an account and signs in
|
|
158
317
|
- `signIn(email, password)` → signs in
|
|
159
|
-
- `signOut()` →
|
|
318
|
+
- `signOut()` → deletes this account's sessions on the server
|
|
160
319
|
- `refreshSession()` → manually refresh (happens automatically on 401)
|
|
320
|
+
- `getUser()` → the current user from `/auth/me`
|
|
321
|
+
- `onAuthStateChange(cb)` → calls `cb(state)` on every auth change, returns an unsubscribe function
|
|
322
|
+
|
|
323
|
+
`signUp` and `signIn` throw a `BasedError` carrying the server's `code`, `message`, HTTP `status`, and optional `details`:
|
|
324
|
+
|
|
325
|
+
<!-- @typecheck -->
|
|
326
|
+
```tsx
|
|
327
|
+
import { BasedError, createClient } from "@weirdscience/based-client";
|
|
328
|
+
|
|
329
|
+
const client = createClient({ url, anonKey });
|
|
330
|
+
|
|
331
|
+
async function signIn(email: string, password: string) {
|
|
332
|
+
try {
|
|
333
|
+
await client.auth.signIn(email, password);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
if (err instanceof BasedError && err.status === 401) {
|
|
336
|
+
return "Wrong email or password";
|
|
337
|
+
}
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Subscribe to every auth change — sign in, sign out, refresh, rehydration:
|
|
344
|
+
|
|
345
|
+
<!-- @typecheck -->
|
|
346
|
+
```ts
|
|
347
|
+
import { createClient } from "@weirdscience/based-client";
|
|
161
348
|
|
|
162
|
-
|
|
349
|
+
const client = createClient({ url, anonKey });
|
|
350
|
+
|
|
351
|
+
const unsubscribe = client.auth.onAuthStateChange((state) => {
|
|
352
|
+
console.log(state.user?.email ?? "signed out", state.isLoading);
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Access tokens auto-refresh on `401`. Concurrent requests that all see a `401` share a single refresh — the server rotates the refresh token, so parallel refreshes would sign the user out.
|
|
163
357
|
|
|
164
358
|
## Session persistence
|
|
165
359
|
|
|
166
|
-
Sessions persist across page reloads via `localStorage` by default
|
|
360
|
+
Sessions persist across page reloads via `localStorage` by default, under the key
|
|
361
|
+
`based.session`. On mount, the client restores the saved session and validates it
|
|
362
|
+
by calling `/auth/me`.
|
|
363
|
+
|
|
364
|
+
Tokens in `localStorage` are readable by any script running on your origin, so an
|
|
365
|
+
XSS bug leaks the session. That is the deliberate tradeoff for a session that
|
|
366
|
+
survives a reload without a cookie backend — pass your own `storage` adapter (or
|
|
367
|
+
`storage: false`) if your threat model needs something stricter.
|
|
167
368
|
|
|
168
369
|
Use `client.ready()` or `useUser().isLoading` to avoid flashing a logged-out UI during hydration:
|
|
169
370
|
|
|
371
|
+
<!-- @typecheck -->
|
|
170
372
|
```tsx
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
373
|
+
import { useUser } from "@weirdscience/based-client";
|
|
374
|
+
|
|
375
|
+
function Gate() {
|
|
376
|
+
const { user, isLoading } = useUser();
|
|
377
|
+
if (isLoading) return <Spinner />;
|
|
378
|
+
if (!user) return <LoginForm />;
|
|
379
|
+
return <Dashboard user={user} />;
|
|
380
|
+
}
|
|
175
381
|
```
|
|
176
382
|
|
|
177
|
-
Opt out or use a custom storage adapter:
|
|
383
|
+
Opt out, pick your own key, or use a custom storage adapter:
|
|
384
|
+
|
|
385
|
+
<!-- @typecheck -->
|
|
386
|
+
```ts
|
|
387
|
+
import { createClient } from "@weirdscience/based-client";
|
|
178
388
|
|
|
179
|
-
```tsx
|
|
180
389
|
// Disable entirely (in-memory only)
|
|
181
390
|
createClient({ url, anonKey, storage: false });
|
|
182
391
|
|
|
@@ -184,6 +393,7 @@ createClient({ url, anonKey, storage: false });
|
|
|
184
393
|
createClient({
|
|
185
394
|
url,
|
|
186
395
|
anonKey,
|
|
396
|
+
storageKey: "my-app.session",
|
|
187
397
|
storage: {
|
|
188
398
|
getItem: (k) => AsyncStorage.getItem(k),
|
|
189
399
|
setItem: (k, v) => AsyncStorage.setItem(k, v),
|
|
@@ -203,47 +413,55 @@ based typegen
|
|
|
203
413
|
# writes based.d.ts
|
|
204
414
|
```
|
|
205
415
|
|
|
206
|
-
|
|
416
|
+
Import it as `./based` — never `./based.d.ts`, which TypeScript refuses — and
|
|
417
|
+
pass `Tables` as a generic:
|
|
207
418
|
|
|
419
|
+
<!-- @typecheck -->
|
|
208
420
|
```tsx
|
|
209
|
-
import type { Tables } from "./based
|
|
421
|
+
import type { Insert, Tables } from "./based";
|
|
210
422
|
import { useQuery, useMutation } from "@weirdscience/based-client";
|
|
211
423
|
|
|
212
|
-
// data is typed as Tables["posts"][]
|
|
424
|
+
// data is typed as Tables["posts"][] | null
|
|
213
425
|
const { data } = useQuery<Tables, "posts">("posts", {
|
|
214
426
|
filter: { status: "published" }, // typed keys
|
|
427
|
+
order: "createdAt.desc", // typed columns
|
|
215
428
|
});
|
|
216
429
|
|
|
430
|
+
// mutate() takes an Insert<"posts">: the row minus the columns the server
|
|
431
|
+
// fills in (id, createdAt, updatedAt)
|
|
217
432
|
const { mutate } = useMutation<Tables, "posts">("posts", "create");
|
|
218
|
-
|
|
433
|
+
const draft: Insert<"posts"> = {
|
|
434
|
+
title: "Hello",
|
|
435
|
+
content: "World",
|
|
436
|
+
status: "draft",
|
|
437
|
+
};
|
|
438
|
+
await mutate(draft);
|
|
219
439
|
```
|
|
220
440
|
|
|
221
441
|
Re-run `based typegen` after any schema change.
|
|
222
442
|
|
|
223
|
-
##
|
|
443
|
+
## Access policies
|
|
444
|
+
|
|
445
|
+
Each table has one policy, and `based table access <table> <policy>` changes it:
|
|
224
446
|
|
|
225
|
-
|
|
447
|
+
- **`owner-scoped`** (default when the table has a `user_id` column) — every
|
|
448
|
+
signed-in user reads and writes only their own rows; `user_id` is forced to the
|
|
449
|
+
caller on write; anon is rejected.
|
|
450
|
+
- **`public-read`** — anyone, anon included, reads every row; only the project
|
|
451
|
+
owner writes.
|
|
452
|
+
- **`private`** (default without a `user_id` column) — only the project owner
|
|
453
|
+
reads or writes.
|
|
226
454
|
|
|
227
455
|
```bash
|
|
228
456
|
based table create notes user_id:text:required title:text:required body:text
|
|
457
|
+
based table access posts public-read
|
|
229
458
|
```
|
|
230
459
|
|
|
231
|
-
|
|
460
|
+
With `owner-scoped`:
|
|
232
461
|
|
|
233
462
|
- `useQuery("notes")` only returns the caller's notes
|
|
234
463
|
- `useMutation("notes", "create")` auto-fills `user_id`
|
|
235
|
-
-
|
|
236
|
-
|
|
237
|
-
## Upsert
|
|
238
|
-
|
|
239
|
-
`PUT /api/:table/:id` creates if missing, updates if present. From the SDK:
|
|
240
|
-
|
|
241
|
-
```tsx
|
|
242
|
-
const { mutate: upsert } = useMutation("preferences", "update");
|
|
243
|
-
|
|
244
|
-
// The URL id becomes the row id — perfect for deterministic keys like userId:key
|
|
245
|
-
await upsert({ id: "alice:theme", value: "dark" });
|
|
246
|
-
```
|
|
464
|
+
- other users' rows return 404
|
|
247
465
|
|
|
248
466
|
## Links
|
|
249
467
|
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAGlB,WAAW,EAEX,aAAa,EAId,MAAM,SAAS,CAAC;AAsBjB,wBAAgB,YAAY,CAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,aAAa,EACjE,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAGlB,WAAW,EAEX,aAAa,EAId,MAAM,SAAS,CAAC;AAsBjB,wBAAgB,YAAY,CAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,aAAa,EACjE,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAAC,CAAC,CAAC,CAwY7C"}
|