@statezero/core
Version:
The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate
232 lines (231 loc) • 9.94 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 _Operation__instances, _Operation__frozenInstances;
import { MAX, v7 as uuidv7 } from "uuid";
import { isNil } from 'lodash-es';
import mitt from 'mitt';
import { tempPkMap } from "../../flavours/django/tempPk";
import { modelStoreRegistry } from "../registries/modelStoreRegistry.js";
export const operationEvents = mitt();
export const Status = {
CREATED: 'operation:created',
UPDATED: 'operation:updated',
CONFIRMED: 'operation:confirmed',
REJECTED: 'operation:rejected',
CLEAR: 'clear:all',
MUTATED: 'operation:mutated'
};
export const Type = {
CREATE: 'create',
BULK_CREATE: 'bulk_create',
UPDATE: 'update',
DELETE: 'delete',
UPDATE_INSTANCE: 'update_instance',
DELETE_INSTANCE: 'delete_instance',
GET_OR_CREATE: 'get_or_create',
UPDATE_OR_CREATE: 'update_or_create',
CHECKPOINT: 'checkpoint',
// Aggregation operations
COUNT: 'count',
MIN: 'min',
MAX: 'max',
AVG: 'avg',
SUM: 'sum',
AGGREGATE: 'aggregate',
};
export class Operation {
constructor(data, restore = false) {
_Operation__instances.set(this, void 0);
_Operation__frozenInstances.set(this, void 0);
if (!data || typeof data !== 'object') {
throw new Error("Operation constructor requires a data object.");
}
if (!data.type) {
throw new Error("Operation data must include a 'type'.");
}
if (!data.instances) {
throw new Error("Operation data must include 'instances'.");
}
this.operationId = data.operationId || `op_${uuidv7()}`;
this.type = data.type;
this.status = data.status || Status.CREATED;
this.queryset = data.queryset;
this.args = data.args;
this.doNotPropagate = data.doNotPropagate || false;
this.localOnly = data.localOnly || false;
let ModelClass = this.queryset.ModelClass;
let instances = data.instances;
// guarantee instances is an array
if (!isNil(instances)) {
instances = Array.isArray(data.instances) ? data.instances : [data.instances];
}
// coerce to object format if its not already
let pkField = ModelClass.primaryKeyField;
if (instances.some(instance => isNil(instance) || typeof instance !== 'object' || !(pkField in instance))) {
throw new Error(`All operation instances must be objects with the '${pkField}' field`);
}
__classPrivateFieldSet(this, _Operation__instances, instances, "f");
// Batch render to warm the cache, then serialize each instance to get plain objects
const pks = instances.map(i => i[pkField]);
const store = modelStoreRegistry.getStore(ModelClass);
store.render(pks, true, false);
__classPrivateFieldSet(this, _Operation__frozenInstances, instances.map(i => ModelClass.fromPk(i[pkField]).serialize()), "f");
this.timestamp = data.timestamp || Date.now();
if (restore)
return;
operationRegistry.register(this);
// Emit operation created event with the entire operation
operationEvents.emit(Status.CREATED, this);
}
/**
* Getter for instances that replaces any temporary PKs with real PKs
*/
get instances() {
if (tempPkMap.size === 0)
return __classPrivateFieldGet(this, _Operation__instances, "f");
const ModelClass = this.queryset.ModelClass;
const pkField = ModelClass.primaryKeyField;
return __classPrivateFieldGet(this, _Operation__instances, "f").map(instance => {
const pk = instance[pkField];
if (typeof pk === 'string' && tempPkMap.has(pk.replace(/[{}]/g, ''))) {
// Return a new instance with the real PK
return {
...instance,
[pkField]: tempPkMap.get(pk.replace(/[{}]/g, ''))
};
}
return instance;
});
}
/**
* Setter for instances
*/
set instances(value) {
__classPrivateFieldSet(this, _Operation__instances, Array.isArray(value) ? value : [value], "f");
}
/**
* Getter for frozenInstances that replaces any temporary PKs with real PKs
*/
get frozenInstances() {
if (tempPkMap.size === 0)
return __classPrivateFieldGet(this, _Operation__frozenInstances, "f");
const ModelClass = this.queryset.ModelClass;
const pkField = ModelClass.primaryKeyField;
return __classPrivateFieldGet(this, _Operation__frozenInstances, "f").map(instance => {
const pk = instance[pkField];
if (typeof pk === 'string' && tempPkMap.has(pk.replace(/[{}]/g, ''))) {
// Return a new instance with the real PK
return {
...instance,
[pkField]: tempPkMap.get(pk.replace(/[{}]/g, ''))
};
}
return instance;
});
}
/**
* Setter for frozenInstances
*/
set frozenInstances(value) {
__classPrivateFieldSet(this, _Operation__frozenInstances, Array.isArray(value) ? value : [value], "f");
}
/**
* Get primary keys of all instances in this operation
* Returns primary keys as simple values (not objects)
*/
get instancePks() {
const pkField = this.queryset.ModelClass.primaryKeyField;
return this.instances.map(instance => instance[pkField]);
}
/**
* Update this operation's status and emit an event
* @param {string} status - New status ('confirmed', 'rejected', etc.)
* @param {Array|Object|null} [instances=null] - New instances for the operation
*/
updateStatus(status, instances = null) {
this.status = status;
this.timestamp = Date.now();
if (instances !== null) {
this.instances = Array.isArray(instances) ? instances : [instances];
}
operationEvents.emit(status, this);
}
/**
* Updates this operation with new data and emits the appropriate event
* @param {Object} newData - New data to update the operation with
* @returns {Operation} - Returns this operation instance for chaining
*/
mutate(newData) {
// Ensure instances is always an array
if (newData.instances && !Array.isArray(newData.instances)) {
newData.instances = [newData.instances];
}
// Use Object.assign to update all properties at once
Object.assign(this, newData);
// Update timestamp
this.timestamp = Date.now();
// Emit the mutated event with the updated operation
operationEvents.emit(Status.MUTATED, this);
return this;
}
}
_Operation__instances = new WeakMap(), _Operation__frozenInstances = new WeakMap();
class OperationRegistry {
constructor() {
this._operations = new Map();
}
/**
* Registers a pre-constructed Operation instance in the registry.
* Ensures the operationId is unique within the registry.
* Throws an Error if the operationId already exists.
*
* @param {Operation} operation - The fully instantiated Operation object to register.
* @throws {Error} If the input is not a valid operation object or if an operation with the same operationId already exists.
*/
register(operation) {
if (!(operation instanceof Operation)) {
throw new Error("OperationRegistry.register requires an Operation object.");
}
// Ensure the ID is unique before registering
if (this._operations.has(operation.operationId)) {
// Throw an error if the ID is already used.
console.warn(`OperationId ${operation.operationId} is already used, overriding existing operation!`);
}
// Register the provided operation object
this._operations.set(operation.operationId, operation);
}
/**
* Retrieves an operation by its ID.
* @param {string} operationId - The ID of the operation.
* @returns {Operation | undefined} The operation instance or undefined if not found.
*/
get(operationId) {
return this._operations.get(operationId);
}
/**
* Checks if an operation with the given ID exists in the registry.
* @param {string} operationId - The ID of the operation to check.
* @returns {boolean} True if the operation exists, false otherwise.
*/
has(operationId) {
return this._operations.has(operationId);
}
/**
* Clears all operations from the registry.
*/
clear() {
console.log("OperationRegistry: Clearing all operations.");
this._operations.clear();
operationEvents.emit(Status.CLEAR);
}
}
export const operationRegistry = new OperationRegistry();