@explorins/pers-sdk
Version:
Platform-agnostic SDK for PERS (Phygital Experience Rewards System)
152 lines (134 loc) • 4.14 kB
text/typescript
/**
* PERS API Client - Core platform-agnostic client for PERS backend
*/
import { HttpClient, RequestOptions } from './abstractions/http-client';
import { PersConfig, buildApiRoot } from './pers-config';
export class PersApiClient {
private readonly apiRoot: string;
constructor(
private httpClient: HttpClient,
private config: PersConfig
) {
// Build API root from environment and version
this.apiRoot = buildApiRoot(config.environment, config.apiVersion);
}
/**
* Get request headers including auth token and project key
*/
private async getHeaders(): Promise<Record<string, string>> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// Add authentication token
if (this.config.authProvider) {
const token = await this.config.authProvider.getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
// Add project key
if (this.config.authProvider) {
const projectKey = await this.config.authProvider.getProjectKey();
if (projectKey) {
headers['x-project-key'] = projectKey;
}
} else {
// Fallback to config project key if no auth provider
headers['x-project-key'] = this.config.apiProjectKey;
}
return headers;
}
/**
* Make a request with proper headers, auth, and error handling
*/
private async request<T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
body?: any,
options?: { retryCount?: number }
): Promise<T> {
const { retryCount = 0 } = options || {};
const url = `${this.apiRoot}${endpoint}`;
const requestOptions: RequestOptions = {
headers: await this.getHeaders(),
timeout: this.config.timeout || 30000,
};
try {
switch (method) {
case 'GET':
return await this.httpClient.get<T>(url, requestOptions);
case 'POST':
return await this.httpClient.post<T>(url, body, requestOptions);
case 'PUT':
return await this.httpClient.put<T>(url, body, requestOptions);
case 'DELETE':
return await this.httpClient.delete<T>(url, requestOptions);
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
} catch (error: any) {
// Handle 401 errors with automatic token refresh
if (error.status === 401 && retryCount === 0 && this.config.authProvider?.onTokenExpired) {
try {
await this.config.authProvider.onTokenExpired();
// Retry once with refreshed token
return this.request<T>(method, endpoint, body, { ...options, retryCount: 1 });
} catch (refreshError) {
throw new PersApiError(
`Authentication refresh failed: ${refreshError}`,
endpoint,
method,
401
);
}
}
throw new PersApiError(
`PERS API request failed: ${error.message || error}`,
endpoint,
method,
error.status
);
}
}
/**
* Generic GET request
*/
async get<T>(endpoint: string): Promise<T> {
return this.request<T>('GET', endpoint);
}
/**
* Generic POST request
*/
async post<T>(endpoint: string, body?: any): Promise<T> {
return this.request<T>('POST', endpoint, body);
}
/**
* Generic PUT request
*/
async put<T>(endpoint: string, body?: any): Promise<T> {
return this.request<T>('PUT', endpoint, body);
}
/**
* Generic DELETE request
*/
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>('DELETE', endpoint);
}
/**
* Get current configuration
*/
getConfig(): PersConfig {
return this.config;
}
}
export class PersApiError extends Error {
constructor(
message: string,
public endpoint: string,
public method: string,
public statusCode?: number
) {
super(message);
this.name = 'PersApiError';
}
}