Skip to content

Commit

Permalink
Replace AOP terminology
Browse files Browse the repository at this point in the history
  • Loading branch information
harryadel committed Sep 17, 2024
1 parent 8484d71 commit 7c3df94
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 141 deletions.
2 changes: 1 addition & 1 deletion client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'
import { Tracker } from 'meteor/tracker'
import { CollectionHooks } from './collection-hooks.js'

import './advices'
import './wrappers.js'

CollectionHooks.getUserId = function getUserId () {
let userId
Expand Down
76 changes: 38 additions & 38 deletions collection-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Mongo } from 'meteor/mongo'
import { EJSON } from 'meteor/ejson'
import { LocalCollection } from 'meteor/minimongo'

// Relevant AOP terminology:
// Aspect: User code that runs before/after (hook)
// Advice: Wrapper code that knows when to call user code (aspects)
// Pointcut: before/after
const advices = {}
// Hooks terminology:
// Hook: User-defined function that runs before/after collection operations
// Wrapper: Code that knows when to call user-defined hooks
// Timing: before/after
const wrappers = {}

export const CollectionHooks = {
defaults: {
Expand Down Expand Up @@ -44,33 +44,33 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (
self,
constructor
) {
// Offer a public API to allow the user to define aspects
// Offer a public API to allow the user to define hooks
// Example: collection.before.insert(func);
['before', 'after'].forEach(function (pointcut) {
Object.entries(advices).forEach(function ([method, advice]) {
if (advice === 'upsert' && pointcut === 'after') return
['before', 'after'].forEach(function (timing) {
Object.entries(wrappers).forEach(function ([method, wrapper]) {
if (wrapper === 'upsert' && timing === 'after') return

Meteor._ensure(self, pointcut, method)
Meteor._ensure(self, '_hookAspects', method)
Meteor._ensure(self, timing, method)
Meteor._ensure(self, '_hooks', method)

self._hookAspects[method][pointcut] = []
self[pointcut][method] = function (aspect, options) {
self._hooks[method][timing] = []
self[timing][method] = function (hook, options) {
let target = {
aspect,
options: CollectionHooks.initOptions(options, pointcut, method)
hook,
options: CollectionHooks.initOptions(options, timing, method)
}
// adding is simply pushing it to the array
self._hookAspects[method][pointcut].push(target)
self._hooks[method][timing].push(target)

return {
replace (aspect, options) {
replace (hook, options) {
// 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 src = self._hooks[method][timing]
const targetIndex = src.findIndex((entry) => entry === target)
const newTarget = {
aspect,
options: CollectionHooks.initOptions(options, pointcut, method)
hook,
options: CollectionHooks.initOptions(options, timing, method)
}
src.splice(targetIndex, 1, newTarget)
// update the target to get the correct index in future calls
Expand All @@ -79,9 +79,9 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (
remove () {
// 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 src = self._hooks[method][timing]
const targetIndex = src.findIndex((entry) => entry === target)
self._hookAspects[method][pointcut].splice(targetIndex, 1)
self._hooks[method][timing].splice(targetIndex, 1)
}
}
}
Expand All @@ -93,8 +93,8 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (
// Example: collection.hookOptions.after.update = {fetchPrevious: false};
self.hookOptions = EJSON.clone(CollectionHooks.defaults)

// Wrap mutator methods, letting the defined advice do the work
Object.entries(advices).forEach(function ([method, advice]) {
// Wrap mutator methods, letting the defined wrapper do the work
Object.entries(wrappers).forEach(function ([method, wrapper]) {
// For client side, it wraps around minimongo LocalCollection
// For server side, it wraps around mongo Collection._collection (i.e. driver directly)
const collection =
Expand Down Expand Up @@ -140,21 +140,21 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (
// even on an `insert`. That's why we've chosen to disable this for now.
// if (method === "update" && Object(args[2]) === args[2] && args[2].upsert) {
// method = "upsert";
// advice = CollectionHooks.getAdvice(method);
// wrapper = CollectionHooks.getWrapper(method);
// }

return advice.call(
return wrapper.call(
this,
CollectionHooks.getUserId(),
_super,
self,
method === 'upsert'
? {
insert: self._hookAspects.insert || {},
update: self._hookAspects.update || {},
upsert: self._hookAspects.upsert || {}
insert: self._hooks.insert || {},
update: self._hooks.update || {},
upsert: self._hooks.upsert || {}
}
: self._hookAspects[method] || {},
: self._hooks[method] || {},
function (doc) {
return typeof self._transform === 'function'
? function (d) {
Expand Down Expand Up @@ -187,26 +187,26 @@ CollectionHooks.extendCollectionInstance = function extendCollectionInstance (
})
}

CollectionHooks.defineAdvice = (method, advice) => {
advices[method] = advice
CollectionHooks.defineWrapper = (method, wrapper) => {
wrappers[method] = wrapper
}

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

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

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

CollectionHooks.getDocs = function getDocs (
Expand Down
54 changes: 15 additions & 39 deletions find.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const ASYNC_METHODS = ['countAsync', 'fetchAsync', 'forEachAsync', 'mapAsync']
* That's why we need to wrap all async methods of cursor instance. We're doing this by creating another cursor
* within these wrapped methods with selector and options updated by before hooks.
*/
CollectionHooks.defineAdvice('find', function (userId, _super, instance, aspects, getTransform, args, suppressAspects) {
CollectionHooks.defineWrapper('find', function (userId, _super, instance, hooks, getTransform, args, suppressHooks) {
// const ctx = { context: this, _super, args }
const selector = CollectionHooks.normalizeSelector(instance._getFindSelector(args))
const options = instance._getFindOptions(args)
Expand All @@ -19,52 +19,28 @@ CollectionHooks.defineAdvice('find', function (userId, _super, instance, aspects
// Wrap async cursor methods
ASYNC_METHODS.forEach((method) => {
if (cursor[method]) {
cursor[method] = async (...args) => {
let abort = false
for (const aspect of aspects.before) {
const result = await aspect.aspect.call(this, userId, selector, options)
const originalMethod = cursor[method];

Check failure on line 22 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon
cursor[method] = async function (...args) {
let abort = false;

Check failure on line 24 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon
for (const hook of hooks.before) {
const result = await hook.hook.call(this, userId, selector, options);

Check failure on line 26 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon
if (result === false) {
abort = true
abort = true;

Check failure on line 28 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon
}
}

// Take #1 - monkey patch existing cursor
// Now that before hooks have run, update the cursor selector & options
// Special case for "undefined" selector, which means none of the documents
// This is a full c/p from Meteor's minimongo/cursor.js, it probably doesn't make too much sense and is too
// error-prone to maintain as each Meteor change would require
// Modify the existing cursor instead of creating a new one
this.selector = abort ? undefined : selector;

Check failure on line 33 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon
this.options = options;

Check failure on line 34 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon

// cursor.sorter = null
// cursor.matcher = new Minimongo.Matcher(Mongo.Collection._rewriteSelector(abort ? undefined : selector))
// if (Minimongo.LocalCollection._selectorIsIdPerhapsAsObject(selector)) {
// // eslint-disable-next-line no-prototype-builtins
// cursor._selectorId = Object.prototype.hasOwnProperty(selector, '_id') ? selector._id : selector
// } else {
// cursor._selectorId = undefined
// if (cursor.matcher.hasGeoQuery() || options.sort) {
// cursor.sorter = new Minimongo.Sorter(options.sort || [])
// }
// }
// cursor.skip = options.skip || 0
// cursor.limit = options.limit
// cursor.fields = options.projection || options.fields
// cursor._projectionFn = Minimongo.LocalCollection._compileProjection(cursor.fields || {})
// cursor._transform = Minimongo.LocalCollection.wrapTransform(options.transform)
// if (typeof Tracker !== 'undefined') {
// cursor.reactive = options.reactive === undefined ? true : options.reactive
// }
const result = await originalMethod.apply(this, args);

Check failure on line 36 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon

// Take #2 - create new cursor
const newCursor = _super.call(this, abort ? undefined : selector, options)

const result = await newCursor[method](...args)

for (const aspect of aspects.after) {
await aspect.aspect.call(this, userId, selector, options, cursor)
for (const hook of hooks.after) {
await hook.hook.call(this, userId, selector, options, this);

Check failure on line 39 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon
}

return result
}
return result;

Check failure on line 42 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon
};

Check failure on line 43 in find.js

View workflow job for this annotation

GitHub Actions / test

Extra semicolon
}
})

Expand Down
14 changes: 7 additions & 7 deletions findone.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { CollectionHooks } from './collection-hooks'

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

// before
if (!suppressAspects) {
for (const o of aspects.before) {
const r = await o.aspect.call(ctx, userId, selector, options)
if (!suppressHooks) {
for (const o of hooks.before) {
const r = await o.hook.call(ctx, userId, selector, options)
if (r === false) {
abort = true
break
Expand All @@ -20,9 +20,9 @@ CollectionHooks.defineAdvice('findOne', async function (userId, _super, instance
}

async function after (doc) {
if (!suppressAspects) {
for (const o of aspects.after) {
await o.aspect.call(ctx, userId, selector, options, doc)
if (!suppressHooks) {
for (const o of hooks.after) {
await o.hook.call(ctx, userId, selector, options, doc)
}
}
}
Expand Down
16 changes: 7 additions & 9 deletions insert.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EJSON } from 'meteor/ejson'
import { Mongo } from 'meteor/mongo'
import { CollectionHooks } from './collection-hooks'

CollectionHooks.defineAdvice('insert', async function (userId, _super, instance, aspects, getTransform, args, suppressAspects) {
CollectionHooks.defineWrapper('insert', async function (userId, _super, instance, hooks, getTransform, args, suppressHooks) {
const ctx = { context: this, _super, args }
let doc = args[0]
let callback
Expand All @@ -15,14 +15,12 @@ CollectionHooks.defineAdvice('insert', async function (userId, _super, instance,
let ret

// before
if (!suppressAspects) {
if (!suppressHooks) {
try {
for (const o of aspects.before) {
const r = await o.aspect.call({ transform: getTransform(doc), ...ctx }, userId, doc)
for (const o of hooks.before) {
const r = await o.hook.call({ transform: getTransform(doc), ...ctx }, userId, doc)
if (r === false) {
abort = true
// TODO(v3): before it was before.forEach() so break was not possible
// maybe we need to keep it that way?
break
}
}
Expand All @@ -49,11 +47,11 @@ CollectionHooks.defineAdvice('insert', async function (userId, _super, instance,
doc = EJSON.clone(doc)
doc._id = id
}
if (!suppressAspects) {
if (!suppressHooks) {
const lctx = { transform: getTransform(doc), _id: id, err, ...ctx }

for (const o of aspects.after) {
await o.aspect.call(lctx, userId, doc)
for (const o of hooks.after) {
await o.hook.call(lctx, userId, doc)
}
}
return id
Expand Down
22 changes: 11 additions & 11 deletions remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { CollectionHooks } from './collection-hooks'

const isEmpty = (a) => !Array.isArray(a) || !a.length

CollectionHooks.defineAdvice(
CollectionHooks.defineWrapper(
'remove',
async function (
userId,
_super,
instance,
aspects,
hooks,
getTransform,
args,
suppressAspects
suppressHooks
) {
const ctx = { context: this, _super, args }
const [selector, callback] = args
Expand All @@ -21,9 +21,9 @@ CollectionHooks.defineAdvice(
let abort
const prev = []

if (!suppressAspects) {
if (!suppressHooks) {
try {
if (!isEmpty(aspects.before) || !isEmpty(aspects.after)) {
if (!isEmpty(hooks.before) || !isEmpty(hooks.after)) {
const cursor = await CollectionHooks.getDocs.call(
this,
instance,
Expand All @@ -33,14 +33,14 @@ CollectionHooks.defineAdvice(
}

// copy originals for convenience for the 'after' pointcut
if (!isEmpty(aspects.after)) {
if (!isEmpty(hooks.after)) {
docs.forEach((doc) => prev.push(EJSON.clone(doc)))
}

// before
for (const o of aspects.before) {
for (const o of hooks.before) {
for (const doc of docs) {
const r = await o.aspect.call(
const r = await o.hook.call(
{ transform: getTransform(doc), ...ctx },
userId,
doc
Expand All @@ -64,10 +64,10 @@ CollectionHooks.defineAdvice(
}

async function after (err) {
if (!suppressAspects) {
for (const o of aspects.after) {
if (!suppressHooks) {
for (const o of hooks.after) {
for (const doc of prev) {
await o.aspect.call(
await o.hook.call(
{ transform: getTransform(doc), err, ...ctx },
userId,
doc
Expand Down
2 changes: 1 addition & 1 deletion server.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor'
import { CollectionHooks } from './collection-hooks'

import './advices'
import './wrappers'

const publishUserId = new Meteor.EnvironmentVariable()

Expand Down
Loading

0 comments on commit 7c3df94

Please sign in to comment.