@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
JavaScript
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,
});
}
}