@lenne.tech/nest-server 11.22.1 → 11.23.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.
Files changed (26) hide show
  1. package/.claude/rules/configurable-features.md +1 -0
  2. package/CLAUDE.md +58 -0
  3. package/FRAMEWORK-API.md +6 -2
  4. package/dist/core/common/decorators/restricted.decorator.js +21 -4
  5. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  6. package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
  7. package/dist/core/common/services/crud.service.d.ts +4 -1
  8. package/dist/core/common/services/crud.service.js +24 -2
  9. package/dist/core/common/services/crud.service.js.map +1 -1
  10. package/dist/core/common/services/module.service.d.ts +3 -2
  11. package/dist/core/common/services/module.service.js +43 -20
  12. package/dist/core/common/services/module.service.js.map +1 -1
  13. package/dist/core/common/services/request-context.service.d.ts +3 -0
  14. package/dist/core/common/services/request-context.service.js +12 -0
  15. package/dist/core/common/services/request-context.service.js.map +1 -1
  16. package/dist/tsconfig.build.tsbuildinfo +1 -1
  17. package/docs/REQUEST-LIFECYCLE.md +25 -2
  18. package/docs/native-driver-security.md +153 -0
  19. package/docs/process-performance-optimization.md +493 -0
  20. package/migration-guides/11.22.x-to-11.23.0.md +235 -0
  21. package/package.json +16 -16
  22. package/src/core/common/decorators/restricted.decorator.ts +44 -4
  23. package/src/core/common/interfaces/server-options.interface.ts +8 -0
  24. package/src/core/common/services/crud.service.ts +77 -5
  25. package/src/core/common/services/module.service.ts +96 -35
  26. package/src/core/common/services/request-context.service.ts +47 -0
