@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.
@@ -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
- 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('');
22
-
23
-
24
- const collection = Reflect.get(opModel, '_collection');
25
- const dbData = await opModel.services.dbService.findOneBy(collection, conditions, fields, ordering);
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
- 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
- }
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
- return null;
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
- const ordering = findParams?.ordering ?? null;
43
- const fields = findParams?.fields ?? null;
44
- const allowRelations = findParams?.allowRelations ?? true;
45
- const fullData = findParams?.fullData ?? false;
46
-
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);
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
- 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
- }
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
- return null;
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
- 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 })();
82
-
83
- instanced.push((await inst._asyncFill(data, fullData, allowRelations, findParams?.cancelPostLoad ? false : true)) as T);
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 instanced;
87
- }
136
+ return [];
137
+ } catch (rwsError: Error | any) {
138
+ console.error(rwsError);
88
139
 
89
- return [];
90
- } catch (rwsError: Error | any) {
91
- console.error(rwsError);
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
- 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);
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 instanced;
121
- }
188
+ return [];
189
+ } catch (rwsError: Error | any) {
190
+ console.error(rwsError);
122
191
 
123
- return [];
124
- } catch (rwsError: Error | any) {
125
- console.error(rwsError);
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';