UNPKG

@statezero/core

Version:

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

138 lines (137 loc) 6.04 kB
import PQueue from 'p-queue'; import axios from 'axios'; import { configInstance } from '../../config.js'; import { replaceTempPks } from './tempPk.js'; import { parseStateZeroError, MultipleObjectsReturned, DoesNotExist } from './errors.js'; import { FileObject } from './files.js'; import { querysetStoreRegistry } from '../../syncEngine/registries/querysetStoreRegistry.js'; const apiCallQueue = new PQueue({ concurrency: 1 }); /** * Process included entities from a response and register them in the model store. * Uses the model registry to find the appropriate model class for each entity type. * * @param {ModelStoreRegistry} modelStoreRegistry - The model store registry to use * @param {Object} included - The included entities object from the response * @param {Function} ModelClass - The base model class to get the configKey from * @param {QuerySet} [queryset] - Optional queryset to track which PKs came from this fetch */ export function processIncludedEntities(modelStoreRegistry, included, ModelClass, queryset = null) { if (!included) return; const configKey = ModelClass.configKey; // Get the queryset store if a queryset is provided let querysetStore = null; if (queryset) { querysetStore = querysetStoreRegistry.getStore(queryset); } try { // Process each model type for (const [modelName, entityMap] of Object.entries(included)) { // Get the appropriate model class for this model name const EntityClass = configInstance.getModelClass(modelName, configKey); if (!EntityClass) { console.error(`Model class not found for ${modelName} in config ${configKey}`); throw new Error(`Model class not found for ${modelName}`); } // Track which PKs are included if a queryset store is available if (querysetStore) { if (!querysetStore.includedPks.has(modelName)) { querysetStore.includedPks.set(modelName, new Set()); } const pksSet = querysetStore.includedPks.get(modelName); // Add all PKs from this model to the set for (const pk of Object.keys(entityMap)) { pksSet.add(Number(pk)); } } // Register all entities in the model store in a single batch call const entities = Object.values(entityMap); modelStoreRegistry.setEntities(EntityClass, entities); } } catch (error) { console.error("Error processing included entities with model registry:", error); throw new Error(`Failed to process included entities: ${error.message}`); } } /** * Makes an API call to the backend with the given QuerySet. * Automatically handles FileObject replacement with file paths for write operations. * * @param {QuerySet} querySet - The QuerySet to execute. * @param {string} operationType - The type of operation to perform. * @param {Object} args - Additional arguments for the operation. * @param {string} operationId - A unique id for the operation * @param {Function} beforeExit - Optional callback before returning * @param {string} canonicalId - Optional canonical_id for cache sharing * @returns {Promise<Object>} The API response. */ export async function makeApiCall(querySet, operationType, args = {}, operationId, beforeExit = null, canonicalId = null) { const ModelClass = querySet.ModelClass; const config = configInstance.getConfig(); const backend = config.backendConfigs[ModelClass.configKey]; if (!backend) { throw new Error(`No backend configuration found for key: ${ModelClass.configKey}`); } // Build the base query let query = { ...querySet.build(), type: operationType, }; // Add args to the query if provided if (args && Object.keys(args).length > 0) { query = { ...query, ...args, }; } const { serializerOptions, ...restOfQuery } = query; let payload = { ast: { query: restOfQuery, serializerOptions, }, }; let limit = payload?.ast?.serializerOptions?.limit; let overfetch = payload?.ast?.serializerOptions?.overfetch || 10; if (limit && overfetch) { payload.ast.serializerOptions.limit = limit + overfetch; } // Determine if this is a write operation that needs FileObject processing const writeOperations = [ "create", "bulk_create", "update", "delete", "update_instance", "delete_instance", "get_or_create", "update_or_create" ]; const isWriteOperation = writeOperations.includes(operationType); const baseUrl = backend.API_URL.replace(/\/+$/, ""); const finalUrl = `${baseUrl}/${ModelClass.modelName}/`; const headers = backend.getAuthHeaders ? backend.getAuthHeaders() : {}; if (operationId) { headers["X-Operation-ID"] = operationId; } if (canonicalId) { headers["X-Canonical-ID"] = canonicalId; } // Use the queue for write operations, bypass for read operations const apiCall = async () => { try { let response = await axios.post(finalUrl, replaceTempPks(payload), { headers }); if (typeof beforeExit === 'function' && response?.data) { await beforeExit(response.data); } return response.data; } catch (error) { if (error.response && error.response.data) { const parsedError = parseStateZeroError(error.response.data); if (Error.captureStackTrace) { Error.captureStackTrace(parsedError, makeApiCall); } throw parsedError; } throw new Error(`API call failed: ${error.message}`); } }; // Queue write operations, execute read operations immediately return isWriteOperation ? apiCallQueue.add(apiCall) : apiCall(); }