@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,472 @@
1
+ import { AxiosResponse } from 'axios'
2
+ import { HttpService } from './core/HttpService'
3
+ import { HttpClient } from '../utils/axios'
4
+ import {
5
+ LoginResponse,
6
+ RefreshTokenRequest,
7
+ RefreshTokenResponse,
8
+ BaserowError,
9
+ BaserowAuthTokenInvalidError,
10
+ BaserowReLoginRequiredError,
11
+ BaserowCredentials,
12
+ Logger
13
+ } from '../types'
14
+ import { validateString } from '../utils/validation'
15
+ import { decodeJwtExpiry } from '../utils/jwt-decoder'
16
+
17
+ export class AuthService extends HttpService {
18
+ private accessToken?: string
19
+ private refreshToken?: string
20
+ private tokenExpiry?: Date
21
+ private keepAliveTimer?: NodeJS.Timeout
22
+ private credentials?: BaserowCredentials
23
+
24
+ constructor(http: HttpClient, logger?: Logger) {
25
+ super(http, logger)
26
+ }
27
+
28
+ /**
29
+ * Realizar login con credenciales y obtener JWT tokens
30
+ *
31
+ * Autentica al usuario con Baserow y almacena los tokens de acceso y refresh en memoria.
32
+ * Si keep-alive está habilitado, también almacena las credenciales para re-login automático.
33
+ *
34
+ * @param credentials - Email y password del usuario Baserow
35
+ * @returns Respuesta de login con tokens y datos del usuario
36
+ * @throws {BaserowError} Si las credenciales son inválidas (401)
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const response = await authService.login({
41
+ * email: 'user@example.com',
42
+ * password: 'securepassword'
43
+ * })
44
+ * console.log(`Logged in as: ${response.user.username}`)
45
+ * ```
46
+ *
47
+ * @since 1.0.0
48
+ */
49
+ async login(credentials: BaserowCredentials): Promise<LoginResponse> {
50
+ validateString(credentials.email, 'email')
51
+ validateString(credentials.password, 'password')
52
+
53
+ this.logger?.info?.(`🔐 Attempting login for user: ${credentials.email}`)
54
+
55
+ const loginData = {
56
+ username: credentials.email,
57
+ password: credentials.password
58
+ }
59
+
60
+ try {
61
+ const response = await this.http.post<LoginResponse>('/user/token-auth/', loginData)
62
+
63
+ // Almacenar tokens en memoria
64
+ this.accessToken = response.access_token
65
+ this.refreshToken = response.refresh_token
66
+
67
+ // Almacenar credenciales para keep-alive (re-login automático)
68
+ this.credentials = credentials
69
+
70
+ // Decodificar expiración desde el claim 'exp' del JWT (estándar)
71
+ this.tokenExpiry = decodeJwtExpiry(response.access_token, this.logger)
72
+ if (!this.tokenExpiry) {
73
+ this.logger?.warn?.('⚠️ No se pudo decodificar exp del JWT, usando fallback de 10 min')
74
+ this.tokenExpiry = new Date(Date.now() + 10 * 60 * 1000) // Fallback conservador
75
+ }
76
+
77
+ // Actualizar el token en HttpClient
78
+ this.http.setAuthToken(response.access_token)
79
+
80
+ this.logger?.info?.(`✅ Login successful for user: ${response.user.username} (ID: ${response.user.id})`)
81
+ this.logger?.debug?.(`Access token expires at: ${this.tokenExpiry.toISOString()}`)
82
+ this.logger?.debug?.(`Refresh token stored (expires ~7 days from now)`)
83
+
84
+ return response
85
+ } catch (error) {
86
+ if (error instanceof BaserowError && error.status === 401) {
87
+ this.logger?.error?.(`❌ Login failed: Invalid credentials for ${credentials.email}`)
88
+ throw new BaserowError('Invalid credentials', 401, 'INVALID_CREDENTIALS')
89
+ }
90
+ this.logger?.error?.('❌ Login failed with unexpected error:', error)
91
+ throw error
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Renovar access token usando refresh token
97
+ *
98
+ * IMPORTANTE: En Baserow, el refresh token NO se renueva al hacer refresh (ROTATE_REFRESH_TOKENS=False).
99
+ * Solo se obtiene un nuevo access_token válido por 10 minutos. El refresh_token expira 7 días después
100
+ * del login original y NO se extiende con actividad.
101
+ *
102
+ * Para backends 24/7, usar keep-alive con re-login automático en lugar de depender de refresh token.
103
+ *
104
+ * @returns Nuevo access token válido
105
+ * @throws {BaserowReLoginRequiredError} Si el refresh token expiró (401) o no está disponible
106
+ * @throws {BaserowAuthTokenInvalidError} Si el refresh token es inválido (400)
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * try {
111
+ * const newToken = await authService.refreshAccessToken()
112
+ * console.log('Token renovado exitosamente')
113
+ * } catch (error) {
114
+ * if (error instanceof BaserowReLoginRequiredError) {
115
+ * console.log('Refresh token expirado, se requiere re-login')
116
+ * await authService.login(credentials)
117
+ * }
118
+ * }
119
+ * ```
120
+ *
121
+ * @since 1.0.0
122
+ */
123
+ async refreshAccessToken(): Promise<string> {
124
+ if (!this.refreshToken) {
125
+ this.logger?.error?.('❌ No refresh token available - tokens may have been lost')
126
+ throw new BaserowReLoginRequiredError('tokens_lost', {
127
+ message: 'No refresh token available. This can happen after process restart or if tokens were manually cleared.'
128
+ })
129
+ }
130
+
131
+ const refreshData: RefreshTokenRequest = {
132
+ refresh_token: this.refreshToken
133
+ }
134
+
135
+ try {
136
+ this.logger?.debug?.('🔄 Attempting to refresh access token...')
137
+
138
+ const response = await this.http.post<RefreshTokenResponse>('/user/token-refresh/', refreshData)
139
+
140
+ // Actualizar tokens almacenados
141
+ this.accessToken = response.access_token
142
+ // El refresh token se mantiene igual, no se renueva
143
+
144
+ // Decodificar expiración desde el claim 'exp' del JWT (estándar)
145
+ this.tokenExpiry = decodeJwtExpiry(response.access_token, this.logger)
146
+ if (!this.tokenExpiry) {
147
+ this.logger?.warn?.('⚠️ No se pudo decodificar exp del JWT, usando fallback de 10 min')
148
+ this.tokenExpiry = new Date(Date.now() + 10 * 60 * 1000) // Fallback conservador
149
+ }
150
+
151
+ // Actualizar el token en HttpClient
152
+ this.http.setAuthToken(response.access_token)
153
+
154
+ this.logger?.info?.('✅ Refresh token successful - new access token obtained')
155
+ this.logger?.debug?.(`New token expires at: ${this.tokenExpiry.toISOString()}`)
156
+
157
+ return response.access_token
158
+ } catch (error) {
159
+ // Si el refresh falla, limpiar tokens
160
+ this.clearTokens()
161
+
162
+ // Proporcionar mensajes de error específicos y accionables
163
+ if (error instanceof BaserowError) {
164
+ if (error.status === 401) {
165
+ // 401 en refresh significa que el refresh token expiró (típicamente después de ~7 días de inactividad)
166
+ this.logger?.error?.('❌ Refresh token has EXPIRED - Re-authentication required')
167
+ this.logger?.error?.(' Refresh tokens expire after ~7 days of inactivity in Baserow')
168
+ this.logger?.error?.(' User must login again with email/password')
169
+
170
+ throw new BaserowReLoginRequiredError('refresh_token_expired', {
171
+ originalError: error.message,
172
+ hint: 'Baserow refresh tokens expire after approximately 7 days. User must re-authenticate with credentials.'
173
+ })
174
+ } else if (error.status === 400) {
175
+ this.logger?.error?.('❌ Refresh token invalid or corrupted')
176
+ throw new BaserowAuthTokenInvalidError('Refresh token is invalid or corrupted', {
177
+ originalStatus: error.status,
178
+ originalCode: error.code
179
+ })
180
+ }
181
+ }
182
+
183
+ this.logger?.error?.('❌ Refresh token failed with unexpected error:', error)
184
+ throw error
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Verificar si el token actual ha expirado
190
+ *
191
+ * Considera el token expirado si:
192
+ * - No hay token almacenado
193
+ * - No hay fecha de expiración conocida
194
+ * - Expira en los próximos 5 minutos (margen de seguridad)
195
+ *
196
+ * @returns `true` si el token está expirado o próximo a expirar, `false` si es válido
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * if (authService.isTokenExpired()) {
201
+ * await authService.refreshAccessToken()
202
+ * }
203
+ * ```
204
+ *
205
+ * @since 1.0.0
206
+ */
207
+ isTokenExpired(): boolean {
208
+ if (!this.tokenExpiry || !this.accessToken) {
209
+ return true
210
+ }
211
+
212
+ // Verificar si expira en los próximos 5 minutos
213
+ const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000)
214
+ return this.tokenExpiry <= fiveMinutesFromNow
215
+ }
216
+
217
+ /**
218
+ * Obtener token válido, renovándolo automáticamente si ha expirado
219
+ *
220
+ * Verifica la expiración del token y lo renueva automáticamente si es necesario.
221
+ * Ideal para usar antes de realizar requests a la API.
222
+ *
223
+ * @returns Access token válido
224
+ * @throws {BaserowError} Si no hay token disponible y se requiere login
225
+ * @throws {BaserowReLoginRequiredError} Si el refresh token expiró
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * const token = await authService.getValidToken()
230
+ * // Usar token en headers de request
231
+ * ```
232
+ *
233
+ * @since 1.0.0
234
+ */
235
+ async getValidToken(): Promise<string> {
236
+ if (!this.accessToken) {
237
+ throw new BaserowError('No access token available, login required', 401, 'NO_ACCESS_TOKEN')
238
+ }
239
+
240
+ if (this.isTokenExpired()) {
241
+ await this.refreshAccessToken()
242
+ }
243
+
244
+ return this.accessToken!
245
+ }
246
+
247
+ /**
248
+ * Verificar si el usuario está autenticado con token válido
249
+ *
250
+ * @returns `true` si hay token válido y no expirado, `false` en caso contrario
251
+ *
252
+ * @example
253
+ * ```typescript
254
+ * if (!authService.isAuthenticated()) {
255
+ * await authService.login(credentials)
256
+ * }
257
+ * ```
258
+ *
259
+ * @since 1.0.0
260
+ */
261
+ isAuthenticated(): boolean {
262
+ return !!this.accessToken && !this.isTokenExpired()
263
+ }
264
+
265
+ /**
266
+ * Obtener el access token actual (sin validar expiración)
267
+ *
268
+ * Retorna el token almacenado en memoria sin verificar si está expirado.
269
+ * Para obtener un token válido garantizado, usar `getValidToken()`.
270
+ *
271
+ * @returns Access token actual o `undefined` si no hay token
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const token = authService.getCurrentToken()
276
+ * if (token) {
277
+ * console.log('Token disponible')
278
+ * }
279
+ * ```
280
+ *
281
+ * @since 1.0.0
282
+ */
283
+ getCurrentToken(): string | undefined {
284
+ return this.accessToken
285
+ }
286
+
287
+ /**
288
+ * Limpiar todos los tokens y credenciales almacenadas
289
+ *
290
+ * Limpia access token, refresh token, credenciales y detiene el keep-alive si está activo.
291
+ * También limpia el token del HttpClient.
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * authService.clearTokens()
296
+ * console.log('Sesión limpiada')
297
+ * ```
298
+ *
299
+ * @since 1.0.0
300
+ */
301
+ clearTokens(): void {
302
+ this.stopKeepAlive()
303
+ this.accessToken = undefined
304
+ this.refreshToken = undefined
305
+ this.tokenExpiry = undefined
306
+ this.credentials = undefined
307
+ this.http.clearAuthToken()
308
+ }
309
+
310
+ /**
311
+ * Cerrar sesión y limpiar tokens
312
+ *
313
+ * Alias de `clearTokens()`. Limpia todos los tokens, credenciales y detiene keep-alive.
314
+ * No hace petición al servidor (Baserow no tiene endpoint de logout).
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * authService.logout()
319
+ * console.log('Sesión cerrada')
320
+ * ```
321
+ *
322
+ * @since 1.0.0
323
+ */
324
+ logout(): void {
325
+ this.clearTokens()
326
+ }
327
+
328
+ /**
329
+ * Iniciar keep-alive automático para evitar expiración de refresh token
330
+ *
331
+ * IMPORTANTE: El refresh token de Baserow expira 7 días después del login (fijo, NO se renueva con refresh).
332
+ * Keep-alive ejecuta RE-LOGIN completo periódicamente para obtener tokens frescos (access + refresh)
333
+ * y mantener la sesión activa indefinidamente en aplicaciones backend 24/7.
334
+ *
335
+ * ⚠️ SEGURIDAD: Las credenciales se almacenan en memoria del proceso.
336
+ * Solo usar en backends propios/confiables, NO en frontends o apps compartidas.
337
+ *
338
+ * @param intervalMinutes - Intervalo en minutos para re-login (default: 7200 = 5 días, margen 2 días)
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * // Re-login cada 5 días (recomendado para backends 24/7)
343
+ * authService.startKeepAlive()
344
+ *
345
+ * // Re-login cada 3 días (más conservador, margen 4 días)
346
+ * authService.startKeepAlive(4320)
347
+ * ```
348
+ *
349
+ * @since 1.1.0
350
+ */
351
+ startKeepAlive(intervalMinutes: number = 7200): void {
352
+ // Prevenir múltiples timers simultáneos
353
+ if (this.keepAliveTimer) {
354
+ this.logger?.warn?.('⚠️ Keep-alive ya está activo, deteniendo timer anterior')
355
+ this.stopKeepAlive()
356
+ }
357
+
358
+ if (!this.credentials) {
359
+ this.logger?.error?.('❌ Keep-alive requiere credenciales almacenadas')
360
+ return
361
+ }
362
+
363
+ const intervalMs = intervalMinutes * 60 * 1000
364
+ const days = (intervalMinutes / 1440).toFixed(1)
365
+
366
+ this.logger?.info?.(`🔄 Keep-alive iniciado: re-login cada ${days} días (${intervalMinutes} min)`)
367
+
368
+ this.keepAliveTimer = setInterval(async () => {
369
+ if (!this.credentials) {
370
+ this.logger?.error?.('❌ Keep-alive: credenciales no disponibles')
371
+ return
372
+ }
373
+
374
+ try {
375
+ this.logger?.debug?.('🔄 Keep-alive: ejecutando re-login preventivo...')
376
+ await this.login(this.credentials)
377
+ this.logger?.info?.('✅ Keep-alive: Re-login exitoso, tokens renovados (válidos 7 días más)')
378
+ } catch (error) {
379
+ this.logger?.error?.('❌ Keep-alive: error en re-login preventivo:', error)
380
+ // No detenemos el timer, siguiente intento puede funcionar
381
+ }
382
+ }, intervalMs)
383
+
384
+ // Prevenir que el timer mantenga el proceso Node.js vivo indefinidamente
385
+ if (this.keepAliveTimer.unref) {
386
+ this.keepAliveTimer.unref()
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Detener keep-alive automático
392
+ *
393
+ * Limpia el timer de keep-alive. Se llama automáticamente en `logout()` y `clearTokens()`.
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * // Detener manualmente el keep-alive
398
+ * authService.stopKeepAlive()
399
+ * ```
400
+ *
401
+ * @since 1.1.0
402
+ */
403
+ stopKeepAlive(): void {
404
+ if (this.keepAliveTimer) {
405
+ clearInterval(this.keepAliveTimer)
406
+ this.keepAliveTimer = undefined
407
+ this.logger?.info?.('🛑 Keep-alive detenido')
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Configurar interceptor para auto-refresh en 401 usando interceptors nativos de Axios
413
+ *
414
+ * Intercepta respuestas 401 (Unauthorized) y automáticamente intenta renovar el token
415
+ * usando refresh token. Si la renovación es exitosa, reintenta la petición original.
416
+ *
417
+ * NO intercepta endpoints de autenticación (`/user/token-*`) para evitar loops infinitos.
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * await authService.login(credentials)
422
+ * authService.setupAutoRefresh()
423
+ * // Ahora los 401 se manejan automáticamente
424
+ * ```
425
+ *
426
+ * @since 1.0.0
427
+ */
428
+ setupAutoRefresh(): void {
429
+ this.http.addResponseInterceptor(
430
+ (response: AxiosResponse) => response,
431
+ async (error: any) => {
432
+ // El config puede venir del error original de Axios o del BaserowError transformado
433
+ const originalRequest = error.config
434
+
435
+ // NO hacer auto-refresh en endpoints de autenticación (previene loops infinitos)
436
+ const isAuthEndpoint = originalRequest?.url?.includes('/user/token-')
437
+
438
+ // Detectar 401 de cualquier fuente (error raw de Axios o BaserowError transformado)
439
+ const is401Error = error.response?.status === 401 || (error instanceof BaserowError && error.status === 401)
440
+
441
+ // Si recibimos 401 y tenemos refresh token, intentar renovar
442
+ // EXCEPTO en endpoints de autenticación (login, refresh)
443
+ if (is401Error && !isAuthEndpoint && this.refreshToken && originalRequest && !originalRequest._retry) {
444
+ originalRequest._retry = true
445
+
446
+ this.logger?.info?.('🔄 Auto-refresh: Detectado 401, intentando renovar token...')
447
+
448
+ try {
449
+ const newToken = await this.refreshAccessToken()
450
+
451
+ this.logger?.info?.('✅ Auto-refresh: Token renovado, reintentando request original')
452
+
453
+ // Actualizar el header Authorization con el nuevo token
454
+ if (originalRequest.headers && newToken) {
455
+ originalRequest.headers.Authorization = `JWT ${newToken}`
456
+ }
457
+
458
+ // Reintentar la petición original con el nuevo token actualizado
459
+ return this.http.axios.request(originalRequest)
460
+ } catch {
461
+ // Si el refresh falla, limpiar tokens y propagar error original
462
+ this.logger?.error?.('❌ Auto-refresh: Falló la renovación del token')
463
+ this.clearTokens()
464
+ throw error
465
+ }
466
+ }
467
+
468
+ throw error
469
+ }
470
+ )
471
+ }
472
+ }