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.
admin
orcollaborator
; 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
,slogan
etc.
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
Object
with the following properties:name*
String - Display name for the organization. Does not have to be unique.description
String - 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
name
anddescription
fields can be updated. If you want to add / remove members or change their permissions, call theaddMembers
,removeMembers
and / orchangePermissions
methods instead. - Deleted organizations would not be updated
Argument(s)
*
denotes a required field
id*
String - ID of the organization to update- An
Object
with the following properties:name
String - Display name for the organization. Does not have to be unique.description
String - 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
permissions
field 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_id
of 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
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
†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
true
when a database update operation was performed, orfalse
if 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
options
Object - AnObject
with the following properties:name*
String - Display name for the organization. Does not have to be unique.description
String - Description of the organization
caller
String - The ID of the user making the call, ornull
if 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
id
String - ID of the organization being updatedoptions
Object - 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:name
String - Display name for the organization. Does not have to be unique.description
String - Description of the organization
caller
String - The ID of the user making the call, ornull
if 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, ornull
if 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 sameuserId
property would be deduplicated first, and the first value encountered would be used (see theuniqBy
method of Lodash for more details about this deduplication). The following properties are required:userId*
String -_id
of the userpermissions
[String] - An array of strings representing permissions. E.g.["admin", "manager"]
caller*
String - The ID of the user making the call, ornull
if 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, ornull
if 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, ornull
if 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 -_id
of the user in theMeteor.users
collection
Get all membership objects related to the organization from the Organization.Collections.Membership
collection
Arguments
organizationId*
String -_id
of the organization in theOrganization.Collections.Organization
collection
Get all user objects of members in an organization from the Meteor.users
collection
Arguments
organizationId*
String -_id
of the organization in theOrganization.Collections.Organization
collection
Get all objects for organizations for which the user is a member of
Arguments
userId*
String -_id
of the user in theMeteor.users
collection
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 push
ed 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
organization
Object - details about the organization being created, containers the following properties:name
String - Display name for the organization. Does not have to be unique.description
String - Description of the organization
caller
String - User calling thecreate
function
Arguments
err
Object | undefined - Error object orundefined
if there are no errorsid
String - ID of the organization in theOrganization.Collections.Organization
collectionorganization
Object - details about the organization being created, containers the following properties:name
String - Display name for the organization. Does not have to be unique.description
String - Description of the organization
caller
String - User calling the function
Arguments
id
String - ID of the organization being updatedoptions
Object - details about the changes to apply to the organization, may contain any (or none) of the following properties:name
String - Display name for the organization. Does not have to be unique.description
String - Description of the organization
caller
String - User calling theupdate
function
Arguments
err
Object | undefined - Error object orundefined
if there are no errorsn
Number - The number of organizations affected, should always be1
if the operation was successfulid
String - The ID of the organization we updatedupdateObj
Object - details about the changes to apply to the organization, may contain any (or none) of the following properties:name
String - Display name for the organization. Does not have to be unique.description
String - Description of the organization
caller
String - User calling theupdate
function
Arguments
id
String - ID of the organization being deletedcaller
String - The ID of the user making the call, ornull
if it is from an anonymous user
Arguments
err
Object | undefined - Error object orundefined
if there are no errorsn
Number - The number of organizations affected, should always be1
if the operation was successfulid
String - The ID of the organization we deletedcaller
String - User calling theupdate
function
Arguments
id
String - ID of the organization we're adding members tomembers
[Object] - An array of user objects. The following properties are allowed:userId
String -_id
of 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 ([]
)
caller
String - The ID of the user making the call, ornull
if it is from an anonymous user
Called after each time a member is added to the organization
Arguments
err
Object | undefined - Error object orundefined
if there are no errorsn
Number - If the user was already a member of the organization, this would be1
, otherwise0
id
String - ID of the organization we're adding members tomember
Object - The user object of the member we added. The following properties are guaranteed to be present:userId
String -_id
of the userpermissions
[String] - An array of strings representing permissions. E.g.["admin", "manager"]
caller
String - The ID of the user making the call, ornull
if 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
id
String - 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:userId
String -_id
of the userpermissions
[String] - An array of strings representing permissions. E.g.["admin", "manager"]
caller
String - The ID of the user making the call, ornull
if it is from an anonymous user
Arguments
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, ornull
if it is from an anonymous user
Arguments
err
Object | undefined - Error object orundefined
if there are no errorsn
Number - Number of members removedid
String - ID of the organization to remove members frommembers
[String] - An array of user IDs that was removedcaller
String - The ID of the user making the call, ornull
if it is from an anonymous user
Arguments
id
String - 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
caller
String - The ID of the user making the call, ornull
if it is from an anonymous user
Arguments
err
Object | undefined - Error object orundefined
if there are no errorsn
Number - Number of members removedid
String - 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
caller
String - The ID of the user making the call, ornull
if it is from an anonymous user
Arguments
ids
[String] - Array of IDs of the organization the user is trying to accessthis.userId
String - ID of the user making the subscription request
Arguments
userId
String - IDs of the user in questionthis.userId
String - ID of the user making the subscription request
Arguments
organizationId
String - IDs of the organizationthis.userId
String - ID of the user making the subscription request
Arguments
organizationId
String - IDs of the organizationthis.userId
String - ID of the user making the subscription request
Arguments
userId
String - IDs of the user in questionthis.userId
String - 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:
createdAt
username
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 stringsuserId
String - 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
userId
String - 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.