@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.
- package/LICENSE +3 -2
- package/dist/cli/commands/sync.d.ts +6 -0
- package/dist/cli/commands/sync.js +30 -0
- package/dist/cli/commands/syncActions.d.ts +46 -0
- package/dist/cli/commands/syncActions.js +623 -0
- package/dist/cli/commands/syncModels.js +61 -29
- package/dist/cli/index.js +18 -10
- package/dist/config.d.ts +5 -0
- package/dist/config.js +40 -10
- package/dist/flavours/django/dates.js +3 -3
- package/dist/flavours/django/files.d.ts +8 -7
- package/dist/flavours/django/files.js +36 -2
- package/dist/flavours/django/model.d.ts +15 -0
- package/dist/flavours/django/model.js +143 -24
- package/dist/setup.js +11 -0
- package/dist/syncEngine/registries/metricRegistry.d.ts +5 -0
- package/dist/syncEngine/registries/metricRegistry.js +8 -0
- package/dist/syncEngine/registries/querysetStoreGraph.d.ts +21 -0
- package/dist/syncEngine/registries/querysetStoreGraph.js +95 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +14 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +64 -16
- package/dist/syncEngine/stores/modelStore.d.ts +1 -0
- package/dist/syncEngine/stores/modelStore.js +23 -12
- package/dist/syncEngine/stores/querysetStore.d.ts +18 -0
- package/dist/syncEngine/stores/querysetStore.js +129 -18
- package/dist/syncEngine/sync.d.ts +5 -0
- package/dist/syncEngine/sync.js +61 -5
- package/package.json +126 -123
- package/readme.md +1 -1
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 (
|
|
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
|
|
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
|
|
101
|
-
case
|
|
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 ===
|
|
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(
|
|
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 (
|
|
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
|
|
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,
|
|
247
|
+
instance = await QueryExecutor.execute(querySet, "create", { data });
|
|
201
248
|
}
|
|
202
249
|
else {
|
|
203
250
|
// Update existing instance
|
|
204
|
-
instance = await QueryExecutor.execute(querySet,
|
|
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(
|
|
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,
|
|
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(
|
|
289
|
+
throw new Error("Cannot refresh unsaved instance");
|
|
241
290
|
}
|
|
242
291
|
const ModelClass = this.constructor;
|
|
243
|
-
const fresh = await ModelClass.objects.get({
|
|
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 ===
|
|
44
|
+
if (prop === "touch") {
|
|
44
45
|
return () => this.touch();
|
|
45
46
|
}
|
|
46
|
-
if (prop ===
|
|
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 ===
|
|
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 ===
|
|
66
|
+
else if (prop === "length") {
|
|
57
67
|
return this.getCurrentItems().length;
|
|
58
68
|
}
|
|
59
|
-
else if (typeof 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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
}
|