@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,781 @@
1
+ import { HttpService } from './core/HttpService'
2
+ import { ValidationService } from './core/ValidationService'
3
+ import {
4
+ Table,
5
+ CreateTableRequest,
6
+ UpdateTableRequest,
7
+ BaserowResponse,
8
+ BaserowNotFoundError,
9
+ Field,
10
+ Logger,
11
+ TableFromAllTables
12
+ } from '../types/index'
13
+
14
+ /**
15
+ * Servicio para operaciones CRUD de tablas de Baserow
16
+ *
17
+ * Proporciona operaciones completas de tablas incluyendo CRUD básico,
18
+ * creación asíncrona, duplicación, reordenamiento y análisis de esquemas.
19
+ * Las tablas son contenedores de campos y filas en la jerarquía de Baserow.
20
+ *
21
+ * **Características:**
22
+ * - CRUD completo: create, read, update, delete
23
+ * - Creación síncrona y asíncrona (mejor para archivos grandes)
24
+ * - Búsqueda de tablas por nombre
25
+ * - Operaciones avanzadas: duplicar, reordenar
26
+ * - Análisis de esquemas y estadísticas
27
+ * - Validación de nombres y IDs
28
+ *
29
+ * **Jerarquía de Baserow:**
30
+ * ```
31
+ * Workspace → Database → Table → Field/Row
32
+ * ```
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // Operaciones básicas
37
+ * const tables = await tableService.findMany(databaseId)
38
+ * const table = await tableService.get(tableId)
39
+ *
40
+ * // Buscar tabla por nombre o ID
41
+ * const found = await tableService.findUnique(databaseId, 'usuarios')
42
+ *
43
+ * // Crear tabla con datos iniciales
44
+ * const newTable = await tableService.create(databaseId, {
45
+ * name: 'Usuarios',
46
+ * data: [['Nombre', 'Email'], ['Juan', 'juan@example.com']],
47
+ * first_row_header: true
48
+ * })
49
+ *
50
+ * // Operaciones avanzadas
51
+ * const duplicated = await tableService.duplicate(tableId, 'Copia de Usuarios')
52
+ * const stats = await tableService.getStats(tableId)
53
+ * ```
54
+ *
55
+ * @since 1.0.0
56
+ */
57
+ export class TableService extends HttpService {
58
+ private validationService: ValidationService
59
+
60
+ constructor(http: any, logger?: Logger) {
61
+ super(http, logger)
62
+ this.validationService = new ValidationService(http, logger)
63
+ }
64
+
65
+ // ===== PUBLIC API (Prisma-style) =====
66
+
67
+ /**
68
+ * Listar todas las tablas de una database
69
+ *
70
+ * Obtiene todas las tablas de una database específica con sus metadatos.
71
+ * Incluye información de campos si está disponible en la respuesta.
72
+ *
73
+ * @param databaseId - ID numérico de la database
74
+ * @returns Promise con array de tablas
75
+ *
76
+ * @throws {BaserowValidationError} Si el databaseId es inválido
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const tables = await tableService.findMany(123)
81
+ * tables.forEach(table => {
82
+ * console.log(`${table.name} (ID: ${table.id})`)
83
+ * })
84
+ * ```
85
+ *
86
+ * @since 1.0.0
87
+ */
88
+ async findMany(databaseId: number): Promise<Table[]> {
89
+ this.validationService.validateId(databaseId, 'database ID')
90
+
91
+ try {
92
+ const response = await this.http.get<BaserowResponse<Table> | Table[]>(`/database/tables/database/${databaseId}/`)
93
+
94
+ // La API puede devolver array directo o {results: []}
95
+ const tables = Array.isArray(response) ? response : response.results || []
96
+ this.logSuccess('find many tables', databaseId, { count: tables.length })
97
+ return tables
98
+ } catch (error) {
99
+ this.handleHttpError(error, 'find many tables', databaseId)
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Obtener tabla por ID
105
+ *
106
+ * Recupera una tabla específica por su ID con metadatos completos.
107
+ * Útil para obtener información detallada de una tabla.
108
+ *
109
+ * @param tableId - ID numérico de la tabla
110
+ * @returns Promise con datos completos de la tabla
111
+ *
112
+ * @throws {BaserowNotFoundError} Si la tabla no existe
113
+ * @throws {BaserowValidationError} Si el tableId es inválido
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * const table = await tableService.get(456)
118
+ * console.log(`Tabla: ${table.name} - Database: ${table.database_id}`)
119
+ * ```
120
+ *
121
+ * @since 1.0.0
122
+ */
123
+ async get(tableId: number): Promise<Table> {
124
+ this.validationService.validateId(tableId, 'table ID')
125
+ return this.getById<Table>('/database/tables', tableId)
126
+ }
127
+
128
+ /**
129
+ * Buscar tabla por ID o nombre en una database
130
+ *
131
+ * Busca una tabla específica por su ID o nombre dentro de una database.
132
+ * Útil para operaciones dinámicas donde se conoce el identificador pero no se sabe si es ID o nombre.
133
+ *
134
+ * @param databaseId - ID numérico de la database
135
+ * @param identifier - ID numérico o nombre de la tabla a buscar
136
+ * @returns Promise con la tabla encontrada o null si no existe
137
+ *
138
+ * @throws {BaserowValidationError} Si los parámetros son inválidos
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * const table = await tableService.findUnique(123, 'usuarios')
143
+ * if (table) {
144
+ * console.log(`Tabla encontrada: ${table.id}`)
145
+ * } else {
146
+ * console.log('Tabla no encontrada')
147
+ * }
148
+ * ```
149
+ *
150
+ * @since 1.0.0
151
+ */
152
+ async findUnique(databaseId: number, identifier: string | number): Promise<Table | null> {
153
+ this.validationService.validateId(databaseId, 'database ID')
154
+
155
+ if (typeof identifier === 'number') {
156
+ // Buscar por ID
157
+ try {
158
+ return await this.get(identifier)
159
+ } catch (error) {
160
+ if (error instanceof BaserowNotFoundError) {
161
+ return null
162
+ }
163
+ throw error
164
+ }
165
+ } else {
166
+ // Buscar por nombre
167
+ this.validationService.validateResourceName(identifier, 'table')
168
+ const tables = await this.findMany(databaseId)
169
+ const found = tables.find(table => table.name === identifier) || null
170
+
171
+ if (found) {
172
+ this.logSuccess(`find table by name "${identifier}"`, found.id)
173
+ } else {
174
+ this.logDebug(`No table found with name "${identifier}" in database ${databaseId}`)
175
+ }
176
+
177
+ return found
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Crear nueva tabla - API pública
183
+ *
184
+ * Crea una nueva tabla en la database especificada. Soporta creación
185
+ * con datos iniciales y configuración de esquema. Modo síncrono ideal
186
+ * para tablas pequeñas y medianas.
187
+ *
188
+ * **⚠️ Comportamiento importante de Baserow:**
189
+ * Cuando se crea una tabla SIN datos iniciales (`data` vacío o undefined),
190
+ * Baserow automáticamente agrega campos y filas de ejemplo. Esta librería
191
+ * **automáticamente limpia** este contenido usando `ensureTableEmpty()`
192
+ * para garantizar tablas mínimas (solo campo primario, 0 filas).
193
+ *
194
+ * **Nota**: Baserow NO permite eliminar el campo primario, por lo que las
195
+ * tablas "vacías" tendrán siempre 1 campo primario como mínimo.
196
+ *
197
+ * @param databaseId - ID numérico de la database
198
+ * @param data - Configuración de la tabla
199
+ * @param data.name - Nombre de la tabla
200
+ * @param data.data - Datos iniciales (array 2D, opcional)
201
+ * @param data.first_row_header - Primera fila como headers (default: false)
202
+ * @param data.skipDefaultCleanup - Saltarse limpieza automática del contenido por defecto de Baserow (default: false)
203
+ * @returns Promise con la tabla creada (garantizada mínima si no hay datos)
204
+ *
205
+ * @throws {BaserowValidationError} Si los datos son inválidos
206
+ *
207
+ * @example
208
+ * ```typescript
209
+ * // Tabla mínima (auto-limpiada)
210
+ * const emptyTable = await tableService.create(123, {
211
+ * name: 'Nueva Tabla'
212
+ * })
213
+ * // Resultado: 1 campo primario, 0 filas (limpieza automática)
214
+ *
215
+ * // Tabla con contenido por defecto de Baserow (sin limpieza)
216
+ * const defaultTable = await tableService.create(123, {
217
+ * name: 'Tabla con Defaults',
218
+ * skipDefaultCleanup: true
219
+ * })
220
+ * // Resultado: campos y filas de ejemplo creados por Baserow
221
+ *
222
+ * // Tabla con datos iniciales (sin limpieza)
223
+ * const dataTable = await tableService.create(123, {
224
+ * name: 'Usuarios',
225
+ * data: [
226
+ * ['Nombre', 'Email', 'Activo'],
227
+ * ['Juan', 'juan@example.com', true],
228
+ * ['María', 'maria@example.com', false]
229
+ * ],
230
+ * first_row_header: true
231
+ * })
232
+ * // Resultado: 3 campos, 2 filas (sin limpieza automática)
233
+ * ```
234
+ *
235
+ * @since 1.0.0
236
+ */
237
+ async create(databaseId: number, data: CreateTableRequest): Promise<Table> {
238
+ return this.createTableInternal(databaseId, data)
239
+ }
240
+
241
+ // ===== PRIVATE METHODS =====
242
+
243
+ /**
244
+ * Crear nueva tabla (método interno)
245
+ * @private - Solo para uso interno del servicio
246
+ */
247
+ private async createTableInternal(databaseId: number, data: CreateTableRequest): Promise<Table> {
248
+ this.validationService.validateId(databaseId, 'database ID')
249
+ this.validationService.validateResourceName(data.name, 'table')
250
+
251
+ try {
252
+ this.logDebug(`Creating table "${data.name}" in database ${databaseId}`, data)
253
+ const response = await this.http.post<Table>(`/database/tables/database/${databaseId}/`, data)
254
+
255
+ // Limpiar contenido por defecto de Baserow si es necesario
256
+ if (!data.data?.length && !data.skipDefaultCleanup) {
257
+ const cleanupStats = await this.ensureTableEmpty((response as any).id, data.name)
258
+ this.logDebug(`Table cleanup completed`, cleanupStats)
259
+ } else if (data.skipDefaultCleanup) {
260
+ this.logDebug(`Skipping default cleanup for table "${data.name}" as requested`)
261
+ }
262
+
263
+ this.logSuccess('create table', (response as any).id, { name: data.name })
264
+ return response
265
+ } catch (error) {
266
+ this.handleHttpError(error, 'create table', databaseId)
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Crear nueva tabla (modo asíncrono)
272
+ *
273
+ * Crea una nueva tabla de forma asíncrona, ideal para archivos grandes
274
+ * o datasets extensos. Retorna un job_id para monitorear el progreso.
275
+ *
276
+ * @param databaseId - ID numérico de la database
277
+ * @param data - Configuración de la tabla (igual que createTable)
278
+ * @returns Promise con job_id para monitorear la creación
279
+ *
280
+ * @throws {BaserowValidationError} Si los datos son inválidos
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * // Crear tabla asíncrona para archivo CSV grande
285
+ * const job = await tableService.createAsync(123, {
286
+ * name: 'Datos Masivos',
287
+ * data: largeCsvData,
288
+ * first_row_header: true
289
+ * })
290
+ *
291
+ * console.log(`Job iniciado: ${job.job_id}`)
292
+ * // Usar job_id para monitorear progreso
293
+ * ```
294
+ *
295
+ * @since 1.0.0
296
+ */
297
+ async createAsync(databaseId: number, data: CreateTableRequest): Promise<{ job_id: string }> {
298
+ this.validationService.validateId(databaseId, 'database ID')
299
+ this.validationService.validateResourceName(data.name, 'table')
300
+
301
+ try {
302
+ this.logDebug(`Creating table async "${data.name}" in database ${databaseId}`, data)
303
+ const response = await this.http.post<{ job_id: string }>(
304
+ `/database/tables/database/${databaseId}/create-async/`,
305
+ data
306
+ )
307
+
308
+ // NOTA: createAsync no puede usar ensureTableEmpty() porque retorna job_id, no table_id
309
+ // El usuario debe verificar el estado del job y limpiar la tabla manualmente si es necesario
310
+
311
+ this.logSuccess('create table async', `job_${response.job_id}`, { name: data.name })
312
+ return response
313
+ } catch (error) {
314
+ this.handleHttpError(error, 'create table async', databaseId)
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Actualizar tabla existente (método interno)
320
+ * @private - Solo para uso por TableContext
321
+ */
322
+ private async updateTableInternal(tableId: number, data: UpdateTableRequest): Promise<Table> {
323
+ this.validationService.validateId(tableId, 'table ID')
324
+
325
+ if (data.name !== undefined) {
326
+ this.validationService.validateResourceName(data.name, 'table')
327
+ }
328
+
329
+ try {
330
+ this.logDebug(`Updating table ${tableId}`, data)
331
+ const response = await this.http.patch<Table>(`/database/tables/${tableId}/`, data)
332
+ this.logSuccess('update table', tableId)
333
+ return response
334
+ } catch (error) {
335
+ if ((error as any).status === 404) {
336
+ throw new BaserowNotFoundError('Table', tableId)
337
+ }
338
+ this.handleHttpError(error, 'update table', tableId)
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Eliminar tabla (método interno)
344
+ * @private - Solo para uso por TableContext
345
+ */
346
+ private async deleteTableInternal(tableId: number): Promise<void> {
347
+ this.validationService.validateId(tableId, 'table ID')
348
+ return this.deleteById('/database/tables', tableId)
349
+ }
350
+
351
+ /**
352
+ * Verificar si una tabla existe
353
+ *
354
+ * Verifica la existencia de una tabla sin cargar todos sus metadatos.
355
+ * Útil para validaciones antes de operaciones.
356
+ *
357
+ * @param tableId - ID numérico de la tabla
358
+ * @returns Promise que resuelve a true si existe, false si no
359
+ *
360
+ * @example
361
+ * ```typescript
362
+ * const exists = await tableService.exists(456)
363
+ * if (exists) {
364
+ * console.log('La tabla existe')
365
+ * } else {
366
+ * console.log('La tabla no existe')
367
+ * }
368
+ * ```
369
+ *
370
+ * @since 1.0.0
371
+ */
372
+ async exists(tableId: number): Promise<boolean> {
373
+ try {
374
+ await this.get(tableId)
375
+ return true
376
+ } catch (error) {
377
+ if (error instanceof BaserowNotFoundError) {
378
+ return false
379
+ }
380
+ throw error
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Obtener esquema completo de una tabla
386
+ *
387
+ * Recupera el esquema completo de una tabla incluyendo sus metadatos
388
+ * y todos los campos con sus definiciones. Útil para análisis de estructura.
389
+ *
390
+ * @param tableId - ID numérico de la tabla
391
+ * @returns Promise con esquema completo (tabla + campos)
392
+ *
393
+ * @throws {BaserowValidationError} Si el tableId es inválido
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * const schema = await tableService.getSchema(456)
398
+ * console.log(`Tabla: ${schema.table.name}`)
399
+ * console.log(`Campos: ${schema.fields.length}`)
400
+ * schema.fields.forEach(field => {
401
+ * console.log(`- ${field.name} (${field.type})`)
402
+ * })
403
+ * ```
404
+ *
405
+ * @since 1.0.0
406
+ */
407
+ async getSchema(tableId: number): Promise<{
408
+ table: Table
409
+ fields: Field[]
410
+ }> {
411
+ this.validationService.validateId(tableId, 'table ID')
412
+
413
+ try {
414
+ this.logDebug(`Fetching schema for table ${tableId}`)
415
+ const [table, fieldsResponse] = await Promise.all([
416
+ this.get(tableId),
417
+ this.http.get<BaserowResponse<Field>>(`/database/fields/`, {
418
+ params: { table_id: tableId }
419
+ })
420
+ ])
421
+
422
+ this.logSuccess('get table schema', tableId, { fieldCount: fieldsResponse.results.length })
423
+ return {
424
+ table,
425
+ fields: fieldsResponse.results
426
+ }
427
+ } catch (error) {
428
+ this.handleHttpError(error, 'get table schema', tableId)
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Duplicar tabla
434
+ *
435
+ * Crea una copia exacta de una tabla existente incluyendo estructura
436
+ * y datos. Opcionalmente permite especificar un nuevo nombre.
437
+ *
438
+ * @param tableId - ID numérico de la tabla a duplicar
439
+ * @param newName - Nombre para la tabla duplicada (opcional)
440
+ * @returns Promise con la tabla duplicada
441
+ *
442
+ * @throws {BaserowValidationError} Si los parámetros son inválidos
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * const duplicated = await tableService.duplicate(456, 'Copia de Usuarios')
447
+ * console.log(`Tabla duplicada: ${duplicated.id}`)
448
+ * ```
449
+ *
450
+ * @since 1.0.0
451
+ */
452
+ async duplicate(tableId: number, newName?: string): Promise<Table> {
453
+ this.validationService.validateId(tableId, 'table ID')
454
+
455
+ const originalTable = await this.get(tableId)
456
+ const name = newName || `${originalTable.name} (copy)`
457
+
458
+ if (newName) {
459
+ this.validationService.validateResourceName(newName, 'table')
460
+ }
461
+
462
+ try {
463
+ this.logDebug(`Duplicating table ${tableId} with name "${name}"`, { originalName: originalTable.name })
464
+ const response = await this.http.post<Table>(`/database/tables/${tableId}/duplicate/`, { name })
465
+ this.logSuccess('duplicate table', (response as any).id, { originalId: tableId, newName: name })
466
+ return response
467
+ } catch (error) {
468
+ this.handleHttpError(error, 'duplicate table', tableId)
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Reordenar tabla en la database
474
+ *
475
+ * Cambia la posición de una tabla en el orden de visualización
476
+ * dentro de la database. Permite especificar posición relativa.
477
+ *
478
+ * @param tableId - ID numérico de la tabla a reordenar
479
+ * @param beforeId - ID de tabla antes de la cual colocar (opcional)
480
+ * @returns Promise con la tabla reordenada
481
+ *
482
+ * @throws {BaserowNotFoundError} Si alguna tabla no existe
483
+ * @throws {BaserowValidationError} Si los IDs son inválidos
484
+ *
485
+ * @example
486
+ * ```typescript
487
+ * // Mover tabla al final
488
+ * await tableService.reorder(456)
489
+ *
490
+ * // Mover tabla antes de otra específica
491
+ * await tableService.reorder(456, 123)
492
+ * ```
493
+ *
494
+ * @since 1.0.0
495
+ */
496
+ async reorder(tableId: number, beforeId?: number): Promise<Table> {
497
+ this.validationService.validateId(tableId, 'table ID')
498
+
499
+ const data: any = {}
500
+ if (beforeId !== undefined) {
501
+ this.validationService.validateId(beforeId, 'before table ID')
502
+ data.before_id = beforeId
503
+ }
504
+
505
+ try {
506
+ this.logDebug(`Reordering table ${tableId}`, data)
507
+ const response = await this.http.patch<Table>(`/database/tables/${tableId}/move/`, data)
508
+ this.logSuccess('reorder table', tableId)
509
+ return response
510
+ } catch (error) {
511
+ if ((error as any).status === 404) {
512
+ throw new BaserowNotFoundError('Table', tableId)
513
+ }
514
+ this.handleHttpError(error, 'reorder table', tableId)
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Obtener estadísticas de una tabla
520
+ *
521
+ * Recopila información estadística sobre una tabla incluyendo
522
+ * número de campos y filas. Útil para dashboards y monitoreo.
523
+ *
524
+ * @param tableId - ID numérico de la tabla
525
+ * @returns Promise con estadísticas de la tabla
526
+ *
527
+ * @throws {BaserowValidationError} Si el tableId es inválido
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * const stats = await tableService.getStats(456)
532
+ * console.log(`Campos: ${stats.fieldCount}`)
533
+ * console.log(`Filas: ${stats.rowCount}`)
534
+ * ```
535
+ *
536
+ * @since 1.0.0
537
+ */
538
+ async getStats(tableId: number): Promise<{
539
+ fieldCount: number
540
+ rowCount: number
541
+ }> {
542
+ this.validationService.validateId(tableId, 'table ID')
543
+
544
+ try {
545
+ this.logDebug(`Fetching stats for table ${tableId}`)
546
+ const [schema, rowCountResponse] = await Promise.all([
547
+ this.getSchema(tableId),
548
+ this.http.get<BaserowResponse<any>>(`/database/rows/table/${tableId}/`, { params: { size: 1 } })
549
+ ])
550
+
551
+ const stats = {
552
+ fieldCount: schema.fields.length,
553
+ rowCount: rowCountResponse.count
554
+ }
555
+
556
+ this.logSuccess('get table stats', tableId, stats)
557
+ return stats
558
+ } catch (error) {
559
+ this.handleHttpError(error, 'get table stats', tableId)
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Asegurar que una tabla esté completamente vacía (sin campos ni filas)
565
+ *
566
+ * **Contexto**: Baserow automáticamente crea campos y filas de ejemplo cuando
567
+ * se crea una tabla sin datos iniciales. Este método limpia ese contenido
568
+ * para garantizar tablas mínimas (solo campo primario).
569
+ *
570
+ * **Proceso de limpieza**:
571
+ * 1. Obtiene todos los campos de la tabla
572
+ * 2. Elimina campos secundarios (preserva campo primario obligatorio)
573
+ * 3. Verifica que no queden filas residuales
574
+ * 4. Registra estadísticas de limpieza
575
+ *
576
+ * **Limitación de Baserow**: El campo primario NO se puede eliminar
577
+ * (ERROR_CANNOT_DELETE_PRIMARY_FIELD), por lo que las tablas "vacías"
578
+ * siempre tendrán al menos 1 campo primario.
579
+ *
580
+ * @param tableId - ID de la tabla a limpiar
581
+ * @param tableName - Nombre de la tabla (para logging)
582
+ * @returns Promise que resuelve cuando la limpieza está completa
583
+ *
584
+ * @private
585
+ * @since 1.0.0
586
+ */
587
+ private async ensureTableEmpty(
588
+ tableId: number,
589
+ tableName: string
590
+ ): Promise<{
591
+ wasAlreadyEmpty: boolean
592
+ removedFields: number
593
+ finalRowCount: number
594
+ cleanupErrors: number
595
+ }> {
596
+ const stats = {
597
+ wasAlreadyEmpty: false,
598
+ removedFields: 0,
599
+ finalRowCount: 0,
600
+ cleanupErrors: 0
601
+ }
602
+
603
+ try {
604
+ this.logDebug(`[BASEROW CLEANUP] Ensuring table "${tableName}" (ID: ${tableId}) is empty`)
605
+
606
+ // 1. Verificar y limpiar campos por defecto de Baserow
607
+ const fieldsResponse = await this.http.get<BaserowResponse<Field> | Field[]>(`/database/fields/table/${tableId}/`)
608
+ const fields = Array.isArray(fieldsResponse) ? fieldsResponse : fieldsResponse.results || []
609
+
610
+ stats.removedFields = fields.length
611
+
612
+ if (fields.length <= 1 && fields.every(f => f.primary)) {
613
+ stats.wasAlreadyEmpty = true
614
+ this.logDebug(`[BASEROW CLEANUP] Table "${tableName}" only has primary field (no extra default fields created)`)
615
+ } else {
616
+ this.logger?.info(
617
+ `[BASEROW CLEANUP] Table "${tableName}" created with ${fields.length} default fields by Baserow - removing them`,
618
+ {
619
+ tableId,
620
+ fieldNames: fields.map(f => f.name),
621
+ fieldTypes: fields.map(f => f.type)
622
+ }
623
+ )
624
+
625
+ // Eliminar todos los campos EXCEPTO el campo primario
626
+ // Baserow no permite eliminar el campo primario (ERROR_CANNOT_DELETE_PRIMARY_FIELD)
627
+ const nonPrimaryFields = fields.filter(field => !field.primary)
628
+ const primaryFields = fields.filter(field => field.primary)
629
+
630
+ this.logger?.info(
631
+ `[BASEROW CLEANUP] Found ${fields.length} fields: ${primaryFields.length} primary, ${nonPrimaryFields.length} secondary`,
632
+ {
633
+ primaryFields: primaryFields.map(f => ({ name: f.name, id: f.id, type: f.type })),
634
+ secondaryFields: nonPrimaryFields.map(f => ({ name: f.name, id: f.id, type: f.type }))
635
+ }
636
+ )
637
+
638
+ // Solo eliminar campos no-primarios
639
+ for (const field of nonPrimaryFields) {
640
+ try {
641
+ await this.http.delete(`/database/fields/${field.id}/`)
642
+ this.logDebug(
643
+ `[BASEROW CLEANUP] Removed secondary field "${field.name}" (type: ${field.type}, ID: ${field.id})`
644
+ )
645
+ } catch (deleteError) {
646
+ stats.cleanupErrors++
647
+ this.logger?.error(`[BASEROW CLEANUP] Failed to remove field "${field.name}" from table "${tableName}":`, {
648
+ fieldId: field.id,
649
+ fieldType: field.type,
650
+ error: deleteError
651
+ })
652
+ }
653
+ }
654
+
655
+ // Actualizar stats para reflejar solo campos eliminables
656
+ stats.removedFields = nonPrimaryFields.length
657
+ }
658
+
659
+ // 2. Limpiar filas por defecto que crea Baserow
660
+ const rowsResponse = await this.http.get<BaserowResponse<any>>(`/database/rows/table/${tableId}/`, {
661
+ params: { size: 100 } // Obtener hasta 100 filas para eliminar
662
+ })
663
+
664
+ if (rowsResponse.count > 0) {
665
+ this.logger?.info(
666
+ `[BASEROW CLEANUP] Table "${tableName}" has ${rowsResponse.count} default rows - removing them`,
667
+ {
668
+ tableId,
669
+ rowIds: rowsResponse.results?.slice(0, 5).map((r: any) => r.id) || []
670
+ }
671
+ )
672
+
673
+ // Eliminar todas las filas por defecto
674
+ const allRows = rowsResponse.results || []
675
+ for (const row of allRows) {
676
+ try {
677
+ await this.http.delete(`/database/rows/table/${tableId}/${row.id}/`)
678
+ this.logDebug(`[BASEROW CLEANUP] Removed default row ID ${row.id} from table "${tableName}"`)
679
+ } catch (deleteError) {
680
+ stats.cleanupErrors++
681
+ this.logger?.error(`[BASEROW CLEANUP] Failed to remove row ${row.id} from table "${tableName}":`, {
682
+ rowId: row.id,
683
+ error: deleteError
684
+ })
685
+ }
686
+ }
687
+
688
+ // Verificar estado final después de limpiar filas
689
+ const finalRowsResponse = await this.http.get<BaserowResponse<any>>(`/database/rows/table/${tableId}/`, {
690
+ params: { size: 1 }
691
+ })
692
+ stats.finalRowCount = finalRowsResponse.count
693
+ } else {
694
+ stats.finalRowCount = 0
695
+ }
696
+
697
+ // 3. Log resultado final
698
+ const resultLevel = stats.cleanupErrors > 0 ? 'warn' : 'info'
699
+ const message = stats.wasAlreadyEmpty
700
+ ? `[BASEROW CLEANUP] Table "${tableName}" only had primary field`
701
+ : `[BASEROW CLEANUP] Successfully cleaned table "${tableName}" (primary field preserved)`
702
+
703
+ this.logger?.[resultLevel]?.(message, {
704
+ tableId,
705
+ tableName,
706
+ wasAlreadyEmpty: stats.wasAlreadyEmpty,
707
+ removedFields: stats.removedFields,
708
+ finalRowCount: stats.finalRowCount,
709
+ cleanupErrors: stats.cleanupErrors,
710
+ summary: `Removed ${stats.removedFields} fields, ${stats.finalRowCount} rows remaining, ${stats.cleanupErrors} errors`
711
+ })
712
+
713
+ this.logSuccess('ensured table empty', tableId, stats)
714
+ return stats
715
+ } catch (error) {
716
+ // Log error pero no fallar la creación de la tabla
717
+ this.logger?.error(`[BASEROW CLEANUP] Failed to verify/clean table "${tableName}":`, {
718
+ tableId,
719
+ tableName,
720
+ error: error,
721
+ note: 'Table creation succeeded but cleanup failed'
722
+ })
723
+
724
+ return {
725
+ ...stats,
726
+ cleanupErrors: stats.cleanupErrors + 1
727
+ }
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Listar todas las tablas accesibles con Database Token
733
+ *
734
+ * Endpoint específico para Database Tokens que devuelve todas las tablas
735
+ * a las que el token tiene acceso, sin necesidad de especificar database.
736
+ *
737
+ * @returns Promise con array de tablas con información básica
738
+ * @throws {BaserowError} Si hay error de red o el token no es válido
739
+ *
740
+ * @example
741
+ * ```typescript
742
+ * const tables = await tableService.findAllTablesWithToken()
743
+ * console.log(`Token tiene acceso a ${tables.length} tablas`)
744
+ * tables.forEach(table => {
745
+ * console.log(`Tabla: ${table.name} (DB: ${table.database_id})`)
746
+ * })
747
+ * ```
748
+ *
749
+ * @since 1.0.0
750
+ */
751
+ async findAllTablesWithToken(): Promise<TableFromAllTables[]> {
752
+ try {
753
+ this.logDebug('Fetching all tables accessible with database token')
754
+ const response = await this.http.get<TableFromAllTables[]>('/database/tables/all-tables/')
755
+
756
+ // La respuesta es directamente un array de tablas
757
+ const tables = Array.isArray(response) ? response : []
758
+
759
+ this.logSuccess('fetch all tables with token', undefined, { count: tables.length })
760
+ return tables
761
+ } catch (error) {
762
+ this.handleHttpError(error, 'fetch all tables with token')
763
+ }
764
+ }
765
+
766
+ // ===== FRIEND ACCESS PATTERN FOR TABLE CONTEXT =====
767
+ // Symbol-based access que no aparece en la API pública pero permite acceso interno
768
+
769
+ /**
770
+ * Friend access para TableContext
771
+ * No aparece en intellisense normal ni en la API pública
772
+ * @internal
773
+ */
774
+ get [Symbol.for('tableContext')]() {
775
+ return {
776
+ createTable: this.createTableInternal.bind(this),
777
+ updateTable: this.updateTableInternal.bind(this),
778
+ deleteTable: this.deleteTableInternal.bind(this)
779
+ }
780
+ }
781
+ }