@@ -0,0 +1,235 @@
1
+ # Migration Guide: 11.22.x → 11.23.0
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ |----------|---------|
7
+ | **Breaking Changes** | `mainDbModel.collection` and `mainDbModel.db` blocked via TypeScript (`SafeModel`) |
8
+ | **New Features** | `process()` depth-based optimization, `debugProcessInput` config, `getNativeCollection(reason)`, `getNativeConnection(reason)`, restricted metadata cache |
9
+ | **Performance** | ~70% less memory on nested service cascades, eliminated redundant `JSON.stringify` and recursive `process()` calls |
10
+ | **Migration Effort** | ~5 minutes if using `mainDbModel.db`; 0 minutes otherwise |
11
+
12
+ ---
13
+
14
+ ## Quick Migration (No `mainDbModel.db` Usage)
15
+
16
+ If your project does **not** access `this.mainDbModel.collection` or `this.mainDbModel.db` directly:
17
+
18
+ ```bash
19
+ # Update package
20
+ pnpm add @lenne.tech/nest-server@11.23.0
21
+
22
+ # Verify build
23
+ pnpm run build
24
+
25
+ # Run tests
26
+ pnpm test
27
+ ```
28
+
29
+ No code changes required. All performance optimizations activate automatically.
30
+
31
+ ---
32
+
33
+ ## Breaking Changes
34
+
35
+ ### `mainDbModel` Type Changed to `SafeModel` (TypeScript Only)
36
+
37
+ `mainDbModel` is now typed as `SafeModel<T>` which is `Omit<Model<T>, 'collection' | 'db'>`. This is a **compile-time-only** change — runtime behavior is identical.
38
+
39
+ **What breaks:** Direct access to `.collection` or `.db` on `this.mainDbModel` produces a TypeScript error.
40
+
41
+ **What does NOT break:** All Mongoose Model methods (`find`, `findById`, `insertMany`, `updateOne`, `aggregate`, `bulkWrite`, etc.) continue to work unchanged.
42
+
43
+ #### If You Use `mainDbModel.collection`
44
+
45
+ **Before:**
46
+ ```typescript
47
+ // Direct native collection access — bypasses ALL Mongoose plugins
48
+ await this.mainDbModel.collection.insertOne(doc);
49
+ ```
50
+
51
+ **After (Option A — use Mongoose Model method):**
52
+ ```typescript
53
+ // Mongoose method — plugins (Tenant, Audit, RoleGuard, Password) fire correctly
54
+ await this.mainDbModel.insertMany([doc]);
55
+ ```
56
+
57
+ **After (Option B — intentional native access with logging):**
58
+ ```typescript
59
+ // Logged escape hatch — requires justification
60
+ const col = this.getNativeCollection('Migration: bulk-import historical data without tenant context');
61
+ await col.insertOne(doc);
62
+ ```
63
+
64
+ #### If You Use `mainDbModel.db`
65
+
66
+ **Before:**
67
+ ```typescript
68
+ // Native DB access via model — bypasses ALL Mongoose plugins
69
+ const count = await this.mainDbModel.db.db.collection('chatmessages').countDocuments({ ... });
70
+ ```
71
+
72
+ **After (Option A — inject the target model):**
73
+ ```typescript
74
+ // Inject the model via @InjectModel and use Mongoose methods
75
+ constructor(@InjectModel('ChatMessage') private chatMessageModel: Model<ChatMessageDocument>) {}
76
+
77
+ const count = await this.chatMessageModel.countDocuments({ ... });
78
+ ```
79
+
80
+ **After (Option B — intentional native access with logging):**
81
+ ```typescript
82
+ // Logged escape hatch — requires justification
83
+ const conn = this.getNativeConnection('Statistics: count chatmessages across all tenants');
84
+ const count = await conn.db.collection('chatmessages').countDocuments({ ... });
85
+ ```
86
+
87
+ #### If You Use `getModel()`
88
+
89
+ `getModel()` continues to return the full `Model` type (including `.collection` and `.db`). No change needed.
90
+
91
+ ---
92
+
93
+ ## What's New in 11.23.0
94
+
95
+ ### 1. process() Pipeline Performance Optimization
96
+
97
+ The `process()` method in `ModuleService` now tracks nesting depth via `RequestContext`. On nested calls (service cascades like A.create → B.create → C.create), redundant pipeline steps are skipped:
98
+
99
+ | Step | Outermost Call | Nested Calls |
100
+ |------|---------------|--------------|
101
+ | prepareInput | Runs | Runs |
102
+ | checkRights (input) | Runs | Runs |
103
+ | serviceFunc | Runs | Runs |
104
+ | processFieldSelection (populate) | Runs | **Skipped** (unless explicit `populate` is set) |
105
+ | prepareOutput (model mapping) | Runs | **Skipped** (secret removal still active) |
106
+ | checkRights (output) | Runs | **Skipped** |
107
+
108
+ **Security is maintained** through three layers:
109
+ 1. Input checkRights always runs at every depth
110
+ 2. Output checkRights runs at depth 0 (outermost call)
111
+ 3. `CheckSecurityInterceptor` (Safety Net) runs on the final HTTP response
112
+
113
+ **Estimated savings for an 8-level service cascade:** ~70% less memory (~120 KB instead of ~400 KB).
114
+
115
+ No configuration needed — this activates automatically.
116
+
117
+ ### 2. Lean Query for Rights Checking
118
+
119
+ The `process()` pipeline previously called `this.get()` to resolve `dbObject` for authorization checks, which triggered a **recursive** `process()` call. It now uses a direct lean query:
120
+
121
+ ```typescript
122
+ // Before: recursive process() call with full pipeline
123
+ const dbObject = await this.get(id);
124
+
125
+ // After: single lean query + map
126
+ const rawDoc = await this.mainDbModel.findById(id).lean().exec();
127
+ const dbObject = mainModelConstructor.map(rawDoc);
128
+ ```
129
+
130
+ This preserves ALL fields (including `createdBy`) needed for `S_CREATOR` and `S_SELF` checks, while avoiding the overhead of a full pipeline pass.
131
+
132
+ ### 3. `debugProcessInput` Configuration Option
133
+
134
+ The previous `JSON.stringify` debug comparison that ran on **every** `process()` call is now behind a config flag:
135
+
136
+ ```typescript
137
+ // config.env.ts — enable only for debugging
138
+ {
139
+ debugProcessInput: true, // default: false
140
+ }
141
+ ```
142
+
143
+ When disabled (default), two `JSON.stringify` calls per `process()` invocation are eliminated.
144
+
145
+ ### 4. Restricted Metadata Cache
146
+
147
+ `getRestricted()` results (from `@Restricted()` decorators) are now cached per class + property. Since decorator metadata is static (set at compile time and never changes), this eliminates hundreds of `Reflect.getMetadata()` lookups per request for objects with many properties.
148
+
149
+ ### 5. Native Driver Security (`SafeModel`, `getNativeCollection`, `getNativeConnection`)
150
+
151
+ Two new protected methods in `CrudService` provide logged escape hatches for legitimate native MongoDB driver access:
152
+
153
+ ```typescript
154
+ // Get the native MongoDB Collection for this model's collection
155
+ protected getNativeCollection(reason: string): Collection
156
+
157
+ // Get the Mongoose Connection (for cross-collection access, schema-less collections)
158
+ protected getNativeConnection(reason: string): Connection
159
+ ```
160
+
161
+ Both methods:
162
+ - Require a non-empty justification string
163
+ - Log a `[SECURITY]` warning on every call
164
+ - Throw an `Error` if no reason is provided
165
+
166
+ The `SafeModel<T>` type is exported for use in custom service implementations.
167
+
168
+ ### 6. RequestContext.processDepth API
169
+
170
+ New methods for tracking `process()` nesting depth:
171
+
172
+ ```typescript
173
+ // Get current depth (0 = outermost or no process() call)
174
+ RequestContext.getProcessDepth(): number
175
+
176
+ // Run a function with incremented depth
177
+ RequestContext.runWithIncrementedProcessDepth<T>(fn: () => T): T
178
+ ```
179
+
180
+ These are used internally by `process()` but are also available for custom pipeline implementations.
181
+
182
+ ---
183
+
184
+ ## Compatibility Notes
185
+
186
+ | Pattern | Status |
187
+ |---------|--------|
188
+ | All `CoreModule.forRoot()` patterns | Works unchanged |
189
+ | `CrudService` subclasses | Works unchanged (SafeModel is transparent for Mongoose methods) |
190
+ | Custom `process()` calls with `populate` option | Works unchanged (explicit populate overrides nested skip) |
191
+ | `getModel()` callers | Works unchanged (returns full Model type) |
192
+ | `@Restricted()` decorators | Works unchanged (cache is transparent) |
193
+ | Services using `this.mainDbModel.find/save/update/etc.` | Works unchanged |
194
+ | Services using `this.mainDbModel.collection` | **TypeScript error** — use `getNativeCollection(reason)` or Mongoose methods |
195
+ | Services using `this.mainDbModel.db` | **TypeScript error** — use `getNativeConnection(reason)` or inject target model |
196
+ | Services using `@InjectConnection` | Works unchanged (Connection is not affected by SafeModel) |
197
+
198
+ ---
199
+
200
+ ## Troubleshooting
201
+
202
+ | Issue | Solution |
203
+ |-------|----------|
204
+ | TypeScript error on `mainDbModel.collection` | Replace with Mongoose Model method or `this.getNativeCollection('reason')` |
205
+ | TypeScript error on `mainDbModel.db` | Replace with `this.getNativeConnection('reason')` or inject the target model via `@InjectModel` |
206
+ | `getModel()` return type mismatch | `getModel()` returns full `Model` — no change needed |
207
+ | `processFieldSelection` type error in custom override | Update parameter type to accept `SafeModel` (see `ModuleService.processFieldSelection`) |
208
+ | Debug logs from `prepareInput` disappeared | Set `debugProcessInput: true` in config to re-enable |
209
+
210
+ ---
211
+
212
+ ## Module Documentation
213
+
214
+ ### Native Driver Security
215
+
216
+ - **Security Policy:** [`docs/native-driver-security.md`](../docs/native-driver-security.md)
217
+ - **Performance Optimization:** [`docs/process-performance-optimization.md`](../docs/process-performance-optimization.md)
218
+
219
+ ### Affected Source Files
220
+
221
+ | File | Changes |
222
+ |------|---------|
223
+ | `src/core/common/services/module.service.ts` | `SafeModel` type, `mainDbModel` type change, `process()` depth optimization, lean query, debug config |
224
+ | `src/core/common/services/crud.service.ts` | `getNativeCollection(reason)`, `getNativeConnection(reason)`, `getModel()` cast |
225
+ | `src/core/common/services/request-context.service.ts` | `processDepth` field, `getProcessDepth()`, `runWithIncrementedProcessDepth()` |
226
+ | `src/core/common/decorators/restricted.decorator.ts` | Metadata cache, `_.uniq()` optimization |
227
+ | `src/core/common/interfaces/server-options.interface.ts` | `debugProcessInput` config option |
228
+
229
+ ---
230
+
231
+ ## References
232
+
233
+ - [Native Driver Security Policy](../docs/native-driver-security.md)
234
+ - [process() Performance Optimization](../docs/process-performance-optimization.md)
235
+ - [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.22.1",
3
+ "version": "11.23.0",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -92,13 +92,14 @@
92
92
  "@nestjs/websockets": "11.1.18",
93
93
  "@tus/file-store": "2.0.0",
94
94
  "@tus/server": "2.3.0",
95
+ "@types/supertest": "7.2.0",
95
96
  "bcrypt": "6.0.0",
96
97
  "better-auth": "1.5.5",
97
98
  "class-transformer": "0.5.1",
98
99
  "class-validator": "0.15.1",
99
100
  "compression": "1.8.1",
100
101
  "cookie-parser": "1.4.7",
101
- "dotenv": "17.4.0",
102
+ "dotenv": "17.4.1",
102
103
  "ejs": "5.0.1",
103
104
  "express": "5.2.1",
104
105
  "graphql": "16.13.2",
@@ -112,18 +113,19 @@
112
113
  "mongoose": "9.4.1",
113
114
  "multer": "2.1.1",
114
115
  "node-mailjet": "6.0.11",
115
- "nodemailer": "8.0.4",
116
+ "nodemailer": "8.0.5",
116
117
  "passport": "0.7.0",
117
118
  "passport-jwt": "4.0.1",
118
119
  "reflect-metadata": "0.2.2",
119
120
  "rfdc": "1.4.1",
120
121
  "rxjs": "7.8.2",
122
+ "supertest": "7.2.2",
121
123
  "ts-morph": "27.0.2",
122
124
  "yuml-diagram": "1.2.0"
123
125
  },
