@sudowealth/schwab-api
Version:
TypeScript client for Charles Schwab API with OAuth support, market data, trading functionality, and complete type safety
170 lines (169 loc) • 8.58 kB
JavaScript
import * as authNs from './auth/index.js';
import { getSchwabApiConfigDefaults,
// resolveBaseUrl, // Not directly used in the provided snippet modification
} from './core/config.js';
import { createRequestContext, createEndpoint as coreHttpCreateEndpoint, // Aliased import
} from './core/http.js';
import * as errorsNs from './errors.js';
import { SchwabError, SchwabApiError, SchwabAuthError, SchwabRateLimitError, SchwabAuthorizationError, SchwabInvalidRequestError, SchwabNotFoundError, SchwabServerError, SchwabNetworkError, SchwabTimeoutError, isSchwabError, isSchwabApiError, ApiErrorCode, AuthErrorCode, ErrorResponseSchema, parseErrorResponse, } from './errors.js';
import * as marketDataNs from './market-data/index.js';
import { compose } from './middleware/compose.js';
import { buildMiddlewarePipeline, } from './middleware/pipeline.js';
import * as schemasNs from './schemas/index.js';
import * as traderNs from './trader/index.js';
/**
* Helper function to recursively process namespaces and convert Meta objects to endpoints
* Uses the ProcessNamespaceResult type to maintain proper typing of the result
*/
function processNamespace(ns, clientCreateEndpoint) {
// Process the namespace and return a properly typed result
const result = {};
// First pass - process all Meta objects and prepare for endpoint creation
const endpointsToCreate = {};
for (const key in ns) {
if (Object.hasOwn(ns, key)) {
const value = ns[key];
if (typeof value === 'object' && value !== null) {
// Check if it's likely an EndpointMetadata object exported from an endpoints module
// It should have properties like 'method' and 'path', and we're looking for a 'Meta' suffix.
// Also ensure it's not a sub-namespace object that could also contain such properties.
// A simple check for 'method' and 'path' and key ending with 'Meta' is a heuristic.
if (key.endsWith('Meta') &&
'method' in value &&
'path' in value &&
!Object.values(value).some((v) => typeof v === 'object' &&
v !== null &&
'method' in v &&
'path' in v)) {
const endpointName = key.substring(0, key.length - 'Meta'.length);
endpointsToCreate[endpointName] = value;
// Also keep the original Meta object
result[key] = value;
}
else {
// Recursively process sub-namespaces (like 'quotes' within 'marketData')
result[key] = processNamespace(value, clientCreateEndpoint);
}
}
else {
// Copy other properties (functions like extractQuoteErrors, primitives, etc.) as is
result[key] = value;
}
}
}
// Second pass - create all endpoints
for (const [endpointName, meta] of Object.entries(endpointsToCreate)) {
result[endpointName] = clientCreateEndpoint(meta);
}
// Cast the result to the expected type
// This maintains type safety while allowing the actual implementation to be dynamic
return result;
}
// Implementation signature
export function createApiClient(options = {}) {
const finalConfig = { ...getSchwabApiConfigDefaults(), ...options.config };
let authManager;
if (typeof options.auth === 'string') {
// This path makes createApiClient inherently async.
// For synchronous init, this path should not be taken.
throw new Error('createApiClient with a string token is an async operation and not supported in this synchronous path. Please provide an EnhancedTokenManager instance.');
}
else if (options.auth && 'strategy' in options.auth) {
// This path is also async because createSchwabAuth could be async or ETM setup could be.
throw new Error('createApiClient with AuthFactoryConfig is an async operation and not supported in this synchronous path. Please provide an EnhancedTokenManager instance.');
}
else if (options.auth instanceof authNs.EnhancedTokenManager) {
authManager = options.auth;
}
else {
// Throw an error instead of creating a dummy token manager
throw new SchwabAuthError(AuthErrorCode.INVALID_CONFIGURATION, 'Authentication configuration is required. Please provide either:\n' +
'- An EnhancedTokenManager instance\n' +
'- A string access token (async operation)\n' +
'- An AuthFactoryConfig object (async operation)\n\n' +
'Example:\n' +
'const authManager = new EnhancedTokenManager({ clientId, clientSecret, redirectUri });\n' +
'const client = createApiClient({ auth: authManager });');
}
const middlewareConfig = options.middleware ?? {};
// Call the synchronous buildMiddlewarePipeline, ensuring authManager is an ETM instance
const middleware = buildMiddlewarePipeline(middlewareConfig, authManager);
const chain = compose(...middleware);
const apiClientContext = createRequestContext(finalConfig, (req) => chain(req));
const clientBoundCreateEndpoint = (meta) => coreHttpCreateEndpoint(apiClientContext, meta);
const processedMarketData = processNamespace(marketDataNs, clientBoundCreateEndpoint);
const processedTrader = processNamespace(traderNs, clientBoundCreateEndpoint);
const client = {
marketData: processedMarketData,
trader: processedTrader,
schemas: schemasNs,
auth: authNs,
errors: {
SchwabError,
isSchwabError,
SchwabApiError,
isSchwabApiError,
SchwabRateLimitError,
SchwabAuthorizationError,
SchwabInvalidRequestError,
SchwabNotFoundError,
SchwabServerError,
SchwabNetworkError,
SchwabTimeoutError,
SchwabAuthError,
ApiErrorCode,
AuthErrorCode,
ErrorResponse: ErrorResponseSchema,
parseErrorResponse,
},
_context: apiClientContext,
createEndpoint: clientBoundCreateEndpoint,
all: {
marketData: processedMarketData,
trader: processedTrader,
schemas: schemasNs,
auth: authNs,
errors: errorsNs,
},
debugAuth: async (debugOptions = {}) => {
// Ensure authManager is the one associated with this client instance
const currentAuthManager = authManager; // Capture from closure
const { logger } = apiClientContext;
logger.info('[debugAuth] Starting auth diagnostics');
try {
// Use the new getDiagnostics method from EnhancedTokenManager
const diagnostics = await currentAuthManager.getDiagnostics(debugOptions);
// Log diagnostics summary for troubleshooting
logger.info('[debugAuth] Auth diagnostics complete:', {
authManagerType: diagnostics.authManagerType,
supportsRefresh: diagnostics.supportsRefresh,
hasAccessToken: diagnostics.tokenStatus.hasAccessToken,
hasRefreshToken: diagnostics.tokenStatus.hasRefreshToken,
isExpired: diagnostics.tokenStatus.isExpired,
expiresInSeconds: diagnostics.tokenStatus.expiresInSeconds,
apiEnvironment: diagnostics.environment.apiEnvironment,
});
return diagnostics;
}
catch (error) {
logger.error('[debugAuth] Error during diagnostics:', error);
// Return error information in a consistent format
return {
authManagerType: authManager.constructor.name,
supportsRefresh: authManager.supportsRefresh(),
tokenStatus: {
hasAccessToken: false,
hasRefreshToken: false,
isExpired: true,
errorMessage: error instanceof Error ? error.message : String(error),
diagnosticsError: true,
},
environment: {
apiEnvironment: finalConfig.environment,
},
};
}
},
};
return client;
}