@gzl10/baserow 1.2.0

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +435 -0
  2. package/README.md +847 -0
  3. package/dist/index.d.ts +8749 -0
  4. package/dist/index.js +11167 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +91 -0
  7. package/src/BaserowClient.ts +501 -0
  8. package/src/ClientWithCreds.ts +545 -0
  9. package/src/ClientWithCredsWs.ts +852 -0
  10. package/src/ClientWithToken.ts +171 -0
  11. package/src/contexts/DatabaseClientContext.ts +114 -0
  12. package/src/contexts/DatabaseContext.ts +870 -0
  13. package/src/contexts/DatabaseTokenContext.ts +331 -0
  14. package/src/contexts/FieldContext.ts +399 -0
  15. package/src/contexts/RowContext.ts +99 -0
  16. package/src/contexts/TableClientContext.ts +291 -0
  17. package/src/contexts/TableContext.ts +1247 -0
  18. package/src/contexts/TableOnlyContext.ts +74 -0
  19. package/src/contexts/WorkspaceContext.ts +490 -0
  20. package/src/express/errors.ts +260 -0
  21. package/src/express/index.ts +69 -0
  22. package/src/express/middleware.ts +225 -0
  23. package/src/express/serializers.ts +314 -0
  24. package/src/index.ts +247 -0
  25. package/src/presets/performance.ts +262 -0
  26. package/src/services/AuthService.ts +472 -0
  27. package/src/services/DatabaseService.ts +246 -0
  28. package/src/services/DatabaseTokenService.ts +186 -0
  29. package/src/services/FieldService.ts +1543 -0
  30. package/src/services/RowService.ts +982 -0
  31. package/src/services/SchemaControlService.ts +420 -0
  32. package/src/services/TableService.ts +781 -0
  33. package/src/services/WorkspaceService.ts +113 -0
  34. package/src/services/core/BaseAuthClient.ts +111 -0
  35. package/src/services/core/BaseClient.ts +107 -0
  36. package/src/services/core/BaseService.ts +71 -0
  37. package/src/services/core/HttpService.ts +115 -0
  38. package/src/services/core/ValidationService.ts +149 -0
  39. package/src/types/auth.ts +177 -0
  40. package/src/types/core.ts +91 -0
  41. package/src/types/errors.ts +105 -0
  42. package/src/types/fields.ts +456 -0
  43. package/src/types/index.ts +222 -0
  44. package/src/types/requests.ts +333 -0
  45. package/src/types/responses.ts +50 -0
  46. package/src/types/schema.ts +446 -0
  47. package/src/types/tokens.ts +36 -0
  48. package/src/types.ts +11 -0
  49. package/src/utils/auth.ts +174 -0
  50. package/src/utils/axios.ts +647 -0
  51. package/src/utils/field-cache.ts +164 -0
  52. package/src/utils/httpFactory.ts +66 -0
  53. package/src/utils/jwt-decoder.ts +188 -0
  54. package/src/utils/jwtTokens.ts +50 -0
  55. package/src/utils/performance.ts +105 -0
  56. package/src/utils/prisma-mapper.ts +961 -0
  57. package/src/utils/validation.ts +463 -0
  58. package/src/validators/schema.ts +419 -0
