Jazz 0.11.0 is out!
Jazz 0.11.0 brings several improvements to member handling, roles, and permissions management. This guide will help you upgrade your application to the latest version.
What's new?
Here is what's changed in this release:
- New permissions check APIs: New methods like
canRead,canWrite,canAdmin, andgetRoleOfto simplify permission checks. - Group.revokeExtend: New method to revoke group extension permissions.
- Group.getParentGroups: New method to get all the parent groups of an account.
- Account Profile & Migrations: Fixed issues with custom account profile migrations for a more consistent experience
- Dropped support for Accounts owning Profiles: Profiles can now only be owned by Groups.
- Group.members now includes inherited members: Updated behavior for the
membersgetter method to include inherited members and have a more intuitive type definition.
New Features
New permissions check APIs
New methods have been added to both Account and Group classes to improve permission handling:
Permission checks
The new canRead, canWrite and canAdmin methods on Account allow you to easily check if the account has specific permissions on a CoValue:
const me = Account.getMe(); if (me.canAdmin(value)) { console.log("I can share value with others"); } else if (me.canWrite(value)) { console.log("I can edit value"); } else if (me.canRead(value)) { console.log("I can view value"); } else { console.log("I cannot access value"); }
Getting the role of an account
The getRoleOf method has been added to query the role of specific entities:
const group = Group.create(); group.getRoleOf(me); // admin group.getRoleOf(Eve); // undefined group.addMember(Eve, "writer"); group.getRoleOf(Eve); // writer
Group.revokeExtend
We added a new method to revoke the extend of a Group:
function addTrackToPlaylist(playlist: Playlist, track: MusicTrack) { const trackGroup = track._owner.castAs(Group); trackGroup.extend(playlist._owner, "reader"); // Grant read access to the track to the playlist accounts playlist.tracks.push(track); } function removeTrackFromPlaylist(playlist: Playlist, track: MusicTrack) { const trackGroup = track._owner.castAs(Group); trackGroup.revokeExtend(playlist._owner); // Revoke access to the track to the playlist accounts const index = playlist.tracks.findIndex(t => t.id === track.id); if (index !== -1) { playlist.tracks.splice(index, 1); } }
Group.getParentGroups
The getParentGroups method has been added to Group to get all the parent groups of a group.
const childGroup = Group.create(); const parentGroup = Group.create(); childGroup.extend(parentGroup); console.log(childGroup.getParentGroups()); // [parentGroup]
Breaking Changes
Account Profile & Migrations
The previous way of making the Profile migration work was to assume that the profile was always already there:
export class MyAppAccount extends Account { profile = coField.ref(MyAppProfile); async migrate(this: MyAppAccount, creationProps: { name: string, lastName: string }) { if (creationProps) { const { profile } = await this.ensureLoaded({ profile: {} }); profile.name = creationProps.name; profile.bookmarks = ListOfBookmarks.create([], profileGroup); } } }
This was kind-of tricky to picture, and having different migration strategies for different CoValues was confusing.
We changed the logic so the default profile is created only if you didn't provide one in your migration.
This way you can use the same pattern for both root and profile migrations:
export class MyAppAccount extends Account { profile = coField.ref(MyAppProfile); async migrate(this: MyAppAccount, creationProps?: { name: string }) { if (this.profile === undefined) { const profileGroup = Group.create(); profileGroup.addMember("everyone", "reader"); this.profile = MyAppProfile.create({ name: creationProps?.name, bookmarks: ListOfBookmarks.create([], profileGroup), }, profileGroup); } } }
If you provide a custom Profile in your Account schema and migration for a Worker account,
make sure to also add everyone as member with reader role to the owning group.
Failing to do so will prevent any account from sending messages to the Worker's Inbox.
Dropped support for Accounts owning Profiles
Starting from 0.11.0 Profiles can only be owned by Groups.
Existing profiles owned by Accounts will still work, but you will get incorrect types when accessing a Profile's _owner.
Member Inheritance Changes
The behavior of groups' members getter method has been updated to return both direct members and inherited ones from ancestor groups.
This might affect your application if you were relying on only direct members being returned.
/** * The following pseudocode only illustrates the inheritance logic, * the actual implementation is different. */ const parentGroup = Group.create(); parentGroup.addMember(John, "admin"); const childGroup = Group.create(); childGroup.addMember(Eve, "admin"); childGroup.extend(parentGroup); console.log(childGroup.members); // Before 0.11.0 // [Eve] // After 0.11.0 // [Eve, John]
Additionally:
- now
Group.membersdoesn't include theeveryonemember anymore - the account type in
Group.membersis now the globally registered Account schema and we have removed theco.membersway to define an AccountSchema for members
If you need to explicitly check if "everyone" is a member of a group, you can use the getRoleOf method instead:
if (group.getRoleOf("everyone")) { console.log("Everyone has access to the group"); }
Migration Steps
- Review your member querying logic to account for inherited members.
- Update your permission checking code to utilize the new
hasPermissionsandgetRoleOfmethods. - Consider implementing
"everyone"role checks where appropriate.
Removed auto-update of profile.name in usePasskeyAuth
The usePasskeyAuth hook now doesn't update the profile.name if the provided username is empty.
Troubleshooting
I'm getting the following error:
Error: Profile must be owned by a Group
If you previously forced a migration of your Account schema to include a custom Profile,
and assigned its ownership to an Account, you need to recreate your profile code and assign it to a Group instead.
export class MyAppAccount extends Account { profile = coField.ref(MyAppProfile); override async migrate() { // ... const me = await this.ensureLoaded({ profile: {}, }); if ((me.profile._owner as Group | Account)._type === "Account") { const profileGroup = Group.create(); profileGroup.addMember("everyone", "reader"); // recreate your profile here... me.profile = Profile.create( { name: me.profile.name, }, profileGroup, ); } } }