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