magically-sdk
Version:
Official SDK for Magically - Build mobile apps with AI
135 lines (118 loc) • 4.05 kB
text/typescript
import { Logger } from './Logger';
import { SDKConfig } from './types';
export interface RequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
headers?: Record<string, string>;
body?: any;
operation?: string; // For logging context
}
export class APIClient {
private config: SDKConfig;
private logger: Logger;
private baseUrl: string;
private apiKey: string | null = null;
constructor(config: SDKConfig, loggerPrefix: string = 'MagicallyAPI') {
this.config = config;
this.logger = new Logger(config.debug || false, loggerPrefix);
this.baseUrl = config.apiUrl || 'https://trymagically.com';
// Check for API key in environment (for edge functions)
if (typeof globalThis !== 'undefined' && 'MAGICALLY_API_KEY' in globalThis) {
this.apiKey = (globalThis as any).MAGICALLY_API_KEY;
this.logger.info('API key detected in environment - using API key authentication');
}
}
/**
* Make an authenticated API request with automatic logging
*/
async request<T = any>(
endpoint: string,
options: RequestOptions,
token?: string | null
): Promise<T> {
const startTime = Date.now();
let requestId: string = '';
const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options.headers,
};
// Use API key if available (edge environment), otherwise use provided token
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
this.logger.debug('Using API key authentication for request');
} else if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const requestConfig: RequestInit = {
method: options.method,
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
};
// Log the request
requestId = this.logger.networkRequest(options.method, url, {
headers,
body: options.body,
operation: options.operation
});
try {
const response = await fetch(url, requestConfig);
const responseData = await response.json();
const duration = Date.now() - startTime;
if (!response.ok) {
// Log error response
this.logger.networkError(requestId, responseData, {
duration,
operation: options.operation
});
// Throw structured error
this.handleAPIError(responseData, `${options.operation || 'Request'} failed`);
}
// Log successful response
this.logger.networkResponse(requestId, {
status: response.status,
statusText: response.statusText,
duration,
data: this.sanitizeResponseData(responseData, options.operation),
operation: options.operation
});
return responseData;
} catch (error) {
// Log network errors
if (requestId) {
this.logger.networkError(requestId, error, {
duration: Date.now() - startTime,
operation: options.operation
});
}
throw error;
}
}
/**
* Check if running in edge environment (has API key)
*/
isEdgeEnvironment(): boolean {
return this.apiKey !== null;
}
/**
* Sanitize response data for logging (avoid logging large arrays)
*/
private sanitizeResponseData(data: any, operation?: string): any {
if (operation === 'query' && data.data && Array.isArray(data.data)) {
return {
...data,
dataCount: data.data.length,
data: '[Array of items]'
};
}
return data;
}
/**
* Handle API errors with structured error information
*/
private handleAPIError(errorData: any, fallbackMessage: string): never {
// Create an error with the response data attached for the Logger
const error = new Error(errorData.error || errorData.message || fallbackMessage) as any;
error.responseData = errorData; // Preserve the original error response for Logger
throw error;
}
}