@statezero/core
Version:
The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate
293 lines (292 loc) • 13.5 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _LiveQueryset_queryset, _LiveQueryset_ModelClass, _LiveQueryset_proxy, _LiveQueryset_array;
import { QuerysetStore } from '../stores/querysetStore.js';
import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
import { wrapReactiveQuerySet } from '../../reactiveAdaptor.js';
import { processQuery, getRequiredFields, pickRequiredFields } from '../../filtering/localFiltering.js';
import { filter } from '../../filtering/localFiltering.js';
import { makeApiCall } from '../../flavours/django/makeApiCall.js';
import { QuerysetStoreGraph } from './querysetStoreGraph.js';
import { isNil, pick } from 'lodash-es';
import hash from 'object-hash';
import { Operation } from '../stores/operation.js';
import { Cache } from '../cache/cache.js';
/**
* A dynamic wrapper that always returns the latest queryset results
* This class proxies array operations to always reflect the current state
* of the underlying QuerysetStore.
*/
export class LiveQueryset {
constructor(queryset) {
_LiveQueryset_queryset.set(this, void 0);
_LiveQueryset_ModelClass.set(this, void 0);
_LiveQueryset_proxy.set(this, void 0);
_LiveQueryset_array.set(this, []);
// used internally
__classPrivateFieldSet(this, _LiveQueryset_queryset, queryset, "f");
__classPrivateFieldSet(this, _LiveQueryset_ModelClass, queryset.ModelClass, "f");
__classPrivateFieldGet(this, _LiveQueryset_array, "f").queryset = queryset;
__classPrivateFieldGet(this, _LiveQueryset_array, "f").ModelClass = queryset.ModelClass;
// Create a proxy that intercepts all array access
__classPrivateFieldSet(this, _LiveQueryset_proxy, new Proxy(__classPrivateFieldGet(this, _LiveQueryset_array, "f"), {
get: (target, prop, receiver) => {
// Expose the touch method through the proxy
if (prop === "touch") {
return () => this.touch();
}
if (prop === "serialize") {
return () => this.serialize();
}
// Special handling for iterators and common array methods
if (prop === Symbol.iterator) {
return () => this.getCurrentItems()[Symbol.iterator]();
}
else if (typeof prop === "string" &&
[
"forEach",
"map",
"filter",
"reduce",
"some",
"every",
"find",
].includes(prop)) {
return (...args) => this.getCurrentItems()[prop](...args);
}
else if (prop === "length") {
return this.getCurrentItems().length;
}
else if (typeof prop === "string" && !isNaN(parseInt(prop))) {
// Handle numeric indices
return this.getCurrentItems()[prop];
}
return target[prop];
},
}), "f");
return __classPrivateFieldGet(this, _LiveQueryset_proxy, "f");
}
/**
* Serializes the lqs as a simple array of objects, for freezing e.g in the metric stores
*/
serialize() {
const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
// Get the current primary keys from the store
const pks = store.render();
// Map primary keys to full model objects
return pks.map((pk) => {
// Get the full model instance from the model store
const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f")).serialize();
});
}
/**
* Refresh the queryset data from the database
* Delegates to the underlying store's sync method
*/
refreshFromDb() {
const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
return store.sync();
}
/**
* Get the current items from the store
* @private
* @returns {Array} The current items in the queryset
*/
getCurrentItems() {
const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
// Get the current primary keys from the store
const pks = store.render();
// Map primary keys to full model objects
const instances = pks
.map((pk) => {
// Get the full model instance from the model store
const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
});
return instances;
}
}
_LiveQueryset_queryset = new WeakMap(), _LiveQueryset_ModelClass = new WeakMap(), _LiveQueryset_proxy = new WeakMap(), _LiveQueryset_array = new WeakMap();
export class QuerysetStoreRegistry {
constructor() {
this._stores = new Map(); // Map<semanticKey, Store>
this._tempStores = new WeakMap(); // WeakMap<Queryset, Store>
this.followingQuerysets = new Map(); // Map<semanticKey, Set<queryset>>
this.syncManager = () => { console.warn("SyncManager not set for QuerysetStoreRegistry"); };
this.querysetStoreGraph = new QuerysetStoreGraph((semanticKey) => {
return this._stores.has(semanticKey);
});
}
clear() {
this._stores.forEach((store) => {
this.syncManager.unfollowModel(this, store.modelClass);
});
this._stores = new Map();
this.followingQuerysets = new Map();
this.querysetStoreGraph.clear();
}
setSyncManager(syncManager) {
this.syncManager = syncManager;
}
/**
* Add a queryset to the following set for a semantic key
*/
addFollowingQueryset(semanticKey, queryset) {
if (!this.followingQuerysets.has(semanticKey)) {
this.followingQuerysets.set(semanticKey, new Set());
}
this.followingQuerysets.get(semanticKey).add(queryset);
}
getStore(queryset, seed = false) {
if (isNil(queryset) || isNil(queryset.ModelClass)) {
throw new Error("QuerysetStoreRegistry.getStore requires a valid queryset");
}
this.querysetStoreGraph.addQueryset(queryset);
// Check if we already have a temporary store for this exact QuerySet instance
if (this._tempStores.has(queryset)) {
return this._tempStores.get(queryset);
}
// Get the semanticKey
const semanticKey = queryset.semanticKey;
// Check if we have a permanent store with this semanticKey
if (this._stores.has(semanticKey)) {
this.addFollowingQueryset(semanticKey, queryset);
return this._stores.get(semanticKey);
}
// Create a new temporary store
const fetchQueryset = async ({ ast, modelClass }) => {
// Directly assemble the request and call the API to avoid recursive logic from the
// queryset back to the registry / store
const payload = {
...ast,
type: 'list'
};
const response = await makeApiCall(queryset, 'list', payload);
return response.data;
};
let initialGroundTruthPks = null;
let ast = queryset.build();
let ModelClass = queryset.ModelClass;
if (queryset.__parent && seed) {
const parentKey = queryset.__parent.semanticKey;
if (this._stores.has(parentKey)) {
let parentLiveQuerySet = this.getEntity(queryset.__parent);
initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
}
}
// Get the parent registry
const store = new QuerysetStore(ModelClass, fetchQueryset, queryset, initialGroundTruthPks, // Initial ground truth PKs
null, // Initial operations
{
getRootStore: this.getRootStore.bind(this),
isTemp: true,
});
// Store it in the temp store map
this._tempStores.set(queryset, store);
return store;
}
/**
* Function to return the root store for a queryset
*/
getRootStore(queryset) {
if (isNil(queryset)) {
throw new Error("QuerysetStoreRegistry.getRootStore requires a valid queryset");
}
const { isRoot, root } = this.querysetStoreGraph.findRoot(queryset);
const rootStore = this._stores.get(root);
if (!isRoot && rootStore) {
return { isRoot: false, rootStore: rootStore };
}
else {
return { isRoot: true, rootStore: null };
}
}
/**
* Get the current state of the queryset, wrapped in a LiveQueryset
* @param {Object} queryset - The queryset
* @param {Boolean} seed - Should we optimistically seed the queryset with relevant items from the parent?
* @param {Boolean} sync - Schedule a sync of the queryset with the backend
* @returns {LiveQueryset} - A live view of the queryset
*/
getEntity(queryset, seed = true, sync = false) {
if (isNil(queryset))
throw new Error(`qsStoreRegistry: getEntity cannot be called without a queryset`);
const semanticKey = queryset.semanticKey;
this.addFollowingQueryset(semanticKey, queryset);
let store;
// If we have a temporary store, promote it
if (this._tempStores.has(queryset)) {
store = this._tempStores.get(queryset);
store.isTemp = false; // Promote to permanent store
this._stores.set(semanticKey, store);
this.syncManager.followModel(this, queryset.ModelClass);
}
// Otherwise, ensure we have a permanent store
else if (!this._stores.has(semanticKey)) {
store = this.getStore(queryset, seed);
store.isTemp = false;
this._stores.set(semanticKey, store);
this.syncManager.followModel(this, queryset.ModelClass);
}
else {
store = this._stores.get(semanticKey);
}
const liveQueryset = new LiveQueryset(queryset);
if (sync)
store.sync();
return wrapReactiveQuerySet(liveQueryset);
}
/**
* Set ground truth for a queryset
* @param {Object} queryset - The queryset
* @param {Array} instances - Array of instances to set as ground truth
* @returns {Array} - The set instances
*/
setEntity(queryset, instances) {
if (isNil(queryset) || isNil(instances))
return [];
const semanticKey = queryset.semanticKey;
this.addFollowingQueryset(semanticKey, queryset);
let store;
if (this._stores.has(semanticKey)) {
store = this._stores.get(semanticKey);
}
else {
// If we have a temp store, promote it
if (this._tempStores.has(queryset)) {
store = this._tempStores.get(queryset);
store.isTemp = false; // Promote to permanent store
this._stores.set(semanticKey, store);
}
else {
// Create a new permanent store
store = this.getStore(queryset);
store.isTemp = false;
this._stores.set(semanticKey, store);
}
}
store.setGroundTruth(instances.map(instance => instance[queryset.ModelClass.primaryKeyField] || instance));
return instances;
}
/**
* Get all queryset stores for a specific model class
* @param {ModelClass} ModelClass - The model class to get stores for
* @returns {Array} - Array of queryset stores for this model
*/
getAllStoresForModel(ModelClass) {
if (!ModelClass)
return [];
return Array.from(this._stores.values()).filter(store => store.modelClass === ModelClass);
}
}
export const querysetStoreRegistry = new QuerysetStoreRegistry();