Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions grouping.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
Grouping contains _id: userId and groupId: groupId
###

Partitioner = {}
Partitioner =
_documentGroupIdProperty: "_groupId"
_returnGroupIdAsField: false

Grouping = new Mongo.Collection("ts.grouping")

# Meteor environment variables for scoping group operations
Expand All @@ -16,6 +19,13 @@ Partitioner._directOps = new Meteor.EnvironmentVariable()
Public API
###

# Allow to change the document property for groupId
Partitioner.setGroupIdPropertyName = (name) ->
Partitioner._groupIdProperty = name

Partitioner.setReturnGroupIdAsField = (b) ->
Partitioner._returnGroupIdAsField = b

Partitioner.setUserGroup = (userId, groupId) ->
check(userId, String)
check(groupId, String)
Expand Down Expand Up @@ -59,7 +69,8 @@ Partitioner.directOperation = (func) ->
Partitioner._isAdmin = (userId) -> Meteor.users.findOne(userId).admin is true

getPartitionedIndex = (index) ->
defaultIndex = { _groupId : 1 }
defaultIndex = {}
defaultIndex[Partitioner._documentGroupIdProperty] = 1
return defaultIndex unless index
return _.extend( defaultIndex, index )

Expand Down Expand Up @@ -156,22 +167,25 @@ findHook = (userId, selector, options) ->

# if object (or empty) selector, just filter by group
unless selector?
@args[0] = { _groupId : groupId }
@args[0] = {}
@args[0][Partitioner._documentGroupIdProperty] = groupId;
else
selector._groupId = groupId
selector[Partitioner._documentGroupIdProperty] = groupId

# Adjust options to not return _groupId
unless options?
@args[1] = { fields: {_groupId: 0} }
else
# If options already exist, add {_groupId: 0} unless fields has {foo: 1} somewhere
options.fields ?= {}
options.fields._groupId = 0 unless _.any(options.fields, (v) -> v is 1)
if Partitioner._returnGroupIdAsField == false
unless options?
@args[1] = {fields: {}}
@args[1]['fields'][Partitioner._documentGroupIdProperty] = 0;
else
# If options already exist, add {_groupId: 0} unless fields has {foo: 1} somewhere
options.fields ?= {}
options.fields[Partitioner._documentGroupIdProperty] = 0 unless _.any(options.fields, (v) -> v is 1)

return true

insertHook = (userId, doc) ->
# Don't add group for direct inserts
# Don't add group for direct inserts
return true if Partitioner._directOps.get() is true

groupId = Partitioner._currentGroup.get()
Expand All @@ -180,20 +194,20 @@ insertHook = (userId, doc) ->
groupId = Grouping.findOne(userId)?.groupId
throw new Meteor.Error(403, ErrMsg.groupErr) unless groupId

doc._groupId = groupId
doc[Partitioner._documentGroupIdProperty] = groupId
return true

# Sync grouping needed for hooking Meteor.users
Grouping.find().observeChanges
added: (id, fields) ->
unless Meteor.users.update(id, $set: {"group": fields.groupId} )
unless Meteor.users.update(id, $set: {"group": fields.groupId})
Meteor._debug "Tried to set group for nonexistent user #{id}"
return
changed: (id, fields) ->
unless Meteor.users.update(id, $set: {"group": fields.groupId} )
unless Meteor.users.update(id, $set: {"group": fields.groupId})
Meteor._debug "Tried to change group for nonexistent user #{id}"
removed: (id) ->
unless Meteor.users.update(id, $unset: {"group": null} )
unless Meteor.users.update(id, $unset: {"group": null})
Meteor._debug "Tried to unset group for nonexistent user #{id}"

TestFuncs =
Expand Down
14 changes: 9 additions & 5 deletions grouping_client.coffee
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Partitioner = {}
Partitioner =
_documentGroupIdProperty: "_groupId"

