@rws-framework/db 3.8.0 → 3.9.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.
@@ -0,0 +1,139 @@
1
+ # LoadingContext Implementation - Solution 1 Documentation (Updated)
2
+
3
+ ## Problem Statement
4
+
5
+ The RWS framework had two related issues in the postLoad mechanism:
6
+
7
+ ### 1. Original Circular Reference Issue
8
+ - **User model postLoad** calls `userGroup.reload(true)`
9
+ - **UserGroup model postLoad** calls `User.find(owner, { cancelPostLoad: true })`
10
+ - If that User gets loaded elsewhere without `cancelPostLoad: true`, it could trigger an infinite cycle
11
+
12
+ ### 2. Cross-Request Stack Persistence Issue (Discovered)
13
+ - The LoadingContext had a **global loading stack** that persisted across HTTP requests
14
+ - Stack built up and never got cleared between requests
15
+ - Caused false positive "already being loaded" errors for subsequent requests
16
+ - Led to models not loading properly and authentication failures
17
+
18
+ ## Solution: Request-Scoped Loading Context
19
+
20
+ ### Key Fix: Execution Context Isolation
21
+
22
+ The critical fix was changing from a **global static loading stack** to **per-execution-context stacks**:
23
+
24
+ **Before (Problematic):**
25
+ ```typescript
26
+ class LoadingContext {
27
+ private static loadingStack: Set<string> = new Set(); // GLOBAL - persisted across requests
28
+ }
29
+ ```
30
+
31
+ **After (Fixed):**
32
+ ```typescript
33
+ class LoadingContext {
34
+ private static executionContexts = new WeakMap<object, Set<string>>(); // Per-context isolation
35
+
36
+ static withNewExecutionContext<T>(fn: () => Promise<T>): Promise<T> {
37
+ // Creates fresh context for each operation
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### Implementation Components
43
+
44
+ #### 1. LoadingContext Class (`/models/utils/LoadingContext.ts`) - Updated
45
+
46
+ **Key Changes:**
47
+ - **Execution Context Isolation**: Each operation gets its own loading stack
48
+ - **WeakMap for Context Storage**: Associates loading stacks with execution contexts
49
+ - **Automatic Context Creation**: `withNewExecutionContext()` creates fresh contexts
50
+ - **Per-Request Isolation**: Each HTTP request gets its own clean loading context
51
+
52
+ #### 2. FindUtils Integration (`/models/utils/LoadingUtils.ts`) - Critical Fix
53
+
54
+ **All top-level methods now wrap operations in new execution contexts:**
55
+
56
+ ```typescript
57
+ public static async findOneBy<T extends RWSModel<T>>(
58
+ opModel: OpModelType<T>,
59
+ findParams?: FindByType
60
+ ): Promise<T | null> {
61
+ // CRITICAL: Wrap in new execution context to ensure clean loading stack
62
+ return LoadingContext.withNewExecutionContext(async () => {
63
+ // ... existing logic with circular reference protection
64
+ });
65
+ }
66
+ ```
67
+
68
+ ### How The Fix Works
69
+
70
+ #### Before Fix (Problematic):
71
+ ```
72
+ Request 1: User.find(11) -> adds "User:11" to global stack
73
+ Request 1: UserGroup.reload() -> adds "UserGroup:10" to global stack
74
+ Request 1: Completes, but stack NOT cleared
75
+ Request 2: User.find(11) -> "User:11" already in global stack -> FALSE POSITIVE error
76
+ ```
77
+
78
+ #### After Fix (Working):
79
+ ```
80
+ Request 1: LoadingContext.withNewExecutionContext() -> creates fresh context
81
+ Request 1: User.find(11) -> adds "User:11" to context-specific stack
82
+ Request 1: UserGroup.reload() -> adds "UserGroup:10" to same context stack
83
+ Request 1: Completes -> context automatically cleaned up
84
+
85
+ Request 2: LoadingContext.withNewExecutionContext() -> creates NEW fresh context
86
+ Request 2: User.find(11) -> adds "User:11" to NEW context stack -> works normally
87
+ ```
88
+
89
+ ### Request Isolation Benefits
90
+
91
+ 1. **No Cross-Request Interference**: Each HTTP request gets its own loading context
92
+ 2. **Automatic Cleanup**: No manual stack management needed
93
+ 3. **Proper Circular Detection**: Still prevents infinite loops within single operations
94
+ 4. **No False Positives**: Eliminates "already loading" errors between requests
95
+
96
+ ### Configuration
97
+
98
+ ```typescript
99
+ // Set maximum loading depth for new contexts (default: 10)
100
+ LoadingContext.setDefaultMaxDepth(15);
101
+
102
+ // Get current loading stack for debugging current context
103
+ console.log(LoadingContext.getLoadingStack());
104
+
105
+ // Manual context creation (usually not needed)
106
+ await LoadingContext.withNewExecutionContext(async () => {
107
+ // Database operations here get fresh context
108
+ });
109
+ ```
110
+
111
+ ### Testing Results
112
+
113
+ **Before Fix - Error Logs:**
114
+ ```
115
+ Circular reference detected: User:11 is already being loaded. Breaking cycle.
116
+ UnauthorizedException: Unauthorized
117
+ Circular reference detected: User:11 is already being loaded. Breaking cycle.
118
+ Circular reference detected: UserGroup:10 is already being loaded. Breaking cycle.
119
+ Circular reference detected: UserGroup:10 is already being loaded. Breaking cycle.
120
+ // Repeated across multiple requests - FALSE POSITIVES
121
+ ```
122
+
123
+ **After Fix:**
124
+ - Clean loading per HTTP request
125
+ - No false positive circular reference detections
126
+ - Proper circular reference protection within individual operations
127
+ - No authentication failures due to loading issues
128
+ - No cross-request interference
129
+
130
+ ### Backward Compatibility
131
+
132
+ - **100% backward compatible**: No changes needed to existing model code
133
+ - **Existing `cancelPostLoad` still works**: Provides additional layer of protection
134
+ - **All existing APIs preserved**: No breaking changes to any interfaces
135
+ - **Automatic context management**: Developers don't need to manage contexts manually
136
+
137
+ ### Summary
138
+
139
+ This fix resolves both the original circular reference issue AND the newly discovered cross-request stack persistence problem. The key insight was that the loading context needs to be **request-scoped** rather than **application-global**. Each database operation chain now gets its own clean loading context, preventing both infinite recursion within operations and false positive detection across operations.
@@ -0,0 +1,122 @@
1
+ # LoadingContext Implementation - Solution 1 Documentation
2
+
3
+ ## Problem Statement
4
+
5
+ The RWS framework had a potential infinite recursion issue in the postLoad mechanism where:
6
+
7
+ 1. **User model postLoad** calls `userGroup.reload(true)`
8
+ 2. **UserGroup model postLoad** calls `User.find(owner, { cancelPostLoad: true })`
9
+ 3. If that User gets loaded elsewhere without `cancelPostLoad: true`, it could trigger an infinite cycle
10
+
11
+ The existing anti-recursion mechanism was **per-instance** rather than **per-loading-context**, meaning it only prevented the same object from running postLoad multiple times, but didn't prevent circular loading between different instances of related models.
12
+
13
+ ## Solution: Context-Based Loading Stack
14
+
15
+ ### Implementation Components
16
+
17
+ #### 1. LoadingContext Class (`/models/utils/LoadingContext.ts`)
18
+
19
+ A utility class that tracks the chain of models currently being loaded to prevent circular references:
20
+
21
+ **Key Features:**
22
+ - **Static loading stack**: Tracks `ModelType:ID` combinations currently being loaded
23
+ - **Circular reference detection**: Prevents loading a model that's already in the loading chain
24
+ - **Maximum depth protection**: Prevents infinite chains with configurable depth limit
25
+ - **Automatic cleanup**: Uses try/finally blocks to ensure proper cleanup
26
+ - **Debugging support**: Provides stack inspection and clear error messages
27
+
28
+ **Key Methods:**
29
+ - `isLoading(modelType, id)`: Check if a model is currently being loaded
30
+ - `startLoading(modelType, id)`: Mark a model as being loaded
31
+ - `finishLoading(modelType, id)`: Mark a model as finished loading
32
+ - `withLoadingContext(modelType, id, fn)`: Execute function with automatic context management
33
+
34
+ #### 2. FindUtils Integration (`/models/utils/FindUtils.ts`)
35
+
36
+ Modified all find methods to use LoadingContext:
37
+
38
+ **Changes in `find()`, `findOneBy()`, `findBy()`, and `paginate()`:**
39
+ - Check if model is already being loaded before creating instance
40
+ - Use `LoadingContext.withLoadingContext()` to wrap model loading
41
+ - Return `null` or skip items that are already being loaded
42
+ - Provide warning messages for detected circular references
43
+
44
+ #### 3. RWSModel Integration (`/models/core/RWSModel.ts`)
45
+
46
+ - Added LoadingContext import
47
+ - The reload method now uses FindUtils which automatically gets the protection
48
+
49
+ ### How It Works
50
+
51
+ #### Normal Loading Flow
52
+ ```
53
+ 1. User.find(1) -> LoadingContext.startLoading('User', 1)
54
+ 2. User._asyncFill() -> loads userGroup relation
55
+ 3. UserGroup.reload() -> LoadingContext.startLoading('UserGroup', 2)
56
+ 4. UserGroup._asyncFill() -> loads owner relation
57
+ 5. User.find(owner) -> LoadingContext.startLoading('User', 3)
58
+ 6. Complete normally -> LoadingContext.finishLoading() for each
59
+ ```
60
+
61
+ #### Circular Reference Prevention
62
+ ```
63
+ 1. User.find(1) -> LoadingContext.startLoading('User', 1)
64
+ 2. User._asyncFill() -> loads userGroup relation
65
+ 3. UserGroup.reload() -> LoadingContext.startLoading('UserGroup', 2)
66
+ 4. UserGroup._asyncFill() -> tries to load User.find(1) again
67
+ 5. LoadingContext.isLoading('User', 1) returns true
68
+ 6. Returns null instead of creating infinite loop
69
+ 7. Warning logged: "Circular reference detected"
70
+ ```
71
+
72
+ #### Maximum Depth Protection
73
+ ```
74
+ If loading chain exceeds configurable limit (default 10):
75
+ - Throws descriptive error with full loading stack
76
+ - Shows exact chain: "Model1:1 -> Model2:2 -> Model3:3 -> ..."
77
+ - Helps identify complex circular dependencies
78
+ ```
79
+
80
+ ### Benefits
81
+
82
+ 1. **Complete Circular Reference Prevention**: Unlike the previous per-instance flag, this prevents all types of circular loading
83
+ 2. **Transparent Integration**: Existing code doesn't need changes - works automatically through FindUtils
84
+ 3. **Performance Optimized**: Minimal overhead - just Set operations for tracking
85
+ 4. **Debugging Friendly**: Clear error messages and stack inspection capabilities
86
+ 5. **Configurable**: Adjustable maximum depth and behavior
87
+ 6. **Fail-Safe**: Always cleans up loading stack even if errors occur
88
+
89
+ ### Configuration Options
90
+
91
+ ```typescript
92
+ // Set maximum loading depth (default: 10)
93
+ LoadingContext.setMaxDepth(15);
94
+
95
+ // Get current loading stack for debugging
96
+ console.log(LoadingContext.getLoadingStack());
97
+
98
+ // Clear stack in emergency situations
99
+ LoadingContext.clearStack();
100
+ ```
101
+
102
+ ### Backward Compatibility
103
+
104
+ - **100% backward compatible**: No changes needed to existing model code
105
+ - **Existing `cancelPostLoad` still works**: Provides additional layer of protection
106
+ - **All existing APIs preserved**: No breaking changes to FindUtils or RWSModel
107
+
108
+ ### Testing
109
+
110
+ A test file (`LoadingContext.test.ts`) demonstrates:
111
+ - Basic loading detection
112
+ - Circular reference prevention
113
+ - Maximum depth protection
114
+ - Automatic cleanup
115
+
116
+ ### Error Handling
117
+
118
+ The implementation provides clear error messages:
119
+ - **Circular Reference**: "Circular reference detected: User:1 is already being loaded. Breaking cycle."
120
+ - **Maximum Depth**: "Maximum loading depth (10) exceeded. Possible circular reference detected. Loading stack: User:1 -> UserGroup:2 -> ..."
121
+
122
+ This solution provides robust protection against infinite recursion while maintaining full backward compatibility and excellent debugging capabilities.
@@ -73,6 +73,6 @@ declare class RWSModel<T> implements IModel {
73
73
  [k: string]: any;
74
74
  }): Promise<number>;
75
75
  static getDb(): DBService;
76
- reload(): Promise<RWSModel<T> | null>;
76
+ reload(inPostLoad?: boolean): Promise<RWSModel<T> | null>;
77
77
  }
78
78
  export { RWSModel };
@@ -12,11 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.RWSModel = void 0;
13
13
  const decorators_1 = require("../../decorators");
14
14
  const FieldsHelper_1 = require("../../helper/FieldsHelper");
15
- const RelationUtils_1 = require("../utils/RelationUtils");
16
- const TimeSeriesUtils_1 = require("../utils/TimeSeriesUtils");
17
- const ModelUtils_1 = require("../utils/ModelUtils");
18
- const HydrateUtils_1 = require("../utils/HydrateUtils");
19
- const FindUtils_1 = require("../utils/FindUtils");
15
+ const utils_1 = require("../utils");
20
16
  class RWSModel {
21
17
  static services = {};
22
18
  id;
@@ -87,13 +83,13 @@ class RWSModel {
87
83
  return this;
88
84
  }
89
85
  async hasRelation(key) {
90
- return RelationUtils_1.RelationUtils.hasRelation(this.constructor, key);
86
+ return utils_1.RelationUtils.hasRelation(this.constructor, key);
91
87
  }
92
88
  async getRelationKey(key) {
93
- return RelationUtils_1.RelationUtils.getRelationKey(this.constructor, key);
89
+ return utils_1.RelationUtils.getRelationKey(this.constructor, key);
94
90
  }
95
91
  bindRelation(key, relatedModel) {
96
- return RelationUtils_1.RelationUtils.bindRelation(relatedModel);
92
+ return utils_1.RelationUtils.bindRelation(relatedModel);
97
93
  }
98
94
  async _asyncFill(data, fullDataMode = false, allowRelations = true, postLoadExecute = true) {
99
95
  const collections_to_models = {};
@@ -108,37 +104,36 @@ class RWSModel {
108
104
  });
109
105
  const seriesHydrationfields = [];
110
106
  if (allowRelations) {
111
- await HydrateUtils_1.HydrateUtils.hydrateRelations(this, relManyData, relOneData, seriesHydrationfields, fullDataMode, data);
107
+ await utils_1.HydrateUtils.hydrateRelations(this, relManyData, relOneData, seriesHydrationfields, fullDataMode, data);
112
108
  }
113
109
  // Process regular fields and time series
114
- await HydrateUtils_1.HydrateUtils.hydrateDataFields(this, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data);
110
+ await utils_1.HydrateUtils.hydrateDataFields(this, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data);
115
111
  if (!this.isPostLoadExecuted() && postLoadExecute) {
116
112
  await this.postLoad();
117
- this.setPostLoadExecuted();
118
113
  }
119
114
  return this;
120
115
  }
121
116
  getModelScalarFields(model) {
122
- return ModelUtils_1.ModelUtils.getModelScalarFields(model);
117
+ return utils_1.ModelUtils.getModelScalarFields(model);
123
118
  }
124
119
  async getRelationOneMeta(classFields) {
125
- return RelationUtils_1.RelationUtils.getRelationOneMeta(this, classFields);
120
+ return utils_1.RelationUtils.getRelationOneMeta(this, classFields);
126
121
  }
127
122
  static async getRelationOneMeta(model, classFields) {
128
- return RelationUtils_1.RelationUtils.getRelationOneMeta(model, classFields);
123
+ return utils_1.RelationUtils.getRelationOneMeta(model, classFields);
129
124
  }
130
125
  async getRelationManyMeta(classFields) {
131
- return RelationUtils_1.RelationUtils.getRelationManyMeta(this, classFields);
126
+ return utils_1.RelationUtils.getRelationManyMeta(this, classFields);
132
127
  }
133
128
  static async getRelationManyMeta(model, classFields) {
134
- return RelationUtils_1.RelationUtils.getRelationManyMeta(model, classFields);
129
+ return utils_1.RelationUtils.getRelationManyMeta(model, classFields);
135
130
  }
136
131
  static async paginate(paginateParams, findParams) {
137
- return await FindUtils_1.FindUtils.paginate(this, paginateParams, findParams);
132
+ return await utils_1.FindUtils.paginate(this, paginateParams, findParams);
138
133
  }
139
134
  async toMongo() {
140
135
  const data = {};
141
- const timeSeriesIds = TimeSeriesUtils_1.TimeSeriesUtils.getTimeSeriesModelFields(this);
136
+ const timeSeriesIds = utils_1.TimeSeriesUtils.getTimeSeriesModelFields(this);
142
137
  const timeSeriesHydrationFields = [];
143
138
  for (const key in this) {
144
139
  if (await this.hasRelation(key)) {
@@ -180,10 +175,10 @@ class RWSModel {
180
175
  async save() {
181
176
  const data = await this.toMongo();
182
177
  let updatedModelData = data;
183
- const entryExists = await ModelUtils_1.ModelUtils.entryExists(this);
178
+ const entryExists = await utils_1.ModelUtils.entryExists(this);
184
179
  if (entryExists) {
185
180
  await this.preUpdate();
186
- const pk = ModelUtils_1.ModelUtils.findPrimaryKeyFields(this.constructor);
181
+ const pk = utils_1.ModelUtils.findPrimaryKeyFields(this.constructor);
187
182
  updatedModelData = await this.dbService.update(data, this.getCollection(), pk);
188
183
  await this._asyncFill(updatedModelData);
189
184
  await this.postUpdate();
@@ -198,12 +193,13 @@ class RWSModel {
198
193
  return this;
199
194
  }
200
195
  static async getModelAnnotations(constructor) {
201
- return ModelUtils_1.ModelUtils.getModelAnnotations(constructor);
196
+ return utils_1.ModelUtils.getModelAnnotations(constructor);
202
197
  }
203
198
  async preUpdate() {
204
199
  return;
205
200
  }
206
201
  async postLoad() {
202
+ this.setPostLoadExecuted();
207
203
  return;
208
204
  }
209
205
  async postUpdate() {
@@ -216,19 +212,19 @@ class RWSModel {
216
212
  return;
217
213
  }
218
214
  static isSubclass(constructor, baseClass) {
219
- return ModelUtils_1.ModelUtils.isSubclass(constructor, baseClass);
215
+ return utils_1.ModelUtils.isSubclass(constructor, baseClass);
220
216
  }
221
217
  hasTimeSeries() {
222
- return TimeSeriesUtils_1.TimeSeriesUtils.checkTimeSeries(this.constructor);
218
+ return utils_1.TimeSeriesUtils.checkTimeSeries(this.constructor);
223
219
  }
224
220
  static checkTimeSeries(constructor) {
225
- return TimeSeriesUtils_1.TimeSeriesUtils.checkTimeSeries(constructor);
221
+ return utils_1.TimeSeriesUtils.checkTimeSeries(constructor);
226
222
  }
227
223
  async isDbVariable(variable) {
228
- return ModelUtils_1.ModelUtils.checkDbVariable(this.constructor, variable);
224
+ return utils_1.ModelUtils.checkDbVariable(this.constructor, variable);
229
225
  }
230
226
  static async checkDbVariable(constructor, variable) {
231
- return ModelUtils_1.ModelUtils.checkDbVariable(constructor, variable);
227
+ return utils_1.ModelUtils.checkDbVariable(constructor, variable);
232
228
  }
233
229
  sanitizeDBData(data) {
234
230
  const dataKeys = Object.keys(data);
@@ -246,13 +242,13 @@ class RWSModel {
246
242
  return await this.services.dbService.watchCollection(collection, preRun);
247
243
  }
248
244
  static async findOneBy(findParams) {
249
- return await FindUtils_1.FindUtils.findOneBy(this, findParams);
245
+ return await utils_1.FindUtils.findOneBy(this, findParams);
250
246
  }
251
247
  static async find(id, findParams = null) {
252
- return await FindUtils_1.FindUtils.find(this, id, findParams);
248
+ return await utils_1.FindUtils.find(this, id, findParams);
253
249
  }
254
250
  static async findBy(findParams) {
255
- return await FindUtils_1.FindUtils.findBy(this, findParams);
251
+ return await utils_1.FindUtils.findBy(this, findParams);
256
252
  }
257
253
  static async delete(conditions) {
258
254
  const collection = Reflect.get(this, '_collection');
@@ -279,7 +275,7 @@ class RWSModel {
279
275
  return RWSModel.loadModels();
280
276
  }
281
277
  checkRelDisabled(key) {
282
- return RelationUtils_1.RelationUtils.checkRelDisabled(this, key);
278
+ return utils_1.RelationUtils.checkRelDisabled(this, key);
283
279
  }
284
280
  static setServices(services) {
285
281
  this.allModels = services.configService.get('db_models');
@@ -294,8 +290,8 @@ class RWSModel {
294
290
  static getDb() {
295
291
  return this.services.dbService;
296
292
  }
297
- async reload() {
298
- const pk = ModelUtils_1.ModelUtils.findPrimaryKeyFields(this.constructor);
293
+ async reload(inPostLoad = false) {
294
+ const pk = utils_1.ModelUtils.findPrimaryKeyFields(this.constructor);
299
295
  const where = {};
300
296
  if (Array.isArray(pk)) {
301
297
  for (const pkElem of pk) {
@@ -305,7 +301,7 @@ class RWSModel {
305
301
  else {
306
302
  where[pk] = this[pk];
307
303
  }
308
- return await FindUtils_1.FindUtils.findOneBy(this.constructor, { conditions: where });
304
+ return await utils_1.FindUtils.findOneBy(this.constructor, { conditions: where, cancelPostLoad: inPostLoad });
309
305
  }
310
306
  }
311
307
  exports.RWSModel = RWSModel;
@@ -9,7 +9,7 @@ export interface IModel {
9
9
  save: () => Promise<this>;
10
10
  getDb: () => DBService;
11
11
  getCollection: () => string | null;
12
- reload: () => Promise<RWSModel<any>>;
12
+ reload: (inPostLoad: boolean) => Promise<RWSModel<any>>;
13
13
  delete: () => Promise<void>;
14
14
  hasTimeSeries: () => boolean;
15
15
  _asyncFill: (data: any, fullDataMode?: boolean, allowRelations?: boolean) => Promise<any>;