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