###
Client selector modifiers
Expand All @@ -10,7 +11,7 @@ Partitioner.group = ->
return Meteor.users.findOne(userId, fields: {group: 1})?.group

userFindHook = (userId, selector, options) ->
# Do the usual find for no user or single selector
# Do the usual find for no user or single selector
return true if !userId or Helpers.isDirectUserSelector(selector)

# No hooking needed for regular users, taken care of on server
Expand All @@ -31,15 +32,18 @@ insertHook = (userId, doc) ->
throw new Meteor.Error(403, ErrMsg.userIdErr) unless userId
groupId = Partitioner.group()
throw new Meteor.Error(403, ErrMsg.groupErr) unless groupId
doc._groupId = groupId
doc[Partitioner._documentGroupIdProperty] = groupId
return true

# Add in groupId for client so as not to cause unexpected sync changes
Partitioner.partitionCollection = (collection) ->
# No find hooks needed if server side filtering works properly

# No find hooks needed if server side filtering works properly
collection.before.insert insertHook

# Allow to change the document property for groupId
Partitioner.setGroupIdPropertyName = (name) ->
Partitioner._groupIdProperty = name

TestFuncs =
userFindHook: userFindHook
insertHook: insertHook
90 changes: 45 additions & 45 deletions package.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,60 @@
Package.describe({
name: "mizzao:partitioner",
summary: "Transparently divide a meteor app into different instances shared between groups of users.",
version: "0.5.9",
git: "https://github.com/mizzao/meteor-partitioner.git"
name: "mizzao:partitioner",
summary: "Transparently divide a meteor app into different instances shared between groups of users.",
version: "0.5.9",
git: "https://github.com/mizzao/meteor-partitioner.git"
});

Package.onUse(function (api) {
api.versionsFrom("1.2.0.1");
api.versionsFrom("1.2.0.1");

// Client & Server deps
api.use([
'accounts-base',
'underscore',
'coffeescript',
'check',
'ddp', // Meteor.publish available
'mongo' // Mongo.Collection available
]);
// Client & Server deps
api.use([
'accounts-base',
'underscore',
'coffeescript',
'check',
'ddp', // Meteor.publish available
'mongo' // Mongo.Collection available
]);

api.use("matb33:collection-hooks@0.7.15");
api.use("matb33:collection-hooks@0.7.15");

api.addFiles('common.coffee');
api.addFiles('common.coffee');

api.addFiles('grouping.coffee', 'server');
api.addFiles('grouping_client.coffee', 'client');
api.addFiles('grouping.coffee', 'server');
api.addFiles('grouping_client.coffee', 'client');

api.export(['Partitioner', 'Grouping']);
api.export(['Partitioner', 'Grouping']);

// Package-level variables that should not be exported
// See http://docs.meteor.com/#/full/coffeescript
api.export(['ErrMsg', 'Helpers'], {testOnly: true});
// Package-level variables that should not be exported
// See http://docs.meteor.com/#/full/coffeescript
api.export(['ErrMsg', 'Helpers'], {testOnly: true});

api.export('TestFuncs', {testOnly: true});
api.export('TestFuncs', {testOnly: true});
});

Package.onTest(function (api) {
api.use("mizzao:partitioner");

api.use([
'accounts-base',
'accounts-password', // For createUser
'coffeescript',
'underscore',
'ddp', // Meteor.publish available
'mongo', // Mongo.Collection available
'tracker' // Deps/Tracker available
]);

api.use([
'tinytest',
'test-helpers'
]);

api.addFiles("tests/insecure_login.js");

api.addFiles('tests/hook_tests.coffee');
api.addFiles('tests/grouping_index_tests.coffee', 'server');
api.addFiles('tests/grouping_tests.coffee');
api.use("mizzao:partitioner");

api.use([
'accounts-base',
'accounts-password', // For createUser
'coffeescript',
'underscore',
'ddp', // Meteor.publish available
'mongo', // Mongo.Collection available
'tracker' // Deps/Tracker available
]);

api.use([
'tinytest',
'test-helpers'
]);

api.addFiles("tests/insecure_login.js");

api.addFiles('tests/hook_tests.coffee');
api.addFiles('tests/grouping_index_tests.coffee', 'server');
api.addFiles('tests/grouping_tests.coffee');
});
46 changes: 46 additions & 0 deletions tests/grouping_tests.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ if Meteor.isServer
Partitioner.directOperation ->
twoGroupCollection.insert
_groupId: myGroup
_otherGroupId: myGroup
a: 1

