Groups as members
Groups can be added to other groups using the addMember method.
When a group is added as a member of another group, members of the added group will become part of the containing group.
Basic usage
Here's how to add a group as a member of another group:
constconst playlistGroup: GroupplaylistGroup =class GroupGroup.create(); constGroup.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst trackGroup: GrouptrackGroup =class GroupGroup.create(); // Tracks are now visible to the members of playlistGroup.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst trackGroup: GrouptrackGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const playlistGroup: GroupplaylistGroup);
When you add groups as members:
- Members of the added group become members of the container group
- Their roles are inherited (with some exceptions, see below)
- Revoking access from the member group also removes its access to the container group
Levels of inheritance
Adding a group as a member of another is not limited in depth:
constconst grandParentGroup: GroupgrandParentGroup =class GroupGroup.create(); constGroup.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst parentGroup: GroupparentGroup =class GroupGroup.create(); constGroup.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst childGroup: GroupchildGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst childGroup: GroupchildGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const parentGroup: GroupparentGroup);const parentGroup: GroupparentGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const grandParentGroup: GroupgrandParentGroup);
Members of the grandparent group will get access to all descendant groups based on their roles.
Roles
The rules of role inheritance
If the account is already a member of the container group, it will get the more permissive role:
constconst addedGroup: GroupaddedGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst addedGroup: GroupaddedGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(bob, "reader"); constconst bob: Account | ({ readonly [x: string]: any; } & Account)const containingGroup: GroupcontainingGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst addedGroup: GroupaddedGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(bob, "writer");const bob: Account | ({ readonly [x: string]: any; } & Account)const containingGroup: GroupcontainingGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const addedGroup: GroupaddedGroup); // Bob stays a writer because his role is higher // than the inherited reader role.
When adding a group to another group, only admin, writer and reader roles are inherited:
constconst addedGroup: GroupaddedGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst addedGroup: GroupaddedGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(bob, "writeOnly"); constconst bob: Account | ({ readonly [x: string]: any; } & Account)const containingGroup: GroupcontainingGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst containingGroup: GroupcontainingGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const addedGroup: GroupaddedGroup); // Bob does not become a member of the containing group
Overriding the added group's roles
In some cases you might want to inherit all members from an added group but override their roles to the same specific role in the containing group. You can do so by passing an "override role" as a second argument to addMember:
constconst organizationGroup: GrouporganizationGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst organizationGroup: GrouporganizationGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(bob, "admin"); constconst bob: Account | ({ readonly [x: string]: any; } & Account)const billingGroup: GroupbillingGroup =class GroupGroup.create(); // This way the members of the organization // can only read the billing dataGroup.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst billingGroup: GroupbillingGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const organizationGroup: GrouporganizationGroup, "reader");
The "override role" works in both directions:
constconst addedGroup: GroupaddedGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst addedGroup: GroupaddedGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(bob, "reader");const bob: Account | ({ readonly [x: string]: any; } & Account)const addedGroup: GroupaddedGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(alice, "admin"); constconst alice: Account | ({ readonly [x: string]: any; } & Account)const containingGroup: GroupcontainingGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst containingGroup: GroupcontainingGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const addedGroup: GroupaddedGroup, "writer"); // Bob and Alice are now writers in the containing group
Permission changes
When you remove a member from an added group, they automatically lose access to all containing groups. We handle key rotation automatically to ensure security.
// Remove member from added group awaitconst addedGroup: GroupaddedGroup.Group.removeMember(member: Everyone | Account): void (+1 overload)removeMember(bob); // Bob loses access to both groups. // If Bob was also a member of the containing group, // he wouldn't have lost access.const bob: Account | ({ readonly [x: string]: any; } & Account)
Removing groups from other groups
You can remove a group from another group by using the removeMember method:
constconst addedGroup: GroupaddedGroup =class GroupGroup.create(); constGroup.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst containingGroup: GroupcontainingGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst containingGroup: GroupcontainingGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const addedGroup: GroupaddedGroup); // Revoke the extension awaitconst containingGroup: GroupcontainingGroup.Group.removeMember(member: Group): void (+1 overload)removeMember(const addedGroup: GroupaddedGroup);
Getting all added groups
You can get all of the groups added to a group by calling the getParentGroups method:
constconst containingGroup: GroupcontainingGroup =class GroupGroup.create(); constGroup.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst addedGroup: GroupaddedGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst containingGroup: GroupcontainingGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const addedGroup: GroupaddedGroup);var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(const containingGroup: GroupcontainingGroup.Group.getParentGroups(): Array<Group>getParentGroups()); // [addedGroup]
Ownership on implicit CoValue creation
When creating CoValues that contain other CoValues (or updating references to CoValues) using plain JSON objects, Jazz not only creates the necessary CoValues automatically but it will also manage their group ownership.
constconst Task: co.PlainTextTask =import coco.plainText(); constfunction plainText(): co.PlainText export plainTextconst Column: co.List<co.PlainText>Column =import coco.list(list<co.PlainText>(element: co.PlainText): co.List<co.PlainText> export listconst Task: co.PlainTextTask); constBoard =const Board: co.Map<{ title: z.z.ZodString; columns: co.List<co.List<co.PlainText>>; }, unknown, Account | Group>import coco.map({map<{ title: z.z.ZodString; columns: co.List<co.List<co.PlainText>>; }>(shape: { title: z.z.ZodString; columns: co.List<co.List<co.PlainText>>; }): co.Map<{ title: z.z.ZodString; columns: co.List<co.List<co.PlainText>>; }, unknown, Account | Group> export maptitle: z.z.ZodStringtitle:import zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringcolumns: co.List<co.List<co.PlainText>>columns:import coco.list(list<co.List<co.PlainText>>(element: co.List<co.PlainText>): co.List<co.List<co.PlainText>> export listconst Column: co.List<co.PlainText>Column), }); constboard =const board: { readonly title: string; readonly columns: CoList<CoList<CoPlainText>>; } & CoMapBoard.const Board: co.Map<{ title: z.z.ZodString; columns: co.List<co.List<co.PlainText>>; }, unknown, Account | Group>create({CoMapSchema<{ title: ZodString; columns: CoListSchema<CoListSchema<PlainTextSchema>>; }, unknown, Account | Group>.create(init: { title: string; columns: CoList<CoList<CoPlainText | null> | null> | readonly (CoList<CoPlainText | null> | readonly (string | CoPlainText)[])[]; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)title: stringtitle: "My board",columns: CoList<CoList<CoPlainText | null> | null> | readonly (CoList<CoPlainText | null> | readonly (string | CoPlainText)[])[]columns: [ ["Task 1.1", "Task 1.2"], ["Task 2.1", "Task 2.2"], ], });
For each created column and task CoValue, Jazz also creates a new group as its owner and adds the referencing CoValue's owner as a member of that group. This means permissions for nested CoValues are inherited from the CoValue that references them, but can also be modified independently for each CoValue if needed.
constconst writeAccess: GroupwriteAccess =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst writeAccess: GroupwriteAccess.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(const bob: Accountbob, "writer"); // Give Bob write access to the board, columns and tasks constboard =const board: { readonly title: string; readonly columns: CoList<CoList<CoPlainText>>; } & CoMapBoard.const Board: co.Map<{ title: z.z.ZodString; columns: co.List<co.List<co.PlainText>>; }, unknown, Group | Account>create({CoMapSchema<{ title: ZodString; columns: CoListSchema<CoListSchema<PlainTextSchema>>; }, unknown, Group | Account>.create(init: { title: string; columns: CoList<CoList<CoPlainText | null> | null> | readonly (CoList<CoPlainText | null> | readonly (string | CoPlainText)[])[]; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)title: stringtitle: "My board",columns: CoList<CoList<CoPlainText | null> | null> | readonly (CoList<CoPlainText | null> | readonly (string | CoPlainText)[])[]columns: [ ["Task 1.1", "Task 1.2"], ["Task 2.1", "Task 2.2"], ], },const writeAccess: GroupwriteAccess); // Give Alice read access to one specific task constconst task: CoPlainTexttask =board.const board: { readonly title: string; readonly columns: CoList<CoList<CoPlainText>>; } & CoMapcolumns: CoList<CoList<CoPlainText>>columns[0][0]; constconst taskGroup: GrouptaskGroup =const task: CoPlainTexttask.CoPlainText.$jazz: CoTextJazzApi<CoPlainText>$jazz.CoTextJazzApi<CoPlainText>.owner: Groupowner;const taskGroup: GrouptaskGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(const alice: Accountalice, "reader");
If you prefer to manage permissions differently, you can always create CoValues explicitly:
constconst writeAccess: GroupwriteAccess =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst writeAccess: GroupwriteAccess.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(const bob: Accountbob, "writer"); constconst readAccess: GroupreadAccess =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst readAccess: GroupreadAccess.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(const bob: Accountbob, "reader"); // Give Bob read access to the board and write access to the columns and tasks constboard =const board: { readonly title: string; readonly columns: CoList<CoList<CoPlainText>>; } & CoMapBoard.const Board: co.Map<{ title: z.z.ZodString; columns: co.List<co.List<co.PlainText>>; }, unknown, Group | Account>create({CoMapSchema<{ title: ZodString; columns: CoListSchema<CoListSchema<PlainTextSchema>>; }, unknown, Group | Account>.create(init: { title: string; columns: readonly (CoList<CoPlainText | null> | readonly (string | CoPlainText)[])[] | CoList<CoList<CoPlainText | null> | null>; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)title: stringtitle: "My board",columns: readonly (CoList<CoPlainText | null> | readonly (string | CoPlainText)[])[] | CoList<CoList<CoPlainText | null> | null>columns:import coco.list(list<co.List<co.PlainText>>(element: co.List<co.PlainText>): co.List<co.List<co.PlainText>> export listconst Column: co.List<co.PlainText>Column).create([ ["Task 1.1", "Task 1.2"], ["Task 2.1", "Task 2.2"], ],CoListSchema<CoListSchema<PlainTextSchema>>.create(items: readonly (CoList<CoPlainText | null> | readonly (string | CoPlainText)[])[], options?: { owner: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): CoListInstance<...> (+1 overload)const writeAccess: GroupwriteAccess), },const readAccess: GroupreadAccess);
Example: Team Hierarchy
Here's a practical example of using group inheritance for team permissions:
// Company-wide group constconst companyGroup: GroupcompanyGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst companyGroup: GroupcompanyGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(CEO, "admin"); // Team group with elevated permissions constconst CEO: Account | ({ readonly [x: string]: any; } & Account)const teamGroup: GroupteamGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst teamGroup: GroupteamGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const companyGroup: GroupcompanyGroup); // Inherits company-wide accessconst teamGroup: GroupteamGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(teamLead, "admin");const teamLead: Account | ({ readonly [x: string]: any; } & Account)const teamGroup: GroupteamGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(developer, "writer"); // Project group with specific permissions constconst developer: Account | ({ readonly [x: string]: any; } & Account)const projectGroup: GroupprojectGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst projectGroup: GroupprojectGroup.Group.addMember(member: Group, role?: "reader" | "writer" | "admin" | "manager" | "inherit"): void (+3 overloads)addMember(const teamGroup: GroupteamGroup); // Inherits team permissionsconst projectGroup: GroupprojectGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(client, "reader"); // Client can only read project itemsconst client: Account | ({ readonly [x: string]: any; } & Account)
This creates a hierarchy where:
- The CEO has admin access to everything
- Team members get writer access to team and project content
- Team leads get admin access to team and project content
- The client can only read project content