UNPKG

@sudowealth/schwab-api

Version:

TypeScript client for Charles Schwab API with OAuth support, market data, trading functionality, and complete type safety

288 lines (287 loc) 9.7 kB
import { SchwabAuthError, AuthErrorCode, SchwabApiError, ApiErrorCode, } from '../errors.js'; /** * Enhanced error mapper with extensible mapping support */ export class SchwabErrorMapper { customMappers = []; authErrorMappings = new Map(); apiErrorMappings = new Map(); constructor(options) { // Initialize default mappings this.initializeDefaultMappings(); // Add custom mappers if (options?.customMappers) { this.customMappers.push(...options.customMappers); } // Override auth mappings if (options?.customAuthMappings) { Object.entries(options.customAuthMappings).forEach(([code, mapping]) => { this.authErrorMappings.set(code, mapping); }); } // Override API mappings if (options?.customApiMappings) { Object.entries(options.customApiMappings).forEach(([code, mapping]) => { this.apiErrorMappings.set(code, mapping); }); } } /** * Initialize default error mappings */ initializeDefaultMappings() { // Auth error mappings this.authErrorMappings.set(AuthErrorCode.INVALID_CODE, { message: 'Invalid or expired authorization code', httpStatus: 400, isRetryable: false, requiresReauth: true, }); this.authErrorMappings.set(AuthErrorCode.TOKEN_EXPIRED, { message: 'Refresh token has expired', httpStatus: 401, isRetryable: false, requiresReauth: true, }); this.authErrorMappings.set(AuthErrorCode.UNAUTHORIZED, { message: 'Invalid client credentials', httpStatus: 401, isRetryable: false, requiresReauth: true, }); this.authErrorMappings.set(AuthErrorCode.REFRESH_NEEDED, { message: 'Access token needs refresh', httpStatus: 401, isRetryable: true, requiresReauth: false, }); this.authErrorMappings.set(AuthErrorCode.NETWORK, { message: 'Network error during authentication', httpStatus: 503, isRetryable: true, requiresReauth: false, }); this.authErrorMappings.set(AuthErrorCode.TOKEN_PERSISTENCE_LOAD_FAILED, { message: 'Failed to load stored tokens', httpStatus: 500, isRetryable: true, requiresReauth: false, }); this.authErrorMappings.set(AuthErrorCode.TOKEN_PERSISTENCE_SAVE_FAILED, { message: 'Failed to save tokens', httpStatus: 500, isRetryable: true, requiresReauth: false, }); this.authErrorMappings.set(AuthErrorCode.TOKEN_VALIDATION_ERROR, { message: 'Token validation failed', httpStatus: 401, isRetryable: false, requiresReauth: true, }); this.authErrorMappings.set(AuthErrorCode.PKCE_VERIFIER_MISSING, { message: 'PKCE code verifier is missing', httpStatus: 400, isRetryable: false, requiresReauth: true, }); // API error mappings this.apiErrorMappings.set(ApiErrorCode.RATE_LIMIT, { message: 'Rate limit exceeded', httpStatus: 429, isRetryable: true, requiresReauth: false, }); this.apiErrorMappings.set(ApiErrorCode.UNAUTHORIZED, { message: 'Unauthorized API access', httpStatus: 401, isRetryable: false, requiresReauth: true, }); this.apiErrorMappings.set(ApiErrorCode.FORBIDDEN, { message: 'Access forbidden', httpStatus: 403, isRetryable: false, requiresReauth: false, }); this.apiErrorMappings.set(ApiErrorCode.NOT_FOUND, { message: 'Resource not found', httpStatus: 404, isRetryable: false, requiresReauth: false, }); this.apiErrorMappings.set(ApiErrorCode.SERVER_ERROR, { message: 'Internal server error', httpStatus: 500, isRetryable: true, requiresReauth: false, }); this.apiErrorMappings.set(ApiErrorCode.SERVICE_UNAVAILABLE, { message: 'Service temporarily unavailable', httpStatus: 503, isRetryable: true, requiresReauth: false, }); this.apiErrorMappings.set(ApiErrorCode.GATEWAY_ERROR, { message: 'Gateway error', httpStatus: 502, isRetryable: true, requiresReauth: false, }); this.apiErrorMappings.set(ApiErrorCode.TIMEOUT, { message: 'Request timeout', httpStatus: 408, isRetryable: true, requiresReauth: false, }); this.apiErrorMappings.set(ApiErrorCode.NETWORK, { message: 'Network error', httpStatus: 0, isRetryable: true, requiresReauth: false, }); } /** * Map a Schwab error to an error mapping result */ map(error) { // Try custom mappers first for (const mapper of this.customMappers) { const result = mapper.map(error); if (result) { return result; } } // Handle SchwabAuthError if (error instanceof SchwabAuthError) { return this.mapAuthError(error); } // Handle SchwabApiError if (error instanceof SchwabApiError) { return this.mapApiError(error); } // Default mapping for unknown errors return { code: ApiErrorCode.UNKNOWN, message: error instanceof Error ? error.message : 'Unknown error', httpStatus: 500, isRetryable: false, requiresReauth: false, }; } /** * Map an auth error */ mapAuthError(error) { const baseMapping = this.authErrorMappings.get(error.code) || {}; return { code: error.code, message: baseMapping.message || error.message, httpStatus: baseMapping.httpStatus || error.status || 500, isRetryable: baseMapping.isRetryable ?? error.isRetryable(), requiresReauth: baseMapping.requiresReauth ?? true, context: { originalMessage: error.message, ...(error.body ? { body: error.body } : {}), }, }; } /** * Map an API error */ mapApiError(error) { const baseMapping = this.apiErrorMappings.get(error.code) || {}; return { code: error.code, message: baseMapping.message || error.getFormattedDetails(), httpStatus: baseMapping.httpStatus || error.status, isRetryable: baseMapping.isRetryable ?? error.isRetryable(), requiresReauth: baseMapping.requiresReauth ?? error.status === 401, context: { originalMessage: error.message, requestId: error.getRequestId(), ...(error.metadata && { metadata: error.metadata }), }, }; } /** * Add a custom error mapper */ addMapper(mapper) { this.customMappers.push(mapper); } /** * Override a specific auth error mapping */ setAuthMapping(code, mapping) { this.authErrorMappings.set(code, mapping); } /** * Override a specific API error mapping */ setApiMapping(code, mapping) { this.apiErrorMappings.set(code, mapping); } } /** * Default error mapper instance */ export const defaultErrorMapper = new SchwabErrorMapper(); /** * Map a Schwab SDK error to appropriate error metadata * This is a convenience function that uses the default mapper */ export function mapSchwabError(error) { return defaultErrorMapper.map(error); } /** * Create an error handler function for Express/Hono style frameworks */ export function schwabErrorHandler(options) { const mapper = options?.customMapper || defaultErrorMapper; return (error, _req, res) => { const mapping = mapper.map(error); const response = { error: { code: mapping.code, message: mapping.message, ...(mapping.context?.requestId && { requestId: mapping.context.requestId, }), }, ...(options?.includeStackTrace && error instanceof Error && { stack: error.stack, }), }; // Set appropriate headers if (mapping.context?.requestId) { res.setHeader('X-Request-ID', mapping.context.requestId); } res.status(mapping.httpStatus).json(response); }; } /** * Check if an error requires reauthentication */ export function requiresReauthentication(error) { const mapping = defaultErrorMapper.map(error); return mapping.requiresReauth; } /** * Get retry information from an error */ export function getRetryInfo(error) { const mapping = defaultErrorMapper.map(error); const result = { isRetryable: mapping.isRetryable, }; // Check for retry-after information in API errors if (error instanceof SchwabApiError && error.hasRetryInfo()) { const retryDelay = error.getRetryDelayMs(); if (retryDelay !== null) { result.retryAfterMs = retryDelay; } } return result; }