UNPKG

@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
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();