Compare commits
25 Commits
jazz-inspe
...
feat/1206-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69ea14c513 | ||
|
|
05d6f36f30 | ||
|
|
02541d3f48 | ||
|
|
ac474c4afb | ||
|
|
afe8e1f3b2 | ||
|
|
b12b3808fa | ||
|
|
08ec8fe709 | ||
|
|
f70cae6bf6 | ||
|
|
59fe373863 | ||
|
|
be58b4c1d8 | ||
|
|
96c520ae4d | ||
|
|
a9553b4945 | ||
|
|
fa516522e3 | ||
|
|
59e4b5da54 | ||
|
|
2fa3f94b4a | ||
|
|
a2f8461e26 | ||
|
|
cabaf079be | ||
|
|
d8de4a7ada | ||
|
|
e510a544b7 | ||
|
|
b2ee30630d | ||
|
|
ab0d4f364a | ||
|
|
6d9c6ae698 | ||
|
|
914af3deae | ||
|
|
4ad8e9ae78 | ||
|
|
1e0b496555 |
@@ -1,5 +1,12 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [b2ee306]
|
||||
- jazz-svelte@0.14.3
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.85",
|
||||
"version": "0.0.86",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,46 +1,29 @@
|
||||
import { Account, CoList, CoMap, FileStream, Profile, coField, Group } from 'jazz-tools';
|
||||
import { FileStream, Group, co, z } from 'jazz-tools';
|
||||
|
||||
export class SharedFile extends CoMap {
|
||||
name = coField.string;
|
||||
file = coField.ref(FileStream);
|
||||
createdAt = coField.Date;
|
||||
uploadedAt = coField.Date;
|
||||
size = coField.number;
|
||||
}
|
||||
export const SharedFile = co.map({
|
||||
name: z.string(),
|
||||
file: FileStream,
|
||||
createdAt: z.date(),
|
||||
uploadedAt: z.date(),
|
||||
size: z.number(),
|
||||
});
|
||||
|
||||
export class FileShareProfile extends Profile {
|
||||
name = coField.string;
|
||||
}
|
||||
export const FileShareAccountRoot = co.map({
|
||||
type: z.literal('file-share-account'),
|
||||
sharedFiles: co.list(SharedFile),
|
||||
})
|
||||
|
||||
export class ListOfSharedFiles extends CoList.Of(coField.ref(SharedFile)) {}
|
||||
export const FileShareAccount = co.account({
|
||||
profile: co.profile(),
|
||||
root: FileShareAccountRoot,
|
||||
}).withMigration((account) => {
|
||||
if (account.root === undefined) {
|
||||
const publicGroup = Group.create({ owner: account });
|
||||
publicGroup.addMember('everyone', 'reader');
|
||||
|
||||
export class FileShareAccountRoot extends CoMap {
|
||||
type = coField.literal('file-share-account');
|
||||
sharedFiles = coField.ref(ListOfSharedFiles);
|
||||
}
|
||||
|
||||
export class FileShareAccount extends Account {
|
||||
profile = coField.ref(FileShareProfile);
|
||||
root = coField.ref(FileShareAccountRoot);
|
||||
|
||||
/** The account migration is run on account creation and on every log-in.
|
||||
* You can use it to set up the account root and any other initial CoValues you need.
|
||||
*/
|
||||
async migrate() {
|
||||
await this._refs.root?.load();
|
||||
|
||||
// Initialize root if it doesn't exist
|
||||
if (this.root === undefined || this.root?.type !== 'file-share-account') {
|
||||
// Create a group that will own all shared files
|
||||
const publicGroup = Group.create({ owner: this });
|
||||
publicGroup.addMember('everyone', 'reader');
|
||||
|
||||
this.root = FileShareAccountRoot.create(
|
||||
{
|
||||
type: 'file-share-account',
|
||||
sharedFiles: ListOfSharedFiles.create([], { owner: publicGroup }),
|
||||
},
|
||||
);
|
||||
}
|
||||
account.root = FileShareAccountRoot.create({
|
||||
type: 'file-share-account',
|
||||
sharedFiles: co.list(SharedFile).create([], publicGroup),
|
||||
}, publicGroup);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" module>
|
||||
declare module 'jazz-svelte' {
|
||||
interface Register {
|
||||
Account: FileShareAccount;
|
||||
Account: typeof FileShareAccount;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { AccountCoState } from 'jazz-svelte';
|
||||
import { SharedFile } from '$lib/schema';
|
||||
import { FileStream } from 'jazz-tools';
|
||||
import { SharedFile, FileShareAccount } from '$lib/schema';
|
||||
import { FileStream, type Loaded } from 'jazz-tools';
|
||||
import FileItem from '$lib/components/FileItem.svelte';
|
||||
import { CloudUpload } from 'lucide-svelte';
|
||||
|
||||
const me = new AccountCoState({
|
||||
const me = new AccountCoState(FileShareAccount, {
|
||||
resolve: {
|
||||
profile: true,
|
||||
root: {
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile(file: SharedFile) {
|
||||
async function deleteFile(file: Loaded<typeof SharedFile>) {
|
||||
if (!sharedFiles) return;
|
||||
|
||||
const index = sharedFiles.indexOf(file);
|
||||
|
||||
@@ -3,17 +3,16 @@
|
||||
import { CoState } from 'jazz-svelte';
|
||||
import { SharedFile } from '$lib/schema';
|
||||
import { File, FileDown, Link2 } from 'lucide-svelte';
|
||||
import type { ID } from 'jazz-tools';
|
||||
import { FileStream } from 'jazz-tools';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { downloadFileBlob } from '$lib/utils';
|
||||
|
||||
const fileId = $page.params.fileId;
|
||||
|
||||
const file = $derived(new CoState(SharedFile, fileId as ID<SharedFile>));
|
||||
const file = $derived(new CoState(SharedFile, fileId ));
|
||||
const isAdmin = $derived(file.current?._owner?.myRole() === 'admin');
|
||||
|
||||
const fileStreamId = $derived(file.current?._refs.file?.id);
|
||||
const fileStreamId = $derived(file.current?._refs?.file?.id);
|
||||
|
||||
async function downloadFile() {
|
||||
if (!fileStreamId || !file.current) {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [b2ee306]
|
||||
- jazz-svelte@0.14.3
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.89",
|
||||
"version": "0.0.90",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Jazz Documentation: Actionable Task List
|
||||
|
||||
## Immediate Priority (Next 1-2 Weeks)
|
||||
|
||||
1. **Complete Link Audit**
|
||||
|
||||
- [x] Verify all links in Building with Jazz section
|
||||
- [ ] Replace generic links with section-specific links
|
||||
- [ ] Add backlinks from reference docs to concept docs
|
||||
|
||||
2. **Standardize Documentation Structure**
|
||||
|
||||
- [x] Create consistent format for "Further Reading" sections in all docs
|
||||
- [x] Standardize intro paragraphs to explain document purpose
|
||||
- [x] Ensure all Next Steps sections correctly link to next document
|
||||
|
||||
3. **Resolve Authentication Content Duplication**
|
||||
- [x] Decide approach (remove conceptual sections, cross-reference, or reorganize)
|
||||
- [x] Implement chosen approach
|
||||
- [x] Add appropriate links between documents
|
||||
|
||||
## High Priority (Next 3-4 Weeks)
|
||||
|
||||
4. **Create New Technical Reference Docs**
|
||||
|
||||
- [ ] Sync Server Configuration Guide
|
||||
- [ ] Group Patterns Cookbook
|
||||
- [ ] Data Loading Patterns Guide
|
||||
|
||||
5. **Develop Documentation Style Guide**
|
||||
|
||||
- [ ] Define standards for explanation vs reference content
|
||||
- [ ] Create terminology guide for consistent language
|
||||
- [ ] Establish code example formatting standards
|
||||
|
||||
6. **Technical Validation**
|
||||
- [ ] Review all code examples for correctness
|
||||
- [ ] Verify technical accuracy of explanations
|
||||
- [ ] Test code examples in real applications
|
||||
|
||||
## Medium Priority (Next 1-2 Months)
|
||||
|
||||
7. **Enhance Visual Elements**
|
||||
|
||||
- [ ] Create component relationship diagrams
|
||||
- [ ] Add flow charts for authentication and data processes
|
||||
- [ ] Design visual navigation aids
|
||||
|
||||
8. **Improve Discoverability**
|
||||
|
||||
- [ ] Review information architecture
|
||||
- [ ] Optimize section organization
|
||||
- [ ] Add cross-linking between related concepts
|
||||
|
||||
9. **Create Quick Reference Materials**
|
||||
- [ ] Develop downloadable cheat sheets
|
||||
- [ ] Create concept summaries
|
||||
- [ ] Build a glossary of terms
|
||||
|
||||
## Future Enhancements (2+ Months)
|
||||
|
||||
10. **Implement Analytics and Feedback**
|
||||
|
||||
- [ ] Set up documentation usage tracking
|
||||
- [ ] Add feedback collection mechanism
|
||||
- [ ] Create process for incorporating feedback
|
||||
|
||||
11. **Develop Interactive Elements**
|
||||
|
||||
- [ ] Create interactive code examples
|
||||
- [ ] Build mini-tutorials within documentation
|
||||
- [ ] Add "try it yourself" sections
|
||||
|
||||
12. **Add Real-World Context**
|
||||
- [ ] Develop case studies
|
||||
- [ ] Include real application examples
|
||||
- [ ] Add decision guides for architecture choices
|
||||
|
||||
Each task should be assigned an owner and delivery date when work begins.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Technical Content for Reference Documentation
|
||||
|
||||
This document tracks content that was removed from the explanation-focused "Building with Jazz" section but should be preserved for technical reference documentation.
|
||||
|
||||
## Schemas
|
||||
|
||||
### Removed Content
|
||||
- Detailed information about specific CoValue types
|
||||
- In-depth explanation of field types and references
|
||||
- Code examples showing how to create and manipulate schema instances
|
||||
- Information about SchemaUnions and their usage
|
||||
- Examples of computed fields and methods
|
||||
- Specific details about optional fields and refs
|
||||
|
||||
### Source Location
|
||||
Original content from: `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/schemas/covalues.mdx`
|
||||
|
||||
## Providers
|
||||
|
||||
### Removed Content
|
||||
- Detailed provider configuration options:
|
||||
```tsx
|
||||
// Configure how your app connects to the Jazz network
|
||||
const syncConfig = {
|
||||
// Connect to Jazz Cloud (or your own sync server)
|
||||
peer: "wss://cloud.jazz.tools/?key=your-api-key",
|
||||
|
||||
// When to sync: "always" (default), "never", or "signedUp"
|
||||
when: "always"
|
||||
}
|
||||
```
|
||||
- Account Schema specific configuration details
|
||||
- Section on provider options including:
|
||||
- Guest mode configuration
|
||||
- Default profile name setting
|
||||
- Logout handling
|
||||
- Anonymous account data handling
|
||||
- References to authentication states and how they affect synchronization
|
||||
|
||||
### Source Location
|
||||
Original content from: `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/project-setup/providers/react.mdx`
|
||||
|
||||
## Authentication
|
||||
|
||||
### Removed Content
|
||||
- Detailed code examples showing implementation of authentication state detection
|
||||
- Specific code for migrating data from anonymous to authenticated accounts
|
||||
- Configuration code examples for providers related to authentication
|
||||
- Implementation details for controlling sync in different authentication states
|
||||
- Code examples for guest mode configuration
|
||||
|
||||
### Source Location
|
||||
Original content from: `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/authentication/overview.mdx` and `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/authentication/authentication-states.mdx`
|
||||
|
||||
Most of the conceptual content about authentication states was borrowed from authentication-states.mdx. Options for handling this duplication:
|
||||
|
||||
1. **Remove the conceptual sections**: Remove overlapping conceptual content from authentication-states.mdx, leaving only implementation details and code examples. Add a prominent link at the top directing users to the explanation document for conceptual understanding.
|
||||
|
||||
2. **Keep both but cross-reference**: Keep the original content but add clear cross-references between the documents, with explanation document for concepts and the original for implementation details.
|
||||
|
||||
3. **Complete reorganization**: Completely reorganize authentication-states.mdx to focus solely on implementation, moving all conceptual content to the explanation document.
|
||||
|
||||
For now, we're maintaining both documents but should decide on an approach when finalizing the documentation structure.
|
||||
|
||||
## Groups and Ownership
|
||||
|
||||
### Removed Content
|
||||
- Detailed implementation code examples for adding members to groups
|
||||
- Code examples for public sharing and invite links
|
||||
- Common Group Patterns section with examples for:
|
||||
- Organization Structure
|
||||
- Project Collaboration
|
||||
- Public Community
|
||||
|
||||
Note: The Common Group Patterns section contained valuable examples showing real-world usage patterns. This content should be preserved and potentially added to a patterns guide, cookbook, or examples section in the reference documentation.
|
||||
|
||||
### Source Location
|
||||
Original content from: `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/groups/intro.mdx`, `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/groups/sharing.mdx`, and `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/groups/inheritance.mdx`
|
||||
|
||||
## Sync and Storage
|
||||
|
||||
### Removed Content
|
||||
- Detailed instructions for using Jazz Cloud with API key examples
|
||||
- Command line instructions for running a self-hosted sync server
|
||||
- Command line options and parameters for configuring a sync server
|
||||
- References to the source code and GitHub repositories
|
||||
- Configuring Sync in Your Application section with code examples
|
||||
- Offline-First Approach section explaining offline capabilities
|
||||
- Sync and Authentication section explaining how sync relates to authentication states
|
||||
|
||||
### Source Location
|
||||
Original content from: `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/sync-and-storage.mdx`
|
||||
|
||||
### Technical Documentation Needed
|
||||
We should create a detailed technical reference document for sync server configuration that covers:
|
||||
- Complete configuration options for self-hosted sync servers
|
||||
- Performance tuning parameters
|
||||
- Security considerations
|
||||
- Deployment scenarios and best practices
|
||||
- Monitoring and maintenance
|
||||
|
||||
This would be valuable for users who need to self-host their sync server with specific configuration requirements.
|
||||
|
||||
## Server Workers
|
||||
|
||||
### Removed Content
|
||||
- Code example for generating Server Worker credentials
|
||||
- Instructions for storing credentials as environment variables
|
||||
- Code example for starting a Server Worker
|
||||
- Implementation details for loading/subscribing to CoValues
|
||||
- Specific implementation patterns with code examples
|
||||
|
||||
### Source Location
|
||||
Original content from: `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/project-setup/server-side.mdx` and `/Users/benjamin/projects/gcmp/jazz/homepage/homepage/content/docs/building-with-jazz/server-workers.mdx`
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Missing Topics to Address
|
||||
- ~~**Data Loading and Subscriptions**~~: Rather than creating a separate document, we've incorporated this topic into existing documents:
|
||||
- Added a substantial section about subscriptions and deep loading to the **Providers** document
|
||||
- Added an explanation of accessing shared data to the **Groups** document
|
||||
- Added links to the comprehensive [Subscription and Loading](/docs/react/using-covalues/subscription-and-loading) documentation in both documents
|
||||
|
||||
This approach ensures that data loading concepts are covered in relevant contexts rather than isolated in a separate document.
|
||||
|
||||
We've updated all Next Steps sections to reflect the current document flow:
|
||||
1. Installation → Schemas
|
||||
2. Schemas → Providers
|
||||
3. Providers → Accounts
|
||||
4. Accounts → Authentication
|
||||
5. Authentication → Groups
|
||||
6. Groups → Sync
|
||||
7. Sync → Server Workers
|
||||
8. Server Workers → (end of journey)
|
||||
|
||||
As we continue creating explanation-focused content for the "Building with Jazz" section, we should add to this document to ensure all technical reference material is preserved for the appropriate documentation.
|
||||
@@ -20,23 +20,21 @@ For this example, users within an `Organization` will be sharing `Project`s.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, CoMap, CoList } from "jazz-tools";
|
||||
import { co, z } from "jazz-tools";
|
||||
// ---cut---
|
||||
// schema.ts
|
||||
export class Project extends CoMap {
|
||||
name = coField.string;
|
||||
}
|
||||
export const Project = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export class ListOfProjects extends CoList.Of(coField.ref(Project)) {}
|
||||
|
||||
export class Organization extends CoMap {
|
||||
name = coField.string;
|
||||
export const Organization = co.map({
|
||||
name: z.string(),
|
||||
|
||||
// shared data between users of each organization
|
||||
projects = coField.ref(ListOfProjects);
|
||||
}
|
||||
projects: co.list(Project),
|
||||
});
|
||||
|
||||
export class ListOfOrganizations extends CoList.Of(coField.ref(Organization)) {}
|
||||
export const ListOfOrganizations = co.list(Organization);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -48,54 +46,47 @@ Let's add the list of `Organization`s to the user's Account `root` so they can a
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, CoMap, CoList, Account, Group } from "jazz-tools";
|
||||
export class Project extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
import { Group, co, z } from "jazz-tools";
|
||||
export const Project = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export class ListOfProjects extends CoList.Of(co.ref(Project)) {}
|
||||
export const Organization = co.map({
|
||||
name: z.string(),
|
||||
|
||||
export class Organization extends CoMap {
|
||||
name = co.string;
|
||||
// shared data between users of each organization
|
||||
projects: co.list(Project),
|
||||
});
|
||||
|
||||
// shared data between users of each organization
|
||||
projects = co.ref(ListOfProjects);
|
||||
}
|
||||
|
||||
export class ListOfOrganizations extends CoList.Of(co.ref(Organization)) {}
|
||||
// ---cut---
|
||||
// schema.ts
|
||||
export class JazzAccountRoot extends CoMap {
|
||||
organizations = coField.ref(ListOfOrganizations);
|
||||
}
|
||||
export const JazzAccountRoot = co.map({
|
||||
organizations: co.list(Organization),
|
||||
});
|
||||
|
||||
export class JazzAccount extends Account {
|
||||
root = coField.ref(JazzAccountRoot);
|
||||
|
||||
async migrate() {
|
||||
if (this.root === undefined) {
|
||||
export const JazzAccount = co
|
||||
.account({
|
||||
root: JazzAccountRoot,
|
||||
profile: co.profile({}),
|
||||
})
|
||||
.withMigration((account) => {
|
||||
if (account.root === undefined) {
|
||||
// Using a Group as an owner allows you to give access to other users
|
||||
const organizationGroup = Group.create();
|
||||
|
||||
const organizations = ListOfOrganizations.create(
|
||||
[
|
||||
// Create the first Organization so users can start right away
|
||||
Organization.create(
|
||||
{
|
||||
name: "My organization",
|
||||
projects: ListOfProjects.create([], organizationGroup),
|
||||
},
|
||||
organizationGroup,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
this.root = JazzAccountRoot.create(
|
||||
{ organizations },
|
||||
);
|
||||
const organizations = co.list(Organization).create([
|
||||
// Create the first Organization so users can start right away
|
||||
Organization.create(
|
||||
{
|
||||
name: "My organization",
|
||||
projects: co.list(Project).create([], organizationGroup),
|
||||
},
|
||||
organizationGroup,
|
||||
),
|
||||
]);
|
||||
account.root = JazzAccountRoot.create({ organizations });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -120,44 +111,35 @@ When the user accepts the invite, add the `Organization` to the user's `organiza
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
import { useAcceptInvite, useAccount } from "jazz-react";
|
||||
import { ID } from "jazz-tools";
|
||||
import { Account, CoList, CoMap, Group, co } from "jazz-tools";
|
||||
import * as React from "react";
|
||||
import { useAcceptInvite, useAccount } from "jazz-react";
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
export class Project extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
const Project = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export class ListOfProjects extends CoList.Of(co.ref(Project)) {}
|
||||
const Organization = co.map({
|
||||
name: z.string(),
|
||||
projects: co.list(Project),
|
||||
});
|
||||
|
||||
export class Organization extends CoMap {
|
||||
name = co.string;
|
||||
projects = co.ref(ListOfProjects);
|
||||
}
|
||||
const JazzAccountRoot = co.map({
|
||||
organizations: co.list(Organization),
|
||||
});
|
||||
|
||||
export class ListOfOrganizations extends CoList.Of(co.ref(Organization)) {}
|
||||
const JazzAccount = co.account({
|
||||
root: JazzAccountRoot,
|
||||
profile: co.profile({}),
|
||||
});
|
||||
|
||||
export class JazzAccountRoot extends CoMap {
|
||||
organizations = co.ref(ListOfOrganizations);
|
||||
}
|
||||
|
||||
export class JazzAccount extends Account {
|
||||
root = co.ref(JazzAccountRoot);
|
||||
}
|
||||
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
Account: JazzAccount;
|
||||
}
|
||||
}
|
||||
// ---cut---
|
||||
export function AcceptInvitePage() {
|
||||
const { me } = useAccount({
|
||||
const { me } = useAccount(JazzAccount, {
|
||||
resolve: { root: { organizations: { $each: { $onError: null } } } },
|
||||
});
|
||||
|
||||
const onAccept = (organizationId: ID<Organization>) => {
|
||||
const onAccept = (organizationId: string) => {
|
||||
if (me) {
|
||||
Organization.load(organizationId).then((organization) => {
|
||||
if (organization) {
|
||||
|
||||
@@ -133,14 +133,14 @@ export const docNavigationItems = [
|
||||
// done: 100,
|
||||
// framework: "react-native",
|
||||
// },
|
||||
{
|
||||
// upgrade guides
|
||||
name: "0.9.2 - Local persistence on React Native Expo",
|
||||
href: "/docs/upgrade/react-native-local-persistence",
|
||||
done: 100,
|
||||
framework: "react-native-expo",
|
||||
excludeFromNavigation: true,
|
||||
},
|
||||
// {
|
||||
// // upgrade guides
|
||||
// name: "0.9.2 - Local persistence on React Native Expo",
|
||||
// href: "/docs/upgrade/react-native-local-persistence",
|
||||
// done: 100,
|
||||
// framework: "react-native-expo",
|
||||
// excludeFromNavigation: true,
|
||||
// },
|
||||
// {
|
||||
// // upgrade guides
|
||||
// name: "0.9.0 - Top level imports",
|
||||
|
||||
@@ -15,7 +15,9 @@ When a group extends another group, members of the parent group will become auto
|
||||
Here's how to extend a group:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
// ---cut---
|
||||
const playlistGroup = Group.create();
|
||||
const trackGroup = Group.create();
|
||||
|
||||
@@ -34,7 +36,11 @@ When you extend a group:
|
||||
In some cases you might want to inherit all members from a parent group but override/flatten their roles to the same specific role in the child group. You can do so by passing an "override role" as a second argument to `extend`:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const bob = await createJazzTestAccount();
|
||||
// ---cut---
|
||||
const organizationGroup = Group.create();
|
||||
organizationGroup.addMember(bob, "admin");
|
||||
|
||||
@@ -48,7 +54,12 @@ billingGroup.extend(organizationGroup, "reader");
|
||||
The "override role" works in both directions:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const bob = await createJazzTestAccount();
|
||||
const alice = await createJazzTestAccount();
|
||||
// ---cut---
|
||||
const parentGroup = Group.create();
|
||||
parentGroup.addMember(bob, "reader");
|
||||
parentGroup.addMember(alice, "admin");
|
||||
@@ -65,7 +76,9 @@ childGroup.extend(parentGroup, "writer");
|
||||
Groups can be extended multiple levels deep:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
// ---cut---
|
||||
const grandParentGroup = Group.create();
|
||||
const parentGroup = Group.create();
|
||||
const childGroup = Group.create();
|
||||
@@ -82,7 +95,12 @@ Members of the grandparent group will get access to all descendant groups based
|
||||
When you remove a member from a parent group, they automatically lose access to all child groups. We handle key rotation automatically to ensure security.
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const bob = await createJazzTestAccount();
|
||||
const parentGroup = Group.create();
|
||||
// ---cut---
|
||||
// Remove member from parent
|
||||
await parentGroup.removeMember(bob);
|
||||
|
||||
@@ -94,7 +112,11 @@ await parentGroup.removeMember(bob);
|
||||
|
||||
If the account is already a member of the child group, it will get the more permissive role:
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const bob = await createJazzTestAccount();
|
||||
// ---cut---
|
||||
const parentGroup = Group.create();
|
||||
parentGroup.addMember(bob, "reader");
|
||||
|
||||
@@ -109,7 +131,11 @@ childGroup.extend(parentGroup);
|
||||
|
||||
When extending groups, only admin, writer and reader roles are inherited:
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const bob = await createJazzTestAccount();
|
||||
// ---cut---
|
||||
const parentGroup = Group.create();
|
||||
parentGroup.addMember(bob, "writeOnly");
|
||||
|
||||
@@ -126,7 +152,13 @@ To extend a group:
|
||||
2. The current account must be a member of the parent group
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group, co, z } from "jazz-tools";
|
||||
const Company = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
const company = Company.create({ name: "Garden Computing" });
|
||||
// ---cut---
|
||||
const companyGroup = company._owner.castAs(Group)
|
||||
const teamGroup = Group.create();
|
||||
|
||||
@@ -140,7 +172,9 @@ teamGroup.extend(companyGroup);
|
||||
You can revoke a group extension by using the `revokeExtend` method:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
// ---cut---
|
||||
const parentGroup = Group.create();
|
||||
const childGroup = Group.create();
|
||||
|
||||
@@ -156,7 +190,9 @@ await childGroup.revokeExtend(parentGroup);
|
||||
You can get all the parent groups of a group by calling the `getParentGroups` method:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
// ---cut---
|
||||
const childGroup = Group.create();
|
||||
const parentGroup = Group.create();
|
||||
childGroup.extend(parentGroup);
|
||||
@@ -170,7 +206,14 @@ console.log(childGroup.getParentGroups()); // [parentGroup]
|
||||
Here's a practical example of using group inheritance for team permissions:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const CEO = await createJazzTestAccount();
|
||||
const teamLead = await createJazzTestAccount();
|
||||
const developer = await createJazzTestAccount();
|
||||
const client = await createJazzTestAccount();
|
||||
// ---cut---
|
||||
// Company-wide group
|
||||
const companyGroup = Group.create();
|
||||
companyGroup.addMember(CEO, "admin");
|
||||
|
||||
@@ -16,7 +16,7 @@ have different roles, such as "writer", "reader" or "admin".
|
||||
Here's how you can create a `Group`.
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```tsx twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
|
||||
const group = Group.create();
|
||||
@@ -33,33 +33,54 @@ But if you already know their ID, you can add them directly (see below).
|
||||
You can add group members by ID by using `Account.load` and `Group.addMember`.
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```tsx twoslash
|
||||
import { ID } from "jazz-tools";
|
||||
|
||||
const bobsID = "co_z123" as ID<Account>;
|
||||
|
||||
// ---cut---
|
||||
import { Group, Account } from "jazz-tools";
|
||||
|
||||
const group = Group.create();
|
||||
|
||||
const bob = await Account.load(bobsID, []);
|
||||
group.addMember(bob, "writer");
|
||||
const bob = await Account.load(bobsID);
|
||||
|
||||
if (bob) {
|
||||
group.addMember(bob, "writer");
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
**Note:** if the account ID is of type `string`, because it comes from a URL parameter or something similar, you need to cast it to `ID<Account>` first:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```tsx twoslash
|
||||
const bobsID = "co_z123" as ID<Account>;
|
||||
|
||||
const group = Group.create();
|
||||
|
||||
// ---cut---
|
||||
import { Group, Account, ID } from "jazz-tools";
|
||||
|
||||
const bob = await Account.load(bobsID as ID<Account>, []);
|
||||
group.addMember(bob, "writer");
|
||||
const bob = await Account.load(bobsID as ID<Account>);
|
||||
|
||||
if (bob) {
|
||||
group.addMember(bob, "writer");
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Change a member's role
|
||||
## Changing a member's role
|
||||
|
||||
To change a member's role, use the `addMember` method.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, Account, ID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const bob = await createJazzTestAccount();
|
||||
const group = Group.create();
|
||||
// ---cut---
|
||||
group.addMember(bob, "reader");
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -73,7 +94,12 @@ Bob just went from a writer to a reader.
|
||||
To remove a member, use the `removeMember` method.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, Account, ID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const bob = await createJazzTestAccount();
|
||||
const group = Group.create();
|
||||
// ---cut---
|
||||
group.removeMember(bob);
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -89,7 +115,16 @@ Rules:
|
||||
You can get the group of an existing CoValue by using `coValue._owner`.
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```ts twoslash
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
import { co, z } from "jazz-tools";
|
||||
const existingCoValue = await createJazzTestAccount();
|
||||
|
||||
const MyCoMap = co.map({
|
||||
color: z.string(),
|
||||
});
|
||||
|
||||
// ---cut---
|
||||
const group = existingCoValue._owner;
|
||||
const newValue = MyCoMap.create(
|
||||
{ color: "red"},
|
||||
@@ -101,13 +136,27 @@ const newValue = MyCoMap.create(
|
||||
Because `._owner` can be an `Account` or a `Group`, in cases where you specifically need to use `Group` methods (such as for adding members or getting your own role), you can cast it to assert it to be a Group:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```ts twoslash
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
import { co, z } from "jazz-tools";
|
||||
const bob = await createJazzTestAccount();
|
||||
|
||||
const MyCoMap = co.map({
|
||||
color: z.string(),
|
||||
});
|
||||
|
||||
const existingCoValue = MyCoMap.create(
|
||||
{ color: "red"},
|
||||
{ owner: bob }
|
||||
);
|
||||
|
||||
// ---cut---
|
||||
import { Group } from "jazz-tools";
|
||||
|
||||
const group = existingCoValue._owner.castAs(Group);
|
||||
group.addMember(bob, "writer");
|
||||
|
||||
const role = group.getRoleOf(bob);
|
||||
const role = group.getRoleOf(bob.id);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -116,8 +165,14 @@ const role = group.getRoleOf(bob);
|
||||
You can check the permissions of an account on a CoValue by using the `canRead`, `canWrite` and `canAdmin` methods.
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
const value = await MyCoMap.load(valueID, {});
|
||||
```ts twoslash
|
||||
import { co, z, Account } from "jazz-tools";
|
||||
|
||||
const MyCoMap = co.map({
|
||||
color: z.string(),
|
||||
});
|
||||
// ---cut---
|
||||
const value = await MyCoMap.create({ color: "red"})
|
||||
const me = Account.getMe();
|
||||
|
||||
if (me.canAdmin(value)) {
|
||||
@@ -135,18 +190,29 @@ if (me.canAdmin(value)) {
|
||||
To check the permissions of another account, you need to load it first:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
const value = await MyCoMap.load(valueID, {});
|
||||
const bob = await Account.load(accountID, []);
|
||||
```ts twoslash
|
||||
import { co, z, Account } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
|
||||
if (bob.canAdmin(value)) {
|
||||
console.log("Bob can share value with others");
|
||||
} else if (bob.canWrite(value)) {
|
||||
console.log("Bob can edit value");
|
||||
} else if (bob.canRead(value)) {
|
||||
console.log("Bob can view value");
|
||||
} else {
|
||||
console.log("Bob cannot access value");
|
||||
const MyCoMap = co.map({
|
||||
color: z.string(),
|
||||
});
|
||||
const account = await createJazzTestAccount();
|
||||
const accountID = account.id;
|
||||
// ---cut---
|
||||
const value = await MyCoMap.create({ color: "red"})
|
||||
const bob = await Account.load(accountID);
|
||||
|
||||
if (bob) {
|
||||
if (bob.canAdmin(value)) {
|
||||
console.log("Bob can share value with others");
|
||||
} else if (bob.canWrite(value)) {
|
||||
console.log("Bob can edit value");
|
||||
} else if (bob.canRead(value)) {
|
||||
console.log("Bob can view value");
|
||||
} else {
|
||||
console.log("Bob cannot access value");
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -12,10 +12,12 @@ You can share CoValues publicly by setting the `owner` to a `Group`, and grantin
|
||||
access to "everyone".
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const group = Group.create();
|
||||
group.addMember("everyone", "writer"); // *highlight*
|
||||
```
|
||||
```ts twoslash
|
||||
import { Group } from "jazz-tools";
|
||||
// ---cut---
|
||||
const group = Group.create();
|
||||
group.addMember("everyone", "writer");
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
This is done in the [chat example](https://github.com/garden-co/jazz/tree/main/examples/chat) where anyone can join the chat, and send messages.
|
||||
@@ -29,52 +31,19 @@ You can grant users access to a CoValue by sending them an invite link.
|
||||
This is used in the [pet example](https://github.com/garden-co/jazz/tree/main/examples/pets)
|
||||
and the [todo example](https://github.com/garden-co/jazz/tree/main/examples/todo).
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
<ContentByFramework framework={["react", "react-native", "react-native-expo", "vue", "svelte"]}>
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const Organization = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
const organization = Organization.create({ name: "Garden Computing" });
|
||||
// ---cut---
|
||||
import { createInviteLink } from "jazz-react";
|
||||
|
||||
createInviteLink(organization, "writer"); // or reader, or admin
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="react-native">
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { createInviteLink } from "jazz-react-native";
|
||||
|
||||
createInviteLink(organization, "writer"); // or reader, or admin
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="react-native-expo">
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { createInviteLink } from "jazz-expo";
|
||||
|
||||
createInviteLink(organization, "writer"); // or reader, or admin
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="vue">
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { createInviteLink } from "jazz-vue";
|
||||
|
||||
createInviteLink(organization, "writer"); // or reader, or admin
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="svelte">
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { createInviteLink } from "jazz-browser";
|
||||
|
||||
createInviteLink(organization, "writer"); // or reader, or admin
|
||||
createInviteLink(organization, "writer"); // or reader, admin, writeOnly
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
@@ -85,10 +54,23 @@ In your app, you need to handle this route, and let the user accept the invitati
|
||||
as done [here](https://github.com/garden-co/jazz/tree/main/examples/pets/src/2_main.tsx).
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const Organization = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
const organization = Organization.create({ name: "Garden Computing" });
|
||||
const organizationID = organization.id;
|
||||
// ---cut---
|
||||
import { useAcceptInvite } from "jazz-react";
|
||||
|
||||
useAcceptInvite({
|
||||
invitedObjectSchema: PetPost,
|
||||
onAccept: (petPostID) => navigate("/pet/" + petPostID),
|
||||
invitedObjectSchema: Organization,
|
||||
onAccept: (organizationID) => {
|
||||
console.log("Accepted invite!")
|
||||
// navigate to the organization page
|
||||
},
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -104,14 +86,14 @@ Create the data models.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { CoMap, co, CoValue, Account, CoList } from "jazz-tools";
|
||||
// ---cut-before---
|
||||
class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.literal("pending", "approved", "rejected");
|
||||
}
|
||||
import { co, z, Account } from "jazz-tools";
|
||||
// ---cut---
|
||||
const JoinRequest = co.map({
|
||||
account: Account,
|
||||
status: z.literal(["pending", "approved", "rejected"]),
|
||||
});
|
||||
|
||||
class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
const RequestsList = co.list(JoinRequest);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -119,15 +101,15 @@ Set up the request system with appropriate access controls.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { Group, co, CoList, CoMap, Account } from "jazz-tools";
|
||||
import { co, z, Account, Group, Loaded } from "jazz-tools";
|
||||
import { createInviteLink } from "jazz-react";
|
||||
|
||||
export class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.literal("pending", "approved", "rejected");
|
||||
}
|
||||
const JoinRequest = co.map({
|
||||
account: Account,
|
||||
status: z.literal(["pending", "approved", "rejected"]),
|
||||
});
|
||||
|
||||
export class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
const RequestsList = co.list(JoinRequest);
|
||||
|
||||
// ---cut-before---
|
||||
function createRequestsToJoin() {
|
||||
@@ -138,7 +120,7 @@ function createRequestsToJoin() {
|
||||
}
|
||||
|
||||
async function sendJoinRequest(
|
||||
requestsList: RequestsList,
|
||||
requestsList: Loaded<typeof RequestsList>,
|
||||
account: Account,
|
||||
) {
|
||||
const request = JoinRequest.create(
|
||||
@@ -160,23 +142,23 @@ Using the write-only access users can submit requests that only administrators c
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, CoMap, CoList, ID, Account, Group, Resolved } from "jazz-tools";
|
||||
import { co, z, ID, Account, Group, Loaded } from "jazz-tools";
|
||||
|
||||
class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.string; // Can be "pending", "approved", "rejected"
|
||||
}
|
||||
const JoinRequest = co.map({
|
||||
account: Account,
|
||||
status: z.literal(["pending", "approved", "rejected"]),
|
||||
});
|
||||
|
||||
export class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
const RequestsList = co.list(JoinRequest);
|
||||
|
||||
export class RequestsToJoin extends CoMap {
|
||||
writeOnlyInvite = co.string;
|
||||
requests = co.ref(RequestsList);
|
||||
}
|
||||
const RequestsToJoin = co.map({
|
||||
writeOnlyInvite: z.string(),
|
||||
requests: RequestsList,
|
||||
});
|
||||
|
||||
// ---cut-before---
|
||||
async function approveJoinRequest(
|
||||
joinRequest: JoinRequest,
|
||||
joinRequest: Loaded<typeof JoinRequest, { account: true }>,
|
||||
targetGroup: Group,
|
||||
) {
|
||||
const account = await Account.load(joinRequest._refs.account.id);
|
||||
|
||||
@@ -19,7 +19,6 @@ Server Workers typically have static credentials, consisting of a public Account
|
||||
To generate new credentials for a Server Worker, you can run:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```sh
|
||||
npx jazz-run account create --name "My Server Worker"
|
||||
```
|
||||
@@ -43,13 +42,15 @@ You can use `startWorker` from `jazz-nodejs` to start a Server Worker. Similarly
|
||||
`startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere.
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Account } from "jazz-tools";
|
||||
class MyWorkerAccount extends Account {}
|
||||
// ---cut---
|
||||
import { startWorker } from 'jazz-nodejs';
|
||||
|
||||
const { worker } = await startWorker({
|
||||
AccountSchema: MyWorkerAccount,
|
||||
syncServer: 'wss://cloud.jazz.tools/?key=you@example.com',
|
||||
AccountSchema: MyWorkerAccount,
|
||||
syncServer: 'wss://cloud.jazz.tools/?key=you@example.com',
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -418,3 +418,40 @@ const fullName = Person.fullName(person);
|
||||
const age = Person.ageAsOf(person, new Date());
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Finding Unique CoValues with findUnique
|
||||
|
||||
Jazz provides a powerful mechanism to find unique CoMap instances based on their `CoValueUniqueness` identifier.
|
||||
The `findUnique` method allows for the location of specific CoMap instances without needing to load them first.
|
||||
|
||||
#### When to use findUnique
|
||||
Use findUnique when there's an assigned unique identifier on a CoMap (via CoValueUniqueness) and you want to retrieve that specific instance.
|
||||
This is useful for:
|
||||
- **Singleton CoValues** - When a `CoMap` should only ever have one instance
|
||||
- **Referencing specific instances** - When there's a need to access a CoMap that is known by its unique ID and owner
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import {co, z, Group} from "jazz-tools"
|
||||
const group = Group.create();
|
||||
|
||||
// ---cut---
|
||||
|
||||
const Person = co.map({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
dateOfBirth: z.date()
|
||||
});
|
||||
|
||||
const person = Person.create(
|
||||
{
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
dateOfBirth: new Date("1990-01-01"),
|
||||
},
|
||||
{ owner: group, unique: { lastName: "Doe" } },
|
||||
);
|
||||
|
||||
const foundPerson = Person.findUnique({ lastName: "Doe" }, group.id);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -87,6 +87,7 @@ Similar to Zod v4's new object syntax, recursive and mutually recursive types ar
|
||||
|
||||
### How to pass loaded CoValues
|
||||
|
||||
<ContentByFramework framework={["react", "react-native", "vue", "vanilla", "react-native-expo"]}>
|
||||
Calls to `useCoState()` work just the same, but they return a slightly different type than before.
|
||||
|
||||
And while you can still read from the type just as before...
|
||||
@@ -185,23 +186,293 @@ function PersonAndFirstPetName({ person }: {
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
<ContentByFramework framework="svelte">
|
||||
We've removed the `useCoState`, `useAccount` and `useAccountOrGuest` hooks.
|
||||
|
||||
You should now use the `CoState` and `AccountCoState` reactive classes instead. These provide greater stability and are significantly easier to work with.
|
||||
|
||||
Calls to `new CoState()` work just the same, but they return a slightly different type than before.
|
||||
|
||||
And while you can still read from the type just as before...
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash filename="schema.ts"
|
||||
// @filename: schema.ts
|
||||
import { z, co } from "jazz-tools";
|
||||
|
||||
const Pet = co.map({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
});
|
||||
|
||||
const Person = co.map({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
pets: co.list(Pet),
|
||||
});
|
||||
```
|
||||
```svelte twoslash filename="app.svelte"
|
||||
// @filename: app.svelte
|
||||
<script lang="ts">
|
||||
import { CoState } from "jazz-svelte";
|
||||
import { Person } from "./schema";
|
||||
|
||||
const person = new CoState(Person, id);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{person.current?.name}
|
||||
</div>
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
...you now need to specify the type differently **when passing CoValues as a parameter** or
|
||||
**whenever you need to refer to the type of a loaded CoValue instance:**
|
||||
|
||||
<CodeGroup>
|
||||
```svelte twoslash
|
||||
<script lang="ts" module>
|
||||
export type Props = {
|
||||
person: Loaded<typeof Person>; // [!code ++]
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Person } from './schema';
|
||||
|
||||
let props: Props = $props();
|
||||
</script>
|
||||
|
||||
|
||||
<div>
|
||||
{props.person.name}
|
||||
</div>
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
`Loaded` can also take a second argument to specify the loading depth of the expected CoValue, mirroring the `resolve` options for `CoState`, `load`, `subscribe`, etc.
|
||||
|
||||
<CodeGroup>
|
||||
```svelte twoslash
|
||||
<script lang="ts" module>
|
||||
export type Props = {
|
||||
person: Loaded<typeof Person, { pets: { $each: true } }>; // [!code ++]
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Person } from './schema';
|
||||
|
||||
let props: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{props.person.name}
|
||||
</div>
|
||||
<ul>
|
||||
{#each props.person.pets as pet}
|
||||
<li>{pet.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
### Removing AccountSchema registration
|
||||
|
||||
We have removed the Typescript AccountSchema registration.
|
||||
|
||||
It was causing some deal of confusion to new adopters so we have decided to replace the magic inference with a more explicit approach.
|
||||
|
||||
<ContentByFramework framework={["react", "react-native", "vue", "vanilla", "react-native-expo"]}>
|
||||
When using `useAccount` you should now pass the `Account` schema directly:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @filename: schema.ts
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
export const MyAccount = co.account({
|
||||
profile: co.profile(),
|
||||
root: co.map({})
|
||||
});
|
||||
|
||||
// @filename: app.tsx
|
||||
import React from "react";
|
||||
// ---cut---
|
||||
import { useAccount } from "jazz-react";
|
||||
import { MyAccount } from "./schema";
|
||||
|
||||
function MyComponent() {
|
||||
const { me } = useAccount(MyAccount, {
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
return <div>{me?.profile.name}</div>;
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="svelte">
|
||||
When using `AccountCoState` you should now pass the `Account` schema directly:
|
||||
|
||||
<CodeGroup>
|
||||
```svelte twoslash filename="app.svelte"
|
||||
<script lang="ts">
|
||||
import { AccountCoState } from "jazz-svelte";
|
||||
import { MyAccount } from "./schema";
|
||||
|
||||
const account = new AccountCoState(MyAccount, {
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{account.current?.profile.name}
|
||||
</div>
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
### Defining migrations
|
||||
|
||||
TODO
|
||||
Now account schemas need to be defined with `co.account()` and migrations can be declared using `withMigration()`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, z, Group } from "jazz-tools";
|
||||
|
||||
const Pet = co.map({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
});
|
||||
|
||||
const MyAppRoot = co.map({
|
||||
pets: co.list(Pet),
|
||||
});
|
||||
|
||||
const MyAppProfile = co.profile({
|
||||
name: z.string(),
|
||||
age: z.number().optional(),
|
||||
});
|
||||
|
||||
export const MyAppAccount = co.account({
|
||||
root: MyAppRoot,
|
||||
profile: MyAppProfile,
|
||||
}).withMigration((account, creationProps?: { name: string }) => {
|
||||
if (account.root === undefined) {
|
||||
account.root = MyAppRoot.create({
|
||||
pets: co.list(Pet).create([]),
|
||||
});
|
||||
}
|
||||
|
||||
if (account.profile === undefined) {
|
||||
const profileGroup = Group.create();
|
||||
profileGroup.addMember("everyone", "reader");
|
||||
|
||||
account.profile = MyAppProfile.create({
|
||||
name: creationProps?.name ?? "New user",
|
||||
}, profileGroup);
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Defining Schema helper methods
|
||||
|
||||
TODO
|
||||
|
||||
### Removing AccountSchema registration
|
||||
|
||||
TODO
|
||||
|
||||
## Minor breaking changes
|
||||
|
||||
### `_refs` and `_edits` are now potentially null
|
||||
|
||||
TODO
|
||||
The type of `_refs` and `_edits` is now nullable.
|
||||
|
||||
### `members` and `by` now return basic `Account`s
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { z, co } from "jazz-tools";
|
||||
// ---cut---
|
||||
const Person = co.map({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
});
|
||||
|
||||
const person = Person.create({ name: "John", age: 30 });
|
||||
|
||||
person._refs; // now nullable
|
||||
person._edits; // now nullable
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### `members` and `by` now return basic `Account`
|
||||
|
||||
We have removed the Account schema registration, so now `members` and `by` methods now always return basic `Account`.
|
||||
|
||||
This means that you now need to rely on `useCoState` on them to load their using your account schema.
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
import React from "react";
|
||||
import { co, z, Loaded, Group } from "jazz-tools";
|
||||
import { useCoState } from "jazz-react";
|
||||
|
||||
const Pet = co.map({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
});
|
||||
|
||||
const MyAppRoot = co.map({
|
||||
pets: co.list(Pet),
|
||||
});
|
||||
|
||||
const MyAppProfile = co.profile({
|
||||
name: z.string(),
|
||||
age: z.number().optional(),
|
||||
});
|
||||
|
||||
export const MyAppAccount = co.account({
|
||||
root: MyAppRoot,
|
||||
profile: MyAppProfile,
|
||||
});
|
||||
|
||||
// ---cut---
|
||||
function GroupMembers({ group }: { group: Loaded<typeof Group> }) {
|
||||
const members = group.members;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{members.map((member) => (
|
||||
<GroupMemberDetails
|
||||
accountId={member.account.id}
|
||||
key={member.account.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupMemberDetails({ accountId }: { accountId: string }) {
|
||||
const account = useCoState(MyAppAccount, accountId, {
|
||||
resolve: {
|
||||
profile: true,
|
||||
root: {
|
||||
pets: { $each: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{account?.profile.name}</div>
|
||||
<ul>{account?.root.pets.map((pet) => <li>{pet.name}</li>)}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -19,7 +19,9 @@ The following examples demonstrate a practical use of CoFeeds:
|
||||
CoFeeds are defined by specifying the type of items they'll contain, similar to how you define CoLists:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, z } from "jazz-tools";
|
||||
// ---cut---
|
||||
// Define a schema for feed items
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
@@ -73,24 +75,54 @@ Since CoFeeds are made of entries from users over multiple sessions, you can acc
|
||||
To retrieve entries from a session:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
const sessionId = `${me.id}_session_z1` as SessionID;
|
||||
|
||||
// ---cut---
|
||||
// Get the feed for a specific session
|
||||
const sessionFeed = activityFeed.perSession[sessionId];
|
||||
|
||||
// Latest entry from a session
|
||||
console.log(sessionFeed.value.action); // "watering"
|
||||
console.log(sessionFeed?.value?.action); // "watering"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For convenience, you can also access the latest entry from the current session with `inCurrentSession`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
const sessionId = `${me.id}_session_z1` as SessionID;
|
||||
|
||||
// ---cut---
|
||||
// Get the feed for the current session
|
||||
const currentSessionFeed = activityFeed.inCurrentSession;
|
||||
|
||||
// Latest entry from the current session
|
||||
console.log(currentSessionFeed.value.action); // "harvesting"
|
||||
console.log(currentSessionFeed?.value?.action); // "harvesting"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -99,24 +131,54 @@ console.log(currentSessionFeed.value.action); // "harvesting"
|
||||
To retrieve entries from a specific account (with entries from all sessions combined) use `perAccount`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
const accountId = me.id;
|
||||
|
||||
// ---cut---
|
||||
// Get the feed for a specific account
|
||||
const accountFeed = activityFeed.perAccount[accountId];
|
||||
|
||||
// Latest entry from the account
|
||||
console.log(accountFeed.value.action); // "watering"
|
||||
console.log(accountFeed.value?.action); // "watering"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For convenience, you can also access the latest entry from the current account with `byMe`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
const accountId = me.id;
|
||||
|
||||
// ---cut---
|
||||
// Get the feed for the current account
|
||||
const myLatestEntry = activityFeed.byMe;
|
||||
|
||||
// Latest entry from the current account
|
||||
console.log(myLatestEntry.value.action); // "harvesting"
|
||||
console.log(myLatestEntry?.value?.action); // "harvesting"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -127,7 +189,23 @@ console.log(myLatestEntry.value.action); // "harvesting"
|
||||
To retrieve all entries from a CoFeed:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
const accountId = me.id;
|
||||
const sessionId = `${me.id}_session_z1` as SessionID;
|
||||
|
||||
// ---cut---
|
||||
// Get the feeds for a specific account and session
|
||||
const accountFeed = activityFeed.perAccount[accountId];
|
||||
const sessionFeed = activityFeed.perSession[sessionId];
|
||||
@@ -149,11 +227,25 @@ for (const entry of sessionFeed.all) {
|
||||
To retrieve the latest entry from a CoFeed, ie. the last update:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
|
||||
// ---cut---
|
||||
// Get the latest entry from the current account
|
||||
const latestEntry = activityFeed.byMe;
|
||||
|
||||
console.log(`My last action was ${latestEntry.value.action}`);
|
||||
console.log(`My last action was ${latestEntry?.value?.action}`);
|
||||
// "My last action was harvesting"
|
||||
|
||||
// Get the latest entry from each account
|
||||
@@ -171,7 +263,21 @@ CoFeeds are append-only; you can add new items, but not modify existing ones. Th
|
||||
### Adding Items
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
|
||||
// ---cut---
|
||||
// Log a new activity
|
||||
activityFeed.push(Activity.create({
|
||||
timestamp: new Date(),
|
||||
@@ -188,23 +294,37 @@ Each item is automatically associated with the current user's session. You don't
|
||||
Each entry is automatically added to the current session's feed. When a user has multiple open sessions (like both a mobile app and web browser), each session creates its own separate entries:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const fromMobileFeed = ActivityFeed.create([]);
|
||||
const fromBrowserFeed = ActivityFeed.create([]);
|
||||
|
||||
// ---cut---
|
||||
// On mobile device:
|
||||
fromMobileFeed.push(Activity.create({
|
||||
timestamp: new Date(),
|
||||
action: "harvesting",
|
||||
location: "Vegetable patch"
|
||||
notes: "Vegetable patch"
|
||||
}));
|
||||
|
||||
// On web browser (same user):
|
||||
fromBrowserFeed.push(Activity.create({
|
||||
timestamp: new Date(),
|
||||
action: "planting",
|
||||
location: "Flower bed"
|
||||
notes: "Flower bed"
|
||||
}));
|
||||
|
||||
// These are separate entries in the same feed, from the same account
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -217,7 +337,22 @@ CoFeeds support metadata, which is useful for tracking information about the fee
|
||||
The `by` property is the account that made the entry.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
const accountId = me.id;
|
||||
|
||||
// ---cut---
|
||||
const accountFeed = activityFeed.perAccount[accountId];
|
||||
|
||||
// Get the account that made the last entry
|
||||
@@ -230,7 +365,22 @@ console.log(accountFeed?.by);
|
||||
The `madeAt` property is a timestamp of when the entry was added to the feed.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z, ID, Account, SessionID } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const Activity = co.map({
|
||||
timestamp: z.date(),
|
||||
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
|
||||
notes: z.optional(z.string()),
|
||||
});
|
||||
|
||||
const ActivityFeed = co.feed(Activity);
|
||||
const activityFeed = ActivityFeed.create([]);
|
||||
const accountId = me.id;
|
||||
|
||||
// ---cut---
|
||||
const accountFeed = activityFeed.perAccount[accountId];
|
||||
|
||||
// Get the timestamp of the last update
|
||||
|
||||
@@ -13,7 +13,13 @@ CoLists are ordered collections that work like JavaScript arrays. They provide i
|
||||
CoLists are defined by specifying the type of items they contain:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
const Task = co.map({
|
||||
title: z.string(),
|
||||
status: z.literal(["todo", "in-progress", "complete"]),
|
||||
});
|
||||
|
||||
// ---cut---
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const ListOfResources = co.list(z.string());
|
||||
@@ -25,7 +31,14 @@ const ListOfTasks = co.list(Task);
|
||||
To create a `CoList`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const Task = co.map({
|
||||
title: z.string(),
|
||||
status: z.literal(["todo", "in-progress", "complete"]),
|
||||
});
|
||||
// ---cut---
|
||||
// Create an empty list
|
||||
const resources = co.list(z.string()).create([]);
|
||||
|
||||
@@ -68,7 +81,22 @@ See [Groups as permission scopes](/docs/groups/intro) for more information on ho
|
||||
CoLists support standard array access patterns:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const Task = co.map({
|
||||
title: z.string(),
|
||||
status: z.literal(["todo", "in-progress", "complete"]),
|
||||
});
|
||||
|
||||
const ListOfTasks = co.list(Task);
|
||||
|
||||
const tasks = ListOfTasks.create([
|
||||
Task.create({ title: "Prepare soil beds", status: "todo" }),
|
||||
Task.create({ title: "Order compost", status: "todo" }),
|
||||
]);
|
||||
// ---cut---
|
||||
// Access by index
|
||||
const firstTask = tasks[0];
|
||||
console.log(firstTask.title); // "Prepare soil beds"
|
||||
@@ -94,7 +122,22 @@ console.log(todoTasks.length); // 1
|
||||
Update `CoList`s just like you would JavaScript arrays:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const Task = co.map({
|
||||
title: z.string(),
|
||||
status: z.literal(["todo", "in-progress", "complete"]),
|
||||
});
|
||||
|
||||
const ListOfTasks = co.list(Task);
|
||||
|
||||
const ListOfResources = co.list(z.string());
|
||||
|
||||
const resources = ListOfResources.create([]);
|
||||
const tasks = ListOfTasks.create([]);
|
||||
|
||||
// ---cut---
|
||||
// Add items
|
||||
resources.push("Tomatoes"); // Add to end
|
||||
resources.unshift("Lettuce"); // Add to beginning
|
||||
@@ -116,7 +159,18 @@ tasks[0].status = "complete"; // Update properties of references
|
||||
Remove specific items by index with `splice`, or remove the first or last item with `pop` or `shift`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const ListOfResources = co.list(z.string());
|
||||
|
||||
const resources = ListOfResources.create([
|
||||
"Tomatoes",
|
||||
"Cucumber",
|
||||
"Peppers",
|
||||
]);
|
||||
|
||||
// ---cut---
|
||||
// Remove 2 items starting at index 1
|
||||
resources.splice(1, 2);
|
||||
console.log(resources); // ["Cucumber", "Peppers"]
|
||||
@@ -136,7 +190,14 @@ resources.shift(); // Remove first item
|
||||
`CoList`s support the standard JavaScript array methods you already know:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const ListOfResources = co.list(z.string());
|
||||
|
||||
const resources = ListOfResources.create([]);
|
||||
|
||||
// ---cut---
|
||||
// Add multiple items at once
|
||||
resources.push("Tomatoes", "Basil", "Peppers");
|
||||
|
||||
@@ -158,9 +219,23 @@ console.log(resources); // ["Basil", "Peppers", "Tomatoes"]
|
||||
CoLists maintain type safety for their items:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
const Task = co.map({
|
||||
title: z.string(),
|
||||
status: z.literal(["todo", "in-progress", "complete"]),
|
||||
});
|
||||
|
||||
const ListOfTasks = co.list(Task);
|
||||
const ListOfResources = co.list(z.string());
|
||||
|
||||
const resources = ListOfResources.create([]);
|
||||
const tasks = ListOfTasks.create([]);
|
||||
// ---cut---
|
||||
// TypeScript catches type errors
|
||||
resources.push("Carrots"); // ✓ Valid string
|
||||
// @errors: 2345
|
||||
resources.push(42); // ✗ Type error: expected string
|
||||
|
||||
// For lists of references
|
||||
@@ -178,20 +253,30 @@ tasks.forEach(task => {
|
||||
CoLists work well with UI rendering libraries:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```tsx twoslash
|
||||
import * as React from "react";
|
||||
|
||||
const Task = co.map({
|
||||
title: z.string(),
|
||||
status: z.literal(["todo", "in-progress", "complete"]),
|
||||
});
|
||||
|
||||
// ---cut---
|
||||
import { co, z, Loaded } from "jazz-tools";
|
||||
const ListOfTasks = co.list(Task);
|
||||
|
||||
// React example
|
||||
function TaskList({ tasks: Loaded<typeof ListOfTasks> }) {
|
||||
return (
|
||||
<ul>
|
||||
{tasks.map(task => (
|
||||
function TaskList({ tasks }: { tasks: Loaded<typeof ListOfTasks> }) {
|
||||
return (
|
||||
<ul>
|
||||
{tasks.map(task => (
|
||||
task ? (
|
||||
<li key={task.id}>
|
||||
{task.title} - {task.status}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
): null
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -202,15 +287,34 @@ function TaskList({ tasks: Loaded<typeof ListOfTasks> }) {
|
||||
CoLists can be used to create one-to-many relationships:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { co, z } from "jazz-tools";
|
||||
```ts twoslash
|
||||
import { co, z, CoListSchema } from "jazz-tools";
|
||||
|
||||
const Task = co.map({
|
||||
title: z.string(),
|
||||
status: z.literal(["todo", "in-progress", "complete"]),
|
||||
|
||||
get project(): z.ZodOptional<typeof Project> {
|
||||
return z.optional(Project);
|
||||
}
|
||||
});
|
||||
|
||||
const ListOfTasks = co.list(Task);
|
||||
|
||||
const Project = co.map({
|
||||
name: z.string(),
|
||||
tasks: co.list(Task),
|
||||
|
||||
get tasks(): CoListSchema<typeof Task> {
|
||||
return ListOfTasks;
|
||||
}
|
||||
});
|
||||
|
||||
// ...
|
||||
const project = Project.create(
|
||||
{
|
||||
name: "Garden Project",
|
||||
tasks: ListOfTasks.create([]),
|
||||
},
|
||||
);
|
||||
|
||||
const task = Task.create({
|
||||
title: "Plant seedlings",
|
||||
|
||||
@@ -76,11 +76,11 @@ Create an empty FileStream when you want to manually [add binary data in chunks]
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { Group, co } from "jazz-tools";
|
||||
import { Group, FileStream } from "jazz-tools";
|
||||
const myGroup = Group.create();
|
||||
// ---cut---
|
||||
// Create a new empty FileStream
|
||||
const fileStream = co.fileStream().create({ owner: myGroup } );
|
||||
const fileStream = FileStream.create({ owner: myGroup } );
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -90,7 +90,7 @@ Like other CoValues, you can specify ownership when creating FileStreams.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { Group, co } from "jazz-tools";
|
||||
import { Group, FileStream } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
const colleagueAccount = await createJazzTestAccount();
|
||||
@@ -101,7 +101,7 @@ const teamGroup = Group.create();
|
||||
teamGroup.addMember(colleagueAccount, "writer");
|
||||
|
||||
// Create a FileStream with shared ownership
|
||||
const teamFileStream = co.fileStream().create({ owner: teamGroup });
|
||||
const teamFileStream = FileStream.create({ owner: teamGroup });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -117,8 +117,8 @@ To access the raw binary data and metadata:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co } from "jazz-tools";
|
||||
const fileStream = co.fileStream().create();
|
||||
import { FileStream } from "jazz-tools";
|
||||
const fileStream = FileStream.create();
|
||||
// ---cut---
|
||||
// Get all chunks and metadata
|
||||
const fileData = fileStream.getChunks();
|
||||
@@ -142,8 +142,8 @@ By default, `getChunks()` only returns data for completely synced `FileStream`s.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co } from "jazz-tools";
|
||||
const fileStream = co.fileStream().create();
|
||||
import { FileStream } from "jazz-tools";
|
||||
const fileStream = FileStream.create();
|
||||
// ---cut---
|
||||
// Get data even if the stream isn't complete
|
||||
const partialData = fileStream.getChunks({ allowUnfinished: true });
|
||||
@@ -156,8 +156,8 @@ For easier integration with web APIs, convert to a `Blob`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co } from "jazz-tools";
|
||||
const fileStream = co.fileStream().create();
|
||||
import { FileStream } from "jazz-tools";
|
||||
const fileStream = FileStream.create();
|
||||
// ---cut---
|
||||
// Convert to a Blob
|
||||
const blob = fileStream.toBlob();
|
||||
@@ -187,16 +187,16 @@ You can directly load a `FileStream` as a `Blob` when you only have its ID:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, type ID } from "jazz-tools";
|
||||
import { FileStream, type ID } from "jazz-tools";
|
||||
const fileStreamId = "co_z123" as ID<FileStream>;
|
||||
// ---cut---
|
||||
// Load directly as a Blob when you have an ID
|
||||
const blob = await co.fileStream().loadAsBlob(fileStreamId);
|
||||
const blob = await FileStream.loadAsBlob(fileStreamId);
|
||||
|
||||
// By default, waits for complete uploads
|
||||
// For in-progress uploads:
|
||||
const partialBlob = await co.fileStream().loadAsBlob(fileStreamId, {
|
||||
allowUnfinished: true
|
||||
const partialBlob = await FileStream.loadAsBlob(fileStreamId, {
|
||||
allowUnfinished: true,
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -207,8 +207,8 @@ Check if a `FileStream` is fully synced:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co } from "jazz-tools";
|
||||
const fileStream = co.fileStream().create();
|
||||
import { FileStream } from "jazz-tools";
|
||||
const fileStream = FileStream.create();
|
||||
// ---cut---
|
||||
if (fileStream.isBinaryStreamEnded()) {
|
||||
console.log('File is completely synced');
|
||||
@@ -236,11 +236,11 @@ Begin by providing metadata about the file:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, Group } from "jazz-tools";
|
||||
import { FileStream, Group } from "jazz-tools";
|
||||
const myGroup = Group.create();
|
||||
// ---cut---
|
||||
// Create an empty FileStream
|
||||
const fileStream = co.fileStream().create({ owner: myGroup });
|
||||
const fileStream = FileStream.create({ owner: myGroup });
|
||||
|
||||
// Initialize with metadata
|
||||
fileStream.start({
|
||||
@@ -257,8 +257,8 @@ Add binary data in chunks - this helps with large files and progress tracking:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, Group } from "jazz-tools";
|
||||
const fileStream = co.fileStream().create();
|
||||
import { FileStream, Group } from "jazz-tools";
|
||||
const fileStream = FileStream.create();
|
||||
const file = [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64]; // "Hello World" in ASCII
|
||||
const bytes = new Uint8Array(file);
|
||||
const arrayBuffer = bytes.buffer;
|
||||
@@ -288,8 +288,8 @@ Once all chunks are pushed, mark the `FileStream` as complete:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co } from "jazz-tools";
|
||||
const fileStream = co.fileStream().create();
|
||||
import { FileStream } from "jazz-tools";
|
||||
const fileStream = FileStream.create();
|
||||
// ---cut---
|
||||
// Finalize the upload
|
||||
fileStream.end();
|
||||
@@ -308,11 +308,11 @@ Load a `FileStream` when you have its ID:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, type ID } from "jazz-tools";
|
||||
const fileStreamId = "co_z123" as ID<FileStream>;
|
||||
import { FileStream } from "jazz-tools";
|
||||
const fileStreamId = "co_z123";
|
||||
// ---cut---
|
||||
// Load a FileStream by ID
|
||||
const fileStream = await co.fileStream().load(fileStreamId);
|
||||
const fileStream = await FileStream.load(fileStreamId);
|
||||
|
||||
if (fileStream) {
|
||||
console.log('FileStream loaded successfully');
|
||||
@@ -332,12 +332,12 @@ Subscribe to a `FileStream` to be notified when chunks are added or when the upl
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, type ID } from "jazz-tools";
|
||||
import { FileStream } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const fileStreamId = "co_z123" as ID<FileStream>;
|
||||
const fileStreamId = "co_z123";
|
||||
// ---cut---
|
||||
// Subscribe to a FileStream by ID
|
||||
const unsubscribe = co.fileStream().subscribe(fileStreamId, (fileStream: FileStream) => {
|
||||
const unsubscribe = FileStream.subscribe(fileStreamId, (fileStream: FileStream) => {
|
||||
// Called whenever the FileStream changes
|
||||
console.log('FileStream updated');
|
||||
|
||||
@@ -369,8 +369,8 @@ If you need to wait for a `FileStream` to be fully synchronized across devices:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co } from "jazz-tools";
|
||||
const fileStream = co.fileStream().create();
|
||||
import { FileStream } from "jazz-tools";
|
||||
const fileStream = FileStream.create();
|
||||
// ---cut---
|
||||
// Wait for the FileStream to be fully synced
|
||||
await fileStream.waitForSync({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
export const metadata = {
|
||||
export const metadata = {
|
||||
description: "ImageDefinition is a CoValue for managing images, storage of multiple resolutions, and progressive loading."
|
||||
};
|
||||
|
||||
@@ -19,26 +19,55 @@ The [Image Upload example](https://github.com/gardencmp/jazz/tree/main/examples/
|
||||
The easiest way to create and use images in your Jazz application is with the `createImage()` function:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z } from "jazz-tools";
|
||||
|
||||
const MyProfile = co.profile({
|
||||
name: z.string(),
|
||||
image: z.optional(co.image()),
|
||||
});
|
||||
|
||||
const MyAccount = co.account({
|
||||
root: co.map({}),
|
||||
profile: MyProfile,
|
||||
});
|
||||
|
||||
MyAccount.withMigration((account, creationProps) => {
|
||||
if (account.profile === undefined) {
|
||||
const profileGroup = Group.create();
|
||||
profileGroup.addMember("everyone", "reader");
|
||||
account.profile = MyProfile.create(
|
||||
{
|
||||
name: creationProps?.name ?? "New user",
|
||||
},
|
||||
profileGroup,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const me = await MyAccount.create({ creationProps: { name: "John Doe" } });
|
||||
|
||||
const myGroup = Group.create();
|
||||
|
||||
// ---cut---
|
||||
import { createImage } from "jazz-browser-media-images";
|
||||
|
||||
// Create an image from a file input
|
||||
async function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
async function handleFileUpload(event: Event) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
// Creates ImageDefinition with multiple resolutions automatically
|
||||
const image = await createImage(file, {
|
||||
owner: me.profile._owner,
|
||||
});
|
||||
const image = await createImage(file, { owner: myGroup });
|
||||
|
||||
// Store the image in your application data
|
||||
me.profile.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
> Note: `createImage()` requires a browser environment as it uses browser APIs to process images.
|
||||
**Note:** `createImage()` requires a browser environment as it uses browser APIs to process images.
|
||||
|
||||
The `createImage()` function:
|
||||
- Creates an `ImageDefinition` with the right properties
|
||||
@@ -51,11 +80,16 @@ The `createImage()` function:
|
||||
You can configure `createImage()` with additional options:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```tsx twoslash
|
||||
import { createImage } from "jazz-browser-media-images";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
const file = new File([], "test.jpg", { type: "image/jpeg" });
|
||||
// ---cut---
|
||||
// Configuration options
|
||||
const options = {
|
||||
owner: me, // Owner for access control
|
||||
maxSize: 1024 // Maximum resolution to generate
|
||||
maxSize: 1024 as 1024 // Maximum resolution to generate
|
||||
};
|
||||
|
||||
// Setting maxSize controls which resolutions are generated:
|
||||
@@ -98,7 +132,7 @@ See [Groups as permission scopes](/docs/groups/intro) for more information on ho
|
||||
Create an `ImageDefinition` by specifying the original dimensions and an optional placeholder:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { ImageDefinition } from "jazz-tools";
|
||||
|
||||
// Create with original dimensions
|
||||
@@ -109,7 +143,7 @@ const image = ImageDefinition.create({
|
||||
// With a placeholder for immediate display
|
||||
const imageWithPlaceholder = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAP...",
|
||||
placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJ...",
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -124,15 +158,13 @@ const imageWithPlaceholder = ImageDefinition.create({
|
||||
Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`).
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { CoMap, CoList, ImageDefinition, coField } from "jazz-tools";
|
||||
```ts twoslash
|
||||
import { ImageDefinition, co, z } from "jazz-tools";
|
||||
|
||||
class ListOfImages extends CoList.Of(coField.ref(ImageDefinition)) {}
|
||||
|
||||
class Gallery extends CoMap {
|
||||
title = coField.string;
|
||||
images = coField.ref(ListOfImages);
|
||||
}
|
||||
const Gallery = co.map({
|
||||
title: z.string(),
|
||||
images: co.list(co.image()),
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -141,7 +173,19 @@ class Gallery extends CoMap {
|
||||
Add multiple resolutions to an `ImageDefinition` by creating `FileStream`s for each size:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { FileStream, ImageDefinition } from "jazz-tools";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const fullSizeBlob = new Blob([], { type: "image/jpeg" });
|
||||
const mediumSizeBlob = new Blob([], { type: "image/jpeg" });
|
||||
const thumbnailBlob = new Blob([], { type: "image/jpeg" });
|
||||
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
}, { owner: me });
|
||||
// ---cut---
|
||||
// Create FileStreams for different resolutions
|
||||
const fullRes = await FileStream.createFromBlob(fullSizeBlob);
|
||||
const mediumRes = await FileStream.createFromBlob(mediumSizeBlob);
|
||||
@@ -159,28 +203,46 @@ image["320x180"] = thumbnailRes;
|
||||
The `highestResAvailable` method helps select the best image resolution for the current context:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Get the highest resolution available
|
||||
const highestRes = image.highestResAvailable();
|
||||
if (highestRes) {
|
||||
console.log(`Found resolution: ${highestRes.res}`);
|
||||
```ts twoslash
|
||||
import { ImageDefinition, FileStream } from "jazz-tools";
|
||||
import { createJazzTestAccount } from "jazz-tools/testing";
|
||||
|
||||
// Convert to a usable blob
|
||||
// Simple document environment
|
||||
global.document = {
|
||||
createElement: () =>
|
||||
({ src: "", onload: null }) as unknown as HTMLImageElement,
|
||||
} as unknown as Document;
|
||||
global.window = { innerWidth: 1000 } as unknown as Window & typeof globalThis;
|
||||
|
||||
// Setup
|
||||
const fakeBlob = new Blob(["fake image data"], { type: "image/jpeg" });
|
||||
const me = await createJazzTestAccount();
|
||||
const image = ImageDefinition.create(
|
||||
{ originalSize: [1920, 1080] },
|
||||
{ owner: me },
|
||||
);
|
||||
image["1920x1080"] = await FileStream.createFromBlob(fakeBlob, { owner: me });
|
||||
const imageElement = document.createElement("img");
|
||||
|
||||
// ---cut---
|
||||
// Get highest resolution available (unconstrained)
|
||||
const highestRes = ImageDefinition.highestResAvailable(image);
|
||||
console.log(highestRes);
|
||||
if (highestRes) {
|
||||
const blob = highestRes.stream.toBlob();
|
||||
if (blob) {
|
||||
// Create a URL for the blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
imageElement.src = url;
|
||||
|
||||
// Remember to revoke the URL when no longer needed
|
||||
imageElement.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
// Revoke the URL when the image is loaded
|
||||
imageElement.onload = () => URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
// Or constrain by maximum width
|
||||
const targetWidth = window.innerWidth;
|
||||
const appropriateRes = image.highestResAvailable({ targetWidth });
|
||||
// Get appropriate resolution for specific width
|
||||
const appropriateRes = ImageDefinition.highestResAvailable(image, {
|
||||
targetWidth: window.innerWidth,
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -189,7 +251,11 @@ const appropriateRes = image.highestResAvailable({ targetWidth });
|
||||
`highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { ImageDefinition, FileStream } from "jazz-tools";
|
||||
|
||||
const mediumSizeBlob = new Blob([], { type: "image/jpeg" });
|
||||
// ---cut---
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
});
|
||||
@@ -197,8 +263,8 @@ const image = ImageDefinition.create({
|
||||
image["1920x1080"] = FileStream.create(); // Empty image upload
|
||||
image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob);
|
||||
|
||||
const highestRes = image.highestResAvailable();
|
||||
console.log(highestRes.res); // 800x450
|
||||
const highestRes = ImageDefinition.highestResAvailable(image);
|
||||
console.log(highestRes?.res); // 800x450
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -207,7 +273,31 @@ console.log(highestRes.res); // 800x450
|
||||
`ImageDefinition` supports simple progressive loading with placeholders and resolution selection:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { FileStream, ImageDefinition } from "jazz-tools";
|
||||
import { createJazzTestAccount } from "jazz-tools/testing";
|
||||
|
||||
// Simple document environment
|
||||
global.document = {
|
||||
createElement: () =>
|
||||
({ src: "", onload: null }) as unknown as HTMLImageElement,
|
||||
} as unknown as Document;
|
||||
global.window = { innerWidth: 1000 } as unknown as Window & typeof globalThis;
|
||||
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const mediumSizeBlob = new Blob([], { type: "image/jpeg" });
|
||||
const image = ImageDefinition.create(
|
||||
{
|
||||
originalSize: [1920, 1080],
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
image["1920x1080"] = await FileStream.createFromBlob(mediumSizeBlob, {
|
||||
owner: me,
|
||||
});
|
||||
const imageElement = document.createElement("img");
|
||||
// ---cut---
|
||||
// Start with placeholder for immediate display
|
||||
if (image.placeholderDataURL) {
|
||||
imageElement.src = image.placeholderDataURL;
|
||||
@@ -215,7 +305,9 @@ if (image.placeholderDataURL) {
|
||||
|
||||
// Then load the best resolution for the current display
|
||||
const screenWidth = window.innerWidth;
|
||||
const bestRes = image.highestResAvailable({ targetWidth: screenWidth });
|
||||
const bestRes = ImageDefinition.highestResAvailable(image, {
|
||||
targetWidth: screenWidth,
|
||||
});
|
||||
|
||||
if (bestRes) {
|
||||
const blob = bestRes.stream.toBlob();
|
||||
@@ -229,6 +321,7 @@ if (bestRes) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
## Best Practices
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
export const metadata = {
|
||||
export const metadata = {
|
||||
description: "ImageDefinition is a CoValue for managing images, storage of multiple resolutions, and progressive loading."
|
||||
};
|
||||
|
||||
@@ -20,26 +20,55 @@ The [Image Upload example](https://github.com/gardencmp/jazz/tree/main/examples/
|
||||
The easiest way to create and use images in your Jazz application is with the `createImage()` function:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Group, co, z } from "jazz-tools";
|
||||
|
||||
const MyProfile = co.profile({
|
||||
name: z.string(),
|
||||
image: z.optional(co.image()),
|
||||
});
|
||||
|
||||
const MyAccount = co.account({
|
||||
root: co.map({}),
|
||||
profile: MyProfile,
|
||||
});
|
||||
|
||||
MyAccount.withMigration((account, creationProps) => {
|
||||
if (account.profile === undefined) {
|
||||
const profileGroup = Group.create();
|
||||
profileGroup.addMember("everyone", "reader");
|
||||
account.profile = MyProfile.create(
|
||||
{
|
||||
name: creationProps?.name ?? "New user",
|
||||
},
|
||||
profileGroup,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const me = await MyAccount.create({});
|
||||
|
||||
const myGroup = Group.create();
|
||||
|
||||
// ---cut---
|
||||
import { createImage } from "jazz-browser-media-images";
|
||||
|
||||
// Create an image from a file input
|
||||
async function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
async function handleFileUpload(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
// Creates ImageDefinition with multiple resolutions automatically
|
||||
const image = await createImage(file, {
|
||||
owner: me.profile._owner,
|
||||
});
|
||||
|
||||
const image = await createImage(file, { owner: myGroup });
|
||||
|
||||
// Store the image in your application data
|
||||
me.profile.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
> Note: `createImage()` requires a browser environment as it uses browser APIs to process images.
|
||||
**Note:** `createImage()` requires a browser environment as it uses browser APIs to process images.
|
||||
|
||||
The `createImage()` function:
|
||||
- Creates an `ImageDefinition` with the right properties
|
||||
@@ -52,11 +81,16 @@ The `createImage()` function:
|
||||
You can configure `createImage()` with additional options:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```ts twoslash
|
||||
import { createImage } from "jazz-browser-media-images";
|
||||
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||
const me = await createJazzTestAccount();
|
||||
const file = new File([], "test.jpg", { type: "image/jpeg" });
|
||||
// ---cut---
|
||||
// Configuration options
|
||||
const options = {
|
||||
owner: me, // Owner for access control
|
||||
maxSize: 1024 // Maximum resolution to generate
|
||||
maxSize: 1024 as 1024 // Maximum resolution to generate
|
||||
};
|
||||
|
||||
// Setting maxSize controls which resolutions are generated:
|
||||
@@ -99,10 +133,14 @@ See [Groups as permission scopes](/docs/groups/intro) for more information on ho
|
||||
For a complete progressive loading experience, use the `ProgressiveImg` component:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```tsx twoslash
|
||||
import * as React from "react";
|
||||
// ---cut---
|
||||
import { ProgressiveImg } from "jazz-react";
|
||||
import { Loaded, co } from "jazz-tools";
|
||||
const Image = co.image();
|
||||
|
||||
function GalleryView({ image }) {
|
||||
function GalleryView({ image }: { image: Loaded<typeof Image> }) {
|
||||
return (
|
||||
<div className="image-container">
|
||||
<ProgressiveImg
|
||||
@@ -134,10 +172,14 @@ The `ProgressiveImg` component handles:
|
||||
For more control over image loading, you can implement your own progressive image component:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
```tsx twoslash
|
||||
import * as React from "react";
|
||||
import { Loaded, co } from "jazz-tools";
|
||||
const Image = co.image();
|
||||
// ---cut---
|
||||
import { useProgressiveImg } from "jazz-react";
|
||||
|
||||
function CustomImageComponent({ image }) {
|
||||
function CustomImageComponent({ image }: { image: Loaded<typeof Image> }) {
|
||||
const {
|
||||
src, // Data URI containing the image data as a base64 string,
|
||||
// or a placeholder image URI
|
||||
@@ -186,7 +228,9 @@ Behind the scenes, `ImageDefinition` is a specialized CoValue that stores:
|
||||
Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`).
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { ImageDefinition } from "jazz-tools";
|
||||
// ---cut---
|
||||
// Structure of an ImageDefinition
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
@@ -194,7 +238,7 @@ const image = ImageDefinition.create({
|
||||
});
|
||||
|
||||
// Accessing the highest available resolution
|
||||
const highestRes = image.highestResAvailable();
|
||||
const highestRes = ImageDefinition.highestResAvailable(image);
|
||||
if (highestRes) {
|
||||
console.log(`Found resolution: ${highestRes.res}`);
|
||||
console.log(`Stream: ${highestRes.stream}`);
|
||||
@@ -209,7 +253,11 @@ For more details on using `ImageDefinition` directly, see the [VanillaJS docs](/
|
||||
`highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { ImageDefinition, FileStream } from "jazz-tools";
|
||||
const mediumSizeBlob = new Blob([], { type: "image/jpeg" });
|
||||
|
||||
// ---cut---
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
});
|
||||
@@ -217,7 +265,7 @@ const image = ImageDefinition.create({
|
||||
image["1920x1080"] = FileStream.create(); // Empty image upload
|
||||
image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob);
|
||||
|
||||
const highestRes = image.highestResAvailable();
|
||||
console.log(highestRes.res); // 800x450
|
||||
const highestRes = ImageDefinition.highestResAvailable(image);
|
||||
console.log(highestRes?.res); // 800x450
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -851,4 +851,4 @@ async function completeAllTasks(projectId: string) {
|
||||
2. **Use framework integrations**: They handle subscription lifecycle automatically
|
||||
3. **Clean up subscriptions**: Always store and call the unsubscribe function when you're done
|
||||
4. **Handle all loading states**: Check for undefined (loading), null (not found), and success states
|
||||
5. **Use the Loaded type**: Add compile-time type safety for components that require specific resolution patterns
|
||||
5. **Use the Loaded type**: Add compile-time type safety for components that require specific resolution patterns
|
||||
@@ -28,9 +28,6 @@ export const PACKAGES = [
|
||||
packageName: "jazz-react",
|
||||
entryPoint: "index.ts",
|
||||
description: "React bindings for Jazz, a framework for distributed state.",
|
||||
typedocOptions: {
|
||||
skipErrorChecking: true, // TODO: remove this. Temporary workaround
|
||||
},
|
||||
},
|
||||
{
|
||||
packageName: "jazz-browser",
|
||||
|
||||
50
packages/jazz-react/src/scratch.tsx
Normal file
50
packages/jazz-react/src/scratch.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { FileStream, ImageDefinition } from "jazz-tools";
|
||||
import { createJazzTestAccount } from "jazz-tools/testing";
|
||||
|
||||
// Simple document environment
|
||||
global.document = {
|
||||
createElement: () =>
|
||||
({ src: "", onload: null }) as unknown as HTMLImageElement,
|
||||
} as unknown as Document;
|
||||
global.window = { innerWidth: 1000 } as unknown as Window & typeof globalThis;
|
||||
|
||||
const me = await createJazzTestAccount();
|
||||
|
||||
const mediumSizeBlob = new Blob([], { type: "image/jpeg" });
|
||||
const image = ImageDefinition.create(
|
||||
{
|
||||
originalSize: [1920, 1080],
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
image["100x100"] = await FileStream.createFromBlob(mediumSizeBlob, {
|
||||
owner: me,
|
||||
});
|
||||
image["1920x1080"] = await FileStream.createFromBlob(mediumSizeBlob, {
|
||||
owner: me,
|
||||
});
|
||||
const imageElement = document.createElement("img");
|
||||
// ---cut---
|
||||
// Start with placeholder for immediate display
|
||||
if (image.placeholderDataURL) {
|
||||
imageElement.src = image.placeholderDataURL;
|
||||
}
|
||||
|
||||
// Then load the best resolution for the current display
|
||||
const screenWidth = window.innerWidth;
|
||||
const bestRes = ImageDefinition.highestResAvailable(image, {
|
||||
targetWidth: screenWidth,
|
||||
});
|
||||
|
||||
if (bestRes) {
|
||||
const blob = bestRes.stream.toBlob();
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
imageElement.src = url;
|
||||
|
||||
// Remember to revoke the URL when no longer needed
|
||||
imageElement.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-svelte
|
||||
|
||||
## 0.14.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b2ee306: Fix compat with the new Zod schema.
|
||||
|
||||
BREAKING: Remove RegisterAccount, the useCoState, useAccount and useAccountOrGuest hooks and now AccountCoState requires the schema param
|
||||
|
||||
## 0.14.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-svelte",
|
||||
"version": "0.14.2",
|
||||
"version": "0.14.3",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && npm run package",
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
<script lang="ts" module>
|
||||
export type Props<Acc extends Account = Account> = JazzContextManagerProps<Acc> & {
|
||||
export type Props<
|
||||
S extends (AccountClass<Account> & CoValueFromRaw<Account>) | AnyAccountSchema
|
||||
> = JazzContextManagerProps<S> & {
|
||||
children?: Snippet;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" generics="Acc extends Account">
|
||||
<script
|
||||
lang="ts"
|
||||
generics="S extends
|
||||
| (AccountClass<Account> & CoValueFromRaw<Account>)
|
||||
| AnyAccountSchema,"
|
||||
>
|
||||
import { JazzBrowserContextManager, type JazzContextManagerProps } from 'jazz-browser';
|
||||
import type { AuthSecretStorage } from 'jazz-tools';
|
||||
import { Account } from 'jazz-tools';
|
||||
import type { AuthSecretStorage, InstanceOfSchema } from 'jazz-tools';
|
||||
import { Account, type AccountClass, type CoValueFromRaw, type AnyAccountSchema } from 'jazz-tools';
|
||||
import { type Snippet, setContext, untrack } from 'svelte';
|
||||
import { JAZZ_AUTH_CTX, JAZZ_CTX, type JazzContext } from './jazz.svelte.js';
|
||||
|
||||
let props: Props<Acc> = $props();
|
||||
let props: Props<S> = $props();
|
||||
|
||||
const contextManager = new JazzBrowserContextManager<Acc>();
|
||||
const contextManager = new JazzBrowserContextManager<S>();
|
||||
|
||||
const ctx = $state<JazzContext<Acc>>({ current: undefined });
|
||||
setContext<JazzContext<Acc>>(JAZZ_CTX, ctx);
|
||||
const ctx = $state<JazzContext<InstanceOfSchema<S>>>({ current: undefined });
|
||||
setContext<JazzContext<InstanceOfSchema<S>>>(JAZZ_CTX, ctx);
|
||||
setContext<AuthSecretStorage>(JAZZ_AUTH_CTX, contextManager.getAuthSecretStorage());
|
||||
|
||||
$effect(() => {
|
||||
@@ -35,7 +42,7 @@
|
||||
AccountSchema: props.AccountSchema,
|
||||
defaultProfileName: props.defaultProfileName,
|
||||
onAnonymousAccountDiscarded: props.onAnonymousAccountDiscarded,
|
||||
onLogOut: props.onLogOut,
|
||||
onLogOut: props.onLogOut
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error creating Jazz browser context:', error);
|
||||
|
||||
@@ -1,169 +1,193 @@
|
||||
import type { Account, CoValue, CoValueClass, ID, RefsToResolve, RefsToResolveStrict, RegisteredAccount, Resolved } from "jazz-tools";
|
||||
import { createSubscriber } from "svelte/reactivity";
|
||||
import { getJazzContext } from "./jazz.svelte.js";
|
||||
import { subscribeToCoValue } from "jazz-tools";
|
||||
|
||||
export class CoState<V extends CoValue, R extends RefsToResolve<V> = true> {
|
||||
#value: Resolved<V, R> | undefined | null = undefined;
|
||||
import type {
|
||||
Account,
|
||||
AccountClass,
|
||||
AnyAccountSchema,
|
||||
CoValueFromRaw,
|
||||
CoValueOrZodSchema,
|
||||
InstanceOfSchema,
|
||||
Loaded,
|
||||
ResolveQuery,
|
||||
ResolveQueryStrict,
|
||||
} from "jazz-tools";
|
||||
import { createSubscriber } from "svelte/reactivity";
|
||||
import { getJazzContext } from "./jazz.svelte.js";
|
||||
import { anySchemaToCoSchema, subscribeToCoValue } from "jazz-tools";
|
||||
|
||||
export class CoState<
|
||||
V extends CoValueOrZodSchema,
|
||||
R extends ResolveQuery<V> = true,
|
||||
> {
|
||||
#value: Loaded<V, R> | undefined | null = undefined;
|
||||
#subscribe: VoidFunction;
|
||||
#ctx = $derived(getJazzContext<RegisteredAccount>());
|
||||
#ctx = $derived(getJazzContext<InstanceOfSchema<AccountClass<Account>>>());
|
||||
#update: VoidFunction = () => {};
|
||||
#Schema: CoValueClass<V>;
|
||||
#options: { resolve?: RefsToResolveStrict<V, R> } | undefined;
|
||||
#id: ID<CoValue> | undefined | null;
|
||||
|
||||
#Schema: V;
|
||||
#options: { resolve?: ResolveQueryStrict<V, R> } | undefined;
|
||||
#id: string | undefined | null;
|
||||
|
||||
#unsubscribe: VoidFunction = () => {};
|
||||
|
||||
|
||||
constructor(
|
||||
Schema: CoValueClass<V>,
|
||||
id: ID<CoValue> | undefined | null | (() => ID<CoValue> | undefined | null),
|
||||
options?: { resolve?: RefsToResolveStrict<V, R> }
|
||||
Schema: V,
|
||||
id: string | undefined | null | (() => string | undefined | null),
|
||||
options?: { resolve?: ResolveQueryStrict<V, R> },
|
||||
) {
|
||||
this.#Schema = Schema;
|
||||
this.#options = options;
|
||||
this.#id = typeof id === 'function' ? id() : id;
|
||||
|
||||
this.#subscribe = createSubscriber((update) => {
|
||||
this.#update = update;
|
||||
|
||||
// Using an effect to react to the id and ctx changes
|
||||
$effect.pre(() => {
|
||||
this.#id = typeof id === 'function' ? id() : id;
|
||||
|
||||
this.subscribeTo();
|
||||
});
|
||||
|
||||
return () => {
|
||||
this.#unsubscribe();
|
||||
}
|
||||
this.#Schema = Schema;
|
||||
this.#options = options;
|
||||
this.#id = typeof id === "function" ? id() : id;
|
||||
|
||||
this.#subscribe = createSubscriber((update) => {
|
||||
this.#update = update;
|
||||
|
||||
// Using an effect to react to the id and ctx changes
|
||||
$effect.pre(() => {
|
||||
this.#id = typeof id === "function" ? id() : id;
|
||||
|
||||
this.subscribeTo();
|
||||
});
|
||||
|
||||
return () => {
|
||||
this.#unsubscribe();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
subscribeTo() {
|
||||
const ctx = this.#ctx;
|
||||
const id = this.#id;
|
||||
|
||||
// Reset state when dependencies change
|
||||
this.updateValue(undefined);
|
||||
this.#unsubscribe();
|
||||
|
||||
if (!ctx.current || !id) return;
|
||||
|
||||
const agent = "me" in ctx.current ? ctx.current.me : ctx.current.guest;
|
||||
|
||||
// Setup subscription with current values
|
||||
this.#unsubscribe = subscribeToCoValue<V, R>(
|
||||
this.#Schema,
|
||||
id,
|
||||
{
|
||||
resolve: this.#options?.resolve,
|
||||
loadAs: agent,
|
||||
onUnavailable: () => {
|
||||
this.updateValue(null);
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
this.updateValue(null);
|
||||
},
|
||||
syncResolution: true,
|
||||
},
|
||||
(value) => {
|
||||
this.updateValue(value as Resolved<V, R>);
|
||||
},
|
||||
);
|
||||
const ctx = this.#ctx;
|
||||
const id = this.#id;
|
||||
|
||||
// Reset state when dependencies change
|
||||
this.updateValue(undefined);
|
||||
this.#unsubscribe();
|
||||
|
||||
if (!ctx.current || !id) return;
|
||||
|
||||
const agent = "me" in ctx.current ? ctx.current.me : ctx.current.guest;
|
||||
|
||||
// Setup subscription with current values
|
||||
this.#unsubscribe = subscribeToCoValue(
|
||||
anySchemaToCoSchema(this.#Schema),
|
||||
id,
|
||||
{
|
||||
// @ts-expect-error The resolve query type isn't compatible with the anySchemaToCoSchema conversion
|
||||
resolve: this.#options?.resolve,
|
||||
loadAs: agent,
|
||||
onUnavailable: () => {
|
||||
this.updateValue(null);
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
this.updateValue(null);
|
||||
},
|
||||
syncResolution: true,
|
||||
},
|
||||
(value) => {
|
||||
this.updateValue(value as Loaded<V, R>);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updateValue(value: Resolved<V, R> | undefined | null) {
|
||||
if (value !== this.#value) {
|
||||
this.#value = value;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
updateValue(value: Loaded<V, R> | undefined | null) {
|
||||
if (value !== this.#value) {
|
||||
this.#value = value;
|
||||
this.#update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get current() {
|
||||
this.#subscribe();
|
||||
|
||||
return this.#value;
|
||||
this.#subscribe();
|
||||
|
||||
return this.#value;
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountCoState<A extends Account = RegisteredAccount, R extends RefsToResolve<A> = true> {
|
||||
#value: Resolved<A, R> | undefined | null = undefined;
|
||||
}
|
||||
|
||||
export class AccountCoState<
|
||||
A extends
|
||||
| (AccountClass<Account> & CoValueFromRaw<Account>)
|
||||
| AnyAccountSchema,
|
||||
R extends ResolveQuery<A> = true,
|
||||
> {
|
||||
#value: Loaded<A, R> | undefined | null = undefined;
|
||||
#subscribe: VoidFunction;
|
||||
#ctx = $derived(getJazzContext<A>());
|
||||
#ctx = $derived(getJazzContext<InstanceOfSchema<A>>());
|
||||
#Schema: A;
|
||||
#update: VoidFunction = () => {};
|
||||
#unsubscribe: VoidFunction = () => {};
|
||||
#options: { resolve?: RefsToResolveStrict<A, R> } | undefined;
|
||||
|
||||
constructor(
|
||||
options?: { resolve?: RefsToResolveStrict<A, R> }
|
||||
) {
|
||||
this.#options = options;
|
||||
this.#subscribe = createSubscriber((update) => {
|
||||
this.#update = update;
|
||||
|
||||
// Using an effect to react to the ctx changes
|
||||
$effect.pre(() => {
|
||||
this.subscribeTo();
|
||||
});
|
||||
|
||||
return () => {
|
||||
this.#unsubscribe();
|
||||
}
|
||||
#options: { resolve?: ResolveQueryStrict<A, R> } | undefined;
|
||||
|
||||
constructor(Schema: A, options?: { resolve?: ResolveQueryStrict<A, R> }) {
|
||||
this.#Schema = Schema;
|
||||
this.#options = options;
|
||||
this.#subscribe = createSubscriber((update) => {
|
||||
this.#update = update;
|
||||
|
||||
// Using an effect to react to the ctx changes
|
||||
$effect.pre(() => {
|
||||
this.subscribeTo();
|
||||
});
|
||||
|
||||
return () => {
|
||||
this.#unsubscribe();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
subscribeTo() {
|
||||
const ctx = this.#ctx;
|
||||
const ctx = this.#ctx;
|
||||
|
||||
// Reset state when dependencies change
|
||||
this.updateValue(undefined);
|
||||
this.#unsubscribe();
|
||||
|
||||
if (!ctx.current) return;
|
||||
if (!("me" in ctx.current)) return;
|
||||
|
||||
// Reset state when dependencies change
|
||||
this.updateValue(undefined);
|
||||
this.#unsubscribe();
|
||||
|
||||
if (!ctx.current) return;
|
||||
|
||||
if (!('me' in ctx.current)) {
|
||||
throw new Error(
|
||||
"useAccount can't be used in a JazzProvider with auth === 'guest' - consider using useAccountOrGuest()"
|
||||
);
|
||||
}
|
||||
|
||||
const me = ctx.current.me;
|
||||
|
||||
// Setup subscription with current values
|
||||
this.#unsubscribe = subscribeToCoValue<A, R>(
|
||||
me.constructor as CoValueClass<A>,
|
||||
me.id,
|
||||
{
|
||||
resolve: this.#options?.resolve,
|
||||
loadAs: me,
|
||||
onUnavailable: () => {
|
||||
this.updateValue(null);
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
this.updateValue(null);
|
||||
},
|
||||
syncResolution: true,
|
||||
},
|
||||
(value) => {
|
||||
this.updateValue(value as Resolved<A, R>);
|
||||
},
|
||||
);
|
||||
const me = ctx.current.me;
|
||||
|
||||
// Setup subscription with current values
|
||||
this.#unsubscribe = subscribeToCoValue(
|
||||
anySchemaToCoSchema(this.#Schema),
|
||||
me.id,
|
||||
{
|
||||
// @ts-expect-error The resolve query type isn't compatible with the anySchemaToCoSchema conversion
|
||||
resolve: this.#options?.resolve,
|
||||
loadAs: me,
|
||||
onUnavailable: () => {
|
||||
this.updateValue(null);
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
this.updateValue(null);
|
||||
},
|
||||
syncResolution: true,
|
||||
},
|
||||
(value) => {
|
||||
this.updateValue(value as Loaded<A, R>);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
logOut = () => {
|
||||
this.#ctx.current?.logOut();
|
||||
};
|
||||
|
||||
updateValue(value: Loaded<A, R> | undefined | null) {
|
||||
if (value !== this.#value) {
|
||||
this.#value = value;
|
||||
this.#update();
|
||||
}
|
||||
}
|
||||
|
||||
updateValue(value: Resolved<A, R> | undefined | null) {
|
||||
if (value !== this.#value) {
|
||||
this.#value = value;
|
||||
this.#update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get current() {
|
||||
this.#subscribe();
|
||||
|
||||
return this.#value;
|
||||
this.#subscribe();
|
||||
|
||||
return this.#value;
|
||||
}
|
||||
}
|
||||
|
||||
get agent() {
|
||||
if (!this.#ctx.current) {
|
||||
throw new Error("No context found");
|
||||
}
|
||||
|
||||
return "me" in this.#ctx.current ? this.#ctx.current.me : this.#ctx.current.guest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { consumeInviteLinkFromWindowLocation } from 'jazz-browser';
|
||||
import type {
|
||||
AnonymousJazzAgent,
|
||||
AccountClass,
|
||||
AuthSecretStorage,
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
CoValueOrZodSchema,
|
||||
ID,
|
||||
JazzAuthContext,
|
||||
JazzContextType,
|
||||
JazzGuestContext,
|
||||
RefsToResolve,
|
||||
Resolved
|
||||
InstanceOfSchema,
|
||||
JazzContextType
|
||||
} from 'jazz-tools';
|
||||
import { Account, subscribeToCoValue } from 'jazz-tools';
|
||||
import { Account } from 'jazz-tools';
|
||||
import { getContext, untrack } from 'svelte';
|
||||
import Provider from './Provider.svelte';
|
||||
import type { RefsToResolveStrict } from 'jazz-tools';
|
||||
|
||||
export { Provider as JazzProvider };
|
||||
|
||||
@@ -61,156 +56,6 @@ export function getAuthSecretStorage() {
|
||||
return context;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface Register {}
|
||||
|
||||
export type RegisteredAccount = Register extends { Account: infer Acc } ? Acc : Account;
|
||||
|
||||
declare module "jazz-tools" {
|
||||
export interface Register {
|
||||
Account: RegisteredAccount;
|
||||
}
|
||||
}
|
||||
|
||||
export function useAccount<const R extends RefsToResolve<RegisteredAccount>>(
|
||||
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
|
||||
): { me: Resolved<RegisteredAccount, R> | undefined | null; logOut: () => void };
|
||||
export function useAccount<const R extends RefsToResolve<RegisteredAccount>>(
|
||||
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
|
||||
): { me: RegisteredAccount | Resolved<RegisteredAccount, R> | undefined | null; logOut: () => void } {
|
||||
const ctx = getJazzContext<RegisteredAccount>();
|
||||
if (!ctx?.current) {
|
||||
throw new Error('useAccount must be used within a JazzProvider');
|
||||
}
|
||||
if (!('me' in ctx.current)) {
|
||||
throw new Error(
|
||||
"useAccount can't be used in a JazzProvider with auth === 'guest' - consider using useAccountOrGuest()"
|
||||
);
|
||||
}
|
||||
|
||||
// If no depth is specified, return the context's me directly
|
||||
if (options?.resolve === undefined) {
|
||||
return {
|
||||
get me() {
|
||||
return (ctx.current as JazzAuthContext<RegisteredAccount>).me;
|
||||
},
|
||||
logOut() {
|
||||
return ctx.current?.logOut();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// If depth is specified, use useCoState to get the deeply loaded version
|
||||
const me = useCoState<RegisteredAccount, R>(
|
||||
ctx.current.me.constructor as CoValueClass<RegisteredAccount>,
|
||||
(ctx.current as JazzAuthContext<RegisteredAccount>).me.id,
|
||||
options
|
||||
);
|
||||
|
||||
return {
|
||||
get me() {
|
||||
return me.current;
|
||||
},
|
||||
logOut() {
|
||||
return ctx.current?.logOut();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useAccountOrGuest(): { me: RegisteredAccount | AnonymousJazzAgent };
|
||||
export function useAccountOrGuest<R extends RefsToResolve<RegisteredAccount>>(
|
||||
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
|
||||
): { me: Resolved<RegisteredAccount, R> | undefined | null| AnonymousJazzAgent };
|
||||
export function useAccountOrGuest<R extends RefsToResolve<RegisteredAccount>>(
|
||||
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
|
||||
): { me: RegisteredAccount | Resolved<RegisteredAccount, R> | undefined | null| AnonymousJazzAgent } {
|
||||
const ctx = getJazzContext<RegisteredAccount>();
|
||||
|
||||
if (!ctx?.current) {
|
||||
throw new Error('useAccountOrGuest must be used within a JazzProvider');
|
||||
}
|
||||
|
||||
const contextMe = 'me' in ctx.current ? ctx.current.me : undefined;
|
||||
|
||||
const me = useCoState<RegisteredAccount, R>(
|
||||
contextMe?.constructor as CoValueClass<RegisteredAccount>,
|
||||
contextMe?.id,
|
||||
options
|
||||
);
|
||||
|
||||
// If the context has a me, return the account.
|
||||
if ('me' in ctx.current) {
|
||||
return {
|
||||
get me() {
|
||||
return options?.resolve === undefined
|
||||
? me.current || (ctx.current as JazzAuthContext<RegisteredAccount>)?.me
|
||||
: me.current;
|
||||
}
|
||||
};
|
||||
}
|
||||
// If the context has no me, return the guest.
|
||||
else {
|
||||
return {
|
||||
get me() {
|
||||
return (ctx.current as JazzGuestContext)?.guest;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function useCoState<V extends CoValue, R extends RefsToResolve<V>>(
|
||||
Schema: CoValueClass<V>,
|
||||
id: ID<CoValue> | undefined | (() => ID<CoValue> | undefined),
|
||||
options?: { resolve?: RefsToResolveStrict<V, R> }
|
||||
): {
|
||||
current: Resolved<V, R> | undefined | null;
|
||||
} {
|
||||
const ctx = getJazzContext<RegisteredAccount>();
|
||||
|
||||
// Create state and a stable observable
|
||||
let state = $state.raw<Resolved<V, R> | undefined | null>(undefined);
|
||||
|
||||
// Effect to handle subscription
|
||||
$effect(() => {
|
||||
// Reset state when dependencies change
|
||||
state = undefined;
|
||||
|
||||
const idValue = typeof id === 'function' ? id() : id;
|
||||
|
||||
// Return early if no context or id, effectively cleaning up any previous subscription
|
||||
if (!ctx?.current || !idValue) return;
|
||||
|
||||
const agent = "me" in ctx.current ? ctx.current.me : ctx.current.guest;
|
||||
|
||||
// Setup subscription with current values
|
||||
return subscribeToCoValue<V, R>(
|
||||
Schema,
|
||||
idValue,
|
||||
{
|
||||
resolve: options?.resolve,
|
||||
loadAs: agent,
|
||||
onUnavailable: () => {
|
||||
state = null;
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
state = null;
|
||||
},
|
||||
syncResolution: true,
|
||||
},
|
||||
(value) => {
|
||||
// Get current value from our stable observable
|
||||
state = value;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return state;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the accept invite hook.
|
||||
* @param invitedObjectSchema - The invited object schema.
|
||||
@@ -218,37 +63,32 @@ export function useCoState<V extends CoValue, R extends RefsToResolve<V>>(
|
||||
* @param forValueHint - Hint for the value.
|
||||
* @returns The accept invite hook.
|
||||
*/
|
||||
export function useAcceptInvite<V extends CoValue>({
|
||||
export function useAcceptInvite<V extends CoValueOrZodSchema>({
|
||||
invitedObjectSchema,
|
||||
onAccept,
|
||||
forValueHint
|
||||
}: {
|
||||
invitedObjectSchema: CoValueClass<V>;
|
||||
invitedObjectSchema: V;
|
||||
onAccept: (projectID: ID<V>) => void;
|
||||
forValueHint?: string;
|
||||
}): void {
|
||||
const ctx = getJazzContext<RegisteredAccount>();
|
||||
const _onAccept = onAccept;
|
||||
|
||||
if (!ctx.current) {
|
||||
throw new Error('useAcceptInvite must be used within a JazzProvider');
|
||||
}
|
||||
|
||||
if (!('me' in ctx.current)) {
|
||||
throw new Error("useAcceptInvite can't be used in a JazzProvider with auth === 'guest'.");
|
||||
}
|
||||
|
||||
// Subscribe to the onAccept function.
|
||||
$effect(() => {
|
||||
const ctx = getJazzContext<InstanceOfSchema<AccountClass<Account>>>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
_onAccept;
|
||||
// Subscribe to the onAccept function.
|
||||
untrack(() => {
|
||||
// If there is no context, return.
|
||||
if (!ctx.current) return;
|
||||
if (!('me' in ctx.current)) return;
|
||||
|
||||
// Consume the invite link from the window location.
|
||||
const result = consumeInviteLinkFromWindowLocation({
|
||||
as: (ctx.current as JazzAuthContext<RegisteredAccount>).me,
|
||||
as: ctx.current.me,
|
||||
invitedObjectSchema,
|
||||
forValueHint
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createJazzTestAccount, createJazzTestContext, setupJazzTestSync } from
|
||||
import UpdateNestedValue from './components/CoState/UpdateNestedValue.svelte';
|
||||
import { Person } from './components/CoState/schema.js';
|
||||
import { Dog } from './components/CoState/schema.js';
|
||||
import type { Loaded } from 'jazz-tools';
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupJazzTestSync();
|
||||
@@ -13,7 +14,7 @@ beforeEach(async () => {
|
||||
});
|
||||
});
|
||||
|
||||
function setup(person: Person) {
|
||||
function setup(person: Loaded<typeof Person>) {
|
||||
const result = render(UpdateNestedValue, {
|
||||
context: createJazzTestContext(),
|
||||
props: { id: person.id }
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts" module>
|
||||
export type Props = {
|
||||
id: ID<Person>;
|
||||
id: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { CoState } from '../../../jazz.class.svelte.js';
|
||||
|
||||
import { type ID } from 'jazz-tools';
|
||||
import { Person } from './schema.js';
|
||||
|
||||
let props: Props = $props();
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
<script lang="ts" module>
|
||||
export type Props = {
|
||||
id: ID<Person>;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { AccountCoState } from '../../../jazz.class.svelte.js';
|
||||
|
||||
import { type ID } from 'jazz-tools';
|
||||
import { MyAccount, Person } from './schema.js';
|
||||
import { MyAccount } from './schema.js';
|
||||
|
||||
const person = new AccountCoState<MyAccount, { root: { dog: true } }>({
|
||||
const me = new AccountCoState(MyAccount, {
|
||||
resolve: {
|
||||
root: {
|
||||
dog: true
|
||||
@@ -22,13 +15,13 @@
|
||||
<!-- Using non-null assertions because we want to test that locally available values are never null -->
|
||||
<label>
|
||||
Name
|
||||
<input type="text" bind:value={person.current!.root.name} />
|
||||
<input type="text" bind:value={me.current!.root.name} />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Dog
|
||||
<input type="text" bind:value={person.current!.root.dog.name} />
|
||||
<input type="text" bind:value={me.current!.root.dog.name} />
|
||||
</label>
|
||||
|
||||
<div data-testid="person-name">{person.current!.root.name}</div>
|
||||
<div data-testid="person-dog-name">{person.current!.root.dog.name}</div>
|
||||
<div data-testid="person-name">{me.current!.root.name}</div>
|
||||
<div data-testid="person-dog-name">{me.current!.root.dog.name}</div>
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { Account, coField, CoMap } from "jazz-tools";
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
export class Dog extends CoMap {
|
||||
name = coField.string;
|
||||
}
|
||||
export const Dog = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export class Person extends CoMap {
|
||||
name = coField.string;
|
||||
age = coField.number;
|
||||
dog = coField.ref(Dog);
|
||||
}
|
||||
export const Person = co.map({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
dog: Dog,
|
||||
});
|
||||
|
||||
export class MyAccount extends Account {
|
||||
root = coField.ref(Person);
|
||||
|
||||
migrate() {
|
||||
if (!this._refs.root) {
|
||||
this.root = Person.create({ name: "John", age: 30, dog: Dog.create({ name: "Rex" }, this) }, this);
|
||||
}
|
||||
export const MyAccount = co.account({
|
||||
profile: co.profile(),
|
||||
root: Person,
|
||||
}).withMigration((account) => {
|
||||
if (!account._refs.root) {
|
||||
account.root = Person.create({ name: "John", age: 30, dog: Dog.create({ name: "Rex" }, account) }, account);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts" module>
|
||||
export type Props<R extends CoValue> = {
|
||||
invitedObjectSchema: CoValueClass<R>;
|
||||
export type Props<R extends CoValueOrZodSchema> = {
|
||||
invitedObjectSchema: R;
|
||||
onAccept: (id: ID<R>) => void;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" generics="R extends CoValue">
|
||||
<script lang="ts" generics="R extends CoValueOrZodSchema">
|
||||
import { useAcceptInvite } from '../../jazz.svelte.js';
|
||||
import type { CoValue, CoValueClass, ID } from 'jazz-tools';
|
||||
import type { CoValueOrZodSchema, ID } from 'jazz-tools';
|
||||
|
||||
let { invitedObjectSchema, onAccept }: Props<R> = $props();
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
export type Props = {
|
||||
depth?: RefsToResolve<RegisteredAccount>;
|
||||
setResult: (result: ReturnType<typeof useAccount> | undefined) => void;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { useAccount, type RegisteredAccount } from '../../jazz.svelte.js';
|
||||
import type { RefsToResolve } from 'jazz-tools';
|
||||
|
||||
let { depth, setResult }: Props = $props();
|
||||
|
||||
if (depth) {
|
||||
const result = $derived(useAccount({
|
||||
resolve: depth
|
||||
}));
|
||||
|
||||
$effect(() => {
|
||||
setResult(result);
|
||||
});
|
||||
} else {
|
||||
const result = $derived(useAccount());
|
||||
|
||||
$effect(() => {
|
||||
setResult(result);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,29 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
export type Props = {
|
||||
depth?: RefsToResolve<RegisteredAccount>;
|
||||
setResult: (result: ReturnType<typeof useAccountOrGuest> | undefined) => void;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { useAccountOrGuest, type RegisteredAccount } from '../../jazz.svelte.js';
|
||||
import type { RefsToResolve } from 'jazz-tools';
|
||||
|
||||
let { depth, setResult }: Props = $props();
|
||||
|
||||
if (depth) {
|
||||
const result = $derived(useAccountOrGuest({
|
||||
resolve: depth
|
||||
}));
|
||||
|
||||
$effect(() => {
|
||||
setResult(result);
|
||||
});
|
||||
} else {
|
||||
const result = $derived(useAccountOrGuest());
|
||||
|
||||
$effect(() => {
|
||||
setResult(result);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
export type Props<R extends CoValue> = {
|
||||
Schema: CoValueClass<R>;
|
||||
id: ID<R>;
|
||||
resolve: RefsToResolve<R>;
|
||||
setResult: (result: R | undefined | null) => void;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" generics="R extends CoValue">
|
||||
import { useCoState } from '../../jazz.svelte.js';
|
||||
import type { CoValue, CoValueClass, RefsToResolve, ID } from 'jazz-tools';
|
||||
|
||||
let { Schema, id, resolve, setResult }: Props<R> = $props();
|
||||
|
||||
const result = $derived(useCoState(Schema, id, { resolve: resolve as any }));
|
||||
|
||||
$effect(() => {
|
||||
setResult(result.current);
|
||||
});
|
||||
</script>
|
||||
@@ -1,13 +1,13 @@
|
||||
import { render, waitFor } from "@testing-library/svelte";
|
||||
import { Account, CoMap, Group, coField, type CoValue, type CoValueClass, type ID } from "jazz-tools";
|
||||
import { Account, Group, co, z, type CoValue, type CoValueOrZodSchema, type ID } from "jazz-tools";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createInviteLink } from "../index.js";
|
||||
import { createJazzTestAccount, createJazzTestContext, linkAccounts } from "../testing.js";
|
||||
import UseAcceptInvite from "./components/useAcceptInvite.svelte";
|
||||
|
||||
function setup<T extends CoValue>(options: {
|
||||
function setup<T extends CoValueOrZodSchema>(options: {
|
||||
account: Account;
|
||||
invitedObjectSchema: CoValueClass<T>;
|
||||
invitedObjectSchema: CoValueOrZodSchema;
|
||||
}) {
|
||||
const result = { current: undefined } as { current: ID<T> | undefined };
|
||||
|
||||
@@ -26,9 +26,9 @@ function setup<T extends CoValue>(options: {
|
||||
|
||||
describe("useAcceptInvite", () => {
|
||||
it("should accept the invite", async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
const TestMap = co.map({
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
const account = await createJazzTestAccount();
|
||||
const inviteSender = await createJazzTestAccount();
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { render } from "@testing-library/svelte";
|
||||
import { Account, CoMap, coField, type RefsToResolve } from "jazz-tools";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useAccount, type RegisteredAccount } from "../index.js";
|
||||
import { createJazzTestAccount, createJazzTestContext } from "../testing.js";
|
||||
import UseAccount from "./components/useAccount.svelte";
|
||||
|
||||
function setup(options: {
|
||||
account: RegisteredAccount;
|
||||
depth?: RefsToResolve<RegisteredAccount>;
|
||||
}) {
|
||||
const result = { current: undefined } as { current: ReturnType<typeof useAccount> | undefined };
|
||||
|
||||
render(UseAccount, {
|
||||
context: createJazzTestContext({ account: options.account }),
|
||||
props: {
|
||||
depth: options.depth ?? {},
|
||||
setResult: (value) => {
|
||||
result.current = value
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
declare module "../jazz.svelte.js" {
|
||||
interface Register {
|
||||
Account: AccountSchema;
|
||||
}
|
||||
}
|
||||
|
||||
class AccountRoot extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
|
||||
class AccountSchema extends Account {
|
||||
root = coField.ref(AccountRoot);
|
||||
|
||||
migrate() {
|
||||
if (!this._refs.root) {
|
||||
this.root = AccountRoot.create({ value: "123" }, { owner: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("useAccount", () => {
|
||||
it("should return the correct value", async () => {
|
||||
const account = await createJazzTestAccount({
|
||||
AccountSchema,
|
||||
});
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result.current?.me).toEqual(account);
|
||||
});
|
||||
|
||||
it("should load nested values if requested", async () => {
|
||||
const account = await createJazzTestAccount({
|
||||
AccountSchema,
|
||||
});
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
depth: {
|
||||
root: {}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.current?.me?.root?.value).toBe("123");
|
||||
});
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
import { render } from "@testing-library/svelte";
|
||||
import { Account, AnonymousJazzAgent, CoMap, coField, type RefsToResolve } from "jazz-tools";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useAccountOrGuest, type RegisteredAccount } from "../index.js";
|
||||
import { createJazzTestAccount, createJazzTestContext, createJazzTestGuest } from "../testing.js";
|
||||
import UseAccountOrGuest from "./components/useAccountOrGuest.svelte";
|
||||
|
||||
function setup(options: {
|
||||
account: RegisteredAccount | { guest: AnonymousJazzAgent };
|
||||
depth?: RefsToResolve<RegisteredAccount>;
|
||||
}) {
|
||||
const result = { current: undefined } as { current: ReturnType<typeof useAccountOrGuest> | undefined };
|
||||
|
||||
render(UseAccountOrGuest, {
|
||||
context: createJazzTestContext({ account: options.account }),
|
||||
props: {
|
||||
depth: options.depth ?? {},
|
||||
setResult: (value) => {
|
||||
result.current = value
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
declare module "../jazz.svelte.js" {
|
||||
interface Register {
|
||||
Account: AccountSchema;
|
||||
}
|
||||
}
|
||||
|
||||
class AccountRoot extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
|
||||
class AccountSchema extends Account {
|
||||
root = coField.ref(AccountRoot);
|
||||
|
||||
migrate() {
|
||||
if (!this._refs.root) {
|
||||
this.root = AccountRoot.create({ value: "123" }, { owner: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("useAccountOrGuest", () => {
|
||||
it("should return the correct value", async () => {
|
||||
const account = await createJazzTestAccount({
|
||||
AccountSchema,
|
||||
});
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result.current?.me).toEqual(account);
|
||||
});
|
||||
|
||||
it("should return the guest agent if the account is a guest", async () => {
|
||||
const account = await createJazzTestGuest();
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result.current?.me).toEqual(account.guest);
|
||||
});
|
||||
|
||||
it("should load nested values if requested", async () => {
|
||||
const account = await createJazzTestAccount({
|
||||
AccountSchema,
|
||||
});
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
depth: {
|
||||
root: {}
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-expect-error Skipping the guest check
|
||||
expect(result.current?.me?.root?.value).toBe("123");
|
||||
});
|
||||
});
|
||||
@@ -1,217 +0,0 @@
|
||||
import { render, waitFor } from '@testing-library/svelte';
|
||||
import {
|
||||
Account,
|
||||
CoMap,
|
||||
coField,
|
||||
cojsonInternals,
|
||||
type CoValue,
|
||||
type CoValueClass,
|
||||
type RefsToResolve,
|
||||
} from 'jazz-tools';
|
||||
import { describe, expect, it, expectTypeOf, beforeEach } from 'vitest';
|
||||
import { createJazzTestAccount, createJazzTestContext, setupJazzTestSync } from '../testing.js';
|
||||
import UseCoState from './components/useCoState.svelte';
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupJazzTestSync();
|
||||
});
|
||||
|
||||
cojsonInternals.setCoValueLoadingRetryDelay(300);
|
||||
|
||||
function setup<T extends CoValue>(options: { account: Account; map: T; resolve?: RefsToResolve<T> }) {
|
||||
const result = { current: undefined } as { current: T | undefined };
|
||||
|
||||
render(UseCoState, {
|
||||
context: createJazzTestContext({ account: options.account }),
|
||||
props: {
|
||||
Schema: options.map.constructor as CoValueClass<T>,
|
||||
id: options.map.id,
|
||||
resolve: options.resolve ?? true,
|
||||
setResult: (value) => {
|
||||
result.current = value as T;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('useCoState', () => {
|
||||
it('should return the correct value', async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
|
||||
const account = await createJazzTestAccount();
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: '123'
|
||||
},
|
||||
{ owner: account }
|
||||
);
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
map
|
||||
});
|
||||
|
||||
expect(result.current?.value).toBe('123');
|
||||
});
|
||||
|
||||
it('should update the value when the coValue changes', async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
|
||||
const account = await createJazzTestAccount();
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: '123'
|
||||
},
|
||||
{ owner: account }
|
||||
);
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
map
|
||||
});
|
||||
|
||||
expect(result.current?.value).toBe('123');
|
||||
|
||||
map.value = '456';
|
||||
|
||||
expect(result.current?.value).toBe('456');
|
||||
});
|
||||
|
||||
it('should load nested values if requested', async () => {
|
||||
class TestNestedMap extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
|
||||
class TestMap extends CoMap {
|
||||
value = coField.string;
|
||||
nested = coField.ref(TestNestedMap);
|
||||
}
|
||||
|
||||
const account = await createJazzTestAccount();
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: '123',
|
||||
nested: TestNestedMap.create(
|
||||
{
|
||||
value: '456'
|
||||
},
|
||||
{ owner: account }
|
||||
)
|
||||
},
|
||||
{ owner: account }
|
||||
);
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
map,
|
||||
resolve: {
|
||||
nested: {}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.current?.value).toBe('123');
|
||||
expect(result.current?.nested?.value).toBe('456');
|
||||
});
|
||||
|
||||
it('should load nested values on access even if not requested', async () => {
|
||||
class TestNestedMap extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
|
||||
class TestMap extends CoMap {
|
||||
value = coField.string;
|
||||
nested = coField.ref(TestNestedMap);
|
||||
}
|
||||
|
||||
const account = await createJazzTestAccount();
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: '123',
|
||||
nested: TestNestedMap.create(
|
||||
{
|
||||
value: '456'
|
||||
},
|
||||
{ owner: account }
|
||||
)
|
||||
},
|
||||
{ owner: account }
|
||||
);
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
map,
|
||||
resolve: {
|
||||
nested: {}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.current?.value).toBe('123');
|
||||
expect(result.current?.nested?.value).toBe('456');
|
||||
});
|
||||
|
||||
it('should return null if the coValue is not found', async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
|
||||
const unreachableAccount = await createJazzTestAccount({});
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: '123'
|
||||
},
|
||||
unreachableAccount
|
||||
);
|
||||
|
||||
unreachableAccount._raw.core.node.gracefulShutdown();
|
||||
|
||||
const account = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true
|
||||
});
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
map
|
||||
});
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the same type as Schema', async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = coField.string;
|
||||
}
|
||||
|
||||
const account = await createJazzTestAccount();
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: '123'
|
||||
},
|
||||
{ owner: account }
|
||||
);
|
||||
|
||||
const result = setup({
|
||||
account,
|
||||
map
|
||||
});
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<{
|
||||
current: TestMap | undefined;
|
||||
}>();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user