@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.
- package/CHANGELOG.md +435 -0
- package/README.md +847 -0
- package/dist/index.d.ts +8749 -0
- package/dist/index.js +11167 -0
- package/dist/index.js.map +1 -0
- package/package.json +91 -0
- package/src/BaserowClient.ts +501 -0
- package/src/ClientWithCreds.ts +545 -0
- package/src/ClientWithCredsWs.ts +852 -0
- package/src/ClientWithToken.ts +171 -0
- package/src/contexts/DatabaseClientContext.ts +114 -0
- package/src/contexts/DatabaseContext.ts +870 -0
- package/src/contexts/DatabaseTokenContext.ts +331 -0
- package/src/contexts/FieldContext.ts +399 -0
- package/src/contexts/RowContext.ts +99 -0
- package/src/contexts/TableClientContext.ts +291 -0
- package/src/contexts/TableContext.ts +1247 -0
- package/src/contexts/TableOnlyContext.ts +74 -0
- package/src/contexts/WorkspaceContext.ts +490 -0
- package/src/express/errors.ts +260 -0
- package/src/express/index.ts +69 -0
- package/src/express/middleware.ts +225 -0
- package/src/express/serializers.ts +314 -0
- package/src/index.ts +247 -0
- package/src/presets/performance.ts +262 -0
- package/src/services/AuthService.ts +472 -0
- package/src/services/DatabaseService.ts +246 -0
- package/src/services/DatabaseTokenService.ts +186 -0
- package/src/services/FieldService.ts +1543 -0
- package/src/services/RowService.ts +982 -0
- package/src/services/SchemaControlService.ts +420 -0
- package/src/services/TableService.ts +781 -0
- package/src/services/WorkspaceService.ts +113 -0
- package/src/services/core/BaseAuthClient.ts +111 -0
- package/src/services/core/BaseClient.ts +107 -0
- package/src/services/core/BaseService.ts +71 -0
- package/src/services/core/HttpService.ts +115 -0
- package/src/services/core/ValidationService.ts +149 -0
- package/src/types/auth.ts +177 -0
- package/src/types/core.ts +91 -0
- package/src/types/errors.ts +105 -0
- package/src/types/fields.ts +456 -0
- package/src/types/index.ts +222 -0
- package/src/types/requests.ts +333 -0
- package/src/types/responses.ts +50 -0
- package/src/types/schema.ts +446 -0
- package/src/types/tokens.ts +36 -0
- package/src/types.ts +11 -0
- package/src/utils/auth.ts +174 -0
- package/src/utils/axios.ts +647 -0
- package/src/utils/field-cache.ts +164 -0
- package/src/utils/httpFactory.ts +66 -0
- package/src/utils/jwt-decoder.ts +188 -0
- package/src/utils/jwtTokens.ts +50 -0
- package/src/utils/performance.ts +105 -0
- package/src/utils/prisma-mapper.ts +961 -0
- package/src/utils/validation.ts +463 -0
- package/src/validators/schema.ts +419 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import { BaserowAdminConfig, LoginResponse, Workspace, BaserowConfigError } from './types'
|
|
2
|
+
import { createHttpClient } from './utils/httpFactory'
|
|
3
|
+
import { PerformanceManager } from './utils/performance'
|
|
4
|
+
import { setupJwtTokens } from './utils/jwtTokens'
|
|
5
|
+
import { validateRequired, validateString, validateUrl } from './utils/validation'
|
|
6
|
+
import { AuthService } from './services/AuthService'
|
|
7
|
+
import { DatabaseService } from './services/DatabaseService'
|
|
8
|
+
import { DatabaseTokenService } from './services/DatabaseTokenService'
|
|
9
|
+
import { TableService } from './services/TableService'
|
|
10
|
+
import { FieldService } from './services/FieldService'
|
|
11
|
+
import { RowService } from './services/RowService'
|
|
12
|
+
import { WorkspaceService } from './services/WorkspaceService'
|
|
13
|
+
import { DatabaseContext } from './contexts/DatabaseContext'
|
|
14
|
+
import { DatabaseTokenContext } from './contexts/DatabaseTokenContext'
|
|
15
|
+
import { BaseAuthClient } from './services/core/BaseAuthClient'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cliente administrativo restringido a workspace específico
|
|
19
|
+
*
|
|
20
|
+
* Proporciona acceso administrativo completo pero limitado a un workspace específico.
|
|
21
|
+
* API simplificada sin prefijo workspace, ideal para aplicaciones con scope limitado.
|
|
22
|
+
* Se autentica con JWT y auto-refresh, pero valida permisos solo para el workspace configurado.
|
|
23
|
+
*
|
|
24
|
+
* **Características:**
|
|
25
|
+
* - Autenticación JWT con auto-refresh automático
|
|
26
|
+
* - API simplificada: `admin.databases.create(name)` (workspace automático)
|
|
27
|
+
* - Acceso restringido al workspace especificado en configuración
|
|
28
|
+
* - Operaciones administrativas completas dentro del workspace
|
|
29
|
+
* - Resolución automática de nombres a IDs
|
|
30
|
+
* - Validación automática de permisos de workspace
|
|
31
|
+
*
|
|
32
|
+
* **API Simplificada:**
|
|
33
|
+
* - `admin.databases` → operaciones de databases del workspace
|
|
34
|
+
* - `admin.database(name)` → contexto de database específica
|
|
35
|
+
* - `admin.databaseToken` → gestión de Database Tokens del workspace
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const admin = await BaserowWithCredentialsWorkspace.create({
|
|
40
|
+
* url: 'https://baserow.example.com',
|
|
41
|
+
* credentials: {
|
|
42
|
+
* email: 'admin@example.com',
|
|
43
|
+
* password: 'password123'
|
|
44
|
+
* },
|
|
45
|
+
* workspace: 'My Workspace'
|
|
46
|
+
* })
|
|
47
|
+
*
|
|
48
|
+
* // API simplificada sin prefijo workspace
|
|
49
|
+
* const databases = await admin.databases.findMany()
|
|
50
|
+
* const database = await admin.databases.create('My Database')
|
|
51
|
+
*
|
|
52
|
+
* // Acceso directo a operaciones de database
|
|
53
|
+
* const table = await admin.database('My Database')
|
|
54
|
+
* .table('Users')
|
|
55
|
+
* .create({
|
|
56
|
+
* data: [['Name', 'Email'], ['John', 'john@example.com']],
|
|
57
|
+
* first_row_header: true
|
|
58
|
+
* })
|
|
59
|
+
*
|
|
60
|
+
* // Gestión de Database Tokens del workspace
|
|
61
|
+
* const token = await admin.databaseToken.createFullAccess('app-token')
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @since 1.0.0
|
|
65
|
+
*/
|
|
66
|
+
export class ClientWithCredsWs extends BaseAuthClient<BaserowAdminConfig, 'credentials'> {
|
|
67
|
+
private defaultWorkspace: Workspace
|
|
68
|
+
|
|
69
|
+
// Servicios que tienen sentido a nivel workspace
|
|
70
|
+
public databases: DatabaseService
|
|
71
|
+
|
|
72
|
+
// Servicios internos (no públicos)
|
|
73
|
+
private workspaceService: WorkspaceService
|
|
74
|
+
private databaseTokenService: DatabaseTokenService
|
|
75
|
+
|
|
76
|
+
private constructor(config: BaserowAdminConfig, loginResponse: LoginResponse, workspace: Workspace) {
|
|
77
|
+
// Procesar performance con defaults antes de guardar
|
|
78
|
+
const processedConfig = {
|
|
79
|
+
...config,
|
|
80
|
+
performance: PerformanceManager.merge(config.performance)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Inicializar HttpClient con JWT token
|
|
84
|
+
const http = createHttpClient({
|
|
85
|
+
url: processedConfig.url,
|
|
86
|
+
token: loginResponse.access_token,
|
|
87
|
+
logger: processedConfig.logger,
|
|
88
|
+
performance: processedConfig.performance
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Inicializar AuthService
|
|
92
|
+
const auth = new AuthService(http, processedConfig.logger)
|
|
93
|
+
|
|
94
|
+
super(processedConfig, http, auth, processedConfig.logger)
|
|
95
|
+
this.defaultWorkspace = workspace
|
|
96
|
+
|
|
97
|
+
// Transferir tokens al AuthService ANTES de configurar auto-refresh
|
|
98
|
+
setupJwtTokens(this.auth, loginResponse)
|
|
99
|
+
|
|
100
|
+
// Configurar auto-refresh de tokens (ahora tiene los tokens almacenados)
|
|
101
|
+
this.auth.setupAutoRefresh()
|
|
102
|
+
|
|
103
|
+
// Iniciar keep-alive si está habilitado
|
|
104
|
+
if (processedConfig.keepAlive?.enabled) {
|
|
105
|
+
const intervalMinutes = processedConfig.keepAlive.intervalMinutes ?? 7200
|
|
106
|
+
this.auth.startKeepAlive(intervalMinutes)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Inicializar servicios
|
|
110
|
+
this.workspaceService = new WorkspaceService(this.http, this.logger)
|
|
111
|
+
this.databaseTokenService = new DatabaseTokenService(this.http, this.logger)
|
|
112
|
+
this.databases = new DatabaseService(this.http, this.logger, workspace.id)
|
|
113
|
+
|
|
114
|
+
if (this.logger) {
|
|
115
|
+
this.logger.info(
|
|
116
|
+
`BaserowWithCredentialsWorkspace initialized with JWT authentication (workspace-restricted to "${workspace.name}")`
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Campos que deben excluirse por defecto para cliente con credenciales workspace
|
|
123
|
+
* Excluye credentials por seguridad
|
|
124
|
+
*/
|
|
125
|
+
protected getDefaultExcludeKeys(): string[] {
|
|
126
|
+
return ['credentials']
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Crear instancia de BaserowWithCredentialsWorkspace con auto-login y validación de workspace
|
|
131
|
+
*
|
|
132
|
+
* Factory method que maneja la autenticación automáticamente y valida/crea el workspace.
|
|
133
|
+
* Retorna una instancia lista para usar con JWT token válido y workspace configurado.
|
|
134
|
+
*
|
|
135
|
+
* **Keep-Alive opcional** para backends 24/7:
|
|
136
|
+
* - Habilitar con `keepAlive: { enabled: true }` para evitar expiración de refresh token
|
|
137
|
+
* - Default: re-login cada 5 días (7200 min), margen de seguridad de 2 días
|
|
138
|
+
* - ⚠️ Almacena credenciales en memoria (solo para backends confiables)
|
|
139
|
+
*
|
|
140
|
+
* @param config - Configuración del cliente administrativo
|
|
141
|
+
* @param config.url - URL del servidor Baserow
|
|
142
|
+
* @param config.credentials - Credenciales de usuario (email/password)
|
|
143
|
+
* @param config.workspace - Nombre del workspace a usar (se crea si no existe)
|
|
144
|
+
* @param config.keepAlive - Opcional: configuración keep-alive para backends 24/7
|
|
145
|
+
* @param config.logger - Logger opcional para debugging
|
|
146
|
+
* @returns Promise que resuelve a instancia autenticada de BaserowWithCredentialsWorkspace
|
|
147
|
+
*
|
|
148
|
+
* @throws {BaserowConfigError} Si la configuración es inválida o falta workspace
|
|
149
|
+
* @throws {BaserowAuthError} Si las credenciales son incorrectas
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* // Cliente básico sin keep-alive
|
|
154
|
+
* const admin = await BaserowWithCredentialsWorkspace.create({
|
|
155
|
+
* url: 'https://baserow.example.com',
|
|
156
|
+
* credentials: {
|
|
157
|
+
* email: 'admin@example.com',
|
|
158
|
+
* password: 'secure-password'
|
|
159
|
+
* },
|
|
160
|
+
* workspace: 'My Company Workspace'
|
|
161
|
+
* })
|
|
162
|
+
*
|
|
163
|
+
* // Cliente con keep-alive para backend 24/7 (re-login cada 5 días)
|
|
164
|
+
* const adminBackend = await BaserowWithCredentialsWorkspace.create({
|
|
165
|
+
* url: 'https://baserow.example.com',
|
|
166
|
+
* credentials: {
|
|
167
|
+
* email: 'admin@example.com',
|
|
168
|
+
* password: 'secure-password'
|
|
169
|
+
* },
|
|
170
|
+
* workspace: 'My Company Workspace',
|
|
171
|
+
* keepAlive: { enabled: true }
|
|
172
|
+
* })
|
|
173
|
+
*
|
|
174
|
+
* // Cliente listo para usar (restringido al workspace)
|
|
175
|
+
* const databases = await admin.databases.list()
|
|
176
|
+
* const stats = await admin.getUsageStats() // Solo del workspace configurado
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* @since 1.0.0
|
|
180
|
+
*/
|
|
181
|
+
static async create(config: BaserowAdminConfig): Promise<ClientWithCredsWs> {
|
|
182
|
+
validateRequired(config, 'config')
|
|
183
|
+
validateString(config.url, 'url')
|
|
184
|
+
validateRequired(config.credentials, 'credentials')
|
|
185
|
+
validateString(config.credentials.email, 'email')
|
|
186
|
+
validateString(config.credentials.password, 'password')
|
|
187
|
+
|
|
188
|
+
if (!config.workspace) {
|
|
189
|
+
throw new BaserowConfigError('workspace is required for BaserowWithCredentialsWorkspace', 'workspace')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!validateUrl(config.url)) {
|
|
193
|
+
throw new BaserowConfigError('Invalid URL format', 'url')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Crear HttpClient temporal para login
|
|
197
|
+
const tempHttp = createHttpClient({
|
|
198
|
+
url: config.url,
|
|
199
|
+
token: '', // Sin token inicial
|
|
200
|
+
logger: config.logger,
|
|
201
|
+
performance: config.performance
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Crear AuthService temporal
|
|
205
|
+
const tempAuth = new AuthService(tempHttp, config.logger)
|
|
206
|
+
|
|
207
|
+
// Realizar login
|
|
208
|
+
const loginResponse = await tempAuth.login(config.credentials)
|
|
209
|
+
|
|
210
|
+
if (config.logger) {
|
|
211
|
+
config.logger.info(`BaserowWithCredentialsWorkspace: Logged in as ${loginResponse.user.username}`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Resolver workspace por nombre
|
|
215
|
+
const tempWorkspaceService = new WorkspaceService(tempHttp, config.logger)
|
|
216
|
+
let workspace = await tempWorkspaceService.findUnique(config.workspace)
|
|
217
|
+
|
|
218
|
+
// Si no existe, crearlo
|
|
219
|
+
if (!workspace) {
|
|
220
|
+
if (config.logger) {
|
|
221
|
+
config.logger.info(`BaserowWithCredentialsWorkspace: Creating workspace "${config.workspace}"`)
|
|
222
|
+
}
|
|
223
|
+
workspace = await tempWorkspaceService.create({ name: config.workspace })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Crear instancia con token válido y workspace resuelto
|
|
227
|
+
// Los tokens son transferidos dentro del constructor
|
|
228
|
+
const admin = new ClientWithCredsWs(config, loginResponse, workspace)
|
|
229
|
+
|
|
230
|
+
return admin
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Verificar estado de salud del servidor Baserow
|
|
235
|
+
*
|
|
236
|
+
* Realiza un health check del servidor sin requerir autenticación.
|
|
237
|
+
* Útil para verificar conectividad antes de realizar operaciones.
|
|
238
|
+
*
|
|
239
|
+
* @returns Promise que resuelve a `true` si el servidor está saludable, `false` en caso contrario
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const isHealthy = await admin.health()
|
|
244
|
+
* if (!isHealthy) {
|
|
245
|
+
* console.log('Servidor Baserow no disponible')
|
|
246
|
+
* return
|
|
247
|
+
* }
|
|
248
|
+
*
|
|
249
|
+
* // Proceder con operaciones del workspace
|
|
250
|
+
* const databases = await admin.databases.list()
|
|
251
|
+
* ```
|
|
252
|
+
*
|
|
253
|
+
* @since 1.0.0
|
|
254
|
+
*/
|
|
255
|
+
async health(): Promise<boolean> {
|
|
256
|
+
try {
|
|
257
|
+
// Usar endpoint de salud oficial (no requiere autenticación)
|
|
258
|
+
const healthUrl = `${this.config.url.replace(/\/$/, '')}/api/_health/`
|
|
259
|
+
const response = await fetch(healthUrl, {
|
|
260
|
+
method: 'GET',
|
|
261
|
+
headers: { 'Content-Type': 'application/json' }
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return response.ok
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (this.logger) {
|
|
267
|
+
this.logger.error('BaserowWithCredentialsWorkspace health check failed:', error)
|
|
268
|
+
}
|
|
269
|
+
return false
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Obtener la configuración actual del cliente (sin exponer credenciales)
|
|
275
|
+
*
|
|
276
|
+
* Retorna una copia de solo lectura de la configuración, excluyendo las credenciales
|
|
277
|
+
* por seguridad. Incluye el workspace configurado.
|
|
278
|
+
*
|
|
279
|
+
* @returns Configuración actual sin credenciales (solo lectura)
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* const config = admin.getConfig()
|
|
284
|
+
* console.log('URL servidor:', config.url)
|
|
285
|
+
* console.log('Workspace:', config.workspace)
|
|
286
|
+
* // Las credenciales no están incluidas por seguridad
|
|
287
|
+
* ```
|
|
288
|
+
*
|
|
289
|
+
* @since 1.0.0
|
|
290
|
+
*/
|
|
291
|
+
getConfig(): Readonly<Omit<BaserowAdminConfig, 'credentials'>> {
|
|
292
|
+
return this.getConfigBase()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Verificar si el cliente está autenticado
|
|
297
|
+
*
|
|
298
|
+
* Comprueba si hay un token JWT válido y no expirado.
|
|
299
|
+
* Si el token está próximo a expirar, el auto-refresh se encargará de renovarlo.
|
|
300
|
+
*
|
|
301
|
+
* @returns `true` si está autenticado con token válido, `false` en caso contrario
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* if (admin.isAuthenticated()) {
|
|
306
|
+
* const databases = await admin.databases.list()
|
|
307
|
+
* } else {
|
|
308
|
+
* console.log('Sesión expirada, necesario re-login')
|
|
309
|
+
* }
|
|
310
|
+
* ```
|
|
311
|
+
*
|
|
312
|
+
* @since 1.0.0
|
|
313
|
+
*/
|
|
314
|
+
isAuthenticated(): boolean {
|
|
315
|
+
return this.auth.isAuthenticated()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Obtener el token JWT actual
|
|
320
|
+
*
|
|
321
|
+
* Retorna el token de acceso actual si está disponible.
|
|
322
|
+
* Útil para debugging o integración con otros sistemas.
|
|
323
|
+
*
|
|
324
|
+
* @returns Token JWT actual o `undefined` si no está autenticado
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```typescript
|
|
328
|
+
* const token = admin.getCurrentToken()
|
|
329
|
+
* if (token) {
|
|
330
|
+
* console.log('Token disponible:', token.substring(0, 20) + '...')
|
|
331
|
+
* }
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
334
|
+
* @since 1.0.0
|
|
335
|
+
*/
|
|
336
|
+
getCurrentToken(): string | undefined {
|
|
337
|
+
return this.auth.getCurrentToken()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Renovar el token de acceso manualmente
|
|
342
|
+
*
|
|
343
|
+
* Fuerza la renovación del token JWT usando el refresh token.
|
|
344
|
+
* Normalmente no es necesario llamar esto manualmente ya que el auto-refresh
|
|
345
|
+
* se encarga automáticamente.
|
|
346
|
+
*
|
|
347
|
+
* @returns Promise que resuelve al nuevo token de acceso
|
|
348
|
+
*
|
|
349
|
+
* @throws {BaserowReLoginRequiredError} Si el refresh token ha expirado (~7 días)
|
|
350
|
+
* @throws {BaserowAuthTokenInvalidError} Si el refresh token es inválido
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* try {
|
|
355
|
+
* const newToken = await admin.refreshToken()
|
|
356
|
+
* console.log('Token renovado exitosamente')
|
|
357
|
+
* } catch (error) {
|
|
358
|
+
* if (error instanceof BaserowReLoginRequiredError) {
|
|
359
|
+
* console.log('Sesión expirada, necesario re-login con credenciales')
|
|
360
|
+
* // Redirigir a login o solicitar credenciales nuevamente
|
|
361
|
+
* }
|
|
362
|
+
* }
|
|
363
|
+
* ```
|
|
364
|
+
*
|
|
365
|
+
* @since 1.0.0
|
|
366
|
+
*/
|
|
367
|
+
async refreshToken(): Promise<string> {
|
|
368
|
+
return await this.auth.refreshAccessToken()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Verificar si el cliente necesita re-autenticación
|
|
373
|
+
*
|
|
374
|
+
* Útil para detectar si los tokens se han perdido o el refresh token expiró,
|
|
375
|
+
* permitiendo manejar proactivamente la re-autenticación antes de hacer requests.
|
|
376
|
+
*
|
|
377
|
+
* @aiUsage
|
|
378
|
+
* **✅ Usar cuando:**
|
|
379
|
+
* - Antes de operaciones críticas (validación proactiva de sesión)
|
|
380
|
+
* - Después de períodos largos de inactividad (>6 horas)
|
|
381
|
+
* - Al iniciar aplicación (verificar si sesión previa sigue válida)
|
|
382
|
+
* - Después de errores 401 (distinguir si es recoverable con refresh)
|
|
383
|
+
* - En background jobs/cron tasks (sesiones de larga duración)
|
|
384
|
+
*
|
|
385
|
+
* **❌ NO usar cuando:**
|
|
386
|
+
* - Ya tienes try/catch con BaserowReLoginRequiredError (manejo reactivo suficiente)
|
|
387
|
+
* - Llamando cada request (overhead innecesario, auto-refresh ya maneja)
|
|
388
|
+
* - Solo para logging (usar `getAuthStatus()` que da más info)
|
|
389
|
+
*
|
|
390
|
+
* @aiPattern
|
|
391
|
+
* ## 🎯 Patrón: Wrapper con Auto-Relogin
|
|
392
|
+
*
|
|
393
|
+
* Crea una función wrapper que automáticamente reautentica si la sesión expiró:
|
|
394
|
+
*
|
|
395
|
+
* ```typescript
|
|
396
|
+
* async function withAutoRelogin<T>(
|
|
397
|
+
* admin: ClientWithCredsWs,
|
|
398
|
+
* credentials: { email: string; password: string },
|
|
399
|
+
* operation: (client: ClientWithCredsWs) => Promise<T>
|
|
400
|
+
* ): Promise<T> {
|
|
401
|
+
* // 1. Verificar proactivamente si necesita re-login
|
|
402
|
+
* if (admin.needsReLogin()) {
|
|
403
|
+
* console.log('🔄 Sesión expirada, reautenticando...')
|
|
404
|
+
*
|
|
405
|
+
* admin = await ClientWithCredsWs.create({
|
|
406
|
+
* url: admin.getConfig().url,
|
|
407
|
+
* credentials,
|
|
408
|
+
* workspace: admin.getConfig().workspace!
|
|
409
|
+
* })
|
|
410
|
+
*
|
|
411
|
+
* console.log('✅ Re-autenticación exitosa')
|
|
412
|
+
* }
|
|
413
|
+
*
|
|
414
|
+
* // 2. Ejecutar operación con manejo de errores
|
|
415
|
+
* try {
|
|
416
|
+
* return await operation(admin)
|
|
417
|
+
* } catch (error) {
|
|
418
|
+
* // 3. Si falla con re-login required, reintentar UNA vez
|
|
419
|
+
* if (error instanceof BaserowReLoginRequiredError) {
|
|
420
|
+
* console.log('🔄 Token refresh falló, reautenticando...')
|
|
421
|
+
*
|
|
422
|
+
* admin = await ClientWithCredsWs.create({
|
|
423
|
+
* url: admin.getConfig().url,
|
|
424
|
+
* credentials,
|
|
425
|
+
* workspace: admin.getConfig().workspace!
|
|
426
|
+
* })
|
|
427
|
+
*
|
|
428
|
+
* return await operation(admin)
|
|
429
|
+
* }
|
|
430
|
+
* throw error
|
|
431
|
+
* }
|
|
432
|
+
* }
|
|
433
|
+
*
|
|
434
|
+
* // Uso del wrapper
|
|
435
|
+
* const result = await withAutoRelogin(
|
|
436
|
+
* admin,
|
|
437
|
+
* { email: 'user@example.com', password: 'secret' },
|
|
438
|
+
* async (client) => {
|
|
439
|
+
* const databases = await client.databases.findMany()
|
|
440
|
+
* return databases
|
|
441
|
+
* }
|
|
442
|
+
* )
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @aiPattern
|
|
446
|
+
* ## 🔄 Patrón: Session Manager con Polling
|
|
447
|
+
*
|
|
448
|
+
* Para aplicaciones de larga duración, monitorea la sesión periódicamente:
|
|
449
|
+
*
|
|
450
|
+
* ```typescript
|
|
451
|
+
* class SessionManager {
|
|
452
|
+
* private checkInterval: NodeJS.Timeout | null = null
|
|
453
|
+
*
|
|
454
|
+
* constructor(
|
|
455
|
+
* private admin: ClientWithCredsWs,
|
|
456
|
+
* private credentials: { email: string; password: string }
|
|
457
|
+
* ) {}
|
|
458
|
+
*
|
|
459
|
+
* // Monitorear sesión cada N minutos
|
|
460
|
+
* startMonitoring(intervalMinutes: number = 30) {
|
|
461
|
+
* this.checkInterval = setInterval(async () => {
|
|
462
|
+
* if (this.admin.needsReLogin()) {
|
|
463
|
+
* console.warn('⚠️ Sesión expirada detectada, reautenticando...')
|
|
464
|
+
*
|
|
465
|
+
* try {
|
|
466
|
+
* this.admin = await ClientWithCredsWs.create({
|
|
467
|
+
* url: this.admin.getConfig().url,
|
|
468
|
+
* credentials: this.credentials,
|
|
469
|
+
* workspace: this.admin.getConfig().workspace!
|
|
470
|
+
* })
|
|
471
|
+
*
|
|
472
|
+
* console.log('✅ Sesión renovada automáticamente')
|
|
473
|
+
* } catch (error) {
|
|
474
|
+
* console.error('❌ Error renovando sesión:', error)
|
|
475
|
+
* // Notificar al usuario o detener el servicio
|
|
476
|
+
* }
|
|
477
|
+
* }
|
|
478
|
+
* }, intervalMinutes * 60 * 1000)
|
|
479
|
+
* }
|
|
480
|
+
*
|
|
481
|
+
* stopMonitoring() {
|
|
482
|
+
* if (this.checkInterval) {
|
|
483
|
+
* clearInterval(this.checkInterval)
|
|
484
|
+
* }
|
|
485
|
+
* }
|
|
486
|
+
*
|
|
487
|
+
* getClient(): ClientWithCredsWs {
|
|
488
|
+
* return this.admin
|
|
489
|
+
* }
|
|
490
|
+
* }
|
|
491
|
+
*
|
|
492
|
+
* // Uso
|
|
493
|
+
* const sessionManager = new SessionManager(admin, credentials)
|
|
494
|
+
* sessionManager.startMonitoring(30) // Check cada 30 min
|
|
495
|
+
*
|
|
496
|
+
* // Usar cliente desde manager
|
|
497
|
+
* const client = sessionManager.getClient()
|
|
498
|
+
* const data = await client.databases.findMany()
|
|
499
|
+
* ```
|
|
500
|
+
*
|
|
501
|
+
* @aiErrorHandling
|
|
502
|
+
* ## ⚠️ Casos de Re-Login Necesario
|
|
503
|
+
*
|
|
504
|
+
* **Caso 1: Tokens perdidos (reinicio de proceso)**
|
|
505
|
+
* ```typescript
|
|
506
|
+
* // Al iniciar aplicación, verificar sesión previa
|
|
507
|
+
* if (admin.needsReLogin()) {
|
|
508
|
+
* console.log('Tokens perdidos (reinicio), necesario login')
|
|
509
|
+
* // needsReLogin() = true porque no hay tokens en memoria
|
|
510
|
+
* }
|
|
511
|
+
* ```
|
|
512
|
+
*
|
|
513
|
+
* **Caso 2: Refresh token expirado (~7 días inactividad)**
|
|
514
|
+
* ```typescript
|
|
515
|
+
* // Después de 7+ días sin uso
|
|
516
|
+
* if (admin.needsReLogin()) {
|
|
517
|
+
* console.log('Refresh token expirado, necesario login')
|
|
518
|
+
* // Access token expirado + no refresh token disponible
|
|
519
|
+
* }
|
|
520
|
+
* ```
|
|
521
|
+
*
|
|
522
|
+
* **Caso 3: Sesión válida (NO necesita re-login)**
|
|
523
|
+
* ```typescript
|
|
524
|
+
* if (!admin.needsReLogin()) {
|
|
525
|
+
* // Access token válido O refresh token disponible para renovar
|
|
526
|
+
* const data = await admin.databases.findMany()
|
|
527
|
+
* // Auto-refresh manejará la renovación si access token expira
|
|
528
|
+
* }
|
|
529
|
+
* ```
|
|
530
|
+
*
|
|
531
|
+
* @aiWarning
|
|
532
|
+
* **⚠️ Limitaciones:**
|
|
533
|
+
* - No detecta refresh token inválido (solo detecta ausencia o access token expirado)
|
|
534
|
+
* - Si refresh token está corrupto, `needsReLogin()` retorna `false` pero el refresh fallará
|
|
535
|
+
* - Para validación completa, usar try/catch con `BaserowReLoginRequiredError`
|
|
536
|
+
*
|
|
537
|
+
* **⚠️ Credenciales en Memoria:**
|
|
538
|
+
* - El wrapper `withAutoRelogin()` requiere credenciales en memoria
|
|
539
|
+
* - Evalúa riesgos de seguridad vs conveniencia
|
|
540
|
+
* - Alternativa: solicitar credenciales al usuario cuando `needsReLogin()` = true
|
|
541
|
+
*
|
|
542
|
+
* @returns `true` si necesita re-login, `false` si la sesión es válida
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```typescript
|
|
546
|
+
* // Validación proactiva simple
|
|
547
|
+
* if (admin.needsReLogin()) {
|
|
548
|
+
* console.log('Sesión expirada, solicitando credenciales...')
|
|
549
|
+
* // Mostrar formulario de login o solicitar credenciales
|
|
550
|
+
* const newAdmin = await ClientWithCredsWs.create({
|
|
551
|
+
* url: config.url,
|
|
552
|
+
* credentials: { email, password },
|
|
553
|
+
* workspace: config.workspace
|
|
554
|
+
* })
|
|
555
|
+
* } else {
|
|
556
|
+
* // Sesión válida, continuar normalmente
|
|
557
|
+
* const databases = await admin.databases.findMany()
|
|
558
|
+
* }
|
|
559
|
+
*
|
|
560
|
+
* // Con wrapper de auto-relogin
|
|
561
|
+
* const databases = await withAutoRelogin(
|
|
562
|
+
* admin,
|
|
563
|
+
* credentials,
|
|
564
|
+
* async (client) => client.databases.findMany()
|
|
565
|
+
* )
|
|
566
|
+
* ```
|
|
567
|
+
*
|
|
568
|
+
* @since 1.0.4
|
|
569
|
+
*/
|
|
570
|
+
needsReLogin(): boolean {
|
|
571
|
+
// No hay tokens = necesita login
|
|
572
|
+
if (!this.auth.getCurrentToken() || !this.auth['refreshToken']) {
|
|
573
|
+
return true
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Si el access token está expirado y no tenemos refresh token, necesita login
|
|
577
|
+
if (this.auth.isTokenExpired() && !this.auth['refreshToken']) {
|
|
578
|
+
return true
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return false
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Obtener estado detallado de autenticación
|
|
586
|
+
*
|
|
587
|
+
* Proporciona información completa sobre el estado actual de la sesión,
|
|
588
|
+
* útil para debugging y monitoreo de sesiones.
|
|
589
|
+
*
|
|
590
|
+
* @returns Objeto con información detallada del estado de autenticación
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```typescript
|
|
594
|
+
* const status = admin.getAuthStatus()
|
|
595
|
+
* console.log('Autenticado:', status.isAuthenticated)
|
|
596
|
+
* console.log('Token expira:', status.tokenExpiry)
|
|
597
|
+
* console.log('Tiene refresh token:', status.hasRefreshToken)
|
|
598
|
+
* console.log('Necesita re-login:', status.needsReLogin)
|
|
599
|
+
* ```
|
|
600
|
+
*
|
|
601
|
+
* @since 1.0.4
|
|
602
|
+
*/
|
|
603
|
+
getAuthStatus(): {
|
|
604
|
+
isAuthenticated: boolean
|
|
605
|
+
hasAccessToken: boolean
|
|
606
|
+
hasRefreshToken: boolean
|
|
607
|
+
tokenExpiry?: Date
|
|
608
|
+
isTokenExpired: boolean
|
|
609
|
+
needsReLogin: boolean
|
|
610
|
+
} {
|
|
611
|
+
const hasAccessToken = !!this.auth.getCurrentToken()
|
|
612
|
+
const hasRefreshToken = !!this.auth['refreshToken']
|
|
613
|
+
const tokenExpiry = this.auth['tokenExpiry'] as Date | undefined
|
|
614
|
+
const isTokenExpired = this.auth.isTokenExpired()
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
isAuthenticated: this.auth.isAuthenticated(),
|
|
618
|
+
hasAccessToken,
|
|
619
|
+
hasRefreshToken,
|
|
620
|
+
tokenExpiry,
|
|
621
|
+
isTokenExpired,
|
|
622
|
+
needsReLogin: this.needsReLogin()
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Cerrar sesión y limpiar tokens
|
|
628
|
+
*
|
|
629
|
+
* Invalida los tokens JWT, detiene el keep-alive y limpia el estado de autenticación.
|
|
630
|
+
* Después de llamar este método, el cliente necesitará volver a autenticarse.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```typescript
|
|
634
|
+
* // Al final de la aplicación o cambio de usuario
|
|
635
|
+
* admin.logout()
|
|
636
|
+
*
|
|
637
|
+
* // El cliente ya no está autenticado
|
|
638
|
+
* console.log(admin.isAuthenticated()) // false
|
|
639
|
+
* ```
|
|
640
|
+
*
|
|
641
|
+
* @since 1.0.0
|
|
642
|
+
*/
|
|
643
|
+
logout(): void {
|
|
644
|
+
this.auth.logout() // Esto internamente llama stopKeepAlive()
|
|
645
|
+
if (this.logger) {
|
|
646
|
+
this.logger.info('BaserowWithCredentialsWorkspace: Logged out')
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Obtener el workspace configurado
|
|
652
|
+
*
|
|
653
|
+
* Retorna la información completa del workspace al que está restringido este cliente.
|
|
654
|
+
* Útil para obtener ID, nombre y metadatos del workspace actual.
|
|
655
|
+
*
|
|
656
|
+
* @returns Información completa del workspace configurado
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* ```typescript
|
|
660
|
+
* const workspace = admin.getDefaultWorkspace()
|
|
661
|
+
* console.log(`Workspace actual: ${workspace.name} (ID: ${workspace.id})`)
|
|
662
|
+
* ```
|
|
663
|
+
*
|
|
664
|
+
* @since 1.0.0
|
|
665
|
+
*/
|
|
666
|
+
getDefaultWorkspace(): Workspace {
|
|
667
|
+
return this.defaultWorkspace
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Obtener ID del workspace configurado
|
|
672
|
+
*
|
|
673
|
+
* Retorna únicamente el ID numérico del workspace al que está restringido este cliente.
|
|
674
|
+
* Método de conveniencia para obtener solo el ID sin metadatos adicionales.
|
|
675
|
+
*
|
|
676
|
+
* @returns ID numérico del workspace configurado
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```typescript
|
|
680
|
+
* const workspaceId = admin.getWorkspaceId()
|
|
681
|
+
* console.log(`Operando en workspace ID: ${workspaceId}`)
|
|
682
|
+
* ```
|
|
683
|
+
*
|
|
684
|
+
* @since 1.0.0
|
|
685
|
+
*/
|
|
686
|
+
getWorkspaceId(): number {
|
|
687
|
+
return this.defaultWorkspace.id
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ============================================================================
|
|
691
|
+
// API DIRECTO - SIN WORKSPACE PREFIX
|
|
692
|
+
// ============================================================================
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Acceder a una database específica del workspace configurado (API simplificada)
|
|
696
|
+
*
|
|
697
|
+
* Crea un contexto de database que permite operaciones en tablas y filas.
|
|
698
|
+
* Solo puede acceder a databases del workspace configurado.
|
|
699
|
+
* Acepta tanto nombres de database como IDs numéricos.
|
|
700
|
+
*
|
|
701
|
+
* @param databaseIdentifier - Nombre de la database o ID numérico
|
|
702
|
+
* @returns Contexto de database con acceso a tablas y operaciones
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```typescript
|
|
706
|
+
* // Usando nombre de la database
|
|
707
|
+
* const tables = await admin.database('My Database').tables.findMany()
|
|
708
|
+
* const table = await admin.database('My Database').table('Users').create()
|
|
709
|
+
*
|
|
710
|
+
* // Usando ID numérico
|
|
711
|
+
* const rows = await admin.database(123).table('Users').rows.list()
|
|
712
|
+
*
|
|
713
|
+
* // API simplificada completa
|
|
714
|
+
* const newTable = await admin.database('CRM')
|
|
715
|
+
* .table('Customers')
|
|
716
|
+
* .create({
|
|
717
|
+
* data: [['Name', 'Email'], ['John', 'john@example.com']],
|
|
718
|
+
* first_row_header: true
|
|
719
|
+
* })
|
|
720
|
+
*
|
|
721
|
+
* // Gestión de campos
|
|
722
|
+
* const field = await admin.database('CRM')
|
|
723
|
+
* .table('Customers')
|
|
724
|
+
* .field.createText('Company', undefined, 'Nombre de la empresa')
|
|
725
|
+
* ```
|
|
726
|
+
*
|
|
727
|
+
* @since 1.0.0
|
|
728
|
+
*/
|
|
729
|
+
database(databaseIdentifier: string | number): DatabaseContext {
|
|
730
|
+
// Crear instancias de servicios según se necesiten para el contexto
|
|
731
|
+
const tableService = new TableService(this.http, this.logger)
|
|
732
|
+
const fieldService = new FieldService(this.http, this.logger)
|
|
733
|
+
const rowService = new RowService(this.http, this.logger)
|
|
734
|
+
|
|
735
|
+
return new DatabaseContext(
|
|
736
|
+
this.workspaceService,
|
|
737
|
+
this.defaultWorkspace.id,
|
|
738
|
+
databaseIdentifier,
|
|
739
|
+
this.databases,
|
|
740
|
+
tableService,
|
|
741
|
+
fieldService,
|
|
742
|
+
rowService,
|
|
743
|
+
this.logger
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Acceder a gestión de Database Tokens del workspace configurado
|
|
749
|
+
*
|
|
750
|
+
* Proporciona operaciones completas de Database Tokens limitadas al workspace actual.
|
|
751
|
+
* Los Database Tokens creados solo tendrán acceso a databases de este workspace.
|
|
752
|
+
*
|
|
753
|
+
* @returns Contexto de Database Token con operaciones CRUD
|
|
754
|
+
*
|
|
755
|
+
* @example
|
|
756
|
+
* ```typescript
|
|
757
|
+
* // Crear token con acceso completo al workspace
|
|
758
|
+
* const fullToken = await admin.databaseToken.createFullAccess('app-token')
|
|
759
|
+
*
|
|
760
|
+
* // Crear token con permisos específicos
|
|
761
|
+
* const readOnlyToken = await admin.databaseToken.create('readonly-token', {
|
|
762
|
+
* databases: [{ database_id: 123, permissions: 'read' }]
|
|
763
|
+
* })
|
|
764
|
+
*
|
|
765
|
+
* // Listar todos los tokens del workspace
|
|
766
|
+
* const tokens = await admin.databaseToken.list()
|
|
767
|
+
*
|
|
768
|
+
* // Gestionar token específico
|
|
769
|
+
* await admin.databaseToken.update(tokenId, { name: 'New Name' })
|
|
770
|
+
* await admin.databaseToken.delete(tokenId)
|
|
771
|
+
* ```
|
|
772
|
+
*
|
|
773
|
+
* @since 1.0.0
|
|
774
|
+
*/
|
|
775
|
+
get databaseToken(): DatabaseTokenContext {
|
|
776
|
+
return new DatabaseTokenContext(
|
|
777
|
+
this.workspaceService,
|
|
778
|
+
this.defaultWorkspace.id,
|
|
779
|
+
this.databaseTokenService,
|
|
780
|
+
this.logger
|
|
781
|
+
)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Obtener estadísticas de uso del workspace configurado
|
|
786
|
+
*
|
|
787
|
+
* Recopila información sobre recursos y estado del workspace al que está
|
|
788
|
+
* restringido este cliente. Útil para dashboards y monitoreo.
|
|
789
|
+
*
|
|
790
|
+
* @returns Promise con estadísticas completas del workspace
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* ```typescript
|
|
794
|
+
* const stats = await admin.getUsageStats()
|
|
795
|
+
* console.log(`Workspace: ${stats.workspace.name}`)
|
|
796
|
+
* console.log(`Databases: ${stats.totalDatabases}`)
|
|
797
|
+
* console.log(`Tables: ${stats.totalTables}`)
|
|
798
|
+
* console.log(`Autenticado: ${stats.isAuthenticated}`)
|
|
799
|
+
* console.log(`Token válido: ${stats.tokenValid}`)
|
|
800
|
+
* console.log(`Modo: ${stats.mode}`) // 'workspace-restricted'
|
|
801
|
+
* ```
|
|
802
|
+
*
|
|
803
|
+
* @since 1.0.0
|
|
804
|
+
*/
|
|
805
|
+
async getUsageStats(): Promise<{
|
|
806
|
+
totalDatabases: number
|
|
807
|
+
totalTables: number
|
|
808
|
+
isAuthenticated: boolean
|
|
809
|
+
tokenValid: boolean
|
|
810
|
+
workspace: { id: number; name: string }
|
|
811
|
+
mode: 'workspace-restricted'
|
|
812
|
+
}> {
|
|
813
|
+
// Filtrar databases del workspace configurado
|
|
814
|
+
const allDatabases = await this.databases.findMany()
|
|
815
|
+
const databases = allDatabases.filter(db => db.workspace?.id === this.defaultWorkspace.id)
|
|
816
|
+
|
|
817
|
+
// Usar las tablas que ya vienen en el objeto Database (más eficiente)
|
|
818
|
+
const totalTables = databases.reduce((sum, db) => sum + (db.tables?.length || 0), 0)
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
totalDatabases: databases.length,
|
|
822
|
+
totalTables,
|
|
823
|
+
isAuthenticated: this.isAuthenticated(),
|
|
824
|
+
tokenValid: !this.auth.isTokenExpired(),
|
|
825
|
+
mode: 'workspace-restricted',
|
|
826
|
+
workspace: {
|
|
827
|
+
id: this.defaultWorkspace.id,
|
|
828
|
+
name: this.defaultWorkspace.name
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Cerrar conexiones HTTP y limpiar recursos
|
|
835
|
+
*
|
|
836
|
+
* Detiene el keep-alive, libera recursos del cliente HTTP y cierra conexiones pendientes.
|
|
837
|
+
* Debe llamarse cuando el cliente ya no se necesite para evitar memory leaks.
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* ```typescript
|
|
841
|
+
* // Al final de la aplicación o cuando ya no se necesite el cliente
|
|
842
|
+
* admin.destroy()
|
|
843
|
+
* ```
|
|
844
|
+
*
|
|
845
|
+
* @since 1.0.0
|
|
846
|
+
*/
|
|
847
|
+
destroy(): void {
|
|
848
|
+
this.logger?.debug?.('Destroying BaserowWithCredentialsWorkspace')
|
|
849
|
+
this.auth.stopKeepAlive()
|
|
850
|
+
this.http.destroy()
|
|
851
|
+
}
|
|
852
|
+
}
|