ministry-platform-provider
Version:
TypeScript client library for Ministry Platform API integration
395 lines (299 loc) • 11.3 kB
Markdown
# 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.