Jazz 0.12.0 - Deeply resolved data
Jazz 0.12.0 makes it easier and safer to load nested data. You can now specify exactly which nested data you want to load, and Jazz will check permissions and handle missing data gracefully. This helps catch errors earlier during development and makes your code more reliable.
What's new?
- New resolve API for a more type-safe deep loading
- A single, consistent load option for all loading methods
- Improved permission checks on deep loading
- Easier type safety with the
Resolvedtype helper
Breaking changes
New Resolve API
We're introducing a new resolve API for deep loading, more friendly to TypeScript, IDE autocompletion and LLMs.
Major changes:
- Functions and hooks for loading now take the resolve query as an explicit nested
resolveprop - Shallowly loading a collection is now done with
trueinstead of[]or{}
const { me } = useAccount({ root: { friends: [] } }); // After const { me } = useAccount({ resolve: { root: { friends: true } } });
- For collections, resolving items deeply is now done with a special
$eachkey.
For a CoList:
class Task extends CoMap { } class ListOfTasks extends CoList.Of(coField.ref(Task)) {} const id = "co_123" as ID<Task>; // Before // @ts-expect-error const tasks = useCoState(ListOfTasks, id, [{}]); // After const tasks = useCoState(ListOfTasks, id, { resolve: { $each: true } });
For a CoMap.Record:
class UsersByUsername extends CoMap.Record(coField.ref(MyAppAccount)) {} // Before // @ts-expect-error const usersByUsername = useCoState(UsersByUsername, id, [{}]); // After const usersByUsername = useCoState(UsersByUsername, id, { resolve: { $each: true } });
Nested loading — note how it's now less terse, but more readable:
class Org extends CoMap { name = coField.string; } class Assignee extends CoMap { name = coField.string; org = coField.ref(Org); } class ListOfAssignees extends CoList.Of(coField.ref(Assignee)) {} class Task extends CoMap { content = coField.string; assignees = coField.ref(ListOfAssignees); } class ListOfTasks extends CoList.Of(coField.ref(Task)) {} // Before // @ts-expect-error const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, [{ assignees: [{ org: {}}]} ]); // After const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, { resolve: { $each: { assignees: { $each: { org: true } } } } });
It's also a lot more auto-complete friendly:
const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, { resolve: { $each: { assignees: { $ // ^| } } } });
A single, consistent load option
The new API works across all loading methods, and separating out the resolve query means other options with default values are easier to manage, for example: loading a value as a specific account instead of using the implicit current account:
// Before // @ts-expect-error Playlist.load(id, otherAccount, { tracks: [], }); // After Playlist.load(id, { loadAs: otherAccount, resolve: { tracks: true } });
Improved permission checks on deep loading
Now useCoState will return null when the current user lacks permissions to load requested data.
Previously, useCoState would return undefined if the current user lacked permissions, making it hard to tell if the value is loading or if it's missing.
Now undefined means that the value is definitely loading, and null means that the value is temporarily missing.
We also have implemented a more granular permission checking, where if an optional CoValue cannot be accessed, useCoState will return the data stripped of that CoValue.
Note: The state handling around loading and error states will become more detailed and easy-to-handle in future releases, so this is just a small step towards consistency.
class ListOfTracks extends CoList.Of(coField.optional.ref(Track)) {} function TrackListComponent({ id }: { id: ID<ListOfTracks> }) { // Before (ambiguous states) // @ts-expect-error const tracks = useCoState(ListOfTracks, id, [{}]); if (tracks === undefined) return <div>Loading or access denied</div>; if (tracks === null) return <div>Not found</div>; // After const tracks = useCoState(ListOfTracks, id, { resolve: { $each: true } }); if (tracks === undefined) return <div>Loading...</div>; if (tracks === null) return <div>Not found or access denied</div>; // This will only show tracks that we have access to and that are loaded. return tracks.map(track => track && <TrackComponent track={track} />); }
The same change is applied to the load function, so now it returns null instead of undefined when the value is missing.
// Before // @ts-expect-error const map = await MyCoMap.load(id); if (map === undefined) { throw new Error("Map not found"); } // After const map = await MyCoMap.load(id); if (map === null) { throw new Error("Map not found or access denied"); }
New Features
The Resolved type helper
The new Resolved type can be used to define what kind of deeply loaded data you expect in your parameters, using the same resolve query syntax as the new loading APIs:
type PlaylistResolved = Resolved<Playlist, { tracks: { $each: true } }>; function TrackListComponent({ playlist }: { playlist: PlaylistResolved }) { // Safe access to resolved tracks return playlist.tracks.map(track => <TrackComponent track={track} />); }