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/dist/index.mjs ADDED
@@ -0,0 +1,402 @@
1
+ // src/reporters/format-finding.ts
2
+ function renderVal(val) {
3
+ if (val === null) {
4
+ return "<null>";
5
+ }
6
+ if (val === void 0) {
7
+ return "<undefined>";
8
+ }
9
+ if (typeof val === "string") {
10
+ if (val.startsWith("<") && val.endsWith(">")) {
11
+ return val;
12
+ }
13
+ return `"${val}"`;
14
+ }
15
+ if (Array.isArray(val)) {
16
+ if (val.length === 0) {
17
+ return "[]";
18
+ }
19
+ return `[${renderVal(val[0])}]`;
20
+ }
21
+ if (typeof val === "object") {
22
+ const keys = Object.keys(val);
23
+ if (keys.length === 0) {
24
+ return "{}";
25
+ }
26
+ const entries = keys.map((key) => `${key}: ${renderVal(val[key])}`);
27
+ return `{ ${entries.join(", ")} }`;
28
+ }
29
+ return String(val);
30
+ }
31
+ function extractQueryShape(finding) {
32
+ const prefix = `${finding.model}:${finding.operation}:`;
33
+ if (!finding.fingerprint || !finding.fingerprint.startsWith(prefix)) {
34
+ return {};
35
+ }
36
+ const jsonShapeStr = finding.fingerprint.slice(prefix.length);
37
+ try {
38
+ return JSON.parse(jsonShapeStr);
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+ function formatQueryLine(finding) {
44
+ const shape = extractQueryShape(finding);
45
+ const keys = Object.keys(shape);
46
+ if (keys.length === 0) {
47
+ return `${finding.operation}({})`;
48
+ }
49
+ return `${finding.operation}(${renderVal(shape)})`;
50
+ }
51
+ function suggestFix(finding) {
52
+ const op = finding.operation;
53
+ if (op === "find" || op === "findOne" || op === "findById") {
54
+ return "use .populate() or batch this with a single query using $in";
55
+ }
56
+ return "consider batching this operation instead of calling it in a loop (e.g. bulkWrite)";
57
+ }
58
+
59
+ // src/reporters/console.ts
60
+ var ConsoleReporter = class {
61
+ /**
62
+ * Reports the identified N+1 query findings.
63
+ * @param findings The list of findings to report.
64
+ */
65
+ report(findings) {
66
+ if (findings.length === 0) {
67
+ return;
68
+ }
69
+ for (const finding of findings) {
70
+ console.warn(`[mongoosleuth] N+1 detected
71
+ model: ${finding.model}
72
+ query: ${formatQueryLine(finding)}
73
+ called ${finding.count} times in ${finding.callSite}
74
+ fix: ${suggestFix(finding)}`);
75
+ }
76
+ }
77
+ };
78
+
79
+ // src/scope.ts
80
+ import { AsyncLocalStorage } from "async_hooks";
81
+ var storage = new AsyncLocalStorage();
82
+ function runInScope(fn) {
83
+ const scopeMap = /* @__PURE__ */ new Map();
84
+ return storage.run(scopeMap, fn);
85
+ }
86
+ function recordQuery(entry) {
87
+ const store = storage.getStore();
88
+ if (!store) {
89
+ return;
90
+ }
91
+ const key = `${entry.fingerprint}::${entry.callSite}`;
92
+ const existing = store.get(key);
93
+ if (existing) {
94
+ existing.count++;
95
+ } else {
96
+ store.set(key, {
97
+ model: entry.model,
98
+ operation: entry.operation,
99
+ fingerprint: entry.fingerprint,
100
+ callSite: entry.callSite,
101
+ count: 1
102
+ });
103
+ }
104
+ }
105
+ function getRawStore() {
106
+ return storage.getStore();
107
+ }
108
+
109
+ // src/fingerprint.ts
110
+ function isObjectId(val) {
111
+ if (!val || typeof val !== "object") return false;
112
+ const anyVal = val;
113
+ if (anyVal._bsontype === "ObjectID") return true;
114
+ if (anyVal.constructor && (anyVal.constructor.name === "ObjectId" || anyVal.constructor.name === "ObjectID")) {
115
+ return true;
116
+ }
117
+ if (typeof anyVal.toHexString === "function") return true;
118
+ return false;
119
+ }
120
+ function normalizeValue(val) {
121
+ if (val === null) return "<null>";
122
+ if (val === void 0) return "<undefined>";
123
+ if (isObjectId(val)) {
124
+ return "<ObjectId>";
125
+ }
126
+ if (val instanceof Date) {
127
+ return "<Date>";
128
+ }
129
+ if (val instanceof RegExp) {
130
+ return "<RegExp>";
131
+ }
132
+ if (typeof val === "string") {
133
+ return "<string>";
134
+ }
135
+ if (typeof val === "number") {
136
+ return "<number>";
137
+ }
138
+ if (typeof val === "boolean") {
139
+ return "<boolean>";
140
+ }
141
+ if (Array.isArray(val)) {
142
+ if (val.length === 0) {
143
+ return "<empty array>";
144
+ }
145
+ return [normalizeValue(val[0])];
146
+ }
147
+ if (typeof val === "object") {
148
+ const result = {};
149
+ for (const key of Object.keys(val)) {
150
+ result[key] = normalizeValue(val[key]);
151
+ }
152
+ return result;
153
+ }
154
+ return `<${typeof val}>`;
155
+ }
156
+ function sortObjectKeys(val) {
157
+ if (val === null || val === void 0 || typeof val !== "object") {
158
+ return val;
159
+ }
160
+ if (Array.isArray(val)) {
161
+ return val.map(sortObjectKeys);
162
+ }
163
+ const sortedObj = {};
164
+ const keys = Object.keys(val).sort();
165
+ for (const key of keys) {
166
+ sortedObj[key] = sortObjectKeys(val[key]);
167
+ }
168
+ return sortedObj;
169
+ }
170
+ function normalizeFilterShape(filter) {
171
+ if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
172
+ return {};
173
+ }
174
+ return normalizeValue(filter);
175
+ }
176
+ function buildFingerprint(modelName, operation, filter) {
177
+ const normalized = normalizeFilterShape(filter);
178
+ const sorted = sortObjectKeys(normalized);
179
+ return `${modelName}:${operation}:${JSON.stringify(sorted)}`;
180
+ }
181
+
182
+ // src/interceptor.ts
183
+ var originalExec = null;
184
+ function buildCallSite(stack) {
185
+ const lines = stack.split("\n");
186
+ const isInternal = /node_modules[\\/]mongoose[\\/]|node_modules[\\/]mongoosleuth[\\/]|[\\/]mongoosleuth[\\/]src[\\/]|[\\/]mongoosleuth[\\/]dist[\\/]|[\\/]src[\\/](?:interceptor|scope|mongoosleuth|fingerprint|analyzer)\.ts/i;
187
+ for (const line of lines) {
188
+ if (!line.includes("at ")) {
189
+ continue;
190
+ }
191
+ if (isInternal.test(line)) {
192
+ continue;
193
+ }
194
+ const parenMatch = line.match(/\(([^)]+)\)/);
195
+ const pathStr = parenMatch ? parenMatch[1] : line.trim().slice(3);
196
+ let normalizedPath = pathStr.replace(/\\/g, "/");
197
+ const cwd = process.cwd().replace(/\\/g, "/");
198
+ if (normalizedPath.toLowerCase().startsWith(cwd.toLowerCase() + "/")) {
199
+ normalizedPath = normalizedPath.slice(cwd.length + 1);
200
+ }
201
+ const cleanMatch = normalizedPath.match(/^(.*?):(\d+)(:\d+)?$/);
202
+ if (cleanMatch) {
203
+ return `${cleanMatch[1]}:${cleanMatch[2]}`;
204
+ }
205
+ }
206
+ return "unknown";
207
+ }
208
+ function attachInterceptor(mongooseInstance, options) {
209
+ if (!mongooseInstance || !mongooseInstance.Query || !mongooseInstance.Query.prototype) {
210
+ return;
211
+ }
212
+ if (mongooseInstance.Query.prototype.exec && mongooseInstance.Query.prototype.exec.__mongoosleuthPatched) {
213
+ console.warn("[mongoosleuth] already attached, skipping");
214
+ return;
215
+ }
216
+ originalExec = mongooseInstance.Query.prototype.exec;
217
+ const enabled = options.enabled !== false;
218
+ const captureStackTrace = options.captureStackTrace !== false;
219
+ const ignore = options.ignore || [];
220
+ const patchedExec = function(...args) {
221
+ if (!enabled) {
222
+ return originalExec.apply(this, args);
223
+ }
224
+ const modelName = this.model?.modelName;
225
+ const operation = this.op;
226
+ if (!modelName || !operation) {
227
+ return originalExec.apply(this, args);
228
+ }
229
+ const isIgnored = ignore.some((entry) => {
230
+ const matchModel = !entry.model || entry.model === modelName;
231
+ const matchOp = !entry.operation || entry.operation === operation;
232
+ return matchModel && matchOp;
233
+ });
234
+ if (isIgnored) {
235
+ return originalExec.apply(this, args);
236
+ }
237
+ const filter = this.getQuery() || {};
238
+ const fingerprint = buildFingerprint(modelName, operation, filter);
239
+ let callSite = "unknown";
240
+ if (captureStackTrace) {
241
+ const stack = new Error().stack;
242
+ if (stack) {
243
+ callSite = buildCallSite(stack);
244
+ }
245
+ }
246
+ recordQuery({
247
+ model: modelName,
248
+ operation,
249
+ fingerprint,
250
+ callSite
251
+ });
252
+ return originalExec.apply(this, args);
253
+ };
254
+ patchedExec.__mongoosleuthPatched = true;
255
+ mongooseInstance.Query.prototype.exec = patchedExec;
256
+ }
257
+
258
+ // src/analyzer.ts
259
+ function analyzeRecords(records, options) {
260
+ if (!records) {
261
+ return [];
262
+ }
263
+ const findings = records.filter((r) => r.count >= options.threshold).map((r) => ({
264
+ model: r.model,
265
+ operation: r.operation,
266
+ fingerprint: r.fingerprint,
267
+ count: r.count,
268
+ callSite: r.callSite
269
+ }));
270
+ findings.sort((a, b) => b.count - a.count);
271
+ return findings;
272
+ }
273
+
274
+ // src/mongoosleuth.ts
275
+ var Mongoosleuth = class {
276
+ options;
277
+ /**
278
+ * Initializes a new instance of Mongoosleuth.
279
+ * @param options Configuration options.
280
+ */
281
+ constructor(options) {
282
+ const enabled = options?.enabled !== void 0 ? options.enabled : process.env.NODE_ENV !== "production";
283
+ const threshold = options?.threshold !== void 0 ? options.threshold : 3;
284
+ const captureStackTrace = options?.captureStackTrace !== void 0 ? options.captureStackTrace : true;
285
+ const ignore = options?.ignore || [];
286
+ const reporters = options?.reporters || [new ConsoleReporter()];
287
+ if (options?.threshold !== void 0 && options.threshold < 1) {
288
+ throw new Error(
289
+ `[mongoosleuth] Invalid threshold value: ${options.threshold}. Threshold must be >= 1.`
290
+ );
291
+ }
292
+ this.options = {
293
+ enabled,
294
+ threshold,
295
+ captureStackTrace,
296
+ ignore,
297
+ reporters
298
+ };
299
+ }
300
+ /**
301
+ * Patches the given Mongoose instance to intercept all query executions.
302
+ *
303
+ * @param mongooseInstance Mongoose library instance to attach to.
304
+ */
305
+ attach(mongooseInstance) {
306
+ attachInterceptor(mongooseInstance, this.options);
307
+ }
308
+ /**
309
+ * Internal helper to run query pattern analysis and trigger reporters.
310
+ */
311
+ analyzeAndReport(records) {
312
+ const findings = analyzeRecords(records, {
313
+ threshold: this.options.threshold
314
+ });
315
+ if (findings.length > 0) {
316
+ for (const reporter of this.options.reporters) {
317
+ reporter.report(findings);
318
+ }
319
+ }
320
+ }
321
+ /**
322
+ * Express/Koa/Fastify middleware that opens a RequestScope for each incoming request
323
+ * and runs pattern analysis upon request completion.
324
+ *
325
+ * @returns Request middleware function.
326
+ */
327
+ middleware() {
328
+ return (req, res, next) => {
329
+ if (!this.options.enabled) {
330
+ next();
331
+ return;
332
+ }
333
+ runInScope(async () => {
334
+ const store = getRawStore();
335
+ res.on("finish", () => {
336
+ const records = store ? Array.from(store.values()) : [];
337
+ this.analyzeAndReport(records);
338
+ });
339
+ next();
340
+ });
341
+ };
342
+ }
343
+ /**
344
+ * Manually runs a function within a new query tracking scope.
345
+ * Useful for background jobs, scripts, or manual request cycles outside standard middleware.
346
+ *
347
+ * @param fn The asynchronous function to execute within the scope.
348
+ */
349
+ async run(fn) {
350
+ if (!this.options.enabled) {
351
+ return fn();
352
+ }
353
+ let records;
354
+ try {
355
+ return await runInScope(async () => {
356
+ const store = getRawStore();
357
+ try {
358
+ return await fn();
359
+ } finally {
360
+ records = store ? Array.from(store.values()) : [];
361
+ }
362
+ });
363
+ } finally {
364
+ this.analyzeAndReport(records);
365
+ }
366
+ }
367
+ };
368
+
369
+ // src/reporters/json.ts
370
+ var JsonReporter = class {
371
+ write;
372
+ /**
373
+ * Initializes a new instance of JsonReporter.
374
+ * @param write Optional callback function to write the JSON line. Defaults to console.log.
375
+ */
376
+ constructor(write) {
377
+ this.write = write || console.log;
378
+ }
379
+ /**
380
+ * Serializes the findings to JSON lines and writes them using the write handler.
381
+ * @param findings The list of findings to serialize and report.
382
+ */
383
+ report(findings) {
384
+ if (findings.length === 0) {
385
+ return;
386
+ }
387
+ for (const finding of findings) {
388
+ const output = {
389
+ type: "mongoosleuth_finding",
390
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
391
+ ...finding
392
+ };
393
+ this.write(JSON.stringify(output));
394
+ }
395
+ }
396
+ };
397
+ export {
398
+ ConsoleReporter,
399
+ JsonReporter,
400
+ Mongoosleuth
401
+ };
402
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reporters/format-finding.ts","../src/reporters/console.ts","../src/scope.ts","../src/fingerprint.ts","../src/interceptor.ts","../src/analyzer.ts","../src/mongoosleuth.ts","../src/reporters/json.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Finding } from '../types';\n\n/**\n * Helper to recursively format a parsed shape value into a JS-like object literal notation.\n * Strips quotes from keys and type tag values (e.g. <ObjectId>).\n */\nfunction renderVal(val: any): string {\n if (val === null) {\n return '<null>';\n }\n if (val === undefined) {\n return '<undefined>';\n }\n\n if (typeof val === 'string') {\n if (val.startsWith('<') && val.endsWith('>')) {\n return val;\n }\n return `\"${val}\"`;\n }\n\n if (Array.isArray(val)) {\n if (val.length === 0) {\n return '[]';\n }\n return `[${renderVal(val[0])}]`;\n }\n\n if (typeof val === 'object') {\n const keys = Object.keys(val);\n if (keys.length === 0) {\n return '{}';\n }\n const entries = keys.map((key) => `${key}: ${renderVal(val[key])}`);\n return `{ ${entries.join(', ')} }`;\n }\n\n return String(val);\n}\n\n/**\n * Extracts and parses the JSON shape from a fingerprint.\n *\n * @param finding The N+1 query finding.\n * @returns Parsed shape object or {} if parsing fails.\n */\nexport function extractQueryShape(finding: Finding): Record<string, any> {\n const prefix = `${finding.model}:${finding.operation}:`;\n if (!finding.fingerprint || !finding.fingerprint.startsWith(prefix)) {\n return {};\n }\n const jsonShapeStr = finding.fingerprint.slice(prefix.length);\n try {\n return JSON.parse(jsonShapeStr);\n } catch {\n return {};\n }\n}\n\n/**\n * Formats the query line in the format: operation({ key1: value1, key2: value2 }).\n *\n * @param finding The N+1 query finding.\n * @returns Formatted query execution line.\n */\nexport function formatQueryLine(finding: Finding): string {\n const shape = extractQueryShape(finding);\n const keys = Object.keys(shape);\n if (keys.length === 0) {\n return `${finding.operation}({})`;\n }\n return `${finding.operation}(${renderVal(shape)})`;\n}\n\n/**\n * Suggests an N+1 query fix based on the operation.\n *\n * @param finding The N+1 query finding.\n * @returns Suggestion text.\n */\nexport function suggestFix(finding: Finding): string {\n const op = finding.operation;\n if (op === 'find' || op === 'findOne' || op === 'findById') {\n return 'use .populate() or batch this with a single query using $in';\n }\n return 'consider batching this operation instead of calling it in a loop (e.g. bulkWrite)';\n}\n","import { Finding, Reporter } from '../types';\nimport { formatQueryLine, suggestFix } from './format-finding';\n\n/**\n * Default reporter that prints query N+1 findings to console.warn.\n * See AGENTS.md: Reporter.\n */\nexport class ConsoleReporter implements Reporter {\n /**\n * Reports the identified N+1 query findings.\n * @param findings The list of findings to report.\n */\n public report(findings: Finding[]): void {\n if (findings.length === 0) {\n return;\n }\n\n for (const finding of findings) {\n console.warn(`[mongoosleuth] N+1 detected\n model: ${finding.model}\n query: ${formatQueryLine(finding)}\n called ${finding.count} times in ${finding.callSite}\n fix: ${suggestFix(finding)}`);\n }\n }\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\n\n/**\n * Represents a recorded query within a tracking scope.\n */\nexport interface QueryRecord {\n model: string;\n operation: string;\n fingerprint: string;\n callSite: string;\n count: number;\n}\n\n// Thread-safe request/execution context isolation for queries\nconst storage = new AsyncLocalStorage<Map<string, QueryRecord>>();\n\n/**\n * Opens a new query tracking scope for the duration of the asynchronous function `fn`.\n *\n * @param fn The asynchronous function to execute within the scope.\n * @returns The resolved/rejected promise value of `fn`.\n */\nexport function runInScope<T>(fn: () => Promise<T>): Promise<T> {\n const scopeMap = new Map<string, QueryRecord>();\n return storage.run(scopeMap, fn);\n}\n\n/**\n * Records an intercepted query into the active scope, if one is active.\n * If called outside of an active scope, it silently does nothing.\n *\n * @param entry The query details to record.\n */\nexport function recordQuery(entry: {\n model: string;\n operation: string;\n fingerprint: string;\n callSite: string;\n}): void {\n const store = storage.getStore();\n if (!store) {\n return;\n }\n\n const key = `${entry.fingerprint}::${entry.callSite}`;\n const existing = store.get(key);\n\n if (existing) {\n existing.count++;\n } else {\n store.set(key, {\n model: entry.model,\n operation: entry.operation,\n fingerprint: entry.fingerprint,\n callSite: entry.callSite,\n count: 1,\n });\n }\n}\n\n/**\n * Returns all recorded queries inside the currently active scope as an array.\n * Returns `undefined` if called outside any active scope.\n *\n * @returns Array of QueryRecord objects or undefined.\n */\nexport function getActiveRecords(): QueryRecord[] | undefined {\n const store = storage.getStore();\n if (!store) {\n return undefined;\n }\n return Array.from(store.values());\n}\n\n/**\n * Returns the raw Map store of the active scope.\n * Internal utility to capture the store reference across asynchronous boundaries.\n *\n * @returns The active Map store or undefined.\n */\nexport function getRawStore(): Map<string, QueryRecord> | undefined {\n return storage.getStore();\n}\n","/**\n * Checks if a value behaves like a MongoDB/Mongoose ObjectId.\n * Since this module is pure, it does not import from mongoose.\n */\nfunction isObjectId(val: unknown): boolean {\n if (!val || typeof val !== 'object') return false;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const anyVal = val as any;\n if (anyVal._bsontype === 'ObjectID') return true;\n if (\n anyVal.constructor &&\n (anyVal.constructor.name === 'ObjectId' || anyVal.constructor.name === 'ObjectID')\n ) {\n return true;\n }\n if (typeof anyVal.toHexString === 'function') return true;\n return false;\n}\n\n/**\n * Recursively normalizes a query filter value into its shape representation.\n */\nfunction normalizeValue(val: unknown): unknown {\n if (val === null) return '<null>';\n if (val === undefined) return '<undefined>';\n\n if (isObjectId(val)) {\n return '<ObjectId>';\n }\n if (val instanceof Date) {\n return '<Date>';\n }\n if (val instanceof RegExp) {\n return '<RegExp>';\n }\n if (typeof val === 'string') {\n return '<string>';\n }\n if (typeof val === 'number') {\n return '<number>';\n }\n if (typeof val === 'boolean') {\n return '<boolean>';\n }\n if (Array.isArray(val)) {\n if (val.length === 0) {\n return '<empty array>';\n }\n // Normalize using only the shape of the first element\n return [normalizeValue(val[0])];\n }\n if (typeof val === 'object') {\n // If it's a plain object (or behaves like one), recurse\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(val as Record<string, unknown>)) {\n result[key] = normalizeValue((val as Record<string, unknown>)[key]);\n }\n return result;\n }\n return `<${typeof val}>`;\n}\n\n/**\n * Recursively sorts keys of an object alphabetically.\n */\nfunction sortObjectKeys(val: unknown): unknown {\n if (val === null || val === undefined || typeof val !== 'object') {\n return val;\n }\n if (Array.isArray(val)) {\n return val.map(sortObjectKeys);\n }\n const sortedObj: Record<string, unknown> = {};\n const keys = Object.keys(val as Record<string, unknown>).sort();\n for (const key of keys) {\n sortedObj[key] = sortObjectKeys((val as Record<string, unknown>)[key]);\n }\n return sortedObj;\n}\n\n/**\n * Normalizes a query filter object, replacing primitive leaf values with type tags (e.g., '<string>', '<ObjectId>').\n * Keeps object keys and nesting structure intact. For arrays, uses the shape of the first element.\n *\n * See AGENTS.md: fingerprinting rules.\n *\n * @param filter The raw Mongoose query filter object.\n * @returns The normalized filter shape.\n */\nexport function normalizeFilterShape(filter: Record<string, unknown>): Record<string, unknown> {\n if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {\n return {};\n }\n return normalizeValue(filter) as Record<string, unknown>;\n}\n\n/**\n * Builds a query fingerprint string in the format \"${modelName}:${operation}:${normalizedFilterShape}\".\n *\n * @param modelName Name of the Mongoose model.\n * @param operation Name of the query operation (e.g., find, findOne).\n * @param filter The query filter object.\n * @returns A unique fingerprint string representing the query template.\n */\nexport function buildFingerprint(\n modelName: string,\n operation: string,\n filter: Record<string, unknown>\n): string {\n const normalized = normalizeFilterShape(filter);\n const sorted = sortObjectKeys(normalized);\n return `${modelName}:${operation}:${JSON.stringify(sorted)}`;\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { MongoosleuthOptions } from './types';\nimport { buildFingerprint } from './fingerprint';\nimport { recordQuery } from './scope';\n\n// Global reference to the original Mongoose Query.prototype.exec function\nlet originalExec: any = null;\n\n/**\n * Extracts and formats the relative callsite from a stack trace string.\n * Filters out internal library frames from mongoose and mongoosleuth.\n *\n * @param stack Raw stack trace string from Error().stack.\n * @returns Formatted \"file:line\" string or \"unknown\".\n */\nexport function buildCallSite(stack: string): string {\n const lines = stack.split('\\n');\n\n // Regular expression to identify internal library frames that should be skipped\n const isInternal =\n /node_modules[\\\\/]mongoose[\\\\/]|node_modules[\\\\/]mongoosleuth[\\\\/]|[\\\\/]mongoosleuth[\\\\/]src[\\\\/]|[\\\\/]mongoosleuth[\\\\/]dist[\\\\/]|[\\\\/]src[\\\\/](?:interceptor|scope|mongoosleuth|fingerprint|analyzer)\\.ts/i;\n\n for (const line of lines) {\n if (!line.includes('at ')) {\n continue;\n }\n\n if (isInternal.test(line)) {\n continue;\n }\n\n // Extract path between parentheses or directly after 'at '\n const parenMatch = line.match(/\\(([^)]+)\\)/);\n const pathStr = parenMatch ? parenMatch[1] : line.trim().slice(3); // strip \"at \"\n\n // Normalize backslashes to forward slashes for cross-platform consistency\n let normalizedPath = pathStr.replace(/\\\\/g, '/');\n const cwd = process.cwd().replace(/\\\\/g, '/');\n\n // Make path relative to the current working directory\n if (normalizedPath.toLowerCase().startsWith(cwd.toLowerCase() + '/')) {\n normalizedPath = normalizedPath.slice(cwd.length + 1);\n }\n\n // Capture file path and line number, discarding column info\n const cleanMatch = normalizedPath.match(/^(.*?):(\\d+)(:\\d+)?$/);\n if (cleanMatch) {\n return `${cleanMatch[1]}:${cleanMatch[2]}`;\n }\n }\n\n return 'unknown';\n}\n\n/**\n * Patches mongoose.Query.prototype.exec once to intercept executed queries.\n * Fingerprints and records each query into the active RequestScope.\n *\n * See AGENTS.md: QueryInterceptor.\n *\n * @param mongooseInstance The Mongoose instance to patch.\n * @param options Mongoosleuth options (e.g. threshold, captureStackTrace).\n */\nexport function attachInterceptor(mongooseInstance: any, options: MongoosleuthOptions): void {\n if (!mongooseInstance || !mongooseInstance.Query || !mongooseInstance.Query.prototype) {\n return;\n }\n\n // Ensure idempotency\n if (\n mongooseInstance.Query.prototype.exec &&\n mongooseInstance.Query.prototype.exec.__mongoosleuthPatched\n ) {\n console.warn('[mongoosleuth] already attached, skipping');\n return;\n }\n\n // Store reference to the original function for restoration\n originalExec = mongooseInstance.Query.prototype.exec;\n\n // Capture options by value (closure)\n const enabled = options.enabled !== false;\n const captureStackTrace = options.captureStackTrace !== false;\n const ignore = options.ignore || [];\n\n const patchedExec = function (this: any, ...args: any[]) {\n if (!enabled) {\n return originalExec.apply(this, args);\n }\n\n const modelName = this.model?.modelName;\n const operation = this.op;\n\n if (!modelName || !operation) {\n return originalExec.apply(this, args);\n }\n\n // Check if the current model and operation should be ignored\n const isIgnored = ignore.some((entry) => {\n const matchModel = !entry.model || entry.model === modelName;\n const matchOp = !entry.operation || entry.operation === operation;\n return matchModel && matchOp;\n });\n\n if (isIgnored) {\n return originalExec.apply(this, args);\n }\n\n const filter = this.getQuery() || {};\n const fingerprint = buildFingerprint(modelName, operation, filter);\n\n let callSite = 'unknown';\n if (captureStackTrace) {\n const stack = new Error().stack;\n if (stack) {\n callSite = buildCallSite(stack);\n }\n }\n\n recordQuery({\n model: modelName,\n operation,\n fingerprint,\n callSite,\n });\n\n return originalExec.apply(this, args);\n };\n\n // Add the marker flag to ensure idempotency\n (patchedExec as any).__mongoosleuthPatched = true;\n\n // Overwrite prototype exec\n mongooseInstance.Query.prototype.exec = patchedExec;\n}\n\n/**\n * Resets the patched Mongoose Query.prototype.exec back to its original state.\n * Internal-only utility for test cleanups.\n *\n * @param mongooseInstance Mongoose instance to reset.\n */\nexport function __resetForTests(mongooseInstance: any): void {\n if (\n mongooseInstance &&\n mongooseInstance.Query &&\n mongooseInstance.Query.prototype &&\n originalExec\n ) {\n mongooseInstance.Query.prototype.exec = originalExec;\n originalExec = null;\n }\n}\n","import type { QueryRecord } from './scope';\nimport type { Finding } from './types';\n\n/**\n * Analyzes recorded queries and maps them to public N+1 query findings.\n * Filters out query records whose count is below the specified threshold.\n *\n * This function is pure and side-effect free.\n * See AGENTS.md: PatternAnalyzer.\n *\n * @param records List of recorded queries from the active scope, or undefined if outside scope.\n * @param options Analyzer options (containing threshold).\n * @returns Array of N+1 query findings.\n */\nexport function analyzeRecords(\n records: QueryRecord[] | undefined,\n options: { threshold: number }\n): Finding[] {\n if (!records) {\n return [];\n }\n\n // Filter records with count >= threshold and map to Finding structure\n const findings: Finding[] = records\n .filter((r) => r.count >= options.threshold)\n .map((r) => ({\n model: r.model,\n operation: r.operation,\n fingerprint: r.fingerprint,\n count: r.count,\n callSite: r.callSite,\n }));\n\n // Sort descending by count only (stable sort by default)\n findings.sort((a, b) => b.count - a.count);\n\n return findings;\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { MongoosleuthOptions } from './types';\nimport { ConsoleReporter } from './reporters/console';\nimport { runInScope, getRawStore } from './scope';\nimport { attachInterceptor } from './interceptor';\nimport { analyzeRecords } from './analyzer';\n\n/**\n * The main Mongoosleuth library class.\n * Coordinates intercepting queries, request-scoped tracking, and analyzing/reporting.\n *\n * See AGENTS.md: Public API surface.\n */\nexport class Mongoosleuth {\n public readonly options: Required<MongoosleuthOptions>;\n\n /**\n * Initializes a new instance of Mongoosleuth.\n * @param options Configuration options.\n */\n constructor(options?: MongoosleuthOptions) {\n const enabled =\n options?.enabled !== undefined ? options.enabled : process.env.NODE_ENV !== 'production';\n const threshold = options?.threshold !== undefined ? options.threshold : 3;\n const captureStackTrace =\n options?.captureStackTrace !== undefined ? options.captureStackTrace : true;\n const ignore = options?.ignore || [];\n const reporters = options?.reporters || [new ConsoleReporter()];\n\n if (options?.threshold !== undefined && options.threshold < 1) {\n throw new Error(\n `[mongoosleuth] Invalid threshold value: ${options.threshold}. Threshold must be >= 1.`\n );\n }\n\n this.options = {\n enabled,\n threshold,\n captureStackTrace,\n ignore,\n reporters,\n };\n }\n\n /**\n * Patches the given Mongoose instance to intercept all query executions.\n *\n * @param mongooseInstance Mongoose library instance to attach to.\n */\n public attach(mongooseInstance: any): void {\n attachInterceptor(mongooseInstance, this.options);\n }\n\n /**\n * Internal helper to run query pattern analysis and trigger reporters.\n */\n private analyzeAndReport(records: any[] | undefined): void {\n const findings = analyzeRecords(records, {\n threshold: this.options.threshold,\n });\n if (findings.length > 0) {\n for (const reporter of this.options.reporters) {\n reporter.report(findings);\n }\n }\n }\n\n /**\n * Express/Koa/Fastify middleware that opens a RequestScope for each incoming request\n * and runs pattern analysis upon request completion.\n *\n * @returns Request middleware function.\n */\n public middleware(): (req: any, res: any, next: (err?: any) => void) => void {\n return (req: any, res: any, next: (err?: any) => void) => {\n if (!this.options.enabled) {\n next();\n return;\n }\n\n runInScope(async () => {\n const store = getRawStore();\n res.on('finish', () => {\n const records = store ? Array.from(store.values()) : [];\n this.analyzeAndReport(records);\n });\n next();\n });\n };\n }\n\n /**\n * Manually runs a function within a new query tracking scope.\n * Useful for background jobs, scripts, or manual request cycles outside standard middleware.\n *\n * @param fn The asynchronous function to execute within the scope.\n */\n public async run<T>(fn: () => Promise<T>): Promise<T> {\n if (!this.options.enabled) {\n return fn();\n }\n\n let records: any[] | undefined;\n try {\n return await runInScope(async () => {\n const store = getRawStore();\n try {\n return await fn();\n } finally {\n records = store ? Array.from(store.values()) : [];\n }\n });\n } finally {\n this.analyzeAndReport(records);\n }\n }\n}\n","import { Finding, Reporter } from '../types';\n\n/**\n * Pluggable reporter that outputs query N+1 findings as a single-line JSON string (NDJSON format).\n * See AGENTS.md: Reporter.\n */\nexport class JsonReporter implements Reporter {\n private write: (line: string) => void;\n\n /**\n * Initializes a new instance of JsonReporter.\n * @param write Optional callback function to write the JSON line. Defaults to console.log.\n */\n constructor(write?: (line: string) => void) {\n this.write = write || console.log;\n }\n\n /**\n * Serializes the findings to JSON lines and writes them using the write handler.\n * @param findings The list of findings to serialize and report.\n */\n public report(findings: Finding[]): void {\n if (findings.length === 0) {\n return;\n }\n\n for (const finding of findings) {\n const output = {\n type: 'mongoosleuth_finding',\n timestamp: new Date().toISOString(),\n ...finding,\n };\n this.write(JSON.stringify(output));\n }\n }\n}\n"],"mappings":";AAOA,SAAS,UAAU,KAAkB;AACnC,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,QAAQ,UAAU;AAC3B,QAAI,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,GAAG;AAC5C,aAAO;AAAA,IACT;AACA,WAAO,IAAI,GAAG;AAAA,EAChB;AAEA,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,QAAI,IAAI,WAAW,GAAG;AACpB,aAAO;AAAA,IACT;AACA,WAAO,IAAI,UAAU,IAAI,CAAC,CAAC,CAAC;AAAA,EAC9B;AAEA,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM,OAAO,OAAO,KAAK,GAAG;AAC5B,QAAI,KAAK,WAAW,GAAG;AACrB,aAAO;AAAA,IACT;AACA,UAAM,UAAU,KAAK,IAAI,CAAC,QAAQ,GAAG,GAAG,KAAK,UAAU,IAAI,GAAG,CAAC,CAAC,EAAE;AAClE,WAAO,KAAK,QAAQ,KAAK,IAAI,CAAC;AAAA,EAChC;AAEA,SAAO,OAAO,GAAG;AACnB;AAQO,SAAS,kBAAkB,SAAuC;AACvE,QAAM,SAAS,GAAG,QAAQ,KAAK,IAAI,QAAQ,SAAS;AACpD,MAAI,CAAC,QAAQ,eAAe,CAAC,QAAQ,YAAY,WAAW,MAAM,GAAG;AACnE,WAAO,CAAC;AAAA,EACV;AACA,QAAM,eAAe,QAAQ,YAAY,MAAM,OAAO,MAAM;AAC5D,MAAI;AACF,WAAO,KAAK,MAAM,YAAY;AAAA,EAChC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAQO,SAAS,gBAAgB,SAA0B;AACxD,QAAM,QAAQ,kBAAkB,OAAO;AACvC,QAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,MAAI,KAAK,WAAW,GAAG;AACrB,WAAO,GAAG,QAAQ,SAAS;AAAA,EAC7B;AACA,SAAO,GAAG,QAAQ,SAAS,IAAI,UAAU,KAAK,CAAC;AACjD;AAQO,SAAS,WAAW,SAA0B;AACnD,QAAM,KAAK,QAAQ;AACnB,MAAI,OAAO,UAAU,OAAO,aAAa,OAAO,YAAY;AAC1D,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;AChFO,IAAM,kBAAN,MAA0C;AAAA;AAAA;AAAA;AAAA;AAAA,EAKxC,OAAO,UAA2B;AACvC,QAAI,SAAS,WAAW,GAAG;AACzB;AAAA,IACF;AAEA,eAAW,WAAW,UAAU;AAC9B,cAAQ,KAAK;AAAA,WACR,QAAQ,KAAK;AAAA,WACb,gBAAgB,OAAO,CAAC;AAAA,WACxB,QAAQ,KAAK,aAAa,QAAQ,QAAQ;AAAA,SAC5C,WAAW,OAAO,CAAC,EAAE;AAAA,IAC1B;AAAA,EACF;AACF;;;ACzBA,SAAS,yBAAyB;AAclC,IAAM,UAAU,IAAI,kBAA4C;AAQzD,SAAS,WAAc,IAAkC;AAC9D,QAAM,WAAW,oBAAI,IAAyB;AAC9C,SAAO,QAAQ,IAAI,UAAU,EAAE;AACjC;AAQO,SAAS,YAAY,OAKnB;AACP,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI,CAAC,OAAO;AACV;AAAA,EACF;AAEA,QAAM,MAAM,GAAG,MAAM,WAAW,KAAK,MAAM,QAAQ;AACnD,QAAM,WAAW,MAAM,IAAI,GAAG;AAE9B,MAAI,UAAU;AACZ,aAAS;AAAA,EACX,OAAO;AACL,UAAM,IAAI,KAAK;AAAA,MACb,OAAO,MAAM;AAAA,MACb,WAAW,MAAM;AAAA,MACjB,aAAa,MAAM;AAAA,MACnB,UAAU,MAAM;AAAA,MAChB,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAsBO,SAAS,cAAoD;AAClE,SAAO,QAAQ,SAAS;AAC1B;;;AC9EA,SAAS,WAAW,KAAuB;AACzC,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAE5C,QAAM,SAAS;AACf,MAAI,OAAO,cAAc,WAAY,QAAO;AAC5C,MACE,OAAO,gBACN,OAAO,YAAY,SAAS,cAAc,OAAO,YAAY,SAAS,aACvE;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,OAAO,gBAAgB,WAAY,QAAO;AACrD,SAAO;AACT;AAKA,SAAS,eAAe,KAAuB;AAC7C,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,QAAQ,OAAW,QAAO;AAE9B,MAAI,WAAW,GAAG,GAAG;AACnB,WAAO;AAAA,EACT;AACA,MAAI,eAAe,MAAM;AACvB,WAAO;AAAA,EACT;AACA,MAAI,eAAe,QAAQ;AACzB,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,WAAW;AAC5B,WAAO;AAAA,EACT;AACA,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,QAAI,IAAI,WAAW,GAAG;AACpB,aAAO;AAAA,IACT;AAEA,WAAO,CAAC,eAAe,IAAI,CAAC,CAAC,CAAC;AAAA,EAChC;AACA,MAAI,OAAO,QAAQ,UAAU;AAE3B,UAAM,SAAkC,CAAC;AACzC,eAAW,OAAO,OAAO,KAAK,GAA8B,GAAG;AAC7D,aAAO,GAAG,IAAI,eAAgB,IAAgC,GAAG,CAAC;AAAA,IACpE;AACA,WAAO;AAAA,EACT;AACA,SAAO,IAAI,OAAO,GAAG;AACvB;AAKA,SAAS,eAAe,KAAuB;AAC7C,MAAI,QAAQ,QAAQ,QAAQ,UAAa,OAAO,QAAQ,UAAU;AAChE,WAAO;AAAA,EACT;AACA,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,WAAO,IAAI,IAAI,cAAc;AAAA,EAC/B;AACA,QAAM,YAAqC,CAAC;AAC5C,QAAM,OAAO,OAAO,KAAK,GAA8B,EAAE,KAAK;AAC9D,aAAW,OAAO,MAAM;AACtB,cAAU,GAAG,IAAI,eAAgB,IAAgC,GAAG,CAAC;AAAA,EACvE;AACA,SAAO;AACT;AAWO,SAAS,qBAAqB,QAA0D;AAC7F,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAClE,WAAO,CAAC;AAAA,EACV;AACA,SAAO,eAAe,MAAM;AAC9B;AAUO,SAAS,iBACd,WACA,WACA,QACQ;AACR,QAAM,aAAa,qBAAqB,MAAM;AAC9C,QAAM,SAAS,eAAe,UAAU;AACxC,SAAO,GAAG,SAAS,IAAI,SAAS,IAAI,KAAK,UAAU,MAAM,CAAC;AAC5D;;;AC1GA,IAAI,eAAoB;AASjB,SAAS,cAAc,OAAuB;AACnD,QAAM,QAAQ,MAAM,MAAM,IAAI;AAG9B,QAAM,aACJ;AAEF,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,SAAS,KAAK,GAAG;AACzB;AAAA,IACF;AAEA,QAAI,WAAW,KAAK,IAAI,GAAG;AACzB;AAAA,IACF;AAGA,UAAM,aAAa,KAAK,MAAM,aAAa;AAC3C,UAAM,UAAU,aAAa,WAAW,CAAC,IAAI,KAAK,KAAK,EAAE,MAAM,CAAC;AAGhE,QAAI,iBAAiB,QAAQ,QAAQ,OAAO,GAAG;AAC/C,UAAM,MAAM,QAAQ,IAAI,EAAE,QAAQ,OAAO,GAAG;AAG5C,QAAI,eAAe,YAAY,EAAE,WAAW,IAAI,YAAY,IAAI,GAAG,GAAG;AACpE,uBAAiB,eAAe,MAAM,IAAI,SAAS,CAAC;AAAA,IACtD;AAGA,UAAM,aAAa,eAAe,MAAM,sBAAsB;AAC9D,QAAI,YAAY;AACd,aAAO,GAAG,WAAW,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC;AAAA,IAC1C;AAAA,EACF;AAEA,SAAO;AACT;AAWO,SAAS,kBAAkB,kBAAuB,SAAoC;AAC3F,MAAI,CAAC,oBAAoB,CAAC,iBAAiB,SAAS,CAAC,iBAAiB,MAAM,WAAW;AACrF;AAAA,EACF;AAGA,MACE,iBAAiB,MAAM,UAAU,QACjC,iBAAiB,MAAM,UAAU,KAAK,uBACtC;AACA,YAAQ,KAAK,2CAA2C;AACxD;AAAA,EACF;AAGA,iBAAe,iBAAiB,MAAM,UAAU;AAGhD,QAAM,UAAU,QAAQ,YAAY;AACpC,QAAM,oBAAoB,QAAQ,sBAAsB;AACxD,QAAM,SAAS,QAAQ,UAAU,CAAC;AAElC,QAAM,cAAc,YAAwB,MAAa;AACvD,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,MAAM,MAAM,IAAI;AAAA,IACtC;AAEA,UAAM,YAAY,KAAK,OAAO;AAC9B,UAAM,YAAY,KAAK;AAEvB,QAAI,CAAC,aAAa,CAAC,WAAW;AAC5B,aAAO,aAAa,MAAM,MAAM,IAAI;AAAA,IACtC;AAGA,UAAM,YAAY,OAAO,KAAK,CAAC,UAAU;AACvC,YAAM,aAAa,CAAC,MAAM,SAAS,MAAM,UAAU;AACnD,YAAM,UAAU,CAAC,MAAM,aAAa,MAAM,cAAc;AACxD,aAAO,cAAc;AAAA,IACvB,CAAC;AAED,QAAI,WAAW;AACb,aAAO,aAAa,MAAM,MAAM,IAAI;AAAA,IACtC;AAEA,UAAM,SAAS,KAAK,SAAS,KAAK,CAAC;AACnC,UAAM,cAAc,iBAAiB,WAAW,WAAW,MAAM;AAEjE,QAAI,WAAW;AACf,QAAI,mBAAmB;AACrB,YAAM,QAAQ,IAAI,MAAM,EAAE;AAC1B,UAAI,OAAO;AACT,mBAAW,cAAc,KAAK;AAAA,MAChC;AAAA,IACF;AAEA,gBAAY;AAAA,MACV,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,WAAO,aAAa,MAAM,MAAM,IAAI;AAAA,EACtC;AAGA,EAAC,YAAoB,wBAAwB;AAG7C,mBAAiB,MAAM,UAAU,OAAO;AAC1C;;;ACxHO,SAAS,eACd,SACA,SACW;AACX,MAAI,CAAC,SAAS;AACZ,WAAO,CAAC;AAAA,EACV;AAGA,QAAM,WAAsB,QACzB,OAAO,CAAC,MAAM,EAAE,SAAS,QAAQ,SAAS,EAC1C,IAAI,CAAC,OAAO;AAAA,IACX,OAAO,EAAE;AAAA,IACT,WAAW,EAAE;AAAA,IACb,aAAa,EAAE;AAAA,IACf,OAAO,EAAE;AAAA,IACT,UAAU,EAAE;AAAA,EACd,EAAE;AAGJ,WAAS,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAEzC,SAAO;AACT;;;ACxBO,IAAM,eAAN,MAAmB;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMhB,YAAY,SAA+B;AACzC,UAAM,UACJ,SAAS,YAAY,SAAY,QAAQ,UAAU,QAAQ,IAAI,aAAa;AAC9E,UAAM,YAAY,SAAS,cAAc,SAAY,QAAQ,YAAY;AACzE,UAAM,oBACJ,SAAS,sBAAsB,SAAY,QAAQ,oBAAoB;AACzE,UAAM,SAAS,SAAS,UAAU,CAAC;AACnC,UAAM,YAAY,SAAS,aAAa,CAAC,IAAI,gBAAgB,CAAC;AAE9D,QAAI,SAAS,cAAc,UAAa,QAAQ,YAAY,GAAG;AAC7D,YAAM,IAAI;AAAA,QACR,2CAA2C,QAAQ,SAAS;AAAA,MAC9D;AAAA,IACF;AAEA,SAAK,UAAU;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOO,OAAO,kBAA6B;AACzC,sBAAkB,kBAAkB,KAAK,OAAO;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAiB,SAAkC;AACzD,UAAM,WAAW,eAAe,SAAS;AAAA,MACvC,WAAW,KAAK,QAAQ;AAAA,IAC1B,CAAC;AACD,QAAI,SAAS,SAAS,GAAG;AACvB,iBAAW,YAAY,KAAK,QAAQ,WAAW;AAC7C,iBAAS,OAAO,QAAQ;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQO,aAAsE;AAC3E,WAAO,CAAC,KAAU,KAAU,SAA8B;AACxD,UAAI,CAAC,KAAK,QAAQ,SAAS;AACzB,aAAK;AACL;AAAA,MACF;AAEA,iBAAW,YAAY;AACrB,cAAM,QAAQ,YAAY;AAC1B,YAAI,GAAG,UAAU,MAAM;AACrB,gBAAM,UAAU,QAAQ,MAAM,KAAK,MAAM,OAAO,CAAC,IAAI,CAAC;AACtD,eAAK,iBAAiB,OAAO;AAAA,QAC/B,CAAC;AACD,aAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAa,IAAO,IAAkC;AACpD,QAAI,CAAC,KAAK,QAAQ,SAAS;AACzB,aAAO,GAAG;AAAA,IACZ;AAEA,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,WAAW,YAAY;AAClC,cAAM,QAAQ,YAAY;AAC1B,YAAI;AACF,iBAAO,MAAM,GAAG;AAAA,QAClB,UAAE;AACA,oBAAU,QAAQ,MAAM,KAAK,MAAM,OAAO,CAAC,IAAI,CAAC;AAAA,QAClD;AAAA,MACF,CAAC;AAAA,IACH,UAAE;AACA,WAAK,iBAAiB,OAAO;AAAA,IAC/B;AAAA,EACF;AACF;;;AC9GO,IAAM,eAAN,MAAuC;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,YAAY,OAAgC;AAC1C,SAAK,QAAQ,SAAS,QAAQ;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,OAAO,UAA2B;AACvC,QAAI,SAAS,WAAW,GAAG;AACzB;AAAA,IACF;AAEA,eAAW,WAAW,UAAU;AAC9B,YAAM,SAAS;AAAA,QACb,MAAM;AAAA,QACN,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,GAAG;AAAA,MACL;AACA,WAAK,MAAM,KAAK,UAAU,MAAM,CAAC;AAAA,IACnC;AAAA,EACF;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "mongoosleuth",
3
+ "version": "0.1.0",
4
+ "description": "N+1 query pattern detector for Mongoose",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "test": "vitest run",
21
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
22
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\""
23
+ },
24
+ "keywords": [
25
+ "mongoose",
26
+ "mongodb",
27
+ "n+1",
28
+ "n1",
29
+ "detector",
30
+ "profiler",
31
+ "performance"
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "peerDependencies": {
36
+ "mongoose": "^7.0.0 || ^8.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/express": "^5.0.6",
40
+ "@types/node": "^20.0.0",
41
+ "@types/supertest": "^7.2.0",
42
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
43
+ "@typescript-eslint/parser": "^7.0.0",
44
+ "eslint": "^8.56.0",
45
+ "eslint-config-prettier": "^9.1.0",
46
+ "eslint-plugin-prettier": "^5.1.3",
47
+ "express": "^5.2.1",
48
+ "mongodb-memory-server": "^9.0.0",
49
+ "mongoose": "^8.0.0",
50
+ "prettier": "^3.2.5",
51
+ "supertest": "^7.2.2",
52
+ "tsup": "^8.0.2",
53
+ "typescript": "^5.3.3",
54
+ "vitest": "^4.1.9"
55
+ }
56
+ }