@statezero/core 0.1.19 → 0.1.21

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,21 @@
1
+ /**
2
+ * Simple graph for tracking queryset store ancestry
3
+ */
4
+ export class QuerysetStoreGraph {
5
+ constructor(hasStoreFn?: null);
6
+ graph: any;
7
+ hasStoreFn: () => boolean;
8
+ processedQuerysets: Set<any>;
9
+ setHasStoreFn(hasStoreFn: any): void;
10
+ /**
11
+ * Add a queryset and its parent relationship to the graph
12
+ */
13
+ addQueryset(queryset: any): void;
14
+ /**
15
+ * Find the root store for a queryset
16
+ * @param {Object} queryset - The queryset to analyze
17
+ * @returns {Object} { isRoot: boolean, root: semanticKey|null }
18
+ */
19
+ findRoot(queryset: Object): Object;
20
+ clear(): void;
21
+ }
@@ -0,0 +1,93 @@
1
+ import { Graph } from "graphlib";
2
+ /**
3
+ * Simple graph for tracking queryset store ancestry
4
+ */
5
+ export class QuerysetStoreGraph {
6
+ constructor(hasStoreFn = null) {
7
+ this.graph = new Graph({ directed: true });
8
+ this.hasStoreFn = hasStoreFn || (() => false);
9
+ this.processedQuerysets = new Set(); // Track UUIDs of processed querysets
10
+ }
11
+ setHasStoreFn(hasStoreFn) {
12
+ this.hasStoreFn = hasStoreFn;
13
+ }
14
+ /**
15
+ * Add a queryset and its parent relationship to the graph
16
+ */
17
+ addQueryset(queryset) {
18
+ if (!queryset)
19
+ return;
20
+ if (this.processedQuerysets.has(queryset.key)) {
21
+ return; // Already processed, skip
22
+ }
23
+ let current = queryset;
24
+ while (current && !this.processedQuerysets.has(current.key)) {
25
+ const currentKey = current.semanticKey;
26
+ const currentUuid = current.key;
27
+ this.processedQuerysets.add(currentUuid);
28
+ this.graph.setNode(currentKey);
29
+ if (current.__parent) {
30
+ const parentKey = current.__parent.semanticKey;
31
+ this.graph.setNode(parentKey);
32
+ this.graph.setEdge(currentKey, parentKey); // child -> parent
33
+ current = current.__parent;
34
+ }
35
+ else {
36
+ break;
37
+ }
38
+ }
39
+ }
40
+ /**
41
+ * Find the root store for a queryset
42
+ * @param {Object} queryset - The queryset to analyze
43
+ * @returns {Object} { isRoot: boolean, root: semanticKey|null }
44
+ */
45
+ findRoot(queryset) {
46
+ // Validate input - null/undefined is a programming error
47
+ if (!queryset) {
48
+ throw new Error("findRoot was called with a null object, instead of a queryset");
49
+ }
50
+ // Handle queryset without semanticKey
51
+ if (!queryset.semanticKey) {
52
+ throw new Error("findRoot was called on an object without a semanticKey, which means its not a queryset. findRoot only works on querysets");
53
+ }
54
+ const semanticKey = queryset.semanticKey;
55
+ if (!this.graph.hasNode(semanticKey)) {
56
+ this.addQueryset(queryset);
57
+ }
58
+ // Traverse ALL the way up to find the HIGHEST ancestor with a store
59
+ const visited = new Set();
60
+ let current = semanticKey;
61
+ let highestAncestorWithStore = null;
62
+ while (current && !visited.has(current)) {
63
+ visited.add(current);
64
+ // Check if current node has a store
65
+ if (this.hasStoreFn(current)) {
66
+ highestAncestorWithStore = current;
67
+ }
68
+ // Move to parent
69
+ const parents = this.graph.successors(current) || [];
70
+ if (parents.length > 0) {
71
+ current = parents[0]; // Follow the parent chain
72
+ }
73
+ else {
74
+ break; // No more parents
75
+ }
76
+ }
77
+ if (highestAncestorWithStore) {
78
+ if (highestAncestorWithStore === semanticKey) {
79
+ // This queryset itself is the highest with a store
80
+ return { isRoot: true, root: semanticKey };
81
+ }
82
+ else {
83
+ // Found a higher ancestor with a store
84
+ return { isRoot: false, root: highestAncestorWithStore };
85
+ }
86
+ }
87
+ // No stores found anywhere in the chain
88
+ return { isRoot: true, root: null };
89
+ }
90
+ clear() {
91
+ this.graph = new Graph({ directed: true });
92
+ }
93
+ }
@@ -23,6 +23,7 @@ declare class QuerysetStoreRegistry {
23
23
  _tempStores: WeakMap<object, any>;
24
24
  followingQuerysets: Map<any, any>;
25
25
  syncManager: () => void;
26
+ querysetStoreGraph: QuerysetStoreGraph;
26
27
  clear(): void;
27
28
  setSyncManager(syncManager: any): void;
28
29
  /**
@@ -30,6 +31,13 @@ declare class QuerysetStoreRegistry {
30
31
  */
31
32
  addFollowingQueryset(semanticKey: any, queryset: any): void;
32
33
  getStore(queryset: any, seed?: boolean): any;
34
+ /**
35
+ * Function to return the root store for a queryset
36
+ */
37
+ getRootStore(queryset: any): {
38
+ isRoot: boolean;
39
+ rootStore: any;
40
+ };
33
41
  /**
34
42
  * Get the current state of the queryset, wrapped in a LiveQueryset
35
43
  * @param {Object} queryset - The queryset
@@ -52,4 +60,5 @@ declare class QuerysetStoreRegistry {
52
60
  */
53
61
  getAllStoresForModel(ModelClass: any): any[];
54
62
  }
