jazz-tools 2.0.0-alpha.21 → 2.0.0-alpha.22
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/bin/docs-index.db +0 -0
- package/bin/docs-index.txt +1624 -542
- package/bin/jazz-tools.js +19 -40
- package/bin/native/jazz-tools-darwin-arm64 +0 -0
- package/bin/native/jazz-tools-darwin-x64 +0 -0
- package/bin/native/jazz-tools-linux-arm64 +0 -0
- package/bin/native/jazz-tools-linux-x64 +0 -0
- package/bin/native/jazz-tools-windows-x64.exe +0 -0
- package/dist/backend/create-jazz-context.d.ts +31 -6
- package/dist/backend/create-jazz-context.d.ts.map +1 -1
- package/dist/backend/create-jazz-context.js +35 -5
- package/dist/backend/create-jazz-context.js.map +1 -1
- package/dist/backend/create-jazz-context.test.js +61 -6
- package/dist/backend/create-jazz-context.test.js.map +1 -1
- package/dist/cli.d.ts +29 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +648 -246
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +512 -297
- package/dist/cli.test.js.map +1 -1
- package/dist/codegen/schema-reader.d.ts.map +1 -1
- package/dist/codegen/schema-reader.js +6 -1
- package/dist/codegen/schema-reader.js.map +1 -1
- package/dist/dev-tools/dev-tools.d.ts.map +1 -1
- package/dist/dev-tools/dev-tools.js +61 -13
- package/dist/dev-tools/dev-tools.js.map +1 -1
- package/dist/dev-tools/dev-tools.test.js +166 -0
- package/dist/dev-tools/dev-tools.test.js.map +1 -1
- package/dist/dev-tools/extension-panel.d.ts.map +1 -1
- package/dist/dev-tools/extension-panel.js +30 -7
- package/dist/dev-tools/extension-panel.js.map +1 -1
- package/dist/dev-tools/protocol.d.ts +49 -1
- package/dist/dev-tools/protocol.d.ts.map +1 -1
- package/dist/dev-tools/protocol.js +3 -0
- package/dist/dev-tools/protocol.js.map +1 -1
- package/dist/drivers/index.d.ts +1 -1
- package/dist/drivers/index.d.ts.map +1 -1
- package/dist/drivers/schema-wire.d.ts.map +1 -1
- package/dist/drivers/schema-wire.js +12 -1
- package/dist/drivers/schema-wire.js.map +1 -1
- package/dist/drivers/schema-wire.test.d.ts +2 -0
- package/dist/drivers/schema-wire.test.d.ts.map +1 -0
- package/dist/drivers/schema-wire.test.js +31 -0
- package/dist/drivers/schema-wire.test.js.map +1 -0
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/dsl.d.ts +139 -95
- package/dist/dsl.d.ts.map +1 -1
- package/dist/dsl.js +64 -8
- package/dist/dsl.js.map +1 -1
- package/dist/dsl.test.js +78 -8
- package/dist/dsl.test.js.map +1 -1
- package/dist/index.d.ts +32 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/magic-columns.d.ts +3 -1
- package/dist/magic-columns.d.ts.map +1 -1
- package/dist/magic-columns.js +20 -4
- package/dist/magic-columns.js.map +1 -1
- package/dist/mcp/build-index.test.js +1 -1
- package/dist/migrations.d.ts +126 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +112 -0
- package/dist/migrations.js.map +1 -0
- package/dist/permissions/index.test.js +35 -0
- package/dist/permissions/index.test.js.map +1 -1
- package/dist/react-native/create-jazz-client.test.js +62 -42
- package/dist/react-native/create-jazz-client.test.js.map +1 -1
- package/dist/react-native/jazz-rn-runtime-adapter.d.ts +18 -3
- package/dist/react-native/jazz-rn-runtime-adapter.d.ts.map +1 -1
- package/dist/react-native/jazz-rn-runtime-adapter.js +110 -6
- package/dist/react-native/jazz-rn-runtime-adapter.js.map +1 -1
- package/dist/react-native/jazz-rn-runtime-adapter.test.js +149 -4
- package/dist/react-native/jazz-rn-runtime-adapter.test.js.map +1 -1
- package/dist/reconcile-array.d.ts +29 -0
- package/dist/reconcile-array.d.ts.map +1 -0
- package/dist/reconcile-array.js +110 -0
- package/dist/reconcile-array.js.map +1 -0
- package/dist/reconcile-array.test.d.ts +2 -0
- package/dist/reconcile-array.test.d.ts.map +1 -0
- package/dist/reconcile-array.test.js +118 -0
- package/dist/reconcile-array.test.js.map +1 -0
- package/dist/runtime/client.d.ts +24 -20
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.for-request.test.js +8 -8
- package/dist/runtime/client.for-request.test.js.map +1 -1
- package/dist/runtime/client.js +58 -25
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/client.mutations.test.js +72 -1
- package/dist/runtime/client.mutations.test.js.map +1 -1
- package/dist/runtime/cloud-server.integration.test.js +145 -88
- package/dist/runtime/cloud-server.integration.test.js.map +1 -1
- package/dist/runtime/db.d.ts +3 -7
- package/dist/runtime/db.d.ts.map +1 -1
- package/dist/runtime/db.js +16 -14
- package/dist/runtime/db.js.map +1 -1
- package/dist/runtime/db.schema-order.test.js +8 -8
- package/dist/runtime/db.schema-order.test.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/napi.integration.test.js +113 -136
- package/dist/runtime/napi.integration.test.js.map +1 -1
- package/dist/runtime/query-adapter.d.ts.map +1 -1
- package/dist/runtime/query-adapter.js +22 -2
- package/dist/runtime/query-adapter.js.map +1 -1
- package/dist/runtime/query-adapter.test.js +81 -5
- package/dist/runtime/query-adapter.test.js.map +1 -1
- package/dist/runtime/row-transformer.js +2 -2
- package/dist/runtime/row-transformer.js.map +1 -1
- package/dist/runtime/row-transformer.test.js +9 -9
- package/dist/runtime/row-transformer.test.js.map +1 -1
- package/dist/runtime/schema-fetch.d.ts +103 -1
- package/dist/runtime/schema-fetch.d.ts.map +1 -1
- package/dist/runtime/schema-fetch.js +106 -0
- package/dist/runtime/schema-fetch.js.map +1 -1
- package/dist/runtime/sync-transport.d.ts.map +1 -1
- package/dist/runtime/sync-transport.js +15 -0
- package/dist/runtime/sync-transport.js.map +1 -1
- package/dist/runtime/sync-transport.test.js +33 -0
- package/dist/runtime/sync-transport.test.js.map +1 -1
- package/dist/runtime/value-converter.d.ts +9 -6
- package/dist/runtime/value-converter.d.ts.map +1 -1
- package/dist/runtime/value-converter.js +22 -9
- package/dist/runtime/value-converter.js.map +1 -1
- package/dist/runtime/value-converter.test.js +32 -26
- package/dist/runtime/value-converter.test.js.map +1 -1
- package/dist/schema-loader.d.ts +14 -0
- package/dist/schema-loader.d.ts.map +1 -0
- package/dist/schema-loader.js +217 -0
- package/dist/schema-loader.js.map +1 -0
- package/dist/schema-permissions.d.ts +8 -0
- package/dist/schema-permissions.d.ts.map +1 -0
- package/dist/schema-permissions.js +266 -0
- package/dist/schema-permissions.js.map +1 -0
- package/dist/schema-permissions.test.d.ts +2 -0
- package/dist/schema-permissions.test.d.ts.map +1 -0
- package/dist/schema-permissions.test.js +43 -0
- package/dist/schema-permissions.test.js.map +1 -0
- package/dist/schema.d.ts +11 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/svelte/context.svelte.test.js +50 -0
- package/dist/svelte/rune-patterns.svelte.test.js +301 -0
- package/dist/svelte/test-helpers.svelte.js +14 -0
- package/dist/svelte/use-all.svelte.d.ts.map +1 -1
- package/dist/svelte/use-all.svelte.js +7 -1
- package/dist/testing/fixtures/basic/schema.d.ts +11 -0
- package/dist/testing/fixtures/basic/schema.d.ts.map +1 -0
- package/dist/testing/fixtures/basic/schema.js +10 -0
- package/dist/testing/fixtures/basic/schema.js.map +1 -0
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/index.test.js +109 -9
- package/dist/testing/index.test.js.map +1 -1
- package/dist/testing/local-jazz-server.d.ts +2 -0
- package/dist/testing/local-jazz-server.d.ts.map +1 -1
- package/dist/testing/local-jazz-server.js +21 -51
- package/dist/testing/local-jazz-server.js.map +1 -1
- package/dist/testing/policy-test-app.d.ts.map +1 -1
- package/dist/testing/policy-test-app.js +71 -3
- package/dist/testing/policy-test-app.js.map +1 -1
- package/dist/typed-app.d.ts +364 -0
- package/dist/typed-app.d.ts.map +1 -0
- package/dist/{testing/fixtures/basic/app.js → typed-app.js} +118 -30
- package/dist/typed-app.js.map +1 -0
- package/dist/vue/use-all.d.ts +2 -2
- package/dist/vue/use-all.d.ts.map +1 -1
- package/dist/vue/use-all.js +9 -3
- package/dist/vue/use-all.js.map +1 -1
- package/dist/vue/use-all.test.js +137 -0
- package/dist/vue/use-all.test.js.map +1 -1
- package/package.json +15 -13
- package/dist/codegen/codegen.test.d.ts +0 -2
- package/dist/codegen/codegen.test.d.ts.map +0 -1
- package/dist/codegen/codegen.test.js +0 -1134
- package/dist/codegen/codegen.test.js.map +0 -1
- package/dist/codegen/index.d.ts +0 -18
- package/dist/codegen/index.d.ts.map +0 -1
- package/dist/codegen/index.js +0 -22
- package/dist/codegen/index.js.map +0 -1
- package/dist/codegen/query-builder-generator.d.ts +0 -26
- package/dist/codegen/query-builder-generator.d.ts.map +0 -1
- package/dist/codegen/query-builder-generator.js +0 -377
- package/dist/codegen/query-builder-generator.js.map +0 -1
- package/dist/codegen/type-generator.d.ts +0 -30
- package/dist/codegen/type-generator.d.ts.map +0 -1
- package/dist/codegen/type-generator.js +0 -368
- package/dist/codegen/type-generator.js.map +0 -1
- package/dist/runtime/napi.fjall.db.all.integration.test.d.ts +0 -2
- package/dist/runtime/napi.fjall.db.all.integration.test.d.ts.map +0 -1
- package/dist/runtime/napi.fjall.db.all.integration.test.js +0 -76
- package/dist/runtime/napi.fjall.db.all.integration.test.js.map +0 -1
- package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts +0 -2
- package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts.map +0 -1
- package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js +0 -47
- package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js.map +0 -1
- package/dist/runtime/napi.fjall.test-helpers.d.ts +0 -34
- package/dist/runtime/napi.fjall.test-helpers.d.ts.map +0 -1
- package/dist/runtime/napi.fjall.test-helpers.js +0 -172
- package/dist/runtime/napi.fjall.test-helpers.js.map +0 -1
- package/dist/sql-gen.d.ts +0 -5
- package/dist/sql-gen.d.ts.map +0 -1
- package/dist/sql-gen.js +0 -234
- package/dist/sql-gen.js.map +0 -1
- package/dist/sql-gen.test.d.ts +0 -2
- package/dist/sql-gen.test.d.ts.map +0 -1
- package/dist/sql-gen.test.js +0 -481
- package/dist/sql-gen.test.js.map +0 -1
- package/dist/svelte/context.test.d.ts +0 -2
- package/dist/svelte/context.test.d.ts.map +0 -1
- package/dist/svelte/context.test.js +0 -55
- package/dist/svelte/use-all.test.d.ts +0 -2
- package/dist/svelte/use-all.test.d.ts.map +0 -1
- package/dist/svelte/use-all.test.js +0 -147
- package/dist/testing/fixtures/basic/app.d.ts +0 -59
- package/dist/testing/fixtures/basic/app.d.ts.map +0 -1
- package/dist/testing/fixtures/basic/app.js.map +0 -1
- package/dist/testing/fixtures/basic/current.d.ts +0 -2
- package/dist/testing/fixtures/basic/current.d.ts.map +0 -1
- package/dist/testing/fixtures/basic/current.js +0 -6
- package/dist/testing/fixtures/basic/current.js.map +0 -1
package/bin/docs-index.txt
CHANGED
|
@@ -14,11 +14,6 @@ Browser clients default to anonymous local auth when no JWT/backend auth is conf
|
|
|
14
14
|
On first load, Jazz auto-generates a device token and persists it in `localStorage`.
|
|
15
15
|
|
|
16
16
|
```tsx
|
|
17
|
-
const anonymousAuthClient = createJazzClient({
|
|
18
|
-
appId: "my-app",
|
|
19
|
-
env: "dev",
|
|
20
|
-
userBranch: "main",
|
|
21
|
-
});
|
|
22
17
|
|
|
23
18
|
return (
|
|
24
19
|
|
|
@@ -73,11 +68,6 @@ const client = createJazzClient({
|
|
|
73
68
|
If you want to force a specific device token, pass `localAuthToken` as an explicit override:
|
|
74
69
|
|
|
75
70
|
```tsx
|
|
76
|
-
const anonymousAuthWithTokenClient = createJazzClient({
|
|
77
|
-
appId: "my-app",
|
|
78
|
-
localAuthMode: "anonymous",
|
|
79
|
-
localAuthToken: "device-token-123",
|
|
80
|
-
});
|
|
81
71
|
|
|
82
72
|
return (
|
|
83
73
|
|
|
@@ -138,12 +128,6 @@ When you want to test multi-user flows locally, switch to `demo` profiles and ex
|
|
|
138
128
|
```tsx
|
|
139
129
|
const demoAuthAppId = "my-app";
|
|
140
130
|
const demoAuthActive = getActiveSyntheticAuth(demoAuthAppId, { defaultMode: "demo" });
|
|
141
|
-
const demoAuthClient = createJazzClient({
|
|
142
|
-
appId: demoAuthAppId,
|
|
143
|
-
serverUrl: "http://127.0.0.1:4200",
|
|
144
|
-
localAuthMode: demoAuthActive.localAuthMode,
|
|
145
|
-
localAuthToken: demoAuthActive.localAuthToken,
|
|
146
|
-
});
|
|
147
131
|
|
|
148
132
|
return (
|
|
149
133
|
<>
|
|
@@ -242,15 +226,6 @@ Use `useLinkExternalIdentity` right after sign-in to preserve anon/demo-created
|
|
|
242
226
|
const externalAuthAppId = "my-app";
|
|
243
227
|
const externalAuthServerUrl = "http://127.0.0.1:4200";
|
|
244
228
|
const externalAuthProviderJwt = "<provider-jwt>";
|
|
245
|
-
const externalAuthLocalClient = createJazzClient({
|
|
246
|
-
appId: externalAuthAppId,
|
|
247
|
-
serverUrl: externalAuthServerUrl,
|
|
248
|
-
});
|
|
249
|
-
const externalAuthJwtClient = createJazzClient({
|
|
250
|
-
appId: externalAuthAppId,
|
|
251
|
-
serverUrl: externalAuthServerUrl,
|
|
252
|
-
jwtToken: externalAuthProviderJwt,
|
|
253
|
-
});
|
|
254
229
|
|
|
255
230
|
const [hasJwt, setHasJwt] = useState(false);
|
|
256
231
|
const linkExternalIdentity = useLinkExternalIdentity({
|
|
@@ -372,6 +347,122 @@ Use `JazzClient.linkExternalIdentity(...)` directly:
|
|
|
372
347
|
|
|
373
348
|
After linking, external JWT sessions resolve to the same principal.
|
|
374
349
|
|
|
350
|
+
## Use the Current Session in Client Code
|
|
351
|
+
|
|
352
|
+
When you want to attach rows to the current user, or fetch only the rows relevant to that
|
|
353
|
+
user, read the resolved session in your client and derive a user id from it.
|
|
354
|
+
|
|
355
|
+
### 1. Get the Session and User ID
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
const session = useSession();
|
|
359
|
+
```
|
|
360
|
+
```tsx
|
|
361
|
+
const sessionUserId = session?.user_id ?? null;
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
```vue
|
|
365
|
+
const session = useSession();
|
|
366
|
+
```
|
|
367
|
+
```vue
|
|
368
|
+
const sessionUserId = session?.user_id ?? null;
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```svelte
|
|
372
|
+
const session = getSession();
|
|
373
|
+
```
|
|
374
|
+
```svelte
|
|
375
|
+
const sessionUserId = $derived(session?.user_id ?? null);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
const session = await resolveClientSession(config);
|
|
380
|
+
```
|
|
381
|
+
```ts
|
|
382
|
+
const sessionUserId = session?.user_id ?? null;
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 2. Read Rows Relevant to the Current User
|
|
386
|
+
|
|
387
|
+
Use your framework's normal reactive read primitive with the same user-scoped filter.
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
const ownedTodos =
|
|
391
|
+
useAll(sessionUserId ? app.todos.where({ ownerId: sessionUserId }) : undefined) ?? [];
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```vue
|
|
395
|
+
const ownedTodos = useAll(app.todos.where({ ownerId: sessionUserId ?? "__no-session__" }));
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
```svelte
|
|
399
|
+
const ownedTodos = new QuerySubscription(
|
|
400
|
+
app.todos.where({ ownerId: sessionUserId ?? '__no-session__' }),
|
|
401
|
+
);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
const ownedTodos = sessionUserId ? await db.all(app.todos.where({ ownerId: sessionUserId })) : [];
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### 3. Insert a User-Owned Row
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
function addOwnedTodo(title: string) {
|
|
412
|
+
if (!sessionUserId) return;
|
|
413
|
+
|
|
414
|
+
db.insert(app.todos, {
|
|
415
|
+
title,
|
|
416
|
+
done: false,
|
|
417
|
+
ownerId: sessionUserId,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
```vue
|
|
423
|
+
function addOwnedTodo(title: string) {
|
|
424
|
+
if (!sessionUserId) return;
|
|
425
|
+
|
|
426
|
+
db.insert(app.todos, {
|
|
427
|
+
title,
|
|
428
|
+
done: false,
|
|
429
|
+
ownerId: sessionUserId,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
```svelte
|
|
435
|
+
function addOwnedTodo(title: string) {
|
|
436
|
+
if (!sessionUserId) return;
|
|
437
|
+
|
|
438
|
+
db.insert(app.todos, {
|
|
439
|
+
title,
|
|
440
|
+
done: false,
|
|
441
|
+
ownerId: sessionUserId,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
function addOwnedTodo(title: string) {
|
|
448
|
+
if (!sessionUserId) return;
|
|
449
|
+
|
|
450
|
+
db.insert(app.todos, {
|
|
451
|
+
title,
|
|
452
|
+
done: false,
|
|
453
|
+
ownerId: sessionUserId,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Use whatever column matches your schema, such as `owner_id`, `author_id`, `assignee_id`, or
|
|
459
|
+
`user_id`. The same pattern works whether a row is strictly owned by one user or simply scoped
|
|
460
|
+
to a user.
|
|
461
|
+
|
|
462
|
+
These client-side filters are useful for UX, but they do not make data private on their own.
|
|
463
|
+
Use [Permissions](/docs/permissions) to define row-level policies that actually enforce which
|
|
464
|
+
sessions can read or mutate a row.
|
|
465
|
+
|
|
375
466
|
## Session Resolution Order
|
|
376
467
|
|
|
377
468
|
For `jazz-tools server`:
|
|
@@ -408,7 +499,6 @@ jazz-tools server "$JAZZ_APP_ID" \
|
|
|
408
499
|
### Offline-only mode
|
|
409
500
|
|
|
410
501
|
```tsx
|
|
411
|
-
const offlineOnlyAuthClient = createJazzClient({ appId: "my-app" });
|
|
412
502
|
|
|
413
503
|
return (
|
|
414
504
|
|
|
@@ -450,6 +540,410 @@ const client = createJazzClient({ appId: "my-app" });
|
|
|
450
540
|
}
|
|
451
541
|
```
|
|
452
542
|
|
|
543
|
+
===PAGE:files-and-blobs===
|
|
544
|
+
TITLE:Files & Blobs
|
|
545
|
+
DESCRIPTION:Chunked browser Blob and stream storage using conventional files and file_parts tables.
|
|
546
|
+
|
|
547
|
+
This page explains how to store images and other binary blobs in Jazz using browser `Blob`,
|
|
548
|
+
`File`, and `ReadableStream` values. These files benefit from the same sync, local-first storage,
|
|
549
|
+
and permission model as the rest of your structured data.
|
|
550
|
+
|
|
551
|
+
Use plain `s.bytes()` when the binary value is small, naturally belongs on the row itself, and
|
|
552
|
+
should always load together with the rest of that row.
|
|
553
|
+
|
|
554
|
+
## Add the Conventional Tables
|
|
555
|
+
|
|
556
|
+
`db.createFileFromBlob`, `db.createFileFromStream`, `db.loadFileAsBlob`, and
|
|
557
|
+
`db.loadFileAsStream` currently expect these exact table and column names on `app`:
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
file_parts: s.table({
|
|
561
|
+
data: s.bytes(),
|
|
562
|
+
}),
|
|
563
|
+
files: s.table({
|
|
564
|
+
name: s.string().optional(),
|
|
565
|
+
mimeType: s.string(),
|
|
566
|
+
partIds: s.array(s.ref("file_parts")),
|
|
567
|
+
partSizes: s.array(s.int()),
|
|
568
|
+
}),
|
|
569
|
+
uploads: s.table({
|
|
570
|
+
ownerId: s.string(),
|
|
571
|
+
label: s.string(),
|
|
572
|
+
fileId: s.ref("files"),
|
|
573
|
+
}),
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
`file_parts.data` stores the raw chunk bytes. `files.partIds` keeps the ordered chunk ids, and
|
|
577
|
+
`files.partSizes` stores each chunk's byte length in the same order.
|
|
578
|
+
|
|
579
|
+
In the example above, `uploads.file` is your app-owned reference to a stored file. `files.name`
|
|
580
|
+
is optional metadata; `createFileFromBlob(...)` fills it from `File.name` when available.
|
|
581
|
+
|
|
582
|
+
## Add Permissions
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
|
|
586
|
+
policy.uploads.allowRead.where({ ownerId: session.user_id });
|
|
587
|
+
policy.uploads.allowInsert.where({ ownerId: session.user_id });
|
|
588
|
+
policy.uploads.allowUpdate.where({ ownerId: session.user_id });
|
|
589
|
+
policy.uploads.allowDelete.where({ ownerId: session.user_id });
|
|
590
|
+
|
|
591
|
+
// Files are created before the parent upload row exists, so inserts are direct for now.
|
|
592
|
+
policy.files.allowInsert.where({});
|
|
593
|
+
policy.file_parts.allowInsert.where({});
|
|
594
|
+
|
|
595
|
+
policy.files.allowRead.where(allowedTo.readReferencing(policy.uploads, "fileId"));
|
|
596
|
+
policy.file_parts.allowRead.where(allowedTo.readReferencing(policy.files, "partIds"));
|
|
597
|
+
|
|
598
|
+
policy.files.allowDelete.where(allowedTo.deleteReferencing(policy.uploads, "fileId"));
|
|
599
|
+
policy.file_parts.allowDelete.where(allowedTo.deleteReferencing(policy.files, "partIds"));
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
`uploads` owns access. `files` inherit read and delete access from `uploads.file`, and
|
|
604
|
+
`file_parts` inherit from `files.partIds`.
|
|
605
|
+
|
|
606
|
+
`createFileFromBlob(...)` and `createFileFromStream(...)` create `file_parts` and `files` before
|
|
607
|
+
your parent row exists, so insert inheritance does not naturally apply yet. In this MVP, grant
|
|
608
|
+
direct insert access to the clients that may upload, or perform uploads in a trusted backend
|
|
609
|
+
context.
|
|
610
|
+
|
|
611
|
+
Files are write-once in this MVP, so `files` and `file_parts` normally do not need update
|
|
612
|
+
policies.
|
|
613
|
+
|
|
614
|
+
## Create from a Blob
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
|
|
618
|
+
const file = await db.createFileFromBlob(app, blob, { tier: "edge" });
|
|
619
|
+
|
|
620
|
+
return db.insertDurable(
|
|
621
|
+
app.uploads,
|
|
622
|
+
{
|
|
623
|
+
ownerId: EXAMPLE_OWNER_ID,
|
|
624
|
+
label: "Profile photo",
|
|
625
|
+
fileId: file.id,
|
|
626
|
+
},
|
|
627
|
+
{ tier: "edge" },
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
The helper chunks the blob, creates the `file_parts` rows first, then creates the `files` row,
|
|
633
|
+
and returns that file row so you can store its id on your own table.
|
|
634
|
+
|
|
635
|
+
## Create from a ReadableStream
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
|
|
639
|
+
const file = await db.createFileFromStream(app, stream, {
|
|
640
|
+
tier: "edge",
|
|
641
|
+
name: "camera.raw",
|
|
642
|
+
mimeType: "application/octet-stream",
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
return db.insertDurable(
|
|
646
|
+
app.uploads,
|
|
647
|
+
{
|
|
648
|
+
ownerId: EXAMPLE_OWNER_ID,
|
|
649
|
+
label: "Camera import",
|
|
650
|
+
fileId: file.id,
|
|
651
|
+
},
|
|
652
|
+
{ tier: "edge" },
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## Load as a Blob
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
|
|
661
|
+
const upload = await db.one(app.uploads.where({ id: uploadId }), { tier: "edge" });
|
|
662
|
+
if (!upload) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const blob = await db.loadFileAsBlob(app, upload.fileId, { tier: "edge" });
|
|
667
|
+
return blob;
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## Load as a ReadableStream
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
|
|
675
|
+
const upload = await db.one(app.uploads.where({ id: uploadId }), { tier: "edge" });
|
|
676
|
+
if (!upload) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const stream = await db.loadFileAsStream(app, upload.fileId, { tier: "edge" });
|
|
681
|
+
return stream;
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
`loadFileAsBlob(...)` and `loadFileAsStream(...)` first fetch the `files` row without includes,
|
|
686
|
+
then query each referenced `file_parts` row one at a time. That keeps loading naive and
|
|
687
|
+
sequential instead of eager-loading every chunk into one query.
|
|
688
|
+
|
|
689
|
+
## Durability and Incomplete Local Data
|
|
690
|
+
|
|
691
|
+
The file helpers forward normal Jazz durability options:
|
|
692
|
+
|
|
693
|
+
- Pass `tier` to `createFileFromBlob(...)` or `createFileFromStream(...)`.
|
|
694
|
+
- Pass query options like `{ tier: "edge" }` to `loadFileAsBlob(...)` or `loadFileAsStream(...)`.
|
|
695
|
+
|
|
696
|
+
If the requested tier does not have the full file yet, `loadFileAsBlob(...)` fails the whole
|
|
697
|
+
read and `loadFileAsStream(...)` errors when it reaches the missing part. Use `edge` or `global`
|
|
698
|
+
when you need the read to wait for a more complete remote snapshot than local OPFS currently has.
|
|
699
|
+
|
|
700
|
+
## No Automatic Cascade Yet
|
|
701
|
+
|
|
702
|
+
Until file cascade delete lands, delete the chunk rows and the `files` row before deleting your
|
|
703
|
+
parent app row so the inherited delete policies still match:
|
|
704
|
+
|
|
705
|
+
```ts
|
|
706
|
+
|
|
707
|
+
const upload = await db.one(app.uploads.where({ id: uploadId }), { tier: "edge" });
|
|
708
|
+
if (!upload) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const file = await db.one(app.files.where({ id: upload.fileId }), { tier: "edge" });
|
|
713
|
+
|
|
714
|
+
if (file) {
|
|
715
|
+
// Delete chunks and the file while the parent upload row still exists.
|
|
716
|
+
for (const partId of file.partIds) {
|
|
717
|
+
db.delete(app.file_parts, partId);
|
|
718
|
+
}
|
|
719
|
+
db.delete(app.files, file.id);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
db.delete(app.uploads, uploadId);
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
===PAGE:guides/framework-patterns===
|
|
727
|
+
TITLE:Framework Patterns
|
|
728
|
+
DESCRIPTION:Side-by-side reference for React, Vue, and Svelte Jazz APIs — subscriptions, database access, and dependency injection.
|
|
729
|
+
|
|
730
|
+
## API equivalents
|
|
731
|
+
|
|
732
|
+
| Concept | React / Expo | Vue | Svelte |
|
|
733
|
+
| --------------- | -------------------------------- | --------------------------------- | ------------------------------- |
|
|
734
|
+
| Provider | `` | `` | `` |
|
|
735
|
+
| Live query | `useAll(query)` | `useAll(query)` | `new QuerySubscription(query)` |
|
|
736
|
+
| DB access | `useDb()` | `useDb()` | `getDb()` |
|
|
737
|
+
| Session | `useSession()` | `useSession()` | `getSession()` |
|
|
738
|
+
| Client creation | `createJazzClient(config)` | `createJazzClient(config)` | `createJazzClient(config)` |
|
|
739
|
+
|
|
740
|
+
## Provider setup
|
|
741
|
+
|
|
742
|
+
All frameworks wrap the app in a provider that makes the database available to every
|
|
743
|
+
component in the tree — no prop drilling required.
|
|
744
|
+
|
|
745
|
+
```tsx
|
|
746
|
+
|
|
747
|
+
return (
|
|
748
|
+
Loading...</p>}>
|
|
749
|
+
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
````
|
|
753
|
+
|
|
754
|
+
```vue
|
|
755
|
+
<script setup lang="ts">
|
|
756
|
+
|
|
757
|
+
const client = createJazzClient(config);
|
|
758
|
+
|
|
759
|
+
</script>
|
|
760
|
+
|
|
761
|
+
<template>
|
|
762
|
+
|
|
763
|
+
<template #fallback>
|
|
764
|
+
<p>Loading...</p>
|
|
765
|
+
</template>
|
|
766
|
+
|
|
767
|
+
</template>
|
|
768
|
+
````
|
|
769
|
+
|
|
770
|
+
```svelte
|
|
771
|
+
<script lang="ts">
|
|
772
|
+
import { createJazzClient, JazzSvelteProvider } from "jazz-tools/svelte";
|
|
773
|
+
const client = createJazzClient(config);
|
|
774
|
+
</script>
|
|
775
|
+
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
## Live queries
|
|
779
|
+
|
|
780
|
+
React and Vue use composition hooks; Svelte uses a class with a reactive `.current` property.
|
|
781
|
+
|
|
782
|
+
```tsx
|
|
783
|
+
const tasks = useAll(app.tasks.where({ done: false }));
|
|
784
|
+
|
|
785
|
+
// undefined = not yet connected; [] = connected, no rows; [...] = rows present
|
|
786
|
+
if (tasks === undefined) return ;
|
|
787
|
+
|
|
788
|
+
````
|
|
789
|
+
|
|
790
|
+
```vue
|
|
791
|
+
<script setup lang="ts">
|
|
792
|
+
|
|
793
|
+
const tasks = useAll(app.tasks.where({ done: false }));
|
|
794
|
+
// undefined = not yet connected; [] = connected, no rows; [...] = rows present
|
|
795
|
+
</script>
|
|
796
|
+
|
|
797
|
+
<template>
|
|
798
|
+
|
|
799
|
+
</template>
|
|
800
|
+
````
|
|
801
|
+
|
|
802
|
+
```svelte
|
|
803
|
+
<script lang="ts">
|
|
804
|
+
import { QuerySubscription } from "jazz-tools/svelte";
|
|
805
|
+
const tasks = new QuerySubscription(
|
|
806
|
+
app.tasks.where({ done: false }),
|
|
807
|
+
);
|
|
808
|
+
</script>
|
|
809
|
+
|
|
810
|
+
{#each tasks.current ?? [] as task}
|
|
811
|
+
|
|
812
|
+
{/each}
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
The `?? []` guard handles the `undefined` (not yet connected) case. See
|
|
816
|
+
[Reading Data: First delivery and the undefined signal](/docs/reading-data#first-delivery-and-the-undefined-signal) for patterns that depend on this signal.
|
|
817
|
+
|
|
818
|
+
In Vue, `useAll` returns a `ShallowRef` — it unwraps automatically in templates, but in
|
|
819
|
+
`<script setup>` you would access it as `tasks.value`.
|
|
820
|
+
|
|
821
|
+
## Accessing the database for writes
|
|
822
|
+
|
|
823
|
+
```tsx
|
|
824
|
+
// Must be called at component top level (rules of hooks)
|
|
825
|
+
|
|
826
|
+
const db = useDb();
|
|
827
|
+
|
|
828
|
+
await db.insert(app.messages, { text, createdAt: Date.now() });
|
|
829
|
+
|
|
830
|
+
````
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
// Must be called inside setup() or <script setup>
|
|
834
|
+
|
|
835
|
+
const db = useDb();
|
|
836
|
+
|
|
837
|
+
await db.insert(app.messages, { text, createdAt: Date.now() });
|
|
838
|
+
````
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
// Callable anywhere — component, store, or utility module
|
|
842
|
+
|
|
843
|
+
const db = getDb();
|
|
844
|
+
|
|
845
|
+
await db.insert(app.messages, { text, createdAt: Date.now() });
|
|
846
|
+
|
|
847
|
+
````
|
|
848
|
+
|
|
849
|
+
`getDb()` in Svelte can be called outside of component lifecycle — useful in event handlers,
|
|
850
|
+
class methods, and utility modules. `useDb()` in React and Vue must follow the rules of hooks.
|
|
851
|
+
|
|
852
|
+
## Session / user identity
|
|
853
|
+
|
|
854
|
+
```tsx
|
|
855
|
+
|
|
856
|
+
const session = useSession(); // { user_id: string } | null
|
|
857
|
+
````
|
|
858
|
+
|
|
859
|
+
```ts
|
|
860
|
+
|
|
861
|
+
const session = useSession(); // { user_id: string } | null
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
|
|
866
|
+
const session = getSession(); // { user_id: string } | null
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
===PAGE:guides/where-operators===
|
|
870
|
+
TITLE:WHERE Operators
|
|
871
|
+
DESCRIPTION:Full reference for filter operators available in Jazz query builders, with examples for each column kind.
|
|
872
|
+
|
|
873
|
+
## Summary
|
|
874
|
+
|
|
875
|
+
## Operator reference by column type
|
|
876
|
+
|
|
877
|
+
## Examples
|
|
878
|
+
|
|
879
|
+
### Equality and inequality
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
// Exact match (shorthand — no operator object needed)
|
|
883
|
+
db.all(app.orders.where({ status: "pending" }));
|
|
884
|
+
|
|
885
|
+
// Not equal
|
|
886
|
+
db.all(app.users.where({ id: { ne: currentUserId } }));
|
|
887
|
+
|
|
888
|
+
// One of a set
|
|
889
|
+
db.all(app.orders.where({ status: { in: ["pending", "processing"] } }));
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
### Numeric comparisons
|
|
893
|
+
|
|
894
|
+
```ts
|
|
895
|
+
// Greater than / less than
|
|
896
|
+
db.all(app.sessions.where({ lastSeen: { gt: staleCutoff } }));
|
|
897
|
+
db.all(app.orders.where({ total: { gte: 1000 } }));
|
|
898
|
+
db.all(app.products.where({ stock: { lt: 10 } }));
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
### String contains
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
// Substring match (case-sensitive)
|
|
905
|
+
db.all(app.messages.where({ text: { contains: searchTerm } }));
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### Null checks on optional references
|
|
909
|
+
|
|
910
|
+
```ts
|
|
911
|
+
// Rows where the optional ref is not set
|
|
912
|
+
db.all(app.tasks.where({ assignedTo: { isNull: true } }));
|
|
913
|
+
|
|
914
|
+
// Rows where it is set
|
|
915
|
+
db.all(app.tasks.where({ assignedTo: { isNull: false } }));
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
### Multiple conditions (AND)
|
|
919
|
+
|
|
920
|
+
All predicates passed to `where(...)` are AND-combined:
|
|
921
|
+
|
|
922
|
+
```ts
|
|
923
|
+
// done AND assigned to this user
|
|
924
|
+
db.all(
|
|
925
|
+
app.tasks.where({
|
|
926
|
+
done: true,
|
|
927
|
+
assignedTo: userId,
|
|
928
|
+
}),
|
|
929
|
+
);
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
### Combining with ordering and limits
|
|
933
|
+
|
|
934
|
+
```ts
|
|
935
|
+
db.all(app.messages.where({ threadId }).orderBy("createdAt", "asc").limit(50));
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
## Live subscriptions with WHERE
|
|
939
|
+
|
|
940
|
+
`useAll` and `QuerySubscription` accept the same query builders as `db.all`. The subscription
|
|
941
|
+
stays active and re-delivers results whenever any row enters or exits the filter:
|
|
942
|
+
|
|
943
|
+
```ts
|
|
944
|
+
const pending = useAll(app.tasks.where({ done: false }));
|
|
945
|
+
```
|
|
946
|
+
|
|
453
947
|
===PAGE:index===
|
|
454
948
|
TITLE:Overview
|
|
455
949
|
DESCRIPTION:The database that syncs.
|
|
@@ -550,79 +1044,166 @@ Slugs match the URL path under `/docs/` — for example `schemas`, `reading-data
|
|
|
550
1044
|
|
|
551
1045
|
===PAGE:migrations===
|
|
552
1046
|
TITLE:Migrations
|
|
553
|
-
DESCRIPTION:
|
|
1047
|
+
DESCRIPTION:Review and publish typed compatibility edges between schema hashes.
|
|
554
1048
|
|
|
555
1049
|
If you are trying to get your first app running, you can skip this page and return later.
|
|
556
1050
|
|
|
557
|
-
##
|
|
1051
|
+
## Why Jazz Migrations Are Different
|
|
1052
|
+
|
|
1053
|
+
- Traditional migration systems run a linear sequence and require peers to converge before older versions stop writing.
|
|
1054
|
+
- Jazz migration lenses are applied at read/write time, so mixed-version clients can keep operating while data is translated between schema hashes.
|
|
1055
|
+
- Schemas still auto-publish on connect. Migrations do not: they are always reviewed and pushed explicitly.
|
|
558
1056
|
|
|
559
|
-
|
|
560
|
-
- Jazz migration lenses are evaluated at read/write time, so mixed-version clients can continue operating while data is translated across schema versions.
|
|
561
|
-
- This enables incremental rollouts where older and newer app versions overlap safely during migration windows.
|
|
1057
|
+
## Workflow
|
|
562
1058
|
|
|
563
|
-
|
|
1059
|
+
All Jazz apps use the same migration workflow, including Rust backends.
|
|
564
1060
|
|
|
565
1061
|
```bash
|
|
566
1062
|
#!/usr/bin/env bash
|
|
567
1063
|
|
|
568
|
-
# 1) Edit schema
|
|
569
|
-
|
|
570
|
-
|
|
1064
|
+
# 1) Edit schema.ts during development and watch for Jazz migration warnings.
|
|
1065
|
+
|
|
1066
|
+
# 2) Generate a typed migration stub after Jazz reports the old/new hashes.
|
|
1067
|
+
npx jazz-tools@alpha migrations create <fromHash> <toHash>
|
|
1068
|
+
|
|
1069
|
+
# 3) Fill in migrate, rename the file, then publish the reviewed edge.
|
|
1070
|
+
npx jazz-tools@alpha migrations push <fromHash> <toHash>
|
|
571
1071
|
|
|
572
1072
|
```
|
|
573
1073
|
|
|
574
1074
|
```bash
|
|
575
1075
|
#!/usr/bin/env bash
|
|
576
1076
|
|
|
577
|
-
# 1) Edit schema
|
|
578
|
-
|
|
579
|
-
|
|
1077
|
+
# 1) Edit schema.ts during development and watch for Jazz migration warnings.
|
|
1078
|
+
|
|
1079
|
+
# 2) Generate a typed migration stub after Jazz reports the old/new hashes.
|
|
1080
|
+
npx jazz-tools@alpha migrations create <fromHash> <toHash>
|
|
1081
|
+
|
|
1082
|
+
# 3) Fill in migrate, rename the file, then publish the reviewed edge.
|
|
1083
|
+
npx jazz-tools@alpha migrations push <fromHash> <toHash>
|
|
580
1084
|
|
|
581
1085
|
```
|
|
582
1086
|
|
|
583
|
-
|
|
584
|
-
For SQL migrations, keep explicit forward/backward files and edit each direction intentionally.
|
|
1087
|
+
When Jazz encounters rows whose schema hash is no longer reachable from the current app schema, it logs a warning with the exact short-hash `migrations create <fromHash> <toHash>` command to run next.
|
|
585
1088
|
|
|
586
|
-
## Generated
|
|
1089
|
+
## Generated Stub
|
|
587
1090
|
|
|
588
|
-
|
|
1091
|
+
`migrations create` writes a copy-pastable file with short `fromHash`, `toHash`, and minimal typed `from` / `to` witnesses. You only need to fill in `migrate`, then rename the file.
|
|
1092
|
+
|
|
1093
|
+
```ts
|
|
589
1094
|
|
|
590
|
-
migrate
|
|
591
|
-
|
|
1095
|
+
migrate: {
|
|
1096
|
+
todos: {
|
|
1097
|
+
description: s.add.string({ default: null }),
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
fromHash: "a01f5c72ec47",
|
|
1101
|
+
toHash: "311995e9a178",
|
|
1102
|
+
from: {
|
|
1103
|
+
todos: s.table({
|
|
1104
|
+
title: s.string(),
|
|
1105
|
+
done: s.boolean(),
|
|
1106
|
+
parentId: s.ref("todos").optional(),
|
|
1107
|
+
projectId: s.ref("projects").optional(),
|
|
1108
|
+
owner_id: s.string(),
|
|
1109
|
+
}),
|
|
1110
|
+
},
|
|
1111
|
+
to: {
|
|
1112
|
+
todos: s.table({
|
|
1113
|
+
title: s.string(),
|
|
1114
|
+
done: s.boolean(),
|
|
1115
|
+
description: s.string().optional(),
|
|
1116
|
+
parentId: s.ref("todos").optional(),
|
|
1117
|
+
projectId: s.ref("projects").optional(),
|
|
1118
|
+
owner_id: s.string(),
|
|
1119
|
+
}),
|
|
1120
|
+
},
|
|
592
1121
|
});
|
|
593
1122
|
|
|
594
1123
|
```
|
|
595
1124
|
|
|
596
|
-
|
|
597
|
-
```sql
|
|
598
|
-
ALTER TABLE todos ADD COLUMN description TEXT DEFAULT '';
|
|
599
|
-
```
|
|
600
|
-
Backward:
|
|
601
|
-
```sql
|
|
602
|
-
ALTER TABLE todos DROP COLUMN description;
|
|
603
|
-
```
|
|
1125
|
+
## Edited Stub
|
|
604
1126
|
|
|
605
|
-
|
|
1127
|
+
After review, keep the same hashes and adjust the `migrate` object as needed:
|
|
606
1128
|
|
|
607
|
-
|
|
1129
|
+
```ts
|
|
608
1130
|
|
|
609
1131
|
// Example of editing a generated migration stub.
|
|
610
|
-
|
|
611
|
-
|
|
1132
|
+
|
|
1133
|
+
migrate: {
|
|
1134
|
+
todos: {
|
|
1135
|
+
description: s.add.string({ default: "No description" }),
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
fromHash: "a01f5c72ec47",
|
|
1139
|
+
toHash: "311995e9a178",
|
|
1140
|
+
from: {
|
|
1141
|
+
todos: s.table({
|
|
1142
|
+
title: s.string(),
|
|
1143
|
+
done: s.boolean(),
|
|
1144
|
+
parentId: s.ref("todos").optional(),
|
|
1145
|
+
projectId: s.ref("projects").optional(),
|
|
1146
|
+
owner_id: s.string(),
|
|
1147
|
+
}),
|
|
1148
|
+
},
|
|
1149
|
+
to: {
|
|
1150
|
+
todos: s.table({
|
|
1151
|
+
title: s.string(),
|
|
1152
|
+
done: s.boolean(),
|
|
1153
|
+
description: s.string().optional(),
|
|
1154
|
+
parentId: s.ref("todos").optional(),
|
|
1155
|
+
projectId: s.ref("projects").optional(),
|
|
1156
|
+
owner_id: s.string(),
|
|
1157
|
+
}),
|
|
1158
|
+
},
|
|
612
1159
|
});
|
|
613
1160
|
|
|
614
1161
|
```
|
|
615
1162
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1163
|
+
## Backwards Defaults
|
|
1164
|
+
|
|
1165
|
+
When a newer schema drops a column that older clients still expect, define a backwards default in the migration edge:
|
|
1166
|
+
|
|
1167
|
+
```ts
|
|
1168
|
+
|
|
1169
|
+
// Example: dropping a column with a backwards default.
|
|
1170
|
+
// Clients still on the older schema continue seeing legacy_priority.
|
|
1171
|
+
|
|
1172
|
+
migrate: {
|
|
1173
|
+
todos: {
|
|
1174
|
+
legacy_priority: s.drop.int({ backwardsDefault: 0 }),
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
fromHash: "311995e9a178",
|
|
1178
|
+
toHash: "73b65d082ab8",
|
|
1179
|
+
from: {
|
|
1180
|
+
todos: s.table({
|
|
1181
|
+
title: s.string(),
|
|
1182
|
+
done: s.boolean(),
|
|
1183
|
+
description: s.string().optional(),
|
|
1184
|
+
parentId: s.ref("todos").optional(),
|
|
1185
|
+
projectId: s.ref("projects").optional(),
|
|
1186
|
+
owner_id: s.string(),
|
|
1187
|
+
legacy_priority: s.int(),
|
|
1188
|
+
}),
|
|
1189
|
+
},
|
|
1190
|
+
to: {
|
|
1191
|
+
todos: s.table({
|
|
1192
|
+
title: s.string(),
|
|
1193
|
+
done: s.boolean(),
|
|
1194
|
+
description: s.string().optional(),
|
|
1195
|
+
parentId: s.ref("todos").optional(),
|
|
1196
|
+
projectId: s.ref("projects").optional(),
|
|
1197
|
+
owner_id: s.string(),
|
|
1198
|
+
}),
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
620
1201
|
|
|
621
1202
|
```
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1203
|
+
|
|
1204
|
+
## Rust Apps Use the Same Files
|
|
1205
|
+
|
|
1206
|
+
Rust services still author and ship TypeScript migrations. The Rust runtime consumes the compiled internal schema representation through the CLI and low-level APIs; it does not maintain a separate SQL migration format.
|
|
626
1207
|
|
|
627
1208
|
===PAGE:permissions===
|
|
628
1209
|
TITLE:Permissions
|
|
@@ -630,33 +1211,33 @@ DESCRIPTION:Fast RLS (ReBAC), simple policies, combinators, and inherited access
|
|
|
630
1211
|
|
|
631
1212
|
## Schema Policy Syntax
|
|
632
1213
|
|
|
633
|
-
Define policies in `
|
|
634
|
-
|
|
635
|
-
Assuming the following schema:
|
|
636
|
-
|
|
637
|
-
```ts
|
|
638
|
-
table("projects", {
|
|
639
|
-
name: col.string(),
|
|
640
|
-
ownerId: col.string(),
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
table("todos", {
|
|
644
|
-
title: col.string(),
|
|
645
|
-
done: col.boolean(),
|
|
646
|
-
project: col.ref("projects").optional(),
|
|
647
|
-
ownerId: col.string(),
|
|
648
|
-
});
|
|
1214
|
+
Define policies in a root `permissions.ts` next to `schema.ts`. This applies to every Jazz app, including Rust backends that consume the compiled schema via the CLI.
|
|
649
1215
|
|
|
650
|
-
|
|
651
|
-
todo: col.ref("todos"),
|
|
652
|
-
user_id: col.string(),
|
|
653
|
-
can_read: col.boolean(),
|
|
654
|
-
});
|
|
655
|
-
```
|
|
1216
|
+
Assuming the following schema:
|
|
656
1217
|
|
|
657
|
-
|
|
1218
|
+
```ts
|
|
1219
|
+
const schema = {
|
|
1220
|
+
projects: s.table({
|
|
1221
|
+
name: s.string(),
|
|
1222
|
+
ownerId: s.string(),
|
|
1223
|
+
}),
|
|
1224
|
+
todos: s.table({
|
|
1225
|
+
title: s.string(),
|
|
1226
|
+
done: s.boolean(),
|
|
1227
|
+
projectId: s.ref("projects").optional(),
|
|
1228
|
+
owner_id: s.string(),
|
|
1229
|
+
}),
|
|
1230
|
+
todoShares: s.table({
|
|
1231
|
+
todoId: s.ref("todos"),
|
|
1232
|
+
userId: s.string(),
|
|
1233
|
+
can_read: s.boolean(),
|
|
1234
|
+
}),
|
|
1235
|
+
};
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
Define permissions in `permissions.ts`:
|
|
658
1239
|
|
|
659
|
-
|
|
1240
|
+
```ts
|
|
660
1241
|
|
|
661
1242
|
policy.todos.allowRead.where({ owner_id: session.user_id });
|
|
662
1243
|
policy.todos.allowInsert.where({ owner_id: session.user_id });
|
|
@@ -666,32 +1247,12 @@ table("todoShares", {
|
|
|
666
1247
|
|
|
667
1248
|
```
|
|
668
1249
|
|
|
669
|
-
|
|
670
|
-
CREATE TABLE projects (
|
|
671
|
-
name TEXT NOT NULL
|
|
672
|
-
);
|
|
673
|
-
|
|
674
|
-
CREATE TABLE todos (
|
|
675
|
-
title TEXT NOT NULL,
|
|
676
|
-
done BOOLEAN NOT NULL,
|
|
677
|
-
description TEXT,
|
|
678
|
-
parent UUID REFERENCES todos,
|
|
679
|
-
project UUID REFERENCES projects,
|
|
680
|
-
owner_id TEXT NOT NULL
|
|
681
|
-
);
|
|
682
|
-
CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);
|
|
683
|
-
CREATE POLICY todos_insert_policy ON todos FOR INSERT WITH CHECK (owner_id = @session.user_id);
|
|
684
|
-
CREATE POLICY todos_update_policy ON todos FOR UPDATE USING (owner_id = @session.user_id) WITH CHECK (owner_id = @session.user_id);
|
|
685
|
-
CREATE POLICY todos_delete_policy ON todos FOR DELETE USING (owner_id = @session.user_id);
|
|
686
|
-
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
Apps that do not need user-scoped filtering can still define explicit allow-all policies (`policy.todos.allowRead.where({})` / `USING (TRUE)`).
|
|
1250
|
+
Apps that do not need user-scoped filtering can still define explicit allow-all policies (`policy.todos.allowRead.always()` or `policy.todos.allowRead.where({})`).
|
|
690
1251
|
|
|
691
1252
|
## Simple Policies
|
|
692
1253
|
|
|
693
|
-
|
|
694
|
-
definePermissions(app, ({ policy, allOf, session }) => {
|
|
1254
|
+
```ts
|
|
1255
|
+
s.definePermissions(app, ({ policy, allOf, session }) => {
|
|
695
1256
|
policy.todos.allowRead.where({ owner_id: session.user_id });
|
|
696
1257
|
policy.todos.allowInsert.where({ owner_id: session.user_id });
|
|
697
1258
|
policy.todos.allowUpdate
|
|
@@ -701,25 +1262,36 @@ definePermissions(app, ({ policy, allOf, session }) => {
|
|
|
701
1262
|
});
|
|
702
1263
|
```
|
|
703
1264
|
|
|
704
|
-
|
|
705
|
-
-- #region permissions-simple-sql
|
|
706
|
-
CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);
|
|
707
|
-
CREATE POLICY todos_insert_policy ON todos FOR INSERT WITH CHECK (owner_id = @session.user_id);
|
|
708
|
-
CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((owner_id = @session.user_id) AND (done = FALSE)) WITH CHECK (owner_id = @session.user_id);
|
|
709
|
-
CREATE POLICY todos_delete_policy ON todos FOR DELETE USING (owner_id = @session.user_id);
|
|
710
|
-
-- #endregion permissions-simple-sql
|
|
1265
|
+
## Explicitly Allow an Operation (`.always()`)
|
|
711
1266
|
|
|
712
|
-
|
|
713
|
-
CREATE POLICY todos_select_policy ON todos FOR SELECT USING ((done = FALSE) OR (INHERITS SELECT VIA project));
|
|
714
|
-
CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((INHERITS UPDATE VIA project) AND (done = FALSE)) WITH CHECK (INHERITS UPDATE VIA project);
|
|
715
|
-
-- #endregion permissions-inherits-sql
|
|
1267
|
+
Use `.always()` when an operation should always be permitted. It is equivalent to `.where({})`.
|
|
716
1268
|
|
|
1269
|
+
```ts
|
|
1270
|
+
s.definePermissions(app, ({ policy }) => {
|
|
1271
|
+
policy.todos.allowRead.always();
|
|
1272
|
+
policy.todos.allowInsert.always();
|
|
1273
|
+
policy.todos.allowUpdate.always();
|
|
1274
|
+
policy.todos.allowDelete.always();
|
|
1275
|
+
});
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
## Explicitly Deny an Operation (`.never()`)
|
|
1279
|
+
|
|
1280
|
+
Use `.never()` when an operation should be impossible. It is equivalent to `.where(anyOf([]))`.
|
|
1281
|
+
|
|
1282
|
+
```ts
|
|
1283
|
+
s.definePermissions(app, ({ policy }) => {
|
|
1284
|
+
policy.todos.allowRead.never();
|
|
1285
|
+
policy.todos.allowInsert.never();
|
|
1286
|
+
policy.todos.allowUpdate.never();
|
|
1287
|
+
policy.todos.allowDelete.never();
|
|
1288
|
+
});
|
|
717
1289
|
```
|
|
718
1290
|
|
|
719
1291
|
## Combining Conditions (`allOf` / `anyOf`)
|
|
720
1292
|
|
|
721
1293
|
```ts
|
|
722
|
-
definePermissions(app, ({ policy, allOf, anyOf, allowedTo, session }) => {
|
|
1294
|
+
s.definePermissions(app, ({ policy, allOf, anyOf, allowedTo, session }) => {
|
|
723
1295
|
policy.todos.allowRead.where(
|
|
724
1296
|
anyOf([{ owner_id: session.user_id }, allOf([{ done: false }, allowedTo.read("project")])]),
|
|
725
1297
|
);
|
|
@@ -740,32 +1312,29 @@ pub fn combinator_policy() -> TablePolicies {
|
|
|
740
1312
|
|
|
741
1313
|
`allOf([...])` compiles to logical `AND`, and `anyOf([...])` compiles to logical `OR`.
|
|
742
1314
|
|
|
1315
|
+
## JWT Session Claims
|
|
1316
|
+
|
|
1317
|
+
When external auth JWTs carry claims, `session.where(...)` lets you check them directly in permissions without mapping them onto row columns first.
|
|
1318
|
+
|
|
1319
|
+
```ts
|
|
1320
|
+
s.definePermissions(app, ({ policy, anyOf, session }) => {
|
|
1321
|
+
policy.todos.allowRead.where(
|
|
1322
|
+
anyOf([{ owner_id: session.user_id }, session.where({ "claims.role": "manager" })]),
|
|
1323
|
+
);
|
|
1324
|
+
});
|
|
1325
|
+
```
|
|
1326
|
+
|
|
743
1327
|
## Inherited Access Policies (`allowedTo.*`)
|
|
744
1328
|
|
|
745
|
-
|
|
1329
|
+
`allowedTo.read/insert/update/delete(...)` lets one table inherit access from another through relationships.
|
|
746
1330
|
|
|
747
|
-
|
|
748
|
-
definePermissions(app, ({ policy, anyOf, allOf, allowedTo }) => {
|
|
1331
|
+
```ts
|
|
1332
|
+
s.definePermissions(app, ({ policy, anyOf, allOf, allowedTo }) => {
|
|
749
1333
|
policy.todos.allowRead.where(anyOf([{ done: false }, allowedTo.read("project")]));
|
|
750
1334
|
policy.todos.allowUpdate
|
|
751
1335
|
.whereOld(allOf([allowedTo.update("project"), { done: false }]))
|
|
752
1336
|
.whereNew(allowedTo.update("project"));
|
|
753
1337
|
});
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
```sql
|
|
757
|
-
-- #region permissions-simple-sql
|
|
758
|
-
CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);
|
|
759
|
-
CREATE POLICY todos_insert_policy ON todos FOR INSERT WITH CHECK (owner_id = @session.user_id);
|
|
760
|
-
CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((owner_id = @session.user_id) AND (done = FALSE)) WITH CHECK (owner_id = @session.user_id);
|
|
761
|
-
CREATE POLICY todos_delete_policy ON todos FOR DELETE USING (owner_id = @session.user_id);
|
|
762
|
-
-- #endregion permissions-simple-sql
|
|
763
|
-
|
|
764
|
-
-- #region permissions-inherits-sql
|
|
765
|
-
CREATE POLICY todos_select_policy ON todos FOR SELECT USING ((done = FALSE) OR (INHERITS SELECT VIA project));
|
|
766
|
-
CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((INHERITS UPDATE VIA project) AND (done = FALSE)) WITH CHECK (INHERITS UPDATE VIA project);
|
|
767
|
-
-- #endregion permissions-inherits-sql
|
|
768
|
-
|
|
769
1338
|
```
|
|
770
1339
|
|
|
771
1340
|
## Advanced: Recursive INHERITS
|
|
@@ -773,7 +1342,7 @@ CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((INHERITS UPDATE VI
|
|
|
773
1342
|
Use depth-bounded inherited access when foreign keys can form recursive chains.
|
|
774
1343
|
|
|
775
1344
|
```ts
|
|
776
|
-
definePermissions(app, ({ policy, allowedTo }) => {
|
|
1345
|
+
s.definePermissions(app, ({ policy, allowedTo }) => {
|
|
777
1346
|
policy.todos.allowRead.where(allowedTo.read("parent"));
|
|
778
1347
|
policy.todos.allowUpdate
|
|
779
1348
|
.whereOld(allowedTo.update("parent", { maxDepth: 5 }))
|
|
@@ -794,16 +1363,14 @@ pub fn recursive_inherits_policy() -> TablePolicies {
|
|
|
794
1363
|
|
|
795
1364
|
## Policy Enforcement and Request Context
|
|
796
1365
|
|
|
797
|
-
Jazz permissions use relationship-based policy evaluation with requester session context.
|
|
798
|
-
Backend endpoints should scope a client/session per request before reads and writes.
|
|
799
|
-
Frontend clients are evaluated with the auth/session identity used to initialize context.
|
|
1366
|
+
Jazz permissions use relationship-based policy evaluation with requester session context. Backend endpoints should scope a client/session per request before reads and writes.
|
|
800
1367
|
|
|
801
1368
|
```ts
|
|
802
1369
|
|
|
803
1370
|
try {
|
|
804
1371
|
const rows = await context
|
|
805
1372
|
.forRequest(req, schemaApp)
|
|
806
|
-
.
|
|
1373
|
+
.all(schemaApp.todos.where({ done: true }));
|
|
807
1374
|
res.json(rows);
|
|
808
1375
|
} catch {
|
|
809
1376
|
sendQueryError(res);
|
|
@@ -840,27 +1407,80 @@ Install dependencies in your Expo app:
|
|
|
840
1407
|
|
|
841
1408
|
For full environment and server setup options, see [Setup](/docs/setup).
|
|
842
1409
|
|
|
1410
|
+
## Project configuration
|
|
1411
|
+
|
|
1412
|
+
### Polyfills
|
|
1413
|
+
|
|
1414
|
+
Jazz requires a few global polyfills for networking and streams on React Native. Import them as the very first line in your entry point (`index.js`), before any other code:
|
|
1415
|
+
|
|
1416
|
+
```js
|
|
1417
|
+
|
|
1418
|
+
registerRootComponent(App);
|
|
1419
|
+
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
### Babel
|
|
1423
|
+
|
|
1424
|
+
Create a `babel.config.cjs` with the Expo preset and `import.meta` transform enabled:
|
|
1425
|
+
|
|
1426
|
+
```js
|
|
1427
|
+
module.exports = function (api) {
|
|
1428
|
+
api.cache(true);
|
|
1429
|
+
return {
|
|
1430
|
+
presets: [["babel-preset-expo", { unstable_transformImportMeta: true }]],
|
|
1431
|
+
};
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
### Metro
|
|
1437
|
+
|
|
1438
|
+
Create a `metro.config.js` that extends Expo's defaults. If you use **pnpm**, enable symlink support:
|
|
1439
|
+
|
|
1440
|
+
```js
|
|
1441
|
+
|
|
1442
|
+
const require = createRequire(import.meta.url);
|
|
1443
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
1444
|
+
|
|
1445
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1446
|
+
const projectRoot = path.dirname(__filename);
|
|
1447
|
+
|
|
1448
|
+
const config = getDefaultConfig(projectRoot);
|
|
1449
|
+
|
|
1450
|
+
// So Metro uses our Babel config (babel.config.cjs); it doesn't auto-detect .cjs
|
|
1451
|
+
config.transformer = config.transformer || {};
|
|
1452
|
+
config.transformer.extendsBabelConfigPath = path.resolve(projectRoot, "babel.config.cjs");
|
|
1453
|
+
|
|
1454
|
+
// pnpm uses symlinks for hoisted packages
|
|
1455
|
+
config.resolver.unstable_enableSymlinks = true;
|
|
1456
|
+
|
|
1457
|
+
```
|
|
1458
|
+
|
|
843
1459
|
## Define your schema
|
|
844
1460
|
|
|
845
|
-
Create `schema
|
|
1461
|
+
Create `schema.ts` at the root of your project. This is the source of truth for your data model.
|
|
846
1462
|
|
|
847
1463
|
```ts
|
|
848
1464
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1465
|
+
const schema = {
|
|
1466
|
+
projects: s.table({
|
|
1467
|
+
name: s.string(),
|
|
1468
|
+
}),
|
|
1469
|
+
todos: s.table({
|
|
1470
|
+
title: s.string(),
|
|
1471
|
+
done: s.boolean(),
|
|
1472
|
+
description: s.string().optional(),
|
|
1473
|
+
ownerId: s.string(),
|
|
1474
|
+
parentId: s.ref("todos").optional(),
|
|
1475
|
+
projectId: s.ref("projects").optional(),
|
|
1476
|
+
}),
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
type AppSchema = s.Schema<typeof schema>;
|
|
852
1480
|
|
|
853
|
-
table("todos", {
|
|
854
|
-
title: col.string(),
|
|
855
|
-
done: col.boolean(),
|
|
856
|
-
description: col.string().optional(),
|
|
857
|
-
ownerId: col.string(),
|
|
858
|
-
parent: col.ref("todos").optional(),
|
|
859
|
-
project: col.ref("projects").optional(),
|
|
860
|
-
});
|
|
861
1481
|
```
|
|
862
1482
|
|
|
863
|
-
###
|
|
1483
|
+
### Validate schema
|
|
864
1484
|
|
|
865
1485
|
Deep dive: [Schemas](/docs/schemas).
|
|
866
1486
|
|
|
@@ -869,12 +1489,6 @@ Deep dive: [Schemas](/docs/schemas).
|
|
|
869
1489
|
`JazzProvider` initializes the Jazz runtime and makes the database available to descendant components via React context.
|
|
870
1490
|
|
|
871
1491
|
```tsx
|
|
872
|
-
const client = createJazzClient({
|
|
873
|
-
appId: "00000000-0000-0000-0000-000000000002",
|
|
874
|
-
serverUrl: "http://10.0.2.2:1625",
|
|
875
|
-
localAuthMode: "demo",
|
|
876
|
-
// jwtToken: authToken, // Use this (instead of localAuthMode) for external auth.
|
|
877
|
-
});
|
|
878
1492
|
|
|
879
1493
|
return (
|
|
880
1494
|
|
|
@@ -917,7 +1531,7 @@ Query builders are fluent and immutable. Chain `.where()`, `.orderBy()`, `.limit
|
|
|
917
1531
|
}
|
|
918
1532
|
```
|
|
919
1533
|
|
|
920
|
-
By default `useAll` returns results immediately from the local replica. Use
|
|
1534
|
+
By default `useAll` returns results immediately from the local replica. Use query options when you need explicit confirmation boundaries.
|
|
921
1535
|
|
|
922
1536
|
Deep dive: [Reading Data](/docs/reading-data).
|
|
923
1537
|
|
|
@@ -926,19 +1540,19 @@ Deep dive: [Reading Data](/docs/reading-data).
|
|
|
926
1540
|
`useDb` returns the `Db` handle for mutations. Mutations apply locally first and return promises that resolve at the selected durability tier.
|
|
927
1541
|
|
|
928
1542
|
```tsx
|
|
929
|
-
const addTodo =
|
|
1543
|
+
const addTodo = () => {
|
|
930
1544
|
const trimmed = title.trim();
|
|
931
1545
|
if (!trimmed || !sessionUserId) return;
|
|
932
|
-
|
|
1546
|
+
db.insert(app.todos, { title: trimmed, done: false, ownerId: sessionUserId });
|
|
933
1547
|
setTitle("");
|
|
934
1548
|
};
|
|
935
1549
|
|
|
936
|
-
const toggleTodo =
|
|
937
|
-
|
|
1550
|
+
const toggleTodo = (todo: Todo) => {
|
|
1551
|
+
db.update(app.todos, todo.id, { done: !todo.done });
|
|
938
1552
|
};
|
|
939
1553
|
|
|
940
|
-
const removeTodo =
|
|
941
|
-
|
|
1554
|
+
const removeTodo = (id: string) => {
|
|
1555
|
+
db.delete(app.todos, id);
|
|
942
1556
|
};
|
|
943
1557
|
```
|
|
944
1558
|
|
|
@@ -953,7 +1567,7 @@ Mutation API details: [Writing Data](/docs/writing-data).
|
|
|
953
1567
|
|
|
954
1568
|
### Reading with a durability tier
|
|
955
1569
|
|
|
956
|
-
|
|
1570
|
+
Inside components, use `useAll(query, options)` with the same `QueryOptions` as `db.subscribeAll`. Outside components, use `db.subscribeAll` (or `db.all`) directly when you need tier-gated delivery or deltas.
|
|
957
1571
|
|
|
958
1572
|
```ts
|
|
959
1573
|
|
|
@@ -975,8 +1589,8 @@ Use durable mutation APIs with `{ tier }` when you need explicit write durabilit
|
|
|
975
1589
|
{
|
|
976
1590
|
title: todoTitle,
|
|
977
1591
|
done: false,
|
|
978
|
-
|
|
979
|
-
|
|
1592
|
+
ownerId: EXAMPLE_OWNER_ID,
|
|
1593
|
+
projectId: EXAMPLE_PROJECT_ID,
|
|
980
1594
|
},
|
|
981
1595
|
{ tier: "worker" },
|
|
982
1596
|
);
|
|
@@ -1016,7 +1630,7 @@ Outside components (or when you need deltas), use `db.subscribeAll` directly:
|
|
|
1016
1630
|
|
|
1017
1631
|
### Relations with include
|
|
1018
1632
|
|
|
1019
|
-
If your schema has `
|
|
1633
|
+
If your schema has `s.ref()` columns, Jazz infers typed include interfaces directly from `schema.ts`. Use `.include()` to load related rows in one query.
|
|
1020
1634
|
|
|
1021
1635
|
```ts
|
|
1022
1636
|
|
|
@@ -1087,10 +1701,6 @@ More query patterns: [Reading Data](/docs/reading-data).
|
|
|
1087
1701
|
If you provide no `jwtToken` or explicit local auth fields, Jazz uses local synthetic auth:
|
|
1088
1702
|
|
|
1089
1703
|
```tsx
|
|
1090
|
-
const anonymousAuthExpoClient = createJazzClient({
|
|
1091
|
-
appId: "my-app",
|
|
1092
|
-
serverUrl: "http://127.0.0.1:4200",
|
|
1093
|
-
});
|
|
1094
1704
|
|
|
1095
1705
|
return (
|
|
1096
1706
|
|
|
@@ -1101,11 +1711,6 @@ const anonymousAuthExpoClient = createJazzClient({
|
|
|
1101
1711
|
If needed, you can explicitly override the local token:
|
|
1102
1712
|
|
|
1103
1713
|
```tsx
|
|
1104
|
-
const anonymousAuthWithTokenExpoClient = createJazzClient({
|
|
1105
|
-
appId: "my-app",
|
|
1106
|
-
localAuthMode: "anonymous",
|
|
1107
|
-
localAuthToken: "device-token-123",
|
|
1108
|
-
});
|
|
1109
1714
|
|
|
1110
1715
|
return (
|
|
1111
1716
|
|
|
@@ -1120,12 +1725,6 @@ Use demo-mode synthetic profiles for local multi-user testing:
|
|
|
1120
1725
|
```tsx
|
|
1121
1726
|
const demoAuthExpoAppId = "my-app";
|
|
1122
1727
|
const demoAuthExpoActive = getActiveSyntheticAuth(demoAuthExpoAppId, { defaultMode: "demo" });
|
|
1123
|
-
const demoAuthExpoClient = createJazzClient({
|
|
1124
|
-
appId: demoAuthExpoAppId,
|
|
1125
|
-
serverUrl: "http://127.0.0.1:4200",
|
|
1126
|
-
localAuthMode: demoAuthExpoActive.localAuthMode,
|
|
1127
|
-
localAuthToken: demoAuthExpoActive.localAuthToken,
|
|
1128
|
-
});
|
|
1129
1728
|
|
|
1130
1729
|
return (
|
|
1131
1730
|
|
|
@@ -1141,15 +1740,6 @@ After provider sign-in, link the current local identity so existing local data s
|
|
|
1141
1740
|
const externalAuthExpoAppId = "my-app";
|
|
1142
1741
|
const externalAuthExpoServerUrl = "http://127.0.0.1:4200";
|
|
1143
1742
|
const externalAuthExpoProviderJwt = "<provider-jwt>";
|
|
1144
|
-
const externalAuthExpoLocalClient = createJazzClient({
|
|
1145
|
-
appId: externalAuthExpoAppId,
|
|
1146
|
-
serverUrl: externalAuthExpoServerUrl,
|
|
1147
|
-
});
|
|
1148
|
-
const externalAuthExpoJwtClient = createJazzClient({
|
|
1149
|
-
appId: externalAuthExpoAppId,
|
|
1150
|
-
serverUrl: externalAuthExpoServerUrl,
|
|
1151
|
-
jwtToken: externalAuthExpoProviderJwt,
|
|
1152
|
-
});
|
|
1153
1743
|
|
|
1154
1744
|
const [hasJwt, setHasJwt] = useState(false);
|
|
1155
1745
|
const linkExternalIdentity = useLinkExternalIdentity({
|
|
@@ -1193,7 +1783,6 @@ jazz-tools server "$JAZZ_APP_ID" \
|
|
|
1193
1783
|
Omit `serverUrl` to keep all data local:
|
|
1194
1784
|
|
|
1195
1785
|
```tsx
|
|
1196
|
-
const offlineOnlyAuthExpoClient = createJazzClient({ appId: "my-app" });
|
|
1197
1786
|
|
|
1198
1787
|
return (
|
|
1199
1788
|
|
|
@@ -1205,19 +1794,19 @@ Full auth flows and server behavior: [Authentication](/docs/authentication).
|
|
|
1205
1794
|
|
|
1206
1795
|
## Permissions
|
|
1207
1796
|
|
|
1208
|
-
Define row-level policies in `
|
|
1797
|
+
Define row-level policies in `permissions.ts` with `s.definePermissions(...)`:
|
|
1209
1798
|
|
|
1210
1799
|
```ts
|
|
1211
1800
|
|
|
1212
1801
|
// Each user only sees their own rows.
|
|
1213
|
-
policy.todos.allowRead.where({
|
|
1802
|
+
policy.todos.allowRead.where({ ownerId: session.user_id }),
|
|
1214
1803
|
// New rows must belong to the current user.
|
|
1215
|
-
policy.todos.allowInsert.where({
|
|
1804
|
+
policy.todos.allowInsert.where({ ownerId: session.user_id }),
|
|
1216
1805
|
// Users can only mutate their own incomplete todos.
|
|
1217
1806
|
policy.todos.allowUpdate
|
|
1218
|
-
.whereOld(allOf([{
|
|
1219
|
-
.whereNew({
|
|
1220
|
-
policy.todos.allowDelete.where(allOf([{
|
|
1807
|
+
.whereOld(allOf([{ ownerId: session.user_id }, { done: false }]))
|
|
1808
|
+
.whereNew({ ownerId: session.user_id }),
|
|
1809
|
+
policy.todos.allowDelete.where(allOf([{ ownerId: session.user_id }, { done: false }])),
|
|
1221
1810
|
]);
|
|
1222
1811
|
```
|
|
1223
1812
|
|
|
@@ -1244,24 +1833,28 @@ For full environment and server setup options, see [Setup](/docs/setup).
|
|
|
1244
1833
|
|
|
1245
1834
|
## Define your schema
|
|
1246
1835
|
|
|
1247
|
-
Create `schema
|
|
1836
|
+
Create `schema.ts` at the root of your project. This is the source of truth for your data model.
|
|
1248
1837
|
|
|
1249
1838
|
```ts
|
|
1250
1839
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1840
|
+
const schema = {
|
|
1841
|
+
projects: s.table({
|
|
1842
|
+
name: s.string(),
|
|
1843
|
+
}),
|
|
1844
|
+
todos: s.table({
|
|
1845
|
+
title: s.string(),
|
|
1846
|
+
done: s.boolean(),
|
|
1847
|
+
description: s.string().optional(),
|
|
1848
|
+
parentId: s.ref("todos").optional(),
|
|
1849
|
+
projectId: s.ref("projects").optional(),
|
|
1850
|
+
}),
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
type AppSchema = s.Schema<typeof schema>;
|
|
1254
1854
|
|
|
1255
|
-
table("todos", {
|
|
1256
|
-
title: col.string(),
|
|
1257
|
-
done: col.boolean(),
|
|
1258
|
-
description: col.string().optional(),
|
|
1259
|
-
parent: col.ref("todos").optional(),
|
|
1260
|
-
project: col.ref("projects").optional(),
|
|
1261
|
-
});
|
|
1262
1855
|
```
|
|
1263
1856
|
|
|
1264
|
-
###
|
|
1857
|
+
### Validate schema
|
|
1265
1858
|
|
|
1266
1859
|
Deep dive: [Schemas](/docs/schemas).
|
|
1267
1860
|
|
|
@@ -1270,12 +1863,6 @@ Deep dive: [Schemas](/docs/schemas).
|
|
|
1270
1863
|
`JazzProvider` initializes the Jazz runtime and makes the database available to descendant components via React context.
|
|
1271
1864
|
|
|
1272
1865
|
```tsx
|
|
1273
|
-
const client = createJazzClient({
|
|
1274
|
-
appId: "todo-react-example",
|
|
1275
|
-
serverUrl: "http://127.0.0.1:1625",
|
|
1276
|
-
localAuthMode: "anonymous",
|
|
1277
|
-
// jwtToken: authToken, // Use this (instead of localAuthMode) for external auth.
|
|
1278
|
-
});
|
|
1279
1866
|
|
|
1280
1867
|
return (
|
|
1281
1868
|
|
|
@@ -1313,7 +1900,7 @@ Query builders are fluent and immutable. Chain `.where()`, `.orderBy()`, `.limit
|
|
|
1313
1900
|
);
|
|
1314
1901
|
```
|
|
1315
1902
|
|
|
1316
|
-
By default `useAll` returns results immediately from the local replica. Use
|
|
1903
|
+
By default `useAll` returns results immediately from the local replica. Use query options when you need explicit confirmation boundaries.
|
|
1317
1904
|
|
|
1318
1905
|
Deep dive: [Reading Data](/docs/reading-data).
|
|
1319
1906
|
|
|
@@ -1322,16 +1909,16 @@ Deep dive: [Reading Data](/docs/reading-data).
|
|
|
1322
1909
|
`useDb` returns the `Db` handle for mutations. Mutations apply locally first and return promises that resolve at the selected durability tier.
|
|
1323
1910
|
|
|
1324
1911
|
```tsx
|
|
1325
|
-
|
|
1326
|
-
|
|
1912
|
+
function addTodo(todoTitle: string) {
|
|
1913
|
+
db.insert(app.todos, { title: todoTitle, done: false });
|
|
1327
1914
|
}
|
|
1328
1915
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1916
|
+
function toggleTodo(todo: { id: string; done: boolean }) {
|
|
1917
|
+
db.update(app.todos, todo.id, { done: !todo.done });
|
|
1331
1918
|
}
|
|
1332
1919
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1920
|
+
function removeTodo(id: string) {
|
|
1921
|
+
db.delete(app.todos, id);
|
|
1335
1922
|
}
|
|
1336
1923
|
```
|
|
1337
1924
|
|
|
@@ -1347,7 +1934,7 @@ Mutation API details: [Writing Data](/docs/writing-data).
|
|
|
1347
1934
|
|
|
1348
1935
|
### Reading with a durability tier
|
|
1349
1936
|
|
|
1350
|
-
|
|
1937
|
+
Inside components, use `useAll(query, options)` with the same `QueryOptions` as `db.subscribeAll`. Outside components, use `db.subscribeAll` (or `db.all`) directly when you need tier-gated delivery or deltas.
|
|
1351
1938
|
|
|
1352
1939
|
```ts
|
|
1353
1940
|
|
|
@@ -1364,7 +1951,11 @@ Use durable mutation APIs with `{ tier }` to wait for specific durability tiers:
|
|
|
1364
1951
|
|
|
1365
1952
|
```ts
|
|
1366
1953
|
|
|
1367
|
-
const { id } = await db.insertDurable(
|
|
1954
|
+
const { id } = await db.insertDurable(
|
|
1955
|
+
app.todos,
|
|
1956
|
+
{ title: todoTitle, done: false },
|
|
1957
|
+
{ tier: "edge" },
|
|
1958
|
+
);
|
|
1368
1959
|
await db.updateDurable(app.todos, id, { done: true }, { tier: "edge" });
|
|
1369
1960
|
await db.deleteDurable(app.todos, id, { tier: "global" });
|
|
1370
1961
|
}
|
|
@@ -1400,7 +1991,7 @@ Outside components (or when you need deltas), use `db.subscribeAll` directly:
|
|
|
1400
1991
|
|
|
1401
1992
|
### Relations with include
|
|
1402
1993
|
|
|
1403
|
-
If your schema has `
|
|
1994
|
+
If your schema has `s.ref()` columns, Jazz infers typed include interfaces directly from `schema.ts`. Use `.include()` to load related rows in one query.
|
|
1404
1995
|
|
|
1405
1996
|
```ts
|
|
1406
1997
|
|
|
@@ -1473,11 +2064,6 @@ For browser clients, `anonymous` is the default when no external JWT/local auth
|
|
|
1473
2064
|
If you provide no `jwtToken` or explicit local auth fields, Jazz auto-generates an anonymous token on first load and persists it in local storage.
|
|
1474
2065
|
|
|
1475
2066
|
```tsx
|
|
1476
|
-
const anonymousAuthClient = createJazzClient({
|
|
1477
|
-
appId: "my-app",
|
|
1478
|
-
env: "dev",
|
|
1479
|
-
userBranch: "main",
|
|
1480
|
-
});
|
|
1481
2067
|
|
|
1482
2068
|
return (
|
|
1483
2069
|
|
|
@@ -1488,11 +2074,6 @@ const anonymousAuthClient = createJazzClient({
|
|
|
1488
2074
|
If needed, you can explicitly override the local token:
|
|
1489
2075
|
|
|
1490
2076
|
```tsx
|
|
1491
|
-
const anonymousAuthWithTokenClient = createJazzClient({
|
|
1492
|
-
appId: "my-app",
|
|
1493
|
-
localAuthMode: "anonymous",
|
|
1494
|
-
localAuthToken: "device-token-123",
|
|
1495
|
-
});
|
|
1496
2077
|
|
|
1497
2078
|
return (
|
|
1498
2079
|
|
|
@@ -1507,12 +2088,6 @@ Use synthetic user profiles for local multi-user testing:
|
|
|
1507
2088
|
```tsx
|
|
1508
2089
|
const demoAuthAppId = "my-app";
|
|
1509
2090
|
const demoAuthActive = getActiveSyntheticAuth(demoAuthAppId, { defaultMode: "demo" });
|
|
1510
|
-
const demoAuthClient = createJazzClient({
|
|
1511
|
-
appId: demoAuthAppId,
|
|
1512
|
-
serverUrl: "http://127.0.0.1:4200",
|
|
1513
|
-
localAuthMode: demoAuthActive.localAuthMode,
|
|
1514
|
-
localAuthToken: demoAuthActive.localAuthToken,
|
|
1515
|
-
});
|
|
1516
2091
|
|
|
1517
2092
|
return (
|
|
1518
2093
|
<>
|
|
@@ -1530,15 +2105,6 @@ After provider sign-in, link the current local identity so existing local data s
|
|
|
1530
2105
|
const externalAuthAppId = "my-app";
|
|
1531
2106
|
const externalAuthServerUrl = "http://127.0.0.1:4200";
|
|
1532
2107
|
const externalAuthProviderJwt = "<provider-jwt>";
|
|
1533
|
-
const externalAuthLocalClient = createJazzClient({
|
|
1534
|
-
appId: externalAuthAppId,
|
|
1535
|
-
serverUrl: externalAuthServerUrl,
|
|
1536
|
-
});
|
|
1537
|
-
const externalAuthJwtClient = createJazzClient({
|
|
1538
|
-
appId: externalAuthAppId,
|
|
1539
|
-
serverUrl: externalAuthServerUrl,
|
|
1540
|
-
jwtToken: externalAuthProviderJwt,
|
|
1541
|
-
});
|
|
1542
2108
|
|
|
1543
2109
|
const [hasJwt, setHasJwt] = useState(false);
|
|
1544
2110
|
const linkExternalIdentity = useLinkExternalIdentity({
|
|
@@ -1581,7 +2147,6 @@ jazz-tools server "$JAZZ_APP_ID" \
|
|
|
1581
2147
|
Omit `serverUrl` to keep all data local:
|
|
1582
2148
|
|
|
1583
2149
|
```tsx
|
|
1584
|
-
const offlineOnlyAuthClient = createJazzClient({ appId: "my-app" });
|
|
1585
2150
|
|
|
1586
2151
|
return (
|
|
1587
2152
|
|
|
@@ -1593,7 +2158,7 @@ Full auth flows and server behavior: [Authentication](/docs/authentication).
|
|
|
1593
2158
|
|
|
1594
2159
|
## Permissions
|
|
1595
2160
|
|
|
1596
|
-
Define row-level policies in `
|
|
2161
|
+
Define row-level policies in `permissions.ts` with `s.definePermissions(...)`:
|
|
1597
2162
|
|
|
1598
2163
|
```ts
|
|
1599
2164
|
|
|
@@ -1631,24 +2196,28 @@ For full environment and server setup options, see [Setup](/docs/setup).
|
|
|
1631
2196
|
|
|
1632
2197
|
## Define your schema
|
|
1633
2198
|
|
|
1634
|
-
Create `schema
|
|
2199
|
+
Create `schema.ts` at the root of your project. This is the source of truth for your data model.
|
|
1635
2200
|
|
|
1636
2201
|
```ts
|
|
1637
2202
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
2203
|
+
const schema = {
|
|
2204
|
+
projects: s.table({
|
|
2205
|
+
name: s.string(),
|
|
2206
|
+
}),
|
|
2207
|
+
todos: s.table({
|
|
2208
|
+
title: s.string(),
|
|
2209
|
+
done: s.boolean(),
|
|
2210
|
+
description: s.string().optional(),
|
|
2211
|
+
parentId: s.ref("todos").optional(),
|
|
2212
|
+
projectId: s.ref("projects").optional(),
|
|
2213
|
+
}),
|
|
2214
|
+
};
|
|
2215
|
+
|
|
2216
|
+
type AppSchema = s.Schema<typeof schema>;
|
|
1641
2217
|
|
|
1642
|
-
table("todos", {
|
|
1643
|
-
title: col.string(),
|
|
1644
|
-
done: col.boolean(),
|
|
1645
|
-
description: col.string().optional(),
|
|
1646
|
-
parent: col.ref("todos").optional(),
|
|
1647
|
-
project: col.ref("projects").optional(),
|
|
1648
|
-
});
|
|
1649
2218
|
```
|
|
1650
2219
|
|
|
1651
|
-
###
|
|
2220
|
+
### Validate schema
|
|
1652
2221
|
|
|
1653
2222
|
Deep dive: [Schemas](/docs/schemas).
|
|
1654
2223
|
|
|
@@ -1660,8 +2229,12 @@ Call `createJazzClient` with your config outside the component tree, then pass t
|
|
|
1660
2229
|
|
|
1661
2230
|
<script lang="ts">
|
|
1662
2231
|
import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
|
|
2232
|
+
import AuthSessionExamples from './AuthSessionExamples.svelte';
|
|
1663
2233
|
import TodoList from './TodoList.svelte';
|
|
1664
2234
|
|
|
2235
|
+
// Keep docs-only auth snippets in the compiled example app.
|
|
2236
|
+
void AuthSessionExamples;
|
|
2237
|
+
|
|
1665
2238
|
const client = createJazzClient({
|
|
1666
2239
|
appId: 'todo-svelte-example',
|
|
1667
2240
|
serverUrl: 'http://127.0.0.1:1625',
|
|
@@ -1720,7 +2293,7 @@ Deep dive: [Reading Data](/docs/reading-data).
|
|
|
1720
2293
|
|
|
1721
2294
|
<script lang="ts">
|
|
1722
2295
|
import { getDb, QuerySubscription } from 'jazz-tools/svelte';
|
|
1723
|
-
import { app } from '../schema
|
|
2296
|
+
import { app } from '../schema.js';
|
|
1724
2297
|
|
|
1725
2298
|
const db = getDb();
|
|
1726
2299
|
const todos = new QuerySubscription(app.todos);
|
|
@@ -1729,25 +2302,25 @@ Deep dive: [Reading Data](/docs/reading-data).
|
|
|
1729
2302
|
app.todos.where({ done: false }).orderBy('title', 'asc').limit(50),
|
|
1730
2303
|
);
|
|
1731
2304
|
|
|
1732
|
-
const confirmedTodos = new QuerySubscription(app.todos, 'edge');
|
|
2305
|
+
const confirmedTodos = new QuerySubscription(app.todos, { tier: 'edge' });
|
|
1733
2306
|
|
|
1734
2307
|
let title = $state('');
|
|
1735
2308
|
|
|
1736
|
-
|
|
2309
|
+
function handleSubmit(e: SubmitEvent) {
|
|
1737
2310
|
e.preventDefault();
|
|
1738
2311
|
if (!title.trim()) return;
|
|
1739
2312
|
|
|
1740
|
-
|
|
2313
|
+
db.insert(app.todos, { title: title.trim(), done: false });
|
|
1741
2314
|
|
|
1742
2315
|
title = '';
|
|
1743
2316
|
}
|
|
1744
2317
|
|
|
1745
|
-
|
|
1746
|
-
|
|
2318
|
+
function toggleTodo(todo: { id: string; done: boolean }) {
|
|
2319
|
+
db.update(app.todos, todo.id, { done: !todo.done });
|
|
1747
2320
|
}
|
|
1748
2321
|
|
|
1749
|
-
|
|
1750
|
-
|
|
2322
|
+
function removeTodo(id: string) {
|
|
2323
|
+
db.delete(app.todos, id);
|
|
1751
2324
|
}
|
|
1752
2325
|
|
|
1753
2326
|
async function addImportantTodo(todoTitle: string) {
|
|
@@ -1795,12 +2368,12 @@ Mutation API details: [Writing Data](/docs/writing-data).
|
|
|
1795
2368
|
has synced to the nearest server. Use `global` when you need the broadest server-side durability
|
|
1796
2369
|
acknowledgement.
|
|
1797
2370
|
|
|
1798
|
-
### Reading with
|
|
2371
|
+
### Reading with query options
|
|
1799
2372
|
|
|
1800
|
-
Pass
|
|
2373
|
+
Pass `QueryOptions` as the second argument to `QuerySubscription` to control subscription delivery. Use `tier` when you need to gate initial delivery.
|
|
1801
2374
|
|
|
1802
2375
|
```ts
|
|
1803
|
-
const confirmedTodos = new QuerySubscription(app.todos, 'edge');
|
|
2376
|
+
const confirmedTodos = new QuerySubscription(app.todos, { tier: 'edge' });
|
|
1804
2377
|
```
|
|
1805
2378
|
|
|
1806
2379
|
### Writing with a durability tier
|
|
@@ -1847,7 +2420,7 @@ Outside components (or when you need deltas), use `db.subscribeAll` directly:
|
|
|
1847
2420
|
|
|
1848
2421
|
### Relations with include
|
|
1849
2422
|
|
|
1850
|
-
If your schema has `
|
|
2423
|
+
If your schema has `s.ref()` columns, Jazz infers typed include interfaces directly from `schema.ts`. Use `.include()` to load related rows in one query.
|
|
1851
2424
|
|
|
1852
2425
|
```ts
|
|
1853
2426
|
|
|
@@ -2048,51 +2621,143 @@ jazz-tools server "$JAZZ_APP_ID" \
|
|
|
2048
2621
|
|
|
2049
2622
|
```
|
|
2050
2623
|
|
|
2051
|
-
### Offline-only mode
|
|
2052
|
-
|
|
2053
|
-
Omit `serverUrl` to keep all data local:
|
|
2624
|
+
### Offline-only mode
|
|
2625
|
+
|
|
2626
|
+
Omit `serverUrl` to keep all data local:
|
|
2627
|
+
|
|
2628
|
+
```svelte
|
|
2629
|
+
|
|
2630
|
+
<script lang="ts">
|
|
2631
|
+
import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
|
|
2632
|
+
|
|
2633
|
+
const client = createJazzClient({ appId: 'my-app' });
|
|
2634
|
+
</script>
|
|
2635
|
+
|
|
2636
|
+
{#snippet children({ db })}
|
|
2637
|
+
<slot />
|
|
2638
|
+
{/snippet}
|
|
2639
|
+
|
|
2640
|
+
```
|
|
2641
|
+
|
|
2642
|
+
Full auth flows and server behavior: [Authentication](/docs/authentication).
|
|
2643
|
+
|
|
2644
|
+
## Permissions
|
|
2645
|
+
|
|
2646
|
+
Define row-level policies in `permissions.ts` with `s.definePermissions(...)`:
|
|
2647
|
+
|
|
2648
|
+
```ts
|
|
2649
|
+
|
|
2650
|
+
// Everyone can read todos.
|
|
2651
|
+
policy.todos.allowRead.where({}),
|
|
2652
|
+
// New todos start as incomplete.
|
|
2653
|
+
policy.todos.allowInsert.where({ done: false }),
|
|
2654
|
+
// Completed todos are immutable.
|
|
2655
|
+
policy.todos.allowUpdate.whereOld({ done: false }).whereNew({}),
|
|
2656
|
+
// Only open todos can be deleted.
|
|
2657
|
+
policy.todos.allowDelete.where({ done: false }),
|
|
2658
|
+
]);
|
|
2659
|
+
```
|
|
2660
|
+
|
|
2661
|
+
Think about permissions in four checks:
|
|
2662
|
+
|
|
2663
|
+
- Who can read a row?
|
|
2664
|
+
- Who can insert a row?
|
|
2665
|
+
- Which old/new row states are allowed on update?
|
|
2666
|
+
- Who can delete a row?
|
|
2667
|
+
|
|
2668
|
+
For advanced policy patterns and Rust parity, see [Permissions](/docs/permissions).
|
|
2669
|
+
|
|
2670
|
+
===PAGE:quickstarts/typescript-browser===
|
|
2671
|
+
TITLE:TypeScript (Browser)
|
|
2672
|
+
DESCRIPTION:Build a local-first browser app with Jazz using plain TypeScript. Schema, subscriptions, and mutations without a framework.
|
|
2673
|
+
|
|
2674
|
+
## Install
|
|
2675
|
+
|
|
2676
|
+
Install the runtime package:
|
|
2677
|
+
|
|
2678
|
+
`pnpm add jazz-tools`
|
|
2679
|
+
|
|
2680
|
+
For full environment and server setup options, see [Setup](/docs/setup).
|
|
2681
|
+
|
|
2682
|
+
## Define your schema
|
|
2683
|
+
|
|
2684
|
+
Create `schema.ts` at the root of your project. This is the source of truth for your data model.
|
|
2685
|
+
|
|
2686
|
+
```ts
|
|
2687
|
+
projects: s.table({
|
|
2688
|
+
name: s.string(),
|
|
2689
|
+
}),
|
|
2690
|
+
todos: s.table({
|
|
2691
|
+
title: s.string(),
|
|
2692
|
+
done: s.boolean(),
|
|
2693
|
+
description: s.string().optional(),
|
|
2694
|
+
ownerId: s.string().optional(),
|
|
2695
|
+
parentId: s.ref("todos").optional(),
|
|
2696
|
+
projectId: s.ref("projects").optional(),
|
|
2697
|
+
}),
|
|
2698
|
+
```
|
|
2699
|
+
|
|
2700
|
+
### Validate schema
|
|
2701
|
+
|
|
2702
|
+
Deep dive: [Schemas](/docs/schemas).
|
|
2703
|
+
For chunked browser file storage, see [Files & Blobs](/docs/files-and-blobs).
|
|
2704
|
+
|
|
2705
|
+
## Create a client
|
|
2706
|
+
|
|
2707
|
+
`createDb` initialises the Jazz runtime and returns a `Db` instance. Call it once at app startup.
|
|
2708
|
+
|
|
2709
|
+
```ts
|
|
2710
|
+
const db = await createDb({
|
|
2711
|
+
appId: readEnvAppId() ?? "todo-client-example",
|
|
2712
|
+
env: "dev",
|
|
2713
|
+
userBranch: "main",
|
|
2714
|
+
...config,
|
|
2715
|
+
});
|
|
2716
|
+
```
|
|
2717
|
+
|
|
2718
|
+
- `appId` identifies the app namespace for storage and sync.
|
|
2719
|
+
- `serverUrl` enables sync; omit it for offline-only mode.
|
|
2054
2720
|
|
|
2055
|
-
|
|
2721
|
+
## Read data
|
|
2056
2722
|
|
|
2057
|
-
|
|
2058
|
-
import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
|
|
2723
|
+
### Subscriptions
|
|
2059
2724
|
|
|
2060
|
-
|
|
2061
|
-
</script>
|
|
2725
|
+
`subscribeAll` streams live results into a callback whenever the data changes.
|
|
2062
2726
|
|
|
2063
|
-
|
|
2064
|
-
<slot />
|
|
2065
|
-
{/snippet}
|
|
2727
|
+
```ts
|
|
2066
2728
|
|
|
2729
|
+
return db.subscribeAll(app.todos.where({ done: false }), ({ all }) => onCount(all.length));
|
|
2730
|
+
}
|
|
2067
2731
|
```
|
|
2068
2732
|
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
## Permissions
|
|
2733
|
+
The callback receives `{ all }` — an array of the current matching rows. Call the returned function to unsubscribe.
|
|
2072
2734
|
|
|
2073
|
-
|
|
2735
|
+
### One-shot reads
|
|
2074
2736
|
|
|
2075
2737
|
```ts
|
|
2076
2738
|
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
// New todos start as incomplete.
|
|
2080
|
-
policy.todos.allowInsert.where({ done: false }),
|
|
2081
|
-
// Completed todos are immutable.
|
|
2082
|
-
policy.todos.allowUpdate.whereOld({ done: false }).whereNew({}),
|
|
2083
|
-
// Only open todos can be deleted.
|
|
2084
|
-
policy.todos.allowDelete.where({ done: false }),
|
|
2085
|
-
]);
|
|
2739
|
+
return db.all(app.todos.where({ done: false }));
|
|
2740
|
+
}
|
|
2086
2741
|
```
|
|
2087
2742
|
|
|
2088
|
-
|
|
2743
|
+
Deep dive: [Reading Data](/docs/reading-data).
|
|
2089
2744
|
|
|
2090
|
-
|
|
2091
|
-
- Who can insert a row?
|
|
2092
|
-
- Which old/new row states are allowed on update?
|
|
2093
|
-
- Who can delete a row?
|
|
2745
|
+
## Write data
|
|
2094
2746
|
|
|
2095
|
-
|
|
2747
|
+
```ts
|
|
2748
|
+
|
|
2749
|
+
db.insert(app.todos, {
|
|
2750
|
+
title: "Write docs",
|
|
2751
|
+
done: false,
|
|
2752
|
+
ownerId: EXAMPLE_OWNER_ID,
|
|
2753
|
+
projectId: EXAMPLE_PROJECT_ID,
|
|
2754
|
+
});
|
|
2755
|
+
db.update(app.todos, todoId, { done: true });
|
|
2756
|
+
db.delete(app.todos, todoId);
|
|
2757
|
+
}
|
|
2758
|
+
```
|
|
2759
|
+
|
|
2760
|
+
Mutation API details: [Writing Data](/docs/writing-data).
|
|
2096
2761
|
|
|
2097
2762
|
===PAGE:quickstarts/typescript-server===
|
|
2098
2763
|
TITLE:TypeScript Server
|
|
@@ -2108,61 +2773,67 @@ For full environment and server setup options, see [Setup](/docs/setup).
|
|
|
2108
2773
|
|
|
2109
2774
|
## Define your schema
|
|
2110
2775
|
|
|
2111
|
-
Create `schema
|
|
2776
|
+
Create `schema.ts` at the root of your project. This is the source of truth for your data model.
|
|
2112
2777
|
|
|
2113
2778
|
```ts
|
|
2114
2779
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2780
|
+
const schema = {
|
|
2781
|
+
projects: s.table({
|
|
2782
|
+
name: s.string(),
|
|
2783
|
+
}),
|
|
2784
|
+
todos: s.table({
|
|
2785
|
+
title: s.string(),
|
|
2786
|
+
done: s.boolean(),
|
|
2787
|
+
description: s.string().optional(),
|
|
2788
|
+
parentId: s.ref("todos").optional(),
|
|
2789
|
+
projectId: s.ref("projects").optional(),
|
|
2790
|
+
owner_id: s.string(),
|
|
2791
|
+
}),
|
|
2792
|
+
};
|
|
2118
2793
|
|
|
2119
|
-
|
|
2120
|
-
title: col.string(),
|
|
2121
|
-
done: col.boolean(),
|
|
2122
|
-
description: col.string().optional(),
|
|
2123
|
-
parent: col.ref("todos").optional(),
|
|
2124
|
-
project: col.ref("projects").optional(),
|
|
2125
|
-
ownerId: col.string(),
|
|
2126
|
-
});
|
|
2794
|
+
type AppSchema = s.Schema<typeof schema>;
|
|
2127
2795
|
|
|
2128
2796
|
```
|
|
2129
2797
|
|
|
2130
|
-
###
|
|
2798
|
+
### Validate schema
|
|
2131
2799
|
|
|
2132
2800
|
Deep dive: [Schemas](/docs/schemas).
|
|
2133
2801
|
|
|
2134
2802
|
## Create a server context
|
|
2135
2803
|
|
|
2136
|
-
Initialize a Jazz context once at process startup and lazily create a shared
|
|
2804
|
+
Initialize a Jazz context once at process startup and lazily create a shared `Db`:
|
|
2137
2805
|
|
|
2138
2806
|
```ts
|
|
2139
2807
|
const context = createJazzContext({
|
|
2140
2808
|
appId: "todo-server-ts",
|
|
2141
2809
|
app: schemaApp,
|
|
2142
|
-
|
|
2810
|
+
permissions,
|
|
2811
|
+
driver: { type: "persistent", dataPath: "./data/jazz.db" },
|
|
2143
2812
|
serverUrl: process.env.JAZZ_SERVER_URL,
|
|
2144
2813
|
backendSecret: process.env.JAZZ_BACKEND_SECRET,
|
|
2145
2814
|
});
|
|
2146
|
-
const
|
|
2815
|
+
const db = context.db();
|
|
2816
|
+
void db;
|
|
2147
2817
|
```
|
|
2148
2818
|
|
|
2149
2819
|
- `appId` identifies the app namespace for storage and sync.
|
|
2150
|
-
- `app` is your
|
|
2151
|
-
- `
|
|
2820
|
+
- `app` is your typed schema export (`app.wasmSchema` is loaded for runtime setup).
|
|
2821
|
+
- `context.db()` gives you a high-level `Db` using the context's configured runtime/auth.
|
|
2822
|
+
- `serverUrl` + `backendSecret` enable backend-authenticated handles via `context.asBackend()` and `context.forRequest(req)`.
|
|
2152
2823
|
- Runtime storage path controls where local server state persists.
|
|
2153
2824
|
|
|
2154
2825
|
For more server context options, see [Setup](/docs/setup#backend-context-setup).
|
|
2155
2826
|
|
|
2156
2827
|
## Read data in API handlers
|
|
2157
2828
|
|
|
2158
|
-
Use `context.forRequest(req)` so each request
|
|
2829
|
+
Use `context.forRequest(req)` so each request gets a requester-scoped `Db`.
|
|
2159
2830
|
|
|
2160
2831
|
### One-shot reads
|
|
2161
2832
|
|
|
2162
2833
|
```ts
|
|
2163
2834
|
|
|
2164
2835
|
const requester = context.forRequest(req);
|
|
2165
|
-
const rows = await requester.
|
|
2836
|
+
const rows = await requester.all(
|
|
2166
2837
|
schemaApp.todos.where({ done: false }).orderBy("title", "asc").limit(100),
|
|
2167
2838
|
);
|
|
2168
2839
|
res.json(rows);
|
|
@@ -2181,15 +2852,15 @@ Use `context.forRequest(req)` so each request is scoped to the requester session
|
|
|
2181
2852
|
res.setHeader("Connection", "keep-alive");
|
|
2182
2853
|
res.flushHeaders();
|
|
2183
2854
|
|
|
2184
|
-
const snapshot = await requester.
|
|
2855
|
+
const snapshot = await requester.all(query);
|
|
2185
2856
|
res.write(`data: ${JSON.stringify({ type: "snapshot", rows: snapshot })}\n\n`);
|
|
2186
2857
|
|
|
2187
|
-
const
|
|
2858
|
+
const unsubscribe = requester.subscribeAll(query, (delta) => {
|
|
2188
2859
|
res.write(`data: ${JSON.stringify({ type: "delta", delta })}\n\n`);
|
|
2189
2860
|
});
|
|
2190
2861
|
|
|
2191
2862
|
req.on("close", () => {
|
|
2192
|
-
|
|
2863
|
+
unsubscribe();
|
|
2193
2864
|
});
|
|
2194
2865
|
}
|
|
2195
2866
|
```
|
|
@@ -2198,7 +2869,7 @@ Deep dive: [Reading Data](/docs/reading-data).
|
|
|
2198
2869
|
|
|
2199
2870
|
## Write data behind API routes
|
|
2200
2871
|
|
|
2201
|
-
Mutations should also use `context.forRequest(req)` so policies evaluate against the requester context.
|
|
2872
|
+
Mutations should also use `context.forRequest(req)` so policies evaluate against the requester context while keeping the same high-level `Db` API.
|
|
2202
2873
|
|
|
2203
2874
|
```ts
|
|
2204
2875
|
|
|
@@ -2214,24 +2885,15 @@ Mutations should also use `context.forRequest(req)` so policies evaluate against
|
|
|
2214
2885
|
return;
|
|
2215
2886
|
}
|
|
2216
2887
|
|
|
2217
|
-
const values: Value[] = [
|
|
2218
|
-
{ type: "Text", value: title },
|
|
2219
|
-
{ type: "Boolean", value: false },
|
|
2220
|
-
{ type: "Text", value: req.body.description?.trim() ?? "" },
|
|
2221
|
-
{ type: "Null" },
|
|
2222
|
-
{ type: "Null" },
|
|
2223
|
-
{ type: "Text", value: userId },
|
|
2224
|
-
];
|
|
2225
|
-
|
|
2226
2888
|
const requester = context.forRequest(req);
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
res.status(201).json({
|
|
2230
|
-
id,
|
|
2889
|
+
const todo = requester.insert(schemaApp.todos, {
|
|
2231
2890
|
title,
|
|
2232
2891
|
done: false,
|
|
2892
|
+
description: req.body.description?.trim() || undefined,
|
|
2233
2893
|
owner_id: userId,
|
|
2234
2894
|
});
|
|
2895
|
+
|
|
2896
|
+
res.status(201).json(todo);
|
|
2235
2897
|
}
|
|
2236
2898
|
```
|
|
2237
2899
|
|
|
@@ -2275,7 +2937,7 @@ Full auth flows and session resolution: [Authentication](/docs/authentication).
|
|
|
2275
2937
|
|
|
2276
2938
|
## Permissions
|
|
2277
2939
|
|
|
2278
|
-
Define row-level policies in `
|
|
2940
|
+
Define row-level policies in `permissions.ts`:
|
|
2279
2941
|
|
|
2280
2942
|
```ts
|
|
2281
2943
|
|
|
@@ -2303,24 +2965,28 @@ For full environment and server setup options, see [Setup](/docs/setup).
|
|
|
2303
2965
|
|
|
2304
2966
|
## Define your schema
|
|
2305
2967
|
|
|
2306
|
-
Create `schema
|
|
2968
|
+
Create `schema.ts` at the root of your project. This is the source of truth for your data model.
|
|
2307
2969
|
|
|
2308
2970
|
```ts
|
|
2309
2971
|
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2972
|
+
const schema = {
|
|
2973
|
+
projects: s.table({
|
|
2974
|
+
name: s.string(),
|
|
2975
|
+
}),
|
|
2976
|
+
todos: s.table({
|
|
2977
|
+
title: s.string(),
|
|
2978
|
+
done: s.boolean(),
|
|
2979
|
+
description: s.string().optional(),
|
|
2980
|
+
parentId: s.ref("todos").optional(),
|
|
2981
|
+
projectId: s.ref("projects").optional(),
|
|
2982
|
+
}),
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2985
|
+
type AppSchema = s.Schema<typeof schema>;
|
|
2313
2986
|
|
|
2314
|
-
table("todos", {
|
|
2315
|
-
title: col.string(),
|
|
2316
|
-
done: col.boolean(),
|
|
2317
|
-
description: col.string().optional(),
|
|
2318
|
-
parent: col.ref("todos").optional(),
|
|
2319
|
-
project: col.ref("projects").optional(),
|
|
2320
|
-
});
|
|
2321
2987
|
```
|
|
2322
2988
|
|
|
2323
|
-
###
|
|
2989
|
+
### Validate schema
|
|
2324
2990
|
|
|
2325
2991
|
Deep dive: [Schemas](/docs/schemas).
|
|
2326
2992
|
|
|
@@ -2331,6 +2997,9 @@ Deep dive: [Schemas](/docs/schemas).
|
|
|
2331
2997
|
```vue
|
|
2332
2998
|
<script setup lang="ts">
|
|
2333
2999
|
|
|
3000
|
+
// Keep docs-only auth snippets in the compiled example app.
|
|
3001
|
+
void AuthSessionExamples;
|
|
3002
|
+
|
|
2334
3003
|
const client = createJazzClient({
|
|
2335
3004
|
appId: "todo-vue-example",
|
|
2336
3005
|
serverUrl: "http://127.0.0.1:1625",
|
|
@@ -2386,15 +3055,15 @@ Deep dive: [Reading Data](/docs/reading-data).
|
|
|
2386
3055
|
`useDb` returns the shared `Db` handle for mutations. Mutations apply locally first and return promises that resolve at the selected durability tier.
|
|
2387
3056
|
|
|
2388
3057
|
```ts
|
|
2389
|
-
|
|
2390
|
-
|
|
3058
|
+
function addTodo(todoTitle: string) {
|
|
3059
|
+
db.insert(app.todos, { title: todoTitle, done: false });
|
|
2391
3060
|
}
|
|
2392
3061
|
|
|
2393
|
-
|
|
2394
|
-
|
|
3062
|
+
function toggleTodo(todo: { id: string; done: boolean }) {
|
|
3063
|
+
db.update(app.todos, todo.id, { done: !todo.done });
|
|
2395
3064
|
}
|
|
2396
3065
|
|
|
2397
|
-
|
|
3066
|
+
function removeTodo(id: string) {
|
|
2398
3067
|
db.delete(app.todos, id);
|
|
2399
3068
|
}
|
|
2400
3069
|
```
|
|
@@ -2428,7 +3097,11 @@ Use durable mutation APIs with `{ tier }` to wait for specific durability tiers:
|
|
|
2428
3097
|
|
|
2429
3098
|
```ts
|
|
2430
3099
|
|
|
2431
|
-
const { id } = await db.insertDurable(
|
|
3100
|
+
const { id } = await db.insertDurable(
|
|
3101
|
+
app.todos,
|
|
3102
|
+
{ title: todoTitle, done: false },
|
|
3103
|
+
{ tier: "edge" },
|
|
3104
|
+
);
|
|
2432
3105
|
await db.updateDurable(app.todos, id, { done: true }, { tier: "edge" });
|
|
2433
3106
|
await db.deleteDurable(app.todos, id, { tier: "global" });
|
|
2434
3107
|
}
|
|
@@ -2464,7 +3137,7 @@ Outside components (or when you need deltas), use `db.subscribeAll` directly:
|
|
|
2464
3137
|
|
|
2465
3138
|
### Relations with include
|
|
2466
3139
|
|
|
2467
|
-
If your schema has `
|
|
3140
|
+
If your schema has `s.ref()` columns, Jazz infers typed include interfaces directly from `schema.ts`. Use `.include()` to load related rows in one query.
|
|
2468
3141
|
|
|
2469
3142
|
```ts
|
|
2470
3143
|
|
|
@@ -2673,7 +3346,7 @@ Full auth flows and server behavior: [Authentication](/docs/authentication).
|
|
|
2673
3346
|
|
|
2674
3347
|
## Permissions
|
|
2675
3348
|
|
|
2676
|
-
Define row-level policies in `
|
|
3349
|
+
Define row-level policies in `permissions.ts` with `s.definePermissions(...)`:
|
|
2677
3350
|
|
|
2678
3351
|
```ts
|
|
2679
3352
|
|
|
@@ -2766,6 +3439,135 @@ const todos = useAll(app.todos);
|
|
|
2766
3439
|
const todos = new QuerySubscription(app.todos);
|
|
2767
3440
|
```
|
|
2768
3441
|
|
|
3442
|
+
### First delivery and the undefined signal
|
|
3443
|
+
|
|
3444
|
+
`useAll` and `QuerySubscription` return `undefined` until the subscription has received its
|
|
3445
|
+
first response from the server. Once the server has delivered an initial snapshot, the value
|
|
3446
|
+
becomes an array — empty (`[]`) if no rows match, or populated.
|
|
3447
|
+
|
|
3448
|
+
```ts
|
|
3449
|
+
// undefined = still connecting to the sync server
|
|
3450
|
+
// [] = connected, no matching rows yet
|
|
3451
|
+
// [...] = connected, rows present
|
|
3452
|
+
```
|
|
3453
|
+
|
|
3454
|
+
Any initialisation that depends on a consistent remote view can check `undefined` directly:
|
|
3455
|
+
|
|
3456
|
+
```ts
|
|
3457
|
+
useEffect(() => {
|
|
3458
|
+
if (rows === undefined) return; // wait for server snapshot before acting
|
|
3459
|
+
reconcile();
|
|
3460
|
+
}, [rows]);
|
|
3461
|
+
```
|
|
3462
|
+
|
|
3463
|
+
### Running queries conditionally
|
|
3464
|
+
|
|
3465
|
+
There are some cases where you want to defer running a query. This could happen if the query depends on an input the user hasn't provided yet,
|
|
3466
|
+
or on the output of a previous query. In these cases, you can pass `undefined` to `useAll` to defer the query until the condition is met.
|
|
3467
|
+
|
|
3468
|
+
```ts
|
|
3469
|
+
const [filter, setFilter] = useState<string | null>(null);
|
|
3470
|
+
const rows = useAll(filter ? app.todos.where({ title: { contains: filter } }) : undefined);
|
|
3471
|
+
```
|
|
3472
|
+
|
|
3473
|
+
When an undefined query is passed to `useAll`, it will return `undefined` until the query is provided.
|
|
3474
|
+
|
|
3475
|
+
### React Suspense and Transitions
|
|
3476
|
+
|
|
3477
|
+
The docs React example app includes a Suspense-based list that keeps the previous result visible
|
|
3478
|
+
while a filter or page change resolves by moving the query behind a `Suspense` boundary and
|
|
3479
|
+
reading it with `useAllSuspense(query)`.
|
|
3480
|
+
|
|
3481
|
+
```tsx
|
|
3482
|
+
|
|
3483
|
+
const db = useDb();
|
|
3484
|
+
const [title, setTitle] = useState("");
|
|
3485
|
+
const [filterTitle, setFilterTitle] = useState("");
|
|
3486
|
+
const [showDoneOnly, setShowDoneOnly] = useState(false);
|
|
3487
|
+
const [page, setPage] = useState(0);
|
|
3488
|
+
const [isPending, startTransition] = useTransition();
|
|
3489
|
+
const deferredFilterTitle = useDeferredValue(filterTitle);
|
|
3490
|
+
|
|
3491
|
+
let query = app.todos
|
|
3492
|
+
.orderBy("id", "desc")
|
|
3493
|
+
.limit(25)
|
|
3494
|
+
.offset(page * 25);
|
|
3495
|
+
|
|
3496
|
+
if (deferredFilterTitle.trim()) {
|
|
3497
|
+
query = query.where({ title: { contains: deferredFilterTitle.trim() } });
|
|
3498
|
+
}
|
|
3499
|
+
if (showDoneOnly) {
|
|
3500
|
+
query = query.where({ done: true });
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
const isLoading = isPending || deferredFilterTitle !== filterTitle;
|
|
3504
|
+
|
|
3505
|
+
function updatePage(nextPage: number) {
|
|
3506
|
+
startTransition(() => {
|
|
3507
|
+
setPage(nextPage);
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function handleFilterChange(e: React.ChangeEvent) {
|
|
3512
|
+
setFilterTitle(e.target.value);
|
|
3513
|
+
startTransition(() => {
|
|
3514
|
+
setPage(0);
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
3519
|
+
e.preventDefault();
|
|
3520
|
+
const trimmedTitle = title.trim();
|
|
3521
|
+
|
|
3522
|
+
if (!trimmedTitle) {
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
await db.insert(app.todos, { title: trimmedTitle, done: false });
|
|
3527
|
+
setTitle("");
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
return (
|
|
3531
|
+
<>
|
|
3532
|
+
<form onSubmit={(e) => void handleSubmit(e)}>
|
|
3533
|
+
<input
|
|
3534
|
+
type="text"
|
|
3535
|
+
value={title}
|
|
3536
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
3537
|
+
placeholder="What needs to be done?"
|
|
3538
|
+
required
|
|
3539
|
+
/>
|
|
3540
|
+
<button type="submit">Add</button>
|
|
3541
|
+
</form>
|
|
3542
|
+
|
|
3543
|
+
<div>
|
|
3544
|
+
<input
|
|
3545
|
+
type="text"
|
|
3546
|
+
value={filterTitle}
|
|
3547
|
+
onChange={handleFilterChange}
|
|
3548
|
+
placeholder="Filter by title (contains)"
|
|
3549
|
+
aria-label="Filter by title"
|
|
3550
|
+
/>
|
|
3551
|
+
<label>
|
|
3552
|
+
<input
|
|
3553
|
+
type="checkbox"
|
|
3554
|
+
checked={showDoneOnly}
|
|
3555
|
+
onChange={(e) => setShowDoneOnly(e.target.checked)}
|
|
3556
|
+
/>
|
|
3557
|
+
Done only
|
|
3558
|
+
</label>
|
|
3559
|
+
</div>
|
|
3560
|
+
|
|
3561
|
+
Loading todos...</p>}>
|
|
3562
|
+
<div style={{ opacity: isLoading ? 0.5 : 1, transition: "opacity 0.2s" }}>
|
|
3563
|
+
|
|
3564
|
+
</div>
|
|
3565
|
+
|
|
3566
|
+
</>
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3569
|
+
```
|
|
3570
|
+
|
|
2769
3571
|
## Filters
|
|
2770
3572
|
|
|
2771
3573
|
### API Reference
|
|
@@ -2785,7 +3587,7 @@ const todos = useAll(app.todos);
|
|
|
2785
3587
|
|
|
2786
3588
|
await db.all(app.todos.where({ done: false }));
|
|
2787
3589
|
await db.all(app.todos.where({ title: { contains: "milk" } }));
|
|
2788
|
-
await db.all(app.todos.where({
|
|
3590
|
+
await db.all(app.todos.where({ projectId: { ne: EXAMPLE_PROJECT_ID } }));
|
|
2789
3591
|
}
|
|
2790
3592
|
```
|
|
2791
3593
|
|
|
@@ -2869,41 +3671,48 @@ pub async fn read_todo_page(
|
|
|
2869
3671
|
- **Behavior:** `include(...)` loads related rows and returns nested objects in one query result.
|
|
2870
3672
|
- **Parameters (TypeScript):** `include({ relationName: true | nestedInclude | queryBuilder })`.
|
|
2871
3673
|
- **Nested includes:** pass nested objects to load multi-hop relations.
|
|
3674
|
+
- **Missing forward references:** included forward relations may be `undefined` at runtime, even when the FK column is non-nullable. Jazz cannot guarantee referential integrity at read time: a referenced row may be deleted, not synced locally yet, or hidden by permissions.
|
|
3675
|
+
- **Required forward includes (TypeScript):** call `requireIncludes()` after `include(...)` to drop rows whose included references cannot be resolved.
|
|
3676
|
+
- **Nullable scalar FKs:** `requireIncludes()` does not drop a row just because a nullable FK is `null`; in that case the included relation remains `undefined`.
|
|
3677
|
+
- **Reverse relations:** `requireIncludes()` does not force reverse includes to be non-empty and does not filter on them.
|
|
3678
|
+
- **Forward array refs:** without `requireIncludes()`, missing referenced members are skipped from the included array. With `requireIncludes()`, the row is dropped if any referenced member is unavailable.
|
|
3679
|
+
|
|
3680
|
+
For chunked files stored in the conventional `files` / `file_parts` tables, prefer
|
|
3681
|
+
`db.loadFileAsBlob(...)` or `db.loadFileAsStream(...)` over eager `include(...)` loading of
|
|
3682
|
+
parts. See [Files & Blobs](/docs/files-and-blobs).
|
|
2872
3683
|
|
|
2873
3684
|
Schema references used by the include examples:
|
|
2874
3685
|
|
|
2875
3686
|
```ts
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
});
|
|
2888
|
-
|
|
3687
|
+
projects: s.table({
|
|
3688
|
+
name: s.string(),
|
|
3689
|
+
}),
|
|
3690
|
+
todos: s.table({
|
|
3691
|
+
title: s.string(),
|
|
3692
|
+
done: s.boolean(),
|
|
3693
|
+
description: s.string().optional(),
|
|
3694
|
+
ownerId: s.string().optional(),
|
|
3695
|
+
parentId: s.ref("todos").optional(),
|
|
3696
|
+
projectId: s.ref("projects").optional(),
|
|
3697
|
+
}),
|
|
2889
3698
|
```
|
|
2890
3699
|
|
|
2891
|
-
```
|
|
2892
|
-
CREATE TABLE projects (
|
|
2893
|
-
name TEXT NOT NULL
|
|
2894
|
-
);
|
|
3700
|
+
```ts
|
|
2895
3701
|
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
)
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3702
|
+
const schema = {
|
|
3703
|
+
projects: s.table({
|
|
3704
|
+
name: s.string(),
|
|
3705
|
+
}),
|
|
3706
|
+
todos: s.table({
|
|
3707
|
+
title: s.string(),
|
|
3708
|
+
done: s.boolean(),
|
|
3709
|
+
description: s.string().optional(),
|
|
3710
|
+
parent: s.ref("todos").optional(),
|
|
3711
|
+
project: s.ref("projects").optional(),
|
|
3712
|
+
}),
|
|
3713
|
+
};
|
|
3714
|
+
|
|
3715
|
+
type AppSchema = s.Schema<typeof schema>;
|
|
2907
3716
|
|
|
2908
3717
|
```
|
|
2909
3718
|
|
|
@@ -2926,6 +3735,58 @@ pub async fn read_todos_with_related_rows(client: &JazzClient) -> jazz_tools::Re
|
|
|
2926
3735
|
}
|
|
2927
3736
|
```
|
|
2928
3737
|
|
|
3738
|
+
## Picking Columns via select
|
|
3739
|
+
|
|
3740
|
+
### API Reference
|
|
3741
|
+
|
|
3742
|
+
- **Availability:** generated TypeScript query builders.
|
|
3743
|
+
- **Behavior:** `select(...)` narrows the root row to `id` plus the columns you pick.
|
|
3744
|
+
- **With includes:** `include(...)` still works after `select(...)`.
|
|
3745
|
+
- **Selecting in included rows:** pass a query builder inside `include(...)`, for example `include({ project: app.projects.select("name") })`.
|
|
3746
|
+
|
|
3747
|
+
```ts
|
|
3748
|
+
|
|
3749
|
+
return db.all(
|
|
3750
|
+
app.todos
|
|
3751
|
+
.select("title")
|
|
3752
|
+
.where({ done: false })
|
|
3753
|
+
.include({ project: app.projects.select("name") }),
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
```
|
|
3757
|
+
|
|
3758
|
+
## Magic Columns
|
|
3759
|
+
|
|
3760
|
+
### Permission Introspection
|
|
3761
|
+
|
|
3762
|
+
Permission introspection columns are computed for the current session and are omitted from
|
|
3763
|
+
`select("*")`, so you opt into them explicitly.
|
|
3764
|
+
|
|
3765
|
+
For example, `select("*", "$canDelete")` returns the normal row columns plus `$canDelete`.
|
|
3766
|
+
|
|
3767
|
+
- `$canRead` reflects whether the current session can read the row. Since read policies already
|
|
3768
|
+
gate which rows appear, returned rows will usually show `$canRead: true`.
|
|
3769
|
+
- `$canEdit` reflects whether the current session passes the row's `UPDATE USING` policy.
|
|
3770
|
+
- `$canDelete` reflects whether the current session passes the row's `DELETE USING` policy.
|
|
3771
|
+
- Without a session, permission introspection columns return `null`.
|
|
3772
|
+
|
|
3773
|
+
```ts
|
|
3774
|
+
|
|
3775
|
+
return db.all(
|
|
3776
|
+
app.todos.select("title", "$canRead", "$canEdit", "$canDelete").orderBy("title", "asc"),
|
|
3777
|
+
);
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
return db.all(app.todos.select("*", "$canDelete").orderBy("title", "asc"));
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
return db.all(app.todos.where({ $canEdit: true }).select("title", "$canEdit"));
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
return db.all(app.todos.where({ $canDelete: true }).select("title", "$canDelete"));
|
|
3787
|
+
}
|
|
3788
|
+
```
|
|
3789
|
+
|
|
2929
3790
|
## Advanced: Recursive Queries
|
|
2930
3791
|
|
|
2931
3792
|
Recursive queries compute bounded transitive closures from a seed row set.
|
|
@@ -2934,7 +3795,7 @@ Recursive queries compute bounded transitive closures from a seed row set.
|
|
|
2934
3795
|
|
|
2935
3796
|
return app.todos.gather({
|
|
2936
3797
|
start: { done: false },
|
|
2937
|
-
step: ({ current }) => app.todos.where({
|
|
3798
|
+
step: ({ current }) => app.todos.where({ parentId: current }).hopTo("parent"),
|
|
2938
3799
|
maxDepth: 10,
|
|
2939
3800
|
});
|
|
2940
3801
|
}
|
|
@@ -2947,8 +3808,12 @@ Recursive queries compute bounded transitive closures from a seed row set.
|
|
|
2947
3808
|
- `db.all(query, options?)`
|
|
2948
3809
|
- `db.one(query, options?)`
|
|
2949
3810
|
- `db.subscribeAll(query, callback, options?)`
|
|
3811
|
+
- `useAll(query, options?)`
|
|
3812
|
+
- `useAllSuspense(query, options?)`
|
|
3813
|
+
- `new QuerySubscription(query, options?)`
|
|
2950
3814
|
- `options.tier`: `"worker" | "edge" | "global"`
|
|
2951
3815
|
- `options.localUpdates`: `"immediate" | "deferred"` (default: `"immediate"`)
|
|
3816
|
+
- `options.propagation`: `"full" | "local-only"` (default: `"full"`)
|
|
2952
3817
|
|
|
2953
3818
|
Durability tiers control when initial query delivery is considered confirmed:
|
|
2954
3819
|
|
|
@@ -2961,6 +3826,7 @@ Defaults:
|
|
|
2961
3826
|
- Browser/client default tier is `worker`.
|
|
2962
3827
|
- Backend/server-connected default tier is `edge`.
|
|
2963
3828
|
- `localUpdates: "immediate"` keeps local subscription updates synchronous after the first tier-confirmed snapshot. The initial delivery remains tier-gated.
|
|
3829
|
+
- `propagation: "full"` sends query updates through the normal local+remote path. `propagation: "local-only"` limits delivery to local changes.
|
|
2964
3830
|
|
|
2965
3831
|
Tradeoff summary:
|
|
2966
3832
|
|
|
@@ -2987,140 +3853,272 @@ Read durability options pair naturally with write durability tiers:
|
|
|
2987
3853
|
|
|
2988
3854
|
- Use write durability tiers to gate mutation durability checkpoints.
|
|
2989
3855
|
- Use read durability options to gate query/subscription delivery checkpoints.
|
|
3856
|
+
- React hooks forward these options to the underlying `db.subscribeAll(...)` subscription.
|
|
3857
|
+
- Svelte `QuerySubscription` forwards these options to the underlying `db.subscribeAll(...)` subscription.
|
|
2990
3858
|
|
|
2991
3859
|
See [Write Durability Tiers](/docs/writing-data#write-durability-tiers) for write-side semantics.
|
|
2992
3860
|
|
|
3861
|
+
### Seeding and one-time setup
|
|
3862
|
+
|
|
3863
|
+
For one-off setup (seeding default data, initialising a user record on first sign-in), you
|
|
3864
|
+
often need to ensure no other client has already done the same work before writing. Use
|
|
3865
|
+
`db.all` with `{ tier: "global" }` to wait until the global core has been consulted before
|
|
3866
|
+
reading — this prevents duplicate seeding from concurrent fresh clients.
|
|
3867
|
+
|
|
3868
|
+
```ts
|
|
3869
|
+
// Reads nothing until globally consistent — prevents duplicate seeding
|
|
3870
|
+
// from concurrent fresh clients on first visit.
|
|
3871
|
+
const existing = await db.all(app.settings, { tier: "global" });
|
|
3872
|
+
|
|
3873
|
+
if (existing.length === 0) {
|
|
3874
|
+
for (const seed of DEFAULT_SETTINGS) {
|
|
3875
|
+
await db.insert(app.settings, seed);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
```
|
|
3879
|
+
|
|
3880
|
+
| Call | Waits for | Use when |
|
|
3881
|
+
| ----------------------------------- | ------------------------- | ------------------------------------------------- |
|
|
3882
|
+
| `db.all(query)` | Local worker (OPFS) | Reading cached local state |
|
|
3883
|
+
| `db.all(query, { tier: "global" })` | Global core snapshot | Seeding or one-time setup that must not duplicate |
|
|
3884
|
+
| `useAll(query)` | Local snapshot, then live | Live UI that re-renders on every change |
|
|
3885
|
+
|
|
3886
|
+
For most subscriptions, omitting a tier is the right choice: Jazz delivers results from local
|
|
3887
|
+
storage immediately and streams in remote updates as they arrive, giving users the fastest
|
|
3888
|
+
possible experience. Reserve explicit tiers for the cases above where eventual consistency is
|
|
3889
|
+
not acceptable.
|
|
3890
|
+
|
|
2993
3891
|
===PAGE:schemas===
|
|
2994
3892
|
TITLE:Schemas
|
|
2995
|
-
DESCRIPTION:Define tables and relationships in
|
|
3893
|
+
DESCRIPTION:Define tables and relationships in root schema.ts, then review migration edges as your app evolves.
|
|
2996
3894
|
|
|
2997
3895
|
## Table Definitions
|
|
2998
3896
|
|
|
3897
|
+
Every Jazz app defines its schema in a root `schema.ts`, including Rust backends.
|
|
3898
|
+
|
|
2999
3899
|
```ts
|
|
3000
3900
|
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3901
|
+
const schema = {
|
|
3902
|
+
projects: s.table({
|
|
3903
|
+
name: s.string(),
|
|
3904
|
+
}),
|
|
3905
|
+
todos: s.table({
|
|
3906
|
+
title: s.string(),
|
|
3907
|
+
done: s.boolean(),
|
|
3908
|
+
description: s.string().optional(),
|
|
3909
|
+
parent: s.ref("todos").optional(),
|
|
3910
|
+
project: s.ref("projects").optional(),
|
|
3911
|
+
}),
|
|
3912
|
+
};
|
|
3004
3913
|
|
|
3005
|
-
|
|
3006
|
-
title: col.string(),
|
|
3007
|
-
done: col.boolean(),
|
|
3008
|
-
description: col.string().optional(),
|
|
3009
|
-
parent: col.ref("todos").optional(),
|
|
3010
|
-
project: col.ref("projects").optional(),
|
|
3011
|
-
});
|
|
3914
|
+
type AppSchema = s.Schema<typeof schema>;
|
|
3012
3915
|
|
|
3013
3916
|
```
|
|
3014
3917
|
|
|
3015
|
-
|
|
3016
|
-
CREATE TABLE projects (
|
|
3017
|
-
name TEXT NOT NULL
|
|
3018
|
-
);
|
|
3918
|
+
Rust apps use the same `schema.ts` file. The schema definition language is still TypeScript, and Rust consumes the compiled internal schema representation through the CLI and low-level APIs.
|
|
3019
3919
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3920
|
+
```ts
|
|
3921
|
+
|
|
3922
|
+
const schema = {
|
|
3923
|
+
projects: s.table({
|
|
3924
|
+
name: s.string(),
|
|
3925
|
+
}),
|
|
3926
|
+
todos: s.table({
|
|
3927
|
+
title: s.string(),
|
|
3928
|
+
done: s.boolean(),
|
|
3929
|
+
description: s.string().optional(),
|
|
3930
|
+
parent: s.ref("todos").optional(),
|
|
3931
|
+
project: s.ref("projects").optional(),
|
|
3932
|
+
}),
|
|
3933
|
+
};
|
|
3934
|
+
|
|
3935
|
+
type AppSchema = s.Schema<typeof schema>;
|
|
3031
3936
|
|
|
3032
3937
|
```
|
|
3033
3938
|
|
|
3939
|
+
In practice, a Rust app runs the schema export command and deserializes the JSON into Jazz's Rust `Schema` type:
|
|
3940
|
+
|
|
3941
|
+
```bash
|
|
3942
|
+
npx jazz-tools schema export --format json
|
|
3943
|
+
```
|
|
3944
|
+
|
|
3945
|
+
The Rust examples shell out to this command at startup, so the schema definition stays unified even when the app itself is written in Rust. See [Setup](/docs/setup) for the end-to-end backend context pattern.
|
|
3946
|
+
|
|
3947
|
+
Each top-level property of the `schema` object is a table. In the example above, `projects` and `todos` become tables, and each nested property becomes a column on that table.
|
|
3948
|
+
|
|
3949
|
+
The `type AppSchema = s.Schema<typeof schema>; export const app: s.App = s.defineApp(schema);` pattern is there to keep TypeScript hovers short and readable. Without that alias boundary, TypeScript tends to expand the entire schema literal when you hover `app`, `app.todos`, or derived row/query types.
|
|
3950
|
+
|
|
3034
3951
|
## Datatypes and References
|
|
3035
3952
|
|
|
3036
|
-
-
|
|
3037
|
-
-
|
|
3038
|
-
-
|
|
3953
|
+
- Use `s.string()`, `s.boolean()`, `s.int()`, `s.timestamp()`, and `s.bytes()` for scalar fields.
|
|
3954
|
+
- Use `s.ref("tableName")` for foreign keys. Jazz infers typed `.include(...)` relations directly from `schema.ts`.
|
|
3955
|
+
- For browser files and large uploaded blobs, define the conventional `files` / `file_parts` tables and store a normal reference to `files` from your app rows. See [Files & Blobs](/docs/files-and-blobs).
|
|
3039
3956
|
|
|
3040
3957
|
### Available column types
|
|
3041
3958
|
|
|
3042
|
-
##
|
|
3959
|
+
## Workflow and Schema Evolution
|
|
3043
3960
|
|
|
3044
|
-
###
|
|
3961
|
+
### Edit `schema.ts` during development
|
|
3045
3962
|
|
|
3046
|
-
|
|
3047
|
-
#!/usr/bin/env bash
|
|
3963
|
+
Most of the time, schema evolution starts with a normal edit to `schema.ts`. New writes use the new schema immediately.
|
|
3048
3964
|
|
|
3049
|
-
|
|
3050
|
-
npx jazz-tools@alpha build
|
|
3965
|
+
When older rows are no longer reachable from the current schema graph, Jazz logs a warning with the exact hashes you need next. These warnings can appear in browser logs, backend logs, or be forwarded from upstream servers. A typical warning looks like this, wrapped for readability:
|
|
3051
3966
|
|
|
3967
|
+
```text
|
|
3968
|
+
[client] Detected 3 rows of todos with differing schema versions.
|
|
3969
|
+
[client] To ensure data visibility and forward/backward compatibility please create
|
|
3970
|
+
a new migration with:
|
|
3971
|
+
npx jazz-tools migrations create \
|
|
3972
|
+
a01f5c72ec47 \
|
|
3973
|
+
311995e9a178
|
|
3052
3974
|
```
|
|
3053
3975
|
|
|
3054
|
-
|
|
3976
|
+
That warning is the handoff from day-to-day schema editing to reviewed compatibility work.
|
|
3055
3977
|
|
|
3056
|
-
###
|
|
3978
|
+
### Generate a migration edge with `migrations create`
|
|
3057
3979
|
|
|
3058
|
-
|
|
3059
|
-
SQL migrations are split into explicit forward and backward files.
|
|
3980
|
+
`migrations create` connects to your Jazz server, loads both schema hashes, and writes a prefilled file into `migrations/{dateCreated}-unnamed-{fromHash}-{toHash}.ts`.
|
|
3060
3981
|
|
|
3061
|
-
|
|
3982
|
+
```bash
|
|
3983
|
+
npx jazz-tools migrations create <fromHash> <toHash>
|
|
3984
|
+
```
|
|
3062
3985
|
|
|
3063
|
-
|
|
3064
|
-
description: col.add().string({ default: "" }),
|
|
3065
|
-
});
|
|
3986
|
+
The generated file already contains `fromHash`, `toHash`, and minimal typed `from` / `to` witnesses. In most cases, the only manual work is to fill in `migrate` and replace `unnamed` in the filename with a real migration name.
|
|
3066
3987
|
|
|
3067
|
-
|
|
3988
|
+
### Publish a reviewed edge with `migrations push`
|
|
3068
3989
|
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
Backward:
|
|
3074
|
-
```sql
|
|
3075
|
-
ALTER TABLE todos DROP COLUMN description;
|
|
3990
|
+
Publishing is the explicit gate. New schema hashes are still learned automatically when clients connect, but Jazz never auto-pushes migrations.
|
|
3991
|
+
|
|
3992
|
+
```bash
|
|
3993
|
+
npx jazz-tools migrations push <fromHash> <toHash>
|
|
3076
3994
|
```
|
|
3077
3995
|
|
|
3078
|
-
|
|
3996
|
+
Run this after reviewing the migration file. Once pushed, that edge becomes available to make data visible across those schema hashes.
|
|
3079
3997
|
|
|
3080
|
-
|
|
3998
|
+
## Example Migration Files
|
|
3081
3999
|
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
4000
|
+
Migration files live in `migrations/` and are always TypeScript, even for Rust apps.
|
|
4001
|
+
|
|
4002
|
+
### Generated stub
|
|
4003
|
+
|
|
4004
|
+
```ts
|
|
4005
|
+
|
|
4006
|
+
migrate: {
|
|
4007
|
+
todos: {
|
|
4008
|
+
description: s.add.string({ default: null }),
|
|
4009
|
+
},
|
|
4010
|
+
},
|
|
4011
|
+
fromHash: "a01f5c72ec47",
|
|
4012
|
+
toHash: "311995e9a178",
|
|
4013
|
+
from: {
|
|
4014
|
+
todos: s.table({
|
|
4015
|
+
title: s.string(),
|
|
4016
|
+
done: s.boolean(),
|
|
4017
|
+
parentId: s.ref("todos").optional(),
|
|
4018
|
+
projectId: s.ref("projects").optional(),
|
|
4019
|
+
owner_id: s.string(),
|
|
4020
|
+
}),
|
|
4021
|
+
},
|
|
4022
|
+
to: {
|
|
4023
|
+
todos: s.table({
|
|
4024
|
+
title: s.string(),
|
|
4025
|
+
done: s.boolean(),
|
|
4026
|
+
description: s.string().optional(),
|
|
4027
|
+
parentId: s.ref("todos").optional(),
|
|
4028
|
+
projectId: s.ref("projects").optional(),
|
|
4029
|
+
owner_id: s.string(),
|
|
4030
|
+
}),
|
|
4031
|
+
},
|
|
3085
4032
|
});
|
|
3086
4033
|
|
|
3087
4034
|
```
|
|
3088
4035
|
|
|
3089
|
-
|
|
3090
|
-
-- Example of editing a generated migration stub.
|
|
3091
|
-
ALTER TABLE todos ADD COLUMN description TEXT DEFAULT 'No description';
|
|
4036
|
+
### Edited stub
|
|
3092
4037
|
|
|
3093
|
-
```
|
|
4038
|
+
```ts
|
|
3094
4039
|
|
|
3095
|
-
|
|
4040
|
+
// Example of editing a generated migration stub.
|
|
3096
4041
|
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
4042
|
+
migrate: {
|
|
4043
|
+
todos: {
|
|
4044
|
+
description: s.add.string({ default: "No description" }),
|
|
4045
|
+
},
|
|
4046
|
+
},
|
|
4047
|
+
fromHash: "a01f5c72ec47",
|
|
4048
|
+
toHash: "311995e9a178",
|
|
4049
|
+
from: {
|
|
4050
|
+
todos: s.table({
|
|
4051
|
+
title: s.string(),
|
|
4052
|
+
done: s.boolean(),
|
|
4053
|
+
parentId: s.ref("todos").optional(),
|
|
4054
|
+
projectId: s.ref("projects").optional(),
|
|
4055
|
+
owner_id: s.string(),
|
|
4056
|
+
}),
|
|
4057
|
+
},
|
|
4058
|
+
to: {
|
|
4059
|
+
todos: s.table({
|
|
4060
|
+
title: s.string(),
|
|
4061
|
+
done: s.boolean(),
|
|
4062
|
+
description: s.string().optional(),
|
|
4063
|
+
parentId: s.ref("todos").optional(),
|
|
4064
|
+
projectId: s.ref("projects").optional(),
|
|
4065
|
+
owner_id: s.string(),
|
|
4066
|
+
}),
|
|
4067
|
+
},
|
|
4068
|
+
});
|
|
3101
4069
|
|
|
3102
|
-
|
|
4070
|
+
```
|
|
4071
|
+
|
|
4072
|
+
### Backwards default example
|
|
4073
|
+
|
|
4074
|
+
```ts
|
|
3103
4075
|
|
|
3104
4076
|
// Example: dropping a column with a backwards default.
|
|
3105
|
-
// Clients still on
|
|
3106
|
-
|
|
3107
|
-
migrate
|
|
3108
|
-
|
|
4077
|
+
// Clients still on the older schema continue seeing legacy_priority.
|
|
4078
|
+
|
|
4079
|
+
migrate: {
|
|
4080
|
+
todos: {
|
|
4081
|
+
legacy_priority: s.drop.int({ backwardsDefault: 0 }),
|
|
4082
|
+
},
|
|
4083
|
+
},
|
|
4084
|
+
fromHash: "311995e9a178",
|
|
4085
|
+
toHash: "73b65d082ab8",
|
|
4086
|
+
from: {
|
|
4087
|
+
todos: s.table({
|
|
4088
|
+
title: s.string(),
|
|
4089
|
+
done: s.boolean(),
|
|
4090
|
+
description: s.string().optional(),
|
|
4091
|
+
parentId: s.ref("todos").optional(),
|
|
4092
|
+
projectId: s.ref("projects").optional(),
|
|
4093
|
+
owner_id: s.string(),
|
|
4094
|
+
legacy_priority: s.int(),
|
|
4095
|
+
}),
|
|
4096
|
+
},
|
|
4097
|
+
to: {
|
|
4098
|
+
todos: s.table({
|
|
4099
|
+
title: s.string(),
|
|
4100
|
+
done: s.boolean(),
|
|
4101
|
+
description: s.string().optional(),
|
|
4102
|
+
parentId: s.ref("todos").optional(),
|
|
4103
|
+
projectId: s.ref("projects").optional(),
|
|
4104
|
+
owner_id: s.string(),
|
|
4105
|
+
}),
|
|
4106
|
+
},
|
|
3109
4107
|
});
|
|
3110
4108
|
|
|
3111
4109
|
```
|
|
3112
4110
|
|
|
3113
|
-
|
|
3114
|
-
```sql
|
|
3115
|
-
-- Example: drop a column in the forward direction.
|
|
3116
|
-
ALTER TABLE todos DROP COLUMN legacy_priority;
|
|
4111
|
+
## Permissions
|
|
3117
4112
|
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
4113
|
+
To define row-level policies, add a sibling `permissions.ts` next to `schema.ts`.
|
|
4114
|
+
|
|
4115
|
+
```ts
|
|
4116
|
+
|
|
4117
|
+
policy.todos.allowRead.where({ owner_id: session.user_id });
|
|
4118
|
+
policy.todos.allowInsert.where({ owner_id: session.user_id });
|
|
4119
|
+
policy.todos.allowUpdate.where({ owner_id: session.user_id });
|
|
4120
|
+
policy.todos.allowDelete.where({ owner_id: session.user_id });
|
|
4121
|
+
});
|
|
3124
4122
|
|
|
3125
4123
|
```
|
|
3126
4124
|
|
|
@@ -3175,12 +4173,6 @@ Use `JAZZ_APP_ID` as the app ID configuration key across environments.
|
|
|
3175
4173
|
For auth modes and upgrade/link flow, see [Authentication](/docs/authentication).
|
|
3176
4174
|
|
|
3177
4175
|
```tsx
|
|
3178
|
-
const client = createJazzClient({
|
|
3179
|
-
appId: "todo-react-example",
|
|
3180
|
-
serverUrl: "http://127.0.0.1:1625",
|
|
3181
|
-
localAuthMode: "anonymous",
|
|
3182
|
-
// jwtToken: authToken, // Use this (instead of localAuthMode) for external auth.
|
|
3183
|
-
});
|
|
3184
4176
|
|
|
3185
4177
|
return (
|
|
3186
4178
|
|
|
@@ -3193,6 +4185,9 @@ const client = createJazzClient({
|
|
|
3193
4185
|
```vue
|
|
3194
4186
|
<script setup lang="ts">
|
|
3195
4187
|
|
|
4188
|
+
// Keep docs-only auth snippets in the compiled example app.
|
|
4189
|
+
void AuthSessionExamples;
|
|
4190
|
+
|
|
3196
4191
|
const client = createJazzClient({
|
|
3197
4192
|
appId: "todo-vue-example",
|
|
3198
4193
|
serverUrl: "http://127.0.0.1:1625",
|
|
@@ -3217,8 +4212,12 @@ const client = createJazzClient({
|
|
|
3217
4212
|
|
|
3218
4213
|
<script lang="ts">
|
|
3219
4214
|
import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
|
|
4215
|
+
import AuthSessionExamples from './AuthSessionExamples.svelte';
|
|
3220
4216
|
import TodoList from './TodoList.svelte';
|
|
3221
4217
|
|
|
4218
|
+
// Keep docs-only auth snippets in the compiled example app.
|
|
4219
|
+
void AuthSessionExamples;
|
|
4220
|
+
|
|
3222
4221
|
const client = createJazzClient({
|
|
3223
4222
|
appId: 'todo-svelte-example',
|
|
3224
4223
|
serverUrl: 'http://127.0.0.1:1625',
|
|
@@ -3257,11 +4256,12 @@ For React Native/Expo clients, make sure `serverUrl` points to a host your runti
|
|
|
3257
4256
|
const context = createJazzContext({
|
|
3258
4257
|
appId,
|
|
3259
4258
|
app: schemaApp,
|
|
3260
|
-
|
|
4259
|
+
permissions,
|
|
4260
|
+
driver: { type: "persistent", dataPath: dbPath },
|
|
3261
4261
|
env: "dev",
|
|
3262
4262
|
userBranch: "main",
|
|
3263
4263
|
});
|
|
3264
|
-
const
|
|
4264
|
+
const db = context.db();
|
|
3265
4265
|
```
|
|
3266
4266
|
|
|
3267
4267
|
```rs
|
|
@@ -3280,8 +4280,8 @@ For React Native/Expo clients, make sure `serverUrl` points to a host your runti
|
|
|
3280
4280
|
### Backend Identity Pattern
|
|
3281
4281
|
|
|
3282
4282
|
Client-side usage enforces permissions for the local user context.
|
|
3283
|
-
Backend usage
|
|
3284
|
-
In TypeScript backends,
|
|
4283
|
+
Backend usage can either use `context.db()` for the configured runtime identity or derive a requester-scoped `Db` directly from the request.
|
|
4284
|
+
In TypeScript backends, create the context once at startup with both `app` and `permissions`, then call `context.forRequest(req)` when you need a bearer-JWT-scoped high-level `Db`.
|
|
3285
4285
|
|
|
3286
4286
|
#### Per-request user-scoped client
|
|
3287
4287
|
|
|
@@ -3290,7 +4290,7 @@ In TypeScript backends, use `createJazzContext(...)` once at startup, then `cont
|
|
|
3290
4290
|
try {
|
|
3291
4291
|
const rows = await context
|
|
3292
4292
|
.forRequest(req, schemaApp)
|
|
3293
|
-
.
|
|
4293
|
+
.all(schemaApp.todos.where({ done: true }));
|
|
3294
4294
|
res.json(rows);
|
|
3295
4295
|
} catch {
|
|
3296
4296
|
sendQueryError(res);
|
|
@@ -3334,54 +4334,59 @@ DESCRIPTION:Insert, update, and delete APIs with local-first execution, framewor
|
|
|
3334
4334
|
`insert`, `update`, and `delete` apply immediately in the local runtime.
|
|
3335
4335
|
That keeps UI latency low because local state updates do not wait for any network hop.
|
|
3336
4336
|
|
|
3337
|
-
|
|
4337
|
+
`insertDurable`, `updateDurable`, and `deleteDurable` return promises that
|
|
4338
|
+
resolve when the requested durability tier is reached.
|
|
4339
|
+
|
|
4340
|
+
For browser `Blob`, `File`, or `ReadableStream` uploads, use `db.createFileFromBlob(...)`
|
|
4341
|
+
or `db.createFileFromStream(...)` and then store the returned file id on your own row. See
|
|
4342
|
+
[Files & Blobs](/docs/files-and-blobs).
|
|
3338
4343
|
|
|
3339
4344
|
## Mutating from Framework Context
|
|
3340
4345
|
|
|
3341
4346
|
Use the framework context binding to access the shared `Db` instance and execute mutations.
|
|
3342
4347
|
|
|
3343
4348
|
```tsx
|
|
3344
|
-
|
|
3345
|
-
|
|
4349
|
+
function addTodo(todoTitle: string) {
|
|
4350
|
+
db.insert(app.todos, { title: todoTitle, done: false });
|
|
3346
4351
|
}
|
|
3347
4352
|
|
|
3348
|
-
|
|
3349
|
-
|
|
4353
|
+
function toggleTodo(todo: { id: string; done: boolean }) {
|
|
4354
|
+
db.update(app.todos, todo.id, { done: !todo.done });
|
|
3350
4355
|
}
|
|
3351
4356
|
|
|
3352
|
-
|
|
3353
|
-
|
|
4357
|
+
function removeTodo(id: string) {
|
|
4358
|
+
db.delete(app.todos, id);
|
|
3354
4359
|
}
|
|
3355
4360
|
```
|
|
3356
4361
|
|
|
3357
4362
|
```ts
|
|
3358
|
-
|
|
3359
|
-
|
|
4363
|
+
function addTodo(todoTitle: string) {
|
|
4364
|
+
db.insert(app.todos, { title: todoTitle, done: false });
|
|
3360
4365
|
}
|
|
3361
4366
|
|
|
3362
|
-
|
|
3363
|
-
|
|
4367
|
+
function toggleTodo(todo: { id: string; done: boolean }) {
|
|
4368
|
+
db.update(app.todos, todo.id, { done: !todo.done });
|
|
3364
4369
|
}
|
|
3365
4370
|
|
|
3366
|
-
|
|
4371
|
+
function removeTodo(id: string) {
|
|
3367
4372
|
db.delete(app.todos, id);
|
|
3368
4373
|
}
|
|
3369
4374
|
```
|
|
3370
4375
|
|
|
3371
4376
|
```tsx
|
|
3372
|
-
const addTodo =
|
|
4377
|
+
const addTodo = () => {
|
|
3373
4378
|
const trimmed = title.trim();
|
|
3374
4379
|
if (!trimmed || !sessionUserId) return;
|
|
3375
|
-
|
|
4380
|
+
db.insert(app.todos, { title: trimmed, done: false, ownerId: sessionUserId });
|
|
3376
4381
|
setTitle("");
|
|
3377
4382
|
};
|
|
3378
4383
|
|
|
3379
|
-
const toggleTodo =
|
|
3380
|
-
|
|
4384
|
+
const toggleTodo = (todo: Todo) => {
|
|
4385
|
+
db.update(app.todos, todo.id, { done: !todo.done });
|
|
3381
4386
|
};
|
|
3382
4387
|
|
|
3383
|
-
const removeTodo =
|
|
3384
|
-
|
|
4388
|
+
const removeTodo = (id: string) => {
|
|
4389
|
+
db.delete(app.todos, id);
|
|
3385
4390
|
};
|
|
3386
4391
|
```
|
|
3387
4392
|
|
|
@@ -3393,12 +4398,12 @@ async function removeTodo(id: string) {
|
|
|
3393
4398
|
```
|
|
3394
4399
|
|
|
3395
4400
|
```svelte
|
|
3396
|
-
|
|
3397
|
-
|
|
4401
|
+
function toggleTodo(todo: { id: string; done: boolean }) {
|
|
4402
|
+
db.update(app.todos, todo.id, { done: !todo.done });
|
|
3398
4403
|
}
|
|
3399
4404
|
|
|
3400
|
-
|
|
3401
|
-
|
|
4405
|
+
function removeTodo(id: string) {
|
|
4406
|
+
db.delete(app.todos, id);
|
|
3402
4407
|
}
|
|
3403
4408
|
```
|
|
3404
4409
|
|
|
@@ -3406,14 +4411,14 @@ async function removeTodo(id: string) {
|
|
|
3406
4411
|
|
|
3407
4412
|
```ts
|
|
3408
4413
|
|
|
3409
|
-
|
|
4414
|
+
db.insert(app.todos, {
|
|
3410
4415
|
title: "Write docs",
|
|
3411
4416
|
done: false,
|
|
3412
|
-
|
|
3413
|
-
|
|
4417
|
+
ownerId: EXAMPLE_OWNER_ID,
|
|
4418
|
+
projectId: EXAMPLE_PROJECT_ID,
|
|
3414
4419
|
});
|
|
3415
|
-
|
|
3416
|
-
|
|
4420
|
+
db.update(app.todos, todoId, { done: true });
|
|
4421
|
+
db.delete(app.todos, todoId);
|
|
3417
4422
|
}
|
|
3418
4423
|
```
|
|
3419
4424
|
|
|
@@ -3427,7 +4432,7 @@ pub async fn write_todo_crud(client: &JazzClient, existing_id: ObjectId) -> jazz
|
|
|
3427
4432
|
Value::Null,
|
|
3428
4433
|
];
|
|
3429
4434
|
|
|
3430
|
-
let
|
|
4435
|
+
let _new_row = client.create("todos", values).await?;
|
|
3431
4436
|
client
|
|
3432
4437
|
.update(
|
|
3433
4438
|
existing_id,
|
|
@@ -3439,6 +4444,20 @@ pub async fn write_todo_crud(client: &JazzClient, existing_id: ObjectId) -> jazz
|
|
|
3439
4444
|
}
|
|
3440
4445
|
```
|
|
3441
4446
|
|
|
4447
|
+
### Partial Updates and Nullable Fields
|
|
4448
|
+
|
|
4449
|
+
`update(...)` and `updateDurable(...)` only modify the keys you pass.
|
|
4450
|
+
Omitted fields, and fields explicitly set to `undefined`, are left unchanged.
|
|
4451
|
+
To clear a nullable column in TypeScript, pass `null`.
|
|
4452
|
+
Required fields cannot be set to `null`.
|
|
4453
|
+
|
|
4454
|
+
```ts
|
|
4455
|
+
|
|
4456
|
+
db.update(app.todos, todoId, { ownerId: null }); // clears the nullable FK
|
|
4457
|
+
db.update(app.todos, todoId, { description: undefined }); // leaves the field unchanged
|
|
4458
|
+
}
|
|
4459
|
+
```
|
|
4460
|
+
|
|
3442
4461
|
## Reset Browser Storage
|
|
3443
4462
|
|
|
3444
4463
|
For browser clients, you can delete the local OPFS database for the current namespace:
|
|
@@ -3457,33 +4476,70 @@ Notes:
|
|
|
3457
4476
|
|
|
3458
4477
|
## Write Durability Tiers
|
|
3459
4478
|
|
|
3460
|
-
|
|
4479
|
+
The tier on a write controls _where the promise resolves_. Writes flow upward — from the
|
|
4480
|
+
local OPFS worker, to the nearest edge server, to the global core — and the tier determines
|
|
4481
|
+
how far the write must travel before the call returns.
|
|
3461
4482
|
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
4483
|
+
Data flows back down on demand. An edge server only fetches data from the global core when a
|
|
4484
|
+
client connected to it subscribes to that data. Edge nodes are demand-driven caches; the
|
|
4485
|
+
global core is the most durable tier and the point all others reconcile against. Two clients
|
|
4486
|
+
on different edge nodes may never share data unless one of them subscribes to data the other
|
|
4487
|
+
has written.
|
|
4488
|
+
|
|
4489
|
+
### Data consistency
|
|
3466
4490
|
|
|
3467
|
-
|
|
4491
|
+
Jazz uses last-write-wins CRDT merge strategies, so any tier may hold more recent
|
|
4492
|
+
unreconciled writes than the global core at a given moment. The global core represents the
|
|
4493
|
+
most durable snapshot, not necessarily the most current one. You can get closer to strongly
|
|
4494
|
+
consistent reads by using `global`-tier writes and reads exclusively, but under normal
|
|
4495
|
+
operation the system converges quickly.
|
|
3468
4496
|
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
- `
|
|
4497
|
+
### API Reference
|
|
4498
|
+
|
|
4499
|
+
- `insert(table, data)`
|
|
4500
|
+
- `update(table, id, data)`
|
|
4501
|
+
- `delete(table, id)`
|
|
4502
|
+
- `insertDurable(table, data, { tier })`
|
|
4503
|
+
- `updateDurable(table, id, data, options?)`
|
|
4504
|
+
- `deleteDurable(table, id, options?)`
|
|
4505
|
+
- `options.tier`: `"worker" | "edge" | "global"`
|
|
3472
4506
|
|
|
3473
4507
|
Defaults:
|
|
3474
4508
|
|
|
3475
4509
|
- Browser/client default tier is `worker`.
|
|
3476
4510
|
- Backend/server-connected default tier is `edge`.
|
|
3477
4511
|
|
|
3478
|
-
|
|
4512
|
+
### Which tier to use
|
|
3479
4513
|
|
|
3480
|
-
|
|
3481
|
-
|
|
4514
|
+
**`"worker"` (default)** — the write is persisted to the local OPFS database. The promise
|
|
4515
|
+
resolves as soon as the WASM worker confirms it. Other tabs on the same device see the change
|
|
4516
|
+
immediately; remote clients do not yet.
|
|
3482
4517
|
|
|
3483
|
-
|
|
4518
|
+
Use `worker` for:
|
|
3484
4519
|
|
|
3485
|
-
- Local-
|
|
3486
|
-
-
|
|
4520
|
+
- Local-only state (draft text, UI prefs)
|
|
4521
|
+
- Any write where you do not need remote confirmation
|
|
4522
|
+
|
|
4523
|
+
**`"edge"`** — the write has reached the nearest sync server and been acknowledged. Clients
|
|
4524
|
+
connected to the same edge server will see the write immediately via their subscriptions.
|
|
4525
|
+
Clients on other edge nodes will see it once their edge fetches the update from the global
|
|
4526
|
+
core, which happens when they subscribe to the relevant data.
|
|
4527
|
+
|
|
4528
|
+
Use `edge` for:
|
|
4529
|
+
|
|
4530
|
+
- Any write that other clients must receive (messages, shared state, user actions)
|
|
4531
|
+
- Writes that depend on remote acknowledgement before continuing
|
|
4532
|
+
|
|
4533
|
+
**`"global"`** — the write has propagated to the global core and is globally durable. Any
|
|
4534
|
+
client that subscribes to the relevant data will receive it, regardless of which edge node
|
|
4535
|
+
they are connected to. Highest latency; use only when edge-level acknowledgement is not
|
|
4536
|
+
sufficient.
|
|
4537
|
+
|
|
4538
|
+
Use `global` for:
|
|
4539
|
+
|
|
4540
|
+
- Seeding default data or one-time setup that must not duplicate across concurrent clients
|
|
4541
|
+
- Financial or audit records
|
|
4542
|
+
- Actions that must survive edge server failure
|
|
3487
4543
|
|
|
3488
4544
|
```ts
|
|
3489
4545
|
|
|
@@ -3492,8 +4548,8 @@ In practical terms:
|
|
|
3492
4548
|
{
|
|
3493
4549
|
title: "Write docs with durability tier",
|
|
3494
4550
|
done: false,
|
|
3495
|
-
|
|
3496
|
-
|
|
4551
|
+
ownerId: EXAMPLE_OWNER_ID,
|
|
4552
|
+
projectId: EXAMPLE_PROJECT_ID,
|
|
3497
4553
|
},
|
|
3498
4554
|
{ tier: "edge" },
|
|
3499
4555
|
);
|
|
@@ -3507,7 +4563,7 @@ In practical terms:
|
|
|
3507
4563
|
pub async fn write_todo_with_default_durability(
|
|
3508
4564
|
client: &JazzClient,
|
|
3509
4565
|
) -> jazz_tools::Result {
|
|
3510
|
-
let id = client
|
|
4566
|
+
let (id, _row_values) = client
|
|
3511
4567
|
.create(
|
|
3512
4568
|
"todos",
|
|
3513
4569
|
vec for re
|
|
|
3538
4620
|
## Backend Request Context Reminder
|
|
3539
4621
|
|
|
3540
4622
|
Backend writes should always run under a requester-scoped session.
|
|
3541
|
-
See [Setup](/docs/setup) for context setup details.
|
|
4623
|
+
See [Setup](/docs/setup) for context setup details.
|