@@ -0,0 +1,647 @@
1
+ /**
2
+ * Cliente HTTP simplificado con plugins oficiales de Axios
3
+ *
4
+ * Cliente HTTP optimizado para la API de Baserow con funcionalidades avanzadas:
5
+ * - Retry automático con backoff exponencial
6
+ * - Rate limiting configurable
7
+ * - Manejo robusto de errores con tipos específicos
8
+ * - Logging estructurado de requests y respuestas
9
+ * - Soporte para JWT y Database Tokens
10
+ * - Connection pooling automático (Node.js 20+)
11
+ *
12
+ * **Características principales:**
13
+ * - **Retry Logic**: 3 intentos con backoff exponencial usando axios-retry oficial
14
+ * - **Rate Limiting**: Control de requests/segundo con axios-rate-limit oficial
15
+ * - **Error Handling**: Transformación automática a errores tipados de Baserow
16
+ * - **Auto-detection**: Detecta formato de token (JWT vs Database Token)
17
+ * - **Optimizado**: 36% menos código que versiones anteriores
18
+ * - **Probado**: Plugins mantenidos por millones de desarrolladores
19
+ *
20
+ * **Configuración recomendada:**
21
+ * - Producción: maxRequestsPerSecond = 10 (default)
22
+ * - Testing: maxRequestsPerSecond = 1 (usar HttpClient.forTesting())
23
+ * - Retries: 3 (default) para alta disponibilidad
24
+ * - Timeout: 30s (default) para operaciones complejas
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * // Cliente estándar
29
+ * const client = new HttpClient({
30
+ * baseURL: 'https://baserow.example.com',
31
+ * token: 'your-token-here',
32
+ * maxRequestsPerSecond: 10,
33
+ * retries: 3,
34
+ * logger: console
35
+ * })
36
+ *
37
+ * // Cliente para testing (más conservador)
38
+ * const testClient = HttpClient.forTesting({
39
+ * baseURL: 'https://test.baserow.com',
40
+ * token: 'test-token'
41
+ * })
42
+ *
43
+ * // Uso básico
44
+ * const data = await client.get('/api/endpoint')
45
+ * const result = await client.post('/api/create', { name: 'Test' })
46
+ * ```
47
+ *
48
+ * @since 1.0.0 - Versión simplificada con plugins oficiales
49
+ * @since 1.1.0 - Migración de agentkeepalive a Node.js 20+ nativo
50
+ */
51
+
52
+ import axios, { AxiosInstance, AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'
53
+ import axiosRetry from 'axios-retry'
54
+ import rateLimit from 'axios-rate-limit'
55
+ import { PERFORMANCE_DEFAULTS } from './performance'
56
+ import {
57
+ BaserowError,
58
+ BaserowRateLimitError,
59
+ BaserowValidationError,
60
+ BaserowNotFoundError,
61
+ BaserowNetworkError,
62
+ BaserowTimeoutError,
63
+ Logger
64
+ } from '../types'
65
+
66
+ /**
67
+ * Configuración del cliente HTTP
68
+ *
69
+ * Define todas las opciones disponibles para personalizar el comportamiento
70
+ * del cliente HTTP, incluyendo autenticación, retry logic y rate limiting.
71
+ *
72
+ * @since 1.0.0
73
+ */
74
+ export interface HttpClientConfig {
75
+ baseURL: string
76
+ token: string
77
+ timeout?: number
78
+ retries?: number
79
+ logger?: Logger
80
+ // Rate limiting options simplificadas
81
+ maxRequestsPerSecond?: number // Máximo requests por segundo (default: 10)
82
+ enableRateLimiting?: boolean // Habilitar rate limiting (default: true)
83
+ }
84
+
85
+ /**
86
+ * Cliente HTTP avanzado para la API de Baserow
87
+ *
88
+ * Implementa un cliente HTTP robusto y optimizado con características
89
+ * empresariales como retry automático, rate limiting y manejo de errores.
90
+ * Utiliza plugins oficiales de Axios para máxima compatibilidad.
91
+ *
92
+ * **Arquitectura simplificada:**
93
+ * - Axios core para requests HTTP
94
+ * - axios-retry para lógica de reintentos
95
+ * - axios-rate-limit para control de velocidad
96
+ * - Node.js 20+ keepAlive nativo para connection pooling
97
+ *
98
+ * @since 1.0.0
99
+ */
100
+ export class HttpClient {
101
+ private client: AxiosInstance
102
+ private config: HttpClientConfig
103
+ private logger?: Logger
104
+
105
+ /**
106
+ * Crea una nueva instancia del cliente HTTP
107
+ *
108
+ * @param config - Configuración del cliente HTTP
109
+ * @param config.baseURL - URL base de la API de Baserow
110
+ * @param config.token - Token de autenticación (JWT o Database Token)
111
+ * @param config.timeout - Timeout en ms (default: 30000)
112
+ * @param config.retries - Número de reintentos (default: 3)
113
+ * @param config.maxRequestsPerSecond - Rate limit (default: 10)
114
+ * @param config.enableRateLimiting - Habilitar rate limiting (default: true)
115
+ * @param config.logger - Logger para debug y monitoreo
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const client = new HttpClient({
120
+ * baseURL: 'https://baserow.example.com',
121
+ * token: 'your-database-token-or-jwt',
122
+ * timeout: 30000,
123
+ * retries: 3,
124
+ * maxRequestsPerSecond: 10,
125
+ * logger: console
126
+ * })
127
+ * ```
128
+ *
129
+ * @since 1.0.0
130
+ */
131
+ constructor(config: HttpClientConfig) {
132
+ this.config = {
133
+ ...PERFORMANCE_DEFAULTS,
134
+ ...config
135
+ }
136
+ this.logger = config.logger
137
+
138
+ // Crear instancia básica de Axios (Node.js 20+ maneja keepAlive automáticamente)
139
+ this.client = axios.create({
140
+ baseURL: config.baseURL,
141
+ timeout: this.config.timeout,
142
+ headers: {
143
+ 'Content-Type': 'application/json',
144
+ Accept: 'application/json'
145
+ }
146
+ })
147
+
148
+ // Configurar token inicial
149
+ this.setAuthToken(config.token)
150
+
151
+ // Configurar interceptores básicos
152
+ this.setupRequestInterceptor()
153
+ this.setupResponseInterceptor()
154
+
155
+ // Configurar retry automático con plugin oficial
156
+ this.setupRetryPlugin()
157
+
158
+ // Configurar rate limiting con plugin oficial
159
+ if (this.config.enableRateLimiting) {
160
+ this.setupRateLimitingPlugin()
161
+ }
162
+ }
163
+
164
+ private getAuthHeader(token: string): string {
165
+ // Detectar formato del token automáticamente
166
+ const isJWT = token.includes('.') && token.split('.').length === 3
167
+ return isJWT ? `JWT ${token}` : `Token ${token}`
168
+ }
169
+
170
+ private extractResourceFromUrl(url: string): { resource: string; id: string } {
171
+ // Extraer información del contexto desde la URL para mejores mensajes de error
172
+ const patterns = [
173
+ { pattern: /\/database\/tables\/(\d+)\//, resource: 'Table', idIndex: 1 },
174
+ { pattern: /\/database\/fields\/(\d+)\//, resource: 'Field', idIndex: 1 },
175
+ { pattern: /\/database\/rows\/(\d+)\//, resource: 'Row', idIndex: 1 },
176
+ { pattern: /\/applications\/(\d+)\//, resource: 'Database', idIndex: 1 },
177
+ { pattern: /\/workspaces\/(\d+)\/applications\//, resource: 'Workspace', idIndex: 1 },
178
+ { pattern: /\/workspaces\/(\d+)\//, resource: 'Workspace', idIndex: 1 }
179
+ ]
180
+
181
+ for (const { pattern, resource, idIndex } of patterns) {
182
+ const match = url.match(pattern)
183
+ if (match) {
184
+ return { resource, id: match[idIndex] }
185
+ }
186
+ }
187
+
188
+ // Si no encontramos patrón específico, intentar extraer último número como ID
189
+ const genericMatch = url.match(/\/(\d+)\/?$/)
190
+ if (genericMatch) {
191
+ return { resource: 'Resource', id: genericMatch[1] }
192
+ }
193
+
194
+ return { resource: 'Resource', id: 'unknown' }
195
+ }
196
+
197
+ private setupRequestInterceptor(): void {
198
+ this.client.interceptors.request.use(config => {
199
+ // Agregar timestamp para calcular duración
200
+ ;(config as any).startTime = Date.now()
201
+
202
+ // Log de requests salientes (solo en debug)
203
+ if (this.logger?.debug) {
204
+ this.logger.debug('HTTP Request starting', {
205
+ method: config.method?.toUpperCase(),
206
+ url: config.url,
207
+ baseURL: config.baseURL
208
+ })
209
+ }
210
+
211
+ return config
212
+ })
213
+ }
214
+
215
+ private setupResponseInterceptor(): void {
216
+ this.client.interceptors.response.use(
217
+ (response: AxiosResponse) => {
218
+ // Log de respuestas exitosas (solo en modo debug si es necesario)
219
+ if (this.logger?.debug) {
220
+ this.logger.debug('HTTP Request successful', {
221
+ method: response.config.method?.toUpperCase(),
222
+ url: response.config.url,
223
+ status: response.status,
224
+ duration: Date.now() - (response.config as any).startTime
225
+ })
226
+ }
227
+ return response
228
+ },
229
+ async (error: AxiosError) => {
230
+ const requestInfo = {
231
+ method: error.config?.method?.toUpperCase(),
232
+ url: error.config?.url,
233
+ startTime: (error.config as any)?.startTime
234
+ }
235
+
236
+ // Transformar errores de Axios a errores de Baserow
237
+ if (error.response) {
238
+ const { status, data } = error.response
239
+ const errorData = data as any
240
+ const message = errorData?.detail || errorData?.message || `HTTP ${status}: ${error.message}`
241
+
242
+ // Log estructurado del error
243
+ this.logger?.error('HTTP Request failed', {
244
+ ...requestInfo,
245
+ status,
246
+ message,
247
+ errorData: errorData ? JSON.stringify(errorData, null, 2) : null,
248
+ fullErrorData: errorData,
249
+ duration: requestInfo.startTime ? Date.now() - requestInfo.startTime : null
250
+ })
251
+
252
+ switch (status) {
253
+ case 400:
254
+ if (errorData?.error && typeof errorData.error === 'object') {
255
+ throw new BaserowValidationError(message, errorData.error)
256
+ }
257
+ // Mejorar mensaje de error para debugging
258
+ const detailedMessage = `${message} | Full error: ${JSON.stringify(errorData, null, 2)}`
259
+ throw new BaserowError(detailedMessage, 400, 'BAD_REQUEST', errorData)
260
+
261
+ case 404:
262
+ // Intentar extraer contexto del URL para mejor mensaje de error
263
+ const resourceContext = this.extractResourceFromUrl(requestInfo.url || '')
264
+ throw new BaserowNotFoundError(resourceContext.resource, resourceContext.id)
265
+
266
+ case 429:
267
+ const retryAfter = error.response.headers['retry-after']
268
+ throw new BaserowRateLimitError(retryAfter ? parseInt(retryAfter) : undefined)
269
+
270
+ case 401:
271
+ throw new BaserowError('Unauthorized - check your token', 401, 'UNAUTHORIZED', errorData, error.config)
272
+
273
+ case 403:
274
+ throw new BaserowError('Forbidden - insufficient permissions', 403, 'FORBIDDEN', errorData)
275
+
276
+ case 408:
277
+ throw new BaserowTimeoutError(this.config.timeout || 30000)
278
+
279
+ case 500:
280
+ case 502:
281
+ case 503:
282
+ case 504:
283
+ // Errores de servidor - potencialmente transitorios
284
+ throw new BaserowError(
285
+ `Servidor Baserow no disponible (HTTP ${status}). El servidor puede estar sobrecargado o en mantenimiento. Intenta de nuevo más tarde.`,
286
+ status,
287
+ 'SERVER_ERROR',
288
+ errorData
289
+ )
290
+
291
+ default:
292
+ throw new BaserowError(message, status, 'HTTP_ERROR', errorData)
293
+ }
294
+ }
295
+
296
+ // Error de red, timeout o conexión
297
+ if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
298
+ this.logger?.error('Request timeout', {
299
+ ...requestInfo,
300
+ timeout: this.config.timeout,
301
+ duration: requestInfo.startTime ? Date.now() - requestInfo.startTime : null
302
+ })
303
+ throw new BaserowTimeoutError(this.config.timeout || 30000)
304
+ }
305
+
306
+ // Error de red
307
+ this.logger?.error('Network error', {
308
+ ...requestInfo,
309
+ errorCode: error.code,
310
+ errorMessage: error.message,
311
+ duration: requestInfo.startTime ? Date.now() - requestInfo.startTime : null
312
+ })
313
+ throw new BaserowNetworkError(error.message, error)
314
+ }
315
+ )
316
+ }
317
+
318
+ private setupRetryPlugin(): void {
319
+ axiosRetry(this.client, {
320
+ retries: this.config.retries!,
321
+ retryDelay: axiosRetry.exponentialDelay,
322
+ retryCondition: error => {
323
+ // Retry en errores de red o métodos idempotentes con 5xx
324
+ if (axiosRetry.isNetworkOrIdempotentRequestError(error)) {
325
+ return true
326
+ }
327
+
328
+ // También retry en 429 (rate limiting)
329
+ if (error.response?.status === 429) {
330
+ return true
331
+ }
332
+
333
+ return false
334
+ },
335
+ onRetry: (retryCount, error, requestConfig) => {
336
+ this.logger?.warn('Retrying request', {
337
+ method: requestConfig.method?.toUpperCase(),
338
+ url: requestConfig.url,
339
+ attempt: retryCount,
340
+ maxAttempts: this.config.retries,
341
+ errorType: error.name,
342
+ errorMessage: error.message
343
+ })
344
+ }
345
+ })
346
+ }
347
+
348
+ private setupRateLimitingPlugin(): void {
349
+ const maxRequests = this.config.maxRequestsPerSecond!
350
+ this.client = rateLimit(this.client, {
351
+ maxRequests: maxRequests,
352
+ perMilliseconds: 1000,
353
+ maxRPS: maxRequests
354
+ })
355
+
356
+ this.logger?.debug?.('Rate limiting configured', {
357
+ maxRequestsPerSecond: maxRequests
358
+ })
359
+ }
360
+
361
+ /**
362
+ * Actualizar el token de autenticación
363
+ *
364
+ * Permite cambiar el token de autenticación dinámicamente sin crear
365
+ * una nueva instancia del cliente. Detecta automáticamente el formato
366
+ * del token (JWT vs Database Token).
367
+ *
368
+ * @param token - Nuevo token de autenticación
369
+ *
370
+ * @example
371
+ * ```typescript
372
+ * // Cambiar a Database Token
373
+ * client.setAuthToken('new-database-token-123')
374
+ *
375
+ * // Cambiar a JWT
376
+ * client.setAuthToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...')
377
+ * ```
378
+ *
379
+ * @since 1.0.0
380
+ */
381
+ setAuthToken(token: string): void {
382
+ if (token) {
383
+ this.client.defaults.headers.common['Authorization'] = this.getAuthHeader(token)
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Limpiar el token de autenticación
389
+ *
390
+ * Remueve el header de autenticación del cliente.
391
+ * Útil para testing o cuando se necesita acceso anónimo.
392
+ *
393
+ * @example
394
+ * ```typescript
395
+ * client.clearAuthToken()
396
+ * // Ahora las requests no incluirán Authorization header
397
+ * ```
398
+ *
399
+ * @since 1.0.0
400
+ */
401
+ clearAuthToken(): void {
402
+ delete this.client.defaults.headers.common['Authorization']
403
+ }
404
+
405
+ /**
406
+ * Agregar interceptor de respuesta personalizado
407
+ *
408
+ * Permite agregar lógica personalizada para procesar respuestas
409
+ * o manejar errores específicos. Se ejecuta después de los
410
+ * interceptors internos del cliente.
411
+ *
412
+ * @param onFulfilled - Función para procesar respuestas exitosas
413
+ * @param onRejected - Función para manejar errores
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * client.addResponseInterceptor(
418
+ * (response) => {
419
+ * console.log('Response received:', response.status)
420
+ * return response
421
+ * },
422
+ * (error) => {
423
+ * console.error('Request failed:', error.message)
424
+ * return Promise.reject(error)
425
+ * }
426
+ * )
427
+ * ```
428
+ *
429
+ * @since 1.0.0
430
+ */
431
+ addResponseInterceptor(
432
+ onFulfilled: (response: AxiosResponse) => AxiosResponse,
433
+ onRejected: (error: any) => any
434
+ ): void {
435
+ this.client.interceptors.response.use(onFulfilled, onRejected)
436
+ }
437
+
438
+ /**
439
+ * Realizar petición GET
440
+ *
441
+ * @template T - Tipo de datos esperado en la respuesta
442
+ * @param endpoint - Endpoint relativo a la baseURL
443
+ * @param params - Parámetros de query string
444
+ * @returns Promise con los datos de la respuesta
445
+ *
446
+ * @example
447
+ * ```typescript
448
+ * const tables = await client.get<Table[]>('/database/tables/database/123/')
449
+ * const rows = await client.get('/database/rows/table/456/', { page: 1, size: 100 })
450
+ * ```
451
+ *
452
+ * @since 1.0.0
453
+ */
454
+ async get<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> {
455
+ const response = await this.client.get(endpoint, { params })
456
+ return response.data
457
+ }
458
+
459
+ /**
460
+ * Realizar petición POST
461
+ *
462
+ * @template T - Tipo de datos esperado en la respuesta
463
+ * @param endpoint - Endpoint relativo a la baseURL
464
+ * @param data - Datos a enviar en el body
465
+ * @param params - Parámetros de query string
466
+ * @returns Promise con los datos de la respuesta
467
+ *
468
+ * @example
469
+ * ```typescript
470
+ * const newTable = await client.post<Table>('/database/tables/database/123/', {
471
+ * name: 'Nueva Tabla'
472
+ * })
473
+ * ```
474
+ *
475
+ * @since 1.0.0
476
+ */
477
+ async post<T = any>(endpoint: string, data?: any, params?: Record<string, any>): Promise<T> {
478
+ const response = await this.client.post(endpoint, data, { params })
479
+ return response.data
480
+ }
481
+
482
+ /**
483
+ * Realizar petición PUT
484
+ *
485
+ * @template T - Tipo de datos esperado en la respuesta
486
+ * @param endpoint - Endpoint relativo a la baseURL
487
+ * @param data - Datos a enviar en el body
488
+ * @param params - Parámetros de query string
489
+ * @returns Promise con los datos de la respuesta
490
+ *
491
+ * @since 1.0.0
492
+ */
493
+ async put<T = any>(endpoint: string, data?: any, params?: Record<string, any>): Promise<T> {
494
+ const response = await this.client.put(endpoint, data, { params })
495
+ return response.data
496
+ }
497
+
498
+ /**
499
+ * Realizar petición PATCH
500
+ *
501
+ * @template T - Tipo de datos esperado en la respuesta
502
+ * @param endpoint - Endpoint relativo a la baseURL
503
+ * @param data - Datos a enviar en el body
504
+ * @param params - Parámetros de query string
505
+ * @returns Promise con los datos de la respuesta
506
+ *
507
+ * @example
508
+ * ```typescript
509
+ * const updated = await client.patch<Table>('/database/tables/456/', {
510
+ * name: 'Nuevo Nombre'
511
+ * })
512
+ * ```
513
+ *
514
+ * @since 1.0.0
515
+ */
516
+ async patch<T = any>(endpoint: string, data?: any, params?: Record<string, any>): Promise<T> {
517
+ const response = await this.client.patch(endpoint, data, { params })
518
+ return response.data
519
+ }
520
+
521
+ /**
522
+ * Realizar petición DELETE
523
+ *
524
+ * @template T - Tipo de datos esperado en la respuesta
525
+ * @param endpoint - Endpoint relativo a la baseURL
526
+ * @param params - Parámetros de query string
527
+ * @returns Promise con los datos de la respuesta
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * await client.delete('/database/tables/456/')
532
+ * ```
533
+ *
534
+ * @since 1.0.0
535
+ */
536
+ async delete<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> {
537
+ const response = await this.client.delete(endpoint, { params })
538
+ return response.data
539
+ }
540
+
541
+ /**
542
+ * Crear instancia HttpClient optimizada para testing
543
+ *
544
+ * Crea un cliente con configuración conservadora optimizada para tests:
545
+ * - Rate limiting muy bajo (1 req/s) para no saturar el servidor
546
+ * - Timeout alto (60s) para operaciones lentas en CI/CD
547
+ * - Menos reintentos (2) para tests más rápidos
548
+ *
549
+ * @param config - Configuración base (sin maxRequestsPerSecond)
550
+ * @returns Instancia optimizada para testing
551
+ *
552
+ * @example
553
+ * ```typescript
554
+ * const testClient = HttpClient.forTesting({
555
+ * baseURL: process.env.TEST_BASEROW_URL,
556
+ * token: process.env.TEST_TOKEN
557
+ * })
558
+ * // Rate limiting: 1 req/s, timeout: 60s, retries: 2
559
+ * ```
560
+ *
561
+ * @since 1.0.0
562
+ */
563
+ static forTesting(config: Omit<HttpClientConfig, 'maxRequestsPerSecond'>): HttpClient {
564
+ return new HttpClient({
565
+ ...config,
566
+ maxRequestsPerSecond: 1, // Máximo 1 request por segundo en tests (más conservador)
567
+ enableRateLimiting: true,
568
+ retries: 2, // Menos reintentos en tests
569
+ timeout: 60000 // Timeout más alto para tests (60s)
570
+ })
571
+ }
572
+
573
+ /**
574
+ * Acceso directo a la instancia de Axios para casos avanzados
575
+ *
576
+ * Proporciona acceso a la instancia configurada de Axios para
577
+ * casos que requieren funcionalidad no cubierta por los métodos
578
+ * simplificados del cliente.
579
+ *
580
+ * @returns Instancia configurada de Axios
581
+ *
582
+ * @example
583
+ * ```typescript
584
+ * // Usar funcionalidades avanzadas de Axios
585
+ * const response = await client.axios({
586
+ * method: 'post',
587
+ * url: '/custom-endpoint',
588
+ * headers: { 'Custom-Header': 'value' }
589
+ * })
590
+ * ```
591
+ *
592
+ * @since 1.0.0
593
+ */
594
+ get axios(): AxiosInstance {
595
+ return this.client
596
+ }
597
+
598
+ /**
599
+ * Realizar petición personalizada usando configuración de Axios
600
+ *
601
+ * Permite realizar peticiones con configuración avanzada de Axios
602
+ * manteniendo todos los beneficios del cliente (retry, rate limiting, etc.).
603
+ *
604
+ * @template T - Tipo de datos esperado en la respuesta
605
+ * @param config - Configuración completa de Axios
606
+ * @returns Promise con los datos de la respuesta
607
+ *
608
+ * @example
609
+ * ```typescript
610
+ * const result = await client.request<CustomType>({
611
+ * method: 'POST',
612
+ * url: '/special-endpoint',
613
+ * data: complexData,
614
+ * headers: { 'Special-Header': 'value' },
615
+ * timeout: 60000
616
+ * })
617
+ * ```
618
+ *
619
+ * @since 1.0.0
620
+ */
621
+ async request<T = any>(config: AxiosRequestConfig): Promise<T> {
622
+ const response = await this.client.request(config)
623
+ return response.data
624
+ }
625
+
626
+ /**
627
+ * Cerrar las conexiones HTTP y limpiar recursos
628
+ *
629
+ * Limpia los recursos del cliente HTTP. En Node.js 20+ con keepAlive
630
+ * automático, no requiere gestión manual de connection pools.
631
+ * Principalmente útil para testing y cleanup de recursos.
632
+ *
633
+ * @example
634
+ * ```typescript
635
+ * // En tests o al cerrar la aplicación
636
+ * client.destroy()
637
+ * ```
638
+ *
639
+ * @since 1.0.0
640
+ * @since 1.1.0 - Simplificado para Node.js 20+ keepAlive nativo
641
+ */
642
+ destroy(): void {
643
+ this.logger?.debug?.('Destroying HTTP client')
644
+ // En Node.js 20+ con keepAlive automático, no necesitamos gestión manual de agentes
645
+ this.logger?.debug?.('HTTP client destroyed successfully')
646
+ }
647
+ }