mongoosleuth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/dist/index.d.mts +138 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +431 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +402 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mongoosleuth Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="assets/banner.svg" width="100%" alt="Mongoosleuth Banner"/>
|
|
4
|
+
|
|
5
|
+
<a href="https://git.io/typing-svg">
|
|
6
|
+
<img src="https://readme-typing-svg.demolab.com/?font=Fira+Code&size=18&pause=1500&color=0F766E¢er=true&vCenter=true&width=600&lines=Zero%20runtime%20dependencies.%20Just%20a%20peerDependency.;Catches%20the%20N%2B1%20queries%20Bullet%20catches%20in%20Rails.;Built%20for%20Mongoose.%20Built%20for%20TypeScript." alt="Typing SVG"/>
|
|
7
|
+
</a>
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
<img src="https://img.shields.io/npm/v/mongoosleuth?style=for-the-badge&color=0F766E&label=npm" alt="npm version"/>
|
|
12
|
+
<img src="https://img.shields.io/badge/dependencies-zero-0F766E?style=for-the-badge" alt="zero dependencies"/>
|
|
13
|
+
<img src="https://img.shields.io/badge/TypeScript-Ready-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript ready"/>
|
|
14
|
+
<img src="https://img.shields.io/badge/module-ESM%20%2B%20CJS-134E4A?style=for-the-badge" alt="ESM and CJS"/>
|
|
15
|
+
<img src="https://img.shields.io/badge/Node.js-%3E%3D18-339933?style=for-the-badge&logo=node.js&logoColor=white" alt="Node.js >=18"/>
|
|
16
|
+
<img src="https://img.shields.io/npm/l/mongoosleuth?style=for-the-badge&color=134E4A" alt="license"/>
|
|
17
|
+
<br/>
|
|
18
|
+
<img src="https://img.shields.io/github/stars/Fizm00/Mongoosleuth?style=for-the-badge&color=0F766E" alt="GitHub stars"/>
|
|
19
|
+
<img src="https://img.shields.io/github/issues/Fizm00/Mongoosleuth?style=for-the-badge&color=134E4A" alt="GitHub issues"/>
|
|
20
|
+
<img src="https://img.shields.io/github/last-commit/Fizm00/Mongoosleuth?style=for-the-badge&color=134E4A" alt="GitHub last commit"/>
|
|
21
|
+
|
|
22
|
+
<p><strong>A zero-runtime-dependency Node.js + TypeScript library that catches N+1 query
|
|
23
|
+
patterns in Mongoose applications before they reach production.</strong></p>
|
|
24
|
+
<p>Inspired by Ruby on Rails' <code>Bullet</code> gem — built for an ecosystem that never had one.</p>
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<br/>
|
|
29
|
+
|
|
30
|
+
## Table of contents
|
|
31
|
+
|
|
32
|
+
- [The problem](#the-problem)
|
|
33
|
+
- [What Mongoosleuth does about it](#what-mongoosleuth-does-about-it)
|
|
34
|
+
- [Installation](#installation)
|
|
35
|
+
- [Compatibility & Requirements](#compatibility--requirements)
|
|
36
|
+
- [Usage](#usage)
|
|
37
|
+
- [Express / Fastify / Koa middleware](#express--fastify--koa-middleware)
|
|
38
|
+
- [Manual scope (background jobs, scripts, anything without HTTP)](#manual-scope-background-jobs-scripts-anything-without-http)
|
|
39
|
+
- [Ignore options](#ignore-options)
|
|
40
|
+
- [Pluggable Custom Reporters](#pluggable-custom-reporters)
|
|
41
|
+
- [Configuration Reference](#configuration-reference)
|
|
42
|
+
- [How it works](#how-it-works)
|
|
43
|
+
- [Technical details: the fingerprinting system](#technical-details-the-fingerprinting-system)
|
|
44
|
+
- [FAQ](#faq)
|
|
45
|
+
- [Development scripts](#development-scripts)
|
|
46
|
+
- [Non-negotiable principles](#non-negotiable-principles)
|
|
47
|
+
- [Contributing & Issues](#contributing--issues)
|
|
48
|
+
- [Star History](#star-history)
|
|
49
|
+
- [Roadmap](#roadmap)
|
|
50
|
+
- [License](#license)
|
|
51
|
+
|
|
52
|
+
<br/>
|
|
53
|
+
|
|
54
|
+
## The problem
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// Looks completely fine. Isn't.
|
|
58
|
+
const posts = await Post.find();
|
|
59
|
+
|
|
60
|
+
for (const post of posts) {
|
|
61
|
+
// One extra query. Per post. Every single time this route is hit.
|
|
62
|
+
post.author = await User.findById(post.authorId);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Fifty posts. Fifty-one queries. The response still comes back. Nobody notices —
|
|
67
|
+
until the collection grows, the route gets popular, and the database starts
|
|
68
|
+
spending more time on this one endpoint than on everything else combined.
|
|
69
|
+
|
|
70
|
+
This is the N+1 query problem, and in the Ruby on Rails world it has had a
|
|
71
|
+
well-known solution for over a decade: the `Bullet` gem watches your queries
|
|
72
|
+
in development and tells you exactly where you went wrong. Mongoose has never
|
|
73
|
+
had an equivalent. Mongoosleuth is that equivalent.
|
|
74
|
+
|
|
75
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
76
|
+
|
|
77
|
+
## What Mongoosleuth does about it
|
|
78
|
+
|
|
79
|
+
It watches the same code, says nothing while everything is fine, and the
|
|
80
|
+
moment that loop fires the same query shape more times than your threshold
|
|
81
|
+
allows in a single request, it tells you exactly where to look:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
[mongoosleuth] N+1 detected
|
|
85
|
+
model: User
|
|
86
|
+
query: findById(<value>)
|
|
87
|
+
called 50 times in routes/posts.ts:14
|
|
88
|
+
fix: use .populate('author') or User.find({ _id: { $in: [...] } })
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
No raw values are ever logged — only field names and value _types_ — so this
|
|
92
|
+
is safe to leave running against a database full of real, sensitive, user data.
|
|
93
|
+
|
|
94
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install mongoosleuth
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`mongoose` is a peer dependency, not a regular dependency — make sure it is
|
|
103
|
+
already installed in your project.
|
|
104
|
+
|
|
105
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
106
|
+
|
|
107
|
+
## Compatibility & Requirements
|
|
108
|
+
|
|
109
|
+
- **Node.js**: `>= 18.0.0`
|
|
110
|
+
- **Mongoose**: `^7.0.0 || ^8.0.0`
|
|
111
|
+
- **Zero runtime dependencies**: Will not clutter your `node_modules` or increase bundle sizes.
|
|
112
|
+
|
|
113
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
114
|
+
|
|
115
|
+
## Usage
|
|
116
|
+
|
|
117
|
+
### Express / Fastify / Koa middleware
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import mongoose from 'mongoose';
|
|
121
|
+
import { Mongoosleuth } from 'mongoosleuth';
|
|
122
|
+
|
|
123
|
+
const sleuth = new Mongoosleuth({
|
|
124
|
+
enabled: process.env.NODE_ENV !== 'production',
|
|
125
|
+
threshold: 3,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Patch Mongoose's query execution once, at startup.
|
|
129
|
+
sleuth.attach(mongoose);
|
|
130
|
+
|
|
131
|
+
// Open a tracking scope for every incoming request.
|
|
132
|
+
app.use(sleuth.middleware());
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Manual scope (background jobs, scripts, anything without HTTP)
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
await sleuth.run(async () => {
|
|
139
|
+
const users = await User.find({ status: 'active' });
|
|
140
|
+
|
|
141
|
+
for (const user of users) {
|
|
142
|
+
// Tracked and analyzed exactly like a request — fires the same warning
|
|
143
|
+
// if this turns out to be a loop instead of a batched query.
|
|
144
|
+
const profile = await Profile.findOne({ userId: user.id });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Ignore options
|
|
150
|
+
|
|
151
|
+
You can exclude specific collections or operations from N+1 analysis to avoid tracking logs on expected bulk operations:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const sleuth = new Mongoosleuth({
|
|
155
|
+
ignore: [
|
|
156
|
+
{ model: 'AuditLog' }, // Ignore all queries on AuditLog collection
|
|
157
|
+
{ operation: 'updateOne' }, // Ignore all updates on any collection
|
|
158
|
+
{ model: 'Session', operation: 'findOne' } // Ignore specific collection-operation pair
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Pluggable Custom Reporters
|
|
164
|
+
|
|
165
|
+
Mongoosleuth comes built-in with `ConsoleReporter` (default) and `JsonReporter` (for NDJSON logging pipelines). You can also easily implement the `Reporter` interface to ship warnings to Slack, Sentry, or S3:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { Mongoosleuth, JsonReporter, Reporter, Finding } from 'mongoosleuth';
|
|
169
|
+
|
|
170
|
+
class SlackReporter implements Reporter {
|
|
171
|
+
report(findings: Finding[]): void {
|
|
172
|
+
for (const finding of findings) {
|
|
173
|
+
// Post warning to Slack hook
|
|
174
|
+
fetch('https://hooks.slack.com/services/...', {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
text: `🚨 N+1 Query detected on *${finding.model}* (${finding.count} calls) at ${finding.callSite}`
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sleuth = new Mongoosleuth({
|
|
186
|
+
reporters: [
|
|
187
|
+
new SlackReporter(),
|
|
188
|
+
new JsonReporter(line => process.stderr.write(line + '\n'))
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
194
|
+
|
|
195
|
+
## Configuration Reference
|
|
196
|
+
|
|
197
|
+
<details>
|
|
198
|
+
<summary><strong>View Configuration Options (MongoosleuthOptions)</strong></summary>
|
|
199
|
+
|
|
200
|
+
<br/>
|
|
201
|
+
|
|
202
|
+
| Option | Type | Default | Description |
|
|
203
|
+
| :--- | :--- | :--- | :--- |
|
|
204
|
+
| `enabled` | `boolean` | `process.env.NODE_ENV !== 'production'` | Toggle Mongoosleuth N+1 pattern detection. When set to `false`, it acts as a zero-overhead pass-through. |
|
|
205
|
+
| `threshold` | `number` | `3` | Number of identical query shapes executed from the same call site within a single scope before triggering a warning. Must be `>= 1`. |
|
|
206
|
+
| `captureStackTrace` | `boolean` | `true` | When `true`, automatically identifies the exact file and line number (call site) of queries. Set to `false` for absolute zero-overhead performance (logs will group call sites as `'unknown'`). |
|
|
207
|
+
| `ignore` | `Array<{ model?: string; operation?: string }>` | `[]` | Excludes queries matching specific model names or operations from being tracked. |
|
|
208
|
+
| `reporters` | `Reporter[]` | `[new ConsoleReporter()]` | Pluggable output handlers that receive identified N+1 query findings. |
|
|
209
|
+
|
|
210
|
+
</details>
|
|
211
|
+
|
|
212
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
213
|
+
|
|
214
|
+
## How it works
|
|
215
|
+
|
|
216
|
+
```mermaid
|
|
217
|
+
flowchart TD
|
|
218
|
+
A[Incoming request] --> B[Tracking scope opens<br/>AsyncLocalStorage]
|
|
219
|
+
B --> C[Query interceptor<br/>logs every Mongoose query]
|
|
220
|
+
C --> D[Pattern analyzer<br/>flags repeated query patterns]
|
|
221
|
+
D --> E[Reporter<br/>warns in the console]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Each request gets its own isolated tracking scope. Every query fired inside
|
|
225
|
+
that scope is fingerprinted and logged. When the request finishes, the
|
|
226
|
+
analyzer checks whether any fingerprint repeated, from the same line of code,
|
|
227
|
+
often enough to be a loop rather than a coincidence — and if so, the reporter
|
|
228
|
+
prints it.
|
|
229
|
+
|
|
230
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
231
|
+
|
|
232
|
+
## Technical details: the fingerprinting system
|
|
233
|
+
|
|
234
|
+
At the core of the detector is a deterministic fingerprinting function
|
|
235
|
+
(`src/fingerprint.ts`) that identifies _the shape_ of a query without ever
|
|
236
|
+
touching the actual data inside it.
|
|
237
|
+
|
|
238
|
+
**1. Normalization.** Every primitive value inside a query filter is replaced
|
|
239
|
+
with a tag representing its type, never its value:
|
|
240
|
+
|
|
241
|
+
| Value type | Normalized to |
|
|
242
|
+
| ----------- | ------------- |
|
|
243
|
+
| `string` | `<string>` |
|
|
244
|
+
| `number` | `<number>` |
|
|
245
|
+
| `boolean` | `<boolean>` |
|
|
246
|
+
| `Date` | `<Date>` |
|
|
247
|
+
| `ObjectId` | `<ObjectId>` |
|
|
248
|
+
| `RegExp` | `<RegExp>` |
|
|
249
|
+
| `null` | `<null>` |
|
|
250
|
+
| `undefined` | `<undefined>` |
|
|
251
|
+
|
|
252
|
+
MongoDB query operators such as `$gt`, `$in`, or `$regex` are preserved as
|
|
253
|
+
part of the shape — they change what the query means, so they are never
|
|
254
|
+
collapsed away. Arrays are normalized using the shape of their first element
|
|
255
|
+
only; an empty array is tagged `<empty array>`.
|
|
256
|
+
|
|
257
|
+
**2. Deterministic key sorting.** Object keys are sorted alphabetically at
|
|
258
|
+
every nesting level before the fingerprint is built, so field order in your
|
|
259
|
+
code never affects detection:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
{ name: "John", age: 30 } → { age: <number>, name: <string> }
|
|
263
|
+
{ age: 30, name: "John" } → { age: <number>, name: <string> }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Same fingerprint either way — which is exactly the point.
|
|
267
|
+
|
|
268
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
269
|
+
|
|
270
|
+
## FAQ
|
|
271
|
+
|
|
272
|
+
#### Is it safe to run in production?
|
|
273
|
+
Yes, absolutely. When `enabled` is set to `false` (which is the default outside development/staging environments), all of Mongoosleuth's internal hooks are bypassed and act as zero-overhead no-op pass-throughs. Additionally, Mongoosleuth never captures or logs raw query values (PII data is fully protected by substituting types), ensuring complete data privacy.
|
|
274
|
+
|
|
275
|
+
#### Will this affect my application's performance?
|
|
276
|
+
In development (`enabled: true`), capturing stack traces to locate the exact query line (`callSite`) has a minor CPU cost. If you wish to run high-throughput load tests or minimize trace collection overhead, you can set `captureStackTrace: false` to disable call-site tracing and group queries under a single `'unknown'` location.
|
|
277
|
+
|
|
278
|
+
#### Why not just use built-in database query logging?
|
|
279
|
+
Database profilers and query loggers record individual raw queries independently without context of where they originated. Mongoosleuth isolates query shapes per request scope and groups them by their exact code line location. This allows it to distinguish between normal independent query executions and repeated, identical queries fired from a single loop context (N+1 query pattern).
|
|
280
|
+
|
|
281
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
282
|
+
|
|
283
|
+
## Development scripts
|
|
284
|
+
|
|
285
|
+
| Command | What it does |
|
|
286
|
+
| ---------------- | --------------------------------------------------------------------------- |
|
|
287
|
+
| `npm run build` | Builds dual ESM (`.mjs`) and CommonJS (`.js`) output, plus `.d.ts`/`.d.mts` |
|
|
288
|
+
| `npm test` | Runs the unit and integration suite (Vitest + `mongodb-memory-server`) |
|
|
289
|
+
| `npm run lint` | Checks code style and TypeScript linter compliance |
|
|
290
|
+
| `npm run format` | Formats the codebase with Prettier |
|
|
291
|
+
|
|
292
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
293
|
+
|
|
294
|
+
## Non-negotiable principles
|
|
295
|
+
|
|
296
|
+
> Zero runtime dependencies. `mongoose` is a peer dependency, never a
|
|
297
|
+
> dependency.
|
|
298
|
+
>
|
|
299
|
+
> Privacy first. Raw query values are never logged — only field names and
|
|
300
|
+
> value type tags.
|
|
301
|
+
>
|
|
302
|
+
> No shared state. Every request is isolated through `AsyncLocalStorage`; two
|
|
303
|
+
> concurrent requests never see each other's query logs.
|
|
304
|
+
>
|
|
305
|
+
> Dual module output. Full support for ESM and CommonJS consumers, with
|
|
306
|
+
> accurate `.d.ts` definitions for both.
|
|
307
|
+
|
|
308
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
309
|
+
|
|
310
|
+
## Contributing & Issues
|
|
311
|
+
|
|
312
|
+
We welcome contributions in the form of bug reports, feature suggestions, or pull requests. Please report any issues you encounter via the [GitHub Issues](https://github.com/Fizm00/Mongoosleuth/issues) page.
|
|
313
|
+
|
|
314
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
315
|
+
|
|
316
|
+
## Star History
|
|
317
|
+
|
|
318
|
+
[](https://star-history.com/#Fizm00/Mongoosleuth&Date)
|
|
319
|
+
|
|
320
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
321
|
+
|
|
322
|
+
## Roadmap
|
|
323
|
+
|
|
324
|
+
- **Unused eager loading detection** — flagging a `.populate()` call whose
|
|
325
|
+
result was never actually read, the mirror image of the N+1 problem.
|
|
326
|
+
|
|
327
|
+
<p align="right"><a href="#table-of-contents">▲ Back to top</a></p>
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
MIT
|
|
332
|
+
|
|
333
|
+
<div align="center">
|
|
334
|
+
<img src="assets/footer.svg" width="100%" alt="Mongoosleuth Footer"/>
|
|
335
|
+
</div>
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for configuring Mongoosleuth.
|
|
3
|
+
*/
|
|
4
|
+
interface MongoosleuthOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether Mongoosleuth is enabled.
|
|
7
|
+
* @default process.env.NODE_ENV !== 'production'
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The count threshold for identical queries at a single call-site to flag a finding.
|
|
12
|
+
* @default 3
|
|
13
|
+
*/
|
|
14
|
+
threshold?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to capture the stack trace for precise location mapping.
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
captureStackTrace?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Configurations to ignore specific models or operations.
|
|
22
|
+
*/
|
|
23
|
+
ignore?: Array<{
|
|
24
|
+
model?: string;
|
|
25
|
+
operation?: string;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Reporters to write results to.
|
|
29
|
+
* @default [new ConsoleReporter()]
|
|
30
|
+
*/
|
|
31
|
+
reporters?: Reporter[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Represents a single N+1 query pattern finding.
|
|
35
|
+
*/
|
|
36
|
+
interface Finding {
|
|
37
|
+
/**
|
|
38
|
+
* The name of the Mongoose model being queried.
|
|
39
|
+
*/
|
|
40
|
+
model: string;
|
|
41
|
+
/**
|
|
42
|
+
* The Mongoose query operation (e.g., 'find', 'findOne').
|
|
43
|
+
*/
|
|
44
|
+
operation: string;
|
|
45
|
+
/**
|
|
46
|
+
* The fingerprint representing the query shape, normalized filter, and operation.
|
|
47
|
+
*/
|
|
48
|
+
fingerprint: string;
|
|
49
|
+
/**
|
|
50
|
+
* The number of times this exact pattern was executed.
|
|
51
|
+
*/
|
|
52
|
+
count: number;
|
|
53
|
+
/**
|
|
54
|
+
* The call site (file, line number, column) where the query originated.
|
|
55
|
+
*/
|
|
56
|
+
callSite: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pluggable interface for reporting N+1 query findings.
|
|
60
|
+
*/
|
|
61
|
+
interface Reporter {
|
|
62
|
+
/**
|
|
63
|
+
* Reports the findings to the target output.
|
|
64
|
+
* @param findings The list of findings to report.
|
|
65
|
+
*/
|
|
66
|
+
report(findings: Finding[]): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The main Mongoosleuth library class.
|
|
71
|
+
* Coordinates intercepting queries, request-scoped tracking, and analyzing/reporting.
|
|
72
|
+
*
|
|
73
|
+
* See AGENTS.md: Public API surface.
|
|
74
|
+
*/
|
|
75
|
+
declare class Mongoosleuth {
|
|
76
|
+
readonly options: Required<MongoosleuthOptions>;
|
|
77
|
+
/**
|
|
78
|
+
* Initializes a new instance of Mongoosleuth.
|
|
79
|
+
* @param options Configuration options.
|
|
80
|
+
*/
|
|
81
|
+
constructor(options?: MongoosleuthOptions);
|
|
82
|
+
/**
|
|
83
|
+
* Patches the given Mongoose instance to intercept all query executions.
|
|
84
|
+
*
|
|
85
|
+
* @param mongooseInstance Mongoose library instance to attach to.
|
|
86
|
+
*/
|
|
87
|
+
attach(mongooseInstance: any): void;
|
|
88
|
+
/**
|
|
89
|
+
* Internal helper to run query pattern analysis and trigger reporters.
|
|
90
|
+
*/
|
|
91
|
+
private analyzeAndReport;
|
|
92
|
+
/**
|
|
93
|
+
* Express/Koa/Fastify middleware that opens a RequestScope for each incoming request
|
|
94
|
+
* and runs pattern analysis upon request completion.
|
|
95
|
+
*
|
|
96
|
+
* @returns Request middleware function.
|
|
97
|
+
*/
|
|
98
|
+
middleware(): (req: any, res: any, next: (err?: any) => void) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Manually runs a function within a new query tracking scope.
|
|
101
|
+
* Useful for background jobs, scripts, or manual request cycles outside standard middleware.
|
|
102
|
+
*
|
|
103
|
+
* @param fn The asynchronous function to execute within the scope.
|
|
104
|
+
*/
|
|
105
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Default reporter that prints query N+1 findings to console.warn.
|
|
110
|
+
* See AGENTS.md: Reporter.
|
|
111
|
+
*/
|
|
112
|
+
declare class ConsoleReporter implements Reporter {
|
|
113
|
+
/**
|
|
114
|
+
* Reports the identified N+1 query findings.
|
|
115
|
+
* @param findings The list of findings to report.
|
|
116
|
+
*/
|
|
117
|
+
report(findings: Finding[]): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pluggable reporter that outputs query N+1 findings as a single-line JSON string (NDJSON format).
|
|
122
|
+
* See AGENTS.md: Reporter.
|
|
123
|
+
*/
|
|
124
|
+
declare class JsonReporter implements Reporter {
|
|
125
|
+
private write;
|
|
126
|
+
/**
|
|
127
|
+
* Initializes a new instance of JsonReporter.
|
|
128
|
+
* @param write Optional callback function to write the JSON line. Defaults to console.log.
|
|
129
|
+
*/
|
|
130
|
+
constructor(write?: (line: string) => void);
|
|
131
|
+
/**
|
|
132
|
+
* Serializes the findings to JSON lines and writes them using the write handler.
|
|
133
|
+
* @param findings The list of findings to serialize and report.
|
|
134
|
+
*/
|
|
135
|
+
report(findings: Finding[]): void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { ConsoleReporter, type Finding, JsonReporter, Mongoosleuth, type MongoosleuthOptions, type Reporter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for configuring Mongoosleuth.
|
|
3
|
+
*/
|
|
4
|
+
interface MongoosleuthOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether Mongoosleuth is enabled.
|
|
7
|
+
* @default process.env.NODE_ENV !== 'production'
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The count threshold for identical queries at a single call-site to flag a finding.
|
|
12
|
+
* @default 3
|
|
13
|
+
*/
|
|
14
|
+
threshold?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to capture the stack trace for precise location mapping.
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
captureStackTrace?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Configurations to ignore specific models or operations.
|
|
22
|
+
*/
|
|
23
|
+
ignore?: Array<{
|
|
24
|
+
model?: string;
|
|
25
|
+
operation?: string;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Reporters to write results to.
|
|
29
|
+
* @default [new ConsoleReporter()]
|
|
30
|
+
*/
|
|
31
|
+
reporters?: Reporter[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Represents a single N+1 query pattern finding.
|
|
35
|
+
*/
|
|
36
|
+
interface Finding {
|
|
37
|
+
/**
|
|
38
|
+
* The name of the Mongoose model being queried.
|
|
39
|
+
*/
|
|
40
|
+
model: string;
|
|
41
|
+
/**
|
|
42
|
+
* The Mongoose query operation (e.g., 'find', 'findOne').
|
|
43
|
+
*/
|
|
44
|
+
operation: string;
|
|
45
|
+
/**
|
|
46
|
+
* The fingerprint representing the query shape, normalized filter, and operation.
|
|
47
|
+
*/
|
|
48
|
+
fingerprint: string;
|
|
49
|
+
/**
|
|
50
|
+
* The number of times this exact pattern was executed.
|
|
51
|
+
*/
|
|
52
|
+
count: number;
|
|
53
|
+
/**
|
|
54
|
+
* The call site (file, line number, column) where the query originated.
|
|
55
|
+
*/
|
|
56
|
+
callSite: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pluggable interface for reporting N+1 query findings.
|
|
60
|
+
*/
|
|
61
|
+
interface Reporter {
|
|
62
|
+
/**
|
|
63
|
+
* Reports the findings to the target output.
|
|
64
|
+
* @param findings The list of findings to report.
|
|
65
|
+
*/
|
|
66
|
+
report(findings: Finding[]): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The main Mongoosleuth library class.
|
|
71
|
+
* Coordinates intercepting queries, request-scoped tracking, and analyzing/reporting.
|
|
72
|
+
*
|
|
73
|
+
* See AGENTS.md: Public API surface.
|
|
74
|
+
*/
|
|
75
|
+
declare class Mongoosleuth {
|
|
76
|
+
readonly options: Required<MongoosleuthOptions>;
|
|
77
|
+
/**
|
|
78
|
+
* Initializes a new instance of Mongoosleuth.
|
|
79
|
+
* @param options Configuration options.
|
|
80
|
+
*/
|
|
81
|
+
constructor(options?: MongoosleuthOptions);
|
|
82
|
+
/**
|
|
83
|
+
* Patches the given Mongoose instance to intercept all query executions.
|
|
84
|
+
*
|
|
85
|
+
* @param mongooseInstance Mongoose library instance to attach to.
|
|
86
|
+
*/
|
|
87
|
+
attach(mongooseInstance: any): void;
|
|
88
|
+
/**
|
|
89
|
+
* Internal helper to run query pattern analysis and trigger reporters.
|
|
90
|
+
*/
|
|
91
|
+
private analyzeAndReport;
|
|
92
|
+
/**
|
|
93
|
+
* Express/Koa/Fastify middleware that opens a RequestScope for each incoming request
|
|
94
|
+
* and runs pattern analysis upon request completion.
|
|
95
|
+
*
|
|
96
|
+
* @returns Request middleware function.
|
|
97
|
+
*/
|
|
98
|
+
middleware(): (req: any, res: any, next: (err?: any) => void) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Manually runs a function within a new query tracking scope.
|
|
101
|
+
* Useful for background jobs, scripts, or manual request cycles outside standard middleware.
|
|
102
|
+
*
|
|
103
|
+
* @param fn The asynchronous function to execute within the scope.
|
|
104
|
+
*/
|
|
105
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Default reporter that prints query N+1 findings to console.warn.
|
|
110
|
+
* See AGENTS.md: Reporter.
|
|
111
|
+
*/
|
|
112
|
+
declare class ConsoleReporter implements Reporter {
|
|
113
|
+
/**
|
|
114
|
+
* Reports the identified N+1 query findings.
|
|
115
|
+
* @param findings The list of findings to report.
|
|
116
|
+
*/
|
|
117
|
+
report(findings: Finding[]): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pluggable reporter that outputs query N+1 findings as a single-line JSON string (NDJSON format).
|
|
122
|
+
* See AGENTS.md: Reporter.
|
|
123
|
+
*/
|
|
124
|
+
declare class JsonReporter implements Reporter {
|
|
125
|
+
private write;
|
|
126
|
+
/**
|
|
127
|
+
* Initializes a new instance of JsonReporter.
|
|
128
|
+
* @param write Optional callback function to write the JSON line. Defaults to console.log.
|
|
129
|
+
*/
|
|
130
|
+
constructor(write?: (line: string) => void);
|
|
131
|
+
/**
|
|
132
|
+
* Serializes the findings to JSON lines and writes them using the write handler.
|
|
133
|
+
* @param findings The list of findings to serialize and report.
|
|
134
|
+
*/
|
|
135
|
+
report(findings: Finding[]): void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { ConsoleReporter, type Finding, JsonReporter, Mongoosleuth, type MongoosleuthOptions, type Reporter };
|