Compare commits
10 Commits
jazz-react
...
cursor-doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa6c147c8 | ||
|
|
4da51e8f9c | ||
|
|
a2076b179b | ||
|
|
3fa276c18d | ||
|
|
1928519d39 | ||
|
|
f4fa80b782 | ||
|
|
782df5d4b8 | ||
|
|
9db20ad630 | ||
|
|
3405d8f275 | ||
|
|
0a64dca0cd |
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.74",
|
||||
"version": "1.0.75",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3405d8f]
|
||||
- jazz-react-native@0.10.10
|
||||
|
||||
## 1.0.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.70",
|
||||
"version": "1.0.71",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
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
|
||||
7
packages/cursor-docs/CHANGELOG.md
Normal file
7
packages/cursor-docs/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# cursor-docs
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3fa276c: Added Cursor docs
|
||||
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.2",
|
||||
"scripts": {}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
# jazz-react-native-auth-clerk
|
||||
|
||||
## 0.10.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3405d8f]
|
||||
- jazz-react-native@0.10.10
|
||||
|
||||
## 0.10.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-react-native-auth-clerk",
|
||||
"version": "0.10.9",
|
||||
"version": "0.10.10",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.tsx",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# jazz-browser
|
||||
|
||||
## 0.10.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3405d8f: read provided kvStore instead of falling back to the default in-memory store
|
||||
|
||||
## 0.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-react-native",
|
||||
"version": "0.10.8",
|
||||
"version": "0.10.10",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createJazzReactNativeContext,
|
||||
createJazzReactNativeGuestContext,
|
||||
} from "./platform.js";
|
||||
import { KvStoreContext } from "./storage/kv-store-context.js";
|
||||
|
||||
export type JazzContextManagerProps<Acc extends Account> = {
|
||||
guestMode?: boolean;
|
||||
@@ -59,6 +60,10 @@ export class ReactNativeContextManager<
|
||||
await this.updateContext(props, currentContext, authProps);
|
||||
}
|
||||
|
||||
getKvStore(): KvStore {
|
||||
return KvStoreContext.getInstance().getStorage();
|
||||
}
|
||||
|
||||
propsChanged(props: JazzContextManagerProps<Acc>) {
|
||||
if (!this.props) {
|
||||
return true;
|
||||
|
||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -1544,6 +1544,8 @@ importers:
|
||||
specifier: ~5.6.2
|
||||
version: 5.6.3
|
||||
|
||||
packages/cursor-docs: {}
|
||||
|
||||
packages/hash-slash:
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
|
||||
Reference in New Issue
Block a user