UNPKG

okta-mcp-server

Version:

Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching

246 lines 9.76 kB
import { logger } from '../../utils/logger.js'; import { progressTracker } from './progress-tracker.js'; import { RateLimiter } from './rate-limiter.js'; export class BatchProcessor { client; options; rateLimiter; constructor(client, options) { this.client = client; this.options = options; // Initialize rate limiter for Okta API (default 600 requests per minute) this.rateLimiter = new RateLimiter({ max: options.rateLimitPerMinute, window: 60 * 1000, // 1 minute }); } /** * Process items in batches with rate limiting */ async processBatches(items, operationId, processor) { const results = []; const totalBatches = Math.ceil(items.length / this.options.batchSize); progressTracker.startOperation(operationId, totalBatches); for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) { const start = batchIndex * this.options.batchSize; const end = Math.min(start + this.options.batchSize, items.length); const batch = items.slice(start, end); logger.info(`Processing batch ${batchIndex + 1}/${totalBatches}`, { operationId, batchSize: batch.length, progress: `${start + 1}-${end}/${items.length}`, }); progressTracker.updateProgress(operationId, { currentBatch: batchIndex + 1, }); // Process batch items in parallel const batchPromises = batch.map(async (item, index) => { const globalIndex = start + index; // Check rate limit before each request const rateLimitResult = await this.rateLimiter.consume('okta-api'); if (!rateLimitResult.allowed) { // Wait for rate limit reset const waitTime = rateLimitResult.reset - Date.now(); logger.warn(`Rate limit reached, waiting ${waitTime}ms`, { operationId, remaining: rateLimitResult.remaining, reset: new Date(rateLimitResult.reset).toISOString(), }); await this.delay(waitTime); // Retry after waiting await this.rateLimiter.consume('okta-api'); } try { const result = await processor(item, globalIndex); progressTracker.incrementProgress(operationId, result.success, !result.success && result.identifier ? { identifier: result.identifier, error: result.error?.message || 'Unknown error', details: result.error, } : undefined); return result; } catch (error) { const errorResult = { success: false, error: error instanceof Error ? error : new Error('Unknown error'), }; progressTracker.incrementProgress(operationId, false, { error: errorResult.error?.message || 'Unknown error', details: error, }); if (!this.options.continueOnError) { throw error; } return errorResult; } }); // Wait for batch to complete const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // Add delay between batches if specified if (this.options.delayBetweenBatches && batchIndex < totalBatches - 1) { logger.debug(`Waiting ${this.options.delayBetweenBatches}ms between batches`, { operationId, }); await this.delay(this.options.delayBetweenBatches); } // Check if operation was cancelled const operation = progressTracker.getOperation(operationId); if (operation?.status === 'cancelled') { logger.info('Operation cancelled by user', { operationId }); break; } } return results; } /** * Process items with automatic retry on failure */ async processWithRetry(item, processor, maxRetries = 3, retryDelay = 1000) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const data = await processor(item); return { success: true, data }; } catch (error) { lastError = error instanceof Error ? error : new Error('Unknown error'); logger.warn(`Attempt ${attempt}/${maxRetries} failed`, { error: lastError.message, willRetry: attempt < maxRetries, }); if (attempt < maxRetries) { // Exponential backoff const delay = retryDelay * Math.pow(2, attempt - 1); await this.delay(delay); } } } return { success: false, error: lastError || new Error('Max retries exceeded'), }; } /** * Helper to delay execution */ delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Create users in batches */ async createUsersInBatches(users, operationId, options = {}) { return this.processBatches(users, operationId, async (user, index) => { try { const queryParams = { activate: options.activate ?? true, provider: options.provider ?? false, }; if (options.nextLogin !== undefined) { queryParams.nextLogin = options.nextLogin; } const createdUser = await this.client.createUser(user, queryParams); return { success: true, data: createdUser, identifier: user.profile['email'] || user.profile['login'], }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to create user'), identifier: user.profile['email'] || user.profile['login'] || `User ${index + 1}`, }; } }); } /** * Update users in batches */ async updateUsersInBatches(updates, operationId) { return this.processBatches(updates, operationId, async (update) => { try { const updatedUser = await this.client.updateUser(update.userId, update.userData); return { success: true, data: updatedUser, identifier: update.userId, }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to update user'), identifier: update.userId, }; } }); } /** * Delete/deactivate users in batches */ async deleteUsersInBatches(userIds, operationId, options) { return this.processBatches(userIds, operationId, async (userId) => { try { if (options.action === 'deactivate') { await this.client.deactivateUser(userId, options.sendEmail); } else { // First deactivate, then delete await this.client.deactivateUser(userId, false); await this.client.deleteUser(userId); } return { success: true, identifier: userId, }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error('Failed to delete user'), identifier: userId, }; } }); } /** * Fetch all users matching a filter */ async *fetchAllUsers(filter, search) { let after; let hasMore = true; while (hasMore) { const params = { limit: 200, // Max page size }; if (after !== undefined) params.after = after; if (filter !== undefined) params.filter = filter; if (search !== undefined) params.search = search; const response = await this.client.listUsers(params); yield response.data; // Check for next page const linkHeader = response.headers?.link; if (linkHeader && linkHeader.includes('rel="next"')) { const match = linkHeader.match(/<[^>]+after=([^&>]+)[^>]*>;\s*rel="next"/); after = match ? match[1] : undefined; hasMore = !!after; } else { hasMore = false; } // Rate limit between pages await this.rateLimiter.consume('okta-api'); } } } //# sourceMappingURL=batch-processor.js.map