perspectapi-ts-sdk 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,268 @@
1
+ /**
2
+ * HTTP Client for PerspectAPI SDK
3
+ * Cloudflare Workers compatible - uses native fetch API
4
+ */
5
+
6
+ import type {
7
+ ApiResponse,
8
+ ApiError,
9
+ RequestOptions,
10
+ PerspectApiConfig
11
+ } from '../types';
12
+
13
+ export class HttpClient {
14
+ private baseUrl: string;
15
+ private defaultHeaders: Record<string, string>;
16
+ private timeout: number;
17
+ private retries: number;
18
+
19
+ constructor(config: PerspectApiConfig) {
20
+ this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
21
+ this.timeout = config.timeout || 30000;
22
+ this.retries = config.retries || 3;
23
+
24
+ this.defaultHeaders = {
25
+ 'Content-Type': 'application/json',
26
+ 'User-Agent': 'perspectapi-ts-sdk/1.0.0',
27
+ ...config.headers,
28
+ };
29
+
30
+ // Add authentication headers
31
+ if (config.apiKey) {
32
+ this.defaultHeaders['X-API-Key'] = config.apiKey;
33
+ }
34
+ if (config.jwt) {
35
+ this.defaultHeaders['Authorization'] = `Bearer ${config.jwt}`;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Update authentication token
41
+ */
42
+ setAuth(jwt: string) {
43
+ this.defaultHeaders['Authorization'] = `Bearer ${jwt}`;
44
+ }
45
+
46
+ /**
47
+ * Update API key
48
+ */
49
+ setApiKey(apiKey: string) {
50
+ this.defaultHeaders['X-API-Key'] = apiKey;
51
+ }
52
+
53
+ /**
54
+ * Remove authentication
55
+ */
56
+ clearAuth() {
57
+ delete this.defaultHeaders['Authorization'];
58
+ delete this.defaultHeaders['X-API-Key'];
59
+ }
60
+
61
+ /**
62
+ * Make HTTP request with retry logic
63
+ */
64
+ async request<T = any>(
65
+ endpoint: string,
66
+ options: RequestOptions = {}
67
+ ): Promise<ApiResponse<T>> {
68
+ const url = this.buildUrl(endpoint, options.params);
69
+ const requestOptions = this.buildRequestOptions(options);
70
+
71
+ // Log the full request details
72
+ console.log(`[HTTP Client] Making ${options.method || 'GET'} request to: ${url}`);
73
+ console.log(`[HTTP Client] Base URL: ${this.baseUrl}`);
74
+ console.log(`[HTTP Client] Endpoint: ${endpoint}`);
75
+ console.log(`[HTTP Client] Full URL: ${url}`);
76
+ if (options.params) {
77
+ console.log(`[HTTP Client] Query params:`, options.params);
78
+ }
79
+
80
+ let lastError: Error;
81
+
82
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
83
+ try {
84
+ const response = await this.fetchWithTimeout(url, requestOptions);
85
+ return await this.handleResponse<T>(response);
86
+ } catch (error) {
87
+ lastError = error as Error;
88
+
89
+ // Don't retry on client errors (4xx)
90
+ if (error && typeof error === 'object' && 'status' in error &&
91
+ typeof (error as any).status === 'number' && (error as any).status < 500) {
92
+ throw error;
93
+ }
94
+
95
+ // Don't retry on last attempt
96
+ if (attempt === this.retries) {
97
+ break;
98
+ }
99
+
100
+ // Exponential backoff
101
+ await this.delay(Math.pow(2, attempt) * 1000);
102
+ }
103
+ }
104
+
105
+ throw lastError!;
106
+ }
107
+
108
+ /**
109
+ * GET request
110
+ */
111
+ async get<T = any>(endpoint: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
112
+ return this.request<T>(endpoint, { method: 'GET', params });
113
+ }
114
+
115
+ /**
116
+ * POST request
117
+ */
118
+ async post<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
119
+ return this.request<T>(endpoint, { method: 'POST', body });
120
+ }
121
+
122
+ /**
123
+ * PUT request
124
+ */
125
+ async put<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
126
+ return this.request<T>(endpoint, { method: 'PUT', body });
127
+ }
128
+
129
+ /**
130
+ * DELETE request
131
+ */
132
+ async delete<T = any>(endpoint: string): Promise<ApiResponse<T>> {
133
+ return this.request<T>(endpoint, { method: 'DELETE' });
134
+ }
135
+
136
+ /**
137
+ * PATCH request
138
+ */
139
+ async patch<T = any>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
140
+ return this.request<T>(endpoint, { method: 'PATCH', body });
141
+ }
142
+
143
+ /**
144
+ * Build full URL with query parameters
145
+ */
146
+ private buildUrl(endpoint: string, params?: Record<string, any>): string {
147
+ const url = `${this.baseUrl}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
148
+
149
+ if (!params || Object.keys(params).length === 0) {
150
+ return url;
151
+ }
152
+
153
+ const searchParams = new URLSearchParams();
154
+ Object.entries(params).forEach(([key, value]) => {
155
+ if (value !== undefined && value !== null) {
156
+ searchParams.append(key, String(value));
157
+ }
158
+ });
159
+
160
+ return `${url}?${searchParams.toString()}`;
161
+ }
162
+
163
+ /**
164
+ * Build request options
165
+ */
166
+ private buildRequestOptions(options: RequestOptions): RequestInit {
167
+ const headers = {
168
+ ...this.defaultHeaders,
169
+ ...options.headers,
170
+ };
171
+
172
+ const requestOptions: RequestInit = {
173
+ method: options.method || 'GET',
174
+ headers,
175
+ };
176
+
177
+ if (options.body && options.method !== 'GET') {
178
+ if (typeof options.body === 'string') {
179
+ requestOptions.body = options.body;
180
+ } else {
181
+ requestOptions.body = JSON.stringify(options.body);
182
+ }
183
+ }
184
+
185
+ return requestOptions;
186
+ }
187
+
188
+ /**
189
+ * Fetch with timeout support
190
+ */
191
+ private async fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
192
+ const controller = new AbortController();
193
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
194
+
195
+ try {
196
+ const response = await fetch(url, {
197
+ ...options,
198
+ signal: controller.signal,
199
+ });
200
+ return response;
201
+ } finally {
202
+ clearTimeout(timeoutId);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Handle response and errors
208
+ */
209
+ private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
210
+ const contentType = response.headers.get('content-type');
211
+ const isJson = contentType?.includes('application/json');
212
+
213
+ let data: any;
214
+ try {
215
+ data = isJson ? await response.json() : await response.text();
216
+ } catch (error) {
217
+ data = null;
218
+ }
219
+
220
+ if (!response.ok) {
221
+ const error: ApiError = {
222
+ message: data?.error || data?.message || `HTTP ${response.status}: ${response.statusText}`,
223
+ status: response.status,
224
+ code: data?.code,
225
+ details: data,
226
+ };
227
+ throw error;
228
+ }
229
+
230
+ // Handle different response formats
231
+ if (isJson && typeof data === 'object') {
232
+ // If response has data property, return as-is
233
+ if ('data' in data || 'message' in data || 'error' in data) {
234
+ return data as ApiResponse<T>;
235
+ }
236
+ // Otherwise wrap in data property
237
+ return { data: data as T, success: true };
238
+ }
239
+
240
+ return { data: data as T, success: true };
241
+ }
242
+
243
+ /**
244
+ * Delay utility for retries
245
+ */
246
+ private delay(ms: number): Promise<void> {
247
+ return new Promise(resolve => setTimeout(resolve, ms));
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Create API Error from unknown error
253
+ */
254
+ export function createApiError(error: unknown): ApiError {
255
+ if (error instanceof Error) {
256
+ return error as ApiError;
257
+ }
258
+
259
+ // Check if it's already an ApiError-like object
260
+ if (error && typeof error === 'object' && 'message' in error) {
261
+ return error as ApiError;
262
+ }
263
+
264
+ return {
265
+ message: 'Unknown error occurred',
266
+ details: error,
267
+ };
268
+ }