twoGroupCollection.insert
_groupId: otherGroup
_otherGroupId: otherGroup
a: 1

Meteor._debug "collections configured"
Expand Down Expand Up @@ -102,6 +104,38 @@ if Meteor.isServer
basicInsertCollection.insert { foo: "bar"}
test.ok()

Tinytest.add "partitioner - collections - insert with custom groupIdProperty", (test) ->
Partitioner.setGroupIdPropertyName '_custom'
Partitioner.bindGroup "overridden", ->
basicInsertCollection.insert { foo: "bar"}
inserted = basicInsertCollection.findOne({foo:"bar"})
test.isFalse inserted._groupId?
test.isFalse inserted._custom?
test.ok()
Partitioner.setGroupIdPropertyName '_groupId'

Tinytest.add "partitioner - collections - return groupIdProperty", (test) ->
Partitioner.setReturnGroupIdAsField true
Partitioner.bindGroup "overridden", ->
basicInsertCollection.insert { foo: "bar"}
inserted = basicInsertCollection.findOne({foo:"bar"})
test.isTrue inserted._groupId?
test.equal inserted._groupId, "overridden"
test.ok()
Partitioner.setReturnGroupIdAsField false

Tinytest.add "partitioner - collections - return groupIdProperty with custom groupIdProperty", (test) ->
Partitioner.setGroupIdPropertyName '_custom'
Partitioner.setReturnGroupIdAsField true
Partitioner.bindGroup "overridden", ->
basicInsertCollection.insert { foo: "bar"}
inserted = basicInsertCollection.findOne({foo:"bar"})
test.isTrue inserted._custom?
test.equal inserted._custom, "overridden"
test.ok()
Partitioner.setGroupIdPropertyName '_groupId'
Partitioner.setReturnGroupIdAsField false

if Meteor.isClient
###
These tests need to all async so they are in the right order
Expand Down Expand Up @@ -153,6 +187,18 @@ if Meteor.isClient
test.isFalse basicInsertCollection.findOne(a: 1)._groupId?
]

testAsyncMulti "partitioner - collections - basic insert with custom groupIdProperty", [
(test, expect) ->
Partitioner.setGroupIdPropertyName '_custom'
id = basicInsertCollection.insert { a: 1 }, expect (err, res) ->
test.isFalse err, JSON.stringify(err)
test.equal res, id
, (test, expect) ->
test.equal basicInsertCollection.find({a: 1}).count(), 1
test.isFalse basicInsertCollection.findOne(a: 1)._groupId?
Partitioner.setGroupIdPropertyName '_groupId'
]

testAsyncMulti "partitioner - collections - find from two groups", [ (test, expect) ->
test.equal twoGroupCollection.find().count(), 1

Expand Down
11 changes: 11 additions & 0 deletions tests/hook_tests.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,17 @@ if Meteor.isServer
test.equal ctx.args[1].fields.foo, 0
test.equal ctx.args[1].fields._groupId, 0

Tinytest.add "partitioner - hooks - find with custom groupIdProperty", (test) ->
Partitioner.setGroupIdPropertyName "_custom"
ctx =
args: [{ }]

TestFuncs.findHook.call(ctx, userId, ctx.args[0])

test.equal ctx.args[0]._custom, testGroupId

Partitioner.setGroupIdPropertyName "_groupId"

Tinytest.add "partitioner - hooks - insert doc", (test) ->
ctx =
args: [ { foo: "bar" } ]
Expand Down