@lenne.tech/nest-server 11.22.0 → 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 (30) hide show
  1. package/.claude/rules/configurable-features.md +1 -0
  2. package/.claude/rules/framework-compatibility.md +79 -0
  3. package/CLAUDE.md +60 -0
  4. package/FRAMEWORK-API.md +235 -0
  5. package/dist/core/common/decorators/restricted.decorator.js +21 -4
  6. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  7. package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
  8. package/dist/core/common/services/crud.service.d.ts +4 -1
  9. package/dist/core/common/services/crud.service.js +24 -2
  10. package/dist/core/common/services/crud.service.js.map +1 -1
  11. package/dist/core/common/services/module.service.d.ts +3 -2
  12. package/dist/core/common/services/module.service.js +43 -20
  13. package/dist/core/common/services/module.service.js.map +1 -1
  14. package/dist/core/common/services/request-context.service.d.ts +3 -0
  15. package/dist/core/common/services/request-context.service.js +12 -0
  16. package/dist/core/common/services/request-context.service.js.map +1 -1
  17. package/dist/server/modules/file/file-info.model.d.ts +1 -5
  18. package/dist/server/modules/user/user.model.d.ts +1 -5
  19. package/dist/tsconfig.build.tsbuildinfo +1 -1
  20. package/docs/REQUEST-LIFECYCLE.md +25 -2
  21. package/docs/native-driver-security.md +153 -0
  22. package/docs/process-performance-optimization.md +493 -0
  23. package/migration-guides/11.22.0-to-11.22.1.md +105 -0
  24. package/migration-guides/11.22.x-to-11.23.0.md +235 -0
  25. package/package.json +33 -31
  26. package/src/core/common/decorators/restricted.decorator.ts +44 -4
  27. package/src/core/common/interfaces/server-options.interface.ts +8 -0
  28. package/src/core/common/services/crud.service.ts +77 -5
  29. package/src/core/common/services/module.service.ts +96 -35
  30. package/src/core/common/services/request-context.service.ts +47 -0
@@ -863,6 +863,28 @@ The `process()` method in `ModuleService` is the **primary** way to handle CRUD
863
863
  +---------------------------------------------------------------+
864
864
  ```
865
865
 
866
+ ### Depth-Based Optimization (v11.23.0+)
867
+
868
+ When `process()` is called from within another `process()` call (service cascades like A.create → B.create → C.create), steps 4–6 are **conditionally skipped** on inner calls to avoid redundant work:
869
+
870
+ | Step | Depth 0 (outermost) | Depth > 0 (nested) |
871
+ |------|---------------------|---------------------|
872
+ | 1. prepareInput | Runs | Runs |
873
+ | 2. checkRights (INPUT) | Runs | Runs |
874
+ | 3. serviceFunc | Runs | Runs |
875
+ | 4. processFieldSelection | Runs | **Skipped** (unless `populate` explicitly set) |
876
+ | 5. prepareOutput (model mapping) | Runs | **Skipped** (secret removal still active) |
877
+ | 6. checkRights (OUTPUT) | Runs | **Skipped** |
878
+
879
+ **Security is maintained** because:
880
+ 1. Input authorization (step 2) always runs at every depth
881
+ 2. Output authorization (step 6) runs at the outermost call
882
+ 3. `CheckSecurityInterceptor` (Safety Net) runs on the final HTTP response
883
+
884
+ **Important:** Code running at depth > 0 (cron jobs, queue consumers, event handlers outside the HTTP cycle) must NOT return data directly to external consumers without either an outer depth-0 `process()` call or manual `checkRights` — the output rights check is skipped at depth > 0.
885
+
886
+ See [process() Performance Optimization](process-performance-optimization.md) for details.
887
+
866
888
  ### Key Options
867
889
 
868
890
  | Option | Type | Default | Effect |
@@ -870,8 +892,9 @@ The `process()` method in `ModuleService` is the **primary** way to handle CRUD
870
892
  | `force` | boolean | `false` | Disables checkRights, checkRoles, removeSecrets, bypasses role guard plugin |
871
893
  | `raw` | boolean | `false` | Disables prepareInput and prepareOutput entirely |
872
894
  | `checkRights` | boolean | `true` | Enable/disable authorization checks |
873
- | `populate` | object | - | Field selection for population |
895
+ | `populate` | object | - | Field selection for population (overrides nested skip) |
874
896
  | `currentUser` | object | from request | Override the current user |
897
+ | `debugProcessInput` | boolean | `false` | Config flag: log when prepareInput changes the input type (performance cost) |
875
898
 
876
899
  ### Alternative: processResult()
877
900
 
@@ -883,7 +906,7 @@ const doc = await this.mainDbModel.findById(id).exec();
883
906
  return this.processResult(doc, serviceOptions);
884
907
  ```
