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 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 = co.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 = co.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); } } }
Warning

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.

Note

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.members doesn't include the everyone member anymore
  • the account type in Group.members is now the globally registered Account schema and we have removed the co.members way 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

  1. Review your member querying logic to account for inherited members.
  2. Update your permission checking code to utilize the new hasPermissions and getRoleOf methods.
  3. 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 = co.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, ); } } }