Compare commits
39 Commits
docs/coval
...
cursor-doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a3928a637 | ||
|
|
3fa276c18d | ||
|
|
1928519d39 | ||
|
|
f4fa80b782 | ||
|
|
782df5d4b8 | ||
|
|
9db20ad630 | ||
|
|
3405d8f275 | ||
|
|
0a64dca0cd | ||
|
|
4d0b9b1bf1 | ||
|
|
403d61c8e8 | ||
|
|
0685c1cd5f | ||
|
|
834203f270 | ||
|
|
f01bc19257 | ||
|
|
6405a77aa2 | ||
|
|
ace6486075 | ||
|
|
23c3e5a125 | ||
|
|
e3d75e5c97 | ||
|
|
7838075bd6 | ||
|
|
dd792bf0ca | ||
|
|
233aae1deb | ||
|
|
153dc996a5 | ||
|
|
2b548c1758 | ||
|
|
167b588553 | ||
|
|
424930d06d | ||
|
|
d624a676d8 | ||
|
|
06f2af465d | ||
|
|
c7332f84b9 | ||
|
|
68fdbfbe94 | ||
|
|
e98d0e0c7f | ||
|
|
7efe89df31 | ||
|
|
2fb6428ea1 | ||
|
|
5a8f5d8bc2 | ||
|
|
2cc9daab37 | ||
|
|
e0276f42ee | ||
|
|
363be52022 | ||
|
|
1e87fc7772 | ||
|
|
03ec5d3ec8 | ||
|
|
5f272ff6ba | ||
|
|
07273f7ab8 |
5
.changeset/thin-months-explode.md
Normal file
5
.changeset/thin-months-explode.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cursor-docs": patch
|
||||
---
|
||||
|
||||
Added Cursor docs
|
||||
@@ -1,5 +1,29 @@
|
||||
# chat-rn-clerk
|
||||
|
||||
## 1.0.75
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3405d8f]
|
||||
- jazz-react-native@0.10.10
|
||||
- jazz-react-native-auth-clerk@0.10.10
|
||||
|
||||
## 1.0.74
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native-auth-clerk@0.10.9
|
||||
|
||||
## 1.0.73
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react-native@0.10.8
|
||||
- jazz-react-native-auth-clerk@0.10.8
|
||||
- jazz-react-native-media-images@0.10.8
|
||||
|
||||
## 1.0.72
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.72",
|
||||
"version": "1.0.75",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3405d8f]
|
||||
- jazz-react-native@0.10.10
|
||||
|
||||
## 1.0.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react-native@0.10.8
|
||||
|
||||
## 1.0.69
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.69",
|
||||
"version": "1.0.71",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.58
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [834203f]
|
||||
- jazz-browser@0.10.9
|
||||
- jazz-vue@0.10.9
|
||||
|
||||
## 0.0.57
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1e87fc7]
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-browser@0.10.8
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-vue@0.10.8
|
||||
|
||||
## 0.0.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.154
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser-media-images@0.10.9
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.153
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
- jazz-browser-media-images@0.10.8
|
||||
|
||||
## 0.0.152
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.152",
|
||||
"version": "0.0.154",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# minimal-auth-clerk
|
||||
|
||||
## 0.0.53
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.10.9
|
||||
- jazz-react-auth-clerk@0.10.9
|
||||
|
||||
## 0.0.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
- jazz-react-auth-clerk@0.10.8
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.51",
|
||||
"version": "0.0.53",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.4.1",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-react-auth-clerk": "workspace:0.10.7",
|
||||
"jazz-react-auth-clerk": "workspace:0.10.9",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.10.9
|
||||
|
||||
## 0.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-svelte@0.10.8
|
||||
|
||||
## 0.0.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.36",
|
||||
"version": "0.0.38",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# form
|
||||
|
||||
## 0.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser-media-images@0.10.9
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
- jazz-browser-media-images@0.10.8
|
||||
|
||||
## 0.0.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.0.47",
|
||||
"version": "0.0.49",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# image-upload
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser-media-images@0.10.9
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
- jazz-browser-media-images@0.10.8
|
||||
|
||||
## 0.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.49",
|
||||
"version": "0.0.51",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.109
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [153dc99]
|
||||
- cojson@0.10.8
|
||||
- cojson-transport-ws@0.10.8
|
||||
|
||||
## 0.0.108
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector-app",
|
||||
"private": true,
|
||||
"version": "0.0.108",
|
||||
"version": "0.0.109",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.10.7",
|
||||
"cojson-transport-ws": "workspace:0.10.7",
|
||||
"cojson": "workspace:0.10.8",
|
||||
"cojson-transport-ws": "workspace:0.10.8",
|
||||
"hash-slash": "workspace:0.2.2",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.75
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.74
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-inspector@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
|
||||
## 0.0.73
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.73",
|
||||
"version": "0.0.75",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -22,8 +22,8 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"jazz-react": "workspace:0.10.7",
|
||||
"jazz-tools": "workspace:0.10.7",
|
||||
"jazz-react": "workspace:0.10.9",
|
||||
"jazz-tools": "workspace:0.10.8",
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# organization
|
||||
|
||||
## 0.0.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.46
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
|
||||
## 0.0.45
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "organization",
|
||||
"private": true,
|
||||
"version": "0.0.45",
|
||||
"version": "0.0.47",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.42
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.10.9
|
||||
|
||||
## 0.0.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.10.8
|
||||
|
||||
## 0.0.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.40",
|
||||
"version": "0.0.42",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# minimal-auth-passkey
|
||||
|
||||
## 0.0.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
|
||||
## 0.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.50",
|
||||
"version": "0.0.52",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# passphrase
|
||||
|
||||
## 0.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
|
||||
## 0.0.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passphrase",
|
||||
"private": true,
|
||||
"version": "0.0.47",
|
||||
"version": "0.0.49",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.73
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.72
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
|
||||
## 0.0.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.71",
|
||||
"version": "0.0.73",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,8 +12,8 @@
|
||||
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:0.10.7",
|
||||
"jazz-tools": "workspace:0.10.7",
|
||||
"jazz-react": "workspace:0.10.9",
|
||||
"jazz-tools": "workspace:0.10.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.41.5",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.171
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser-media-images@0.10.9
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.170
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
- jazz-browser-media-images@0.10.8
|
||||
|
||||
## 0.0.169
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.169",
|
||||
"version": "0.0.171",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -19,9 +19,9 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.10.7",
|
||||
"jazz-react": "workspace:0.10.7",
|
||||
"jazz-tools": "workspace:0.10.7",
|
||||
"jazz-browser-media-images": "workspace:0.10.9",
|
||||
"jazz-react": "workspace:0.10.9",
|
||||
"jazz-tools": "workspace:0.10.8",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-run": "workspace:0.10.7",
|
||||
"jazz-run": "workspace:0.10.8",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.2",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# reactions
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser-media-images@0.10.9
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
- jazz-browser-media-images@0.10.8
|
||||
|
||||
## 0.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.49",
|
||||
"version": "0.0.51",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# todo-vue
|
||||
|
||||
## 0.0.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [834203f]
|
||||
- jazz-browser@0.10.9
|
||||
- jazz-vue@0.10.9
|
||||
|
||||
## 0.0.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1e87fc7]
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-browser@0.10.8
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-vue@0.10.8
|
||||
|
||||
## 0.0.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.54",
|
||||
"version": "0.0.56",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.170
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.169
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
|
||||
## 0.0.168
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.168",
|
||||
"version": "0.0.170",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.10.7",
|
||||
"jazz-tools": "workspace:0.10.7",
|
||||
"jazz-react": "workspace:0.10.9",
|
||||
"jazz-tools": "workspace:0.10.8",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# version-history
|
||||
|
||||
## 0.0.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.10.9
|
||||
|
||||
## 0.0.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react@0.10.8
|
||||
|
||||
## 0.0.46
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "version-history",
|
||||
"private": true,
|
||||
"version": "0.0.46",
|
||||
"version": "0.0.48",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
FolderArchiveIcon,
|
||||
GaugeIcon,
|
||||
GlobeIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
LinkIcon,
|
||||
LockKeyholeIcon,
|
||||
@@ -53,6 +54,7 @@ const icons = {
|
||||
encryption: LockKeyholeIcon,
|
||||
faceId: ScanFace,
|
||||
file: FileTextIcon,
|
||||
hash: HashIcon,
|
||||
help: MessageCircleQuestionIcon,
|
||||
image: ImageIcon,
|
||||
instant: GaugeIcon,
|
||||
|
||||
@@ -136,6 +136,7 @@ const config = {
|
||||
padding: "0.15rem 0.25rem",
|
||||
borderRadius: "2px",
|
||||
whiteSpace: "nowrap",
|
||||
fontWeight: 400,
|
||||
},
|
||||
p: {
|
||||
marginBottom: theme("spacing.3"),
|
||||
|
||||
@@ -665,7 +665,7 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {//
|
||||
|
||||
The loading-depth spec `{ issues: [{}] }` means "in `Project`, load `issues` and load each item in `issues` shallowly". (Since an `Issue` doesn't have any further references, "shallowly" actually means all its properties will be available).
|
||||
|
||||
- Now, we can get rid of a lot of conditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
|
||||
- Now, we can get rid of a lot of coniditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
|
||||
- This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
|
||||
|
||||
{/* TODO: explain about not loaded vs not set/defined and `_refs` basics */}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { CodeGroup } from '@/components/forMdx'
|
||||
|
||||
# Jazz Inspector
|
||||
|
||||
[Jazz Inspector](https://inspector.jazz.tools) is a tool to visually inspect a Jazz account or other CoValues.
|
||||
|
||||
For now, you can get your account credentials from the `jazz-logged-in-secret` local storage key from within your Jazz app.
|
||||
|
||||
[https://inspector.jazz.tools](https://inspector.jazz.tools)
|
||||
|
||||
## Exporting current account to Inspector from your app
|
||||
|
||||
In development mode, you can launch the Inspector from your Jazz app to inspect your account by pressing `Cmd+J`.
|
||||
|
||||
## Embedding the Inspector widget into your app
|
||||
|
||||
Alternatively, you can embed the Inspector directly into your app, so you don't need to open a separate window.
|
||||
|
||||
Install the package.
|
||||
|
||||
<CodeGroup>
|
||||
```sh
|
||||
npm install jazz-inspector
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Render the component within your `JazzProvider`.
|
||||
|
||||
<CodeGroup>
|
||||
```sh
|
||||
import { JazzInspector } from "jazz-inspector";
|
||||
|
||||
<JazzProvider> // old
|
||||
<JazzInspector />
|
||||
</JazzProvider> // old
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Check out the [music player app](https://github.com/garden-co/jazz/blob/main/examples/music-player/src/2_main.tsx) for a full example.
|
||||
@@ -4,31 +4,24 @@ export const metadata = { title: "Jazz 0.10.0 is out!" };
|
||||
|
||||
# Jazz 0.10.0 is out!
|
||||
|
||||
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
|
||||
11 February 2025
|
||||
</h2>
|
||||
For Jazz 0.10.0 we have been focusing on enhancing authentication to make it optional, more flexible and easier to use.
|
||||
|
||||
<div>
|
||||
For Jazz 0.10.0 we have been focusing on enhancing authentication to make it optional, more flexible and easier to use.
|
||||
The default is now anonymous auth, which means that you can build the functionality of your app first and figure out auth later. For users this means that they can start using your app right away on one device -- and once you integrate an auth method, users can sign up and their anonymous accounts are transparently upgraded to authenticated accounts that work across devices.
|
||||
|
||||
The default is now anonymous auth, which means that you can build the functionality of your app first and figure out auth later. For users this means that they can start using your app right away on one device -- and once you integrate an auth method, users can sign up and their anonymous accounts are transparently upgraded to authenticated accounts that work across devices.
|
||||
|
||||
There are also some other minor improvements that will make your Jazz experience even better!
|
||||
There are also some other minor improvements that will make your Jazz experience even better!
|
||||
|
||||
<h3>What's new?</h3>
|
||||
Here is what's changed in this release:
|
||||
- [New authentication flow](#new-authentication-flow): Now with anonymous auth, redesigned to make Jazz easier to start with and be more flexible.
|
||||
- [Local-only mode](#local-only-mode): Users can now explore your app in local-only mode before signing up.
|
||||
- [Improvements on the loading APIs](#improved-loading-api); `ensureLoaded` now always returns a value and `useCoState` now returns `null` if the value is not found.
|
||||
- [Jazz Workers on native WebSockets](#native-websocket-for-jazz-workers): Improves compatibility with a wider set of Javascript runtimes.
|
||||
- [Group inheritance with role mapping](#group-inheritance): Groups can now inherit members from other groups with a fixed role.
|
||||
- Support for Node 14 dropped on cojson.
|
||||
- Bugfix: `Group.removeMember` now returns a promise.
|
||||
- Now `cojson` and `jazz-tools` don't export directly the crypto providers anymore. Replace the import with `cojson/crypto/WasmCrypto` or `cojson/crypto/PureJSCrypto` depending on your use case.
|
||||
</div>
|
||||
## What's new?
|
||||
Here is what's changed in this release:
|
||||
- [New authentication flow](#new-authentication-flow): Now with anonymous auth, redesigned to make Jazz easier to start with and be more flexible.
|
||||
- [Local-only mode](#local-only-mode): Users can now explore your app in local-only mode before signing up.
|
||||
- [Improvements on the loading APIs](#improved-loading-api); `ensureLoaded` now always returns a value and `useCoState` now returns `null` if the value is not found.
|
||||
- [Jazz Workers on native WebSockets](#native-websocket-for-jazz-workers): Improves compatibility with a wider set of Javascript runtimes.
|
||||
- [Group inheritance with role mapping](#group-inheritance): Groups can now inherit members from other groups with a fixed role.
|
||||
- Support for Node 14 dropped on cojson.
|
||||
- Bugfix: `Group.removeMember` now returns a promise.
|
||||
- Now `cojson` and `jazz-tools` don't export directly the crypto providers anymore. Replace the import with `cojson/crypto/WasmCrypto` or `cojson/crypto/PureJSCrypto` depending on your use case.
|
||||
|
||||
<h3 id="new-authentication-flow">New authentication flow</h3>
|
||||
<div>
|
||||
## New authentication flow
|
||||
Up until now authentication has been the first part to figure out when building a Jazz app, and this was a stumbling block for many.
|
||||
|
||||
Now it is no longer required and setting up a Jazz app is as easy as writing this:
|
||||
@@ -191,8 +184,8 @@ export function AuthButton() {
|
||||
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={logOut}
|
||||
>
|
||||
Sign out
|
||||
@@ -224,8 +217,8 @@ export function AuthButton() {
|
||||
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
onPress={logOut}
|
||||
>
|
||||
Sign out
|
||||
@@ -267,16 +260,16 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
For the changes related to the specific auth providers see the updated [authentication docs](/docs/authentication/overview).
|
||||
</div>
|
||||
|
||||
<h3 id="local-only-mode">Local-only mode</h3>
|
||||
<div>
|
||||
For the changes related to the specific auth providers see the updated [authentication docs](/docs/authentication/overview).
|
||||
|
||||
## Local-only mode
|
||||
|
||||
If you are ok with data not being persisted on the sync server for anonymous users, you can now set your app to local-only depending on the user's authentication state.
|
||||
|
||||
With `sync.when` set to `"signedUp"` the app will work in local-only mode when the user is anonymous and unlock the multiplayer/multi-device features and cloud persistence when they sign up:
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```tsx
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
@@ -290,10 +283,9 @@ With `sync.when` set to `"signedUp"` the app will work in local-only mode when t
|
||||
</CodeGroup>
|
||||
|
||||
You can control when Jazz will sync by switching the `when` config to `"always"` or `"never"`.
|
||||
</div>
|
||||
|
||||
<h3 id="improved-loading-api">Improvements on the loading APIs</h3>
|
||||
<div>
|
||||
## Improvements on the loading APIs
|
||||
|
||||
Before 0.10.0 `ensureLoaded` was returning a nullable value forcing the Typescript code to always include null checks:
|
||||
<CodeGroup>
|
||||
```ts
|
||||
@@ -336,17 +328,17 @@ if (value === null) {
|
||||
return <div>Track not found</div>;
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</div>
|
||||
</CodeGroup>
|
||||
|
||||
<h3 id="native-websocket-for-jazz-workers">Jazz Workers on native WebSockets</h3>
|
||||
<div>
|
||||
We have removed the dependency on `ws` and switched to the native WebSocket API for Jazz Workers.
|
||||
## Jazz Workers on native WebSockets
|
||||
|
||||
This improves the compatibility with a wider set of Javascript runtimes adding drop-in support for Deno, Bun, Browsers and Cloudflare Durable Objects.
|
||||
We have removed the dependency on `ws` and switched to the native WebSocket API for Jazz Workers.
|
||||
|
||||
If you are using a Node.js version lower than 22 you will need to install the `ws` package and provide the WebSocket constructor:
|
||||
<CodeGroup>
|
||||
This improves the compatibility with a wider set of Javascript runtimes adding drop-in support for Deno, Bun, Browsers and Cloudflare Durable Objects.
|
||||
|
||||
If you are using a Node.js version lower than 22 you will need to install the `ws` package and provide the WebSocket constructor:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { WebSocket } from "ws";
|
||||
import { startWorker } from "jazz-nodejs";
|
||||
@@ -355,24 +347,21 @@ const { worker } = await startWorker({
|
||||
WebSocket,
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
</div>
|
||||
</CodeGroup>
|
||||
|
||||
<h3 id="group-inheritance">Group inheritance with role mapping</h3>
|
||||
<div>
|
||||
You can override the inherited role by passing a second argument to `extend`.
|
||||
## Group inheritance with role mapping
|
||||
You can override the inherited role by passing a second argument to `extend`.
|
||||
|
||||
This can be used to give users limited access to a child group:
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const organization = Group.create();
|
||||
const billing = Group.create();
|
||||
This can be used to give users limited access to a child group:
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const organization = Group.create();
|
||||
const billing = Group.create();
|
||||
|
||||
billing.extend(organization, "reader");
|
||||
```
|
||||
</CodeGroup>
|
||||
billing.extend(organization, "reader");
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
This way the members of the organization can only read the billing data, even if they are admins in the organization group.
|
||||
This way the members of the organization can only read the billing data, even if they are admins in the organization group.
|
||||
|
||||
More about the group inheritance can be found in the [dedicated docs page](/docs/groups/inheritance).
|
||||
</div>
|
||||
More about the group inheritance can be found in the [dedicated docs page](/docs/groups/inheritance).
|
||||
|
||||
@@ -1,545 +0,0 @@
|
||||
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
|
||||
|
||||
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
|
||||
# Upgrade to Jazz 0.9.0
|
||||
|
||||
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
|
||||
08 January 2025
|
||||
</h2>
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
<div>
|
||||
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
|
||||
replacing the `createJazzReactApp` API with top-level imports.
|
||||
|
||||
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
|
||||
</div>
|
||||
|
||||
<h3>New provider setup</h3>
|
||||
|
||||
<div>
|
||||
The `JazzProvider` is now imported from `jazz-react` instead of `createJazzReactApp`.
|
||||
|
||||
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
|
||||
we found that this approach made the Jazz setup awkward and confusing for some users.
|
||||
|
||||
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { JazzProvider, usePasskeyAuth, PasskeyAuthBasicUI } from "jazz-react";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
|
||||
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Replace Jazz.Provider with provider from jazz-react */}
|
||||
<JazzProvider
|
||||
auth={passkeyAuth} // old
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
|
||||
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // *add*
|
||||
>
|
||||
{children} // old
|
||||
</JazzProvider>
|
||||
<PasskeyAuthBasicUI state={passKeyState} /> // old
|
||||
</> // old
|
||||
);
|
||||
}
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react" { // *add*
|
||||
interface Register { // *add*
|
||||
Account: MyAppAccount; // *add*
|
||||
} // *add*
|
||||
} // *add*
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<h3>Top level imports for hooks</h3>
|
||||
|
||||
<div>
|
||||
All Jazz hooks are now available as top-level imports from the `jazz-react` package.
|
||||
|
||||
This change improves IDE intellisense support and simplifies imports:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Replace local imports with "jazz-react" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-react"; // *add*
|
||||
|
||||
export function Hello() {
|
||||
const { me } = useAccount();
|
||||
|
||||
return (
|
||||
<>
|
||||
Hello {me.profile?.name}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<h3>New testing utilities</h3>
|
||||
|
||||
<div>
|
||||
Removing `createJazzReactApp` also makes testing way easier!
|
||||
|
||||
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
|
||||
your components and hooks using `JazzTestProvider`:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { createJazzTestAccount, JazzTestProvider } from "jazz-react/testing";
|
||||
import { renderHook } from "@testing-library/react"; // old
|
||||
import { usePlaylist } from "./usePlaylist"; // old
|
||||
import { Playlist, MusicAccount } from "./schema"; // old
|
||||
|
||||
test("should load the playlist", async () => {
|
||||
// ✅ Create a test account with your schema
|
||||
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
|
||||
|
||||
// ✅ Set up test data
|
||||
const playlist = Playlist.create({
|
||||
name: "My playlist",
|
||||
}, account);
|
||||
|
||||
// ✅ Use JazzTestProvider in your tests
|
||||
const { result } = renderHook(() => usePlaylist(playlist.id), {
|
||||
wrapper: ({ children }) => (
|
||||
<JazzTestProvider account={account}>
|
||||
{children}
|
||||
</JazzTestProvider>
|
||||
),
|
||||
});
|
||||
|
||||
// The result is resolved synchronously, so you can assert the value immediately
|
||||
expect(result.current?.name).toBe("My playlist");
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="react-native">
|
||||
<div>
|
||||
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
|
||||
replacing the `createJazzRNApp` API with top-level imports.
|
||||
|
||||
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
|
||||
</div>
|
||||
|
||||
<h3>New provider setup</h3>
|
||||
|
||||
<div>
|
||||
The `JazzProvider` is now imported from `jazz-react-native` instead of `createJazzRNApp`.
|
||||
|
||||
While `createJazzRNApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
|
||||
we found that this approach made the Jazz setup awkward and confusing for some users.
|
||||
|
||||
So we decided to remove `createJazzRNApp` step and to provide the types through namespace declarations:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
|
||||
const [auth, state] = useDemoAuth(); // old
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Replace Jazz.Provider with provider from jazz-react */}
|
||||
<JazzProvider
|
||||
auth={auth} // old
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
|
||||
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */}
|
||||
>
|
||||
{children} // old
|
||||
</JazzProvider>
|
||||
<DemoAuthBasicUI appName="My App" state={state} /> // old
|
||||
</> // old
|
||||
);
|
||||
}
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react-native" {
|
||||
interface Register {
|
||||
Account: MyAppAccount;
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<h3>Top level imports for hooks</h3>
|
||||
|
||||
<div>
|
||||
All Jazz hooks are now available as top-level imports from the `jazz-react-native` package.
|
||||
|
||||
This change improves IDE intellisense support and simplifies imports:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Replace local imports with "jazz-react-native" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-react-native"; // *add*
|
||||
|
||||
export function Hello() {
|
||||
const { me } = useAccount();
|
||||
|
||||
return (
|
||||
<>
|
||||
Hello {me.profile?.name}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<h3>New testing utilities</h3>
|
||||
|
||||
<div>
|
||||
Removing `createJazzRNApp` also makes testing way easier!
|
||||
|
||||
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
|
||||
your components and hooks using `JazzTestProvider`:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { createJazzTestAccount, JazzTestProvider } from "jazz-react-native/testing";
|
||||
import { renderHook } from "@testing-library/react-native"; // old
|
||||
import { usePlaylist } from "./usePlaylist"; // old
|
||||
import { Playlist, MusicAccount } from "./schema"; // old
|
||||
|
||||
test("should load the playlist", async () => {
|
||||
// ✅ Create a test account with your schema
|
||||
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
|
||||
|
||||
// ✅ Set up test data
|
||||
const playlist = Playlist.create({
|
||||
name: "My playlist",
|
||||
}, account);
|
||||
|
||||
// ✅ Use JazzTestProvider in your tests
|
||||
const { result } = renderHook(() => usePlaylist(playlist.id), {
|
||||
wrapper: ({ children }) => (
|
||||
<JazzTestProvider account={account}>
|
||||
{children}
|
||||
</JazzTestProvider>
|
||||
),
|
||||
});
|
||||
|
||||
// The result is resolved synchronously, so you can assert the value immediately
|
||||
expect(result.current?.name).toBe("My playlist");
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="svelte">
|
||||
<div>
|
||||
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
|
||||
replacing the `createJazzApp` API with top-level imports.
|
||||
|
||||
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
|
||||
</div>
|
||||
|
||||
<h3>New provider setup</h3>
|
||||
|
||||
<div>
|
||||
The `JazzProvider` is now imported from `jazz-svelte` instead of `createJazzApp`.
|
||||
|
||||
While `createJazzApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
|
||||
we found that this approach made the Jazz setup awkward and confusing for some users.
|
||||
|
||||
So we decided to remove `createJazzApp` step and to provide the types through namespace declarations:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts" module>
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module 'jazz-svelte' {
|
||||
interface Register {
|
||||
Account: MyAccount;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Provider } from '$lib/jazz'; // *bin*
|
||||
import { JazzProvider } from 'jazz-svelte';
|
||||
|
||||
// Example configuration for authentication and peer connection
|
||||
let auth = null; // Replace with your auth implementation
|
||||
let peer = "wss://your-peer-endpoint";
|
||||
|
||||
// The custom Account schema is passed now as a prop
|
||||
let AccountSchema = MyAccount;
|
||||
</script>
|
||||
|
||||
<JazzProvider {auth} {peer} {AccountSchema}>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<h3>Top level imports for hooks</h3>
|
||||
|
||||
<div>
|
||||
All Jazz hooks are now available as top-level imports from the `jazz-svelte` package.
|
||||
|
||||
This change improves IDE intellisense support and simplifies imports:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```svelte
|
||||
|
||||
<script lang="ts">
|
||||
import { useAccount } from '$lib/jazz'; // *bin*
|
||||
import { useAccount } from 'jazz-svelte'; // *add*
|
||||
|
||||
const { me } = useAccount();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
Hello {me.profile?.name}
|
||||
</div>
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<h3>New testing utilities</h3>
|
||||
|
||||
<div>
|
||||
Removing `createJazzApp` also makes testing way easier!
|
||||
|
||||
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
|
||||
your components and hooks using `JazzTestProvider`:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
import { useCoState } from "jazz-svelte";
|
||||
import { createJazzTestAccount, JazzTestProvider } from "jazz-svelte/testing";
|
||||
import { render } from "@testing-library/svelte"; // old
|
||||
import { Playlist, MusicAccount } from "./schema"; // old
|
||||
|
||||
test("should load the playlist", async () => {
|
||||
// ✅ Create a test account with your schema
|
||||
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
|
||||
|
||||
// ✅ Set up test data
|
||||
const playlist = Playlist.create({
|
||||
name: "My playlist",
|
||||
}, account);
|
||||
|
||||
// ✅ Use createJazzTestContext in your tests
|
||||
render(PlaylistComponent, {
|
||||
context: createJazzTestContext({ account: options.account }),
|
||||
props: {
|
||||
id: playlist.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "My playlist" })).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="vue">
|
||||
<div>
|
||||
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
|
||||
replacing the `createJazzVueApp` API with top-level imports.
|
||||
|
||||
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
|
||||
</div>
|
||||
|
||||
<h3>New provider setup</h3>
|
||||
|
||||
<div>
|
||||
The `JazzProvider` is now imported from `jazz-vue` instead of `createJazzVueApp`.
|
||||
|
||||
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
|
||||
we found that this approach made the Jazz setup awkward and confusing for some users.
|
||||
|
||||
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```typescript
|
||||
import "./assets/main.css"; // old
|
||||
import { DemoAuthBasicUI, useDemoAuth, JazzProvider } from "jazz-vue";
|
||||
import { createApp, defineComponent, h } from "vue"; // old
|
||||
import App from "./App.vue"; // old
|
||||
import router from "./router"; // old
|
||||
import { ToDoAccount } from "./schema"; // old
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
const { JazzProvider } = Jazz; // *bin*
|
||||
|
||||
const RootComponent = defineComponent({ // old
|
||||
name: "RootComponent", // old
|
||||
setup() { // old
|
||||
const { authMethod, state } = useDemoAuth(); // old
|
||||
return () => [ // old
|
||||
h( // old
|
||||
JazzProvider, // old
|
||||
{ // old
|
||||
AccountSchema: ToDoAccount, // The custom Account schema is passed here now
|
||||
auth: authMethod.value, // old
|
||||
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co", // old
|
||||
}, // old
|
||||
{ // old
|
||||
default: () => h(App), // old
|
||||
}, // old
|
||||
), // old
|
||||
state.state !== "signedIn" && // old
|
||||
h(DemoAuthBasicUI, { // old
|
||||
appName: "Jazz Vue Todo", // old
|
||||
state, // old
|
||||
}), // old
|
||||
]; // old
|
||||
}, // old
|
||||
}); // old
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-vue" {
|
||||
interface Register {
|
||||
Account: ToDoAccount;
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(RootComponent); // old
|
||||
app.use(router); // old
|
||||
app.mount("#app"); // old
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<h3>Top level imports for hooks</h3>
|
||||
|
||||
<div>
|
||||
All Jazz hooks are now available as top-level imports from the `jazz-vue` package.
|
||||
|
||||
This change improves IDE intellisense support and simplifies imports:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```typescript
|
||||
<template>
|
||||
Hello {{ me.profile?.name }}
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Replace local imports with "jazz-vue" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-vue"; // *add*
|
||||
|
||||
const { me, logOut } = useAccount();
|
||||
</script>
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<h3>New testing utilities</h3>
|
||||
|
||||
<div>
|
||||
Removing `createJazzTestApp` also makes testing way easier!
|
||||
|
||||
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
|
||||
your components and hooks using `JazzTestProvider`:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { createJazzTestAccount, JazzTestProvider } from "jazz-vue/testing";
|
||||
import { createApp, defineComponent, h } from "vue";
|
||||
import { usePlaylist } from "./usePlaylist";
|
||||
import { Playlist, MusicAccount } from "./schema"; // old
|
||||
|
||||
// This can be reused on other tests!
|
||||
export const renderComposableWithJazz = <C extends (...args: any[]) => any>(
|
||||
composable: C,
|
||||
{ account }: { account: Account | { guest: AnonymousJazzAgent } },
|
||||
) => {
|
||||
let result;
|
||||
|
||||
const wrapper = defineComponent({
|
||||
setup() {
|
||||
result = composable();
|
||||
// suppress missing template warning
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ Use JazzTestProvider in your tests
|
||||
const app = createApp({
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
JazzTestProvider,
|
||||
{
|
||||
account,
|
||||
},
|
||||
{
|
||||
default: () => h(wrapper),
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
app.mount(document.createElement("div"));
|
||||
return [result, app] as [ReturnType<C>, ReturnType<typeof createApp>];
|
||||
};
|
||||
|
||||
test("should load the playlist", async () => {
|
||||
// ✅ Create a test account with your schema
|
||||
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
|
||||
|
||||
// ✅ Set up test data
|
||||
const playlist = Playlist.create({
|
||||
name: "My playlist",
|
||||
}, account);
|
||||
|
||||
// ✅ Set up test data
|
||||
const { result } = renderComposableWithJazz(() => usePlaylist(playlist.id), {
|
||||
account,
|
||||
});
|
||||
|
||||
// The result is resolved synchronously, so you can assert the value immediately
|
||||
expect(result?.name).toBe("My playlist");
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
@@ -0,0 +1,123 @@
|
||||
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
|
||||
|
||||
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
|
||||
# Upgrade to Jazz 0.9.0
|
||||
|
||||
|
||||
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
|
||||
replacing the `createJazzRNApp` API with top-level imports.
|
||||
|
||||
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
|
||||
|
||||
## New provider setup
|
||||
|
||||
The `JazzProvider` is now imported from `jazz-react-native` instead of `createJazzRNApp`.
|
||||
|
||||
While `createJazzRNApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
|
||||
we found that this approach made the Jazz setup awkward and confusing for some users.
|
||||
|
||||
So we decided to remove `createJazzRNApp` step and to provide the types through namespace declarations:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
|
||||
const [auth, state] = useDemoAuth(); // old
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Replace Jazz.Provider with provider from jazz-react */}
|
||||
<JazzProvider
|
||||
auth={auth} // old
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
|
||||
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */}
|
||||
>
|
||||
{children} // old
|
||||
</JazzProvider>
|
||||
<DemoAuthBasicUI appName="My App" state={state} /> // old
|
||||
</> // old
|
||||
);
|
||||
}
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react-native" {
|
||||
interface Register {
|
||||
Account: MyAppAccount;
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Top level imports for hooks
|
||||
|
||||
All Jazz hooks are now available as top-level imports from the `jazz-react-native` package.
|
||||
|
||||
This change improves IDE intellisense support and simplifies imports:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Replace local imports with "jazz-react-native" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-react-native"; // *add*
|
||||
|
||||
export function Hello() {
|
||||
const { me } = useAccount();
|
||||
|
||||
return (
|
||||
<>
|
||||
Hello {me.profile?.name}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## New testing utilities
|
||||
|
||||
<div>
|
||||
Removing `createJazzRNApp` also makes testing way easier!
|
||||
|
||||
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
|
||||
your components and hooks using `JazzTestProvider`:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { createJazzTestAccount, JazzTestProvider } from "jazz-react-native/testing";
|
||||
import { renderHook } from "@testing-library/react-native"; // old
|
||||
import { usePlaylist } from "./usePlaylist"; // old
|
||||
import { Playlist, MusicAccount } from "./schema"; // old
|
||||
|
||||
test("should load the playlist", async () => {
|
||||
// ✅ Create a test account with your schema
|
||||
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
|
||||
|
||||
// ✅ Set up test data
|
||||
const playlist = Playlist.create({
|
||||
name: "My playlist",
|
||||
}, account);
|
||||
|
||||
// ✅ Use JazzTestProvider in your tests
|
||||
const { result } = renderHook(() => usePlaylist(playlist.id), {
|
||||
wrapper: ({ children }) => (
|
||||
<JazzTestProvider account={account}>
|
||||
{children}
|
||||
</JazzTestProvider>
|
||||
),
|
||||
});
|
||||
|
||||
// The result is resolved synchronously, so you can assert the value immediately
|
||||
expect(result.current?.name).toBe("My playlist");
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -0,0 +1,120 @@
|
||||
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
|
||||
|
||||
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
|
||||
# Upgrade to Jazz 0.9.0
|
||||
|
||||
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
|
||||
replacing the `createJazzReactApp` API with top-level imports.
|
||||
|
||||
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
|
||||
|
||||
## New provider setup
|
||||
|
||||
The `JazzProvider` is now imported from `jazz-react` instead of `createJazzReactApp`.
|
||||
|
||||
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
|
||||
we found that this approach made the Jazz setup awkward and confusing for some users.
|
||||
|
||||
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { JazzProvider, usePasskeyAuth, PasskeyAuthBasicUI } from "jazz-react";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
|
||||
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Replace Jazz.Provider with provider from jazz-react */}
|
||||
<JazzProvider
|
||||
auth={passkeyAuth} // old
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
|
||||
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // *add*
|
||||
>
|
||||
{children} // old
|
||||
</JazzProvider>
|
||||
<PasskeyAuthBasicUI state={passKeyState} /> // old
|
||||
</> // old
|
||||
);
|
||||
}
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react" { // *add*
|
||||
interface Register { // *add*
|
||||
Account: MyAppAccount; // *add*
|
||||
} // *add*
|
||||
} // *add*
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Top level imports for hooks
|
||||
|
||||
All Jazz hooks are now available as top-level imports from the `jazz-react` package.
|
||||
|
||||
This change improves IDE intellisense support and simplifies imports:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Replace local imports with "jazz-react" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-react"; // *add*
|
||||
|
||||
export function Hello() {
|
||||
const { me } = useAccount();
|
||||
|
||||
return (
|
||||
<>
|
||||
Hello {me.profile?.name}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## New testing utilities
|
||||
|
||||
Removing `createJazzReactApp` also makes testing way easier!
|
||||
|
||||
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
|
||||
your components and hooks using `JazzTestProvider`:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { createJazzTestAccount, JazzTestProvider } from "jazz-react/testing";
|
||||
import { renderHook } from "@testing-library/react"; // old
|
||||
import { usePlaylist } from "./usePlaylist"; // old
|
||||
import { Playlist, MusicAccount } from "./schema"; // old
|
||||
|
||||
test("should load the playlist", async () => {
|
||||
// ✅ Create a test account with your schema
|
||||
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
|
||||
|
||||
// ✅ Set up test data
|
||||
const playlist = Playlist.create({
|
||||
name: "My playlist",
|
||||
}, account);
|
||||
|
||||
// ✅ Use JazzTestProvider in your tests
|
||||
const { result } = renderHook(() => usePlaylist(playlist.id), {
|
||||
wrapper: ({ children }) => (
|
||||
<JazzTestProvider account={account}>
|
||||
{children}
|
||||
</JazzTestProvider>
|
||||
),
|
||||
});
|
||||
|
||||
// The result is resolved synchronously, so you can assert the value immediately
|
||||
expect(result.current?.name).toBe("My playlist");
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -0,0 +1,113 @@
|
||||
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
|
||||
|
||||
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
|
||||
# Upgrade to Jazz 0.9.0
|
||||
|
||||
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
|
||||
replacing the `createJazzApp` API with top-level imports.
|
||||
|
||||
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
|
||||
|
||||
## New provider setup
|
||||
|
||||
The `JazzProvider` is now imported from `jazz-svelte` instead of `createJazzApp`.
|
||||
|
||||
While `createJazzApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
|
||||
we found that this approach made the Jazz setup awkward and confusing for some users.
|
||||
|
||||
So we decided to remove `createJazzApp` step and to provide the types through namespace declarations:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts" module>
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module 'jazz-svelte' {
|
||||
interface Register {
|
||||
Account: MyAccount;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Provider } from '$lib/jazz'; // *bin*
|
||||
import { JazzProvider } from 'jazz-svelte';
|
||||
|
||||
// Example configuration for authentication and peer connection
|
||||
let auth = null; // Replace with your auth implementation
|
||||
let peer = "wss://your-peer-endpoint";
|
||||
|
||||
// The custom Account schema is passed now as a prop
|
||||
let AccountSchema = MyAccount;
|
||||
</script>
|
||||
|
||||
<JazzProvider {auth} {peer} {AccountSchema}>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Top level imports for hooks
|
||||
|
||||
<div>
|
||||
All Jazz hooks are now available as top-level imports from the `jazz-svelte` package.
|
||||
|
||||
This change improves IDE intellisense support and simplifies imports:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```svelte
|
||||
|
||||
<script lang="ts">
|
||||
import { useAccount } from '$lib/jazz'; // *bin*
|
||||
import { useAccount } from 'jazz-svelte'; // *add*
|
||||
|
||||
const { me } = useAccount();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
Hello {me.profile?.name}
|
||||
</div>
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## New testing utilities
|
||||
|
||||
Removing `createJazzApp` also makes testing way easier!
|
||||
|
||||
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
|
||||
your components and hooks using `JazzTestProvider`:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
import { useCoState } from "jazz-svelte";
|
||||
import { createJazzTestAccount, JazzTestProvider } from "jazz-svelte/testing";
|
||||
import { render } from "@testing-library/svelte"; // old
|
||||
import { Playlist, MusicAccount } from "./schema"; // old
|
||||
|
||||
test("should load the playlist", async () => {
|
||||
// ✅ Create a test account with your schema
|
||||
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
|
||||
|
||||
// ✅ Set up test data
|
||||
const playlist = Playlist.create({
|
||||
name: "My playlist",
|
||||
}, account);
|
||||
|
||||
// ✅ Use createJazzTestContext in your tests
|
||||
render(PlaylistComponent, {
|
||||
context: createJazzTestContext({ account: options.account }),
|
||||
props: {
|
||||
id: playlist.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "My playlist" })).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -0,0 +1,165 @@
|
||||
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
|
||||
|
||||
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
|
||||
# Upgrade to Jazz 0.9.0
|
||||
|
||||
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
|
||||
replacing the `createJazzVueApp` API with top-level imports.
|
||||
|
||||
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
|
||||
|
||||
## New provider setup
|
||||
|
||||
The `JazzProvider` is now imported from `jazz-vue` instead of `createJazzVueApp`.
|
||||
|
||||
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
|
||||
we found that this approach made the Jazz setup awkward and confusing for some users.
|
||||
|
||||
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```typescript
|
||||
import "./assets/main.css"; // old
|
||||
import { DemoAuthBasicUI, useDemoAuth, JazzProvider } from "jazz-vue";
|
||||
import { createApp, defineComponent, h } from "vue"; // old
|
||||
import App from "./App.vue"; // old
|
||||
import router from "./router"; // old
|
||||
import { ToDoAccount } from "./schema"; // old
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
const { JazzProvider } = Jazz; // *bin*
|
||||
|
||||
const RootComponent = defineComponent({ // old
|
||||
name: "RootComponent", // old
|
||||
setup() { // old
|
||||
const { authMethod, state } = useDemoAuth(); // old
|
||||
return () => [ // old
|
||||
h( // old
|
||||
JazzProvider, // old
|
||||
{ // old
|
||||
AccountSchema: ToDoAccount, // The custom Account schema is passed here now
|
||||
auth: authMethod.value, // old
|
||||
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co", // old
|
||||
}, // old
|
||||
{ // old
|
||||
default: () => h(App), // old
|
||||
}, // old
|
||||
), // old
|
||||
state.state !== "signedIn" && // old
|
||||
h(DemoAuthBasicUI, { // old
|
||||
appName: "Jazz Vue Todo", // old
|
||||
state, // old
|
||||
}), // old
|
||||
]; // old
|
||||
}, // old
|
||||
}); // old
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-vue" {
|
||||
interface Register {
|
||||
Account: ToDoAccount;
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(RootComponent); // old
|
||||
app.use(router); // old
|
||||
app.mount("#app"); // old
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Top level imports for hooks
|
||||
|
||||
All Jazz hooks are now available as top-level imports from the `jazz-vue` package.
|
||||
|
||||
This change improves IDE intellisense support and simplifies imports:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```typescript
|
||||
<template>
|
||||
Hello {{ me.profile?.name }}
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Replace local imports with "jazz-vue" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-vue"; // *add*
|
||||
|
||||
const { me, logOut } = useAccount();
|
||||
</script>
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## New testing utilities
|
||||
|
||||
Removing `createJazzTestApp` also makes testing way easier!
|
||||
|
||||
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
|
||||
your components and hooks using `JazzTestProvider`:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { createJazzTestAccount, JazzTestProvider } from "jazz-vue/testing";
|
||||
import { createApp, defineComponent, h } from "vue";
|
||||
import { usePlaylist } from "./usePlaylist";
|
||||
import { Playlist, MusicAccount } from "./schema"; // old
|
||||
|
||||
// This can be reused on other tests!
|
||||
export const renderComposableWithJazz = <C extends (...args: any[]) => any>(
|
||||
composable: C,
|
||||
{ account }: { account: Account | { guest: AnonymousJazzAgent } },
|
||||
) => {
|
||||
let result;
|
||||
|
||||
const wrapper = defineComponent({
|
||||
setup() {
|
||||
result = composable();
|
||||
// suppress missing template warning
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ Use JazzTestProvider in your tests
|
||||
const app = createApp({
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
JazzTestProvider,
|
||||
{
|
||||
account,
|
||||
},
|
||||
{
|
||||
default: () => h(wrapper),
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
app.mount(document.createElement("div"));
|
||||
return [result, app] as [ReturnType<C>, ReturnType<typeof createApp>];
|
||||
};
|
||||
|
||||
test("should load the playlist", async () => {
|
||||
// ✅ Create a test account with your schema
|
||||
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
|
||||
|
||||
// ✅ Set up test data
|
||||
const playlist = Playlist.create({
|
||||
name: "My playlist",
|
||||
}, account);
|
||||
|
||||
// ✅ Set up test data
|
||||
const { result } = renderComposableWithJazz(() => usePlaylist(playlist.id), {
|
||||
account,
|
||||
});
|
||||
|
||||
// The result is resolved synchronously, so you can assert the value immediately
|
||||
expect(result?.name).toBe("My playlist");
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -4,15 +4,9 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
|
||||
# Jazz 0.9.8 - Without me!
|
||||
|
||||
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
|
||||
14 January 2025
|
||||
</h2>
|
||||
We have simplified the API to make the "me" value always optional!
|
||||
|
||||
<div>
|
||||
We have simplified the API to make the "me" value always optional!
|
||||
|
||||
This removes the need of using `useAccount` like the 90% of the time!
|
||||
</div>
|
||||
This removes the need of using `useAccount` like the 90% of the time!
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
@@ -20,6 +14,7 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue.tsx";
|
||||
|
||||
function App() {
|
||||
const [issue, setIssue] = useState<Issue>();
|
||||
|
||||
@@ -43,9 +38,7 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<div>
|
||||
This also applies to the load API:
|
||||
</div>
|
||||
This also applies to the load API:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
@@ -54,9 +47,7 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<div>
|
||||
And `Group.create`:
|
||||
</div>
|
||||
And `Group.create`:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
@@ -67,8 +58,6 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<div>
|
||||
Everything is backward compatible, so no upgrade steps are required.
|
||||
|
||||
With this Jazz API becomes way more lean and more is coming!
|
||||
</div>
|
||||
|
||||
@@ -4,46 +4,34 @@ export const metadata = { title: "Enable local persistence" };
|
||||
|
||||
# Enable local persistence
|
||||
|
||||
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
|
||||
10 January 2025
|
||||
</h2>
|
||||
Version 0.9.2 introduces local persistence for React Native apps using SQLite.
|
||||
|
||||
<ContentByFramework framework="react-native">
|
||||
<div>
|
||||
Version 0.9.2 introduces local persistence for React Native apps using SQLite.
|
||||
If you are upgrading from a version before 0.9.2, you need to enable local persistence by following the steps below.
|
||||
|
||||
If you are upgrading from a version before 0.9.2, you need to enable local persistence by following the steps below.
|
||||
Local persistence will become the default in 0.10.0.
|
||||
|
||||
Local persistence will become the default in 0.10.0.
|
||||
</div>
|
||||
## Add the required dependencies
|
||||
|
||||
<h3>Add the required dependencies</h3>
|
||||
As SQLite package we now use `@op-engineering/op-sqlite`.
|
||||
|
||||
<div>
|
||||
As SQLite package we now use `@op-engineering/op-sqlite`.
|
||||
To get local persistence working, you need to add `@op-engineering/op-sqlite` as a direct dependency to your project and run `npx pod-install`.
|
||||
|
||||
To get local persistence working, you need to add `@op-engineering/op-sqlite` as a direct dependency to your project and run `npx pod-install`.
|
||||
</div>
|
||||
## Update your JazzProvider to enable local persistence
|
||||
|
||||
<h3>Update your JazzProvider to enable local persistence</h3>
|
||||
Local persistence is now disabled by default.
|
||||
|
||||
<div>
|
||||
Local persistence is now disabled by default.
|
||||
To enable it, you need to pass the `storage` option to the `JazzProvider` component:
|
||||
|
||||
To enable it, you need to pass the `storage` option to the `JazzProvider` component:
|
||||
</div>
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
<JazzProvider
|
||||
auth={auto}
|
||||
storage="sqlite"
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com"
|
||||
AccountSchema={MyAppAccount}
|
||||
>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
<JazzProvider
|
||||
auth={auto}
|
||||
storage="sqlite"
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com"
|
||||
AccountSchema={MyAppAccount}
|
||||
>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import { CodeGroup, ComingSoon, ContentByFramework } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "Creation & ownership" };
|
||||
|
||||
# Creation & ownership
|
||||
|
||||
|
||||
CoValues are inherently collaborative, meaning multiple users and devices can edit them at the same time.
|
||||
|
||||
Who gets to read or change each CoValue is controlled by its owner — either an individual `Account` or a shared `Group`. This foundation of ownership is what enables Jazz applications to support real-time collaboration while maintaining proper access control.
|
||||
|
||||
|
||||
## Creating CoValues
|
||||
|
||||
To define the structure of your CoValue, you start with a schema. Here's an example:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// schema.ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
status = co.literal("todo", "in progress", "completed");
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
From there, you can create a `Task` CoValue using the `create` method.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const task = Task.create({
|
||||
title: "Plant spring vegetables",
|
||||
status: "todo",
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
To learn more about defining schemas, different types of CoValues, and the primitive types you can use for the fields,
|
||||
see the [schemas documentation](/docs/schemas/covalues).
|
||||
|
||||
When you create a CoValue, you provide its initial data and optionally specify who owns it. TypeScript will help ensure that your data matches the schema.
|
||||
|
||||
Here's an example of a more detailed schema:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Creating a CoFeed for activity notifications
|
||||
class ActivityNotification extends CoMap {
|
||||
message = co.string;
|
||||
type = co.literal("info", "warning", "success");
|
||||
timestamp = co.Date;
|
||||
}
|
||||
|
||||
class ActivityFeed extends CoFeed.Of(co.ref(ActivityNotification)) {}
|
||||
|
||||
const feed = ActivityFeed.create();
|
||||
|
||||
// Adding an item to the feed
|
||||
feed.addItem(ActivityNotification.create({
|
||||
message: "New task created",
|
||||
type: "info",
|
||||
timestamp: new Date()
|
||||
}));
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Ownership & access control
|
||||
|
||||
Every CoValue needs an owner to control who can access it — either an `Account` or a `Group`.
|
||||
|
||||
In the following example, we create a `Task` that only your account `me`, can access.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const { me } = useAccount(); // *add*
|
||||
|
||||
const task = Task.create({
|
||||
title: "Plant spring vegetables such as peas and carrots",
|
||||
status: "todo",
|
||||
}, {
|
||||
owner: me // *add*
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
But the owner is usually a `Group`, since that lets you share with multiple people.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const group = Group.create(); // *add*
|
||||
|
||||
const task = Task.create({
|
||||
title: "Plant spring vegetables",
|
||||
status: "todo",
|
||||
}, {
|
||||
owner: group // *add*
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
These permission scopes let you implement common patterns like shared workspaces, personal data, or public resources.
|
||||
Learn more about [Groups for permissions](/docs/groups/intro).
|
||||
|
||||
### Groups & Roles
|
||||
|
||||
Groups have members with different roles that control what they can do.
|
||||
|
||||
- `admin`: Full control including managing members; can't be demoted by other admins
|
||||
- `writer`: Can write and read content
|
||||
- `reader`: Can only read content
|
||||
- `writerOnly`: Can only write to the CoValue, not read it
|
||||
|
||||
Here's an example of a gardening project for Spring Planting, where every member has a different role:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Create a group
|
||||
const gardenTeam = Group.create();
|
||||
|
||||
// Add garden members with different roles
|
||||
gardenTeam.addMember(coordinator, "admin"); // Garden coordinator manages everything
|
||||
gardenTeam.addMember(gardener, "writer"); // Gardeners can update tasks
|
||||
gardenTeam.addMember(visitor, "reader"); // Visitors can view progress
|
||||
|
||||
// Create a list of tasks with the same owner
|
||||
const taskList = ListOfTasks.create([]);
|
||||
|
||||
// Create a garden project with nested tasks, all with the same ownership
|
||||
const springProject = Project.create({
|
||||
name: "Spring Planting",
|
||||
tasks: taskList
|
||||
});
|
||||
|
||||
// Add tasks to the list
|
||||
taskList.push(Task.create({
|
||||
title: "Start tomato seedlings",
|
||||
status: "todo"
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For more information on groups and roles, see the [Groups](/docs/groups/intro) documentation.
|
||||
@@ -1,269 +0,0 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "Metadata & time-travel" };
|
||||
|
||||
# Metadata & time-travel
|
||||
|
||||
One of Jazz's most powerful features is that every CoValue automatically tracks its complete edit history. This means you can see who changed what and when, examine the state of your data at any point in time, and build features like audit logs, activity feeds, and undo/redo functionality. This page explores how to access and work with the rich metadata that comes with every CoValue.
|
||||
|
||||
## Understanding edit history
|
||||
|
||||
Every CoValue in Jazz maintains a full history of all changes made to it. This edit history is accessible through two main APIs:
|
||||
|
||||
`CoValue._edits` provides a structured, field-by-field view of a CoValue's edit history. It organizes edits by property name and makes them easily accessible. For each field:
|
||||
- `_edits.fieldName` gives you the most recent edit
|
||||
- `_edits.fieldName.all` provides all historical edits as an array
|
||||
- `_edits.fieldName.madeAt` gives you the timestamp of the last edit
|
||||
- Each edit contains the value, who made the change, and when it happened
|
||||
|
||||
`CoValue._raw` gives you access to the internal state and lower-level operations on a CoValue. As this is an internal API, it should be used with caution. If you find yourself using `_raw`, consider letting us know so we can consider adding a public API for your use case.
|
||||
|
||||
## Working with edit history metadata
|
||||
|
||||
CoValues track who made each change and when. Every edit has metadata attached to it, including the author, timestamp, value, and transaction ID. This metadata enables you to build powerful audit and history features without having to implement your own tracking system.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
description = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
priority = co.literal("low", "medium", "high");
|
||||
subtasks = co.optional.ref(ListOfTasks);
|
||||
}
|
||||
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
const task = Task.create({
|
||||
title: "Plant spring vegetables",
|
||||
description: "Plant peas, carrots, and lettuce in the south garden bed",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
// Change the status
|
||||
task.status = "in-progress";
|
||||
|
||||
// Get the latest edit for a field
|
||||
console.log("Latest edit:", task._edits.status);
|
||||
// { value: "in-progress", by: Account, madeAt: Date, ... }
|
||||
|
||||
// Get when a field was last edited (timestamp)
|
||||
const lastEditTime = task._edits.status.madeAt;
|
||||
console.log(`Status was last changed at: ${lastEditTime?.toLocaleString()}`);
|
||||
|
||||
// Get the full edit history for a field
|
||||
for (const edit of task._edits.status.all) {
|
||||
console.log({
|
||||
author: edit.by, // Account that made the change
|
||||
timestamp: edit.madeAt, // When the change happened
|
||||
value: edit.value, // Value of the change
|
||||
});
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Common patterns
|
||||
|
||||
With knowledge of the edit history, you can build all sorts of useful features that enhance your application's user experience and administrative capabilities. Here are some common patterns that leverage CoValue metadata.
|
||||
|
||||
#### Audit log
|
||||
|
||||
Getting all the changes to a CoValue in order allows you to build an audit log. This is especially useful for tracking important changes in collaborative environments or for compliance purposes:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
|
||||
function getAuditLog(task: Task) {
|
||||
const changes = [];
|
||||
|
||||
for (const field of Object.keys(task)) {
|
||||
// Check if the field has edits to avoid accessing non-existent properties
|
||||
if (task._edits[field as keyof typeof task._edits]) {
|
||||
for (const edit of task._edits[field as keyof typeof task._edits].all) {
|
||||
changes.push({
|
||||
field,
|
||||
...edit,
|
||||
timestamp: edit.madeAt,
|
||||
at: edit.madeAt,
|
||||
by: edit.by,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp
|
||||
return changes.sort((a, b) => b.at.getTime() - a.at.getTime());
|
||||
}
|
||||
|
||||
// Example usage
|
||||
const auditLog = getAuditLog(task);
|
||||
auditLog.forEach((entry) => {
|
||||
console.log(
|
||||
`${entry.timestamp} - ${entry.field} changed to "${entry.value}" by ${entry.by?.id}`,
|
||||
);
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
#### Activity feeds
|
||||
|
||||
Activity feeds are a great way to see recent changes to a CoValue, helping users understand what's happening in a collaborative workspace. They can show who did what and when, creating transparency in team environments:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
function getRecentActivity(project: Project) {
|
||||
const activity = [];
|
||||
const hourAgo = new Date(Date.now() - 3600000);
|
||||
|
||||
for (const field of Object.keys(project)) {
|
||||
// Skip if the field doesn't have edits
|
||||
if (!project._edits[field as keyof typeof project._edits]) continue;
|
||||
|
||||
for (const edit of project._edits[field as keyof typeof project._edits].all) {
|
||||
if (edit.madeAt > hourAgo) {
|
||||
activity.push({
|
||||
field,
|
||||
value: edit.value,
|
||||
by: edit.by,
|
||||
at: edit.madeAt
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return activity.sort((a, b) => b.at.getTime() - a.at.getTime());
|
||||
}
|
||||
|
||||
// Example usage
|
||||
const recentActivity = getRecentActivity(gardenProject);
|
||||
console.log("Recent Garden Activity:");
|
||||
recentActivity.forEach(activity => {
|
||||
console.log(`${activity.at.toLocaleString()} - ${activity.field} updated by ${activity.by?.id}`);
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Edit history & time travel
|
||||
|
||||
CoValues track their entire history of changes, creating a timeline you can explore. You can see who changed what and when, or even view past states of the data. This capability enables powerful debugging tools and user-facing features like history browsing and restoration of previous versions:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
description = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
priority = co.literal("low", "medium", "high");
|
||||
}
|
||||
|
||||
// Create a new task
|
||||
const task = Task.create({
|
||||
title: "Plant spring vegetables",
|
||||
description: "Plant peas, carrots, and lettuce in the south garden bed",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
// Make some changes
|
||||
task.status = "in-progress";
|
||||
task.priority = "high";
|
||||
|
||||
// See all edits for a field
|
||||
for (const edit of task._edits.status.all) {
|
||||
console.log(
|
||||
`${edit.madeAt.toISOString()}: Status changed to "${edit.value}" by ${edit.by?.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the initial value
|
||||
const initialStatus = task._edits.status.all[0]?.value;
|
||||
console.log(`Original status: ${initialStatus}`);
|
||||
|
||||
// Get a specific edit by index
|
||||
const previousEdit = task._edits.status.all[1]; // Second edit
|
||||
console.log(`Previous status: ${previousEdit?.value}`);
|
||||
|
||||
// Check who made the most recent change
|
||||
const latestEdit = task._edits.status;
|
||||
console.log(`Latest change made by: ${latestEdit?.by?.id}`);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Time travel
|
||||
|
||||
The ability to view a CoValue as it existed at any point in time is one of Jazz's most powerful features. Looking into the past can help you understand how things changed - perfect for audit logs, debugging, or showing user activity. You can reconstruct the exact state of any CoValue at any moment in its history:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
status = co.literal("planning", "active", "completed");
|
||||
lastUpdate = co.Date;
|
||||
}
|
||||
|
||||
// See when a project was started
|
||||
function findStatusChange(project: Project, targetStatus: string) {
|
||||
// Get all the edits for the status field
|
||||
const statusEdits = project._edits.status.all;
|
||||
|
||||
for (const edit of statusEdits) {
|
||||
if (edit.value === targetStatus) {
|
||||
console.log({
|
||||
changeTime: edit.madeAt,
|
||||
lastUpdate: project.lastUpdate,
|
||||
changedBy: edit.by,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage
|
||||
findStatusChange(gardenProject, "active");
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Common use cases
|
||||
|
||||
The time travel capabilities of CoValues enable several practical use cases that would otherwise require complex custom implementations. Here are some examples of how you can use time travel in your applications:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Track task progress over time
|
||||
function getTaskStatusHistory(task: Task, days: number = 7) {
|
||||
const statusHistory = [];
|
||||
const dayInMs = 86400000;
|
||||
|
||||
// Check every day for the past week
|
||||
for (let day = 0; day < days; day++) {
|
||||
const timePoint = new Date(Date.now() - day * dayInMs);
|
||||
// Using the internal _raw API to get state at a specific point in time
|
||||
const state = task._raw.atTime(timePoint);
|
||||
statusHistory.push({
|
||||
date: timePoint.toLocaleDateString(),
|
||||
status: state.status,
|
||||
priority: state.priority
|
||||
});
|
||||
}
|
||||
|
||||
return statusHistory;
|
||||
}
|
||||
|
||||
// Example usage
|
||||
const history = getTaskStatusHistory(plantingTask);
|
||||
history.forEach(entry => {
|
||||
console.log(`${entry.date}: Status was "${entry.status}" with ${entry.priority} priority`);
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Best practices
|
||||
|
||||
- Check field existence before accessing edits (`if (task._edits.fieldName)`)
|
||||
- Access the most recent edit directly with `_edits.fieldName` instead of using any `.latest` property
|
||||
- Cache historical queries if you're displaying them in UI
|
||||
- Be specific about time ranges you care about
|
||||
- Remember that accessing history requires loading the CoValue
|
||||
- Consider using timestamps from your data rather than scanning all edits
|
||||
|
||||
Time travel is great for understanding how you got here, but keep queries focused on the range of time that matters to your use case.
|
||||
@@ -1,272 +0,0 @@
|
||||
import { CodeGroup, ComingSoon, ContentByFramework } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "Reading from CoValues" };
|
||||
|
||||
# Reading from CoValues
|
||||
|
||||
Jazz lets you access your collaborative data with familiar JavaScript patterns while providing TypeScript type safety. Once you have a CoValue, you can read its values, traverse references, and iterate through collections using the same syntax you'd use with regular objects and arrays. This page covers how to read from different types of CoValues and handle loading states effectively.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
lead = co.optional.ref(TeamMember);
|
||||
status = co.literal("planning", "active", "completed");
|
||||
}
|
||||
|
||||
// Reading basic fields
|
||||
console.log(project.name); // "Spring Garden Planning"
|
||||
console.log(project.status); // "active"
|
||||
|
||||
// Reading from lists
|
||||
for (const task of project.tasks) {
|
||||
console.log(task.title); // "Plant tomato seedlings"
|
||||
}
|
||||
|
||||
// Checking if an optional field exists
|
||||
if (project.lead) {
|
||||
console.log(project.lead.name); // "Maria Chen"
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Different types of CoValues
|
||||
|
||||
Jazz provides several CoValue types to represent different kinds of data. Each type has its own access patterns, but they all maintain the familiar JavaScript syntax you already know.
|
||||
|
||||
### CoMaps
|
||||
`CoMap`s work like JavaScript objects, providing named properties you can access with dot notation. These are the most common CoValue type and form the foundation of most Jazz data models:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
role = co.string;
|
||||
active = co.boolean;
|
||||
}
|
||||
|
||||
console.log(member.name); // "Maria Chen"
|
||||
console.log(member.role); // "Garden Coordinator"
|
||||
console.log(member.active); // true
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### CoLists
|
||||
`CoList`s work like JavaScript arrays, supporting indexed access, iteration methods, and length properties. They're perfect for ordered collections of items where the order matters:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
// Access items by index
|
||||
console.log(tasks[0].title); // "Plant tomato seedlings"
|
||||
|
||||
// Use array methods
|
||||
tasks.forEach(task => {
|
||||
console.log(task.title); // "Plant tomato seedlings"
|
||||
});
|
||||
|
||||
// Get list length
|
||||
console.log(tasks.length); // 3
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### CoFeeds
|
||||
`CoFeed`s provide a specialized way to track data from different sessions (tabs, devices, app instances). They're ideal for activity logs, presence indicators, or other session-specific streams of information. Each account can have multiple sessions, and each session maintains its own append-only log.
|
||||
|
||||
## Type safety with CoValues
|
||||
|
||||
CoValues are fully typed in TypeScript, giving you the same autocomplete and error checking you'd expect from regular objects. This type safety helps catch errors at compile time rather than runtime, making your application more robust. Here's how the type system works with CoValues:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
memberCount = co.number;
|
||||
priority = co.literal("low", "medium", "high");
|
||||
lead = co.optional.ref(TeamMember);
|
||||
tasks = co.ref(ListOfTasks);
|
||||
}
|
||||
|
||||
// TypeScript knows exactly what fields exist
|
||||
const project = await Project.load(gardenProjectId);
|
||||
|
||||
project.name = "Community Garden"; // ✓ string
|
||||
project.memberCount = "few"; // ✗ Type error: expected number
|
||||
project.priority = "urgent"; // ✗ Type error: must be low/medium/high
|
||||
|
||||
// Optional fields are handled safely
|
||||
if (project.lead) {
|
||||
console.log(project.lead.name); // Type safe
|
||||
}
|
||||
|
||||
// Lists with specific item types
|
||||
project.tasks.forEach(task => {
|
||||
// TypeScript knows each task's structure
|
||||
console.log(`${task.title}: ${task.status}`); // "Plant herbs: in-progress"
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Loading states
|
||||
|
||||
When you load a CoValue, it might not be immediately available due to network latency or data size. Jazz provides patterns to handle these loading states gracefully, and TypeScript helps ensure you check for availability before accessing properties:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const project = await Project.load(gardenProjectId);
|
||||
|
||||
if (!project) {
|
||||
return "Data still loading";
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
And in React, `useCoState` provides a similar pattern to allow you to wait for a CoValue to be loaded before accessing it:
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// Type shows this might be `undefined` while loading
|
||||
const project = useCoState(Project, gardenProjectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return <div>Loading project data...</div>;
|
||||
}
|
||||
|
||||
// TypeScript now knows project exists and has tasks loaded
|
||||
return <div>{project.tasks.length}</div>;
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
### Accessing nested CoValues
|
||||
|
||||
Nested CoValues need special handling for loading and access. Since each reference might need to be loaded separately, you need patterns to manage these dependencies and handle loading states appropriately throughout your object graph.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
resources = co.optional.ref(ResourceList);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
subtasks = co.ref(ListOfSubtasks);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Loading
|
||||
|
||||
Loading nested data efficiently is important for performance. Jazz provides depth specifications to control exactly how much of your object graph is loaded, from shallow loading of just the top-level object to deep loading of complex nested structures:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Basic load - tasks won't be loaded yet
|
||||
const project = await Project.load(gardenProjectId);
|
||||
|
||||
// Load with nested data
|
||||
const projectWithTasks = await Project.load(gardenProjectId, { tasks: {} });
|
||||
|
||||
// Deep load pattern
|
||||
const fullyLoaded = await Project.load(gardenProjectId, {
|
||||
tasks: {
|
||||
subtasks: {}
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
More details on loading and subscribing to CoValues can be found in [Subscribing](/docs/using-covalues/subscribing-and-deep-loading).
|
||||
|
||||
### Handling loading states
|
||||
|
||||
Unloaded references return `undefined`. This means you need to check for undefined values before trying to access properties of nested CoValues.
|
||||
|
||||
For general JavaScript/TypeScript usage, here's a pattern that works across any context:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Generic pattern for handling nested data
|
||||
function processTaskData(project) {
|
||||
// Check if project and its tasks are loaded
|
||||
if (!project || !project.tasks) {
|
||||
return "Data still loading";
|
||||
}
|
||||
|
||||
// Safe to process tasks
|
||||
const completedTasks = project.tasks.filter(task =>
|
||||
task && task.status === "completed"
|
||||
);
|
||||
|
||||
// Check for subtasks before accessing them
|
||||
const subtaskCount = completedTasks.reduce((count, task) => {
|
||||
if (!(task && task.subtasks)) return count
|
||||
return count + task.subtasks.length;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
completedCount: completedTasks.length,
|
||||
subtaskCount: subtaskCount
|
||||
};
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
Handle these loading states in your components:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// React pattern for handling nested data
|
||||
function TaskList({ project }: { project: Project }) {
|
||||
if (!project.tasks) {
|
||||
return <div>Loading tasks...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{project.tasks.map(task => {
|
||||
// Handle potentially missing nested data
|
||||
if (!task.subtasks) {
|
||||
return <div key={task.id}>Loading subtasks...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={task.id}>
|
||||
{task.title}: {task.subtasks.length} subtasks
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
Note: We're working on making these patterns more explicit and robust. We'll provide clearer loading states and better error handling patterns. For now, be defensive with your checks for `undefined`.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Current safest pattern for deep access
|
||||
function getSubtasks(project: Project, taskTitle: string) {
|
||||
const task = project.tasks?.find(t => t.title === taskTitle);
|
||||
const subtasks = task?.subtasks;
|
||||
|
||||
if (!subtasks) {
|
||||
return null; // Could mean loading or error
|
||||
}
|
||||
|
||||
return subtasks.map(st => st.title);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Stay tuned for updates to this API - we're working on making these patterns more robust and explicit.
|
||||
@@ -1,340 +0,0 @@
|
||||
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "Subscriptions & Deep Loading" };
|
||||
|
||||
# Subscriptions & deep loading
|
||||
|
||||
When working with collaborative applications, you need to know when data changes and ensure you have all the necessary related data. Jazz provides powerful subscription and deep loading capabilities that make it easy to keep your UI in sync with the underlying data and efficiently load complex object graphs.
|
||||
|
||||
## Understanding subscriptions
|
||||
|
||||
Subscriptions in Jazz allow you to react to changes in CoValues. When a CoValue changes, all subscribers are notified with the updated value. This is essential for building reactive UIs that stay in sync with collaborative data.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
description = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
assignedTo = co.optional.string;
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
// Subscribe to a Task by ID
|
||||
const unsubscribe = Task.subscribe(taskId, { /* loading depth */ }, (updatedTask) => {
|
||||
console.log("Task updated:", updatedTask.title);
|
||||
console.log("New status:", updatedTask.status);
|
||||
});
|
||||
|
||||
// Later, when you're done:
|
||||
unsubscribe();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Static vs. Instance subscriptions
|
||||
|
||||
There are two main ways to subscribe to CoValues:
|
||||
|
||||
1. **Static Subscription** - When you have an ID but don't have the CoValue loaded yet:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Subscribe by ID (static method)
|
||||
const unsubscribe = Task.subscribe(taskId, { /* loading depth */ }, (task) => {
|
||||
if (task) {
|
||||
console.log("Task loaded/updated:", task.title);
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
2. **Instance Subscription** - When you already have a CoValue instance:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Subscribe to an instance (instance method)
|
||||
const task = Task.create({
|
||||
status: "todo",
|
||||
title: "Cut the grass",
|
||||
});
|
||||
if (task) {
|
||||
const unsubscribe = task.subscribe({ /* loading depth */ }, (updatedTask) => {
|
||||
console.log("Task updated:", updatedTask.title);
|
||||
});
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Deep loading
|
||||
|
||||
When working with related CoValues (like tasks in a project), you often need to load not just the top-level object but also its nested references. Jazz provides a flexible mechanism for specifying exactly how much of the object graph to load.
|
||||
|
||||
### Loading depth specifications
|
||||
|
||||
Loading depth specifications let you declare exactly which references to load and how deep to go:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
subtasks = co.ref(ListOfSubtasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
|
||||
// Load just the project, not its tasks
|
||||
const project = await Project.load(projectId, {});
|
||||
|
||||
// Load the project and its tasks (but not subtasks)
|
||||
const projectWithTasks = await Project.load(projectId, {
|
||||
tasks: {}
|
||||
});
|
||||
|
||||
// Load the project, its tasks, and their subtasks
|
||||
const projectDeep = await Project.load(projectId, {
|
||||
tasks: {
|
||||
subtasks: {}
|
||||
}
|
||||
});
|
||||
|
||||
// Load the project, its tasks, and task assignees
|
||||
const projectWithAssignees = await Project.load(projectId, {
|
||||
tasks: {
|
||||
assignee: {}
|
||||
}
|
||||
});
|
||||
|
||||
// Complex loading pattern: load project, tasks with their subtasks, and the project owner
|
||||
const fullyLoaded = await Project.load(projectId, {
|
||||
tasks: {
|
||||
subtasks: {}
|
||||
},
|
||||
owner: {}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The depth specification object mirrors the structure of your data model, making it intuitive to express which parts of the graph you want to load.
|
||||
|
||||
### Array Notation for Lists
|
||||
|
||||
For lists, you can use array notation to specify how to load the items:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Load project with all tasks but load each task shallowly
|
||||
const project = await Project.load(projectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
|
||||
// Load project with tasks and load subtasks for each task
|
||||
const project = await Project.load(projectId, {
|
||||
tasks: [{
|
||||
subtasks: [{}]
|
||||
}]
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Framework Integration
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
### React integration with useCoState
|
||||
|
||||
In React applications, the `useCoState` hook provides a convenient way to subscribe to CoValues and handle loading states:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
function GardenPlanner({ projectId }: { projectId: ID<Project> }) {
|
||||
// Subscribe to a project and its tasks
|
||||
const project = useCoState(Project, projectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
|
||||
// Handle loading state
|
||||
if (!project) {
|
||||
return <div>Loading garden project...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{project.name}</h1>
|
||||
<TaskList tasks={project.tasks} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskList({ tasks }: { tasks: Task[] }) {
|
||||
return (
|
||||
<ul>
|
||||
{tasks.map(task => (
|
||||
<li key={task.id}>
|
||||
<span>{task.title}</span>
|
||||
<span>{task.status}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The `useCoState` hook handles subscribing when the component mounts and unsubscribing when it unmounts, making it easy to keep your UI in sync with the underlying data.
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="vue">
|
||||
### Vue integration
|
||||
|
||||
In Vue applications, you can use the `useCoState` composable to subscribe to CoValues:
|
||||
|
||||
<CodeGroup>
|
||||
```vue
|
||||
<script setup>
|
||||
import { useCoState } from 'jazz-vue';
|
||||
|
||||
const props = defineProps({
|
||||
projectId: String
|
||||
});
|
||||
|
||||
// Subscribe to a project and its tasks
|
||||
const project = useCoState(Project, props.projectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="project">
|
||||
<h1>{{ project.name }}</h1>
|
||||
<ul>
|
||||
<li v-for="task in project.tasks" :key="task.id">
|
||||
{{ task.title }} - {{ task.status }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
Loading garden project...
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="svelte">
|
||||
### Svelte integration
|
||||
|
||||
In Svelte applications, you can use the `useCoState` function to subscribe to CoValues:
|
||||
|
||||
<CodeGroup>
|
||||
```svelte
|
||||
<script>
|
||||
import { useCoState } from 'jazz-svelte';
|
||||
|
||||
export let projectId;
|
||||
|
||||
// Subscribe to a project and its tasks
|
||||
const project = useCoState(Project, projectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $project}
|
||||
<h1>{$project.name}</h1>
|
||||
<ul>
|
||||
{#each $project.tasks as task (task.id)}
|
||||
<li>{task.title} - {task.status}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div>Loading garden project...</div>
|
||||
{/if}
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
## Ensuring data is loaded
|
||||
|
||||
Sometimes you need to make sure data is loaded before proceeding with an operation. The `ensureLoaded` method lets you guarantee that a CoValue and its referenced data are loaded to a specific depth:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
async function completeAllTasks(projectId: ID<Project>) {
|
||||
// Ensure the project and its tasks are loaded
|
||||
const project = await Project.load(projectId, {});
|
||||
if (!project) return;
|
||||
|
||||
const loadedProject = await project.ensureLoaded({
|
||||
tasks: [{}]
|
||||
});
|
||||
|
||||
// Now we can safely access and modify tasks
|
||||
loadedProject.tasks.forEach(task => {
|
||||
task.status = "completed";
|
||||
});
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Performance considerations
|
||||
|
||||
Loading depth is directly related to performance. Loading too much data can slow down your application, while loading too little can lead to "undefined" references. Here are some guidelines:
|
||||
|
||||
- **Load only what you need** for the current view or operation
|
||||
- **Preload data** that will be needed soon to improve perceived performance
|
||||
- Use **caching** to avoid reloading data that hasn't changed
|
||||
|
||||
{/* TODO: Add a note about supporting pagination */}
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Bad: Loading everything deeply
|
||||
const project = await Project.load(projectId, {
|
||||
tasks: [{
|
||||
subtasks: [{
|
||||
comments: [{}]
|
||||
}]
|
||||
}],
|
||||
members: [{}],
|
||||
resources: [{}]
|
||||
});
|
||||
|
||||
// Better: Loading only what's needed for the current view
|
||||
const project = await Project.load(projectId, {
|
||||
tasks: [{}] // Just load the tasks shallowly
|
||||
});
|
||||
|
||||
// Later, when a task is selected:
|
||||
const task = await Task.load(selectedTaskId, {
|
||||
subtasks: [{}] // Now load its subtasks
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Using a loading cache
|
||||
|
||||
By default, Jazz maintains a cache of loaded CoValues to avoid unnecessary network requests. This means that if you've already loaded a CoValue, subsequent load requests will use the cached version unless you explicitly request a refresh.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// First load: fetches from network or local storage
|
||||
const project = await Project.load(projectId, {});
|
||||
|
||||
// Later loads: uses cached version if available
|
||||
const sameProject = await Project.load(projectId, {});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Best practices
|
||||
|
||||
1. **Be explicit about loading depths**: Always specify exactly what you need
|
||||
2. **Clean up subscriptions**: Always store and call the unsubscribe function when you're done
|
||||
3. **Handle loading states**: Check for undefined/null before accessing properties
|
||||
4. **Use framework integrations**: They handle subscription lifecycle automatically
|
||||
5. **Balance depth and performance**: Load only what you need for the current view
|
||||
|
||||
By effectively using subscriptions and deep loading, you can build responsive, collaborative applications that handle complex data relationships while maintaining good performance.
|
||||
@@ -1,175 +0,0 @@
|
||||
export const metadata = { title: "Writing & deleting CoValues" };
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
# Writing & deleting CoValues
|
||||
|
||||
Collaborative applications need ways to update and remove data. Jazz makes this simple by treating CoValues like regular JavaScript objects while handling all the complexity of syncing changes in the background. This page covers how to modify CoValues, work with collections, handle concurrent edits, and properly remove data when needed.
|
||||
|
||||
## Writing to CoValues
|
||||
|
||||
Once you have a CoValue, modifying it is straightforward. You can update fields like regular JavaScript properties. Changes are applied locally first for immediate feedback, then synchronized to other users with access to the same CoValues. This approach provides a natural programming model while handling all the distributed systems complexity behind the scenes.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
assignee = co.optional.string;
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Update fields
|
||||
task.status = "in-progress"; // Direct assignment
|
||||
task.assignee = "Alex"; // Optional field
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Working with lists
|
||||
|
||||
CoLists support familiar array operations, making it easy to work with collections of data. You can add, remove, and modify items using the standard JavaScript array methods, while Jazz handles the collaborative aspects automatically. These operations work correctly even when multiple users are making changes simultaneously.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Add items
|
||||
project.tasks.push(Task.create({
|
||||
title: "Build raised beds",
|
||||
status: "todo"
|
||||
}));
|
||||
|
||||
// Remove items
|
||||
project.tasks.splice(2, 1); // Remove third task
|
||||
|
||||
// Update items
|
||||
project.tasks[0].status = "in-progress";
|
||||
|
||||
// Bulk updates
|
||||
project.tasks.forEach(task => {
|
||||
if (task.status === "todo") {
|
||||
task.status = "in-progress";
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Changes sync automatically to everyone with access. Any edits you make are immediately visible in your local view and propagate to other users as they sync.
|
||||
|
||||
## Concurrent edits
|
||||
|
||||
CoValues use [CRDTs](/docs/schemas/covalues#defining-schemas-covalues) to handle concurrent edits smoothly. In most cases, you don't need to think about conflicts - Jazz handles them automatically. This conflict resolution happens transparently, allowing multiple users to make changes simultaneously without disruption or data loss.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Dashboard extends CoMap {
|
||||
activeProjects = co.number;
|
||||
status = co.literal("active", "maintenance");
|
||||
notifications = co.ref(ListOfNotifications);
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Multiple users can edit simultaneously
|
||||
// Last-write-wins for simple fields
|
||||
dashboard.status = "maintenance"; // Local change is immediate
|
||||
dashboard.activeProjects = 5; // Syncs automatically
|
||||
|
||||
// Lists handle concurrent edits too
|
||||
dashboard.notifications.push(Notification.create({
|
||||
timestamp: new Date(),
|
||||
message: "System update scheduled"
|
||||
}));
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Deleting CoValues
|
||||
|
||||
There are a few ways to delete CoValues, from simple field removal to full cleanup. Jazz provides flexible options for removing data depending on your needs. You can remove references while keeping the underlying data, remove items from lists, or completely delete CoValues when they're no longer needed.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
tasks = co.ref(ListOfTasks);
|
||||
resources = co.optional.ref(ListOfResources);
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Remove a reference
|
||||
project.resources = null; // Removes the reference but resources still exist
|
||||
|
||||
// Remove from a list
|
||||
project.tasks.splice(2, 1); // Removes third team member from list
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Best practices
|
||||
|
||||
- Load everything you plan to delete
|
||||
- Check permissions before attempting deletes
|
||||
- Consider soft deletes for recoverable data
|
||||
|
||||
## Removing data in CoValues
|
||||
|
||||
You can delete fields from any `CoMap` to remove specific properties while keeping the CoValue itself. This is useful when you need to clear certain data without affecting the rest of your object structure. The deletion operations are also synchronized to all users with access.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
team = co.ref(ListOfMembers);
|
||||
budget = co.optional.ref(Budget);
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Delete fields from a regular CoMap
|
||||
delete project.budget; // Removes the budget reference
|
||||
|
||||
// Delete from a record-type CoMap
|
||||
class ProjectTags extends CoMap.Record(co.string) {}
|
||||
|
||||
const projectTags = ProjectTags.create({
|
||||
"priority-high": "High priority tasks",
|
||||
});
|
||||
|
||||
delete projectTags["priority-high"]; // Removes specific tag
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For `CoList`s, use array methods:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Remove from lists using splice
|
||||
project.team.splice(2, 1); // Removes third team member
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Restoring data
|
||||
|
||||
For data you might want to restore later, consider using status fields instead of permanent deletion. This "soft delete" pattern is common in applications where users might need to recover previously removed items. By using a boolean field to mark items as archived or deleted, you maintain the ability to restore them later.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
archived = co.boolean;
|
||||
}
|
||||
|
||||
// Mark as archived
|
||||
task.archived = true;
|
||||
|
||||
// Restore later
|
||||
task.archived = false; // Task is back in the active list!
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Removed data remains in the edit history. If you need to handle sensitive information, plan your data model accordingly.
|
||||
28
homepage/homepage/components/docs/DocHeading.tsx
Normal file
28
homepage/homepage/components/docs/DocHeading.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode, useRef } from "react";
|
||||
|
||||
interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
children?: ReactNode;
|
||||
}
|
||||
export const Heading = ({ children, tag: Tag, ...props }: HeadingProps) => {
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
return (
|
||||
<Tag {...props} className="group">
|
||||
<a
|
||||
href={`#${props.id}`}
|
||||
className="no-underline float-left absolute -ml-[1.25em] hidden sm:block opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label="Navigate to header"
|
||||
ref={linkRef}
|
||||
>
|
||||
<span aria-hidden="true">#</span>
|
||||
</a>
|
||||
|
||||
<span className="cursor-pointer" onClick={() => linkRef.current?.click()}>
|
||||
{children}
|
||||
</span>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
@@ -29,6 +29,11 @@ export const docNavigationItems = [
|
||||
href: "/docs/ai-tools",
|
||||
done: 100,
|
||||
},
|
||||
{
|
||||
name: "Inspector",
|
||||
href: "/docs/inspector",
|
||||
done: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -108,27 +113,27 @@ export const docNavigationItems = [
|
||||
{
|
||||
name: "Creation & ownership",
|
||||
href: "/docs/using-covalues/creation",
|
||||
done: 80,
|
||||
done: 0,
|
||||
},
|
||||
{
|
||||
name: "Reading",
|
||||
href: "/docs/using-covalues/reading",
|
||||
done: 80,
|
||||
done: 0,
|
||||
},
|
||||
{
|
||||
name: "Subscribing & deep loading",
|
||||
href: "/docs/using-covalues/subscription-and-loading",
|
||||
done: 80,
|
||||
done: 0,
|
||||
},
|
||||
{
|
||||
name: "Writing & deleting",
|
||||
href: "/docs/using-covalues/writing",
|
||||
done: 80,
|
||||
done: 0,
|
||||
},
|
||||
{
|
||||
name: "Metadata & time-travel",
|
||||
href: "/docs/using-covalues/metadata",
|
||||
done: 80,
|
||||
done: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { DocsLink } from "@/components/docs/DocsLink";
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
import { Heading } from "./components/docs/DocHeading";
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
a: (props) => <DocsLink {...props} />,
|
||||
h2: (props) => <Heading tag="h2" {...props} />,
|
||||
h3: (props) => <Heading tag="h3" {...props} />,
|
||||
h4: (props) => <Heading tag="h4" {...props} />,
|
||||
h5: (props) => <Heading tag="h5" {...props} />,
|
||||
h6: (props) => <Heading tag="h6" {...props} />,
|
||||
...components,
|
||||
CodeWithInterpolation: ({
|
||||
highlightedCode,
|
||||
}: { highlightedCode: string }) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
|
||||
},
|
||||
};
|
||||
} satisfies MDXComponents;
|
||||
}
|
||||
|
||||
export function InterpolateInCode(replace: { [key: string]: string }) {
|
||||
|
||||
@@ -37,7 +37,7 @@ const config = {
|
||||
function highlightPlugin() {
|
||||
return async function transformer(tree) {
|
||||
const highlighter = await getHighlighter({
|
||||
langs: ["typescript", "bash", "tsx", "json", "svelte", "vue"],
|
||||
langs: ["typescript", "bash", "tsx", "json", "svelte"],
|
||||
theme: "css-variables", // use css variables in shiki.css
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# cojson-storage-indexeddb
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [153dc99]
|
||||
- cojson@0.10.8
|
||||
- cojson-storage@0.10.8
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.10.7",
|
||||
"version": "0.10.8",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.8.66
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [153dc99]
|
||||
- cojson@0.10.8
|
||||
- cojson-storage@0.10.8
|
||||
|
||||
## 0.8.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cojson-storage-rn-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.8.65",
|
||||
"version": "0.8.66",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [153dc99]
|
||||
- cojson@0.10.8
|
||||
- cojson-storage@0.10.8
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.10.7",
|
||||
"version": "0.10.8",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cojson": "workspace:0.10.7",
|
||||
"cojson": "workspace:0.10.8",
|
||||
"cojson-storage": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# cojson-storage
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [153dc99]
|
||||
- cojson@0.10.8
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage",
|
||||
"version": "0.10.7",
|
||||
"version": "0.10.8",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# cojson-transport-nodejs-ws
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [153dc99]
|
||||
- cojson@0.10.8
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.10.7",
|
||||
"version": "0.10.8",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# cojson
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 153dc99: Catch errors on CoValueCore subscribers to avoid effects on the sync
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.10.7",
|
||||
"version": "0.10.8",
|
||||
"devDependencies": {
|
||||
"@opentelemetry/sdk-metrics": "^1.29.0",
|
||||
"typescript": "~5.6.2",
|
||||
|
||||
@@ -35,6 +35,7 @@ import { CoValueKnownState, NewContentMessage } from "./sync.js";
|
||||
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
|
||||
import { expectGroup } from "./typeUtils/expectGroup.js";
|
||||
import { isAccountID } from "./typeUtils/isAccountID.js";
|
||||
import { parseError } from "./utils.js";
|
||||
|
||||
/**
|
||||
In order to not block other concurrently syncing CoValues we introduce a maximum size of transactions,
|
||||
@@ -326,7 +327,14 @@ export class CoValueCore {
|
||||
if (notifyMode === "immediate") {
|
||||
const content = this.getCurrentContent();
|
||||
for (const listener of this.listeners) {
|
||||
listener(content);
|
||||
try {
|
||||
listener(content);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Error in listener for coValue " + this.id,
|
||||
parseError(e),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!this.nextDeferredNotify) {
|
||||
@@ -336,7 +344,14 @@ export class CoValueCore {
|
||||
this.deferredUpdates = 0;
|
||||
const content = this.getCurrentContent();
|
||||
for (const listener of this.listeners) {
|
||||
listener(content);
|
||||
try {
|
||||
listener(content);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Error in listener for coValue " + this.id,
|
||||
parseError(e),
|
||||
);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
}, 0);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { LocalNode } from "../localNode.js";
|
||||
import { Role } from "../permissions.js";
|
||||
import {
|
||||
createTestNode,
|
||||
createTwoConnectedNodes,
|
||||
loadCoValueOrFail,
|
||||
randomAnonymousAccountAndSessionID,
|
||||
} from "./testUtils.js";
|
||||
|
||||
@@ -221,3 +223,38 @@ test("creating a coValue with a group should't trigger automatically a content c
|
||||
|
||||
getCurrentContentSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("listeners are notified even if the previous listener threw an error", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
const coMap = group.createMap();
|
||||
|
||||
const spy1 = vi.fn();
|
||||
const spy2 = vi.fn();
|
||||
|
||||
coMap.subscribe(spy1);
|
||||
coMap.subscribe(spy2);
|
||||
|
||||
spy1.mockImplementation(() => {
|
||||
throw new Error("test");
|
||||
});
|
||||
|
||||
const errorLog = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
coMap.set("hello", "world");
|
||||
|
||||
expect(spy1).toHaveBeenCalledTimes(2);
|
||||
expect(spy2).toHaveBeenCalledTimes(2);
|
||||
expect(errorLog).toHaveBeenCalledTimes(1);
|
||||
|
||||
await coMap.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, coMap.id);
|
||||
|
||||
expect(mapOnNode2.get("hello")).toBe("world");
|
||||
|
||||
errorLog.mockRestore();
|
||||
});
|
||||
|
||||
9
packages/cojson/src/utils.ts
Normal file
9
packages/cojson/src/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function parseError(e: unknown): {
|
||||
message: string | null;
|
||||
stack: string | null;
|
||||
} {
|
||||
return {
|
||||
message: e instanceof Error ? e.message : null,
|
||||
stack: e instanceof Error ? (e.stack ?? null) : null,
|
||||
};
|
||||
}
|
||||
@@ -238,6 +238,36 @@ module.exports = withNativeWind(config, { input: "./src/global.css" });
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Clone cursor-docs
|
||||
const docsSpinner = ora({
|
||||
text: chalk.blue(`Adding cursor-docs...`),
|
||||
spinner: "dots",
|
||||
}).start();
|
||||
|
||||
try {
|
||||
// Create a temporary directory for cursor-docs
|
||||
const tempDocsDir = `${projectName}-cursor-docs-temp`;
|
||||
const emitter = degit("garden-co/jazz/packages/cursor-docs", {
|
||||
cache: false,
|
||||
force: true,
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
// Clone cursor-docs to temp directory
|
||||
await emitter.clone(tempDocsDir);
|
||||
|
||||
// Copy contents to project root
|
||||
fs.cpSync(tempDocsDir, projectName, { recursive: true });
|
||||
|
||||
// Clean up temp directory
|
||||
fs.rmSync(tempDocsDir, { recursive: true, force: true });
|
||||
|
||||
docsSpinner.succeed(chalk.green("Cursor docs added successfully"));
|
||||
} catch (error) {
|
||||
docsSpinner.fail(chalk.red("Failed to add cursor docs"));
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Final success message
|
||||
console.log("\n" + chalk.green.bold("✨ Project setup completed! ✨\n"));
|
||||
console.log(chalk.cyan("To get started:"));
|
||||
|
||||
28
packages/cursor-docs/.cursor/rules/jazz-rule.mdc
Normal file
28
packages/cursor-docs/.cursor/rules/jazz-rule.mdc
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
description: Creating Jazz Schema Rule
|
||||
globs: *.ts, *.tsx
|
||||
---
|
||||
|
||||
# Creating Jazz Schema Rule
|
||||
You are a helpful AI assistant specialized in software engineering, TypeScript, Jazz.
|
||||
Jazz is a TypeScript framework for building local-first apps.
|
||||
Users can ask your help with anything related to Jazz, including creating Jazz schemas.
|
||||
|
||||
If the user needs help with creating or refining a Jazz Schema, follow this agentic protocol:
|
||||
1) read the Jazz Docs [1_jazz_docs.md](mdc:packages/cursor-docs/docs/1_jazz_docs.md)
|
||||
2) read each example one by one:
|
||||
- [4_1_example_without_specs.md](mdc:docs/4_1_example_without_specs.md) for example app 1: A secure and organized password manager app that allows users to store, manage, and categorize their credentials in folders
|
||||
- [4_2_example_without_specs.md](mdc:docs/4_2_example_without_specs.md) for example app 2: A feature-rich music player app that allows users to manage playlists, store tracks, and visualize audio waveforms
|
||||
- @4_3_example_without_specs.md for example app 3: A social pet app where users can share pet photos, react with fun emojis, and organize posts in a collaborative feed
|
||||
- [4_4_example_without_specs.md](mdc:docs/4_4_example_without_specs.md) for example app 4: A bubble tea ordering app that lets users customize drinks with different tea bases, add-ons, and delivery preferences
|
||||
- [4_5_example_without_specs.md](mdc:docs/4_5_example_without_specs.md) for example app 5: An employee onboarding app that streamlines the hiring process through structured steps, including initial data collection, document uploads, and final approvals
|
||||
- [4_6_example_without_specs.md](mdc:docs/4_6_example_without_specs.md) for example app 6: A task management app that helps users organize their to-dos with categories, tags, due dates, and priority levels
|
||||
3) read the Jazz Schema template you have to follow [2_jazz_schema_template.md](mdc:docs/2_jazz_schema_template.md)
|
||||
4) read the rules for creating Jazz Schema [3_jazz_rules.md](mdc:docs/3_jazz_rules.md)
|
||||
|
||||
When processing files:
|
||||
1. MUST validate each file was read
|
||||
2. MUST process files in sequence
|
||||
3. MUST confirm completion before continuing
|
||||
|
||||
After you followed all four steps, continue with correctly creating the schema based on what you learned.
|
||||
171
packages/cursor-docs/.gitignore
vendored
Normal file
171
packages/cursor-docs/.gitignore
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
\*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
\*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
\*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
\*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
.cache/
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.\*
|
||||
|
||||
.DS_Store
|
||||
2
packages/cursor-docs/.npmignore
Normal file
2
packages/cursor-docs/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
coverage
|
||||
node_modules
|
||||
19
packages/cursor-docs/LICENSE.txt
Normal file
19
packages/cursor-docs/LICENSE.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2024, Garden Computing, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
944
packages/cursor-docs/docs/1_jazz_docs.md
Normal file
944
packages/cursor-docs/docs/1_jazz_docs.md
Normal file
@@ -0,0 +1,944 @@
|
||||
---
|
||||
|
||||
## **CoMap Overview**
|
||||
**CoMap** is a collaborative object mapping system from `jazz-tools`, mapping string keys to values.
|
||||
|
||||
### **1. Basic Definition**
|
||||
```typescript
|
||||
import { CoMap, co } from "jazz-tools";
|
||||
class Person extends CoMap {
|
||||
name = co.string;
|
||||
age = co.number;
|
||||
isActive = co.boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### **2. Field Types**
|
||||
- **Basic:** `co.string`, `co.number`, `co.boolean`
|
||||
- **Optional:** `co.optional.string`, `co.optional.number`
|
||||
- **Literals (Enums):** `co.literal("draft", "published", "archived")`
|
||||
- **Dates:** `co.Date`
|
||||
- **Custom Encoded:**
|
||||
```typescript
|
||||
customField = co.encoded({
|
||||
encode: (v: string) => v.toUpperCase(),
|
||||
decode: (v: unknown) => String(v).toLowerCase()
|
||||
});
|
||||
```
|
||||
|
||||
### **3. References to Other CoMaps**
|
||||
```typescript
|
||||
class Comment extends CoMap {
|
||||
text = co.string;
|
||||
createdAt = co.Date;
|
||||
}
|
||||
class Post extends CoMap {
|
||||
title = co.string;
|
||||
content = co.string;
|
||||
mainComment = co.ref(Comment);
|
||||
pinnedComment = co.optional.ref(Comment);
|
||||
}
|
||||
```
|
||||
|
||||
### **4. Lists with CoList**
|
||||
```typescript
|
||||
import { CoList, CoMap, co } from "jazz-tools";
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
completed = co.boolean;
|
||||
}
|
||||
class TaskList extends CoList.Of(co.ref(Task)) {}
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(TaskList);
|
||||
}
|
||||
```
|
||||
|
||||
### **5. Validation & Custom Methods**
|
||||
```typescript
|
||||
class DraftPost extends CoMap {
|
||||
title = co.optional.string;
|
||||
content = co.optional.string;
|
||||
validate() {
|
||||
const errors: string[] = [];
|
||||
if (!this.title) errors.push("Title is required");
|
||||
if (!this.content) errors.push("Content is required");
|
||||
return { errors };
|
||||
}
|
||||
get summary() { return this.content?.slice(0, 100) + "..."; }
|
||||
}
|
||||
```
|
||||
|
||||
### **Real-World Examples**
|
||||
- **Chat Schema**
|
||||
```typescript
|
||||
class Message extends CoMap { text = co.string; image = co.optional.ref(ImageDefinition); }
|
||||
class Chat extends CoList.Of(co.ref(Message)) {}
|
||||
```
|
||||
- **Organization Schema**
|
||||
```typescript
|
||||
class Project extends CoMap { name = co.string; }
|
||||
class ListOfProjects extends CoList.Of(co.ref(Project)) {}
|
||||
class Organization extends CoMap {
|
||||
name = co.string;
|
||||
projects = co.ref(ListOfProjects);
|
||||
}
|
||||
```
|
||||
- **Issue Tracking**
|
||||
```typescript
|
||||
class Issue extends CoMap {
|
||||
title = co.string;
|
||||
description = co.string;
|
||||
estimate = co.number;
|
||||
status? = co.literal("backlog", "in progress", "done");
|
||||
}
|
||||
class ListOfIssues extends CoList.Of(co.ref(Issue)) {}
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
issues = co.ref(ListOfIssues);
|
||||
}
|
||||
```
|
||||
|
||||
### **Testing Example**
|
||||
```typescript
|
||||
class TestMap extends CoMap {
|
||||
color = co.string;
|
||||
_height = co.number;
|
||||
birthday = co.Date;
|
||||
name? = co.string;
|
||||
nullable = co.optional.encoded<string | undefined>({
|
||||
encode: (v: string | undefined) => v || null,
|
||||
decode: (v: unknown) => (v as string) || undefined,
|
||||
});
|
||||
optionalDate = co.optional.encoded(Encoders.Date);
|
||||
get roughColor() { return this.color + "ish"; }
|
||||
}
|
||||
```
|
||||
|
||||
### **Key Takeaways**
|
||||
- Extend **CoMap** for schemas.
|
||||
- Use `co.ref()` for references, `co.optional` for optional fields.
|
||||
- Use `CoList.Of()` for collections.
|
||||
- Fields auto-sync across clients.
|
||||
- Add computed properties & validation methods.
|
||||
|
||||
---
|
||||
|
||||
## **CoList Overview**
|
||||
**CoList** is a collaborative array in `jazz-tools`.
|
||||
|
||||
### **1. Basic Definition**
|
||||
```typescript
|
||||
import { CoList, co } from "jazz-tools";
|
||||
class ColorList extends CoList.Of(co.string) {}
|
||||
class NumberList extends CoList.Of(co.number) {}
|
||||
class BooleanList extends CoList.Of(co.boolean) {}
|
||||
```
|
||||
|
||||
### **2. Lists of CoMaps**
|
||||
```typescript
|
||||
import { CoList, CoMap, co } from "jazz-tools";
|
||||
class Task extends CoMap { title = co.string; completed = co.boolean; }
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
```
|
||||
|
||||
### **3. CoList Operations**
|
||||
```typescript
|
||||
const taskList = ListOfTasks.create([], { owner: me });
|
||||
taskList.push(Task.create({ title: "New task", completed: false }, { owner: me }));
|
||||
const firstTask = taskList[0];
|
||||
taskList.filter(task => !task.completed);
|
||||
taskList.splice(1, 1);
|
||||
```
|
||||
|
||||
### **4. Nested Lists**
|
||||
```typescript
|
||||
class Comment extends CoMap { text = co.string; createdAt = co.Date; }
|
||||
class ListOfComments extends CoList.Of(co.ref(Comment)) {}
|
||||
class Post extends CoMap {
|
||||
title = co.string;
|
||||
content = co.string;
|
||||
comments = co.ref(ListOfComments);
|
||||
}
|
||||
class ListOfPosts extends CoList.Of(co.ref(Post)) {}
|
||||
```
|
||||
|
||||
### **Real-World Examples**
|
||||
- **Chat Schema**
|
||||
```typescript
|
||||
class Message extends CoMap { text = co.string; image = co.optional.ref(ImageDefinition); }
|
||||
class Chat extends CoList.Of(co.ref(Message)) {}
|
||||
```
|
||||
- **Todo App Schema**
|
||||
```typescript
|
||||
class Task extends CoMap { done = co.boolean; text = co.string; }
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
```
|
||||
- **Organization Schema**
|
||||
```typescript
|
||||
class Project extends CoMap { name = co.string; }
|
||||
class ListOfProjects extends CoList.Of(co.ref(Project)) {}
|
||||
class Organization extends CoMap {
|
||||
name = co.string;
|
||||
projects = co.ref(ListOfProjects);
|
||||
}
|
||||
```
|
||||
|
||||
### **5. Advanced Features**
|
||||
```typescript
|
||||
class TaskList extends CoList.Of(co.ref(Task)) {
|
||||
getCompletedTasks() { return this.filter(task => task.completed); }
|
||||
getPendingTasks() { return this.filter(task => !task.completed); }
|
||||
}
|
||||
```
|
||||
|
||||
### **Key Takeaways**
|
||||
- `CoList.Of()` for list definitions.
|
||||
- `co.ref()` for CoMap references.
|
||||
- Acts like arrays with real-time sync.
|
||||
- Supports custom methods & nested lists.
|
||||
|
||||
---
|
||||
|
||||
## **CoFeed Overview**
|
||||
**CoFeed** is an append-only event stream, ideal for time-ordered data.
|
||||
|
||||
### **1. Basic Definition**
|
||||
```typescript
|
||||
import { CoFeed, co } from "jazz-tools";
|
||||
class ActivityFeed extends CoFeed.Of(co.string) {}
|
||||
class MetricsFeed extends CoFeed.Of(co.number) {}
|
||||
```
|
||||
|
||||
### **2. Feeds with Complex Types**
|
||||
```typescript
|
||||
interface LogEvent {
|
||||
timestamp: number;
|
||||
level: "info" | "warn" | "error";
|
||||
message: string;
|
||||
}
|
||||
class LogFeed extends CoFeed.Of(co.json<LogEvent>()) {}
|
||||
```
|
||||
|
||||
### **3. Pet Reactions Example**
|
||||
```typescript
|
||||
export const ReactionTypes = ["aww","love","haha","wow","tiny","chonkers"] as const;
|
||||
export class PetReactions extends CoFeed.Of(co.json<ReactionType>()) {}
|
||||
```
|
||||
|
||||
### **4. Working with CoFeeds**
|
||||
```typescript
|
||||
const reactions = PetReactions.create({ owner: me });
|
||||
reactions.post("love");
|
||||
reactions.subscribe(feedId, me, {}, (feed) => console.log(feed.latest()));
|
||||
```
|
||||
|
||||
### **5. Common Use Cases**
|
||||
- **Activity Streams**
|
||||
```typescript
|
||||
class ActivityStream extends CoFeed.Of(co.json<{ type:"comment"|"like"|"share";userId:string;timestamp:number;}>) {}
|
||||
```
|
||||
- **Chat Messages**
|
||||
```typescript
|
||||
class ChatFeed extends CoFeed.Of(co.json<{ type:"message"|"join"|"leave";userId:string; content?:string;timestamp:number;}>) {}
|
||||
```
|
||||
- **Audit Logs**
|
||||
```typescript
|
||||
class AuditLog extends CoFeed.Of(co.json<{ action:string; user:string; details:Record<string,unknown>;timestamp:number;}>) {}
|
||||
```
|
||||
|
||||
### **6. Differences: CoFeed vs. CoList**
|
||||
| Feature | CoFeed (append-only) | CoList (mutable) |
|
||||
|----------|----------------------|------------------|
|
||||
| Order | Time-ordered | Arbitrary |
|
||||
| Use Case | Logs, streams | Collections |
|
||||
|
||||
### **Key Takeaways**
|
||||
- **CoFeed**: event logs, activity streams, append-only.
|
||||
- **CoList**: modifiable lists.
|
||||
- Real-time updates, easy to subscribe.
|
||||
|
||||
---
|
||||
|
||||
## **SchemaUnion Overview**
|
||||
**SchemaUnion** handles runtime-discriminated union types of `CoMap` instances.
|
||||
|
||||
### **1. Basic Definition**
|
||||
```typescript
|
||||
import { SchemaUnion, CoMap, co } from "jazz-tools";
|
||||
class BaseShape extends CoMap { type = co.string; }
|
||||
class Circle extends BaseShape { type = co.literal("circle"); radius = co.number; }
|
||||
class Rectangle extends BaseShape { type = co.literal("rectangle"); width = co.number; height = co.number; }
|
||||
const Shape = SchemaUnion.Of<BaseShape>((raw) => {
|
||||
switch (raw.get("type")) {
|
||||
case "circle": return Circle;
|
||||
case "rectangle": return Rectangle;
|
||||
default: throw new Error("Unknown shape");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### **2. Nested Discriminators**
|
||||
```typescript
|
||||
class BaseButton extends CoMap { type = co.literal("button"); variant = co.string; }
|
||||
class PrimaryButton extends BaseButton { variant = co.literal("primary"); label = co.string; size = co.literal("small","medium","large"); }
|
||||
class SecondaryButton extends BaseButton { variant = co.literal("secondary"); label = co.string; outline = co.boolean; }
|
||||
const Button = SchemaUnion.Of<BaseButton>((raw) => {
|
||||
switch (raw.get("variant")) {
|
||||
case "primary": return PrimaryButton;
|
||||
case "secondary": return SecondaryButton;
|
||||
default: throw new Error("Unknown variant");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### **3. Using SchemaUnion with CoLists**
|
||||
```typescript
|
||||
class BaseWidget extends CoMap { type = co.string; }
|
||||
class ButtonWidget extends BaseWidget { type = co.literal("button"); label = co.string; }
|
||||
class SliderWidget extends BaseWidget { type = co.literal("slider"); min = co.number; max = co.number; }
|
||||
const Widget = SchemaUnion.Of<BaseWidget>((raw) => {
|
||||
switch (raw.get("type")) {
|
||||
case "button": return ButtonWidget;
|
||||
case "slider": return SliderWidget;
|
||||
default: throw new Error("Unknown widget");
|
||||
}
|
||||
});
|
||||
class WidgetList extends CoList.Of(co.ref(Widget)) {}
|
||||
```
|
||||
|
||||
### **4. Working with SchemaUnion Instances**
|
||||
```typescript
|
||||
const button = ButtonWidget.create({ type:"button", label:"Click me" }, { owner: me });
|
||||
const widget = await loadCoValue(Widget, widgetId, me, {});
|
||||
if (widget instanceof ButtonWidget) console.log(widget.label);
|
||||
if (widget instanceof SliderWidget) console.log(widget.min, widget.max);
|
||||
```
|
||||
|
||||
### **5. Validation Example**
|
||||
```typescript
|
||||
class BaseFormField extends CoMap {
|
||||
type = co.string;
|
||||
label = co.string;
|
||||
required = co.boolean;
|
||||
}
|
||||
class TextField extends BaseFormField {
|
||||
type = co.literal("text");
|
||||
minLength = co.optional.number;
|
||||
maxLength = co.optional.number;
|
||||
validate(value: string){/* ... */}
|
||||
}
|
||||
class NumberField extends BaseFormField {
|
||||
type = co.literal("number");
|
||||
min = co.optional.number;
|
||||
max = co.optional.number;
|
||||
validate(value: number){/* ... */}
|
||||
}
|
||||
const FormField = SchemaUnion.Of<BaseFormField>((raw) => {
|
||||
switch (raw.get("type")) {
|
||||
case "text": return TextField;
|
||||
case "number": return NumberField;
|
||||
default: throw new Error("Unknown type");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### **Key Takeaways**
|
||||
- **SchemaUnion** = polymorphic CoMaps.
|
||||
- Use `co.literal()` for discriminators.
|
||||
- `instanceof` for type-narrowing.
|
||||
- Great for complex forms & dynamic components.
|
||||
|
||||
---
|
||||
|
||||
## **Groups, Accounts, Owners, Roles & Permissions in Jazz**
|
||||
|
||||
### **1. Ownership & Groups**
|
||||
Every `CoValue` has an owner (an `Account` or `Group`):
|
||||
```typescript
|
||||
import { Account, Group, CoMap, co } from "jazz-tools";
|
||||
const privateDoc = Document.create({ title:"Private" }, { owner: me });
|
||||
const group = Group.create({ owner: me });
|
||||
const sharedDoc = Document.create({ title:"Shared" }, { owner: group });
|
||||
```
|
||||
|
||||
### **2. Roles & Permissions**
|
||||
Built-in roles: `"admin"`, `"writer"`, `"reader"`, `"readerInvite"`, `"writerInvite"`.
|
||||
```typescript
|
||||
group.addMember(bob, "writer");
|
||||
group.addMember(alice, "reader");
|
||||
group.addMember("everyone","reader");
|
||||
```
|
||||
|
||||
### **3. Organizations & Memberships**
|
||||
```typescript
|
||||
import { Account, CoMap, CoList, Group, co } from "jazz-tools";
|
||||
class Project extends CoMap { name = co.string; description = co.string; }
|
||||
class Organization extends CoMap {
|
||||
name = co.string;
|
||||
projects = co.ref(CoList.Of(co.ref(Project)));
|
||||
static create(name: string, owner: Account) {
|
||||
const group = Group.create({ owner });
|
||||
return super.create({ name, projects: CoList.Of(co.ref(Project)).create([], { owner: group }) }, { owner: group });
|
||||
}
|
||||
addMember(account: Account, role: "admin"|"writer"|"reader") {
|
||||
this._owner.castAs(Group).addMember(account, role);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **4. Account Root & Migration Pattern**
|
||||
```typescript
|
||||
class TodoAccountRoot extends CoMap {
|
||||
projects = co.ref(ListOfProjects);
|
||||
}
|
||||
export class UserProfile extends Profile { someProperty = co.string; }
|
||||
class TodoAccount extends Account {
|
||||
root = co.ref(TodoAccountRoot);
|
||||
profile = co.ref(UserProfile);
|
||||
migrate() {
|
||||
if (!this._refs.root) {
|
||||
this.root = TodoAccountRoot.create({ projects: ListOfProjects.create([], { owner: this }) }, { owner: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **5. Public Sharing Example**
|
||||
```typescript
|
||||
class SharedFile extends CoMap {
|
||||
name = co.string;
|
||||
file = co.ref(FileStream);
|
||||
createdAt = co.Date;
|
||||
size = co.number;
|
||||
}
|
||||
class FileShareAccountRoot extends CoMap {
|
||||
type = co.string;
|
||||
sharedFiles = co.ref(ListOfSharedFiles);
|
||||
publicGroup = co.ref(Group);
|
||||
}
|
||||
export class UserProfile extends Profile { someProperty = co.string; }
|
||||
class FileShareAccount extends Account {
|
||||
root = co.ref(FileShareAccountRoot);
|
||||
profile = co.ref(UserProfile);
|
||||
async migrate() {
|
||||
await this._refs.root?.load();
|
||||
if (!this.root || this.root.type !== "file-share-account") {
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember("everyone","reader");
|
||||
this.root = FileShareAccountRoot.create({
|
||||
type:"file-share-account",
|
||||
sharedFiles: ListOfSharedFiles.create([], { owner: publicGroup }),
|
||||
publicGroup
|
||||
}, { owner: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **6. Group Extensions**
|
||||
```typescript
|
||||
const parentGroup = Group.create({ owner: me });
|
||||
parentGroup.addMember(bob, "reader");
|
||||
const childGroup = Group.create({ owner: me });
|
||||
childGroup.extend(parentGroup);
|
||||
const doc = Document.create({ title:"Inherited Access" }, { owner: childGroup });
|
||||
```
|
||||
|
||||
### **7. Checking Permissions**
|
||||
```typescript
|
||||
const group = document._owner.castAs(Group);
|
||||
const myRole = group.myRole();
|
||||
const hasWriteAccess = myRole === "admin" || myRole === "writer";
|
||||
```
|
||||
|
||||
### **8. Invitation Pattern**
|
||||
```typescript
|
||||
class TeamInvite extends CoMap {
|
||||
email = co.string;
|
||||
role = co.literal("admin","writer","reader");
|
||||
accepted = co.boolean;
|
||||
}
|
||||
class Team extends CoMap {
|
||||
invites = co.ref(CoList.Of(co.ref(TeamInvite)));
|
||||
async inviteMember(email: string, role: "admin"|"writer"|"reader") {
|
||||
const group = this._owner.castAs(Group);
|
||||
const invite = TeamInvite.create({ email, role, accepted:false }, { owner: group });
|
||||
this.invites.push(invite);
|
||||
group.addMember(email, (role+"Invite") as const);
|
||||
}
|
||||
acceptInvite(account: Account) {
|
||||
const group = this._owner.castAs(Group);
|
||||
const invite = this.invites.find(i => i.email === account.email);
|
||||
if (invite) {
|
||||
invite.accepted = true;
|
||||
group.addMember(account, invite.role);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Key Takeaways**
|
||||
- Every `CoValue` has an owner (Account or Group).
|
||||
- Groups enable sharing/role-based access (`"admin"`, `"writer"`, `"reader"`, etc.).
|
||||
- Groups can inherit permissions.
|
||||
- Use account roots for private per-user data.
|
||||
- Public sharing via `"everyone"` role.
|
||||
- Invites allow controlled membership.
|
||||
|
||||
---
|
||||
|
||||
## **Inbox Pattern in Jazz**
|
||||
Enables message exchange between accounts using `CoMap`, `CoList`, `Group`.
|
||||
|
||||
### **1. Basic Inbox Setup**
|
||||
```typescript
|
||||
import { CoMap, co, Group } from "jazz-tools";
|
||||
class Message extends CoMap {
|
||||
text = co.string;
|
||||
createdAt = co.Date;
|
||||
read = co.boolean;
|
||||
}
|
||||
class ChatInbox extends CoMap {
|
||||
messages = co.ref(CoList.Of(co.ref(Message)));
|
||||
lastReadAt = co.Date;
|
||||
}
|
||||
```
|
||||
|
||||
### **2. Sending Messages**
|
||||
```typescript
|
||||
async function sendMessage(sender: Account, receiverId: ID<Account>, text: string) {
|
||||
const message = Message.create(
|
||||
{ text, createdAt:new Date(), read:false },
|
||||
{ owner: Group.create({ owner: sender }) }
|
||||
);
|
||||
const inboxSender = await InboxSender.load(receiverId, sender);
|
||||
inboxSender.sendMessage(message);
|
||||
}
|
||||
```
|
||||
|
||||
### **3. Receiving Messages**
|
||||
```typescript
|
||||
async function setupInbox(receiver: Account) {
|
||||
const inbox = await Inbox.load(receiver);
|
||||
return inbox.subscribe(Message,(message,senderId)=>console.log("New:",message.text));
|
||||
}
|
||||
```
|
||||
|
||||
### **4. Chat Application Example**
|
||||
```typescript
|
||||
class ChatMessage extends CoMap { text = co.string; createdAt=co.Date; read=co.boolean; }
|
||||
class ChatThread extends CoMap {
|
||||
participants = co.json<string[]>();
|
||||
messages = co.ref(CoList.Of(co.ref(ChatMessage)));
|
||||
lastReadAt = co.optional.Date;
|
||||
}
|
||||
class ChatRoot extends CoMap {
|
||||
threads = co.ref(CoList.Of(co.ref(ChatThread)));
|
||||
inbox = co.ref(Inbox);
|
||||
}
|
||||
export class UserProfile extends Profile { someProperty=co.string; }
|
||||
class ChatAccount extends Account {
|
||||
root = co.ref(ChatRoot);
|
||||
profile = co.ref(UserProfile);
|
||||
async migrate() {
|
||||
if(!this._refs.root) {
|
||||
const group = Group.create({ owner:this });
|
||||
this.root = ChatRoot.create({
|
||||
threads: CoList.Of(co.ref(ChatThread)).create([], { owner: group }),
|
||||
inbox: await Inbox.create(this)
|
||||
},{ owner:this });
|
||||
}
|
||||
}
|
||||
async sendMessage(to: ID<Account>, text:string) {
|
||||
const message = ChatMessage.create({ text, createdAt:new Date(), read:false },
|
||||
{ owner: Group.create({ owner:this }) });
|
||||
const inboxSender=await InboxSender.load(to,this);
|
||||
inboxSender.sendMessage(message);
|
||||
}
|
||||
async setupInboxListener() {
|
||||
const inbox = await Inbox.load(this);
|
||||
return inbox.subscribe(ChatMessage, async (message, senderId) => {
|
||||
const thread = await this.findOrCreateThread(senderId);
|
||||
thread.messages.push(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **5. Testing the Inbox Pattern**
|
||||
```typescript
|
||||
describe("Inbox", () => {
|
||||
it("should allow message exchange", async () => {
|
||||
const { clientAccount:sender, serverAccount:receiver } = await setupTwoNodes();
|
||||
const receiverInbox = await Inbox.load(receiver);
|
||||
const message = Message.create({ text:"Hello" },{ owner:Group.create({ owner:sender }) });
|
||||
const inboxSender = await InboxSender.load(receiver.id, sender);
|
||||
inboxSender.sendMessage(message);
|
||||
const receivedMessages: Message[] = [];
|
||||
let senderAccountID: unknown;
|
||||
const unsubscribe = receiverInbox.subscribe(Message, (msg, id) => {
|
||||
senderAccountID=id; receivedMessages.push(msg);
|
||||
});
|
||||
await waitFor(() => receivedMessages.length===1);
|
||||
expect(receivedMessages[0]?.text).toBe("Hello");
|
||||
expect(senderAccountID).toBe(sender.id);
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### **6. Message Status Tracking**
|
||||
```typescript
|
||||
class MessageStatus extends CoMap {
|
||||
messageId = co.string;
|
||||
delivered = co.boolean;
|
||||
read = co.boolean;
|
||||
readAt = co.optional.Date;
|
||||
}
|
||||
class EnhancedMessage extends CoMap {
|
||||
text = co.string;
|
||||
createdAt = co.Date;
|
||||
status = co.ref(MessageStatus);
|
||||
}
|
||||
async function sendMessageWithStatus(sender: Account, receiverId: ID<Account>, text:string) {
|
||||
const group = Group.create({ owner:sender });
|
||||
const status = MessageStatus.create({ messageId:crypto.randomUUID(), delivered:false, read:false }, { owner:group });
|
||||
const message = EnhancedMessage.create({ text, createdAt:new Date(), status }, { owner:group });
|
||||
const inboxSender = await InboxSender.load(receiverId,sender);
|
||||
inboxSender.sendMessage(message);
|
||||
return message;
|
||||
}
|
||||
```
|
||||
|
||||
### **Key Takeaways**
|
||||
- Messages owned by a `Group` from the sender.
|
||||
- Use `InboxSender.load()` to send, `Inbox.load()` to receive.
|
||||
- Subscribe for real-time updates.
|
||||
- Append status tracking as needed.
|
||||
|
||||
---
|
||||
|
||||
## **Invite Pattern in Jazz**
|
||||
Sharing access to `CoValues` with other users via invites.
|
||||
|
||||
### **1. Creating & Handling Invites**
|
||||
```typescript
|
||||
import { CoMap, Group, co, createInviteLink } from "jazz-tools";
|
||||
class Project extends CoMap { name = co.string; members = co.ref(CoList.Of(co.ref(Member))); }
|
||||
const group = Group.create({ owner: me });
|
||||
const project = Project.create({ name:"New Project", members:CoList.Of(co.ref(Member)).create([],{owner:group}) }, { owner:group });
|
||||
const readerInvite = createInviteLink(project,"reader");
|
||||
const writerInvite = createInviteLink(project,"writer");
|
||||
const adminInvite = createInviteLink(project,"admin");
|
||||
```
|
||||
|
||||
### **2. Accepting Invites in UI**
|
||||
```typescript
|
||||
import { useAcceptInvite } from "jazz-react";
|
||||
useAcceptInvite({
|
||||
invitedObjectSchema:Project,
|
||||
onAccept:(projectId)=>navigate(`/projects/${projectId}`)
|
||||
});
|
||||
```
|
||||
|
||||
### **3. Organization Example**
|
||||
```typescript
|
||||
class Organization extends CoMap {
|
||||
name = co.string;
|
||||
projects = co.ref(ListOfProjects);
|
||||
createInvite(role:"reader"|"writer"|"admin"){ return createInviteLink(this,role); }
|
||||
}
|
||||
useAcceptInvite({
|
||||
invitedObjectSchema:Organization,
|
||||
onAccept:async(orgId)=>{/* ... */}
|
||||
});
|
||||
```
|
||||
|
||||
### **4. Value Hints in Invites**
|
||||
```typescript
|
||||
class Team extends CoMap {
|
||||
name = co.string;
|
||||
generateInvite(role:"reader"|"writer"|"admin"){ return createInviteLink(this, role, window.location.origin, "team"); }
|
||||
}
|
||||
useAcceptInvite({
|
||||
invitedObjectSchema:Team,
|
||||
forValueHint:"team",
|
||||
onAccept:(teamId)=>navigate(`/teams/${teamId}`)
|
||||
});
|
||||
```
|
||||
|
||||
### **5. Testing Invites**
|
||||
```typescript
|
||||
describe("Invite Links", () => {
|
||||
test("generate and parse invites", async () => {
|
||||
const inviteLink = createInviteLink(group, "writer","https://example.com","myGroup");
|
||||
const parsed = parseInviteLink(inviteLink);
|
||||
expect(parsed?.valueID).toBe(group.id);
|
||||
expect(parsed?.valueHint).toBe("myGroup");
|
||||
});
|
||||
test("accept invite", async () => {
|
||||
const newAccount = await createJazzTestAccount();
|
||||
const inviteLink = createInviteLink(group, "writer");
|
||||
const result = await consumeInviteLink({ inviteURL: inviteLink, as:newAccount, invitedObjectSchema:Group });
|
||||
expect(result?.valueID).toBe(group.id);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### **6. File Sharing with Invites**
|
||||
```typescript
|
||||
class SharedFile extends CoMap {
|
||||
name = co.string;
|
||||
sharedWith = co.ref(CoList.Of(co.ref(SharedWith)));
|
||||
}
|
||||
class SharedWith extends CoMap {
|
||||
email = co.string;
|
||||
role = co.literal("reader","writer");
|
||||
acceptedAt = co.optional.Date;
|
||||
}
|
||||
class FileShareAccount extends Account {
|
||||
async shareFile(file:SharedFile, email:string, role:"reader"|"writer") {
|
||||
const inviteLink = createInviteLink(file,role);
|
||||
file.sharedWith.push(SharedWith.create({ email, role, acceptedAt:null },{ owner:file._owner }));
|
||||
await sendInviteEmail(email, inviteLink);
|
||||
}
|
||||
}
|
||||
useAcceptInvite({
|
||||
invitedObjectSchema:SharedFile,
|
||||
onAccept:async(fileId)=>{
|
||||
const file = await SharedFile.load(fileId,{});
|
||||
const shareRecord = file.sharedWith.find(s=>s.email===currentUser.email);
|
||||
if(shareRecord) shareRecord.acceptedAt=new Date();
|
||||
navigate(`/files/${fileId}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### **Key Takeaways**
|
||||
- Use Groups for shared ownership.
|
||||
- `createInviteLink()` generates invites.
|
||||
- `useAcceptInvite()` handles acceptance.
|
||||
- Value hints (`forValueHint`) differentiate invite types.
|
||||
|
||||
---
|
||||
|
||||
## **CoValue Types & Patterns in Jazz**
|
||||
|
||||
### **1. CoMap**
|
||||
- Use for structured data with named fields.
|
||||
- Example:
|
||||
```typescript
|
||||
class UserProfile extends CoMap {
|
||||
name = co.string;
|
||||
email = co.string;
|
||||
avatar = co.ref(FileStream);
|
||||
preferences = co.json<{ theme:string; notifications:boolean }>();
|
||||
}
|
||||
class TagColors extends CoMap.Record(co.string) {}
|
||||
```
|
||||
|
||||
### **2. CoList**
|
||||
- Use for ordered, real-time collaborative arrays.
|
||||
```typescript
|
||||
class TodoList extends CoList.Of(co.ref(TodoItem)) {}
|
||||
class StringList extends CoList.Of(co.string) {}
|
||||
```
|
||||
|
||||
### **3. CoFeed**
|
||||
- Use for append-only event/log data.
|
||||
```typescript
|
||||
class UserActivity extends CoFeed.Of(co.json<{ type:string; timestamp:number; text?:string }>) {}
|
||||
```
|
||||
|
||||
### **4. SchemaUnion**
|
||||
- Use for polymorphic objects with a runtime discriminator.
|
||||
```typescript
|
||||
class BaseWidget extends CoMap { type=co.string; }
|
||||
class ButtonWidget extends BaseWidget { type=co.literal("button"); label=co.string; }
|
||||
const Widget=SchemaUnion.Of<BaseWidget>(raw=>raw.get("type")==="button"?ButtonWidget:null);
|
||||
```
|
||||
|
||||
### **5. Groups & Permissions**
|
||||
- Owner can be an Account or Group.
|
||||
- Roles: `"admin"|"writer"|"reader"|"readerInvite"|"writerInvite"`.
|
||||
```typescript
|
||||
const group=Group.create({owner:me});
|
||||
group.addMember("everyone","reader");
|
||||
```
|
||||
|
||||
### **6. Accounts**
|
||||
- Per-user data storage with migrations.
|
||||
```typescript
|
||||
class JazzAccount extends Account {
|
||||
root=co.ref(JazzAccountRoot);
|
||||
profile=co.ref(UserProfile);
|
||||
async migrate(){/*...*/}
|
||||
}
|
||||
```
|
||||
|
||||
### **7. Migrations**
|
||||
- Update/initialize user data on account creation/login.
|
||||
|
||||
### **8. Invites**
|
||||
- Role-based sharing through invite links.
|
||||
|
||||
### **Common Patterns**
|
||||
- **Account Root Pattern**: store user’s top-level data.
|
||||
- **Shared Document Pattern**: CoMap for doc, CoList for collaborators, CoFeed for history.
|
||||
- **Draft Pattern**: CoMap with partial fields, validation.
|
||||
- **Public Sharing**: set `Group.addMember("everyone","reader")`.
|
||||
|
||||
---
|
||||
|
||||
## **Examples**
|
||||
|
||||
1. **User Profile Storage (CoMap)**
|
||||
**JSON**:
|
||||
```json
|
||||
{
|
||||
"name":"John Doe","email":"john@example.com",
|
||||
"avatar":{"url":"...","size":"..."},
|
||||
"preferences":{"theme":"dark","notifications":true}
|
||||
}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class UserProfile extends CoMap {
|
||||
name = co.string;
|
||||
email = co.string;
|
||||
avatar = co.ref(FileStream);
|
||||
preferences = co.json<{theme:string;notifications:boolean}>();
|
||||
}
|
||||
```
|
||||
|
||||
2. **To-Do List (CoList)**
|
||||
**JSON**:
|
||||
```json
|
||||
{"tasks":[{"id":1,"title":"Buy groceries","completed":false},{"id":2,"title":"Call mom","completed":true}]}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class TodoItem extends CoMap { title=co.string; completed=co.boolean; }
|
||||
class TodoList extends CoList.Of(co.ref(TodoItem)) {}
|
||||
```
|
||||
|
||||
3. **Activity Feed (CoFeed)**
|
||||
**JSON**:
|
||||
```json
|
||||
{"activities":[{"type":"login","timestamp":1700000000},{"type":"logout","timestamp":1700000500},{"type":"comment","timestamp":1700001000,"text":"Great post!"}]}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class UserActivity extends CoFeed.Of(co.json<{type:string;timestamp:number;text?:string}>()) {}
|
||||
```
|
||||
|
||||
4. **Polymorphic Widgets (SchemaUnion)**
|
||||
**JSON**:
|
||||
```json
|
||||
{"widgets":[{"type":"button","label":"Click Me"},{"type":"slider","min":0,"max":100}]}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class BaseWidget extends CoMap { type=co.string; }
|
||||
class ButtonWidget extends BaseWidget { type=co.literal("button"); label=co.string; }
|
||||
class SliderWidget extends BaseWidget { type=co.literal("slider"); min=co.number; max=co.number; }
|
||||
const Widget=SchemaUnion.Of<BaseWidget>((raw)=>{...});
|
||||
```
|
||||
|
||||
5. **Access Control via Groups**
|
||||
**JSON**:
|
||||
```json
|
||||
{"group":{"owner":"user123","members":[{"id":"user456","role":"admin"},{"id":"user789","role":"writer"}]}}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
const group=Group.create({owner:user123});
|
||||
group.addMember(user456,"admin");
|
||||
group.addMember(user789,"writer");
|
||||
```
|
||||
|
||||
6. **User Account with Root Data (Accounts)**
|
||||
**JSON**:
|
||||
```json
|
||||
{"user":{"profile":{"name":"Jane Doe"},"documents":[{"title":"My Notes","content":"This is a note."}],"activities":[{"type":"login"}]}}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class AppAccountRoot extends CoMap {
|
||||
profile=co.ref(UserProfile);
|
||||
documents=co.ref(CoList.Of(co.ref(Document)));
|
||||
activities=co.ref(UserActivity);
|
||||
}
|
||||
class AppAccount extends Account {
|
||||
root=co.ref(AppAccountRoot);
|
||||
profile=co.ref(UserProfile);
|
||||
}
|
||||
```
|
||||
|
||||
7. **Document Collaboration**
|
||||
**JSON**:
|
||||
```json
|
||||
{
|
||||
"document":{
|
||||
"title":"Project Plan","content":"Detailed...","collaborators":[{"id":"user1","role":"editor"},{"id":"user2","role":"viewer"}],
|
||||
"history":[{"user":"user1","timestamp":1700000000,"change":"Edited content"}]
|
||||
}
|
||||
}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class Document extends CoMap {
|
||||
title=co.string;content=co.string;
|
||||
collaborators=co.ref(CoList.Of(co.ref(UserProfile)));
|
||||
history=co.ref(CoFeed.Of(co.json<{user:string;timestamp:number;change:string}>()));
|
||||
}
|
||||
```
|
||||
|
||||
8. **Draft System**
|
||||
**JSON**:
|
||||
```json
|
||||
{"draft":{"name":"New Project","tasks":[],"valid":false,"errors":["Project name required"]}}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class DraftProject extends CoMap {
|
||||
name=co.optional.string;
|
||||
tasks=co.ref(CoList.Of(co.ref(TodoItem)));
|
||||
validate(){/*...*/}
|
||||
}
|
||||
```
|
||||
|
||||
9. **Public File Sharing**
|
||||
**JSON**:
|
||||
```json
|
||||
{"file":{"name":"Presentation.pdf","size":2048,"uploadedAt":1700000000,"sharedWith":["everyone"]}}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class SharedFile extends CoMap {
|
||||
name=co.string;
|
||||
file=co.ref(FileStream);
|
||||
uploadedAt=co.Date;
|
||||
}
|
||||
const publicGroup=Group.create({owner:me});
|
||||
publicGroup.addMember("everyone","reader");
|
||||
```
|
||||
|
||||
10. **Invite System**
|
||||
**JSON**:
|
||||
```json
|
||||
{"invites":[{"email":"user@example.com","role":"writer","status":"pending"}]}
|
||||
```
|
||||
**Jazz**:
|
||||
```typescript
|
||||
class Invite extends CoMap {
|
||||
email=co.string;
|
||||
role=co.literal("reader","writer","admin");
|
||||
status=co.literal("pending","accepted");
|
||||
}
|
||||
const inviteLink=createInviteLink(project,"writer");
|
||||
useAcceptInvite({ invitedObjectSchema:Project, onAccept:(id)=>navigate(`/projects/${id}`) });
|
||||
```
|
||||
|
||||
---
|
||||
214
packages/cursor-docs/docs/2_jazz_schema_template.md
Normal file
214
packages/cursor-docs/docs/2_jazz_schema_template.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
|
||||
import { Account, CoList, CoMap, Group, Profile, co } from "jazz-tools";
|
||||
|
||||
/**
|
||||
* Represents a main data item in the app’s domain.
|
||||
*
|
||||
* Properties:
|
||||
* - name: Required field identifying the item.
|
||||
* - metadata_field: Optional metadata (string).
|
||||
* - container: Reference to a parent Container.
|
||||
* - deleted: Soft delete flag for archiving/removing without permanent deletion.
|
||||
*/
|
||||
export class MainItem extends CoMap {
|
||||
/** A required, identifying name. */
|
||||
name = co.string;
|
||||
|
||||
/** An optional string field for metadata. */
|
||||
metadata_field = co.optional.string;
|
||||
|
||||
/** Reference to the parent container. */
|
||||
container = co.ref(Container);
|
||||
|
||||
/** Soft-delete flag: if true, treat this item as removed. */
|
||||
deleted = co.boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list/array of MainItem references.
|
||||
* Provides real-time collaboration features (insertion, removal, ordering).
|
||||
*/
|
||||
export class MainItemList extends CoList.Of(co.ref(MainItem)) {}
|
||||
|
||||
/**
|
||||
* A container/organizational structure for grouping MainItem objects.
|
||||
*
|
||||
* Properties:
|
||||
* - name: A human-friendly name for the container.
|
||||
* - items: A CoList of MainItem references.
|
||||
*/
|
||||
export class Container extends CoMap {
|
||||
/** Human-friendly name for this container. */
|
||||
name = co.string;
|
||||
|
||||
/** A list of MainItems held by this container. */
|
||||
items = co.ref(MainItemList);
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-level structure in the user’s account, representing all stored data.
|
||||
*
|
||||
* Properties:
|
||||
* - container: The default or root container for MainItems.
|
||||
* - version: An optional version number for supporting migrations.
|
||||
*/
|
||||
export class AccountRoot extends CoMap {
|
||||
/** A single container to hold or organize items. */
|
||||
container = co.ref(Container);
|
||||
|
||||
/** Tracks schema version for migrations. */
|
||||
version = co.optional.number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user’s profile data.
|
||||
*
|
||||
* Properties:
|
||||
* - email: Required email field for identification/contact.
|
||||
*
|
||||
* Static method:
|
||||
* - validate: Enforces that both name and email are provided.
|
||||
*/
|
||||
export class UserProfile extends Profile {
|
||||
/** Required user email. */
|
||||
email = co.string;
|
||||
|
||||
/**
|
||||
* Validate user profile data, ensuring both "name" and "email" exist and are non-empty.
|
||||
*/
|
||||
static validate(data: { name?: string; email?: string }) {
|
||||
const errors: string[] = [];
|
||||
if (!data.name?.trim()) {
|
||||
errors.push("Please enter a name.");
|
||||
}
|
||||
if (!data.email?.trim()) {
|
||||
errors.push("Please enter an email.");
|
||||
}
|
||||
return { errors };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main Account class that holds the user’s data (AccountRoot) and profile.
|
||||
* Handles initial migrations (setting up default Container, etc.) and can be extended
|
||||
* to run future schema migrations.
|
||||
*/
|
||||
export class JazzAccount extends Account {
|
||||
/** Reference to the user’s profile. */
|
||||
profile = co.ref(UserProfile);
|
||||
|
||||
/** Reference to the account root data (container, version, etc.). */
|
||||
root = co.ref(AccountRoot);
|
||||
|
||||
/**
|
||||
* Migrate is run on creation and each login. If there is no root, creates initial data.
|
||||
* Otherwise, you can add version-based migrations (below).
|
||||
*/
|
||||
async migrate(creationProps?: { name: string; other?: Record<string, unknown> }) {
|
||||
if (!this._refs.root && creationProps) {
|
||||
await this.initialMigration(creationProps);
|
||||
return;
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// uncomment this to add migrations
|
||||
// Check the current version and run subsequent migrations
|
||||
// const currentVersion = this.root?.version || 0;
|
||||
// if (currentVersion < 1) {
|
||||
// await this.migrationV1();
|
||||
// }
|
||||
// Add more version checks and migrations as needed
|
||||
// if (currentVersion < 2) {
|
||||
// await this.migrationV2();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the initial migration logic when the account is first created:
|
||||
* - Validates the user’s profile data (name, email).
|
||||
* - Sets up a public group (readable by "everyone") for the user’s profile.
|
||||
* - Sets up a private group to own private resources.
|
||||
* - Creates a default Container with a single MainItem.
|
||||
*/
|
||||
private async initialMigration(
|
||||
creationProps: { name: string; other?: Record<string, unknown> }
|
||||
) {
|
||||
const { name, other } = creationProps;
|
||||
|
||||
// Validate profile data
|
||||
const profileErrors = UserProfile.validate({ name, ...other });
|
||||
if (profileErrors.errors.length > 0) {
|
||||
throw new Error(
|
||||
"Invalid profile data: " + profileErrors.errors.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
// Create a public group for the profile
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember("everyone", "reader");
|
||||
|
||||
// Create the user profile with validated data
|
||||
this.profile = UserProfile.create(
|
||||
{
|
||||
name,
|
||||
...other,
|
||||
},
|
||||
{ owner: publicGroup },
|
||||
);
|
||||
|
||||
// Create a private group for data that should not be publicly readable
|
||||
const privateGroup = Group.create({ owner: this });
|
||||
|
||||
// Create a default container with one default item
|
||||
const defaultContainer = Container.create(
|
||||
{
|
||||
name: this.profile?.name
|
||||
? \`\${this.profile.name}'s items\`
|
||||
: "Your items",
|
||||
items: MainItemList.create(
|
||||
[
|
||||
MainItem.create({ name: "Default item" }, privateGroup),
|
||||
],
|
||||
privateGroup,
|
||||
),
|
||||
},
|
||||
privateGroup,
|
||||
);
|
||||
|
||||
// Initialize the account root with version tracking
|
||||
this.root = AccountRoot.create(
|
||||
{
|
||||
container: defaultContainer,
|
||||
version: 0, // Start at version 0
|
||||
},
|
||||
{ owner: this },
|
||||
);
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// uncomment this to add migrations
|
||||
// private async migrationV1() {
|
||||
// Example migration logic:
|
||||
// if (this.root) {
|
||||
// // e.g., add a new field to all items
|
||||
// // for (const container of this.root.containers || []) {
|
||||
// // for (const item of container.items || []) {
|
||||
// // item.newField = "default value";
|
||||
// // }
|
||||
// // }
|
||||
// this.root.version = 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// uncomment this to add migrations
|
||||
// private async migrationV2() {
|
||||
// if (this.root) {
|
||||
// // Future migration logic goes here
|
||||
// this.root.version = 2;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
---
|
||||
53
packages/cursor-docs/docs/3_jazz_rules.md
Normal file
53
packages/cursor-docs/docs/3_jazz_rules.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
|
||||
**Jazz Schema Rules:**
|
||||
|
||||
1. **User Profile and Account**
|
||||
1.1. Define `export class UserProfile extends Profile` with exactly one property:
|
||||
```ts
|
||||
name = co.string;
|
||||
```
|
||||
1.2. Add a static `validate` method in `UserProfile` that checks `name` is present and non-empty.
|
||||
1.3. Define `export class JazzAccount extends Account` with exactly two properties:
|
||||
```ts
|
||||
profile = co.ref(UserProfile);
|
||||
root = co.ref(AccountRoot);
|
||||
```
|
||||
1.4. The `JazzAccount` class must have a `migrate(creationProps?: { name: string; other?: Record<string, unknown> })` method.
|
||||
- Within `migrate`, if `this._refs.root` is undefined and `creationProps` is provided, run `initialMigration`.
|
||||
- The `creationProps` **must** include a `name` property; `other` is optional but do not define more fields.
|
||||
|
||||
2. **Container, Root & Ownership**
|
||||
2.1. The `AccountRoot` class (extending `CoMap`) **must** have a `container` property referencing a `Container`.
|
||||
2.2. The `Container` class (extending `CoMap`) should contain the main domain entities of the app.
|
||||
2.3. **Never** define a `name` field in the `Container` class. The template shows an example `name` property for a Container, but these rules override that.
|
||||
2.4. Whenever the root structure is initialized, it is always owned by the current `JazzAccount`:
|
||||
```ts
|
||||
this.root = AccountRoot.create({ container: defaultContainer, version: 0 }, { owner: this });
|
||||
```
|
||||
|
||||
3. **Groups & Ownership**
|
||||
3.1. If the `UserProfile` is intended to be public, set its owner to a `publicGroup` that has `"everyone"` as `"reader"`. Otherwise, use a private group.
|
||||
3.2. When creating a group, no need to explicitly pass `owner: this`. That is implicit if it's the same account.
|
||||
3.3. **Do not** use properties like `user`, `users`, `group`, or `groups` in CoMaps or CoLists. Ownership is implicit.
|
||||
|
||||
4. **No Direct CoList Fields**
|
||||
4.1. **Never** do:
|
||||
```ts
|
||||
co.ref(CoList.Of(co.ref(SomeClass)));
|
||||
```
|
||||
4.2. Instead, define a CoList class (e.g. `export class SomeClassList extends CoList.Of(co.ref(SomeClass)) {}`) and reference it.
|
||||
|
||||
5. **Schema Structure & Fields**
|
||||
5.1. Follow the provided template patterns. **Do not** add extra entities or fields outside the user’s requirements or the template.
|
||||
5.2. Do **not** use properties like `createdAt` or `updatedAt`; they’re implicit in CoValue.
|
||||
5.3. If a property is optional, denote it with a question mark (`?`) in the field definition, or use `co.optional.*`.
|
||||
5.4. Keep comments from the template, especially around migration blocks, intact.
|
||||
5.5. Never set a property to "co.ref(UserProfile)".
|
||||
|
||||
6. **Output & Formatting**
|
||||
6.1. Generate the final schema in TypeScript with no extra markdown or triple backticks.
|
||||
6.2. Do **not** expand or alter the template’s classes beyond what is required.
|
||||
6.3. Avoid redundant or conflicting rules from the template; these revised rules take priority.
|
||||
|
||||
---
|
||||
225
packages/cursor-docs/docs/4_1_example.md
Normal file
225
packages/cursor-docs/docs/4_1_example.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Example app 1: A secure and organized password manager app that allows users to store, manage, and categorize their credentials in folders
|
||||
|
||||
```typescript
|
||||
import { Account, CoList, CoMap, Group, Profile, co } from "jazz-tools";
|
||||
|
||||
/**
|
||||
* Represents a password item in the Password Manager.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The required name identifying the password item.
|
||||
* - username: Optional username.
|
||||
* - username_input_selector: Optional selector for the username input field.
|
||||
* - password: The required password.
|
||||
* - password_input_selector: Optional selector for the password input.
|
||||
* - uri: Optional URI associated with the item.
|
||||
* - folder: Reference to the parent Folder.
|
||||
* - deleted: Soft delete flag.
|
||||
*/
|
||||
export class PasswordItem extends CoMap {
|
||||
name = co.string;
|
||||
username = co.optional.string;
|
||||
username_input_selector = co.optional.string;
|
||||
password = co.string;
|
||||
password_input_selector = co.optional.string;
|
||||
uri = co.optional.string;
|
||||
folder = co.ref(Folder);
|
||||
deleted = co.boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of PasswordItem references.
|
||||
*/
|
||||
export class PasswordList extends CoList.Of(co.ref(PasswordItem)) {}
|
||||
|
||||
/**
|
||||
* Represents a folder that groups password items.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The folder's name.
|
||||
* - items: A list of PasswordItems contained in the folder.
|
||||
*/
|
||||
export class Folder extends CoMap {
|
||||
name = co.string;
|
||||
items = co.ref(PasswordList);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of Folder references.
|
||||
*/
|
||||
export class FolderList extends CoList.Of(co.ref(Folder)) {}
|
||||
|
||||
/**
|
||||
* Top-level container for the Password Manager.
|
||||
* This container holds the main entities of the app.
|
||||
*
|
||||
* Properties:
|
||||
* - folders: A list of Folder entities.
|
||||
*/
|
||||
export class Container extends CoMap {
|
||||
folders = co.ref(FolderList);
|
||||
}
|
||||
|
||||
/**
|
||||
* The account root holds all user data.
|
||||
*
|
||||
* Properties:
|
||||
* - container: The main container that organizes the app’s data.
|
||||
* - version: An optional version number used for migrations.
|
||||
*/
|
||||
export class PasswordManagerAccountRoot extends CoMap {
|
||||
container = co.ref(Container);
|
||||
version = co.optional.number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the user's profile.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The required user name.
|
||||
*
|
||||
* Static method:
|
||||
* - validate: Ensures that a non-empty name is provided.
|
||||
*/
|
||||
export class UserProfile extends Profile {
|
||||
name = co.string;
|
||||
|
||||
static validate(data: { name?: string; email?: string }) {
|
||||
const errors: string[] = [];
|
||||
if (!data.name?.trim()) {
|
||||
errors.push("Please enter a name.");
|
||||
}
|
||||
// Note: In this schema, only 'name' is required.
|
||||
return { errors };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main account class for the Password Manager.
|
||||
* Contains only the profile and root properties.
|
||||
* Handles data initialization and migrations.
|
||||
*/
|
||||
export class PasswordManagerAccount extends Account {
|
||||
profile = co.ref(UserProfile);
|
||||
root = co.ref(PasswordManagerAccountRoot);
|
||||
|
||||
/**
|
||||
* The migrate method is called on account creation and login.
|
||||
* If the root is not initialized, it runs the initial migration.
|
||||
* Otherwise, you can add version-based migrations as needed.
|
||||
*/
|
||||
async migrate(creationProps?: { name: string; other?: Record<string, unknown> }) {
|
||||
if (!this._refs.root && creationProps) {
|
||||
await this.initialMigration(creationProps);
|
||||
return;
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment the following lines to add migrations:
|
||||
// const currentVersion = this.root?.version || 0;
|
||||
// if (currentVersion < 1) {
|
||||
// await this.migrationV1();
|
||||
// }
|
||||
// if (currentVersion < 2) {
|
||||
// await this.migrationV2();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the initial migration logic when the account is first created.
|
||||
* - Validates the user's profile data.
|
||||
* - Sets up a public group for the profile (accessible by "everyone").
|
||||
* - Sets up a private group for private resources.
|
||||
* - Creates a default Container with a default Folder and a default PasswordItem.
|
||||
*/
|
||||
private async initialMigration(creationProps: { name: string; other?: Record<string, unknown> }) {
|
||||
const { name, other } = creationProps;
|
||||
const profileErrors = UserProfile.validate({ name, ...other });
|
||||
if (profileErrors.errors.length > 0) {
|
||||
throw new Error("Invalid profile data: " + profileErrors.errors.join(", "));
|
||||
}
|
||||
|
||||
// Create a public group for the user profile.
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember("everyone", "reader");
|
||||
|
||||
// Create the user profile with validated data.
|
||||
this.profile = UserProfile.create(
|
||||
{
|
||||
name,
|
||||
...other,
|
||||
},
|
||||
{ owner: publicGroup }
|
||||
);
|
||||
|
||||
// Create a private group for private data.
|
||||
const privateGroup = Group.create({ owner: this });
|
||||
|
||||
// Create a default Folder with one default PasswordItem.
|
||||
const defaultFolder = Folder.create(
|
||||
{
|
||||
name: "Default",
|
||||
items: PasswordList.create(
|
||||
[
|
||||
PasswordItem.create(
|
||||
{
|
||||
name: "Gmail",
|
||||
username: "user@gmail.com",
|
||||
password: "password123",
|
||||
uri: "https://gmail.com",
|
||||
// The folder reference will be set after defaultFolder creation.
|
||||
folder: null as any,
|
||||
deleted: false,
|
||||
},
|
||||
privateGroup
|
||||
),
|
||||
],
|
||||
privateGroup
|
||||
),
|
||||
},
|
||||
privateGroup
|
||||
);
|
||||
// Set the folder reference for the default PasswordItem.
|
||||
defaultFolder.items[0].folder = defaultFolder;
|
||||
|
||||
// Create a default container that holds the FolderList.
|
||||
const defaultContainer = Container.create(
|
||||
{
|
||||
folders: FolderList.create([defaultFolder], privateGroup),
|
||||
},
|
||||
privateGroup
|
||||
);
|
||||
|
||||
// Initialize the account root with version tracking.
|
||||
this.root = PasswordManagerAccountRoot.create(
|
||||
{
|
||||
container: defaultContainer,
|
||||
version: 0, // Set initial version
|
||||
},
|
||||
{ owner: this }
|
||||
);
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment the following methods to add migrations:
|
||||
|
||||
// private async migrationV1() {
|
||||
// if (this.root) {
|
||||
// // Example migration logic: add a new field to all password items.
|
||||
// // for (const folder of this.root.container.folders || []) {
|
||||
// // for (const item of folder.items || []) {
|
||||
// // item.newField = "default value";
|
||||
// // }
|
||||
// // }
|
||||
// this.root.version = 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async migrationV2() {
|
||||
// if (this.root) {
|
||||
// // Future migration logic goes here.
|
||||
// this.root.version = 2;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
```
|
||||
173
packages/cursor-docs/docs/4_2_example.md
Normal file
173
packages/cursor-docs/docs/4_2_example.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Example app 2: A feature-rich music player app that allows users to manage playlists, store tracks, and visualize audio waveforms
|
||||
|
||||
```typescript
|
||||
export class MusicTrack extends CoMap {
|
||||
title = co.string;
|
||||
duration = co.number;
|
||||
sourceTrack = co.optional.ref(MusicTrack);
|
||||
file = co.ref(FileStream);
|
||||
waveform = co.ref(MusicTrackWaveform);
|
||||
container = co.ref(Playlist);
|
||||
deleted = co.boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents waveform data for a music track.
|
||||
*
|
||||
* Properties:
|
||||
* - data: A JSON array of numbers representing the waveform.
|
||||
*/
|
||||
export class MusicTrackWaveform extends CoMap {
|
||||
data = co.json<number[]>();
|
||||
}
|
||||
|
||||
/**
|
||||
* A collaborative list of MusicTrack references.
|
||||
*/
|
||||
export class MusicTrackList extends CoList.Of(co.ref(MusicTrack)) {}
|
||||
|
||||
/**
|
||||
* Acts as a container for music tracks.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The name of the playlist.
|
||||
* - items: A list of MusicTracks in this playlist.
|
||||
*/
|
||||
export class Playlist extends CoMap {
|
||||
name = co.string;
|
||||
items = co.ref(MusicTrackList);
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-level account root for the music app.
|
||||
*
|
||||
* Properties:
|
||||
* - container: The main playlist (acting as the container for music tracks).
|
||||
* - version: Optional version number for migrations.
|
||||
*/
|
||||
export class MusicAccountRoot extends CoMap {
|
||||
container = co.ref(Playlist);
|
||||
version = co.optional.number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user's profile.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The required user name.
|
||||
*
|
||||
* Static method:
|
||||
* - validate: Ensures that a non-empty name and email are provided.
|
||||
*/
|
||||
export class UserProfile extends Profile {
|
||||
name = co.string;
|
||||
|
||||
static validate(data: { name?: string; email?: string }) {
|
||||
const errors: string[] = [];
|
||||
if (!data.name?.trim()) {
|
||||
errors.push("Please enter a name.");
|
||||
}
|
||||
if (!data.email?.trim()) {
|
||||
errors.push("Please enter an email.");
|
||||
}
|
||||
return { errors };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main Account class for the music app.
|
||||
* Contains only the profile and root properties.
|
||||
* Handles data initialization and migrations.
|
||||
*/
|
||||
export class MusicAccount extends Account {
|
||||
profile = co.ref(UserProfile);
|
||||
root = co.ref(MusicAccountRoot);
|
||||
|
||||
/**
|
||||
* Migrate is run on account creation and each login.
|
||||
* If the root is not initialized, run initial migration.
|
||||
* Otherwise, version-based migrations can be added.
|
||||
*/
|
||||
async migrate(creationProps?: { name: string; other?: Record<string, unknown> }) {
|
||||
if (!this._refs.root && creationProps) {
|
||||
await this.initialMigration(creationProps);
|
||||
return;
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment to add migrations:
|
||||
// const currentVersion = this.root?.version || 0;
|
||||
// if (currentVersion < 1) {
|
||||
// await this.migrationV1();
|
||||
// }
|
||||
// if (currentVersion < 2) {
|
||||
// await this.migrationV2();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes initial migration when the account is first created:
|
||||
* - Validates the user's profile data (name, email).
|
||||
* - Sets up a public group (with "everyone" as reader) for the profile.
|
||||
* - Creates a default Playlist with an empty MusicTrackList.
|
||||
* - Initializes the account root with version 0.
|
||||
*/
|
||||
private async initialMigration(creationProps: { name: string; other?: Record<string, unknown> }) {
|
||||
const { name, other } = creationProps;
|
||||
const profileErrors = UserProfile.validate({ name, ...other });
|
||||
if (profileErrors.errors.length > 0) {
|
||||
throw new Error("Invalid profile data: " + profileErrors.errors.join(", "));
|
||||
}
|
||||
|
||||
// Create a public group for the profile.
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember("everyone", "reader");
|
||||
|
||||
// Create the user profile with validated data.
|
||||
this.profile = UserProfile.create(
|
||||
{ name, ...other },
|
||||
{ owner: publicGroup }
|
||||
);
|
||||
|
||||
// Create a private group for the user's music data.
|
||||
const privateGroup = Group.create({ owner: this });
|
||||
|
||||
// Create a default Playlist as the main container.
|
||||
const defaultPlaylist = Playlist.create(
|
||||
{
|
||||
name: this.profile.name + "'s playlist",
|
||||
items: MusicTrackList.create([], privateGroup),
|
||||
},
|
||||
privateGroup
|
||||
);
|
||||
|
||||
// Initialize the account root with version tracking.
|
||||
this.root = MusicAccountRoot.create(
|
||||
{
|
||||
container: defaultPlaylist,
|
||||
version: 0, // Set initial version
|
||||
},
|
||||
{ owner: this }
|
||||
);
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment to add migrations:
|
||||
// private async migrationV1() {
|
||||
// if (this.root) {
|
||||
// // Example migration logic: add a new field to all music tracks.
|
||||
// // for (const track of this.root.container.items || []) {
|
||||
// // track.newField = "default value";
|
||||
// // }
|
||||
// this.root.version = 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async migrationV2() {
|
||||
// if (this.root) {
|
||||
// // Future migration logic goes here.
|
||||
// this.root.version = 2;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
```
|
||||
178
packages/cursor-docs/docs/4_3_example.md
Normal file
178
packages/cursor-docs/docs/4_3_example.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Example app 3: A social pet app where users can share pet photos, react with fun emojis, and organize posts in a collaborative feed
|
||||
|
||||
```typescript
|
||||
import { Account, CoFeed, CoList, CoMap, Group, ImageDefinition, Profile, co } from "jazz-tools";
|
||||
|
||||
export const ReactionTypes = [
|
||||
"aww",
|
||||
"love",
|
||||
"haha",
|
||||
"wow",
|
||||
"tiny",
|
||||
"chonkers",
|
||||
] as const;
|
||||
|
||||
export type ReactionType = (typeof ReactionTypes)[number];
|
||||
|
||||
/**
|
||||
* Represents an append-only feed of reactions for a pet post.
|
||||
*/
|
||||
export class PetReactions extends CoFeed.Of(co.json<ReactionType>()) {}
|
||||
|
||||
/**
|
||||
* Represents a pet post.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The title or caption for the pet post.
|
||||
* - image: A reference to an ImageDefinition containing the pet's image.
|
||||
* - reactions: A feed of reactions (of type ReactionType) for the post.
|
||||
*/
|
||||
export class PetPost extends CoMap {
|
||||
name = co.string;
|
||||
image = co.ref(ImageDefinition);
|
||||
reactions = co.ref(PetReactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* A collaborative list of PetPost references.
|
||||
*/
|
||||
export class ListOfPosts extends CoList.Of(co.ref(PetPost)) {}
|
||||
|
||||
/**
|
||||
* Container for the pet posts.
|
||||
*
|
||||
* This container acts as the main organizational structure holding the posts.
|
||||
*
|
||||
* Properties:
|
||||
* - posts: A list of pet posts.
|
||||
*/
|
||||
export class PetContainer extends CoMap {
|
||||
posts = co.ref(ListOfPosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-level account root for the pet app.
|
||||
*
|
||||
* Properties:
|
||||
* - container: The main container that organizes pet posts.
|
||||
* - version: An optional version number for supporting migrations.
|
||||
*/
|
||||
export class PetAccountRoot extends CoMap {
|
||||
container = co.ref(PetContainer);
|
||||
version = co.optional.number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user’s profile.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The required user name.
|
||||
*
|
||||
* Static method:
|
||||
* - validate: Ensures that both "name" and "email" (if provided) are non-empty.
|
||||
*/
|
||||
export class UserProfile extends Profile {
|
||||
name = co.string;
|
||||
|
||||
static validate(data: { name?: string; email?: string }) {
|
||||
const errors: string[] = [];
|
||||
if (!data.name?.trim()) {
|
||||
errors.push("Please enter a name.");
|
||||
}
|
||||
if (data.email !== undefined && !data.email?.trim()) {
|
||||
errors.push("Please enter an email.");
|
||||
}
|
||||
return { errors };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main account class for the pet app.
|
||||
*
|
||||
* Contains only the profile and root properties, and handles account initialization
|
||||
* and migrations.
|
||||
*/
|
||||
export class PetAccount extends Account {
|
||||
profile = co.ref(UserProfile);
|
||||
root = co.ref(PetAccountRoot);
|
||||
|
||||
/**
|
||||
* Migrate is run on account creation and on every log-in.
|
||||
* If the root is not initialized, it runs the initial migration.
|
||||
* Otherwise, version-based migrations can be added.
|
||||
*/
|
||||
async migrate(creationProps?: { name: string; other?: Record<string, unknown> }) {
|
||||
if (!this._refs.root && creationProps) {
|
||||
await this.initialMigration(creationProps);
|
||||
return;
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment the following lines to add migrations:
|
||||
// const currentVersion = this.root?.version || 0;
|
||||
// if (currentVersion < 1) {
|
||||
// await this.migrationV1();
|
||||
// }
|
||||
// if (currentVersion < 2) {
|
||||
// await this.migrationV2();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the initial migration logic when the account is first created:
|
||||
* - Validates the user's profile data (name, email).
|
||||
* - Sets up a public group (accessible by "everyone") for the user’s profile.
|
||||
* - Sets up a private group for the user's pet posts.
|
||||
* - Creates a default container with an empty list of posts.
|
||||
* - Initializes the account root with version 0.
|
||||
*/
|
||||
private async initialMigration(creationProps: { name: string; other?: Record<string, unknown> }) {
|
||||
const { name, other } = creationProps;
|
||||
const profileErrors = UserProfile.validate({ name, ...other });
|
||||
if (profileErrors.errors.length > 0) {
|
||||
throw new Error("Invalid profile data: " + profileErrors.errors.join(", "));
|
||||
}
|
||||
|
||||
// Create a public group for the user profile.
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember("everyone", "reader");
|
||||
|
||||
// Create the user profile with validated data.
|
||||
this.profile = UserProfile.create(
|
||||
{ name, ...other },
|
||||
{ owner: publicGroup }
|
||||
);
|
||||
|
||||
// Create a private group for pet data.
|
||||
const privateGroup = Group.create({ owner: this });
|
||||
|
||||
// Create a default container holding an empty list of posts.
|
||||
const defaultContainer = PetContainer.create(
|
||||
{ posts: ListOfPosts.create([], privateGroup) },
|
||||
privateGroup
|
||||
);
|
||||
|
||||
// Initialize the account root with version tracking.
|
||||
this.root = PetAccountRoot.create(
|
||||
{ container: defaultContainer, version: 0 },
|
||||
{ owner: this }
|
||||
);
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment to add migrations:
|
||||
// private async migrationV1() {
|
||||
// if (this.root) {
|
||||
// // Example migration logic: update pet posts if needed.
|
||||
// this.root.version = 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async migrationV2() {
|
||||
// if (this.root) {
|
||||
// // Future migration logic goes here.
|
||||
// this.root.version = 2;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
```
|
||||
236
packages/cursor-docs/docs/4_4_example.md
Normal file
236
packages/cursor-docs/docs/4_4_example.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Example app 4: A bubble tea ordering app that lets users customize drinks with different tea bases, add-ons, and delivery preferences
|
||||
|
||||
```typescript
|
||||
import { Account, CoList, CoMap, Group, Profile, co } from "jazz-tools";
|
||||
|
||||
export const BubbleTeaAddOnTypes = [
|
||||
"Pearl",
|
||||
"Lychee jelly",
|
||||
"Red bean",
|
||||
"Brown sugar",
|
||||
"Taro",
|
||||
] as const;
|
||||
|
||||
export const BubbleTeaBaseTeaTypes = [
|
||||
"Black",
|
||||
"Oolong",
|
||||
"Jasmine",
|
||||
"Thai",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* A list of Bubble Tea add-ons.
|
||||
* Provides a computed property to check for insertions.
|
||||
*/
|
||||
export class ListOfBubbleTeaAddOns extends CoList.Of(co.literal(...BubbleTeaAddOnTypes)) {
|
||||
get hasChanges() {
|
||||
return Object.entries(this._raw.insertions).length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a finalized Bubble Tea order.
|
||||
*
|
||||
* Properties:
|
||||
* - baseTea: Selected base tea type.
|
||||
* - addOns: Selected add-ons.
|
||||
* - deliveryDate: Delivery date for the order.
|
||||
* - withMilk: Indicates if the order includes milk.
|
||||
* - instructions: Optional additional instructions.
|
||||
*/
|
||||
export class BubbleTeaOrder extends CoMap {
|
||||
baseTea = co.literal(...BubbleTeaBaseTeaTypes);
|
||||
addOns = co.ref(ListOfBubbleTeaAddOns);
|
||||
deliveryDate = co.Date;
|
||||
withMilk = co.boolean;
|
||||
instructions = co.optional.string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a draft (in-progress) Bubble Tea order.
|
||||
*
|
||||
* Properties:
|
||||
* - baseTea: Optional base tea type.
|
||||
* - addOns: Optional reference to selected add-ons.
|
||||
* - deliveryDate: Optional delivery date.
|
||||
* - withMilk: Optional milk preference.
|
||||
* - instructions: Optional instructions.
|
||||
*
|
||||
* Methods:
|
||||
* - validate: Checks that required fields are present.
|
||||
* Computed:
|
||||
* - hasChanges: Indicates if there have been modifications.
|
||||
*/
|
||||
export class DraftBubbleTeaOrder extends CoMap {
|
||||
baseTea = co.optional.literal(...BubbleTeaBaseTeaTypes);
|
||||
addOns = co.optional.ref(ListOfBubbleTeaAddOns);
|
||||
deliveryDate = co.optional.Date;
|
||||
withMilk = co.optional.boolean;
|
||||
instructions = co.optional.string;
|
||||
|
||||
get hasChanges() {
|
||||
return Object.keys(this._edits).length > 1 || this.addOns?.hasChanges;
|
||||
}
|
||||
|
||||
validate() {
|
||||
const errors: string[] = [];
|
||||
if (!this.baseTea) {
|
||||
errors.push("Please select your preferred base tea.");
|
||||
}
|
||||
if (!this.deliveryDate) {
|
||||
errors.push("Please select a delivery date.");
|
||||
}
|
||||
return { errors };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A collaborative list of finalized Bubble Tea orders.
|
||||
*/
|
||||
export class ListOfBubbleTeaOrders extends CoList.Of(co.ref(BubbleTeaOrder)) {}
|
||||
|
||||
/**
|
||||
* Container for Bubble Tea orders.
|
||||
* Holds the draft order and the list of finalized orders.
|
||||
*/
|
||||
export class BubbleTeaContainer extends CoMap {
|
||||
draft = co.ref(DraftBubbleTeaOrder);
|
||||
orders = co.ref(ListOfBubbleTeaOrders);
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-level account root for the Bubble Tea app.
|
||||
*
|
||||
* Properties:
|
||||
* - container: The main container that organizes the Bubble Tea orders.
|
||||
* - version: Optional version number for migration tracking.
|
||||
*/
|
||||
export class BubbleTeaAccountRoot extends CoMap {
|
||||
container = co.ref(BubbleTeaContainer);
|
||||
version = co.optional.number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user's profile.
|
||||
*
|
||||
* Properties:
|
||||
* - name: Required user name.
|
||||
*
|
||||
* Static method:
|
||||
* - validate: Ensures that a non-empty name and email (if provided) are present.
|
||||
*/
|
||||
export class UserProfile extends Profile {
|
||||
name = co.string;
|
||||
|
||||
static validate(data: { name?: string; email?: string }) {
|
||||
const errors: string[] = [];
|
||||
if (!data.name?.trim()) {
|
||||
errors.push("Please enter a name.");
|
||||
}
|
||||
if (data.email !== undefined && !data.email.trim()) {
|
||||
errors.push("Please enter an email.");
|
||||
}
|
||||
return { errors };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main account class for the Bubble Tea app.
|
||||
* Contains only the profile and root properties.
|
||||
* Handles account initialization and migrations.
|
||||
*/
|
||||
export class BubbleTeaAccount extends Account {
|
||||
profile = co.ref(UserProfile);
|
||||
root = co.ref(BubbleTeaAccountRoot);
|
||||
|
||||
/**
|
||||
* The migrate method is run on account creation and login.
|
||||
* If the root is not initialized, it runs the initial migration.
|
||||
* Otherwise, version-based migrations can be added.
|
||||
*/
|
||||
async migrate(creationProps?: { name: string; other?: Record<string, unknown> }) {
|
||||
if (!this._refs.root && creationProps) {
|
||||
await this.initialMigration(creationProps);
|
||||
return;
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment the following lines to add migrations:
|
||||
// const currentVersion = this.root?.version || 0;
|
||||
// if (currentVersion < 1) {
|
||||
// await this.migrationV1();
|
||||
// }
|
||||
// if (currentVersion < 2) {
|
||||
// await this.migrationV2();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the initial migration logic when the account is first created:
|
||||
* - Validates the user's profile data (name, email).
|
||||
* - Sets up a public group (accessible by "everyone") for the user's profile.
|
||||
* - Sets up a private group for the Bubble Tea data.
|
||||
* - Creates a default BubbleTeaContainer with an empty draft and order list.
|
||||
* - Initializes the account root with version 0.
|
||||
*/
|
||||
private async initialMigration(creationProps: { name: string; other?: Record<string, unknown> }) {
|
||||
const { name, other } = creationProps;
|
||||
const profileErrors = UserProfile.validate({ name, ...other });
|
||||
if (profileErrors.errors.length > 0) {
|
||||
throw new Error("Invalid profile data: " + profileErrors.errors.join(", "));
|
||||
}
|
||||
|
||||
// Create a public group for the user profile.
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember("everyone", "reader");
|
||||
|
||||
// Create the user profile with validated data.
|
||||
this.profile = UserProfile.create(
|
||||
{ name, ...other },
|
||||
{ owner: publicGroup }
|
||||
);
|
||||
|
||||
// Create a private group for Bubble Tea data.
|
||||
const privateGroup = Group.create({ owner: this });
|
||||
|
||||
// Create a default container with an empty draft order and empty list of finalized orders.
|
||||
const defaultContainer = BubbleTeaContainer.create(
|
||||
{
|
||||
draft: DraftBubbleTeaOrder.create(
|
||||
{
|
||||
addOns: ListOfBubbleTeaAddOns.create([], privateGroup),
|
||||
},
|
||||
privateGroup
|
||||
),
|
||||
orders: ListOfBubbleTeaOrders.create([], privateGroup),
|
||||
},
|
||||
privateGroup
|
||||
);
|
||||
|
||||
// Initialize the account root with version tracking.
|
||||
this.root = BubbleTeaAccountRoot.create(
|
||||
{
|
||||
container: defaultContainer,
|
||||
version: 0, // Set initial version
|
||||
},
|
||||
{ owner: this }
|
||||
);
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment to add migrations:
|
||||
// private async migrationV1() {
|
||||
// if (this.root) {
|
||||
// // Example migration logic: update orders if needed.
|
||||
// this.root.version = 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async migrationV2() {
|
||||
// if (this.root) {
|
||||
// // Future migration logic goes here.
|
||||
// this.root.version = 2;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
```
|
||||
211
packages/cursor-docs/docs/4_5_example.md
Normal file
211
packages/cursor-docs/docs/4_5_example.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Example app 5: An employee onboarding app that streamlines the hiring process through structured steps, including initial data collection, document uploads, and final approvals
|
||||
|
||||
```typescript
|
||||
import { Account, CoList, CoMap, Group, ImageDefinition, Profile, co } from "jazz-tools";
|
||||
|
||||
type Steps = "initial" | "upload" | "final";
|
||||
|
||||
interface Step {
|
||||
type: Steps;
|
||||
prevStep: ReturnType<typeof co.ref> | undefined;
|
||||
done: boolean;
|
||||
isCurrentStep(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the initial onboarding step.
|
||||
*
|
||||
* Properties:
|
||||
* - type: Always "initial".
|
||||
* - ssn: Optional Social Security Number.
|
||||
* - address: Optional address.
|
||||
* - done: Indicates if this step is completed.
|
||||
* - prevStep: Not applicable for the initial step.
|
||||
*/
|
||||
export class CoInitialStep extends CoMap implements Step {
|
||||
type = co.literal("initial");
|
||||
ssn? = co.string;
|
||||
address? = co.string;
|
||||
done = co.boolean;
|
||||
prevStep = co.null;
|
||||
isCurrentStep() {
|
||||
return !this.done;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the document upload step.
|
||||
*
|
||||
* Properties:
|
||||
* - type: Always "upload".
|
||||
* - prevStep: Reference to the completed initial step.
|
||||
* - photo: Optional reference to an image (e.g. document photo).
|
||||
* - done: Indicates if this step is completed.
|
||||
*/
|
||||
export class CoDocUploadStep extends CoMap implements Step {
|
||||
type = co.literal("upload");
|
||||
prevStep = co.ref(CoInitialStep);
|
||||
photo = co.ref(ImageDefinition, { optional: true });
|
||||
done = co.boolean;
|
||||
isCurrentStep() {
|
||||
return !!(this.prevStep?.done && !this.done);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the final onboarding step.
|
||||
*
|
||||
* Properties:
|
||||
* - type: Always "final".
|
||||
* - prevStep: Reference to the completed document upload step.
|
||||
* - done: Indicates if this step is completed.
|
||||
*/
|
||||
export class CoFinalStep extends CoMap implements Step {
|
||||
type = co.literal("final");
|
||||
prevStep = co.ref(CoDocUploadStep);
|
||||
done = co.boolean;
|
||||
isCurrentStep() {
|
||||
return !!(this.prevStep?.done && !this.done);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an employee undergoing the onboarding process.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The employee's name.
|
||||
* - deleted: Optional soft-delete flag.
|
||||
* - initialStep: Reference to the initial step.
|
||||
* - docUploadStep: Reference to the document upload step.
|
||||
* - finalStep: Reference to the final step.
|
||||
*/
|
||||
export class CoEmployee extends CoMap {
|
||||
name = co.string;
|
||||
deleted? = co.boolean;
|
||||
initialStep = co.ref(CoInitialStep);
|
||||
docUploadStep = co.ref(CoDocUploadStep);
|
||||
finalStep = co.ref(CoFinalStep);
|
||||
}
|
||||
|
||||
/**
|
||||
* A collaborative list of employee references.
|
||||
*/
|
||||
export class EmployeeList extends CoList.Of(co.ref(CoEmployee)) {}
|
||||
|
||||
/**
|
||||
* The top-level account root for the HR app.
|
||||
*
|
||||
* Properties:
|
||||
* - employees: A list of employees.
|
||||
* - version: Optional version number for migrations.
|
||||
*/
|
||||
export class HRAccountRoot extends CoMap {
|
||||
employees = co.ref(EmployeeList);
|
||||
version = co.optional.number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user's profile.
|
||||
*
|
||||
* Properties:
|
||||
* - name: The required user name.
|
||||
*
|
||||
* Static method:
|
||||
* - validate: Ensures that a non-empty name (and email, if provided) is present.
|
||||
*/
|
||||
export class UserProfile extends Profile {
|
||||
name = co.string;
|
||||
|
||||
static validate(data: { name?: string; email?: string }) {
|
||||
const errors: string[] = [];
|
||||
if (!data.name?.trim()) {
|
||||
errors.push("Please enter a name.");
|
||||
}
|
||||
if (data.email !== undefined && !data.email.trim()) {
|
||||
errors.push("Please enter an email.");
|
||||
}
|
||||
return { errors };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main account class for the HR app.
|
||||
* Contains only the profile and root properties.
|
||||
* Handles account initialization and migrations.
|
||||
*/
|
||||
export class HRAccount extends Account {
|
||||
profile = co.ref(UserProfile);
|
||||
root = co.ref(HRAccountRoot);
|
||||
|
||||
/**
|
||||
* Migrate is run on account creation and on every log-in.
|
||||
* If the account root is not initialized, it runs the initial migration.
|
||||
*/
|
||||
async migrate(creationProps?: { name: string; other?: Record<string, unknown> }) {
|
||||
if (!this._refs.root && creationProps) {
|
||||
await this.initialMigration(creationProps);
|
||||
return;
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment the following lines to add migrations:
|
||||
// const currentVersion = this.root?.version || 0;
|
||||
// if (currentVersion < 1) {
|
||||
// await this.migrationV1();
|
||||
// }
|
||||
// if (currentVersion < 2) {
|
||||
// await this.migrationV2();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the initial migration logic when the account is first created:
|
||||
* - Validates the user's profile data.
|
||||
* - Sets up a public group (accessible by "everyone") for the user profile.
|
||||
* - Creates a default HRAccountRoot with an empty employee list.
|
||||
* - Initializes the account root with version 0.
|
||||
*/
|
||||
private async initialMigration(creationProps: { name: string; other?: Record<string, unknown> }) {
|
||||
const { name, other } = creationProps;
|
||||
const profileErrors = UserProfile.validate({ name, ...other });
|
||||
if (profileErrors.errors.length > 0) {
|
||||
throw new Error("Invalid profile data: " + profileErrors.errors.join(", "));
|
||||
}
|
||||
|
||||
// Create a public group for the user profile.
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember("everyone", "reader");
|
||||
|
||||
// Create the user profile with validated data.
|
||||
this.profile = UserProfile.create({ name, ...other }, { owner: publicGroup });
|
||||
|
||||
// Create a private group for HR data.
|
||||
const privateGroup = Group.create({ owner: this });
|
||||
|
||||
// Create a default employee list (empty).
|
||||
const employees = EmployeeList.create([], privateGroup);
|
||||
|
||||
// Initialize the account root with version tracking.
|
||||
this.root = HRAccountRoot.create(
|
||||
{ employees, version: 0 },
|
||||
{ owner: this }
|
||||
);
|
||||
}
|
||||
|
||||
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
|
||||
// Uncomment to add migrations:
|
||||
// private async migrationV1() {
|
||||
// if (this.root) {
|
||||
// // Example migration logic: update employee records if needed.
|
||||
// this.root.version = 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async migrationV2() {
|
||||
// if (this.root) {
|
||||
// // Future migration logic goes here.
|
||||
// this.root.version = 2;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
```
|
||||
148
packages/cursor-docs/docs/4_6_example.md
Normal file
148
packages/cursor-docs/docs/4_6_example.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Example app 6: A task management app that helps users organize their to-dos with categories, tags, due dates, and priority levels
|
||||
|
||||
```typescript
|
||||
import { Account, CoList, CoMap, Group, Profile, co } from "jazz-tools";
|
||||
|
||||
// Task priority levels
|
||||
export type PriorityLevel = "Low" | "Medium" | "High";
|
||||
|
||||
// Represents a tag that can be associated with tasks
|
||||
export class Tag extends CoMap {
|
||||
name = co.string;
|
||||
deleted = co.boolean;
|
||||
}
|
||||
|
||||
export class TagList extends CoList.Of(co.ref(Tag)) {}
|
||||
|
||||
// Represents a category that can group tasks
|
||||
export class Category extends CoMap {
|
||||
name = co.string;
|
||||
deleted = co.boolean;
|
||||
}
|
||||
|
||||
export class CategoryList extends CoList.Of(co.ref(Category)) {}
|
||||
|
||||
// Represents a single task in the todo app
|
||||
export class Task extends CoMap {
|
||||
title = co.string;
|
||||
description = co.optional.string;
|
||||
dueDate = co.optional.Date;
|
||||
isCompleted = co.boolean;
|
||||
priority = co.literal("Low", "Medium", "High");
|
||||
tags = co.ref(TagList);
|
||||
category = co.optional.ref(Category);
|
||||
deleted = co.boolean;
|
||||
}
|
||||
|
||||
export class TaskList extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
// Container for organizing tasks, categories and tags
|
||||
export class Container extends CoMap {
|
||||
tasks = co.ref(TaskList);
|
||||
categories = co.ref(CategoryList);
|
||||
tags = co.ref(TagList);
|
||||
}
|
||||
|
||||
// Root structure holding all data
|
||||
export class AccountRoot extends CoMap {
|
||||
container = co.ref(Container);
|
||||
version = co.optional.number;
|
||||
}
|
||||
|
||||
export class UserProfile extends Profile {
|
||||
name = co.string;
|
||||
|
||||
static validate(data: { name?: string; other?: Record<string, unknown> }) {
|
||||
const errors: string[] = [];
|
||||
if (!data.name?.trim()) {
|
||||
errors.push("Please enter a name.");
|
||||
}
|
||||
return { errors };
|
||||
}
|
||||
}
|
||||
|
||||
// Main account class that handles data initialization
|
||||
export class JazzAccount extends Account {
|
||||
profile = co.ref(UserProfile);
|
||||
root = co.ref(AccountRoot);
|
||||
|
||||
async migrate(creationProps?: {
|
||||
name: string;
|
||||
other?: Record<string, unknown>;
|
||||
}) {
|
||||
if (!this._refs.root && creationProps) {
|
||||
await this.initialMigration(creationProps);
|
||||
return;
|
||||
}
|
||||
|
||||
// uncomment this to add migrations
|
||||
// const currentVersion = this.root?.version || 0;
|
||||
// if (currentVersion < 1) {
|
||||
// await this.migrationV1();
|
||||
// }
|
||||
// if (currentVersion < 2) {
|
||||
// await this.migrationV2();
|
||||
// }
|
||||
}
|
||||
|
||||
private async initialMigration(
|
||||
creationProps: {
|
||||
name: string;
|
||||
other?: Record<string, unknown>;
|
||||
}
|
||||
) {
|
||||
const { name, other } = creationProps;
|
||||
const profileErrors = UserProfile.validate({ name, ...other });
|
||||
if (profileErrors.errors.length > 0) {
|
||||
throw new Error(
|
||||
"Invalid profile data: " + profileErrors.errors.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember("everyone", "reader");
|
||||
|
||||
this.profile = UserProfile.create(
|
||||
{
|
||||
name,
|
||||
...other,
|
||||
},
|
||||
{ owner: publicGroup },
|
||||
);
|
||||
|
||||
const privateGroup = Group.create({ owner: this });
|
||||
|
||||
// Create default container with empty lists
|
||||
const defaultContainer = Container.create(
|
||||
{
|
||||
tasks: TaskList.create([], privateGroup),
|
||||
categories: CategoryList.create([], privateGroup),
|
||||
tags: TagList.create([], privateGroup),
|
||||
},
|
||||
privateGroup,
|
||||
);
|
||||
|
||||
// Initialize root structure with version
|
||||
this.root = AccountRoot.create({
|
||||
container: defaultContainer,
|
||||
version: 0, // Set initial version
|
||||
// here owner is always "this" Account
|
||||
}, { owner: this });
|
||||
}
|
||||
|
||||
// uncomment this to add migrations
|
||||
// private async migrationV1() {
|
||||
// if (this.root) {
|
||||
// // Add migration logic here
|
||||
// this.root.version = 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async migrationV2() {
|
||||
// if (this.root) {
|
||||
// // Future migration logic here
|
||||
// this.root.version = 2;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
```
|
||||
6
packages/cursor-docs/package.json
Normal file
6
packages/cursor-docs/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "cursor-docs",
|
||||
"license": "MIT",
|
||||
"version": "0.0.1",
|
||||
"scripts": {}
|
||||
}
|
||||
@@ -1,5 +1,23 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.10.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [834203f]
|
||||
- jazz-browser@0.10.9
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [153dc99]
|
||||
- Updated dependencies [1e87fc7]
|
||||
- Updated dependencies [2fb6428]
|
||||
- cojson@0.10.8
|
||||
- jazz-browser@0.10.8
|
||||
- jazz-tools@0.10.8
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "jazz-auth-clerk",
|
||||
"version": "0.10.7",
|
||||
"version": "0.10.9",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.10.7",
|
||||
"jazz-browser": "workspace:0.10.7",
|
||||
"jazz-tools": "workspace:0.10.7"
|
||||
"cojson": "workspace:0.10.8",
|
||||
"jazz-browser": "workspace:0.10.9",
|
||||
"jazz-tools": "workspace:0.10.8"
|
||||
},
|
||||
"scripts": {
|
||||
"format-and-lint": "biome check .",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.10.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [834203f]
|
||||
- jazz-browser@0.10.9
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1e87fc7]
|
||||
- Updated dependencies [2fb6428]
|
||||
- jazz-browser@0.10.8
|
||||
- jazz-tools@0.10.8
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-browser-media-images",
|
||||
"version": "0.10.7",
|
||||
"version": "0.10.9",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
@@ -8,8 +8,8 @@
|
||||
"dependencies": {
|
||||
"@types/image-blob-reduce": "^4.1.1",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"jazz-browser": "workspace:0.10.7",
|
||||
"jazz-tools": "workspace:0.10.7",
|
||||
"jazz-browser": "workspace:0.10.9",
|
||||
"jazz-tools": "workspace:0.10.8",
|
||||
"pica": "^9.0.1",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# jazz-browser
|
||||
|
||||
## 0.10.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 834203f: Fix the attachment setting to restore the faceid login
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1e87fc7: Support external authenticators on passkey auth
|
||||
- Updated dependencies [153dc99]
|
||||
- Updated dependencies [2fb6428]
|
||||
- cojson@0.10.8
|
||||
- jazz-tools@0.10.8
|
||||
- cojson-storage-indexeddb@0.10.8
|
||||
- cojson-transport-ws@0.10.8
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.10.7",
|
||||
"version": "0.10.9",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -128,11 +128,16 @@ export class BrowserPasskeyAuth {
|
||||
name: username + ` (${new Date().toLocaleString()})`,
|
||||
displayName: username,
|
||||
},
|
||||
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
||||
pubKeyCredParams: [
|
||||
{ alg: -7, type: "public-key" },
|
||||
{ alg: -8, type: "public-key" },
|
||||
{ alg: -37, type: "public-key" },
|
||||
{ alg: -257, type: "public-key" },
|
||||
],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: "platform",
|
||||
requireResidentKey: true,
|
||||
residentKey: "required",
|
||||
userVerification: "preferred",
|
||||
},
|
||||
timeout: 60000,
|
||||
attestation: "direct",
|
||||
@@ -151,7 +156,9 @@ export class BrowserPasskeyAuth {
|
||||
rpId: this.appHostname,
|
||||
allowCredentials: [],
|
||||
timeout: 60000,
|
||||
userVerification: "preferred",
|
||||
},
|
||||
mediation: "optional",
|
||||
});
|
||||
|
||||
return value as
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# jazz-inspector
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [153dc99]
|
||||
- Updated dependencies [2fb6428]
|
||||
- cojson@0.10.8
|
||||
- jazz-tools@0.10.8
|
||||
- jazz-react-core@0.10.8
|
||||
|
||||
## 0.10.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user