UNPKG

@statezero/core

Version:

The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate

249 lines (248 loc) 11.7 kB
import { Operation, Type, Status } from '../../syncEngine/stores/operation'; import { v7 as uuid7 } from 'uuid'; import { createTempPk } from './tempPk.js'; import { getRequiredFields, pickRequiredFields, processQuery } from '../../filtering/localFiltering.js'; import { evaluateExpression } from './f.js'; import { modelStoreRegistry } from '../../syncEngine/registries/modelStoreRegistry.js'; import { querysetStoreRegistry } from '../../syncEngine/registries/querysetStoreRegistry.js'; import { isNil } from 'lodash-es'; /** * Factory for creating Operation instances with consistent behavior * across QueryExecutor and hotpath event handling */ export class OperationFactory { /** * Create a CREATE operation * @param {QuerySet} queryset - The queryset context * @param {Object} data - The data for the new instance * @param {string} [operationId] - Optional operation ID (for hotpath events) * @returns {Operation} The created operation */ static createCreateOperation(queryset, data = {}, operationId = null) { const ModelClass = queryset.ModelClass; const primaryKeyField = ModelClass.primaryKeyField; const opId = operationId || `${uuid7()}`; const tempPk = createTempPk(opId); return new Operation({ operationId: opId, type: Type.CREATE, instances: [{ ...data, [primaryKeyField]: tempPk }], queryset: queryset, args: { data }, localOnly: queryset._optimisticOnly || false, }); } /** * Create a BULK_CREATE operation * @param {QuerySet} queryset - The queryset context * @param {Array<Object>} dataList - Array of data objects for the new instances * @param {string} [operationId] - Optional operation ID (for hotpath events) * @returns {Operation} The created operation */ static createBulkCreateOperation(queryset, dataList = [], operationId = null) { const ModelClass = queryset.ModelClass; const primaryKeyField = ModelClass.primaryKeyField; const opId = operationId || `${uuid7()}`; // Create temp PKs for each instance const instances = dataList.map((data, index) => { const tempPk = createTempPk(`${opId}_${index}`); return { ...data, [primaryKeyField]: tempPk }; }); return new Operation({ operationId: opId, type: Type.BULK_CREATE, instances: instances, queryset: queryset, args: { data: dataList }, localOnly: queryset._optimisticOnly || false, }); } /** * Create an UPDATE operation with optimistic instance updates * @param {QuerySet} queryset - The queryset context * @param {Object} data - The update data * @param {Object} filter - Optional filter for the update * @param {string} [operationId] - Optional operation ID (for hotpath events) * @returns {Operation} The created operation */ static createUpdateOperation(queryset, data = {}, filter = null, operationId = null) { const ModelClass = queryset.ModelClass; const primaryKeyField = ModelClass.primaryKeyField; const store = querysetStoreRegistry.getStore(queryset); const querysetPks = store.render(); const opId = operationId || `${uuid7()}`; // Create optimistic instances with F expression evaluation const optimisticInstances = querysetPks.map(pk => { const instance = modelStoreRegistry.getEntity(ModelClass, pk); const updatedInstance = { ...instance }; updatedInstance[primaryKeyField] = pk; for (const [key, value] of Object.entries(data)) { if (value && typeof value === 'object' && value.__f_expr) { const evaluatedValue = evaluateExpression(value, instance); if (evaluatedValue !== null) { updatedInstance[key] = evaluatedValue; } else { updatedInstance[key] = instance[key]; } } else { updatedInstance[key] = value; } } return updatedInstance; }); return new Operation({ operationId: opId, type: Type.UPDATE, instances: optimisticInstances, queryset: queryset, args: { filter, data }, localOnly: queryset._optimisticOnly || false, }); } /** * Create a DELETE operation * @param {QuerySet} queryset - The queryset context * @param {string} [operationId] - Optional operation ID (for hotpath events) * @returns {Operation} The created operation */ static createDeleteOperation(queryset, operationId = null) { const ModelClass = queryset.ModelClass; const primaryKeyField = ModelClass.primaryKeyField; const store = querysetStoreRegistry.getStore(queryset); const querysetPks = store.render(); const opId = operationId || `${uuid7()}`; const instances = querysetPks.map((pk) => ({ [primaryKeyField]: pk })); return new Operation({ operationId: opId, type: Type.DELETE, instances: instances, queryset: queryset, args: {}, localOnly: queryset._optimisticOnly || false, }); } /** * Create an UPDATE_INSTANCE operation * @param {QuerySet} queryset - The queryset context * @param {Object} data - The update data * @param {string} [operationId] - Optional operation ID (for hotpath events) * @returns {Operation} The created operation */ static createUpdateInstanceOperation(queryset, data = {}, operationId = null) { const ModelClass = queryset.ModelClass; const primaryKeyField = ModelClass.primaryKeyField; const store = querysetStoreRegistry.getStore(queryset); const querysetPks = store.render(); const opId = operationId || `${uuid7()}`; const instances = querysetPks.map(pk => ({ ...data, [primaryKeyField]: pk })); return new Operation({ operationId: opId, type: Type.UPDATE_INSTANCE, instances: instances, queryset: queryset, args: { data }, localOnly: queryset._optimisticOnly || false, }); } /** * Create a DELETE_INSTANCE operation * @param {QuerySet} queryset - The queryset context * @param {Object} instanceData - The instance to delete (object with PK) * @param {string} [operationId] - Optional operation ID (for hotpath events) * @returns {Operation} The created operation */ static createDeleteInstanceOperation(queryset, instanceData, operationId = null) { const opId = operationId || `${uuid7()}`; return new Operation({ operationId: opId, type: Type.DELETE_INSTANCE, instances: [instanceData], queryset: queryset, args: instanceData, localOnly: queryset._optimisticOnly || false, }); } /** * Create a GET_OR_CREATE operation with local filtering logic * @param {QuerySet} queryset - The queryset context * @param {Object} lookup - The lookup criteria * @param {Object} defaults - The default values for creation * @param {string} [operationId] - Optional operation ID (for hotpath events) * @returns {Operation} The created operation */ static createGetOrCreateOperation(queryset, lookup = {}, defaults = {}, operationId = null) { const ModelClass = queryset.ModelClass; const primaryKeyField = ModelClass.primaryKeyField; const opId = operationId || `${uuid7()}`; // Get all current instances from the store for local filtering const modelStore = modelStoreRegistry.getStore(ModelClass); const allInstances = modelStore.render(); // Create a queryset filter for the lookup criteria const lookupFilter = { ...lookup }; const lookupQuerySet = new queryset.constructor(ModelClass).filter(lookupFilter); const lookupQuery = lookupQuerySet.build(); // Use local filtering to find matching instances const requiredPaths = getRequiredFields(lookupQuery, ModelClass); const prunedData = allInstances.map(inst => pickRequiredFields(requiredPaths, ModelClass.fromPk(inst[primaryKeyField], queryset))); const matchingPks = processQuery(prunedData, lookupQuery, ModelClass); // Find the corresponding instances const matchingInstances = allInstances.filter(inst => matchingPks.includes(inst[primaryKeyField])); const isCreatingNew = matchingInstances.length === 0; const effectiveType = isCreatingNew ? Type.CREATE : Type.UPDATE; // Create the instance data const instanceData = isCreatingNew ? { ...lookup, ...defaults, [primaryKeyField]: opId } : matchingInstances[0]; return new Operation({ operationId: opId, type: effectiveType, instances: [instanceData], queryset: queryset, args: { lookup, defaults }, localOnly: queryset._optimisticOnly || false, }); } /** * Create an UPDATE_OR_CREATE operation with local filtering logic * @param {QuerySet} queryset - The queryset context * @param {Object} lookup - The lookup criteria * @param {Object} defaults - The default values for creation/update * @param {string} [operationId] - Optional operation ID (for hotpath events) * @returns {Operation} The created operation */ static createUpdateOrCreateOperation(queryset, lookup = {}, defaults = {}, operationId = null) { const ModelClass = queryset.ModelClass; const primaryKeyField = ModelClass.primaryKeyField; const opId = operationId || `${uuid7()}`; // Get all current instances from the store for local filtering const modelStore = modelStoreRegistry.getStore(ModelClass); const allInstances = modelStore.render(); // Create a queryset filter for the lookup criteria const lookupFilter = { ...lookup }; const lookupQuerySet = new queryset.constructor(ModelClass).filter(lookupFilter); const lookupQuery = lookupQuerySet.build(); // Use local filtering to find matching instances const requiredPaths = getRequiredFields(lookupQuery, ModelClass); const prunedData = allInstances.map(inst => pickRequiredFields(requiredPaths, ModelClass.fromPk(inst[primaryKeyField], queryset))); const matchingPks = processQuery(prunedData, lookupQuery, ModelClass); // Find the corresponding instances const matchingInstances = allInstances.filter(inst => matchingPks.includes(inst[primaryKeyField])); const isCreatingNew = matchingInstances.length === 0; const isUpdating = !isCreatingNew; const effectiveType = isCreatingNew ? Type.CREATE : Type.UPDATE; // Create the instance data const instanceData = isCreatingNew ? { ...lookup, ...defaults, [primaryKeyField]: opId } : { ...matchingInstances[0], ...defaults }; return new Operation({ operationId: opId, type: effectiveType, instances: [instanceData], queryset: queryset, args: { lookup, defaults }, localOnly: queryset._optimisticOnly || false, }); } }