Allows users to belong to a certain organization, similar to GitHub organizations.
- Members can belong to zero or more organizations
- Organizations can assign arbitrary, user-defined permissions to users (e.g.
adminorcollaborator; orcreate,update,delete)
This is similar to alanning:roles but differs in these aspects:
- Details about the organization (i.e. group) and permissions (i.e. roles) are not stored inside the user object, but rather normalized into their own respective collection. This makes searching for roles, members and organizations much faster.
- Organizations can (potentially) have an infinite number of properties, such as
name,description,sloganetc.
In our application, we are using alanning:roles to manage roles for different platforms that share the same user database. For example, we have a blog, a platform for users, and an internal administrative dashboard. Within the platform for users, we use brewhk:accounts-organization to add users to businesses (i.e. organizations). For example, Alice and Bob would both be members of Brew, but Alice is also a member of Google.
So Alice and Bob would both have roles user in the group platform defined with alanning:roles. Alice and Bob would then also be members of the organization Brew (represented by a Mongo ID).
Run:
meteor add brewhk:accounts-organization
First, import the library into your code:
import Organization from 'meteor/brewhk:accounts-organization';
All client-side methods that mutates the data in some way returns with a promise. All client-side functions that (indirectly) queries the database returns with a cursor of the results.
Create a new organization.
Argument(s)
* denotes a required field
- An
Objectwith the following properties:name*String - Display name for the organization. Does not have to be unique.descriptionString - Description of the organization
Return Values
Returns a promise, which:
- resolves with the ID of the new organization (e.g.
L6eFEyhYgHnBt2F3z), or - rejected with the error object
Examples
Create an organization with no members
Organization.create({
name: "Empty Organization",
description: "There are no members in this organization",
}).then(res => console.log(res)).catch(err => console.log(err));
Create an organization with a single member (see addMembers below)
Organization.create({
name: "Organization with One Member",
description: "There is 1 member in this organization",
}).then(id => {
Organization.addMembers(id, [{
userId: 'PpqKunbxPzBXFkT9K',
}]);
}).catch(err => console.log(err));
Updates an organization.
Note:
- Only the
nameanddescriptionfields can be updated. If you want to add / remove members or change their permissions, call theaddMembers,removeMembersand / orchangePermissionsmethods instead. - Deleted organizations would not be updated
Argument(s)
* denotes a required field
id*String - ID of the organization to update- An
Objectwith the following properties:nameString - Display name for the organization. Does not have to be unique.descriptionString - Description of the organization
Return Values
Returns a promise, which:
- resolves with
true, although it does not mean any database entries was changed (e.g. if no organization exists with that ID) - rejected with the error object
Examples
Update an organization with a new name and description
Organization.update('C3BWcayYpvTCC8qww', {
name: "Updated Organization",
description: Math.random().toString(),
}).then(res => console.log(res)).catch(err => console.log(err));
Trying to update a deleted organization would not change anything
// Organization with ID `JBijDo8P74H3cvguQ` was deleted with `Organization.delete`
Organization.update('JBijDo8P74H3cvguQ', {
name: "Trying to update a deleted organization",
description: "You should not see this text",
}).then(res => console.log(res)).catch(err => console.log(err));
(Soft) deletes an organization.
Argument(s)
* denotes a required field
id*String - ID of the organization to delete
Return Values
Returns a promise, which:
- resolves with
true, although it does not mean any database entries was changed (e.g. if no organization exists with that ID) - rejected with the error object
Examples
Delete an organization
Organization.delete('JBijDo8P74H3cvguQ')
.then(res => console.log(res)).catch(err => console.log(err));
Trying to update a deleted organization would not change anything
Organization.update('JBijDo8P74H3cvguQ', {
name: "Trying to update a deleted organization",
description: "You should not see this text",
}).then(res => console.log(res)).catch(err => console.log(err));
Add members to the organization.
Note:
- If the member with the ID already exists, it will be replaced with the new member object. This is important as if the
permissionsfield is not specified, it will default to[], and this means the existing member would have all permissions removed.
Arguments
* denotes a required field
id*String - ID of the organization to add members tomembers*[Objects] - an array of members, each member is an object with the following properties:userId*String - The_idof the member's user objectpermissions[String] - An array of permission strings
Return Values
Returns a promise, which:
- resolves with
true, although it does not mean any database entries was changed (e.g. if no organization exists with that ID) - rejected with the error object
Examples
Add members with no permissions to the organization
Organization.addMembers('JBijDo8P74H3cvguQ', [
{
userId: 'PpqKunbxPzBXFkT9K'
}, {
userId: 'qgmpHtyKswwrJgs46'
}, {
userId: 'D5hR2H2AEo79b4Jpn'
}, {
userId: '3e48WKy73dWiAnNK5'
}
]).then(res => console.log(res)).catch(err => console.log(err));
Add members with permissions
Organization.addMembers('JBijDo8P74H3cvguQ', [
{
userId: 'PpqKunbxPzBXFkT9K',
permissions: ['manager', 'customer-service']
}
]).then(res => console.log(res)).catch(err => console.log(err));
Remove members from organization.
Arguments
* denotes a required field
id*String - ID of the organization to add members tomembers*[String] - an array of members IDs
Return Values
Returns a promise, which:
- resolves with
true, although it does not mean any database entries was changed (e.g. if no organization exists with that ID) - rejected with the error object
Examples
Remove members from the organization
Organization.removeMembers('JBijDo8P74H3cvguQ', ['PpqKunbxPzBXFkT9K'])
.then(res => console.log(res)).catch(err => console.log(err));
Change permissions for one or more users in an organization.
Arguments
* denotes a required field
id*String - ID of the relevant organizationmembers*Object - an object the defines the constraints of which members this will affect. Any empty object ({}) defaults to modifying all members of the organization. You can set constraints using the following the following properties:only[String] - Only apply the permission changes to this array of user IDsexcept[String] - Apply the permission changes to all members except this array of user IDs
permissionsObject - an object that specifies how the permissions will change. It should include one, and only one, of the following properties†:set[String] - Set the permission to the arrayadd[String] - Add the permissions to the existing array of permissionsremove[String] - Remove the following list of permissions from the existing array of permissions
†Due to the way Mongo works, we cannot modify the same field using the same operation. Thus we cannot use the $push (to add permissions) and $pull (to remove permissions) operators at the same time.
Return Values
Returns a promise, which:
- resolves with
truewhen a database update operation was performed, orfalseif it was not performed (because it didn't need to be updated, for example, due to invalid / conflicting options) - rejected with the error object
Examples
Change permissions of all members of an organization
Organization.changePermissions('JBijDo8P74H3cvguQ', {}, {
set: ["admin", "manager"],
}).then(res => console.log(res)).catch(err => console.log(err));
Add permissions to only the specified members
Organization.changePermissions('JBijDo8P74H3cvguQ', {
only: ['PpqKunbxPzBXFkT9K', 'qgmpHtyKswwrJgs46'],
}, {
add: ["add", "extra", "sauce"],
}).then(res => console.log(res)).catch(err => console.log(err));
Remove permissions from all members except those specified
Organization.changePermissions('JBijDo8P74H3cvguQ', {
except: ['D5hR2H2AEo79b4Jpn', '3e48WKy73dWiAnNK5']
}, {
remove: ["sauce", "manager"],
}).then(res => console.log(res)).catch(err => console.log(err));
Add and remove permission from a specific list of users. This would not work, you should update each one sequentially
Organization.changePermissions('JBijDo8P74H3cvguQ', {
only: ['D5hR2H2AEo79b4Jpn']
}, {
add: ["poo", "foo", "face"],
remove: ["admin", "manager"],
}).then(res => console.log(res)).catch(err => console.log(err));
Creates a new organization
Arguments
* denotes a required field
optionsObject - AnObjectwith the following properties:name*String - Display name for the organization. Does not have to be unique.descriptionString - Description of the organization
callerString - The ID of the user making the call, ornullif it is from an anonymous user
Return Values
The ID of the newly-created organization in the Organization.Collections.Organization collection.
Update an organization - should only be used to update the name and/or description
Arguments
* denotes a required field
idString - ID of the organization being updatedoptionsObject - details about the changes to apply to the organization; after cleaning and validation, would be passed to the update method inside a$set. May contain any (or none) of the following properties:nameString - Display name for the organization. Does not have to be unique.descriptionString - Description of the organization
callerString - The ID of the user making the call, ornullif it is from an anonymous user
Return Values
true
(Soft-)deletes an organization
Arguments
* denotes a required field
id*String - ID of the organization being deletedcaller*String - The ID of the user making the call, ornullif it is from an anonymous user
Return Values
true
Add new members to the organization. The function would check that the users with the userId exists before adding the members. Anonymous users would not be added. Users who are already members would have their user object replaced by the new specification.
Arguments
* denotes a required field
id*String - ID of the organization to add members tomembers*[Object] - An array of user objects. User objects with the sameuserIdproperty would be deduplicated first, and the first value encountered would be used (see theuniqBymethod of Lodash for more details about this deduplication). The following properties are required:userId*String -_idof the userpermissions[String] - An array of strings representing permissions. E.g.["admin", "manager"]
caller*String - The ID of the user making the call, ornullif it is from an anonymous user
Return Values
true
Removes members from organization
Arguments
* denotes a required field
id*String - ID of the organization to remove members frommembers*[String] - An array of user IDscaller*String - The ID of the user making the call, ornullif it is from an anonymous user
Return Values
true
Changes permission(s) for member(s)
Arguments
* denotes a required field
id*String - ID of the relevant organizationmembers*Object - an object the defines the constraints of which members this will affect. Any empty object ({}) defaults to modifying all members of the organization. You can set constraints using the following the following properties:only[String] - Only apply the permission changes to this array of user IDsexcept[String] - Apply the permission changes to all members except this array of user IDs
permissions*Object - an object that specifies how the permissions will change. It should include one, and only one, of the following properties†:set[String] - Set the permission to the arrayadd[String] - Add the permissions to the existing array of permissionsremove[String] - Remove the following list of permissions from the existing array of permissions
caller*String - The ID of the user making the call, ornullif it is from an anonymous user
†Due to the way Mongo works, we cannot modify the same field using the same operation. Thus we cannot use the $push (to add permissions) and $pull (to remove permissions) operators at the same time.
Return Values
Returns true when a database update operation was performed, or false if it was not performed (because it didn't need to be updated, for example, due to invalid / conflicting options)
All methods are namespaced under brewhk:accounts-organization, so to call the create method, you would write:
Meteor.apply('brewhk:accounts-organization/create', [options], function (err, res) {})
However, you would rarely call the methods directly; instead, you should use the methods provided for the client (e.g. Organization.create(options)), which will call the method for you and returns with a promise.
The API for the methods are the same as for the server-side functions specified above, the only difference being the omission of the caller parameter, which is passed from the method to the function as the value of this.userId. Fro details about the arguments, refer to the specification for the server-side functions.
For example, this is the implementation of the create method:
'brewhk:accounts-organization/create': function (options) {
return Organization.create(options, this.userId);
}
The following methods are available:
create(options)update(id, options)delete(id)addMembers(id, members)removeMembers(id, members)changePermissions(id, members, persmissions)
All methods are namespaced under brewhk:accounts-organization, so to subscribe to the organization publication, you would write:
Meteor.subscribe('brewhk:accounts-organization/organization', ["a", "b"], () => {})
Get the corresponding organization objects from the Organization.Collections.Organization collection
Arguments
ids*[String] - Array of organization IDs
Get all membership objects related to the user from the Organization.Collections.Membership collection
Arguments
userId*String -_idof the user in theMeteor.userscollection
Get all membership objects related to the organization from the Organization.Collections.Membership collection
Arguments
organizationId*String -_idof the organization in theOrganization.Collections.Organizationcollection
Get all user objects of members in an organization from the Meteor.users collection
Arguments
organizationId*String -_idof the organization in theOrganization.Collections.Organizationcollection
Get all objects for organizations for which the user is a member of
Arguments
userId*String -_idof the user in theMeteor.userscollection
Hooks are functions which are ran before or after a certain event, such as an update to the database. For example, after an organization is created, functions pushed to the Hooks.Organization.afterCreate array would be executed (in the order they were pushed).
There are before hooks, which are executed before an action takes place, and after hooks, which are executed after an action has succeeded. before hooks will always be executed, but after hooks would only execute if the operation was successful.
A common use case for before hooks is to check whether the user requesting the action has permissions to perform the action; a common use case for after hooks is to notify users of the change.
You can throw an error (preferably a Meteor.Error) inside any of the hooks to stop downstream execution. For example, if a user does not have permission to perform an action, you can throw new Meteor.Error('permission-denied').
Permissions-specific hooks used during publications are grouped into the Permissions object.
To access hooks, you must import them:
import Organization, { Hooks, Permissions } from 'meteor/brewhk:accounts-organization';
Arguments
organizationObject - details about the organization being created, containers the following properties:nameString - Display name for the organization. Does not have to be unique.descriptionString - Description of the organization
callerString - User calling thecreatefunction
Arguments
errObject | undefined - Error object orundefinedif there are no errorsidString - ID of the organization in theOrganization.Collections.OrganizationcollectionorganizationObject - details about the organization being created, containers the following properties:nameString - Display name for the organization. Does not have to be unique.descriptionString - Description of the organization
callerString - User calling the function
Arguments
idString - ID of the organization being updatedoptionsObject - details about the changes to apply to the organization, may contain any (or none) of the following properties:nameString - Display name for the organization. Does not have to be unique.descriptionString - Description of the organization
callerString - User calling theupdatefunction
Arguments
errObject | undefined - Error object orundefinedif there are no errorsnNumber - The number of organizations affected, should always be1if the operation was successfulidString - The ID of the organization we updatedupdateObjObject - details about the changes to apply to the organization, may contain any (or none) of the following properties:nameString - Display name for the organization. Does not have to be unique.descriptionString - Description of the organization
callerString - User calling theupdatefunction
Arguments
idString - ID of the organization being deletedcallerString - The ID of the user making the call, ornullif it is from an anonymous user
Arguments
errObject | undefined - Error object orundefinedif there are no errorsnNumber - The number of organizations affected, should always be1if the operation was successfulidString - The ID of the organization we deletedcallerString - User calling theupdatefunction
Arguments
idString - ID of the organization we're adding members tomembers[Object] - An array of user objects. The following properties are allowed:userIdString -_idof the user (required)permissions[String] - An array of strings representing permissions. E.g.["admin", "manager"]. Not required, and will later on default to an empty array ([])
callerString - The ID of the user making the call, ornullif it is from an anonymous user
Called after each time a member is added to the organization
Arguments
errObject | undefined - Error object orundefinedif there are no errorsnNumber - If the user was already a member of the organization, this would be1, otherwise0idString - ID of the organization we're adding members tomemberObject - The user object of the member we added. The following properties are guaranteed to be present:userIdString -_idof the userpermissions[String] - An array of strings representing permissions. E.g.["admin", "manager"]
callerString - The ID of the user making the call, ornullif it is from an anonymous user
Called after the Organization.addMembers function has finished running. It does not guarantee that the database operation to add members to the organization has completed; in fact, in most cases, those operations would occur after this hook.
Arguments
idString - ID of the organization we're adding members tomembersWithPermissions[Object] - An array of user object. The following properties are guaranteed to be present in each object:userIdString -_idof the userpermissions[String] - An array of strings representing permissions. E.g.["admin", "manager"]
callerString - The ID of the user making the call, ornullif it is from an anonymous user
Arguments
idString - ID of the organization to remove members frommembers[String] - An array of user IDscallerString - The ID of the user making the call, ornullif it is from an anonymous user
Arguments
errObject | undefined - Error object orundefinedif there are no errorsnNumber - Number of members removedidString - ID of the organization to remove members frommembers[String] - An array of user IDs that was removedcallerString - The ID of the user making the call, ornullif it is from an anonymous user
Arguments
idString - ID of the organization to change permissions formembers*Object - an object the defines the constraints of which members this will affect. Any empty object ({}) defaults to modifying all members of the organization. You can set constraints using the following the following properties:only[String] - Only apply the permission changes to this array of user IDsexcept[String] - Apply the permission changes to all members except this array of user IDs
permissions*Object - an object that specifies how the permissions will change. It should include one, and only one, of the following properties:set[String] - Set the permission to the arrayadd[String] - Add the permissions to the existing array of permissionsremove[String] - Remove the following list of permissions from the existing array of permissions
callerString - The ID of the user making the call, ornullif it is from an anonymous user
Arguments
errObject | undefined - Error object orundefinedif there are no errorsnNumber - Number of members removedidString - ID of the organizationmembers*Object - an object the defines the constraints of which members this will affect. Any empty object ({}) defaults to modifying all members of the organization. You can set constraints using the following the following properties:only[String] - Only apply the permission changes to this array of user IDsexcept[String] - Apply the permission changes to all members except this array of user IDs
permissions*Object - an object that specifies how the permissions will change. It should include one, and only one, of the following properties:set[String] - Set the permission to the arrayadd[String] - Add the permissions to the existing array of permissionsremove[String] - Remove the following list of permissions from the existing array of permissions
callerString - The ID of the user making the call, ornullif it is from an anonymous user
Arguments
ids[String] - Array of IDs of the organization the user is trying to accessthis.userIdString - ID of the user making the subscription request
Arguments
userIdString - IDs of the user in questionthis.userIdString - ID of the user making the subscription request
Arguments
organizationIdString - IDs of the organizationthis.userIdString - ID of the user making the subscription request
Arguments
organizationIdString - IDs of the organizationthis.userIdString - ID of the user making the subscription request
Arguments
userIdString - IDs of the user in questionthis.userIdString - ID of the user making the subscription request
The following methods can be ran from both client- and server-side. To get the desired results on the client, you should ensure that you have subscribed to the relevant publications.
For example, to get a list of all organizations a member belongs to, you should do the following:
Meteor.subscribe('brewhk:accounts-organization/organizationsOfUser', <user-id>, () => {
const organizations = Organization.getOrganizationsOfUser(<user-id>);
})
By default, published user objects would expose only the following fields:
createdAtusername
Query for organizations
Argument(s)
* denotes a required field
options*Object - A Mongo selector object
Return Values
Returns a cursor of relevant organizations
Query for organizations by specifying an ID
Argument(s)
* denotes a required field
id*String - ID of the organization
Return Values
Returns a cursor of the organization
Retrieve membership objects relating to organization
Argument(s)
* denotes a required field
organizationId*String - ID of the organization
Return Values
Returns a cursor of the membership objects
Retrieve membership objects relating to an user
Argument(s)
* denotes a required field
userId*String - ID of the user
Return Values
Returns a cursor of the membership objects
Retrieve user IDs of members of an organization
Argument(s)
* denotes a required field
organizationId*String - ID of the organization
Return Values
Returns an array of unique user IDs
Retrieve user objects of members of an organization
Argument(s)
* denotes a required field
organizationId*String - ID of the organization
Return Values
Returns a cursor of user objects from the Meteor.users collection
Retrieve a list of all permissions within an organization
Argument(s)
* denotes a required field
organizationId*String - ID of the organization
Return Values
Returns an array of permission strings (e.g. ["admin", "manager"])
Retrieve a list of user objects who have the specified set of permission(s) within an organization
Argument(s)
* denotes a required field
organizationId*String - ID of the organizationpermissions[String] - An array of permission strings
Return Values
Returns a cursor of user objects from the Meteor.users collection
Checks if a user has a certain set of permission(s) within an organization
Argument(s)
* denotes a required field
organizationId*String - ID of the organizationpermissions[String] - An array of permission stringsuserIdString - User to check for
Return Values
Returns a Boolean of whether the user has all the permissions or not
Get the organizations that a user is a member of
Argument(s)
* denotes a required field
userIdString - User ID
Return Values
Returns a cursor of organization objects from the Organization.Collections.Organization collection
Contributions are always welcomed. Here's how you should go about it:
- typo and other small bug fixes (fork this repository, make the changes, and create a pull request)
- feature addition (email us and we'll add you as a collaborator, and we can work on the feature branch together)
- come onboard as a co-maintainer (email us)
Every new feature must be accompanied by tests. We do not require 100% code coverage, but a higher-than-reasonable amount of tests is expected.
We have started writing client-side tests for this package. However, the Mocha, Chai and Sinon versions wrapped in practicalmeteor:mocha are very out-of-date; as such, we've had to include the mocha, chai and sinon packages directly, through NPM. Meteor, as far as we know, does not support having separate NPM packages for testing, and as such, we have to place these development dependencies on the top-level Npm.depends.
Obviously, we don't want these development dependencies being sent over to the client, so we've commented out the dependencies as well as the test block.
To test this package, simply uncomment those lines from package.js and run:
meteor test-packages ./ --driver-package practicalmeteor:mocha
Note that you should also open up the client console to look for errors, as the version of Mocha included in practicalmeteor:mocha does not catch errors related to promises properly.