124
126
  "devDependencies": {
125
127
  "@compodoc/compodoc": "1.2.1",
126
- "@nestjs/cli": "11.0.17",
128
+ "@nestjs/cli": "11.0.18",
127
129
  "@nestjs/schematics": "11.0.10",
128
130
  "@nestjs/testing": "11.1.18",
129
131
  "@swc/cli": "0.8.1",
@@ -135,30 +137,28 @@
135
137
  "@types/lodash": "4.17.24",
136
138
  "@types/multer": "2.1.0",
137
139
  "@types/node": "25.5.2",
138
- "@types/nodemailer": "7.0.11",
140
+ "@types/nodemailer": "8.0.0",
139
141
  "@types/passport": "1.0.17",
140
- "@types/supertest": "7.2.0",
141
- "@vitest/coverage-v8": "4.1.2",
142
- "@vitest/ui": "4.1.2",
142
+ "@vitest/coverage-v8": "4.1.3",
143
+ "@vitest/ui": "4.1.3",
143
144
  "ansi-colors": "4.1.3",
144
145
  "find-file-up": "2.0.1",
145
146
  "husky": "9.1.7",
146
147
  "nodemon": "3.1.14",
147
148
  "npm-watch": "0.13.0",
148
149
  "otpauth": "9.5.0",
149
- "oxfmt": "0.43.0",
150
- "oxlint": "1.58.0",
150
+ "oxfmt": "0.44.0",
151
+ "oxlint": "1.59.0",
151
152
  "rimraf": "6.1.3",
152
- "supertest": "7.2.2",
153
153
  "ts-node": "10.9.2",
154
154
  "tsconfig-paths": "4.2.0",
155
155
  "tus-js-client": "4.3.1",
156
156
  "typescript": "5.9.3",
157
157
  "unplugin-swc": "1.5.9",
158
- "vite": "7.3.1",
158
+ "vite": "7.3.2",
159
159
  "vite-plugin-node": "7.0.0",
160
160
  "vite-tsconfig-paths": "6.1.1",
161
- "vitest": "4.1.2"
161
+ "vitest": "4.1.3"
162
162
  },