63
+ import { QuerysetStoreGraph } from './querysetStoreGraph.js';
55
64
  export {};
@@ -16,11 +16,11 @@ import { wrapReactiveQuerySet } from '../../reactiveAdaptor.js';
16
16
  import { processQuery, getRequiredFields, pickRequiredFields } from '../../filtering/localFiltering.js';
17
17
  import { filter } from '../../filtering/localFiltering.js';
18
18
  import { makeApiCall } from '../../flavours/django/makeApiCall.js';
19
+ import { QuerysetStoreGraph } from './querysetStoreGraph.js';
19
20
  import { isNil, pick } from 'lodash-es';
20
21
  import hash from 'object-hash';
21
22
  import { Operation } from '../stores/operation.js';
22
23
  import { Cache } from '../cache/cache.js';
23
- import { querysetGraph } from '../stores/querysetSemanticGraph.js';
24
24
  /**
25
25
  * A dynamic wrapper that always returns the latest queryset results
26
26
  * This class proxies array operations to always reflect the current state
@@ -107,6 +107,9 @@ class QuerysetStoreRegistry {
107
107
  this._tempStores = new WeakMap(); // WeakMap<Queryset, Store>
108
108
  this.followingQuerysets = new Map(); // Map<semanticKey, Set<queryset>>
109
109
  this.syncManager = () => { console.warn("SyncManager not set for QuerysetStoreRegistry"); };
110
+ this.querysetStoreGraph = new QuerysetStoreGraph((semanticKey) => {
111
+ return this._stores.has(semanticKey);
112
+ });
110
113
  }
111
114
  clear() {
112
115
  this._stores.forEach((store) => {
@@ -114,7 +117,7 @@ class QuerysetStoreRegistry {
114
117
  });
115
118
  this._stores = new Map();
116
119
  this.followingQuerysets = new Map();
117
- querysetGraph.clear();
120
+ this.querysetStoreGraph.clear();
118
121
  }
119
122
  setSyncManager(syncManager) {
120
123
  this.syncManager = syncManager;
@@ -132,6 +135,7 @@ class QuerysetStoreRegistry {
132
135
  if (isNil(queryset) || isNil(queryset.ModelClass)) {
133
136
  throw new Error("QuerysetStoreRegistry.getStore requires a valid queryset");
134
137
  }
138
+ this.querysetStoreGraph.addQueryset(queryset);
135
139
  // Check if we already have a temporary store for this exact QuerySet instance
136
140
  if (this._tempStores.has(queryset)) {
137
141
  return this._tempStores.get(queryset);
@@ -158,17 +162,39 @@ class QuerysetStoreRegistry {
158
162
  let ast = queryset.build();
159
163
  let ModelClass = queryset.ModelClass;
160
164
  if (queryset.__parent && seed) {
161
- let parentLiveQuerySet = this.getEntity(queryset.__parent);
162
- initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
165
+ const parentKey = queryset.__parent.semanticKey;
166
+ if (this._stores.has(parentKey)) {
167
+ let parentLiveQuerySet = this.getEntity(queryset.__parent);
168
+ initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
169
+ }
163
170
  }
164
171
  // Get the parent registry
165
172
  const store = new QuerysetStore(ModelClass, fetchQueryset, queryset, initialGroundTruthPks, // Initial ground truth PKs
166
- null // Initial operations
167
- );
173
+ null, // Initial operations
174
+ {
175
+ getRootStore: this.getRootStore.bind(this),
176
+ isTemp: true,
177
+ });
168
178
  // Store it in the temp store map
169
179
  this._tempStores.set(queryset, store);
170
180
  return store;
171
181
  }
182
+ /**
183
+ * Function to return the root store for a queryset
184
+ */
185
+ getRootStore(queryset) {
186
+ if (isNil(queryset)) {
187
+ throw new Error("QuerysetStoreRegistry.getRootStore requires a valid queryset");
188
+ }
189
+ const { isRoot, root } = this.querysetStoreGraph.findRoot(queryset);
190
+ const rootStore = this._stores.get(root);
191
+ if (!isRoot && rootStore) {
192
+ return { isRoot: false, rootStore: rootStore };
193
+ }
194
+ else {
195
+ return { isRoot: true, rootStore: null };
196
+ }
197
+ }
172
198
  /**
173
199
  * Get the current state of the queryset, wrapped in a LiveQueryset
174
200
  * @param {Object} queryset - The queryset
@@ -185,12 +211,14 @@ class QuerysetStoreRegistry {
185
211
  // If we have a temporary store, promote it
186
212
  if (this._tempStores.has(queryset)) {
187
213
  store = this._tempStores.get(queryset);
214
+ store.isTemp = false; // Promote to permanent store
188
215
  this._stores.set(semanticKey, store);
189
216
  this.syncManager.followModel(this, queryset.ModelClass);
190
217
  }
191
218
  // Otherwise, ensure we have a permanent store
192
219
  else if (!this._stores.has(semanticKey)) {
193
220
  store = this.getStore(queryset, seed);
221
+ store.isTemp = false;
194
222
  this._stores.set(semanticKey, store);
195
223
  this.syncManager.followModel(this, queryset.ModelClass);
196
224
  }
@@ -221,11 +249,13 @@ class QuerysetStoreRegistry {
221
249
  // If we have a temp store, promote it
222
250
  if (this._tempStores.has(queryset)) {
223
251
  store = this._tempStores.get(queryset);
252
+ store.isTemp = false; // Promote to permanent store
224
253
  this._stores.set(semanticKey, store);
225
254
  }
226
255
  else {
227
256
  // Create a new permanent store
228
257
  store = this.getStore(queryset);
258
+ store.isTemp = false;
229
259
  this._stores.set(semanticKey, store);
230
260
  }
231
261
  }
@@ -4,7 +4,6 @@ import { querysetStoreRegistry } from '../registries/querysetStoreRegistry.js';
4
4
  import { metricRegistry } from '../registries/metricRegistry.js';
5
5
  import { getFingerprint } from './utils.js';
6
6
  import { QuerySet } from '../../flavours/django/querySet.js';
7
- import { querysetGraph } from './querysetSemanticGraph.js';
8
7
  import { isEqual, isNil } from 'lodash-es';
9
8
  import hash from 'object-hash';
10
9
  /**
@@ -13,21 +12,27 @@ import hash from 'object-hash';
13
12
  * @returns {Map<QuerySet, Store>}
14
13
  */
