Repository: Automattic/mongoose (JavaScript)

Issue:
hook is not of type Aggregate
### Prerequisites

- [X] I have written a descriptive issue title
- [X] I have searched existing issues to ensure the bug has not already been reported


### Mongoose version

8.6.1

### Node.js version

20.10.0

### MongoDB server version

7.0.9

### Typescript version (if applicable)

_No response_

### Description

this context in hooks are not of type Aggregate. 

This issue arises when custom static methods overwrite built-in aggregate functions. (as seen in the implementation in `mongoose-delete`)

This issue is similar to the past one (https://github.com/Automattic/mongoose/issues/7790), where the context in hooks was not of type Query. I noticed that it has been fixed in this PR (https://github.com/Automattic/mongoose/commit/f848b54d33cc5f61be3b1f0539370f9c43d9d4e2), which filters custom static methods that overwrite existing query middleware in `applyStaticHooks.js`.

### Steps to Reproduce

I created a minimum reproduction repository. (https://github.com/dragontaek-lee/mongoose-staticHooks-overwrite-support-more-cases)

I created some test cases to reproduce this issue in the reproduction repository.
The code can be found in `index.js`, `test/aggregate.middleware.test.js`, or `test/query.middleware.test.js`. 
Tests and scripts can also be run using the npm commands mentioned in the README.

**The summarized reproduction code is as follows:**

```
schema1.statics.aggregate = function(pipeline) {
    let match = { $match: { deleted: { '$ne': false } } };

    if (pipeline.length && pipeline[0].$match) {
        pipeline[0].$match.deleted = { '$ne': false };
    } else {
        pipeline.unshift(match);
    }

    const query = Model.aggregate.apply(this, [pipeline]);
    return query;
};
```

Assume that the static method overwrites the built-in Mongoose aggregate function.

```
schema1.pre('aggregate', function(next) {
    console.log(this);
    next();
});

schema1.post('aggregate', function() {
    console.log(this);
});
```

In this situation, also assume that there are pre and post hooks for aggregate.

**Test 1**

- both the custom static method and the built-in Mongoose aggregate hooks are executed and both the pre and post hooks are called twice

**Test 2**

- this context differs between the custom static method and the built-in aggregate function.
- in the first pre-hook and the second post-hook call, `this` context **is not an instance of Aggregate**, but in the second pre-hook and the first post-hook call, **it becomes an instance of Aggregate.**

### Expected Behavior

Since the issues with query middleware were fixed in this PR (https://github.com/Automattic/mongoose/commit/f848b54d33cc5f61be3b1f0539370f9c43d9d4e2), it is expected that if a custom static method overwrites an existing aggregate middleware, the middleware should not be applied by default, just as it works with query middleware.

I’m going to submit the PR right away for this.


Discussion:
Fix PR: https://github.com/Automattic/mongoose/pull/14904



Patch:
diff --git a/lib/constants.js b/lib/constants.js
index 83a66832b55..f5f5c5a19b3 100644
--- a/lib/constants.js
+++ b/lib/constants.js
@@ -34,3 +34,13 @@ const queryMiddlewareFunctions = queryOperations.concat([
 ]);
 
 exports.queryMiddlewareFunctions = queryMiddlewareFunctions;
+
+/*!
+ * ignore
+ */
+
+const aggregateMiddlewareFunctions = [
+  'aggregate'
+];
+
+exports.aggregateMiddlewareFunctions = aggregateMiddlewareFunctions;
diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js
index 957e94f2288..34611c3b39c 100644
--- a/lib/helpers/model/applyStaticHooks.js
+++ b/lib/helpers/model/applyStaticHooks.js
@@ -1,7 +1,12 @@
 'use strict';
 
-const middlewareFunctions = require('../../constants').queryMiddlewareFunctions;
 const promiseOrCallback = require('../promiseOrCallback');
+const { queryMiddlewareFunctions, aggregateMiddlewareFunctions } = require('../../constants');
+
+const middlewareFunctions = [
+  ...queryMiddlewareFunctions,
+  ...aggregateMiddlewareFunctions
+];
 
 module.exports = function applyStaticHooks(model, hooks, statics) {
   const kareemOptions = {
@@ -10,7 +15,7 @@ module.exports = function applyStaticHooks(model, hooks, statics) {
   };
 
   hooks = hooks.filter(hook => {
-    // If the custom static overwrites an existing query middleware, don't apply
+    // If the custom static overwrites an existing query/aggregate middleware, don't apply
     // middleware to it by default. This avoids a potential backwards breaking
     // change with plugins like `mongoose-delete` that use statics to overwrite
     // built-in Mongoose functions.


Test patch:
diff --git a/test/model.test.js b/test/model.test.js
index 8e94a876694..c2e3f68c1e8 100644
--- a/test/model.test.js
+++ b/test/model.test.js
@@ -10,6 +10,7 @@ const assert = require('assert');
 const { once } = require('events');
 const random = require('./util').random;
 const util = require('./util');
+const model = require('../lib/model');
 
 const mongoose = start.mongoose;
 const Schema = mongoose.Schema;
@@ -5861,6 +5862,35 @@ describe('Model', function() {
 
   });
 
+  it('custom statics that overwrite aggregate functions dont get hooks by default (gh-14903)', async function() {
+
+    const schema = new Schema({ name: String });
+
+    schema.statics.aggregate = function(pipeline) {
+      return model.aggregate.apply(this, [pipeline]);
+    };
+
+    let called = 0;
+    schema.pre('aggregate', function(next) {
+      ++called;
+      next();
+    });
+    const Model = db.model('Test', schema);
+
+    await Model.create({ name: 'foo' });
+
+    const res = await Model.aggregate([
+      {
+        $match: {
+          name: 'foo'
+        }
+      }
+    ]);
+
+    assert.ok(res[0].name);
+    assert.equal(called, 1);
+  });
+
   it('error handling middleware passes saved doc (gh-7832)', async function() {
     const schema = new Schema({ _id: Number });