UNPKG

n8n-nodes-semble

Version:

n8n community node for Semble practice management system - automate bookings, patients, and product/service catalog management

334 lines (333 loc) 13.5 kB
"use strict"; /** * @fileoverview Error mapping and translation utilities for Semble API integration * @description Transforms Semble API errors into user-friendly messages with GraphQL error parsing and permission handling * @author Mike Hatcher * @website https://progenious.com * @namespace N8nNodesSemble.Core.ErrorMapper * @since 2.0.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultErrorMapper = exports.ErrorMapper = exports.DEFAULT_ERROR_MAPPER_CONFIG = void 0; exports.mapError = mapError; exports.processFieldPermissions = processFieldPermissions; const SembleError_1 = require("./SembleError"); /** * Default error mapper configuration */ exports.DEFAULT_ERROR_MAPPER_CONFIG = { includeStackTrace: false, includeContext: true, simplifyMessages: true, logErrors: true }; /** * Error mapping and translation utilities * * Provides comprehensive error handling for: * - GraphQL error parsing and transformation * - Permission error mapping with field-level granularity * - User-friendly message generation * - Error categorization and severity assessment * - Integration with n8n error handling patterns */ class ErrorMapper { constructor(config = {}) { this.config = { ...exports.DEFAULT_ERROR_MAPPER_CONFIG, ...config }; } /** * Maps any error to appropriate SembleError subclass */ mapError(error, context = {}) { if (error instanceof SembleError_1.SembleError) { return error; } // Handle GraphQL errors if (this.isGraphQLError(error)) { return this.mapGraphQLError(error, context); } // Handle HTTP/Network errors if (this.isHttpError(error)) { return this.mapHttpError(error, context); } // Handle validation errors if (this.isValidationError(error)) { return this.mapValidationError(error, context); } // Handle permission errors if (this.isPermissionError(error)) { return this.mapPermissionError(error, context); } // Default to generic SembleError return new SembleError_1.SembleError(this.sanitizeMessage(error.message || 'Unknown error occurred'), 'UNKNOWN_ERROR', context); } /** * Maps GraphQL errors from Semble API responses */ mapGraphQLError(error, context = {}) { var _a, _b, _c, _d, _e; const message = error.message || 'GraphQL operation failed'; const code = ((_a = error.extensions) === null || _a === void 0 ? void 0 : _a.code) || 'GRAPHQL_ERROR'; // Handle permission-specific GraphQL errors if ((_b = error.extensions) === null || _b === void 0 ? void 0 : _b.permission) { return new SembleError_1.SemblePermissionError(this.generatePermissionMessage(error.extensions.permission, error.extensions.field), error.extensions.permission, error.extensions.field, context.operation, context); } // Handle validation errors from GraphQL if (code === 'BAD_USER_INPUT' || code === 'VALIDATION_ERROR') { return new SembleError_1.SembleValidationError(message, (_c = error.extensions) === null || _c === void 0 ? void 0 : _c.field, (_d = error.extensions) === null || _d === void 0 ? void 0 : _d.value, (_e = error.extensions) === null || _e === void 0 ? void 0 : _e.constraints, context); } // Handle authentication errors if (code === 'UNAUTHENTICATED' || code === 'FORBIDDEN') { return new SembleError_1.SembleAuthError(message, code, 'api_key', 'Please check your API credentials and permissions', context); } // Generic API error return new SembleError_1.SembleAPIError(message, code, undefined, context, error); } /** * Maps HTTP errors to appropriate SembleError types */ mapHttpError(error, context = {}) { var _a, _b; const statusCode = ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) || error.status || 500; const message = this.getHttpErrorMessage(statusCode, error); const responseData = ((_b = error.response) === null || _b === void 0 ? void 0 : _b.data) || error.data; // Authentication errors if (statusCode === 401) { return new SembleError_1.SembleAuthError('Authentication failed - please check your API credentials', 'AUTHENTICATION_FAILED', 'api_key', 'Verify your API key is correct and has not expired', context); } // Permission errors if (statusCode === 403) { return new SembleError_1.SemblePermissionError('Access denied - insufficient permissions', 'unknown', undefined, context.operation, context); } // Not found errors if (statusCode === 404) { return new SembleError_1.SembleAPIError(`Resource not found: ${context.resource || 'Unknown resource'}`, 'RESOURCE_NOT_FOUND', statusCode, context, responseData); } // Rate limiting if (statusCode === 429) { return new SembleError_1.SembleAPIError('API rate limit exceeded - please try again later', 'RATE_LIMIT_EXCEEDED', statusCode, context, responseData); } // Server errors if (statusCode >= 500) { return new SembleError_1.SembleAPIError('Semble API server error - please try again', 'SERVER_ERROR', statusCode, context, responseData); } // Generic API error return new SembleError_1.SembleAPIError(message, 'HTTP_ERROR', statusCode, context, responseData); } /** * Maps validation errors */ mapValidationError(error, context = {}) { return new SembleError_1.SembleValidationError(error.message || 'Validation failed', error.field, error.value, error.constraints || [error.message], context); } /** * Maps permission errors with field-level granularity */ mapPermissionError(error, context = {}) { var _a; const permission = error.requiredPermission || 'unknown'; const field = error.field || ((_a = context.metadata) === null || _a === void 0 ? void 0 : _a.field); return new SembleError_1.SemblePermissionError(this.generatePermissionMessage(permission, field), permission, field, context.operation, context); } /** * Generates user-friendly permission error messages */ generatePermissionMessage(permission, field) { const baseMessage = `Access denied - missing permission: ${permission}`; if (field) { return `${baseMessage} for field '${field}'`; } return baseMessage; } /** * Gets appropriate HTTP error message based on status code */ getHttpErrorMessage(statusCode, error) { const defaultMessages = { 400: 'Bad request - please check your input data', 401: 'Authentication failed - invalid credentials', 403: 'Access denied - insufficient permissions', 404: 'Resource not found', 409: 'Conflict - resource already exists or is in use', 422: 'Validation failed - please check your input data', 429: 'Rate limit exceeded - please try again later', 500: 'Internal server error - please try again', 502: 'Bad gateway - service temporarily unavailable', 503: 'Service unavailable - please try again later', 504: 'Gateway timeout - request took too long' }; return error.message || defaultMessages[statusCode] || `HTTP error ${statusCode}`; } /** * Sanitizes error messages for user display */ sanitizeMessage(message) { if (!this.config.simplifyMessages) { return message; } // Remove technical stack traces and internal references return message .replace(/at .*\(.*\)/g, '') .replace(/\s+/g, ' ') .trim(); } /** * Type guards for error detection */ isGraphQLError(error) { return error && (error.extensions || (error.message && error.locations) || error.path); } isHttpError(error) { var _a; return error && (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) || error.status || error.statusCode); } isValidationError(error) { return error && (error.name === 'ValidationError' || error.code === 'VALIDATION_ERROR' || (error.field && error.constraints)); } isPermissionError(error) { return error && (error.code === 'PERMISSION_DENIED' || error.name === 'PermissionError' || error.requiredPermission || error.__MISSING_PERMISSION__); } /** * Processes field-level permission errors for response sanitization * Replaces restricted fields with permission error objects while maintaining schema structure */ processFieldPermissions(data, errors = []) { if (!data || typeof data !== 'object') { return data; } // Find permission errors in GraphQL response const permissionErrors = errors.filter(error => { var _a; return ((_a = error.extensions) === null || _a === void 0 ? void 0 : _a.code) === 'PERMISSION_DENIED'; }); if (permissionErrors.length === 0) { return data; } // Clone data to avoid mutation const processedData = JSON.parse(JSON.stringify(data)); // Replace restricted fields with permission error objects permissionErrors.forEach(error => { var _a; if (error.path && ((_a = error.extensions) === null || _a === void 0 ? void 0 : _a.field)) { this.setNestedProperty(processedData, error.path, SembleError_1.SemblePermissionError.createFieldPlaceholder(error.extensions.field, error.extensions.permission || 'unknown')); } }); return processedData; } /** * Sets nested property value following GraphQL error path */ setNestedProperty(obj, path, value) { let current = obj; for (let i = 0; i < path.length - 1; i++) { const key = path[i]; if (!(key in current)) { current[key] = typeof path[i + 1] === 'number' ? [] : {}; } current = current[key]; } current[path[path.length - 1]] = value; } /** * Logs errors based on configuration */ logError(error) { if (!this.config.logErrors) { return; } const logData = { message: error.message, code: error.code, category: error.category, severity: error.severity, ...(this.config.includeContext && { context: error.context }), ...(this.config.includeStackTrace && { stack: error.stack }) }; // Use appropriate log level based on severity switch (error.severity) { case 'critical': console.error('[SEMBLE CRITICAL]', logData); break; case 'high': console.error('[SEMBLE ERROR]', logData); break; case 'medium': console.warn('[SEMBLE WARNING]', logData); break; case 'low': console.info('[SEMBLE INFO]', logData); break; } } } exports.ErrorMapper = ErrorMapper; /** * Default error mapper instance */ exports.defaultErrorMapper = new ErrorMapper(); /** * Convenience function for quick error mapping with context * * Provides a simple interface to the default error mapper for transforming * raw errors into structured Semble errors with appropriate context and * user-friendly messages. * * @example * ```typescript * try { * await sembleApi.getPatients(); * } catch (error) { * const mappedError = mapError(error, { * operation: 'patient_retrieval', * resource: 'patients', * userId: 'user123' * }); * throw mappedError; * } * ``` * * @param error - The raw error to map (can be any type) * @param context - Additional context for error mapping * @returns Structured SembleError with proper type and messaging * @since 2.0.0 */ function mapError(error, context = {}) { return exports.defaultErrorMapper.mapError(error, context); } /** * Convenience function for processing field permissions from GraphQL responses * * Processes GraphQL response data and errors to handle field-level permissions, * removing or masking fields that the user doesn't have access to while * preserving accessible data. * * @example * ```typescript * const response = await sembleApi.query(patientQuery); * * // Process response to handle permission errors * const processedData = processFieldPermissions( * response.data, * response.errors || [] * ); * * // processedData now has inaccessible fields removed/masked * return processedData; * ``` * * @param data - The GraphQL response data to process * @param errors - Array of GraphQL errors that may include permission errors * @returns Processed data with permission restrictions applied * @since 2.0.0 */ function processFieldPermissions(data, errors = []) { return exports.defaultErrorMapper.processFieldPermissions(data, errors); }