163
163
  "main": "dist/index.js",
164
164
  "types": "dist/index.d.ts",
@@ -179,13 +179,12 @@
179
179
  "watch": {
180
180
  "build:dev": "src"
181
181
  },
182
- "packageManager": "pnpm@10.29.2",
182
+ "packageManager": "pnpm@10.33.0",
183
183
  "pnpm": {
184
184
  "overrides": {
185
185
  "minimatch@<3.1.5": "3.1.5",
186
186
  "minimatch@>=9.0.0 <9.0.9": "9.0.9",
187
187
  "minimatch@>=10.0.0 <10.2.5": "10.2.5",
188
- "rollup@>=4.0.0 <4.60.1": "4.60.1",
189
188
  "ajv@<6.14.0": "6.14.0",
190
189
  "ajv@>=7.0.0-alpha.0 <8.18.0": "8.18.0",
191
190
  "undici@>=7.0.0 <7.24.7": "7.24.7",
@@ -198,7 +197,8 @@
198
197
  "path-to-regexp@>=8.0.0 <8.4.2": "8.4.2",
199
198
  "kysely@>=0.26.0 <0.28.15": "0.28.15",
200
199
  "lodash@>=4.0.0 <4.18.0": "4.18.1",
201
- "defu@<=6.1.4": "6.1.6"
200
+ "defu@<=6.1.6": "6.1.7",
201
+ "vite@>=7.0.0 <7.3.2": "7.3.2"
202
202
  },
203
203
  "onlyBuiltDependencies": [
204
204
  "bcrypt",
@@ -44,6 +44,19 @@ export const Restricted = (...rolesOrMember: RestrictedType): ClassDecorator & P
44
44
  return Reflect.metadata(restrictedMetaKey, rolesOrMember);
45
45
  };
46
46
 
47
+ /**
48
+ * Cache for Restricted metadata — decorators are static, metadata never changes at runtime.
49
+ * WeakMap<CacheTarget, Map<propertyKey | '__class__', RestrictedType>>
50
+ *
51
+ * Uses WeakMap so that dynamically-generated or hot-reloaded class constructors can be GC'd
52
+ * when no longer reachable (prevents unbounded growth in test suites and dev watch mode).
53
+ *
54
+ * CacheTarget is the class constructor (for instances) or the class itself (when object IS a constructor).
55
+ * This distinction is critical: getRestricted(data.constructor) passes a class as `object`,
56
+ * and (classFunction).constructor === Function for ALL classes — so we must use the class itself.
57
+ */
58
+ const restrictedMetadataCache = new WeakMap<object, Map<string, RestrictedType>>();
59
+
47
60
  /**
48
61
  * Get restricted data for (property of) object
49
62
  */
@@ -51,10 +64,36 @@ export const getRestricted = (object: unknown, propertyKey?: string): Restricted
51
64
  if (!object) {
52
65
  return null;
53
66
  }
54
- if (!propertyKey) {
55
- return Reflect.getMetadata(restrictedMetaKey, object);
67
+
68
+ // Determine cache target: use the class constructor for instances, the object itself for classes.
69
+ // When object IS a constructor (typeof === 'function'), using object.constructor would give Function
70
+ // for ALL classes, causing cache collisions.
71
+ const cacheTarget: object | undefined =
72
+ typeof object === 'function' ? (object as object) : (object as any).constructor;
73
+ if (!cacheTarget) {
74
+ return propertyKey
75
+ ? Reflect.getMetadata(restrictedMetaKey, object, propertyKey)
76
+ : Reflect.getMetadata(restrictedMetaKey, object);
77
+ }
78
+
79
+ let classCache = restrictedMetadataCache.get(cacheTarget);
80
+ if (!classCache) {
81
+ classCache = new Map();
82
+ restrictedMetadataCache.set(cacheTarget, classCache);
83
+ }
84
+
85
+ const cacheKey = propertyKey || '__class__';
86
+ if (classCache.has(cacheKey)) {
87
+ return classCache.get(cacheKey);
56
88
  }
57
- return Reflect.getMetadata(restrictedMetaKey, object, propertyKey);
89
+
90
+ // Cache miss: perform Reflect lookup and cache the result
91
+ const metadata = propertyKey
92
+ ? Reflect.getMetadata(restrictedMetaKey, object, propertyKey)
93
+ : Reflect.getMetadata(restrictedMetaKey, object);
94
+
95
+ classCache.set(cacheKey, metadata);
96
+ return metadata;
58
97
  };
59
98
 
60
99
  /**
@@ -257,7 +296,8 @@ export const checkRestricted = (
257
296
 
258
297
  // Check restricted
259
298
  const restricted = getRestricted(data, propertyKey) || [];
260
- const concatenatedRestrictions = config.mergeRoles ? _.uniq(objectRestrictions.concat(restricted)) : restricted;
299
+ const concatenatedRestrictions =
300
+ config.mergeRoles && objectRestrictions.length ? _.uniq(objectRestrictions.concat(restricted)) : restricted;
261
301
  const valid = validateRestricted(concatenatedRestrictions);
262
302
 
263
303
  // Check rights
@@ -1109,6 +1109,14 @@ export interface IServerOptions {
1109
1109
  CronExpression | CronJobConfigWithTimeZone | CronJobConfigWithUtcOffset | Date | Falsy | string
1110
1110
  >;
1111
1111
 
1112
+ /**
1113
+ * When true, logs a debug message when prepareInput() changes the input type during process().
1114
+ * Enable only for debugging — has performance cost due to JSON.stringify on every process() call.
1115
+ *
1116
+ * @default false
1117
+ */
1118
+ debugProcessInput?: boolean;
1119
+
1112
1120
  /**
1113
1121
  * SMTP and template configuration for sending emails
1114
1122
  */
@@ -1,6 +1,8 @@
1
- import { NotFoundException } from '@nestjs/common';
1
+ import { Logger, NotFoundException } from '@nestjs/common';
2
2
  import {
3
3
  AggregateOptions,
4
+ Collection,
5
+ Connection,
4
6
  Document,
5
7
  Model as MongooseModel,
6
8
  PipelineStage,
@@ -76,7 +78,11 @@ export abstract class CrudService<
76
78
  return this.process(
77
79
  async (data) => {
78
80
  const currentUserId = serviceOptions?.currentUser?.id;
79
- return new this.mainDbModel({ ...data.input, createdBy: currentUserId, updatedBy: currentUserId }).save();
81
+ return new (this.mainDbModel as MongooseModel<Document & Model>)({
82
+ ...data.input,
83
+ createdBy: currentUserId,
84
+ updatedBy: currentUserId,
85
+ }).save();
80
86
  },
81
87
  { input, serviceOptions },
82
88
  );
@@ -314,7 +320,7 @@ export abstract class CrudService<
314
320
  filter?: FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryOptions; samples?: number },
315
321
  serviceOptions: ServiceOptions = {},
316
322
  ): Promise<{ items: Model[]; pagination: PaginationInfo; totalCount: number }> {
317
- serviceOptions.raw = true;
323
+ serviceOptions.force = true;
318
324
  return this.findAndCount(filter, serviceOptions);
319
325
  }
