Skip to content

Commit

Permalink
Async before & after hooks for all methods except find
Browse files Browse the repository at this point in the history
  • Loading branch information
Bero committed Apr 23, 2024
1 parent 5534b93 commit aa97b23
Show file tree
Hide file tree
Showing 11 changed files with 832 additions and 524 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/testsuite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
# - '--release 2.3'
# - '--release 2.8.1'
# - '--release 2.14'
- '--release 3.0-beta.6'
- '--release 3.0-rc.0'
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
112 changes: 75 additions & 37 deletions collection-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,23 @@ const advices = {}

export const CollectionHooks = {
defaults: {
before: { insert: {}, update: {}, remove: {}, upsert: {}, find: {}, findOne: {}, all: {} },
after: { insert: {}, update: {}, remove: {}, find: {}, findOne: {}, all: {} },
before: {
insert: {},
update: {},
remove: {},
upsert: {},
find: {},
findOne: {},
all: {}
},
after: {
insert: {},
update: {},
remove: {},
find: {},
findOne: {},
all: {}
},
all: { insert: {}, update: {}, remove: {}, find: {}, findOne: {}, all: {} }
},
directEnv: new Meteor.EnvironmentVariable(),
Expand All @@ -25,7 +40,10 @@ export const CollectionHooks = {
}
}

