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