@statezero/core 0.1.1 → 0.1.3-9.1

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.
@@ -1,10 +1,18 @@
1
- import { Manager } from './manager.js';
2
- import { ValidationError } from './errors.js';
3
- import { modelStoreRegistry } from '../../syncEngine/registries/modelStoreRegistry.js';
4
- import { isNil } from 'lodash-es';
5
- import { QueryExecutor } from './queryExecutor';
6
- import { wrapReactiveModel } from '../../reactiveAdaptor.js';
7
- import { DateParsingHelpers } from './dates.js';
1
+ var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
2
+ if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
3
+ return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
4
+ };
5
+ import { Manager } from "./manager.js";
6
+ import { ValidationError } from "./errors.js";
7
+ import { modelStoreRegistry } from "../../syncEngine/registries/modelStoreRegistry.js";
8
+ import { isNil } from "lodash-es";
9
+ import { QueryExecutor } from "./queryExecutor";
10
+ import { wrapReactiveModel } from "../../reactiveAdaptor.js";
11
+ import { DateParsingHelpers } from "./dates.js";
12
+ import { FileObject } from './files.js';
13
+ import { configInstance } from "../../config.js";
14
+ import { parseStateZeroError, MultipleObjectsReturned, DoesNotExist, } from "./errors.js";
15
+ import axios from "axios";
8
16
  /**
9
17
  * A constructor for a Model.
10
18
  *
@@ -54,7 +62,7 @@ export class Model {
54
62
  * Instantiate from pk using queryset scoped singletons
55
63
  */
