@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,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
|
+
}
|