@lenne.tech/nest-server 11.22.1 → 11.23.1
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/.claude/rules/configurable-features.md +1 -0
- package/CLAUDE.md +77 -0
- package/FRAMEWORK-API.md +6 -2
- package/dist/core/common/decorators/restricted.decorator.js +21 -4
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/common/services/crud.service.d.ts +4 -1
- package/dist/core/common/services/crud.service.js +24 -2
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/common/services/module.service.d.ts +3 -2
- package/dist/core/common/services/module.service.js +43 -20
- package/dist/core/common/services/module.service.js.map +1 -1
- package/dist/core/common/services/request-context.service.d.ts +3 -0
- package/dist/core/common/services/request-context.service.js +12 -0
- package/dist/core/common/services/request-context.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +25 -2
- package/docs/native-driver-security.md +153 -0
- package/docs/process-performance-optimization.md +493 -0
- package/migration-guides/11.22.x-to-11.23.0.md +235 -0
- package/package.json +16 -16
- package/src/core/common/decorators/restricted.decorator.ts +44 -4
- package/src/core/common/interfaces/server-options.interface.ts +8 -0
- package/src/core/common/services/crud.service.ts +77 -5
- package/src/core/common/services/module.service.ts +96 -35
- 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.
|
|
3
|
+
"version": "11.23.1",
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
140
|
+
"@types/nodemailer": "8.0.0",
|
|
139
141
|
"@types/passport": "1.0.17",
|
|
140
|
-
"@
|
|
141
|
-
"@vitest/
|
|
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.
|
|
150
|
-
"oxlint": "1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
/**
|