Skip to content

Commit 2e040c9

Browse files
Merge pull request #311 from bhunjadi/migrate/3.0
Merge in latest work from @bhunjadi
2 parents 9dc5926 + 66b310f commit 2e040c9

9 files changed

Lines changed: 175 additions & 171 deletions

collection-hooks.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (
175175
if (['insert', 'update', 'upsert', 'remove', 'findOne'].includes(method)) {
176176
const _superAsync = collection[asyncMethod]
177177
collection[asyncMethod] = getWrappedMethod(_superAsync)
178+
} else if (method === 'find') {
179+
// find is returning a cursor and is a sync method
180+
const _superMethod = collection[method]
181+
collection[method] = getWrappedMethod(_superMethod)
178182
}
179183

180184
// Don't do this for v3 since we need to keep client methods sync.

find.js

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,72 @@
11
import { CollectionHooks } from './collection-hooks'
22

3+
const ASYNC_METHODS = ['countAsync', 'fetchAsync', 'forEachAsync', 'mapAsync']
4+
5+
/**
6+
* With Meteor v3 this behaves differently than with Meteor v2.
7+
* We cannot use async hooks on find() directly because in Meteor it is a sync method that returns cursor instance.
8+
*
9+
* That's why we need to wrap all async methods of cursor instance. We're doing this by creating another cursor
10+
* within these wrapped methods with selector and options updated by before hooks.
11+
*/
312
CollectionHooks.defineAdvice('find', function (userId, _super, instance, aspects, getTransform, args, suppressAspects) {
413
// const ctx = { context: this, _super, args }
514
const selector = CollectionHooks.normalizeSelector(instance._getFindSelector(args))
615
const options = instance._getFindOptions(args)
716

8-
// NOTE: v3 not supporting hooks since they would make the return value Promise<Cursor> instead of Cursor
9-
// let abort
10-
// // before
11-
// if (!suppressAspects) {
12-
// aspects.before.forEach((o) => {
13-
// const r = o.aspect.call(ctx, userId, selector, options)
14-
// if (r === false) abort = true
15-
// })
16-
17-
// if (abort) return instance.find(undefined)
18-
// }
19-
20-
// const after = (cursor) => {
21-
// if (!suppressAspects) {
22-
// aspects.after.forEach((o) => {
23-
// o.aspect.call(ctx, userId, selector, options, cursor)
24-
// })
25-
// }
26-
// }
27-
28-
const ret = _super.call(this, selector, options)
29-
// after(ret)
30-
31-
return ret
17+
const cursor = _super.call(this, selector, options)
18+
19+
// Wrap async cursor methods
20+
ASYNC_METHODS.forEach((method) => {
21+
if (cursor[method]) {
22+
cursor[method] = async (...args) => {
23+
let abort = false
24+
for (const aspect of aspects.before) {
25+
const result = await aspect.aspect.call(this, userId, selector, options)
26+
if (result === false) {
27+
abort = true
28+
}
29+
}
30+
31+
// Take #1 - monkey patch existing cursor
32+
// Now that before hooks have run, update the cursor selector & options
33+
// Special case for "undefined" selector, which means none of the documents
34+
// This is a full c/p from Meteor's minimongo/cursor.js, it probably doesn't make too much sense and is too
35+
// error-prone to maintain as each Meteor change would require
36+
37+
// cursor.sorter = null
38+
// cursor.matcher = new Minimongo.Matcher(Mongo.Collection._rewriteSelector(abort ? undefined : selector))
39+
// if (Minimongo.LocalCollection._selectorIsIdPerhapsAsObject(selector)) {
40+
// // eslint-disable-next-line no-prototype-builtins
41+
// cursor._selectorId = Object.prototype.hasOwnProperty(selector, '_id') ? selector._id : selector
42+
// } else {
43+
// cursor._selectorId = undefined
44+
// if (cursor.matcher.hasGeoQuery() || options.sort) {
45+
// cursor.sorter = new Minimongo.Sorter(options.sort || [])
46+
// }
47+
// }
48+
// cursor.skip = options.skip || 0
49+
// cursor.limit = options.limit
50+
// cursor.fields = options.projection || options.fields
51+
// cursor._projectionFn = Minimongo.LocalCollection._compileProjection(cursor.fields || {})
52+
// cursor._transform = Minimongo.LocalCollection.wrapTransform(options.transform)
53+
// if (typeof Tracker !== 'undefined') {
54+
// cursor.reactive = options.reactive === undefined ? true : options.reactive
55+
// }
56+
57+
// Take #2 - create new cursor
58+
const newCursor = _super.call(this, abort ? undefined : selector, options)
59+
60+
const result = await newCursor[method](...args)
61+
62+
for (const aspect of aspects.after) {
63+
await aspect.aspect.call(this, userId, selector, options, cursor)
64+
}
65+
66+
return result
67+
}
68+
}
69+
})
70+
71+
return cursor
3272
})

tests/find.js

Lines changed: 52 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,52 @@
1-
// import { Mongo } from 'meteor/mongo'
2-
// import { Tinytest } from 'meteor/tinytest'
3-
// import { InsecureLogin } from './insecure_login'
4-
5-
// NOTE: no async find hooks in v3
6-
// Tinytest.addAsync('find - selector should be {} when called without arguments', function (test, next) {
7-
// const collection = new Mongo.Collection(null)
8-
9-
// // eslint-disable-next-line array-callback-return
10-
// collection.before.find(function (userId, selector, options) {
11-
// test.equal(selector, {})
12-
// next()
13-
// })
14-
15-
// collection.find()
16-
// })
17-
18-
// NOTE: no async find hooks in v3
19-
// Tinytest.addAsync('find - selector should have extra property', function (test, next) {
20-
// const collection = new Mongo.Collection(null)
21-
22-
// // eslint-disable-next-line array-callback-return
23-
// collection.before.find(function (userId, selector, options) {
24-
// if (options && options.test) {
25-
// delete selector.bogus_value
26-
// selector.before_find = true
27-
// }
28-
// })
29-
30-
// InsecureLogin.ready(function () {
31-
// collection.insert({ start_value: true, before_find: true }, function (err, id) {
32-
// if (err) throw err
33-
// test.equal(collection.find({ start_value: true, bogus_value: true }, { test: 1 }).count(), 1)
34-
// next()
35-
// })
36-
// })
37-
// })
38-
39-
// NOTE: no async find hooks in v3
40-
// Tinytest.addAsync('find - tmp variable should have property added after the find', function (test, next) {
41-
// const collection = new Mongo.Collection(null)
42-
// const tmp = {}
43-
44-
// // eslint-disable-next-line array-callback-return
45-
// collection.after.find(function (userId, selector, options) {
46-
// if (options && options.test) {
47-
// tmp.after_find = true
48-
// }
49-
// })
50-
51-
// InsecureLogin.ready(function () {
52-
// collection.insert({ start_value: true }, function (err, id) {
53-
// if (err) throw err
54-
// collection.find({ start_value: true }, { test: 1 })
55-
// test.equal(tmp.after_find, true)
56-
// next()
57-
// })
58-
// })
59-
// })
1+
import { Mongo } from 'meteor/mongo'
2+
import { Tinytest } from 'meteor/tinytest'
3+
import { InsecureLogin } from './insecure_login'
4+
5+
Tinytest.addAsync('find - selector should be {} when called without arguments', async function (test) {
6+
const collection = new Mongo.Collection(null)
7+
8+
let findSelector = null
9+
collection.before.find(async function (userId, selector, options) {
10+
findSelector = selector
11+
})
12+
13+
// hooks won't be triggered on find() alone, we must call fetchAsync()
14+
await collection.find().fetchAsync()
15+
16+
test.equal(findSelector, {})
17+
})
18+
19+
Tinytest.addAsync('find - selector should have extra property', async function (test) {
20+
const collection = new Mongo.Collection(null)
21+
22+
collection.before.find(async function (userId, selector, options) {
23+
if (options && options.test) {
24+
delete selector.bogus_value
25+
selector.before_find = true
26+
}
27+
})
28+
29+
await InsecureLogin.ready(async function () {
30+
await collection.insertAsync({ start_value: true, before_find: true })
31+
test.equal(await collection.find({ start_value: true, bogus_value: true }, { test: 1 }).countAsync(), 1)
32+
})
33+
})
34+
35+
Tinytest.addAsync('find - tmp variable should have property added after the find', async function (test) {
36+
const collection = new Mongo.Collection(null)
37+
const tmp = {}
38+
39+
// eslint-disable-next-line array-callback-return
40+
collection.after.find(async function (userId, selector, options) {
41+
if (options && options.test) {
42+
tmp.after_find = true
43+
}
44+
})
45+
46+
await InsecureLogin.ready(async function () {
47+
await collection.insertAsync({ start_value: true })
48+
await collection.find({ start_value: true }, { test: 1 }).fetchAsync()
49+
50+
test.equal(tmp.after_find, true)
51+
})
52+
})

tests/find_findone_userid.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ if (Meteor.isServer) {
9292

9393
// Our monkey-patch of Meteor.publish should preserve the value of 'this'.
9494
Tinytest.add('general - this (context) preserved in publish functions', function (test) {
95-
console.log('this', publishContext)
9695
test.isTrue(publishContext && publishContext.userId)
9796
})
9897

tests/hooks_in_loop.js

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ if (Meteor.isServer) {
1111

1212
// full client-side access
1313
collection.allow({
14-
insert: function () { return true },
14+
insertAsync: function () { return true },
1515
updateAsync: function () { return true },
1616
remove: function () { return true }
1717
})
@@ -36,38 +36,24 @@ if (Meteor.isServer) {
3636
if (Meteor.isClient) {
3737
Meteor.subscribe('test_hooks_in_loop_publish_collection')
3838

39-
Tinytest.addAsync('issue #67 - hooks should get called when mutation method called in a tight loop', function (test, next) {
39+
Tinytest.addAsync('issue #67 - hooks should get called when mutation method called in a tight loop', async function (test) {
4040
let c1 = 0
41-
let c2 = 0
4241

4342
collection.before.update(function (userId, doc, fieldNames, modifier) {
4443
c1++
4544
modifier.$set.client_counter = c1
4645
})
4746

48-
InsecureLogin.ready(function () {
49-
Meteor.call('test_hooks_in_loop_reset_collection', function (nil, result) {
50-
function start (id) {
51-
for (let i = 0; i < times; i++) {
52-
// TODO(v3): allow-deny error findOne on server
53-
collection.updateAsync({ _id: id }, { $set: { times } }).then(function (nil) {
54-
c2++
55-
check()
56-
})
57-
}
58-
}
47+
await InsecureLogin.ready(async function () {
48+
await Meteor.callAsync('test_hooks_in_loop_reset_collection')
5949

60-
function check () {
61-
if (c2 === times) {
62-
test.equal(collection.find({ times, client_counter: times, server_counter: times }).count(), 1)
63-
next()
64-
}
65-
}
50+
const id = await collection.insertAsync({ times: 0, client_counter: 0, server_counter: 0 })
6651

67-
collection.insert({ times: 0, client_counter: 0, server_counter: 0 }, function (nil, id) {
68-
start(id)
69-
})
70-
})
52+
for (let i = 0; i < times; i++) {
53+
await collection.updateAsync({ _id: id }, { $set: { times } })
54+
}
55+
56+
test.equal(collection.find({ times, client_counter: times, server_counter: times }).count(), 1)
7157
})
7258
})
7359
}

tests/insert_allow.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ if (Meteor.isServer) {
1010
collection.allow({
1111
insert (userId, doc) { return doc.allowed },
1212
insertAsync (userId, doc) {
13-
console.log('doc', doc)
1413
return doc.allowed
1514
},
1615
update () { return true },

tests/remove_allow.js

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ const collection = new Mongo.Collection('test_remove_allow_collection')
88
if (Meteor.isServer) {
99
// full client-side access
1010
collection.allow({
11-
insert () { return true },
12-
update () { return true },
11+
insertAsync () { return true },
12+
updateAsync () { return true },
1313
remove (userId, doc) { return doc.allowed },
1414
removeAsync (userId, doc) { return doc.allowed }
1515
})
@@ -28,30 +28,27 @@ if (Meteor.isServer) {
2828
if (Meteor.isClient) {
2929
Meteor.subscribe('test_remove_allow_publish_collection')
3030

31-
Tinytest.addAsync('remove - only one of two collection documents should be allowed to be removed', function (test, next) {
31+
Tinytest.addAsync('remove - only one of two collection documents should be allowed to be removed', async function (test) {
3232
collection.before.remove(function (userId, doc) {
3333
test.equal(doc.start_value, true)
3434
})
3535

36-
InsecureLogin.ready(function () {
37-
Meteor.call('test_remove_allow_reset_collection', function (nil, result) {
38-
async function start (id1, id2) {
39-
// TODO(v3): allow-deny
40-
await collection.removeAsync({ _id: id1 })
41-
// just ignore the error
42-
await collection.removeAsync({ _id: id2 }).catch(() => {})
43-
44-
test.equal(collection.find({ start_value: true }).count(), 1, 'only one document should remain')
45-
next()
46-
}
47-
48-
// Insert two documents
49-
collection.insert({ start_value: true, allowed: true }, function (err1, id1) {
50-
collection.insert({ start_value: true, allowed: false }, function (err2, id2) {
51-
start(id1, id2)
52-
})
53-
})
54-
})
36+
await InsecureLogin.ready(async function () {
37+
await Meteor.callAsync('test_remove_allow_reset_collection')
38+
39+
const id1 = await collection.insertAsync({ start_value: true, allowed: true })
40+
const id2 = await collection.insertAsync({ start_value: true, allowed: false })
41+
42+
// TODO(v3): allow-deny
43+
await collection.removeAsync({ _id: id1 })
44+
try {
45+
await collection.removeAsync({ _id: id2 })
46+
test.fail('should not be allowed to remove')
47+
} catch (e) {
48+
// just ignore the error - it is expected
49+
}
50+
51+
test.equal(collection.find({ start_value: true }).count(), 1, 'only one document should remain')
5552
})
5653
})
5754
}

0 commit comments

Comments
 (0)