56
64
  static fromPk(pk, querySet) {
57
- let qsId = querySet ? querySet.__uuid : '';
65
+ let qsId = querySet ? querySet.__uuid : "";
58
66
  let key = `${qsId}__${this.configKey}__${this.modelName}__${pk}`;
59
67
  if (!this.instanceCache.has(key)) {
60
68
  const instance = new this();
@@ -70,6 +78,7 @@ export class Model {
70
78
  * @returns {any} The field value
71
79
  */
72
80
  getField(field) {
81
+ var _a;
73
82
  // Access the reactive __version property to establish dependency for vue integration
74
83
  const trackVersion = this.__version;
75
84
  const ModelClass = this.constructor;
@@ -78,11 +87,39 @@ export class Model {
78
87
  // check local overrides
79
88
  let value = this._data[field];
80
89
  // if its not been overridden, get it from the store
81
- if (isNil(value) && !isNil(this._pk)) {
90
+ if (value === undefined && !isNil(this._pk)) {
82
91
  let storedValue = modelStoreRegistry.getEntity(ModelClass, this._pk);
83
92
  if (storedValue)
84
93
  value = storedValue[field]; // if stops null -> undefined
85
94
  }
95
+ // Date/DateTime fields need special handling - convert to Date objects
96
+ const dateFormats = ["date", "datetime", "date-time"];
97
+ if (ModelClass.schema &&
98
+ dateFormats.includes(ModelClass.schema.properties[field]?.format) &&
99
+ value) {
100
+ // Let DateParsingHelpers.parseDate throw if it fails
101
+ return DateParsingHelpers.parseDate(value, field, ModelClass.schema);
102
+ }
103
+ // File/Image fields need special handling - wrap as FileObject
104
+ const fileFormats = ["file-path", "image-path"];
105
+ if (ModelClass.schema &&
106
+ fileFormats.includes(ModelClass.schema.properties[field]?.format) &&
107
+ value) {
108
+ // Check if it's already a FileObject
109
+ if (value instanceof FileObject) {
110
+ return value;
111
+ }
112
+ // If it's stored file data from API, wrap it as FileObject
113
+ if (typeof value === "object" && value.file_path) {
114
+ // Create anonymous subclass with correct configKey
115
+ const BackendFileObject = (_a = class extends FileObject {
116
+ },
117
+ __setFunctionName(_a, "BackendFileObject"),
118
+ _a.configKey = ModelClass.configKey,
119
+ _a);
120
+ return new BackendFileObject(value);
121
+ }
122
+ }
86
123
  // relationship fields need special handling
87
124
  if (ModelClass.relationshipFields.has(field) && value) {
88
125
  // fetch the stored value
@@ -90,15 +127,15 @@ export class Model {
90
127
  // footgun - fieldInfo.ModelClass() calls the arrow function that lazily gets the model class
91
128
  let relPkField = fieldInfo.ModelClass().primaryKeyField;
92
129
  switch (fieldInfo.relationshipType) {
93
- case 'many-to-many':
130
+ case "many-to-many":
94
131
  // value is an array
95
132
  if (!Array.isArray(value) && value)
96
133
  throw new Error(`Data corruption: m2m field for ${ModelClass.modelName} stored as ${value}`);
97
134
  // set each pk to the full model object for that pk
98
- value = value.map(pk => fieldInfo.ModelClass().fromPk(pk));
135
+ value = value.map((pk) => fieldInfo.ModelClass().fromPk(pk));
99
136
  break;
100
- case 'one-to-one':
101
- case 'foreign-key':
137
+ case "one-to-one":
138
+ case "foreign-key":
102
139
  // footgun - fieldInfo.ModelClass() calls the arrow function that lazily gets the model class
103
140
  if (!isNil(value))
104
141
  value = fieldInfo.ModelClass().fromPk(value);
@@ -135,13 +172,13 @@ export class Model {
135
172
  return;
136
173
  const allowedFields = this.fields;
137
174
  for (const key of Object.keys(data)) {
138
- if (key === 'repr' || key === 'type')
175
+ if (key === "repr" || key === "type")
139
176
  continue;
140
177
  // Handle nested fields by splitting on double underscore
141
178
  // and taking just the base field name
142
- const baseField = key.split('__')[0];
179
+ const baseField = key.split("__")[0];
143
180
  if (!allowedFields.includes(baseField)) {
144
- let errorMsg = `Invalid field: ${baseField}. Allowed fields are: ${allowedFields.join(', ')}`;
181
+ let errorMsg = `Invalid field: ${baseField}. Allowed fields are: ${allowedFields.join(", ")}`;
145
182
  console.error(errorMsg);
146
183
  throw new ValidationError(errorMsg);
147
184
  }
@@ -160,11 +197,19 @@ export class Model {
160
197
  // check local overrides
161
198
  let value = this._data[field];
162
199
  // if it's not been overridden, get it from the store
163
- if (isNil(value) && !isNil(this._pk)) {
200
+ if (value === undefined && !isNil(this._pk)) {
164
201
  let storedValue = modelStoreRegistry.getEntity(ModelClass, this._pk);
165
202
  if (storedValue)
166
203
  value = storedValue[field];
167
204
  }
205
+ // Date/DateTime fields need special handling - convert Date objects to strings for API
206
+ const dateFormats = ["date", "date-time"];
207
+ if (ModelClass.schema &&
208
+ dateFormats.includes(ModelClass.schema.properties[field]?.format) &&
209
+ value instanceof Date) {
210
+ // Let DateParsingHelpers.serializeDate throw if it fails
211
+ return DateParsingHelpers.serializeDate(value, field, ModelClass.schema);
212
+ }
168
213
  return value;
169
214
  }
170
215
  /**
@@ -192,16 +237,20 @@ export class Model {
192
237
  async save() {
193
238
  const ModelClass = this.constructor;
194
239
  const pkField = ModelClass.primaryKeyField;
195
- const querySet = !this.pk ? ModelClass.objects.newQuerySet() : ModelClass.objects.filter({ [pkField]: this.pk });
240
+ const querySet = !this.pk
241
+ ? ModelClass.objects.newQuerySet()
242
+ : ModelClass.objects.filter({ [pkField]: this.pk });
196
243
  const data = this.serialize();
197
244
  let instance;
198
245
  if (!this.pk) {
199
246
  // Create new instance
200
- instance = await QueryExecutor.execute(querySet, 'create', { data });
247
+ instance = await QueryExecutor.execute(querySet, "create", { data });
201
248
  }
202
249
  else {
203
250
  // Update existing instance
204
- instance = await QueryExecutor.execute(querySet, 'update_instance', { data });
251
+ instance = await QueryExecutor.execute(querySet, "update_instance", {
252
+ data,
253
+ });
205
254
  }
206
255
  this._pk = instance.pk;
207
256
  this._data = {};
@@ -218,14 +267,14 @@ export class Model {
218
267
  */
219
268
  async delete() {
220
269
  if (!this.pk) {
221
- throw new Error('Cannot delete unsaved instance');
270
+ throw new Error("Cannot delete unsaved instance");
222
271
  }
223
272
  const ModelClass = this.constructor;
224
273
  const pkField = ModelClass.primaryKeyField;
225
274
  const querySet = ModelClass.objects.filter({ [pkField]: this.pk });
226
275
  // Pass the instance data with primary key as the args
227
276
  const args = { [pkField]: this.pk };
228
- const result = await QueryExecutor.execute(querySet, 'delete_instance', args);
277
+ const result = await QueryExecutor.execute(querySet, "delete_instance", args);
229
278
  // result -> [deletedCount, { [modelName]: deletedCount }];
230
279
  return result;
231
280
  }
@@ -237,13 +286,83 @@ export class Model {
237
286
  */
238
287
  async refreshFromDb() {
239
288
  if (!this.pk) {
240
- throw new Error('Cannot refresh unsaved instance');
289
+ throw new Error("Cannot refresh unsaved instance");
241
290
  }
242
291
  const ModelClass = this.constructor;
243
- const fresh = await ModelClass.objects.get({ [ModelClass.primaryKeyField]: this.pk });
292
+ const fresh = await ModelClass.objects.get({
293
+ [ModelClass.primaryKeyField]: this.pk,
294
+ });
244
295
  // clear the current data and fresh data will flow
245
296
  this._data = {};
246
297
  }
298
+ /**
299
+ * Validates the model instance using the same serialize behavior as save()
300
+ * @param {string} validateType - 'create' or 'update' (defaults to auto-detect)
301
+ * @param {boolean} partial - Whether to allow partial validation
302
+ * @returns {Promise<boolean>} Promise that resolves to true if valid, throws error if invalid
303
+ */
304
+ async validate(validateType = null, partial = false) {
305
+ const ModelClass = this.constructor;
306
+ if (!validateType) {
307
+ validateType = this.pk ? "update" : "create";
308
+ }
309
+ // Validate the validateType parameter
310
+ if (!["update", "create"].includes(validateType)) {
311
+ throw new Error(`Validation type must be 'update' or 'create', not '${validateType}'`);
312
+ }
313
+ // Use the same serialize logic as save()
314
+ const data = this.serialize();
315
+ // Delegate to static method
316
+ return ModelClass.validate(data, validateType, partial);
317
+ }
318
+ /**
319
+ * Static method to validate data without creating an instance
320
+ * @param {Object} data - Data to validate
321
+ * @param {string} validateType - 'create' or 'update'
322
+ * @param {boolean} partial - Whether to allow partial validation
323
+ * @returns {Promise<boolean>} Promise that resolves to true if valid, throws error if invalid
324
+ */
325
+ static async validate(data, validateType = "create", partial = false) {
326
+ const ModelClass = this;
327
+ // Validate the validateType parameter
328
+ if (!["update", "create"].includes(validateType)) {
329
+ throw new Error(`Validation type must be 'update' or 'create', not '${validateType}'`);
330
+ }
331
+ // Get backend config and check if it exists
332
+ const config = configInstance.getConfig();
333
+ const backend = config.backendConfigs[ModelClass.configKey];
334
+ if (!backend) {
335
+ throw new Error(`No backend configuration found for key: ${ModelClass.configKey}`);
336
+ }
337
+ // Build URL for validate endpoint
338
+ const baseUrl = backend.API_URL.replace(/\/+$/, "");
339
+ const url = `${baseUrl}/${ModelClass.modelName}/validate/`;
340
+ // Prepare headers
341
+ const headers = {
342
+ "Content-Type": "application/json",
343
+ ...(backend.getAuthHeaders ? backend.getAuthHeaders() : {}),
344
+ };
345
+ // Make direct API call to validate endpoint
346
+ try {
347
+ const response = await axios.post(url, {
348
+ data: data,
349
+ validate_type: validateType,
350
+ partial: partial,
351
+ }, { headers });
352
+ // Backend returns {"valid": true} on success
353
+ return response.data.valid === true;
354
+ }
355
+ catch (error) {
356
+ if (error.response && error.response.data) {
357
+ const parsedError = parseStateZeroError(error.response.data);
358
+ if (Error.captureStackTrace) {
359
+ Error.captureStackTrace(parsedError, ModelClass.validate);
360
+ }
361
+ throw parsedError;
362
+ }
363
+ throw new Error(`Validation failed: ${error.message}`);
364
+ }
365
+ }
247
366
  }
248
367
  /**
249
368
  * Creates a new Model instance.
package/dist/setup.js CHANGED
@@ -2,6 +2,9 @@ import { configInstance } from "./config.js";
2
2
  import { setAdapters } from "./reactiveAdaptor.js";
3
3
  import { syncManager } from "./syncEngine/sync.js";
4
4
  import { initEventHandler } from "./syncEngine/stores/operationEventHandlers.js";
5
+ import { querysetStoreRegistry } from "./syncEngine/registries/querysetStoreRegistry.js";
6
+ import { modelStoreRegistry } from "./syncEngine/registries/modelStoreRegistry.js";
7
+ import { metricRegistry } from "./syncEngine/registries/metricRegistry.js";
5
8
  /**
6
9
  * Initialize StateZero with the provided configuration
7
10
  *
@@ -19,4 +22,12 @@ export function setupStateZero(config, getModelClass, adapters) {
19
22
  setAdapters(adapters.ModelAdaptor, adapters.QuerySetAdaptor, adapters.MetricAdaptor);
20
23
  initEventHandler();
21
24
  syncManager.initialize();
25
+ // Expose registries and sync manager for devtools de
26
+ if (typeof window !== "undefined") {
27
+ window.__STATEZERO_DEVTOOLS__ = {
28
+ querysetStoreRegistry,
29
+ modelStoreRegistry,
30
+ metricRegistry,
31
+ };
32
+ }
22
33
  }
@@ -8,6 +8,11 @@ export class LiveMetric {
8
8
  metricType: any;
9
9
  field: any;
10
10
  get lqs(): import("./querysetStoreRegistry").LiveQueryset;
11
+ /**
12
+ * Refresh the metric data from the database
13
+ * Delegates to the underlying store's sync method
14
+ */
15
+ refreshFromDb(): any;
11
16
  /**
12
17
  * Getter that always returns the current value from the store
13
18
  */
@@ -17,6 +17,14 @@ export class LiveMetric {
17
17
  get lqs() {
18
18
  return querysetStoreRegistry.getEntity(this.queryset);
19
19
  }
20
+ /**
21
+ * Refresh the metric data from the database
22
+ * Delegates to the underlying store's sync method
23
+ */
24
+ refreshFromDb() {
25
+ const store = metricRegistry.getStore(this.metricType, this.queryset, this.field);
26
+ return store.sync();
27
+ }
20
28
  /**
21
29
  * Getter that always returns the current value from the store
22
30
  */
@@ -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,95 @@
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
+ if (currentKey !== parentKey) {
33
+ this.graph.setEdge(currentKey, parentKey);
34
+ }
35
+ current = current.__parent;
36
+ }
37
+ else {
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ /**
43
+ * Find the root store for a queryset
44
+ * @param {Object} queryset - The queryset to analyze
45
+ * @returns {Object} { isRoot: boolean, root: semanticKey|null }
46
+ */
47
+ findRoot(queryset) {
48
+ // Validate input - null/undefined is a programming error
49
+ if (!queryset) {
50
+ throw new Error("findRoot was called with a null object, instead of a queryset");
51
+ }
52
+ // Handle queryset without semanticKey
53
+ if (!queryset.semanticKey) {
54
+ throw new Error("findRoot was called on an object without a semanticKey, which means its not a queryset. findRoot only works on querysets");
55
+ }
56
+ const semanticKey = queryset.semanticKey;
57
+ if (!this.graph.hasNode(semanticKey)) {
58
+ this.addQueryset(queryset);
59
+ }
60
+ // Traverse ALL the way up to find the HIGHEST ancestor with a store
61
+ const visited = new Set();
62
+ let current = semanticKey;
63
+ let highestAncestorWithStore = null;
64
+ while (current && !visited.has(current)) {
65
+ visited.add(current);
66
+ // Check if current node has a store
67
+ if (this.hasStoreFn(current)) {
68
+ highestAncestorWithStore = current;
69
+ }
70
+ // Move to parent
71
+ const parents = this.graph.successors(current) || [];
72
+ if (parents.length > 0) {
73
+ current = parents[0]; // Follow the parent chain
74
+ }
75
+ else {
76
+ break; // No more parents
77
+ }
78
+ }
79
+ if (highestAncestorWithStore) {
80
+ if (highestAncestorWithStore === semanticKey) {
81
+ // This queryset itself is the highest with a store
82
+ return { isRoot: true, root: semanticKey };
83
+ }
84
+ else {
85
+ // Found a higher ancestor with a store
86
+ return { isRoot: false, root: highestAncestorWithStore };
87
+ }
88
+ }
89
+ // No stores found anywhere in the chain
90
+ return { isRoot: true, root: null };
91
+ }
92
+ clear() {
93
+ this.graph = new Graph({ directed: true });
94
+ }
95
+ }
@@ -9,6 +9,11 @@ export class LiveQueryset {
9
9
  * Serializes the lqs as a simple array of objects, for freezing e.g in the metric stores
10
10
  */
11
11
  serialize(): any;
12
+ /**
13
+ * Refresh the queryset data from the database
14
+ * Delegates to the underlying store's sync method
15
+ */
16
+ refreshFromDb(): any;
12
17
  /**
13
18
  * Get the current items from the store
14
19
  * @private
@@ -23,6 +28,7 @@ declare class QuerysetStoreRegistry {
23
28
  _tempStores: WeakMap<object, any>;
24
29
  followingQuerysets: Map<any, any>;
25
30
  syncManager: () => void;
31
+ querysetStoreGraph: QuerysetStoreGraph;
26
32
  clear(): void;
27
33
  setSyncManager(syncManager: any): void;
28
34
  /**
@@ -30,6 +36,13 @@ declare class QuerysetStoreRegistry {
30
36
  */
31
37
  addFollowingQueryset(semanticKey: any, queryset: any): void;
32
38
  getStore(queryset: any, seed?: boolean): any;
39
+ /**
40
+ * Function to return the root store for a queryset
41
+ */
42
+ getRootStore(queryset: any): {
43
+ isRoot: boolean;
44
+ rootStore: any;
45
+ };
33
46
  /**
34
47
  * Get the current state of the queryset, wrapped in a LiveQueryset
35
48
  * @param {Object} queryset - The queryset
@@ -52,4 +65,5 @@ declare class QuerysetStoreRegistry {
52
65
  */
53
66
  getAllStoresForModel(ModelClass: any): any[];
54
67
  }
68
+ import { QuerysetStoreGraph } from './querysetStoreGraph.js';
55
69
  export {};
@@ -16,6 +16,7 @@ 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';
@@ -40,28 +41,37 @@ export class LiveQueryset {
40
41
  __classPrivateFieldSet(this, _LiveQueryset_proxy, new Proxy(__classPrivateFieldGet(this, _LiveQueryset_array, "f"), {
41
42
  get: (target, prop, receiver) => {
42
43
  // Expose the touch method through the proxy
43
- if (prop === 'touch') {
44
+ if (prop === "touch") {
44
45
  return () => this.touch();
45
46
  }
46
- if (prop === 'serialize') {
47
+ if (prop === "serialize") {
47
48
  return () => this.serialize();
48
49
  }
49
50
  // Special handling for iterators and common array methods
50
51
  if (prop === Symbol.iterator) {
51
52
  return () => this.getCurrentItems()[Symbol.iterator]();
52
53
  }
53
- else if (typeof prop === 'string' && ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'].includes(prop)) {
54
+ else if (typeof prop === "string" &&
55
+ [
56
+ "forEach",
57
+ "map",
58
+ "filter",
59
+ "reduce",
60
+ "some",
61
+ "every",
62
+ "find",
63
+ ].includes(prop)) {
54
64
  return (...args) => this.getCurrentItems()[prop](...args);
55
65
  }
56
- else if (prop === 'length') {
66
+ else if (prop === "length") {
57
67
  return this.getCurrentItems().length;
58
68
  }
59
- else if (typeof prop === 'string' && !isNaN(parseInt(prop))) {
69
+ else if (typeof prop === "string" && !isNaN(parseInt(prop))) {
60
70
  // Handle numeric indices
61
71
  return this.getCurrentItems()[prop];
62
72
  }
63
73
  return target[prop];
64
- }
74
+ },
65
75
  }), "f");
66
76
  return __classPrivateFieldGet(this, _LiveQueryset_proxy, "f");
67
77
  }
@@ -73,30 +83,37 @@ export class LiveQueryset {
73
83
  // Get the current primary keys from the store
74
84
  const pks = store.render();
75
85
  // Map primary keys to full model objects
76
- return pks.map(pk => {
86
+ return pks.map((pk) => {
77
87
  // Get the full model instance from the model store
78
88
  const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
79
89
  return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f")).serialize();
80
90
  });
81
91
  }
92
+ /**
93
+ * Refresh the queryset data from the database
94
+ * Delegates to the underlying store's sync method
95
+ */
96
+ refreshFromDb() {
97
+ const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
98
+ return store.sync();
99
+ }
82
100
  /**
83
101
  * Get the current items from the store
84
102
  * @private
85
103
  * @returns {Array} The current items in the queryset
86
104
  */
87
- getCurrentItems(sortAndFilter = true) {
105
+ getCurrentItems() {
88
106
  const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
89
107
  // Get the current primary keys from the store
90
108
  const pks = store.render();
91
109
  // Map primary keys to full model objects
92
- const instances = pks.map(pk => {
110
+ const instances = pks
111
+ .map((pk) => {
93
112
  // Get the full model instance from the model store
94
113
  const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
95
114
  return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
96
115
  });
97
- if (!sortAndFilter)
98
- return instances;
99
- return filter(instances, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f").build(), __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f"), true);
116
+ return instances;
100
117
  }
101
118
  }
102
119
  _LiveQueryset_queryset = new WeakMap(), _LiveQueryset_ModelClass = new WeakMap(), _LiveQueryset_proxy = new WeakMap(), _LiveQueryset_array = new WeakMap();
@@ -106,6 +123,9 @@ class QuerysetStoreRegistry {
106
123
  this._tempStores = new WeakMap(); // WeakMap<Queryset, Store>
107
124
  this.followingQuerysets = new Map(); // Map<semanticKey, Set<queryset>>
108
125
  this.syncManager = () => { console.warn("SyncManager not set for QuerysetStoreRegistry"); };
126
+ this.querysetStoreGraph = new QuerysetStoreGraph((semanticKey) => {
127
+ return this._stores.has(semanticKey);
128
+ });
109
129
  }
110
130
  clear() {
111
131
  this._stores.forEach((store) => {
@@ -113,6 +133,7 @@ class QuerysetStoreRegistry {
113
133
  });
114
134
  this._stores = new Map();
115
135
  this.followingQuerysets = new Map();
136
+ this.querysetStoreGraph.clear();
116
137
  }
117
138
  setSyncManager(syncManager) {
118
139
  this.syncManager = syncManager;
@@ -130,6 +151,7 @@ class QuerysetStoreRegistry {
130
151
  if (isNil(queryset) || isNil(queryset.ModelClass)) {
131
152
  throw new Error("QuerysetStoreRegistry.getStore requires a valid queryset");
132
153
  }
154
+ this.querysetStoreGraph.addQueryset(queryset);
133
155
  // Check if we already have a temporary store for this exact QuerySet instance
134
156
  if (this._tempStores.has(queryset)) {
135
157
  return this._tempStores.get(queryset);
@@ -156,17 +178,39 @@ class QuerysetStoreRegistry {
156
178
  let ast = queryset.build();
157
179
  let ModelClass = queryset.ModelClass;
158
180
  if (queryset.__parent && seed) {
159
- let parentLiveQuerySet = this.getEntity(queryset.__parent);
160
- initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
181
+ const parentKey = queryset.__parent.semanticKey;
182
+ if (this._stores.has(parentKey)) {
183
+ let parentLiveQuerySet = this.getEntity(queryset.__parent);
184
+ initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
185
+ }
161
186
  }
162
187
  // Get the parent registry
163
188
  const store = new QuerysetStore(ModelClass, fetchQueryset, queryset, initialGroundTruthPks, // Initial ground truth PKs
164
- null // Initial operations
165
- );
189
+ null, // Initial operations
190
+ {
191
+ getRootStore: this.getRootStore.bind(this),
192
+ isTemp: true,
193
+ });
166
194
  // Store it in the temp store map
167
195
  this._tempStores.set(queryset, store);
168
196
  return store;
169
197
  }
198
+ /**
199
+ * Function to return the root store for a queryset
200
+ */
201
+ getRootStore(queryset) {
202
+ if (isNil(queryset)) {
203
+ throw new Error("QuerysetStoreRegistry.getRootStore requires a valid queryset");
204
+ }
205
+ const { isRoot, root } = this.querysetStoreGraph.findRoot(queryset);
206
+ const rootStore = this._stores.get(root);
207
+ if (!isRoot && rootStore) {
208
+ return { isRoot: false, rootStore: rootStore };
209
+ }
210
+ else {
211
+ return { isRoot: true, rootStore: null };
212
+ }
213
+ }
170
214
  /**
171
215
  * Get the current state of the queryset, wrapped in a LiveQueryset
172
216
  * @param {Object} queryset - The queryset
@@ -183,12 +227,14 @@ class QuerysetStoreRegistry {
183
227
  // If we have a temporary store, promote it
184
228
  if (this._tempStores.has(queryset)) {
185
229
  store = this._tempStores.get(queryset);
230
+ store.isTemp = false; // Promote to permanent store
186
231
  this._stores.set(semanticKey, store);
187
232
  this.syncManager.followModel(this, queryset.ModelClass);
188
233
  }
189
234
  // Otherwise, ensure we have a permanent store
190
235
  else if (!this._stores.has(semanticKey)) {
191
236
  store = this.getStore(queryset, seed);
237
+ store.isTemp = false;
192
238
  this._stores.set(semanticKey, store);
193
239
  this.syncManager.followModel(this, queryset.ModelClass);
194
240
  }
@@ -219,11 +265,13 @@ class QuerysetStoreRegistry {
219
265
  // If we have a temp store, promote it
220
266
  if (this._tempStores.has(queryset)) {
221
267
  store = this._tempStores.get(queryset);
268
+ store.isTemp = false; // Promote to permanent store
222
269
  this._stores.set(semanticKey, store);
223
270
  }
224
271
  else {
225
272
  // Create a new permanent store
226
273
  store = this.getStore(queryset);
274
+ store.isTemp = false;
227
275
  this._stores.set(semanticKey, store);
228
276
  }
229
277
  }