885
908
 
886
- `processResult()` handles population and `prepareOutput()` only. Security is handled by the Safety Net (Mongoose plugins for input, interceptors for output).
909
+ `processResult()` handles population and `prepareOutput()` only. It does **not** perform authorization checks (`checkRights`). Security is handled by the Safety Net (Mongoose plugins for input, interceptors for output). If called outside an HTTP request cycle (cron, queue), call `checkRights` manually before returning data to external consumers.
887
910
 
888
911
  ---
889
912
 
@@ -0,0 +1,153 @@
1
+ # Native MongoDB Driver Access — Security Policy
2
+
3
+ ## Problem
4
+
5
+ Mongoose registers plugins (Tenant isolation, Audit fields, RoleGuard, Password hashing, ID transformation) as pre/post hooks on schema operations. These hooks **only** fire on Mongoose Model methods — not on the native MongoDB driver.
6
+
7
+ Three access paths bypass all plugins:
8
+
9
+ ```typescript
10
+ // Path 1: model.collection.* (native driver via Mongoose Model)
11
+ await this.userModel.collection.insertOne(doc); // No tenantId, no audit, no password hash
12
+
13
+ // Path 2: model.db (Mongoose Connection → native Db → native MongoClient)
14
+ await this.userModel.db.db.collection('users').insertOne(doc); // Same problem
15
+
16
+ // Path 3: connection.db.collection() (native driver via injected Connection)
17
+ await this.connection.db.collection('users').insertOne(doc); // Same problem
18
+ ```
19
+
20
+ ### What Gets Bypassed
21
+
22
+ | Plugin | Function | Risk When Bypassed |
23
+ |--------|---------|---------------------|
24
+ | **Tenant Plugin** | Sets tenantId on new documents, filters queries | Data leak between tenants |
25
+ | **Audit Plugin** | Sets createdBy/updatedBy | No traceability |
26
+ | **RoleGuard Plugin** | Prevents role escalation | Privilege escalation |
27
+ | **Password Plugin** | Hashes passwords (bcrypt) | Plaintext passwords in DB |
28
+ | **ID Plugin** | Transforms _id to id | Inconsistent API responses |
29
+
30
+ ---
31
+
32
+ ## Protection: Three Layers
33
+
34
+ ### Layer 1: TypeScript Type Guard in CrudService
35
+
36
+ `mainDbModel` is blocked via `Omit<Model, 'collection' | 'db'>` type (`SafeModel`):
37
+
38
+ ```typescript
39
+ this.mainDbModel.find(...) // Works
40
+ this.mainDbModel.insertMany(...) // Works
41
+ this.mainDbModel.collection // TypeScript error
42
+ this.mainDbModel.db // TypeScript error
43
+
44
+ this.getNativeCollection('reason') // Escape hatch for native Collection, with logging
45
+ this.getNativeConnection('reason') // Escape hatch for Mongoose Connection, with logging
46
+ ```
47
+
48
+ **`getNativeCollection(reason)`** and **`getNativeConnection(reason)`** are the only ways to access the native driver from CrudService-based services. They:
49
+ - Require a reason (string parameter)
50
+ - Log a `[SECURITY]` warning on every access
51
+ - Throw an error if no reason is provided
52
+
53
+ ### Layer 2: CLAUDE.md Rules
54
+
55
+ Documented in `nest-server/CLAUDE.md`, `nest-server-starter/CLAUDE.md`, and `lt-monorepo/CLAUDE.md`:
56
+ - `model.collection.*` is forbidden
57
+ - `connection.db.collection()` only for schema-less collections
58
+ - Mongoose Model methods as alternatives
59
+
60
+ ### Layer 3: AI Review Rules
61
+
62
+ Review agents (backend-reviewer, security-reviewer, code-reviewer) check on every review:
63
+ - `.collection.` access on Mongoose Models → Security risk (HIGH)
64
+ - `.db.collection()` on tenant-scoped collections → Security risk
65
+ - `.db.collection()` on schema-less collections → Allowed
66
+
67
+ ---
68
+
69
+ ## Secure Alternatives
70
+
71
+ | Forbidden (native driver) | Allowed (Mongoose — plugins active) |
72
+ |--------------------------|-------------------------------------|
73
+ | `collection.insertOne(doc)` | `Model.insertMany([doc])` |
74
+ | `collection.bulkWrite(ops)` | `Model.bulkWrite(ops)` |
75
+ | `collection.updateOne(f, u)` | `Model.updateOne(f, u)` |
76
+ | `collection.updateMany(f, u)` | `Model.updateMany(f, u)` |
77
+ | `collection.deleteOne(f)` | `Model.deleteOne(f)` |
78
+ | `collection.deleteMany(f)` | `Model.deleteMany(f)` |
79
+ | `collection.find(f)` | `Model.find(f)` or `Model.find(f).lean()` |
80
+ | `collection.findOne(f)` | `Model.findOne(f)` or `Model.findOne(f).lean()` |
81
+ | `collection.aggregate(p)` | `Model.aggregate(p)` |
82
+ | `collection.countDocuments(f)` | `Model.countDocuments(f)` |
83
+
84
+ **Performance:** Mongoose Model methods have minimal overhead compared to the native driver — the plugins themselves cost < 0.1ms per call (see `docs/process-performance-optimization.md`).
85
+
86
+ ---
87
+
88
+ ## Allowed Cases for connection.db.collection()
89
+
90
+ | Use Case | Example | Why Allowed |
91
+ |----------|---------|-------------|
92
+ | Schema-less collections | `db.collection('mcp_oauth_clients')` | No Mongoose schema, no tenantId field |
93
+ | BetterAuth tables | `db.collection('session')`, `db.collection('account')` | IAM infrastructure, not tenant-scoped |
94
+ | Read-only aggregations | `db.collection('incidents').countDocuments({tenantId, ...})` | Read-only, manual tenant filter |
95
+ | Admin operations | `db.collection('users').createIndex(...)` | Index management, no CRUD |
96
+ | DevOps/Backup | `db.collection(name).drop()` | One-time admin actions |
97
+
98
+ ### Not Allowed
99
+
100
+ ```typescript
101
+ // Write on tenant-scoped collection without Mongoose
102
+ await this.connection.db.collection('orders').insertOne({ ... });
103
+ // → await this.orderModel.insertMany([{ ... }]);
104
+
105
+ // Read without tenant filter on tenant-scoped data
106
+ await this.connection.db.collection('orders').find({}).toArray();
107
+ // → await this.orderModel.find({}); // Tenant plugin filters automatically
108
+ ```
109
+
110
+ ---
111
+
112
+ ## getNativeCollection() / getNativeConnection() Reference
113
+
114
+ ```typescript
115
+ // Definitions in CrudService:
116
+ protected getNativeCollection(reason: string): Collection
117
+ protected getNativeConnection(reason: string): Connection
118
+
119
+ // Usage — native Collection:
120
+ const col = this.getNativeCollection('Migration: Bulk import of historical data without tenant context');
121
+ await col.insertOne(doc);
122
+
123
+ // Usage — native Db (for cross-collection reads, schema-less collections):
124
+ const conn = this.getNativeConnection('Statistics: count chatmessages across all tenants');
125
+ const count = await conn.db.collection('chatmessages').countDocuments({ ... });
126
+
127
+ // Without reason → Error:
128
+ this.getNativeCollection(''); // throws Error
129
+ this.getNativeConnection(''); // throws Error
130
+
131
+ // Logging:
132
+ // [SECURITY] Native collection access: Migration: Bulk import... (Model: MonitorCheck)
133
+ // [SECURITY] Native db access: Statistics: count chatmessages... (Model: Statistics)
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Review Checklist
139
+
140
+ When reviewing code (manual or AI), check for `.collection.` and `.db.collection(`:
141
+
142
+ ```
143
+ 1. Is it model.collection.* ?
144
+ → ALWAYS a security risk. Use getNativeCollection() or Model method.
145
+
146
+ 2. Is it connection.db.collection('name') ?
147
+ → Does the collection have a Mongoose schema with tenantId?
148
+ YES → Security violation. Use Mongoose Model.
149
+ NO → Continue to 3.
150
+ → Are data being written?
151
+ YES → Verify tenantId/audit is set manually.
152
+ NO → Allowed (read-only).
153
+ ```
@@ -0,0 +1,493 @@
1
+ # nest-server: process() Performance-Optimierung
2
+
3
+ ## Kontext
4
+
5
+ Die `process()` Pipeline in `ModuleService` ist der zentrale Verarbeitungspfad fuer alle CRUD-Operationen. Bei hochfrequenten oder verschachtelten Aufrufen (Service-Kaskaden) entsteht unnoetig hoher Memory- und CPU-Verbrauch. Die folgenden Optimierungen reduzieren den Overhead ohne die Sicherheit herabzusetzen.
6
+
7
+ **Analyse-Basis:** 8 Kundenprojekte + 13 lenne.tech-Projekte geprueft — alle Aenderungen sind rueckwaertskompatibel.
8
+
9
+ ---
10
+
11
+ ## Aenderung 1: JSON.stringify Debug konfigurierbar machen
12
+
13
+ ### Datei: `src/core/common/services/module.service.ts`
14
+
15
+ ### Problem
16
+
17
+ Zeilen 149-164 serialisieren den Input bei **jedem** process()-Aufruf zweimal (vorher/nachher) — nur fuer einen `console.debug` Vergleich. Das `new Promise(() => ...)` ist ein Fire-and-Forget ohne await.
18
+
19
+ ### Aktueller Code (Zeile 149-165)
20
+
21
+ ```typescript
22
+ const originalInput = config.input;
23
+ const inputJSON = JSON.stringify(originalInput);
24
+ const preparedInput = await this.prepareInput(config.input, config);
25
+ new Promise(() => {
26
+ if (
27
+ inputJSON?.replace(/"password":\s*"[^"]*"/, '') !==
28
+ JSON.stringify(preparedInput)?.replace(/"password":\s*"[^"]*"/, '')
29
+ ) {
30
+ console.debug(
31
+ 'CheckSecurityInterceptor: securityCheck changed input of type',
32
+ originalInput.constructor.name,
33
+ 'to type',
34
+ preparedInput.constructor.name,
35
+ );
36
+ }
37
+ });
38
+ config.input = preparedInput;
39
+ ```
40
+
41
+ ### Neuer Code
42
+
43
+ ```typescript
44
+ const preparedInput = await this.prepareInput(config.input, config);
45
+
46
+ // Debug-Vergleich nur wenn explizit konfiguriert (default: false)
47
+ if (this.configService?.getFastButReadOnly('debugProcessInput', false)) {
48
+ try {
49
+ const originalJSON = JSON.stringify(config.input)?.replace(/"password":\s*"[^"]*"/, '');
50
+ const preparedJSON = JSON.stringify(preparedInput)?.replace(/"password":\s*"[^"]*"/, '');
51
+ if (originalJSON !== preparedJSON) {
52
+ console.debug(
53
+ 'process: prepareInput changed input of type',
54
+ config.input?.constructor?.name,
55
+ 'to type',
56
+ preparedInput?.constructor?.name,
57
+ );
58
+ }
59
+ } catch {
60
+ // JSON.stringify kann bei zirkulaeren Referenzen fehlschlagen — ignorieren
61
+ }
62
+ }
63
+
64
+ config.input = preparedInput;
65
+ ```
66
+
67
+ ### Datei: `src/core/common/interfaces/server-options.interface.ts`
68
+
69
+ In das `IServerOptions`-Interface (oder dem passenden Config-Bereich) hinzufuegen:
70
+
71
+ ```typescript
72
+ /**
73
+ * When true, logs a debug message when prepareInput() changes the input type.
74
+ * Default: false. Enable only for debugging — has performance cost due to JSON.stringify.
75
+ */
76
+ debugProcessInput?: boolean;
77
+ ```
78
+
79
+ ### Sicherheit
80
+
81
+ Nicht betroffen — rein diagnostisch.
82
+
83
+ ### Kompatibilitaet
84
+
85
+ Default `false` — kein bestehendes Projekt aendert sein Verhalten.
86
+
87
+ ---
88
+
89
+ ## Aenderung 2: this.get(dbObject) durch Lean Query ersetzen
90
+
91
+ ### Datei: `src/core/common/services/module.service.ts`
92
+
93
+ ### Problem
94
+
95
+ Zeilen 168-176 rufen `this.get()` auf, was **rekursiv** die gesamte process()-Pipeline durchlaeuft — inklusive prepareInput, checkRights, processFieldSelection, prepareOutput. Das ist unnoetig, weil das dbObject nur als Kontext fuer den Rights-Check gebraucht wird.
96
+
97
+ Zusaetzlich ist es sogar **kontraproduktiv**: `this.get()` ohne `force` kann Felder wie `createdBy` entfernen (wegen `@Restricted(RoleEnum.ADMIN)`), die dann fuer den `S_CREATOR`-Check fehlen.
98
+
99
+ ### Aktueller Code (Zeile 168-176)
100
+
101
+ ```typescript
102
+ // Get DB object
103
+ if (config.dbObject && config.checkRights && this.checkRights) {
104
+ if (typeof config.dbObject === 'string' || config.dbObject instanceof Types.ObjectId) {
105
+ const dbObject = await this.get(getStringIds(config.dbObject));
106
+ if (dbObject) {
107
+ config.dbObject = dbObject;
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### Neuer Code
114
+
115
+ ```typescript
116
+ // Get DB object for rights checking — lean query to avoid recursive process() call.
117
+ // Using lean preserves ALL fields (including createdBy) which is needed for
118
+ // S_CREATOR and S_SELF checks. The full process() pipeline would remove
119
+ // restricted fields, potentially breaking these checks.
120
+ if (config.dbObject && config.checkRights && this.checkRights) {
121
+ if (typeof config.dbObject === 'string' || config.dbObject instanceof Types.ObjectId) {
122
+ if (this.mainDbModel) {
123
+ const rawDoc = await this.mainDbModel.findById(getStringIds(config.dbObject)).lean().exec();
124
+ if (rawDoc) {
125
+ // Map to Model instance so securityCheck() is available as a method
126
+ config.dbObject = (this.mainModelConstructor as any)?.map
127
+ ? (this.mainModelConstructor as any).map(rawDoc)
128
+ : rawDoc;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### Sicherheit
136
+
137
+ **Verbessert** — lean Query behaelt alle Felder (z.B. `createdBy`), was den `S_CREATOR`-Check zuverlaessiger macht als der aktuelle Code.
138
+
139
+ ### Nachfolgende Prozesse geprueft
140
+
141
+ - `checkRights(input)` (Zeile 179-184): Verwendet `config.dbObject` fuer `S_CREATOR`/`S_SELF`/`memberOf` Checks via `equalIds()` — funktioniert mit lean+map, da `equalIds` sowohl ObjectIds als auch Strings vergleicht.
142
+ - `checkRights(output)` (Zeile 239-250): Gleiche Nutzung — funktioniert.
143
+ - `validateRestricted()` in `checkRestricted()` (restricted.decorator.ts, Zeile 172-174): Prueft `'createdBy' in data && equalIds(data.createdBy, user)` — lean-Objekt hat `createdBy` immer (nicht entfernt durch Pipeline).
144
+ - `memberOf`-Check (Zeile 196-218): Liest `config.dbObject?.[property]` — lean-Objekt hat alle Properties.
145
+
146
+ ### Kompatibilitaet
147
+
148
+ - `CrudService.update()` (Zeile 552): Setzt `dbObject` bereits als lean-Objekt (`findById().lean()`) → konsistent.
149
+ - Kein Projekt uebergibt ein Mongoose-Document als dbObject → kein Konflikt.
150
+ - Services die `process()` direkt aufrufen mit `dbObject` als String (z.B. swaktiv/OfferService): Profitieren von der Optimierung.
151
+
152
+ ---
153
+
154
+ ## Aenderung 3: Depth-Tracking in RequestContext
155
+
156
+ ### Datei: `src/core/common/services/request-context.service.ts`
157
+
158
+ ### Problem
159
+
160
+ Bei Service-Kaskaden (A.create → B.create → C.create) laeuft die volle process()-Pipeline auf jeder Ebene. Populate, prepareOutput-Mapping und Output-checkRights auf inneren Ebenen sind unnoetig, weil die aeussere Ebene und der CheckSecurityInterceptor diese Aufgaben uebernehmen.
161
+
162
+ ### Aenderung am Interface
163
+
164
+ ```typescript
165
+ export interface IRequestContext {
166
+ // ... bestehende Felder ...
167
+
168
+ /**
169
+ * Tracks the nesting depth of process() calls.
170
+ * 0 = outermost call (full pipeline), > 0 = nested call (reduced pipeline).
171
+ * Used to skip redundant populate, output mapping, and output rights checks
172
+ * on inner calls — the outermost call and CheckSecurityInterceptor handle these.
173
+ */
174
+ processDepth?: number;
175
+ }
176
+ ```
177
+
178
+ ### Neue Methoden
179
+
180
+ ```typescript
181
+ /**
182
+ * Get the current process() nesting depth.
183
+ * Returns 0 if not inside a process() call.
184
+ */
185
+ static getProcessDepth(): number {
186
+ return this.storage.getStore()?.processDepth || 0;
187
+ }
188
+
189
+ /**
190
+ * Run a function with incremented process depth.
191
+ * Skips context creation if already at depth > 0 to avoid redundant object spread.
192
+ */
193
+ static runWithIncrementedProcessDepth<T>(fn: () => T): T {
194
+ const currentStore = this.storage.getStore();
195
+ const currentDepth = currentStore?.processDepth || 0;
196
+ const context: IRequestContext = {
197
+ ...currentStore,
198
+ processDepth: currentDepth + 1,
199
+ };
200
+ return this.storage.run(context, fn);
201
+ }
202
+ ```
203
+
204
+ ### Sicherheit
205
+
206
+ Nicht betroffen — das Depth-Tracking ist rein informativ und aendert keine Berechtigungen.
207
+
208
+ ---
209
+
210
+ ## Aenderung 4: process() — Depth-basierte Optimierung
211
+
212
+ ### Datei: `src/core/common/services/module.service.ts`
213
+
214
+ ### Aenderung in process() (Zeile 81ff)
215
+
216
+ Am Anfang der Methode nach der Config-Erstellung (nach Zeile 108):
217
+
218
+ ```typescript
219
+ // Detect nested process() calls
220
+ const currentDepth = RequestContext.getProcessDepth();
221
+ const isNested = currentDepth > 0;
222
+ ```
223
+
224
+ ### serviceFunc mit Depth-Tracking ausfuehren (Zeile 201-205 ersetzen)
225
+
226
+ ```typescript
227
+ // Run service function with incremented depth
228
+ // When force is enabled, also bypass the Mongoose role guard plugin
229
+ const executeServiceFunc = () =>
230
+ RequestContext.runWithIncrementedProcessDepth(() => serviceFunc(config));
231
+
232
+ let result = config.force
233
+ ? await RequestContext.runWithBypassRoleGuard(executeServiceFunc)
234
+ : await executeServiceFunc();
235
+ ```
236
+
237
+ ### processFieldSelection bei inneren Calls ueberspringen (Zeile 207-217 ersetzen)
238
+
239
+ ```typescript
240
+ // Pop and map main model
241
+ // Skip on nested calls UNLESS populate was explicitly requested —
242
+ // the outermost call handles population for the final response.
243
+ if (config.processFieldSelection && config.fieldSelection && this.processFieldSelection) {
244
+ if (!isNested || config.populate) {
245
+ let temps = result;
246
+ if (!Array.isArray(result)) {
247
+ temps = [result];
248
+ }
249
+ for (const temp of temps) {
250
+ const field = config.outputPath ? _.get(temp, config.outputPath) : temp;
251
+ await this.processFieldSelection(field, config.fieldSelection, config.processFieldSelection);
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ **Logik:**
258
+ - `isNested = false` (Depth 0, aeusserster Call): Populate laeuft immer → User bekommt vollstaendiges Ergebnis.
259
+ - `isNested = true` (Depth > 0, innerer Call) OHNE `config.populate`: Populate wird uebersprungen → Ergebnis wird intern weiterverarbeitet.
260
+ - `isNested = true` MIT `config.populate` (explizit vom Caller gesetzt): Populate laeuft → der innere Service hat das explizit angefordert weil er die Daten braucht.
261
+
262
+ ### prepareOutput bei inneren Calls reduzieren (Zeile 219-236 ersetzen)
263
+
264
+ ```typescript
265
+ // Prepare output
266
+ if (config.prepareOutput && this.prepareOutput) {
267
+ const opts = config.prepareOutput;
268
+ if (!opts.targetModel && config.outputType) {
269
+ opts.targetModel = config.outputType;
270
+ }
271
+
272
+ // On nested calls without explicit populate: skip model mapping
273
+ // (the outermost call and CheckSecurityInterceptor handle final mapping).
274
+ // Secret removal (removeSecrets) stays active at ALL depths.
275
+ if (isNested && !config.populate && typeof opts === 'object') {
276
+ opts.targetModel = undefined;
277
+ }
278
+
279
+ if (config.outputPath) {
280
+ let temps = result;
281
+ if (!Array.isArray(result)) {
282
+ temps = [result];
283
+ }
284
+ for (const temp of temps) {
285
+ _.set(temp, config.outputPath, await this.prepareOutput(_.get(temp, config.outputPath), opts));
286
+ }
287
+ } else {
288
+ result = await this.prepareOutput(result, config);
289
+ }
290
+ }
291
+ ```
292
+
293
+ ### Output-checkRights bei inneren Calls ueberspringen (Zeile 238-250 ersetzen)
294
+
295
+ ```typescript
296
+ // Check output rights
297
+ // Skip on nested calls — the outermost process() and CheckSecurityInterceptor
298
+ // perform the final output rights check on the complete response.
299
+ if (!isNested && config.checkRights && (await this.checkRights(undefined, config.currentUser as any, config))) {
300
+ const opts: any = {
301
+ dbObject: config.dbObject,
302
+ processType: ProcessType.OUTPUT,
303
+ roles: config.roles,
304
+ throwError: false,
305
+ };
306
+ if (config.outputType) {
307
+ opts.metatype = config.outputType;
308
+ }
309
+ result = await this.checkRights(result, config.currentUser as any, opts);
310
+ }
311
+ ```
312
+
313
+ ### Sicherheit
314
+
315
+ **Gewaehrleistet durch drei Schichten:**
316
+
317
+ 1. **Input-checkRights laeuft IMMER** (auch bei isNested) — unberechtigte Eingaben werden auf jeder Ebene abgefangen.
318
+ 2. **Output-checkRights auf Depth 0** — der aeusserste Call filtert das finale Ergebnis.
319
+ 3. **CheckSecurityInterceptor** — das letzte Sicherheitsnetz in der HTTP-Response-Pipeline ruft `securityCheck()` rekursiv auf dem gesamten Response-Baum auf, inklusive aller verschachtelten Objekte.
320
+
321
+ **Gegenprobe:** Was passiert wenn ein innerer Service ein Objekt zurueckgibt das der User nicht sehen darf?
322
+ - Der innere process() ueberspringt den Output-Rights-Check → Objekt bleibt unveraendert
323
+ - Der aeussere process() fuehrt den Output-Rights-Check durch → Felder werden entfernt
324
+ - Falls der aeussere process() es auch verpasst → CheckSecurityInterceptor entfernt die Felder
325
+
326
+ ### Kompatibilitaet
327
+
328
+ Geprueft an realen Kaskaden:
329
+ - **CompanyService.create()** (5-Ebenen-Kaskade): Innere creates (ProfileSite, ProfileCategory, ProfileEntry, Form) brauchen kein Populate — Ergebnisse werden nur fuer IDs weiterverwendet.
330
+ - **ParticipationService → UserService**: UserService.create() Ergebnis wird inline weiterverwendet, nicht an User zurueckgegeben.
331
+ - **ShippingService → BounceService/EmailService**: Kein Populate auf inneren Ebenen noetig.
332
+ - **Services mit explizitem `populate:`**: Funktionieren weiterhin, da `config.populate` den isNested-Skip ueberschreibt.
333
+
334
+ ---
335
+
336
+ ## Aenderung 5: checkRestricted Metadata-Caching
337
+
338
+ ### Datei: `src/core/common/decorators/restricted.decorator.ts`
339
+
340
+ ### Problem
341
+
342
+ `getRestricted()` (Zeile 50-58) ruft bei jedem Property-Check `Reflect.getMetadata()` auf. Bei einem Objekt mit 15 Properties und verschachtelten Objekten summieren sich hunderte Reflect-Lookups pro Request. Die Metadata aendert sich zur Laufzeit nie (Decorators sind statisch).
343
+
344
+ ### Aktueller Code (Zeile 50-58)
345
+
346
+ ```typescript
347
+ export const getRestricted = (object: unknown, propertyKey?: string): RestrictedType => {
348
+ if (!object) {
349
+ return null;
350
+ }
351
+ if (!propertyKey) {
352
+ return Reflect.getMetadata(restrictedMetaKey, object);
353
+ }
354
+ return Reflect.getMetadata(restrictedMetaKey, object, propertyKey);
355
+ };
356
+ ```
357
+
358
+ ### Neuer Code
359
+
360
+ ```typescript
361
+ // Cache for Restricted metadata — decorators are static, metadata never changes at runtime.
362
+ // Map<CacheTarget, Map<propertyKey | '__class__', RestrictedType>>
363
+ //
364
+ // CacheTarget is the class constructor (for instances) or the class itself (when object IS a constructor).
365
+ // This distinction is critical: getRestricted(data.constructor) passes a class as `object`,
366
+ // and (classFunction).constructor === Function for ALL classes — so we must use the class itself.
367
+ const restrictedMetadataCache = new Map<unknown, Map<string, RestrictedType>>();
368
+
369
+ export const getRestricted = (object: unknown, propertyKey?: string): RestrictedType => {
370
+ if (!object) {
371
+ return null;
372
+ }
373
+
374
+ // Determine cache target: use the class constructor for instances, the object itself for classes.
375
+ // When object IS a constructor (typeof === 'function'), using object.constructor would give Function
376
+ // for ALL classes, causing cache collisions.
377
+ const cacheTarget = typeof object === 'function' ? object : (object as any).constructor;
378
+ if (!cacheTarget) {
379
+ // Fallback for objects without constructor (e.g. Object.create(null))
380
+ return propertyKey
381
+ ? Reflect.getMetadata(restrictedMetaKey, object, propertyKey)
382
+ : Reflect.getMetadata(restrictedMetaKey, object);
383
+ }
384
+
385
+ // Cache lookup
386
+ let classCache = restrictedMetadataCache.get(cacheTarget);
387
+ if (!classCache) {
388
+ classCache = new Map();
389
+ restrictedMetadataCache.set(cacheTarget, classCache);
390
+ }
391
+
392
+ const cacheKey = propertyKey || '__class__';
393
+ if (classCache.has(cacheKey)) {
394
+ return classCache.get(cacheKey);
395
+ }
396
+
397
+ // Cache miss: perform Reflect lookup and cache the result
398
+ const metadata = propertyKey
399
+ ? Reflect.getMetadata(restrictedMetaKey, object, propertyKey)
400
+ : Reflect.getMetadata(restrictedMetaKey, object);
401
+
402
+ classCache.set(cacheKey, metadata);
403
+ return metadata;
404
+ };
405
+ ```
406
+
407
+ ### Zusaetzlich: _.uniq()-Optimierung in checkRestricted (Zeile 260)
408
+
409
+ ```typescript
410
+ // Aktuell:
411
+ const concatenatedRestrictions = config.mergeRoles ? _.uniq(objectRestrictions.concat(restricted)) : restricted;
412
+
413
+ // Optimiert — vermeidet Array-Allokation wenn objectRestrictions leer:
414
+ const concatenatedRestrictions = config.mergeRoles && objectRestrictions.length
415
+ ? _.uniq(objectRestrictions.concat(restricted))
416
+ : restricted;
417
+ ```
418
+
419
+ Diese Zeile wird fuer **jede Property jedes Objekts** aufgerufen. Wenn `objectRestrictions` leer ist (haeufig bei Models ohne Class-Level `@Restricted`), spart das eine Array-Allokation + uniq-Berechnung pro Property.
420
+
421
+ ### Sicherheit
422
+
423
+ **Nicht betroffen** — gleiche Ergebnisse, da Decorator-Metadata sich zur Laufzeit nicht aendert. Der Cache liefert exakt die gleichen Werte wie der direkte Reflect-Lookup.
424
+
425
+ ### Kompatibilitaet
426
+
427
+ Transparent — kein Projekt muss angepasst werden.
428
+
429
+ ---
430
+
431
+ ## Zusammenfassung der Einsparungen
432
+
433
+ ### Einzelner API-Call (Depth 0, kein Nesting):
434
+
435
+ | Aenderung | Einsparung |
436
+ |-----------|------------|
437
+ | JSON.stringify entfaellt | ~0.1-1 KB + 2x Serialisierung |
438
+ | Lean dbObject statt this.get() | ~5-15 KB + 1 rekursiver process()-Durchlauf |
439
+ | Metadata-Cache | CPU: ~hunderte Reflect-Lookups weniger |
440
+
441
+ ### Verschachtelter Call (Depth > 0):
442
+
443
+ | Schritt | Vorher | Nachher |
444
+ |---------|--------|---------|
445
+ | JSON.stringify | 2x Serialisierung | Uebersprungen |
446
+ | prepareInput | Laeuft | Laeuft (unveraendert) |
447
+ | this.get(dbObject) | Rekursives process() | Lean Query + Map |
448
+ | checkRights(input) | Laeuft | Laeuft (unveraendert) |
449
+ | serviceFunc | Laeuft | Laeuft (unveraendert) |
450
+ | processFieldSelection | Volle Population | Uebersprungen (ausser explizites populate) |
451
+ | prepareOutput | Volles Mapping | Nur Secrets-Removal |
452
+ | checkRights(output) | Laeuft | Uebersprungen |
453
+
454
+ **Pro verschachteltem Call: ~30-70 KB + 2-5 DB-Queries weniger.**
455
+
456
+ ### Beispiel: 8-stufige Service-Kaskade (z.B. Incident-Handling):
457
+
458
+ - Vorher: 8 x ~50 KB = ~400 KB
459
+ - Nachher: 1 x ~50 KB (aeusserer) + 7 x ~10 KB (innere) = ~120 KB
460
+ - **~70% weniger Memory**
461
+
462
+ ---
463
+
464
+ ## Betroffene Dateien
465
+
466
+ | Datei | Aenderung |
467
+ |-------|-----------|
468
+ | `src/core/common/services/module.service.ts` | Aenderungen 1, 2, 4 |
469
+ | `src/core/common/services/request-context.service.ts` | Aenderung 3 |
470
+ | `src/core/common/decorators/restricted.decorator.ts` | Aenderung 5 |
471
+ | `src/core/common/interfaces/server-options.interface.ts` | Config-Option `debugProcessInput` |
472
+
473
+ ---
474
+
475
+ ## Verifizierung
476
+
477
+ 1. **Bestehende E2E-Tests** muessen alle gruen bleiben — die Aenderungen sind rueckwaertskompatibel
478
+ 2. **Service-Kaskade testen**: z.B. ein create() das intern andere Services aufruft
479
+ 3. **Populate auf aeusserster Ebene**: `{ populate: ['user', 'customer'] }` muss weiterhin funktionieren
480
+ 4. **securityCheck()**: Sensitive Felder (password, etc.) duerfen nie in der Response auftauchen
481
+ 5. **S_CREATOR-Check**: User der ein Objekt erstellt hat, muss es weiterhin bearbeiten koennen
482
+ 6. **Performance**: `process.memoryUsage()` vor/nach Optimierung bei verschachtelten Calls messen
483
+
484
+ ---
485
+
486
+ ## Risiken
487
+
488
+ | Risiko | Absicherung |
489
+ |--------|-------------|
490
+ | Innerer Service braucht populate-Ergebnis | `config.populate` ueberschreibt den isNested-Skip — explizite Anforderung wird respektiert |
491
+ | Output-Rights-Check auf innerem Ergebnis fehlt | CheckSecurityInterceptor ist das Sicherheitsnetz auf HTTP-Ebene |
492
+ | Metadata-Cache wird stale | Unmoeglich — Decorator-Metadata ist zur Compile-Zeit fixiert |
493
+ | Lean dbObject hat andere Struktur als hydriertes | `modelConstructor.map()` stellt die erwartete Struktur her |