@rws-framework/db 3.9.0 → 3.9.3
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/dist/models/core/RWSModel.d.ts +1 -1
- package/dist/models/core/RWSModel.js +33 -29
- package/dist/models/interfaces/IModel.d.ts +1 -1
- package/dist/models/utils/FindUtils.js +70 -131
- package/package.json +1 -1
- package/src/models/core/RWSModel.ts +13 -15
- package/src/models/interfaces/IModel.ts +1 -1
- package/src/models/utils/FindUtils.ts +84 -150
- package/LOADING_CONTEXT_FIX.md +0 -139
- package/LOADING_CONTEXT_IMPLEMENTATION.md +0 -122
- package/dist/models/utils/LoadingContext.d.ts +0 -56
- package/dist/models/utils/LoadingContext.js +0 -119
- package/dist/models/utils/LoadingContext.test.d.ts +0 -5
- package/dist/models/utils/LoadingContext.test.js +0 -51
- package/dist/models/utils/index.d.ts +0 -7
- package/dist/models/utils/index.js +0 -17
- package/src/models/utils/LoadingContext.ts +0 -140
- package/src/models/utils/index.ts +0 -7
|
@@ -6,50 +6,32 @@ import { RelationUtils } from "./RelationUtils";
|
|
|
6
6
|
import { OpModelType } from "..";
|
|
7
7
|
import { ModelUtils } from "./ModelUtils";
|
|
8
8
|
import { FindByType, IPaginationParams } from "../../types/FindParams";
|
|
9
|
-
import { LoadingContext } from "./LoadingContext";
|
|
10
|
-
import chalk from 'chalk';
|
|
11
|
-
|
|
12
|
-
function circularReferenceWarning(modelType: string, id: string | number): void {
|
|
13
|
-
console.warn(chalk.yellow(`Circular reference detected: ${modelType}:${id} is already being loaded. Breaking cycle.`));
|
|
14
|
-
}
|
|
15
9
|
|
|
16
10
|
export class FindUtils {
|
|
17
11
|
public static async findOneBy<T extends RWSModel<T>>(
|
|
18
12
|
opModel: OpModelType<T>,
|
|
19
13
|
findParams?: FindByType
|
|
20
14
|
): Promise<T | null> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
opModel.checkForInclusionWithThrow('');
|
|
30
|
-
|
|
31
|
-
const collection = Reflect.get(opModel, '_collection');
|
|
32
|
-
const dbData = await opModel.services.dbService.findOneBy(collection, conditions, fields, ordering);
|
|
33
|
-
|
|
34
|
-
if (dbData) {
|
|
35
|
-
const modelType = opModel.name;
|
|
36
|
-
const id = dbData.id;
|
|
37
|
-
|
|
38
|
-
// Check if this model is already being loaded to prevent circular references
|
|
39
|
-
if (LoadingContext.isLoading(modelType, id)) {
|
|
40
|
-
circularReferenceWarning(modelType, id);
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
15
|
+
const conditions = findParams?.conditions ?? {};
|
|
16
|
+
const ordering = findParams?.ordering ?? null;
|
|
17
|
+
const fields = findParams?.fields ?? null;
|
|
18
|
+
const allowRelations = findParams?.allowRelations ?? true;
|
|
19
|
+
const fullData = findParams?.fullData ?? false;
|
|
20
|
+
|
|
21
|
+
opModel.checkForInclusionWithThrow('');
|
|
43
22
|
|
|
44
|
-
return await LoadingContext.withLoadingContext(modelType, id, async () => {
|
|
45
|
-
const inst: T = new (opModel as { new(): T })();
|
|
46
|
-
const loaded = await inst._asyncFill(dbData, fullData, allowRelations, findParams?.cancelPostLoad ? false : true);
|
|
47
|
-
return loaded as T;
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
23
|
|
|
51
|
-
|
|
52
|
-
|
|
24
|
+
const collection = Reflect.get(opModel, '_collection');
|
|
25
|
+
const dbData = await opModel.services.dbService.findOneBy(collection, conditions, fields, ordering);
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if (dbData) {
|
|
29
|
+
const inst: T = new (opModel as { new(): T })();
|
|
30
|
+
const loaded = await inst._asyncFill(dbData, fullData, allowRelations, findParams?.cancelPostLoad ? false : true);
|
|
31
|
+
return loaded as T;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
53
35
|
}
|
|
54
36
|
|
|
55
37
|
public static async find<T extends RWSModel<T>>(
|
|
@@ -57,89 +39,59 @@ export class FindUtils {
|
|
|
57
39
|
id: string | number,
|
|
58
40
|
findParams: Omit<FindByType, 'conditions'> = null
|
|
59
41
|
): Promise<T | null> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const allowRelations = findParams?.allowRelations ?? true;
|
|
65
|
-
const fullData = findParams?.fullData ?? false;
|
|
66
|
-
|
|
67
|
-
const collection = Reflect.get(opModel, '_collection');
|
|
68
|
-
opModel.checkForInclusionWithThrow(opModel.name);
|
|
69
|
-
|
|
70
|
-
const dbData = await opModel.services.dbService.findOneBy(collection, { id }, fields, ordering);
|
|
71
|
-
|
|
72
|
-
if (dbData) {
|
|
73
|
-
const modelType = opModel.name;
|
|
74
|
-
|
|
75
|
-
// Check if this model is already being loaded to prevent circular references
|
|
76
|
-
if (LoadingContext.isLoading(modelType, id)) {
|
|
77
|
-
circularReferenceWarning(modelType, id);
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
42
|
+
const ordering = findParams?.ordering ?? null;
|
|
43
|
+
const fields = findParams?.fields ?? null;
|
|
44
|
+
const allowRelations = findParams?.allowRelations ?? true;
|
|
45
|
+
const fullData = findParams?.fullData ?? false;
|
|
80
46
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
47
|
+
const collection = Reflect.get(opModel, '_collection');
|
|
48
|
+
opModel.checkForInclusionWithThrow(opModel.name);
|
|
49
|
+
|
|
50
|
+
const dbData = await opModel.services.dbService.findOneBy(collection, { id }, fields, ordering);
|
|
51
|
+
|
|
52
|
+
if (dbData) {
|
|
53
|
+
const inst: T = new (opModel as { new(): T })();
|
|
54
|
+
const loaded = await inst._asyncFill(dbData, fullData, allowRelations, findParams?.cancelPostLoad ? false : true);
|
|
55
|
+
return loaded as T;
|
|
56
|
+
}
|
|
87
57
|
|
|
88
|
-
|
|
89
|
-
});
|
|
58
|
+
return null;
|
|
90
59
|
}
|
|
91
60
|
|
|
92
61
|
public static async findBy<T extends RWSModel<T>>(
|
|
93
62
|
opModel: OpModelType<T>,
|
|
94
63
|
findParams?: FindByType
|
|
95
64
|
): Promise<T[]> {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
for (const data of dbData) {
|
|
114
|
-
const modelType = opModel.name;
|
|
115
|
-
const id = data.id;
|
|
116
|
-
|
|
117
|
-
// Check if this model is already being loaded to prevent circular references
|
|
118
|
-
if (LoadingContext.isLoading(modelType, id)) {
|
|
119
|
-
circularReferenceWarning(modelType, id);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const loaded = await LoadingContext.withLoadingContext(modelType, id, async () => {
|
|
124
|
-
const inst: T = new (opModel as { new(): T })();
|
|
125
|
-
return await inst._asyncFill(data, fullData, allowRelations, findParams?.cancelPostLoad ? false : true) as T;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
if (loaded) {
|
|
129
|
-
instanced.push(loaded);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return instanced;
|
|
134
|
-
}
|
|
65
|
+
const conditions = findParams?.conditions ?? {};
|
|
66
|
+
const ordering = findParams?.ordering ?? null;
|
|
67
|
+
const fields = findParams?.fields ?? null;
|
|
68
|
+
const allowRelations = findParams?.allowRelations ?? true;
|
|
69
|
+
const fullData = findParams?.fullData ?? false;
|
|
70
|
+
|
|
71
|
+
const collection = Reflect.get(opModel, '_collection');
|
|
72
|
+
opModel.checkForInclusionWithThrow(opModel.name);
|
|
73
|
+
try {
|
|
74
|
+
const paginateParams = findParams?.pagination ? findParams?.pagination : undefined;
|
|
75
|
+
const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams);
|
|
76
|
+
|
|
77
|
+
if (dbData.length) {
|
|
78
|
+
const instanced: T[] = [];
|
|
79
|
+
|
|
80
|
+
for (const data of dbData) {
|
|
81
|
+
const inst: T = new (opModel as { new(): T })();
|
|
135
82
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.error(rwsError);
|
|
83
|
+
instanced.push((await inst._asyncFill(data, fullData, allowRelations, findParams?.cancelPostLoad ? false : true)) as T);
|
|
84
|
+
}
|
|
139
85
|
|
|
140
|
-
|
|
86
|
+
return instanced;
|
|
141
87
|
}
|
|
142
|
-
|
|
88
|
+
|
|
89
|
+
return [];
|
|
90
|
+
} catch (rwsError: Error | any) {
|
|
91
|
+
console.error(rwsError);
|
|
92
|
+
|
|
93
|
+
throw rwsError;
|
|
94
|
+
}
|
|
143
95
|
}
|
|
144
96
|
|
|
145
97
|
public static async paginate<T extends RWSModel<T>>(
|
|
@@ -147,50 +99,32 @@ export class FindUtils {
|
|
|
147
99
|
paginateParams: IPaginationParams,
|
|
148
100
|
findParams?: FindByType
|
|
149
101
|
): Promise<T[]> {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
opModel.
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const modelType = opModel.name;
|
|
167
|
-
const id = data.id;
|
|
168
|
-
|
|
169
|
-
// Check if this model is already being loaded to prevent circular references
|
|
170
|
-
if (LoadingContext.isLoading(modelType, id)) {
|
|
171
|
-
circularReferenceWarning(modelType, id);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const loaded = await LoadingContext.withLoadingContext(modelType, id, async () => {
|
|
176
|
-
const inst: T = new (opModel as { new(): T })();
|
|
177
|
-
return await inst._asyncFill(data, fullData, allowRelations, findParams?.cancelPostLoad ? false : true) as T;
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (loaded) {
|
|
181
|
-
instanced.push(loaded);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return instanced;
|
|
102
|
+
const conditions = findParams?.conditions ?? {};
|
|
103
|
+
const ordering = findParams?.ordering ?? null;
|
|
104
|
+
const fields = findParams?.fields ?? null;
|
|
105
|
+
const allowRelations = findParams?.allowRelations ?? true;
|
|
106
|
+
const fullData = findParams?.fullData ?? false;
|
|
107
|
+
|
|
108
|
+
const collection = Reflect.get(opModel, '_collection');
|
|
109
|
+
opModel.checkForInclusionWithThrow(opModel.name);
|
|
110
|
+
try {
|
|
111
|
+
const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams);
|
|
112
|
+
if (dbData.length) {
|
|
113
|
+
const instanced: T[] = [];
|
|
114
|
+
|
|
115
|
+
for (const data of dbData) {
|
|
116
|
+
const inst: T = new (opModel as { new(): T })();
|
|
117
|
+
instanced.push((await inst._asyncFill(data, fullData, allowRelations, findParams?.cancelPostLoad ? false : true)) as T);
|
|
186
118
|
}
|
|
187
119
|
|
|
188
|
-
return
|
|
189
|
-
} catch (rwsError: Error | any) {
|
|
190
|
-
console.error(rwsError);
|
|
191
|
-
|
|
192
|
-
throw rwsError;
|
|
120
|
+
return instanced;
|
|
193
121
|
}
|
|
194
|
-
|
|
122
|
+
|
|
123
|
+
return [];
|
|
124
|
+
} catch (rwsError: Error | any) {
|
|
125
|
+
console.error(rwsError);
|
|
126
|
+
|
|
127
|
+
throw rwsError;
|
|
128
|
+
}
|
|
195
129
|
}
|
|
196
130
|
}
|
package/LOADING_CONTEXT_FIX.md
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
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.
|
|
@@ -1,122 +0,0 @@
|
|
|
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.
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LoadingContext - Prevents circular references during model loading by tracking
|
|
3
|
-
* the chain of models currently being loaded per execution context
|
|
4
|
-
*/
|
|
5
|
-
declare global {
|
|
6
|
-
var __currentExecutionContext: object | undefined;
|
|
7
|
-
}
|
|
8
|
-
export declare class LoadingContext {
|
|
9
|
-
private static executionContexts;
|
|
10
|
-
private static defaultMaxDepth;
|
|
11
|
-
/**
|
|
12
|
-
* Get or create execution context identifier
|
|
13
|
-
*/
|
|
14
|
-
private static getExecutionContext;
|
|
15
|
-
/**
|
|
16
|
-
* Create a new execution context for an operation
|
|
17
|
-
*/
|
|
18
|
-
static withNewExecutionContext<T>(fn: () => Promise<T>): Promise<T>;
|
|
19
|
-
/**
|
|
20
|
-
* Get the loading stack for current execution context
|
|
21
|
-
*/
|
|
22
|
-
private static getLoadingStackInternal;
|
|
23
|
-
/**
|
|
24
|
-
* Check if a model with specific ID is currently being loaded
|
|
25
|
-
*/
|
|
26
|
-
static isLoading(modelType: string, id: string | number): boolean;
|
|
27
|
-
/**
|
|
28
|
-
* Mark a model as currently being loaded
|
|
29
|
-
*/
|
|
30
|
-
static startLoading(modelType: string, id: string | number): void;
|
|
31
|
-
/**
|
|
32
|
-
* Mark a model as finished loading
|
|
33
|
-
*/
|
|
34
|
-
static finishLoading(modelType: string, id: string | number): void;
|
|
35
|
-
/**
|
|
36
|
-
* Get current loading stack for debugging
|
|
37
|
-
*/
|
|
38
|
-
static getLoadingStack(): string[];
|
|
39
|
-
/**
|
|
40
|
-
* Clear the entire loading stack for current context
|
|
41
|
-
*/
|
|
42
|
-
static clearStack(): void;
|
|
43
|
-
/**
|
|
44
|
-
* Set default maximum loading depth
|
|
45
|
-
*/
|
|
46
|
-
static setDefaultMaxDepth(depth: number): void;
|
|
47
|
-
/**
|
|
48
|
-
* Create a unique key for model type and ID
|
|
49
|
-
*/
|
|
50
|
-
private static createKey;
|
|
51
|
-
/**
|
|
52
|
-
* Execute a function with loading context protection
|
|
53
|
-
* Automatically manages start/finish loading calls
|
|
54
|
-
*/
|
|
55
|
-
static withLoadingContext<T>(modelType: string, id: string | number, fn: () => Promise<T>): Promise<T>;
|
|
56
|
-
}
|