Skip to main content
Glama
bulk-operations.ts5.34 kB
/** * Sprint 9 REFACTOR: Common utilities for bulk update operations * Extracted from requirement-service, phase-service, solution-service */ export interface BulkUpdateConfig<TIdField extends string> { entityType: string; entityIdField: TIdField; updateFn: (entityId: string, updates: Record<string, unknown>) => Promise<void>; planId: string; updates: (Record<TIdField, string> & { updates: Record<string, unknown> })[]; atomic?: boolean; storage?: { // BUGFIX: loadEntities must return full entities (not just { id }), so snapshot/rollback // preserves ALL fields. Previously declared Array<{ id: string }> which was too narrow. loadEntities: (planId: string, entityType: string) => Promise<Record<string, unknown>[]>; saveEntities: (planId: string, entityType: string, entities: Record<string, unknown>[]) => Promise<void>; }; } export interface BulkUpdateResult<TIdField extends string> { updated: number; failed: number; results: (Record<TIdField, string> & { success: boolean; error?: string; })[]; } /** * Generic bulk update handler with atomic/non-atomic modes * * @param config Configuration for bulk update operation * @returns Result with success/error for each update * * ATOMIC MODE IMPLEMENTATION: * Uses snapshot/rollback pattern to ensure true atomicity: * 1. Load current state and create deep copy (snapshot) * 2. Validate all entity IDs exist * 3. Execute updates sequentially (each saves to disk immediately) * 4. If any update fails, restore snapshot via saveEntities (rollback) * * This ensures that either ALL updates succeed or NONE persist. * Trade-off: requires extra I/O for snapshot and potential rollback. * * @example * ```ts * const result = await bulkUpdateEntities({ * entityType: 'requirements', * entityIdField: 'requirementId', * updateFn: (id, updates) => this.updateRequirement({ planId, requirementId: id, updates }), * planId, * updates, * atomic: true, * storage: this.storage * }); * ``` */ export async function bulkUpdateEntities<TIdField extends string>( config: BulkUpdateConfig<TIdField> ): Promise<BulkUpdateResult<TIdField>> { const { entityType, entityIdField, updateFn, planId, updates, atomic = false, storage } = config; // API contract validation: atomic mode requires storage for snapshot/rollback if (atomic && !storage) { throw new Error('storage is required for atomic mode (needed for snapshot/rollback)'); } const results: (Record<TIdField, string> & { success: boolean; error?: string })[] = []; let updated = 0; let failed = 0; // Atomic mode: validate all entities exist first, then execute with rollback capability if (atomic && storage) { // BUGFIX: Create snapshot BEFORE any modifications for atomic rollback const currentEntities = await storage.loadEntities(planId, entityType); const snapshot = JSON.parse(JSON.stringify(currentEntities)) as Record<string, unknown>[]; const entityMap = new Map(currentEntities.map((e) => [e.id, e])); // Pre-validate: check all entities exist for (const update of updates) { const entityId = update[entityIdField]; if (!entityMap.has(entityId)) { throw new Error( `${entityType.slice(0, -1)} ${entityId} not found (atomic mode - rolling back)` ); } } // Execute updates sequentially with rollback on failure try { for (const update of updates) { const entityId = update[entityIdField]; // Each updateFn call saves to disk immediately await updateFn(entityId, update.updates); results.push({ [entityIdField]: entityId, success: true } as Record<TIdField, string> & { success: boolean }); updated++; } } catch (error: unknown) { // CRITICAL: Rollback all changes by restoring snapshot // BUGFIX: Capture both original error and any rollback failure for complete diagnostics const errorMessage = error instanceof Error ? error.message : String(error); try { await storage.saveEntities(planId, entityType, snapshot); throw new Error(`Atomic bulk update failed: ${errorMessage} (rolled back all changes)`); } catch (rollbackError: unknown) { // If rollback also fails, include BOTH errors in the message const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError); throw new Error( `Atomic bulk update failed: ${errorMessage}. ` + `Rollback also failed: ${rollbackMessage}` ); } } } else { // Non-atomic mode: process each update independently for (const update of updates) { const entityId = update[entityIdField]; try { await updateFn(entityId, update.updates); results.push({ [entityIdField]: entityId, success: true } as Record<TIdField, string> & { success: boolean }); updated++; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); results.push({ [entityIdField]: entityId, success: false, error: errorMessage, } as Record<TIdField, string> & { success: boolean; error: string }); failed++; } } } return { updated, failed, results }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cppmyjob/cpp-mcp-planner'

If you have feedback or need assistance with the MCP directory API, please join our Discord server