UNPKG

ministry-platform-provider

Version:

TypeScript client library for Ministry Platform API integration

395 lines (299 loc) 11.3 kB
# MinistryPlatformClient - Core HTTP Client with Authentication ## Overview The `MinistryPlatformClient` class serves as the core HTTP client for all Ministry Platform API operations. It manages OAuth2 authentication, token lifecycle, and provides a configured HttpClient instance to all service classes. ## Class Structure ```typescript export class MinistryPlatformClient { private token: string = ""; private expiresAt: Date = new Date(0); private baseUrl: string; private httpClient: HttpClient; constructor() } ``` ## Constants ```typescript const TOKEN_LIFE = 5 * 60 * 1000; // 5 minutes ``` **Purpose**: Defines the token lifetime in milliseconds. Tokens are refreshed 5 minutes before their actual expiration to ensure continuous API access. ## Constructor ```typescript constructor() { this.baseUrl = process.env.MINISTRY_PLATFORM_BASE_URL!; this.httpClient = new HttpClient(this.baseUrl, () => this.token); } ``` **Initialization Process**: 1. Sets the base URL from environment variable 2. Creates HttpClient instance with token getter function 3. Initializes token expiration to epoch (forces immediate refresh) **Environment Variables Required**: - `MINISTRY_PLATFORM_BASE_URL`: The base URL for your Ministry Platform instance ## Methods ### ensureValidToken ```typescript public async ensureValidToken(): Promise<void> ``` **Purpose**: Ensures the authentication token is valid and refreshes it if necessary **Process**: 1. Checks if current token is expired 2. If expired, requests new token using client credentials 3. Updates token and expiration time 4. Provides comprehensive logging for debugging **Example Usage**: ```typescript // Always call before making API requests await this.client.ensureValidToken(); const data = await this.client.getHttpClient().get('/tables/Contacts'); ``` **Logging Output**: ``` Checking token validity... Expires at: 2024-01-15T10:30:00.000Z Current time: 2024-01-15T10:25:00.000Z Token expired, refreshing... Token refreshed. Expires at: 2024-01-15T10:35:00.000Z ``` **Error Handling**: - Throws errors from `getClientCredentialsToken()` if authentication fails - Logs all token lifecycle events for debugging ### getHttpClient ```typescript public getHttpClient(): HttpClient ``` **Purpose**: Returns the configured HttpClient instance **Returns**: HttpClient instance with automatic token injection **Example Usage**: ```typescript const httpClient = this.client.getHttpClient(); const contacts = await httpClient.get<Contact[]>('/tables/Contacts'); ``` ## Token Management ### Token Lifecycle 1. **Initial State**: Token is empty, expiration is set to epoch 2. **First Request**: `ensureValidToken()` detects expired token and requests new one 3. **Subsequent Requests**: Token is reused until 5 minutes before expiration 4. **Refresh**: New token is requested automatically when needed ### Token Refresh Logic ```typescript if (this.expiresAt < new Date()) { console.log("Token expired, refreshing..."); const creds = await getClientCredentialsToken(); this.token = creds.access_token; this.expiresAt = new Date(Date.now() + TOKEN_LIFE); } ``` **Key Features**: - Proactive refresh (5 minutes before expiration) - Automatic retry on expired tokens - Comprehensive logging for debugging - Thread-safe token management ## Integration with Services All service classes receive the MinistryPlatformClient instance in their constructor: ```typescript export class TableService { private client: MinistryPlatformClient; constructor(client: MinistryPlatformClient) { this.client = client; } public async getTableRecords<T>(table: string, params?: TableQueryParams): Promise<T[]> { // Always ensure token is valid before making requests await this.client.ensureValidToken(); const endpoint = `/tables/${encodeURIComponent(table)}`; return await this.client.getHttpClient().get<T[]>(endpoint, params); } } ``` ## Usage Examples ### Basic Service Implementation ```typescript export class CustomService { private client: MinistryPlatformClient; constructor(client: MinistryPlatformClient) { this.client = client; } public async getData<T>(endpoint: string): Promise<T> { try { // Always ensure token is valid await this.client.ensureValidToken(); // Get the HTTP client and make the request const httpClient = this.client.getHttpClient(); return await httpClient.get<T>(endpoint); } catch (error) { console.error(`Error getting data from ${endpoint}:`, error); throw error; } } } ``` ### Error Handling Pattern ```typescript public async makeRequest<T>(endpoint: string): Promise<T> { try { await this.client.ensureValidToken(); return await this.client.getHttpClient().get<T>(endpoint); } catch (error) { // Handle authentication errors if (error.message.includes('401')) { console.error('Authentication failed, token may be invalid'); // Could implement retry logic here } // Handle other HTTP errors if (error.message.includes('400')) { console.error('Bad request, check parameters'); } throw error; } } ``` ## Environment Configuration ### Required Environment Variables ```env # Ministry Platform instance base URL MINISTRY_PLATFORM_BASE_URL=https://your-instance.ministryplatform.com # OAuth2 Client Credentials (used by getClientCredentialsToken) MINISTRY_PLATFORM_CLIENT_ID=your_client_id MINISTRY_PLATFORM_CLIENT_SECRET=your_client_secret ``` ### Configuration Validation ```typescript // Add validation in constructor if needed constructor() { if (!process.env.MINISTRY_PLATFORM_BASE_URL) { throw new Error('MINISTRY_PLATFORM_BASE_URL environment variable is required'); } this.baseUrl = process.env.MINISTRY_PLATFORM_BASE_URL; this.httpClient = new HttpClient(this.baseUrl, () => this.token); } ``` ## Debugging and Monitoring ### Token Logging The client provides detailed token lifecycle logging: ```typescript console.log("Checking token validity..."); console.log("Expires at: ", this.expiresAt); console.log("Current time: ", new Date()); console.log("Token expired, refreshing..."); console.log("Token refreshed. Expires at: ", this.expiresAt); ``` ### Custom Logging ```typescript public async ensureValidToken(): Promise<void> { const now = new Date(); const timeUntilExpiry = this.expiresAt.getTime() - now.getTime(); console.log(`Token check: ${timeUntilExpiry}ms until expiry`); if (this.expiresAt < now) { console.log("Refreshing token..."); const startTime = Date.now(); try { const creds = await getClientCredentialsToken(); this.token = creds.access_token; this.expiresAt = new Date(Date.now() + TOKEN_LIFE); const duration = Date.now() - startTime; console.log(`Token refreshed successfully in ${duration}ms`); } catch (error) { console.error("Token refresh failed:", error); throw error; } } } ``` ## Best Practices ### 1. Always Check Token Before Requests ```typescript // ✅ Good await this.client.ensureValidToken(); const data = await this.client.getHttpClient().get('/endpoint'); // ❌ Bad - might fail with expired token const data = await this.client.getHttpClient().get('/endpoint'); ``` ### 2. Handle Token Refresh Errors ```typescript try { await this.client.ensureValidToken(); // Make API request } catch (error) { if (error.message.includes('Failed to get client credentials token')) { // Handle authentication configuration issues console.error('Check client credentials configuration'); } throw error; } ``` ### 3. Use Proper Error Handling ```typescript public async serviceMethod(): Promise<Data> { try { await this.client.ensureValidToken(); return await this.client.getHttpClient().get<Data>('/endpoint'); } catch (error) { console.error('Service method failed:', error); throw error; // Re-throw to allow caller to handle } } ``` ### 4. Implement Retry Logic for Transient Failures ```typescript public async makeRequestWithRetry<T>(endpoint: string, maxRetries = 3): Promise<T> { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { await this.client.ensureValidToken(); return await this.client.getHttpClient().get<T>(endpoint); } catch (error) { if (attempt === maxRetries) { throw error; } // Wait before retry (exponential backoff) await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); } } throw new Error('Max retries exceeded'); } ``` ## Common Issues and Solutions ### 1. Token Refresh Failures **Issue**: `getClientCredentialsToken()` throws errors **Solutions**: - Verify environment variables are set correctly - Check client ID and secret are valid - Ensure Ministry Platform instance is accessible - Verify OAuth2 configuration in Ministry Platform ### 2. Frequent Token Refreshes **Issue**: Token is refreshed on every request **Solutions**: - Check system clock is accurate - Verify TOKEN_LIFE constant is appropriate - Ensure token expiration calculation is correct ### 3. Concurrent Request Issues **Issue**: Multiple requests trigger simultaneous token refreshes **Solutions**: - Implement token refresh locking - Use promise caching for token requests - Consider singleton pattern for token management ## Integration Points ### With HttpClient ```typescript // HttpClient receives token via callback this.httpClient = new HttpClient(this.baseUrl, () => this.token); // Token is automatically injected into all requests const response = await this.httpClient.get('/endpoint'); ``` ### With Services ```typescript // All services receive the client instance export class ministryPlatformProvider { constructor() { this.client = new MinistryPlatformClient(); this.tableService = new TableService(this.client); this.procedureService = new ProcedureService(this.client); // ... other services } } ``` ### With Client Credentials ```typescript // Depends on clientCredentials module for token acquisition const creds = await getClientCredentialsToken(); this.token = creds.access_token; ``` This client serves as the foundation for all Ministry Platform API operations, providing reliable authentication and HTTP communication services to the entire provider system.