CollectionHooks.extendCollectionInstance = function extendCollectionInstance (self, constructor) {
CollectionHooks.extendCollectionInstance = function extendCollectionInstance (
self,
constructor
) {
// Offer a public API to allow the user to define aspects
// Example: collection.before.insert(func);
['before', 'after'].forEach(function (pointcut) {
Expand All @@ -49,7 +67,7 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (se
// replacing is done by determining the actual index of a given target
// and replace this with the new one
const src = self._hookAspects[method][pointcut]
const targetIndex = src.findIndex(entry => entry === target)
const targetIndex = src.findIndex((entry) => entry === target)
const newTarget = {
aspect,
options: CollectionHooks.initOptions(options, pointcut, method)
Expand All @@ -62,7 +80,7 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (se
// removing a hook is done by determining the actual index of a given target
// and removing it form the source array
const src = self._hookAspects[method][pointcut]
const targetIndex = src.findIndex(entry => entry === target)
const targetIndex = src.findIndex((entry) => entry === target)
self._hookAspects[method][pointcut].splice(targetIndex, 1)
}
}
Expand All @@ -79,7 +97,8 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (se
Object.entries(advices).forEach(function ([method, advice]) {
// For client side, it wraps around minimongo LocalCollection
// For server side, it wraps around mongo Collection._collection (i.e. driver directly)
const collection = Meteor.isClient || method === 'upsert' ? self : self._collection
const collection =
Meteor.isClient || method === 'upsert' ? self : self._collection

// Store a reference to the original mutator method
const _super = collection[method]
Expand Down Expand Up @@ -109,7 +128,8 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (se
if (
(method === 'update' && this.update.isCalledFromAsync) ||
(method === 'remove' && this.remove.isCalledFromAsync) ||
CollectionHooks.directEnv.get() === true) {
CollectionHooks.directEnv.get() === true
) {
return _super.apply(collection, args)
}

Expand All @@ -123,7 +143,8 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (se
// advice = CollectionHooks.getAdvice(method);
// }

return advice.call(this,
return advice.call(
this,
CollectionHooks.getUserId(),
_super,
self,
Expand All @@ -135,11 +156,13 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (se
}
: self._hookAspects[method] || {},
function (doc) {
return (
typeof self._transform === 'function'
? function (d) { return self._transform(d || doc) }
: function (d) { return d || doc }
)
return typeof self._transform === 'function'
? function (d) {
return self._transform(d || doc)
}
: function (d) {
return d || doc
}
},
args,
false
Expand All @@ -162,15 +185,31 @@ CollectionHooks.defineAdvice = (method, advice) => {
advices[method] = advice
}

CollectionHooks.getAdvice = method => advices[method]
CollectionHooks.getAdvice = (method) => advices[method]

CollectionHooks.initOptions = (options, pointcut, method) =>
CollectionHooks.extendOptions(CollectionHooks.defaults, options, pointcut, method)

CollectionHooks.extendOptions = (source, options, pointcut, method) =>
({ ...options, ...source.all.all, ...source[pointcut].all, ...source.all[method], ...source[pointcut][method] })

CollectionHooks.getDocs = function getDocs (collection, selector, options, fetchFields = {}, { useDirect = false } = {}) {
CollectionHooks.extendOptions(
CollectionHooks.defaults,
options,
pointcut,
method
)

CollectionHooks.extendOptions = (source, options, pointcut, method) => ({
...options,
...source.all.all,
...source[pointcut].all,
...source.all[method],
...source[pointcut][method]
})

CollectionHooks.getDocs = function getDocs (
collection,
selector,
options,
fetchFields = {},
{ useDirect = false } = {}
) {
const findOptions = { transform: null, reactive: false }

if (Object.keys(fetchFields).length > 0) {
Expand Down Expand Up @@ -203,12 +242,18 @@ CollectionHooks.getDocs = function getDocs (collection, selector, options, fetch

// Unlike validators, we iterate over multiple docs, so use
// find instead of findOne:
return (useDirect ? collection.direct : collection).find(selector, findOptions)
return (useDirect ? collection.direct : collection).find(
selector,
findOptions
)
}

// This function normalizes the selector (converting it to an Object)
CollectionHooks.normalizeSelector = function (selector) {
if (typeof selector === 'string' || (selector && selector.constructor === Mongo.ObjectID)) {
if (
typeof selector === 'string' ||
(selector && selector.constructor === Mongo.ObjectID)
) {
return {
_id: selector
}
Expand Down Expand Up @@ -245,7 +290,7 @@ CollectionHooks.getFields = function getFields (mutator) {
Object.entries(mutator).forEach(function ([op, params]) {
// ====ADDED START=======================
if (operators.includes(op)) {
// ====ADDED END=========================
// ====ADDED END=========================
Object.keys(params).forEach(function (field) {
// treat dotted fields as if they are replacing their
// top-level part
Expand All @@ -268,22 +313,26 @@ CollectionHooks.getFields = function getFields (mutator) {
return fields
}

CollectionHooks.reassignPrototype = function reassignPrototype (instance, constr) {
CollectionHooks.reassignPrototype = function reassignPrototype (
instance,
constr
) {
const hasSetPrototypeOf = typeof Object.setPrototypeOf === 'function'
constr = constr || Mongo.Collection

// __proto__ is not available in < IE11
// Note: Assigning a prototype dynamically has performance implications
if (hasSetPrototypeOf) {
Object.setPrototypeOf(instance, constr.prototype)
} else if (instance.__proto__) { // eslint-disable-line no-proto
} else if (instance.__proto__) {
// eslint-disable-line no-proto
instance.__proto__ = constr.prototype // eslint-disable-line no-proto
}
}

CollectionHooks.wrapCollection = function wrapCollection (ns, as) {
if (!as._CollectionConstructor) as._CollectionConstructor = as.Collection
if (!as._CollectionPrototype) as._CollectionPrototype = new as.Collection(null)
if (!as._CollectionPrototype) { as._CollectionPrototype = new as.Collection(null) }

const constructor = ns._NewCollectionContructor || as._CollectionConstructor
const proto = as._CollectionPrototype
Expand All @@ -308,17 +357,6 @@ CollectionHooks.wrapCollection = function wrapCollection (ns, as) {
ns.Collection.apply = Function.prototype.apply
}

CollectionHooks.isPromise = (value) => {
return value && typeof value.then === 'function'
}

CollectionHooks.callAfterValueOrPromise = (value, cb) => {
if (CollectionHooks.isPromise(value)) {
return value.then((res) => cb(res))
}
return cb(value)
}

CollectionHooks.modify = LocalCollection._modify

if (typeof Mongo !== 'undefined') {
Expand Down
27 changes: 14 additions & 13 deletions findone.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import { CollectionHooks } from './collection-hooks'

CollectionHooks.defineAdvice('findOne', function (userId, _super, instance, aspects, getTransform, args, suppressAspects) {
CollectionHooks.defineAdvice('findOne', async function (userId, _super, instance, aspects, getTransform, args, suppressAspects) {
const ctx = { context: this, _super, args }
const selector = CollectionHooks.normalizeSelector(instance._getFindSelector(args))
const options = instance._getFindOptions(args)
let abort

// before
if (!suppressAspects) {
aspects.before.forEach((o) => {
const r = o.aspect.call(ctx, userId, selector, options)
if (r === false) abort = true
})
for (const o of aspects.before) {
const r = await o.aspect.call(ctx, userId, selector, options)
if (r === false) {
abort = true
break
}
}

if (abort) return
}

function after (doc) {
async function after (doc) {
if (!suppressAspects) {
aspects.after.forEach((o) => {
for (const o of aspects.after) {
o.aspect.call(ctx, userId, selector, options, doc)
})
}
}

// return because of callAfterValueOrPromise
return doc
}

const ret = _super.call(this, selector, options)
return CollectionHooks.callAfterValueOrPromise(ret, (ret) => after(ret))
const ret = await _super.call(this, selector, options)
await after(ret)
return ret
})
15 changes: 5 additions & 10 deletions package.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@

Package.describe({
name: 'matb33:collection-hooks',
summary: 'Extends Mongo.Collection with before/after hooks for insert/update/upsert/remove/find/findOne',
summary:
'Extends Mongo.Collection with before/after hooks for insert/update/upsert/remove/find/findOne',
version: '1.3.1',
git: 'https://github.com/Meteor-Community-Packages/meteor-collection-hooks'
})

Package.onUse(function (api) {
api.versionsFrom(['2.3', '2.8.1', '3.0-beta.0'])
api.versionsFrom(['2.13', '3.0-beta.0'])

api.use([
'mongo',
'tracker',
'ejson',
'minimongo',
'ecmascript'
])
api.use(['mongo', 'tracker', 'ejson', 'minimongo', 'ecmascript'])

api.use('zodern:[email protected]', 'server')

Expand All @@ -31,7 +26,7 @@ Package.onUse(function (api) {
Package.onTest(function (api) {
// var isTravisCI = process && process.env && process.env.TRAVIS

api.versionsFrom(['1.12', '2.3', '3.0-beta.0'])
api.versionsFrom(['2.13', '3.0-beta.0'])

api.use([
'matb33:collection-hooks',
Expand Down
Loading

1 comment on commit aa97b23

@dokithonon
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested it in a simple app and this is working nice : https://github.com/dokithonon/meteor-test-async/tree/3.0-rc.0
thanks a lot

Please sign in to comment.