@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,982 @@
1
+ import { HttpService } from './core/HttpService'
2
+ import { ValidationService } from './core/ValidationService'
3
+ import {
4
+ Row,
5
+ CreateRowRequest,
6
+ UpdateRowRequest,
7
+ QueryOptions,
8
+ BulkOptions,
9
+ BaserowResponse,
10
+ BaserowNotFoundError,
11
+ Logger
12
+ } from '../types/index'
13
+ import { chunkArray, delay } from '../utils/validation'
14
+
15
+ /**
16
+ * Servicio para operaciones CRUD de filas de Baserow
17
+ *
18
+ * Proporciona operaciones completas de filas incluyendo CRUD básico,
19
+ * operaciones bulk optimizadas y métodos de utilidad avanzados.
20
+ * Maneja paginación automática, rate limiting y validación robusta.
21
+ *
22
+ * **Características:**
23
+ * - CRUD completo: create, read, update, delete
24
+ * - Operaciones bulk optimizadas con chunking automático
25
+ * - `listAll()` con paginación automática para descargar tablas completas
26
+ * - Rate limiting automático entre requests
27
+ * - Validación de datos y IDs
28
+ * - Manejo robusto de errores
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // Operaciones básicas
33
+ * const rows = await rowService.list(tableId, { size: 50 })
34
+ * const row = await rowService.get(tableId, rowId)
35
+ * const newRow = await rowService.createRow(tableId, { name: 'John', email: 'john@example.com' })
36
+ * await rowService.updateRow(tableId, rowId, { name: 'Jane' })
37
+ * await rowService.delete(tableId, rowId)
38
+ *
39
+ * // Operaciones bulk
40
+ * const allRows = await rowService.listAll(tableId) // Descarga tabla completa
41
+ * const createdRows = await rowService.createBulk(tableId, [
42
+ * { name: 'Alice', email: 'alice@example.com' },
43
+ * { name: 'Bob', email: 'bob@example.com' }
44
+ * ])
45
+ * ```
46
+ *
47
+ * @since 1.0.0
48
+ */
49
+ export class RowService extends HttpService {
50
+ private validationService: ValidationService
51
+
52
+ constructor(http: any, logger?: Logger) {
53
+ super(http, logger)
54
+ this.validationService = new ValidationService(http, logger)
55
+ }
56
+
57
+ /**
58
+ * Obtener filas de una tabla con opciones de filtrado y paginación
59
+ *
60
+ * Lista filas de una tabla específica con soporte completo para filtrado,
61
+ * ordenamiento, búsqueda y paginación. Retorna metadatos de paginación.
62
+ *
63
+ * @param tableId - ID numérico de la tabla
64
+ * @param options - Opciones de consulta
65
+ * @param options.page - Número de página (default: 1)
66
+ * @param options.size - Tamaño de página (default: 100)
67
+ * @param options.search - Término de búsqueda en todos los campos
68
+ * @param options.order_by - Campo para ordenar (prefijo '-' para desc)
69
+ * @param options.filters - Filtros por campo específico
70
+ * @param options.user_field_names - Usar nombres de usuario para campos (default: true)
71
+ * @returns Promise con filas y metadatos de paginación
72
+ *
73
+ * @throws {BaserowValidationError} Si el tableId es inválido
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * // Lista básica
78
+ * const result = await rowService.list(123)
79
+ * console.log(`${result.count} filas total, mostrando ${result.rows.length}`)
80
+ *
81
+ * // Con filtros y ordenamiento
82
+ * const filtered = await rowService.list(123, {
83
+ * search: 'John',
84
+ * order_by: '-created_at',
85
+ * filters: { status: 'active' },
86
+ * size: 50
87
+ * })
88
+ * ```
89
+ *
90
+ * @since 1.0.0
91
+ */
92
+ async list(
93
+ tableId: number,
94
+ options: QueryOptions = {}
95
+ ): Promise<{
96
+ rows: Row[]
97
+ count: number
98
+ next: string | null
99
+ previous: string | null
100
+ }> {
101
+ this.validationService.validateId(tableId, 'table ID')
102
+
103
+ const params: any = {
104
+ user_field_names: options.user_field_names ?? true,
105
+ ...options
106
+ }
107
+
108
+ // Aplanar filtros directamente en query params para Baserow API
109
+ if (options.filters && typeof options.filters === 'object' && Object.keys(options.filters).length > 0) {
110
+ Object.assign(params, options.filters)
111
+ delete params.filters // No enviar el objeto anidado
112
+ }
113
+
114
+ // Preservar filter_type si existe (para OR queries)
115
+ if (options.filter_type) {
116
+ params.filter_type = options.filter_type
117
+ }
118
+
119
+ try {
120
+ this.logDebug(`Fetching rows from table ${tableId}`, options)
121
+ this.logDebug(`Final query params being sent to Baserow API:`, params)
122
+ const response = await this.http.get<BaserowResponse<Row>>(`/database/rows/table/${tableId}/`, params)
123
+
124
+ const result = {
125
+ rows: response.results,
126
+ count: response.count,
127
+ next: response.next,
128
+ previous: response.previous
129
+ }
130
+
131
+ this.logSuccess('list rows', tableId, { count: result.count, pageSize: result.rows.length })
132
+ return result
133
+ } catch (error) {
134
+ this.handleHttpError(error, 'list rows', tableId)
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Obtener todas las filas de una tabla (maneja paginación automáticamente)
140
+ *
141
+ * Descarga la tabla completa manejando paginación automáticamente.
142
+ * Ideal para exportar datos o análisis completo de tablas.
143
+ * Incluye rate limiting automático entre páginas.
144
+ *
145
+ * @aiUsage
146
+ * **✅ Usar cuando:**
147
+ * - Necesitas exportar/analizar la tabla completa (< 50K filas)
148
+ * - Generar reportes o backups completos
149
+ * - Sincronización de datos entre sistemas
150
+ * - Análisis de datos que requiere dataset completo
151
+ *
152
+ * **❌ NO usar cuando:**
153
+ * - Tabla tiene > 50K filas (alto consumo de memoria)
154
+ * - Solo necesitas primeras N filas (usa `list()` con paginación)
155
+ * - Datos cambian frecuentemente durante la carga (inconsistencias)
156
+ * - Aplicación tiene límites de memoria estrictos
157
+ *
158
+ * @aiWarning
159
+ * **⚠️ Consumo de Memoria:**
160
+ * - Cada fila ocupa ~1-5 KB en memoria (depende de campos)
161
+ * - 10,000 filas ≈ 10-50 MB
162
+ * - 50,000 filas ≈ 50-250 MB
163
+ * - 100,000+ filas → ⚠️ Posible OutOfMemory en Node.js
164
+ *
165
+ * **⚠️ Tiempo de Ejecución:**
166
+ * - ~200 filas/segundo (aprox. 5 páginas/segundo con pageSize=200)
167
+ * - 10,000 filas → ~50 segundos
168
+ * - 50,000 filas → ~4-5 minutos
169
+ *
170
+ * @aiAlternative
171
+ * **Para tablas grandes (>50K filas), usa paginación manual:**
172
+ * ```typescript
173
+ * // Procesar en streaming sin cargar todo en memoria
174
+ * let page = 1
175
+ * while (true) {
176
+ * const result = await rowService.list(tableId, { page, size: 200 })
177
+ *
178
+ * // Procesar chunk inmediatamente
179
+ * await processChunk(result.rows)
180
+ *
181
+ * if (!result.next) break
182
+ * page++
183
+ * }
184
+ * ```
185
+ *
186
+ * @aiPerformance
187
+ * **Optimizaciones:**
188
+ * - `size: 200` (default) → Balance ideal entre requests y memoria
189
+ * - `delay: 50ms` (default) → Evita rate limiting
190
+ * - Para tablas pequeñas (<5K): aumentar `size: 500` y reducir `delay: 0`
191
+ * - Para tablas grandes (>20K): reducir `size: 100` y aumentar `delay: 100ms`
192
+ *
193
+ * @param tableId - ID numérico de la tabla
194
+ * @param options - Opciones de consulta (sin page/size)
195
+ * @param options.search - Término de búsqueda
196
+ * @param options.order_by - Campo para ordenar
197
+ * @param options.filters - Filtros por campo
198
+ * @param options.delay - Delay entre páginas en ms (default: 50)
199
+ * @param options.size - Tamaño de página interna (default: 200)
200
+ * @returns Promise con array de todas las filas
201
+ *
202
+ * @throws {BaserowValidationError} Si el tableId es inválido
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * // Descargar tabla completa
207
+ * const allRows = await rowService.listAll(123)
208
+ * console.log(`Descargadas ${allRows.length} filas`)
209
+ *
210
+ * // Con filtros aplicados
211
+ * const activeUsers = await rowService.listAll(123, {
212
+ * filters: { status: 'active' },
213
+ * order_by: 'name'
214
+ * })
215
+ *
216
+ * // Optimizado para tabla grande (>20K filas)
217
+ * const allRows = await rowService.listAll(123, {
218
+ * size: 100, // Chunks más pequeños
219
+ * delay: 100 // Más tiempo entre requests
220
+ * })
221
+ * ```
222
+ *
223
+ * @since 1.0.0
224
+ */
225
+ async listAll(tableId: number, options: Omit<QueryOptions, 'page' | 'size'> = {}): Promise<Row[]> {
226
+ this.validationService.validateId(tableId, 'table ID')
227
+
228
+ const allRows: Row[] = []
229
+ let page = 1
230
+ const size = (options as any).size || 200 // Tamaño de página configurable
231
+
232
+ this.logInfo(`Starting listAll for table ${tableId}`, { pageSize: size })
233
+
234
+ while (true) {
235
+ const result = await this.list(tableId, {
236
+ ...options,
237
+ page,
238
+ size
239
+ })
240
+
241
+ allRows.push(...result.rows)
242
+ this.logDebug(`Fetched page ${page}`, { rowsThisPage: result.rows.length, totalRows: allRows.length })
243
+
244
+ if (!result.next) {
245
+ break
246
+ }
247
+
248
+ page++
249
+ await delay((options as any).delay || 50) // Rate limiting configurable
250
+ }
251
+
252
+ this.logSuccess('listAll completed', tableId, { totalRows: allRows.length, pages: page })
253
+ return allRows
254
+ }
255
+
256
+ /**
257
+ * Obtener fila específica por ID
258
+ *
259
+ * Recupera una fila individual por su ID con datos completos.
260
+ * Útil para operaciones de lectura específicas.
261
+ *
262
+ * @param tableId - ID numérico de la tabla
263
+ * @param rowId - ID numérico de la fila
264
+ * @param userFieldNames - Usar nombres de usuario para campos (default: true)
265
+ * @returns Promise con datos completos de la fila
266
+ *
267
+ * @throws {BaserowNotFoundError} Si la fila no existe
268
+ * @throws {BaserowValidationError} Si los IDs son inválidos
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * const row = await rowService.get(123, 456)
273
+ * console.log(`Usuario: ${row.name} - ${row.email}`)
274
+ * ```
275
+ *
276
+ * @since 1.0.0
277
+ */
278
+ async get(tableId: number, rowId: number, userFieldNames = true): Promise<Row> {
279
+ this.validationService.validateId(tableId, 'table ID')
280
+ this.validationService.validateId(rowId, 'row ID')
281
+
282
+ try {
283
+ this.logDebug(`Fetching row ${rowId} from table ${tableId}`, { userFieldNames })
284
+ const response = await this.http.get<Row>(`/database/rows/table/${tableId}/${rowId}/`, {
285
+ user_field_names: userFieldNames
286
+ })
287
+ this.logSuccess('get row', rowId)
288
+ return response
289
+ } catch (error) {
290
+ if ((error as any).status === 404) {
291
+ throw new BaserowNotFoundError('Row', rowId)
292
+ }
293
+ this.handleHttpError(error, 'get row', rowId)
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Crear nueva fila
299
+ *
300
+ * Crea una nueva fila en la tabla especificada con los datos proporcionados.
301
+ * Valida tipos de datos y estructura según el esquema de la tabla.
302
+ *
303
+ * @param tableId - ID numérico de la tabla
304
+ * @param data - Datos de la nueva fila (usar nombres de campo de usuario)
305
+ * @returns Promise con la fila creada (incluye ID asignado)
306
+ *
307
+ * @throws {BaserowValidationError} Si los datos son inválidos
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * const newRow = await rowService.createRow(123, {
312
+ * name: 'John Doe',
313
+ * email: 'john@example.com',
314
+ * age: 30,
315
+ * active: true
316
+ * })
317
+ * console.log(`Fila creada con ID: ${newRow.id}`)
318
+ * ```
319
+ *
320
+ * @since 1.0.0
321
+ */
322
+ async createRow(tableId: number, data: CreateRowRequest): Promise<Row> {
323
+ this.validationService.validateId(tableId, 'table ID')
324
+
325
+ if (!data || typeof data !== 'object') {
326
+ throw new Error('Row data is required and must be an object')
327
+ }
328
+
329
+ try {
330
+ this.logDebug(`Creating row in table ${tableId}`, data)
331
+ const response = await this.http.post<Row>(`/database/rows/table/${tableId}/`, data, {
332
+ user_field_names: true
333
+ })
334
+
335
+ this.logSuccess('create row', response.id)
336
+ return { ...response, id: response.id }
337
+ } catch (error) {
338
+ this.handleHttpError(error, 'create row', tableId)
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Actualizar fila existente
344
+ *
345
+ * Actualiza una fila existente con nuevos datos. Solo actualiza
346
+ * los campos proporcionados (actualización parcial).
347
+ *
348
+ * @param tableId - ID numérico de la tabla
349
+ * @param rowId - ID numérico de la fila a actualizar
350
+ * @param data - Datos a actualizar (solo campos modificados)
351
+ * @returns Promise con la fila actualizada
352
+ *
353
+ * @throws {BaserowNotFoundError} Si la fila no existe
354
+ * @throws {BaserowValidationError} Si los datos son inválidos
355
+ *
356
+ * @example
357
+ * ```typescript
358
+ * const updatedRow = await rowService.updateRow(123, 456, {
359
+ * email: 'newemail@example.com',
360
+ * active: false
361
+ * })
362
+ * console.log(`Fila actualizada: ${updatedRow.name}`)
363
+ * ```
364
+ *
365
+ * @since 1.0.0
366
+ */
367
+ async updateRow(tableId: number, rowId: number, data: UpdateRowRequest): Promise<Row> {
368
+ this.validationService.validateId(tableId, 'table ID')
369
+ this.validationService.validateId(rowId, 'row ID')
370
+
371
+ if (!data || typeof data !== 'object') {
372
+ throw new Error('Row data is required and must be an object')
373
+ }
374
+
375
+ try {
376
+ this.logDebug(`Updating row ${rowId} in table ${tableId}`, data)
377
+ const response = await this.http.patch<Row>(`/database/rows/table/${tableId}/${rowId}/`, data, {
378
+ user_field_names: true
379
+ })
380
+ this.logSuccess('update row', rowId)
381
+ return response
382
+ } catch (error) {
383
+ if ((error as any).status === 404) {
384
+ throw new BaserowNotFoundError('Row', rowId)
385
+ }
386
+ this.handleHttpError(error, 'update row', rowId)
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Eliminar fila
392
+ *
393
+ * Elimina permanentemente una fila de la tabla.
394
+ * Esta operación no se puede deshacer.
395
+ *
396
+ * @param tableId - ID numérico de la tabla
397
+ * @param rowId - ID numérico de la fila a eliminar
398
+ * @returns Promise que resuelve cuando la fila es eliminada
399
+ *
400
+ * @throws {BaserowNotFoundError} Si la fila no existe
401
+ * @throws {BaserowValidationError} Si los IDs son inválidos
402
+ *
403
+ * @example
404
+ * ```typescript
405
+ * await rowService.delete(123, 456)
406
+ * console.log('Fila eliminada exitosamente')
407
+ * ```
408
+ *
409
+ * @since 1.0.0
410
+ */
411
+ async delete(tableId: number, rowId: number): Promise<void> {
412
+ this.validationService.validateId(tableId, 'table ID')
413
+ this.validationService.validateId(rowId, 'row ID')
414
+
415
+ try {
416
+ this.logDebug(`Deleting row ${rowId} from table ${tableId}`)
417
+ await this.http.delete(`/database/rows/table/${tableId}/${rowId}/`)
418
+ this.logSuccess('delete row', rowId)
419
+ } catch (error) {
420
+ if ((error as any).status === 404) {
421
+ throw new BaserowNotFoundError('Row', rowId)
422
+ }
423
+ this.handleHttpError(error, 'delete row', rowId)
424
+ }
425
+ }
426
+
427
+ // ===== OPERACIONES BULK =====
428
+
429
+ /**
430
+ * Crear múltiples filas en lotes (bulk create)
431
+ *
432
+ * Crea múltiples filas de forma eficiente usando operaciones batch.
433
+ * Procesa en chunks para optimizar rendimiento y evitar timeouts.
434
+ * Incluye rate limiting automático entre lotes.
435
+ *
436
+ * @aiUsage
437
+ * **✅ Usar cuando:**
438
+ * - Importar datos desde CSV, JSON o bases de datos externas
439
+ * - Migración de datos entre tablas o sistemas
440
+ * - Seed de datos de prueba o inicialización
441
+ * - Inserción masiva desde formularios o APIs (>10 filas)
442
+ *
443
+ * **❌ NO usar cuando:**
444
+ * - Solo 1-5 filas → Usar `createRow()` individual (más simple)
445
+ * - Necesitas validación compleja fila por fila (usa loop con try/catch)
446
+ * - Datos vienen de stream continuo (procesar en mini-batches)
447
+ *
448
+ * @aiPerformance
449
+ * **Tamaño de Batch Óptimo (batchSize):**
450
+ * - `100-200` → **Recomendado general** (balance velocidad/confiabilidad)
451
+ * - `50-100` → Conexiones lentas o datos complejos (muchos campos)
452
+ * - `200-500` → Conexiones rápidas y datos simples (pocos campos)
453
+ * - `> 500` → ⚠️ Riesgo de timeout en Baserow API
454
+ *
455
+ * **Rendimiento Estimado:**
456
+ * - ~1,000 filas/minuto con batchSize=200 (velocidad promedio)
457
+ * - 10,000 filas → ~10 minutos
458
+ * - 50,000 filas → ~50 minutos
459
+ *
460
+ * @aiErrorHandling
461
+ * **Manejo de Errores Parciales:**
462
+ * ```typescript
463
+ * // Si un batch falla, TODA la operación se detiene
464
+ * try {
465
+ * const created = await rowService.createBulk(tableId, rows, { batchSize: 100 })
466
+ * console.log(`✅ ${created.length} filas creadas`)
467
+ * } catch (error) {
468
+ * console.error('❌ Error en batch, algunas filas pueden haberse creado')
469
+ * // Verificar cuántas filas se crearon antes del error
470
+ * const currentCount = await rowService.count(tableId)
471
+ * console.log(`Total filas actuales: ${currentCount}`)
472
+ * }
473
+ * ```
474
+ *
475
+ * **Para operaciones críticas, considera transacciones manuales:**
476
+ * ```typescript
477
+ * const created = []
478
+ * const failed = []
479
+ *
480
+ * for (const chunk of chunks(rows, 100)) {
481
+ * try {
482
+ * const result = await rowService.createBulk(tableId, chunk, { batchSize: 100 })
483
+ * created.push(...result)
484
+ * } catch (error) {
485
+ * failed.push({ chunk, error })
486
+ * // Continuar o abortar según lógica de negocio
487
+ * }
488
+ * }
489
+ *
490
+ * console.log(`Creadas: ${created.length}, Fallidas: ${failed.length}`)
491
+ * ```
492
+ *
493
+ * @aiWarning
494
+ * **⚠️ Webhooks Deshabilitados por Default:**
495
+ * - `send_webhook_events: false` (default) → Sin notificaciones
496
+ * - Para habilitar webhooks: `{ send_webhook_events: true }`
497
+ * - ⚠️ Con webhooks habilitados, la operación es ~20-30% más lenta
498
+ *
499
+ * @param tableId - ID numérico de la tabla
500
+ * @param rows - Array de datos para crear filas
501
+ * @param options - Opciones de la operación bulk
502
+ * @param options.batchSize - Tamaño de lote (default: 200)
503
+ * @param options.send_webhook_events - Enviar eventos webhook (default: false)
504
+ * @param options.user_field_names - Usar nombres de usuario (default: true)
505
+ * @returns Promise con array de filas creadas
506
+ *
507
+ * @throws {BaserowValidationError} Si los datos son inválidos
508
+ *
509
+ * @example
510
+ * ```typescript
511
+ * // Importación básica
512
+ * const newRows = await rowService.createBulk(123, [
513
+ * { name: 'Alice', email: 'alice@example.com' },
514
+ * { name: 'Bob', email: 'bob@example.com' },
515
+ * // ... más filas
516
+ * ], { batchSize: 100 })
517
+ * console.log(`${newRows.length} filas creadas`)
518
+ *
519
+ * // Con webhooks habilitados (notificaciones activas)
520
+ * const created = await rowService.createBulk(tableId, rows, {
521
+ * batchSize: 150,
522
+ * send_webhook_events: true
523
+ * })
524
+ *
525
+ * // Importación desde CSV con logging
526
+ * const csvData = await parseCsv('data.csv')
527
+ * console.log(`Importando ${csvData.length} filas...`)
528
+ *
529
+ * const imported = await rowService.createBulk(tableId, csvData, {
530
+ * batchSize: 200
531
+ * })
532
+ *
533
+ * console.log(`✅ Importadas ${imported.length} de ${csvData.length} filas`)
534
+ * ```
535
+ *
536
+ * @since 1.0.0
537
+ */
538
+ async createBulk(tableId: number, rows: CreateRowRequest[], options: BulkOptions = {}): Promise<Row[]> {
539
+ this.validationService.validateId(tableId, 'table ID')
540
+
541
+ if (!rows || !Array.isArray(rows) || rows.length === 0) {
542
+ throw new Error('Rows must be a non-empty array')
543
+ }
544
+
545
+ const { batchSize = 200, send_webhook_events = false, user_field_names = true } = options
546
+
547
+ const chunks = chunkArray(rows, batchSize)
548
+ const results: Row[] = []
549
+
550
+ this.logger?.info('🔧 Bulk create iniciado', { tableId, rowCount: rows.length, batchCount: chunks.length })
551
+
552
+ for (let i = 0; i < chunks.length; i++) {
553
+ const chunk = chunks[i]
554
+ const batchNum = i + 1
555
+
556
+ try {
557
+ this.logger?.info('Procesando lote', { batchNum, totalBatches: chunks.length, batchSize: chunk.length })
558
+
559
+ const response = await this.http.post<{ items: Row[] }>(
560
+ `/database/rows/table/${tableId}/batch/`,
561
+ { items: chunk },
562
+ {
563
+ user_field_names,
564
+ send_webhook_events
565
+ }
566
+ )
567
+
568
+ results.push(...(response.items || []))
569
+
570
+ // Rate limiting entre lotes
571
+ if (i < chunks.length - 1) {
572
+ await delay(100)
573
+ }
574
+
575
+ this.logger?.info('Lote procesado correctamente', { batchNum })
576
+ } catch (error) {
577
+ this.logger?.error('❌ Error en lote', { batchNum, totalBatches: chunks.length, error })
578
+ throw error
579
+ }
580
+ }
581
+
582
+ this.logger?.info('✅ Bulk create completado', { createdRows: results.length })
583
+ return results
584
+ }
585
+
586
+ /**
587
+ * Actualizar múltiples filas en lotes (bulk update)
588
+ *
589
+ * Actualiza múltiples filas de forma eficiente usando operaciones batch.
590
+ * Cada elemento del array debe incluir el ID de la fila a actualizar.
591
+ * Procesa en chunks con rate limiting automático.
592
+ *
593
+ * @aiUsage
594
+ * **✅ Usar cuando:**
595
+ * - Actualizar estado/status de múltiples registros (ej: marcar como procesado)
596
+ * - Sincronización de datos desde sistema externo (>10 filas)
597
+ * - Actualizar campos calculados en batch (ej: recalcular totales)
598
+ * - Operaciones administrativas masivas (cambiar propietario, categoría, etc.)
599
+ *
600
+ * **❌ NO usar cuando:**
601
+ * - Solo 1-5 filas → Usar `updateRow()` individual
602
+ * - Updates dependen de estado previo (race conditions) → Actualizar secuencialmente
603
+ * - Necesitas rollback complejo → Implementar transacciones manuales
604
+ *
605
+ * @aiPerformance
606
+ * **Igual que `createBulk()`:**
607
+ * - **Batch Size Recomendado:** 100-200 filas
608
+ * - **Rendimiento:** ~1,000 actualizaciones/minuto
609
+ *
610
+ * @aiErrorHandling
611
+ * **Validación Estricta de IDs:**
612
+ * ```typescript
613
+ * // ❌ INCORRECTO - Faltan IDs
614
+ * await rowService.updateBulk(tableId, [
615
+ * { status: 'active' } // Error: falta 'id'
616
+ * ])
617
+ *
618
+ * // ✅ CORRECTO - Cada update tiene su ID
619
+ * await rowService.updateBulk(tableId, [
620
+ * { id: 1, status: 'active' },
621
+ * { id: 2, status: 'inactive' }
622
+ * ])
623
+ * ```
624
+ *
625
+ * **Manejo de Errores con Rollback Parcial:**
626
+ * ```typescript
627
+ * const updates = [...] // Array grande de updates
628
+ * const originalValues = await rowService.listAll(tableId) // Backup
629
+ *
630
+ * try {
631
+ * const updated = await rowService.updateBulk(tableId, updates, { batchSize: 100 })
632
+ * console.log(`✅ ${updated.length} filas actualizadas`)
633
+ * } catch (error) {
634
+ * console.error('❌ Error durante update, iniciando rollback...')
635
+ *
636
+ * // Rollback manual de filas afectadas
637
+ * const rollbackData = originalValues.map(row => ({
638
+ * id: row.id,
639
+ * ...pickFields(row, changedFields)
640
+ * }))
641
+ *
642
+ * await rowService.updateBulk(tableId, rollbackData)
643
+ * console.log('Rollback completado')
644
+ * }
645
+ * ```
646
+ *
647
+ * @param tableId - ID numérico de la tabla
648
+ * @param updates - Array de actualizaciones (cada una debe incluir 'id')
649
+ * @param options - Opciones de la operación bulk
650
+ * @param options.batchSize - Tamaño de lote (default: 200)
651
+ * @param options.send_webhook_events - Enviar eventos webhook (default: false)
652
+ * @param options.user_field_names - Usar nombres de usuario (default: true)
653
+ * @returns Promise con array de filas actualizadas
654
+ *
655
+ * @throws {BaserowValidationError} Si faltan IDs o datos son inválidos
656
+ *
657
+ * @example
658
+ * ```typescript
659
+ * // Actualización básica de status
660
+ * const updates = [
661
+ * { id: 1, status: 'active' },
662
+ * { id: 2, status: 'inactive' },
663
+ * { id: 3, email: 'updated@example.com' }
664
+ * ]
665
+ *
666
+ * const updatedRows = await rowService.updateBulk(123, updates)
667
+ * console.log(`${updatedRows.length} filas actualizadas`)
668
+ *
669
+ * // Actualizar desde sistema externo
670
+ * const externalData = await fetchFromAPI()
671
+ * const updates = externalData.map(item => ({
672
+ * id: item.baserowId,
673
+ * name: item.fullName,
674
+ * email: item.emailAddress,
675
+ * updated_at: new Date().toISOString()
676
+ * }))
677
+ *
678
+ * await rowService.updateBulk(tableId, updates, { batchSize: 150 })
679
+ * ```
680
+ *
681
+ * @since 1.0.0
682
+ */
683
+ async updateBulk(
684
+ tableId: number,
685
+ updates: Array<{ id: number } & UpdateRowRequest>,
686
+ options: BulkOptions = {}
687
+ ): Promise<Row[]> {
688
+ this.validationService.validateId(tableId, 'table ID')
689
+
690
+ if (!updates || !Array.isArray(updates) || updates.length === 0) {
691
+ throw new Error('Updates must be a non-empty array')
692
+ }
693
+
694
+ // Validar que todas las actualizaciones tienen ID
695
+ updates.forEach((update, index) => {
696
+ if (!update.id || typeof update.id !== 'number' || update.id <= 0) {
697
+ throw new Error(`Update at index ${index} must have a valid id`)
698
+ }
699
+ })
700
+
701
+ const { batchSize = 200, send_webhook_events = false, user_field_names = true } = options
702
+
703
+ const chunks = chunkArray(updates, batchSize)
704
+ const results: Row[] = []
705
+
706
+ this.logger?.info('Bulk update iniciado', { tableId, updateCount: updates.length, batchCount: chunks.length })
707
+
708
+ for (let i = 0; i < chunks.length; i++) {
709
+ const chunk = chunks[i]
710
+ const batchNum = i + 1
711
+
712
+ try {
713
+ this.logger?.info('Procesando lote', { batchNum, totalBatches: chunks.length, batchSize: chunk.length })
714
+
715
+ const response = await this.http.patch<{ items: Row[] }>(
716
+ `/database/rows/table/${tableId}/batch/`,
717
+ { items: chunk },
718
+ {
719
+ user_field_names,
720
+ send_webhook_events
721
+ }
722
+ )
723
+
724
+ results.push(...(response.items || []))
725
+
726
+ // Rate limiting entre lotes
727
+ if (i < chunks.length - 1) {
728
+ await delay(100)
729
+ }
730
+
731
+ this.logger?.info('Lote procesado correctamente', { batchNum })
732
+ } catch (error) {
733
+ this.logger?.error('❌ Error en lote', { batchNum, totalBatches: chunks.length, error })
734
+ throw error
735
+ }
736
+ }
737
+
738
+ this.logger?.info('Bulk update completado', { updatedRows: results.length })
739
+ return results
740
+ }
741
+
742
+ /**
743
+ * Eliminar múltiples filas
744
+ *
745
+ * Elimina múltiples filas de forma eficiente. Procesa en lotes pequeños
746
+ * para evitar problemas con URLs muy largas. Operación irreversible.
747
+ *
748
+ * @aiUsage
749
+ * **✅ Usar cuando:**
750
+ * - Limpieza de datos obsoletos (registros antiguos, duplicados)
751
+ * - Eliminación administrativa masiva (purgar test data)
752
+ * - Sincronización: eliminar filas que ya no existen en origen
753
+ * - Operaciones de mantenimiento (>10 filas)
754
+ *
755
+ * **❌ NO usar cuando:**
756
+ * - Solo 1-5 filas → Usar `delete()` individual
757
+ * - Datos pueden recuperarse → Considerar soft delete (campo deleted=true)
758
+ * - Operación puede fallar parcialmente → Implementar backup previo
759
+ *
760
+ * @aiWarning
761
+ * **⚠️ OPERACIÓN IRREVERSIBLE:**
762
+ * - **NO hay undo/rollback automático** en Baserow
763
+ * - Una vez eliminadas, las filas NO se pueden recuperar
764
+ * - Si la operación falla a mitad, algunas filas YA estarán eliminadas
765
+ *
766
+ * **⚠️ Batch Size Pequeño (50 default):**
767
+ * - Usa batchSize=50 (más pequeño que create/update) para evitar URLs largas
768
+ * - Baserow API usa DELETE con query params: `/table/123/1,2,3,4,5...`
769
+ * - URLs muy largas (>2000 chars) pueden fallar en algunos proxies
770
+ *
771
+ * @aiErrorHandling
772
+ * **Patrón de Backup Preventivo:**
773
+ * ```typescript
774
+ * // ✅ RECOMENDADO: Backup antes de eliminar
775
+ * const rowsToDelete = [1, 2, 3, 4, 5]
776
+ *
777
+ * // 1. Crear backup de las filas
778
+ * const backup = await Promise.all(
779
+ * rowsToDelete.map(id => rowService.get(tableId, id))
780
+ * )
781
+ * console.log(`Backup creado: ${backup.length} filas`)
782
+ *
783
+ * // 2. Guardar backup en archivo o DB
784
+ * await fs.writeFile('backup.json', JSON.stringify(backup, null, 2))
785
+ *
786
+ * // 3. Eliminar filas
787
+ * try {
788
+ * await rowService.deleteBulk(tableId, rowsToDelete)
789
+ * console.log('✅ Eliminación exitosa')
790
+ * } catch (error) {
791
+ * console.error('❌ Error durante eliminación')
792
+ * console.log('Backup disponible en backup.json para restauración manual')
793
+ * throw error
794
+ * }
795
+ * ```
796
+ *
797
+ * **Confirmación Interactiva para Operaciones Peligrosas:**
798
+ * ```typescript
799
+ * async function safeBulkDelete(tableId: number, rowIds: number[]) {
800
+ * console.log(`⚠️ Vas a eliminar ${rowIds.length} filas de forma PERMANENTE`)
801
+ *
802
+ * const confirmed = await promptUser('¿Estás seguro? (yes/no): ')
803
+ * if (confirmed !== 'yes') {
804
+ * console.log('Operación cancelada')
805
+ * return
806
+ * }
807
+ *
808
+ * const backup = await createBackup(tableId, rowIds)
809
+ * console.log(`Backup creado: ${backup.length} filas`)
810
+ *
811
+ * await rowService.deleteBulk(tableId, rowIds)
812
+ * console.log('✅ Eliminación completada')
813
+ * }
814
+ * ```
815
+ *
816
+ * @aiPerformance
817
+ * **Rendimiento de Deletes:**
818
+ * - Más lento que create/update (eliminación paralela en chunks)
819
+ * - ~500-800 eliminaciones/minuto con batchSize=50
820
+ * - 10,000 filas → ~12-20 minutos
821
+ *
822
+ * @param tableId - ID numérico de la tabla
823
+ * @param rowIds - Array de IDs de filas a eliminar
824
+ * @param options - Opciones de la operación
825
+ * @param options.batchSize - Tamaño de lote (default: 50)
826
+ * @returns Promise que resuelve cuando todas las filas son eliminadas
827
+ *
828
+ * @throws {BaserowValidationError} Si los IDs son inválidos
829
+ * @throws {BaserowNotFoundError} Si alguna fila no existe
830
+ *
831
+ * @example
832
+ * ```typescript
833
+ * // Eliminación básica
834
+ * const idsToDelete = [123, 456, 789]
835
+ * await rowService.deleteBulk(tableId, idsToDelete)
836
+ * console.log(`${idsToDelete.length} filas eliminadas`)
837
+ *
838
+ * // Con backup preventivo
839
+ * const rowsToDelete = await rowService.list(tableId, {
840
+ * filters: { status: 'archived' }
841
+ * })
842
+ *
843
+ * const backup = rowsToDelete.rows
844
+ * await fs.writeFile('archived_backup.json', JSON.stringify(backup))
845
+ *
846
+ * const ids = rowsToDelete.rows.map(r => r.id)
847
+ * await rowService.deleteBulk(tableId, ids, { batchSize: 50 })
848
+ *
849
+ * console.log(`✅ ${ids.length} filas archivadas eliminadas (backup guardado)`)
850
+ * ```
851
+ *
852
+ * @since 1.0.0
853
+ */
854
+ async deleteBulk(tableId: number, rowIds: number[], options?: BulkOptions): Promise<void> {
855
+ this.validationService.validateId(tableId, 'table ID')
856
+
857
+ if (!rowIds || !Array.isArray(rowIds) || rowIds.length === 0) {
858
+ throw new Error('Row IDs must be a non-empty array')
859
+ }
860
+
861
+ rowIds.forEach(id => this.validationService.validateId(id, 'row ID'))
862
+
863
+ // Eliminar en lotes más pequeños para evitar problemas con URLs muy largas
864
+ const deleteBatchSize = options?.batchSize || 50
865
+ const chunks = chunkArray(rowIds, deleteBatchSize)
866
+
867
+ this.logger?.info('Bulk delete iniciado', { tableId, deleteCount: rowIds.length, batchCount: chunks.length })
868
+
869
+ for (let i = 0; i < chunks.length; i++) {
870
+ const chunk = chunks[i]
871
+ const batchNum = i + 1
872
+
873
+ try {
874
+ this.logger?.info('Procesando lote', { batchNum, totalBatches: chunks.length, batchSize: chunk.length })
875
+
876
+ // Eliminar una por una en paralelo (más eficiente que secuencial)
877
+ await Promise.all(chunk.map(rowId => this.delete(tableId, rowId)))
878
+
879
+ // Rate limiting entre lotes
880
+ if (i < chunks.length - 1) {
881
+ await delay(200)
882
+ }
883
+
884
+ this.logger?.info('Lote procesado correctamente', { batchNum })
885
+ } catch (error) {
886
+ this.logger?.error('❌ Error en lote', { batchNum, totalBatches: chunks.length, error })
887
+ throw error
888
+ }
889
+ }
890
+
891
+ this.logger?.info('Bulk delete completado', { deletedRows: rowIds.length })
892
+ }
893
+
894
+ /**
895
+ * Buscar filas por criterios específicos
896
+ *
897
+ * Método de conveniencia para buscar filas usando un término de búsqueda.
898
+ * Busca en todos los campos de texto de la tabla.
899
+ *
900
+ * @param tableId - ID numérico de la tabla
901
+ * @param query - Término de búsqueda
902
+ * @param options - Opciones adicionales de consulta (sin search)
903
+ * @returns Promise con array de filas que coinciden
904
+ *
905
+ * @example
906
+ * ```typescript
907
+ * const results = await rowService.search(123, 'john', {
908
+ * order_by: 'name',
909
+ * size: 20
910
+ * })
911
+ * console.log(`Encontradas ${results.length} filas`)
912
+ * ```
913
+ *
914
+ * @since 1.0.0
915
+ */
916
+ async search(tableId: number, query: string, options: Omit<QueryOptions, 'search'> = {}): Promise<Row[]> {
917
+ return (await this.list(tableId, { ...options, search: query })).rows
918
+ }
919
+
920
+ /**
921
+ * Verificar si una fila existe
922
+ *
923
+ * Verifica la existencia de una fila sin cargar todos sus datos.
924
+ * Útil para validaciones antes de operaciones.
925
+ *
926
+ * @param tableId - ID numérico de la tabla
927
+ * @param rowId - ID numérico de la fila
928
+ * @returns Promise que resuelve a true si existe, false si no
929
+ *
930
+ * @example
931
+ * ```typescript
932
+ * const exists = await rowService.exists(123, 456)
933
+ * if (exists) {
934
+ * console.log('La fila existe')
935
+ * } else {
936
+ * console.log('La fila no existe')
937
+ * }
938
+ * ```
939
+ *
940
+ * @since 1.0.0
941
+ */
942
+ async exists(tableId: number, rowId: number): Promise<boolean> {
943
+ try {
944
+ await this.get(tableId, rowId)
945
+ return true
946
+ } catch (error) {
947
+ if (error instanceof BaserowNotFoundError) {
948
+ return false
949
+ }
950
+ throw error
951
+ }
952
+ }
953
+
954
+ /**
955
+ * Contar filas en una tabla
956
+ *
957
+ * Obtiene el número total de filas en una tabla, opcionalmente
958
+ * aplicando filtros. No carga los datos de las filas.
959
+ *
960
+ * @param tableId - ID numérico de la tabla
961
+ * @param filters - Filtros opcionales para aplicar al conteo
962
+ * @returns Promise con el número total de filas
963
+ *
964
+ * @example
965
+ * ```typescript
966
+ * const total = await rowService.count(123)
967
+ * console.log(`Total de filas: ${total}`)
968
+ *
969
+ * const activeCount = await rowService.count(123, { status: 'active' })
970
+ * console.log(`Filas activas: ${activeCount}`)
971
+ * ```
972
+ *
973
+ * @since 1.0.0
974
+ */
975
+ async count(tableId: number, filters?: Record<string, any>): Promise<number> {
976
+ const result = await this.list(tableId, {
977
+ size: 1,
978
+ filters
979
+ })
980
+ return result.count
981
+ }
982
+ }