@rws-framework/db 3.7.3 → 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,48 +104,49 @@ 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)) {
145
- data[key] = this.bindRelation(key, this[key]);
146
- if (data[key] === null) {
147
- const relationKey = await this.getRelationKey(key);
148
- if (relationKey) {
149
- data[relationKey] = null;
150
- delete data[key];
151
- }
140
+ if (this[key] === null) {
141
+ // For null relations, use disconnect or set to null
142
+ data[key] = {
143
+ disconnect: true
144
+ };
145
+ }
146
+ else {
147
+ data[key] = this.bindRelation(key, this[key]);
152
148
  }
149
+ // Don't try to set the foreign key directly anymore
153
150
  continue;
154
151
  }
155
152
  if (!(await this.isDbVariable(key))) {
@@ -178,10 +175,10 @@ class RWSModel {
178
175
  async save() {
179
176
  const data = await this.toMongo();
180
177
  let updatedModelData = data;
181
- const entryExists = await ModelUtils_1.ModelUtils.entryExists(this);
178
+ const entryExists = await utils_1.ModelUtils.entryExists(this);
182
179
  if (entryExists) {
183
180
  await this.preUpdate();
184
- const pk = ModelUtils_1.ModelUtils.findPrimaryKeyFields(this.constructor);
181
+ const pk = utils_1.ModelUtils.findPrimaryKeyFields(this.constructor);
185
182
  updatedModelData = await this.dbService.update(data, this.getCollection(), pk);
186
183
  await this._asyncFill(updatedModelData);
187
184
  await this.postUpdate();
@@ -196,12 +193,13 @@ class RWSModel {
196
193
  return this;
197
194
  }
198
195
  static async getModelAnnotations(constructor) {
199
- return ModelUtils_1.ModelUtils.getModelAnnotations(constructor);
196
+ return utils_1.ModelUtils.getModelAnnotations(constructor);
200
197
  }
201
198
  async preUpdate() {
202
199
  return;
203
200
  }
204
201
  async postLoad() {
202
+ this.setPostLoadExecuted();
205
203
  return;
206
204
  }
207
205
  async postUpdate() {
@@ -214,19 +212,19 @@ class RWSModel {
214
212
  return;
215
213
  }
216
214
  static isSubclass(constructor, baseClass) {
217
- return ModelUtils_1.ModelUtils.isSubclass(constructor, baseClass);
215
+ return utils_1.ModelUtils.isSubclass(constructor, baseClass);
218
216
  }
219
217
  hasTimeSeries() {
220
- return TimeSeriesUtils_1.TimeSeriesUtils.checkTimeSeries(this.constructor);
218
+ return utils_1.TimeSeriesUtils.checkTimeSeries(this.constructor);
221
219
  }
222
220
  static checkTimeSeries(constructor) {
223
- return TimeSeriesUtils_1.TimeSeriesUtils.checkTimeSeries(constructor);
221
+ return utils_1.TimeSeriesUtils.checkTimeSeries(constructor);
224
222
  }
225
223
  async isDbVariable(variable) {
226
- return ModelUtils_1.ModelUtils.checkDbVariable(this.constructor, variable);
224
+ return utils_1.ModelUtils.checkDbVariable(this.constructor, variable);
227
225
  }
228
226
  static async checkDbVariable(constructor, variable) {
229
- return ModelUtils_1.ModelUtils.checkDbVariable(constructor, variable);
227
+ return utils_1.ModelUtils.checkDbVariable(constructor, variable);
230
228
  }
231
229
  sanitizeDBData(data) {
232
230
  const dataKeys = Object.keys(data);
@@ -244,13 +242,13 @@ class RWSModel {
244
242
  return await this.services.dbService.watchCollection(collection, preRun);
245
243
  }
246
244
  static async findOneBy(findParams) {
247
- return await FindUtils_1.FindUtils.findOneBy(this, findParams);
245
+ return await utils_1.FindUtils.findOneBy(this, findParams);
248
246
  }
249
247
  static async find(id, findParams = null) {
250
- return await FindUtils_1.FindUtils.find(this, id, findParams);
248
+ return await utils_1.FindUtils.find(this, id, findParams);
251
249
  }
252
250
  static async findBy(findParams) {
253
- return await FindUtils_1.FindUtils.findBy(this, findParams);
251
+ return await utils_1.FindUtils.findBy(this, findParams);
254
252
  }
255
253
  static async delete(conditions) {
256
254
  const collection = Reflect.get(this, '_collection');
@@ -277,7 +275,7 @@ class RWSModel {
277
275
  return RWSModel.loadModels();
278
276
  }
279
277
  checkRelDisabled(key) {
280
- return RelationUtils_1.RelationUtils.checkRelDisabled(this, key);
278
+ return utils_1.RelationUtils.checkRelDisabled(this, key);
281
279
  }
282
280
  static setServices(services) {
283
281
  this.allModels = services.configService.get('db_models');
@@ -292,8 +290,8 @@ class RWSModel {
292
290
  static getDb() {
293
291
  return this.services.dbService;
294
292
  }
295
- async reload() {
296
- const pk = ModelUtils_1.ModelUtils.findPrimaryKeyFields(this.constructor);
293
+ async reload(inPostLoad = false) {
294
+ const pk = utils_1.ModelUtils.findPrimaryKeyFields(this.constructor);
297
295
  const where = {};
298
296
  if (Array.isArray(pk)) {
299
297
  for (const pkElem of pk) {
@@ -303,7 +301,7 @@ class RWSModel {
303
301
  else {
304
302
  where[pk] = this[pk];
305
303
  }
306
- return await FindUtils_1.FindUtils.findOneBy(this.constructor, { conditions: where });
304
+ return await utils_1.FindUtils.findOneBy(this.constructor, { conditions: where, cancelPostLoad: inPostLoad });
307
305
  }
308
306
  }
309
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>;