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