320
326
 
@@ -443,11 +449,77 @@ export abstract class CrudService<
443
449
  }
444
450
 
445
451
  /**
446
- * Get service model to process queries directly
452
+ * Get service model to process queries directly.
447
453
  * See https://mongoosejs.com/docs/api/model.html
454
+ *
455
+ * Note: Returns the full Mongoose Model (including `.collection`).
456
+ * Prefer using CrudService methods or Mongoose Model methods directly
457
+ * to ensure plugins (Tenant, Audit, RoleGuard) fire correctly.
448
458
  */
449
459
  getModel(): MongooseModel<Document & Model> {
450
- return this.mainDbModel;
460
+ return this.mainDbModel as unknown as MongooseModel<Document & Model>;
461
+ }
462
+
463
+ /**
464
+ * Get the native MongoDB Collection, bypassing all Mongoose plugins.
465
+ *
466
+ * **WARNING:** Native driver access bypasses Tenant-Isolation, Audit-Fields,
467
+ * RoleGuard, and Password-Hashing plugins. Only use this when Mongoose
468
+ * Model methods cannot achieve the goal (e.g., bulk imports, migrations).
469
+ *
470
+ * Every call logs a `[SECURITY]` warning with the provided reason.
471
+ *
472
+ * @param reason - Mandatory justification for native access (logged for audit trail)
473
+ * @throws Error if reason is empty
474
+ *
475
+ * @example
476
+ * ```typescript
477
+ * const col = this.getNativeCollection('Migration: bulk-import historical data without tenant context');
478
+ * await col.insertMany(legacyDocs);
479
+ * ```
480
+ */
481
+ protected getNativeCollection(reason: string): Collection {
482
+ this.validateNativeAccessReason(reason, 'getNativeCollection');
483
+
484
+ const modelName = (this.mainDbModel as any)?.modelName || 'Unknown';
485
+ Logger.warn(`[SECURITY] Native collection access: ${reason} (Model: ${modelName})`, this.constructor.name);
486
+
487
+ return (this.mainDbModel as unknown as MongooseModel<Document & Model>).collection;
488
+ }
489
+
490
+ /**
491
+ * Get the Mongoose Connection (which provides access to the native MongoDB Db and MongoClient).
492
+ *
493
+ * **WARNING:** Via `connection.db` you get the native MongoDB Db instance,
494
+ * and via `connection.getClient()` the native MongoClient. Both bypass ALL
495
+ * Mongoose plugins (Tenant-Isolation, Audit-Fields, RoleGuard, Password-Hashing).
496
+ *
497
+ * Every call logs a `[SECURITY]` warning with the provided reason.
498
+ *
499
+ * @param reason - Mandatory justification (min 20 chars) for native access (logged for audit trail)
500
+ * @throws Error if reason is empty or too short
501
+ *
502
+ * @example
503
+ * ```typescript
504
+ * // Read-only cross-collection count (no Mongoose schema for target collection)
505
+ * const conn = this.getNativeConnection('Statistics: count chatmessages across all tenants');
506
+ * const count = await conn.db.collection('chatmessages').countDocuments({ ... });
507
+ * ```
508
+ */
509
+ protected getNativeConnection(reason: string): Connection {
510
+ this.validateNativeAccessReason(reason, 'getNativeConnection');
511
+
512
+ const modelName = (this.mainDbModel as any)?.modelName || 'Unknown';
513
+ Logger.warn(`[SECURITY] Native connection access: ${reason} (Model: ${modelName})`, this.constructor.name);
514
+
515
+ return (this.mainDbModel as unknown as MongooseModel<Document & Model>).db;
516
+ }
517
+
518
+ private validateNativeAccessReason(reason: string, method: string): void {
519
+ const trimmed = reason?.trim();
520
+ if (!trimmed || trimmed.length < 20) {
521
+ throw new Error(`${method} requires a meaningful reason (min 20 chars) — explain why native access is needed`);
522
+ }
451
523
  }
452
524
 
453
525
  /**