15
14
  function relatedQuerysets(queryset) {
15
+ // Collect ancestor nodes for comparison
16
+ let ancestorNodes = [];
17
+ let current = queryset;
18
+ while (current) {
19
+ ancestorNodes.push(current.nodes);
20
+ current = current.__parent;
21
+ }
22
+ const modelClass = queryset.ModelClass;
16
23
  const result = new Map();
17
- // Use the graph to get semantic ancestors instead of manual traversal
18
- const semanticAncestors = querysetGraph.getSemanticAncestors(queryset);
19
- // Convert querysets to Map<QuerySet, Store>
20
- for (const qs of semanticAncestors) {
24
+ Array.from(querysetStoreRegistry._stores.entries()).forEach(([queryset, store]) => {
25
+ if (store.modelClass !== modelClass)
26
+ return;
21
27
  try {
22
- const store = querysetStoreRegistry.getStore(qs);
23
- if (store) {
24
- result.set(qs, store);
28
+ if (ancestorNodes.some(nodes => isEqual(nodes, store.queryset.nodes))) {
29
+ result.set(store.queryset, store);
25
30
  }
26
31
  }
27
32
  catch (e) {
28
- console.warn('Error getting store for related queryset', e);
33
+ console.warn('Error comparing nodes for related querysets', e);
29
34
  }
30
- }
35
+ });
31
36
  return result;
32
37
  }
33
38
  /**
@@ -6,7 +6,11 @@ export class QuerysetStore {
6
6
  operationsMap: Map<any, any>;
7
7
  groundTruthPks: never[];
8
8
  isSyncing: boolean;
9
+ lastSync: number | null;
10
+ needsSync: boolean;
11
+ isTemp: any;
9
12
  pruneThreshold: any;
13
+ getRootStore: any;
10
14
  qsCache: Cache;
11
15
  get cacheKey(): any;
12
16
  onHydrated(hydratedData: any): void;
@@ -26,6 +30,8 @@ export class QuerysetStore {
26
30
  getInflightOperations(): any[];
27
31
  prune(): void;
28
32
  render(optimistic?: boolean, fromCache?: boolean): any[];
33
+ renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
34
+ renderFromData(optimistic?: boolean): any[];
29
35
  applyOperation(operation: any, currentPks: any): any;
30
36
  sync(): Promise<void>;
31
37
  }
@@ -4,16 +4,21 @@ import { isNil, isEmpty, trim } from 'lodash-es';
4
4
  import { replaceTempPks, containsTempPk } from '../../flavours/django/tempPk.js';
5
5
  import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
6
6
  import { processIncludedEntities } from '../../flavours/django/makeApiCall.js';
7
- import { querysetGraph } from "./querysetSemanticGraph.js";
8
7
  import hash from 'object-hash';
9
8
  import { Cache } from '../cache/cache.js';
9
+ import { filter } from "../../filtering/localFiltering.js";
10
+ import { mod } from 'mathjs';
10
11
  export class QuerysetStore {
11
12
  constructor(modelClass, fetchFn, queryset, initialGroundTruthPks = null, initialOperations = null, options = {}) {
12
13
  this.modelClass = modelClass;
13
14
  this.fetchFn = fetchFn;
14
15
  this.queryset = queryset;
15
16
  this.isSyncing = false;
17
+ this.lastSync = null;
18
+ this.needsSync = false;
19
+ this.isTemp = options.isTemp || false;
16
20
  this.pruneThreshold = options.pruneThreshold || 10;
21
+ this.getRootStore = options.getRootStore || null;
17
22
  this.groundTruthPks = initialGroundTruthPks || [];
18
23
  this.operationsMap = new Map();
19
24
  if (Array.isArray(initialOperations)) {
@@ -23,8 +28,7 @@ export class QuerysetStore {
23
28
  this.operationsMap.set(op.operationId, op);
24
29
  }
25
30
  }
26
- this.qsCache = new Cache('queryset-cache', {}, this.onHydrated.bind(this));
27
- querysetGraph.add(queryset);
31
+ this.qsCache = new Cache("queryset-cache", {}, this.onHydrated.bind(this));
28
32
  }
29
33
  // Caching
30
34
  get cacheKey() {
@@ -42,7 +46,7 @@ export class QuerysetStore {
42
46
  setCache(result) {
43
47
  let nonTempPks = [];
44
48
  result.forEach((pk) => {
45
- if (typeof pk === 'string' && containsTempPk(pk)) {
49
+ if (typeof pk === "string" && containsTempPk(pk)) {
46
50
  pk = replaceTempPks(pk);
47
51
  if (isNil(pk) || isEmpty(trim(pk))) {
48
52
  return;
@@ -95,9 +99,7 @@ export class QuerysetStore {
95
99
  this._emitRenderEvent();
96
100
  }
97
101
  async setGroundTruth(groundTruthPks) {
98
- this.groundTruthPks = Array.isArray(groundTruthPks)
99
- ? groundTruthPks
100
- : [];
102
+ this.groundTruthPks = Array.isArray(groundTruthPks) ? groundTruthPks : [];
101
103
  this._emitRenderEvent();
102
104
  }
103
105
  async setOperations(operations) {
@@ -111,10 +113,10 @@ export class QuerysetStore {
111
113
  }
112
114
  getTrimmedOperations() {
113
115
  const cutoff = Date.now() - 1000 * 60 * 2;
114
- return this.operations.filter(op => op.timestamp > cutoff);
116
+ return this.operations.filter((op) => op.timestamp > cutoff);
115
117
  }
116
118
  getInflightOperations() {
117
- return this.operations.filter(operation => operation.status != Status.CONFIRMED &&
119
+ return this.operations.filter((operation) => operation.status != Status.CONFIRMED &&
118
120
  operation.status != Status.REJECTED);
119
121
  }
120
122
  prune() {
@@ -129,13 +131,16 @@ export class QuerysetStore {
129
131
  return cachedResult;
130
132
  }
131
133
  }
132
- const renderedPks = this.groundTruthSet;
133
- for (const op of this.operations) {
134
- if (op.status !== Status.REJECTED && (optimistic || op.status === Status.CONFIRMED)) {
135
- this.applyOperation(op, renderedPks);
134
+ let result;
135
+ if (this.getRootStore && typeof this.getRootStore === "function" && !this.isTemp) {
136
+ const { isRoot, rootStore } = this.getRootStore(this.queryset);
137
+ if (!isRoot && rootStore) {
138
+ result = this.renderFromRoot(optimistic, rootStore);
136
139
  }
137
140
  }
138
- let result = Array.from(renderedPks);
141
+ if (isNil(result)) {
142
+ result = this.renderFromData(optimistic);
143
+ }
139
144
  let limit = this.queryset.build().serializerOptions?.limit;
140
145
  if (limit) {
141
146
  result = result.slice(0, limit);
@@ -143,12 +148,30 @@ export class QuerysetStore {
143
148
  this.setCache(result);
144
149
  return result;
145
150
  }
151
+ renderFromRoot(optimistic = true, rootStore) {
152
+ let renderedPks = rootStore.render(optimistic);
153
+ let renderedData = renderedPks.map((pk) => {
154
+ return this.modelClass.fromPk(pk, this.queryset);
155
+ });
156
+ let ast = this.queryset.build();
157
+ let result = filter(renderedData, ast, this.modelClass, false);
158
+ return result;
159
+ }
160
+ renderFromData(optimistic = true) {
161
+ const renderedPks = this.groundTruthSet;
162
+ for (const op of this.operations) {
163
+ if (op.status !== Status.REJECTED &&
164
+ (optimistic || op.status === Status.CONFIRMED)) {
165
+ this.applyOperation(op, renderedPks);
166
+ }
167
+ }
168
+ let result = Array.from(renderedPks);
169
+ return result;
170
+ }
146
171
  applyOperation(operation, currentPks) {
147
172
  const pkField = this.pkField;
148
173
  for (const instance of operation.instances) {
149
- if (!instance ||
150
- typeof instance !== 'object' ||
151
- !(pkField in instance)) {
174
+ if (!instance || typeof instance !== "object" || !(pkField in instance)) {
152
175
  console.warn(`[QuerysetStore ${this.modelClass.modelName}] Skipping instance in operation ${operation.operationId} due to missing PK '${String(pkField)}' or invalid format.`);
153
176
  continue;
154
177
  }
@@ -177,12 +200,25 @@ export class QuerysetStore {
177
200
  console.warn(`[QuerysetStore ${id}] Already syncing, request ignored.`);
178
201
  return;
179
202
  }
203
+ // Check if we're delegating to a root store
204
+ if (this.getRootStore && typeof this.getRootStore === "function" && !this.isTemp) {
205
+ const { isRoot, rootStore } = this.getRootStore(this.queryset);
206
+ if (!isRoot && rootStore) {
207
+ // We're delegating to a root store - don't sync, just mark as needing sync
208
+ console.log(`[${id}] Delegating to root store, marking sync needed.`);
209
+ this.needsSync = true;
210
+ this.lastSync = null; // Clear last sync since we're not actually syncing
211
+ this.setOperations(this.getInflightOperations());
212
+ return;
213
+ }
214
+ }
215
+ // We're in independent mode - proceed with normal sync
180
216
  this.isSyncing = true;
181
217
  console.log(`[${id}] Starting sync...`);
182
218
  try {
183
219
  const response = await this.fetchFn({
184
220
  ast: this.queryset.build(),
185
- modelClass: this.modelClass
221
+ modelClass: this.modelClass,
186
222
  });
187
223
  const { data, included } = response;
188
224
  console.log(`[${id}] Sync fetch completed. Received: ${JSON.stringify(data)}.`);
@@ -190,10 +226,13 @@ export class QuerysetStore {
190
226
  processIncludedEntities(modelStoreRegistry, included, this.modelClass);
191
227
  this.setGroundTruth(data);
192
228
  this.setOperations(this.getInflightOperations());
229
+ this.lastSync = Date.now();
230
+ this.needsSync = false;
193
231
  console.log(`[${id}] Sync completed.`);
194
232
  }
195
233
  catch (e) {
196
234
  console.error(`[${id}] Failed to sync ground truth:`, e);
235
+ this.needsSync = true; // Mark as needing sync on error
197
236
  }
198
237
  finally {
199
238
  this.isSyncing = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "module": "ESNext",
6
6
  "description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",
@@ -1,21 +0,0 @@
1
- export const querysetGraph: QuerysetGraph;
2
- /**
3
- * Simple graph using graphlib: AST nodes with parent edges
4
- */
5
- declare class QuerysetGraph {
6
- graph: any;
7
- clear(): void;
8
- add(queryset: any): void;
9
- /**
10
- * Gets all querysets that share semantic equivalence with ancestors of the given queryset.
11
- *
12
- * This traverses up the parent chain to find all ancestor nodes (based on semanticId),
13
- * then returns all querysets stored in those nodes that match the same model class.
14
- * Querysets with the same semanticId are considered semantically equivalent (same AST structure).
15
- *
16
- * @param {QuerySet} queryset - The queryset to find semantic ancestors for
17
- * @returns {QuerySet[]} Array of querysets that are semantically equivalent to ancestors
18
- */
19
- getSemanticAncestors(queryset: QuerySet): QuerySet[];
20
- }
21
- export {};
@@ -1,58 +0,0 @@
1
- import { Graph, alg } from "graphlib";
2
- /**
3
- * Simple graph using graphlib: AST nodes with parent edges
4
- */
5
- class QuerysetGraph {
6
- constructor() {
7
- this.graph = new Graph({ directed: true });
8
- }
9
- clear() {
10
- this.graph = new Graph({ directed: true });
11
- }
12
- add(queryset) {
13
- const astId = queryset.semanticId;
14
- // Get or create node data with Set
15
- let nodeData = this.graph.node(astId);
16
- if (!nodeData) {
17
- nodeData = { querysets: new Set() };
18
- this.graph.setNode(astId, nodeData);
19
- }
20
- // Add queryset to Set (automatically handles duplicates)
21
- nodeData.querysets.add(queryset);
22
- // Add parent edge if exists
23
- if (queryset.__parent) {
24
- const parentAstId = queryset.__parent.semanticId;
25
- this.graph.setEdge(astId, parentAstId);
26
- }
27
- }
28
- /**
29
- * Gets all querysets that share semantic equivalence with ancestors of the given queryset.
30
- *
31
- * This traverses up the parent chain to find all ancestor nodes (based on semanticId),
32
- * then returns all querysets stored in those nodes that match the same model class.
33
- * Querysets with the same semanticId are considered semantically equivalent (same AST structure).
34
- *
35
- * @param {QuerySet} queryset - The queryset to find semantic ancestors for
36
- * @returns {QuerySet[]} Array of querysets that are semantically equivalent to ancestors
37
- */
38
- getSemanticAncestors(queryset) {
39
- const modelClass = queryset.ModelClass;
40
- const result = [];
41
- // Use graphlib to get all ancestors (including the node itself)
42
- const ancestorNodeIds = alg.postorder(this.graph, [queryset.semanticId]);
43
- // Collect all querysets from ancestor nodes
44
- for (const nodeId of ancestorNodeIds) {
45
- const nodeData = this.graph.node(nodeId);
46
- if (!nodeData || !nodeData.querysets)
47
- continue;
48
- // Add all querysets from this node that match the model class
49
- for (const qs of nodeData.querysets) {
50
- if (qs.ModelClass === modelClass) {
51
- result.push(qs);
52
- }
53
- }
54
- }
55
- return result;
56
- }
57
- }
58
- export const querysetGraph = new QuerysetGraph();