@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,870 @@
1
+ import { Logger, Database, BaserowNotFoundError } from '../types'
2
+ import { TableContext } from './TableContext'
3
+ import { WorkspaceService } from '../services/WorkspaceService'
4
+ import { DatabaseService } from '../services/DatabaseService'
5
+ import { TableService } from '../services/TableService'
6
+ import { FieldService } from '../services/FieldService'
7
+ import { RowService } from '../services/RowService'
8
+ import { SchemaControlService } from '../services/SchemaControlService'
9
+ import { validateRequired } from '../utils/validation'
10
+ import { validateDatabaseSchema, validateLoadSchemaOptions } from '../validators/schema'
11
+ import type {
12
+ DatabaseSchema,
13
+ LoadSchemaOptions,
14
+ LoadSchemaResult,
15
+ FieldSchema,
16
+ SchemaChange,
17
+ LoadSchemaStats,
18
+ TableSchema,
19
+ RelationshipSchema,
20
+ LoadSchemaMode,
21
+ SchemaControl,
22
+ Table
23
+ } from '../types/schema'
24
+
25
+ /**
26
+ * Context para operaciones en una database específica
27
+ *
28
+ * Proporciona una API jerárquica fluida para operaciones administrativas
29
+ * dentro de una database específica. Permite el acceso encadenado
30
+ * database → table → field/rows y operaciones CRUD completas.
31
+ *
32
+ * **Características principales:**
33
+ * - Resolución automática de database por nombre o ID (lazy loading)
34
+ * - API fluida para operaciones de tables: list, find, create, update, delete
35
+ * - Acceso directo a table contexts específicos
36
+ * - Operaciones CRUD completas de la database
37
+ * - Cache interno para evitar resoluciones repetidas
38
+ * - Logging opcional de todas las operaciones
39
+ * - Validación automática de permisos y scope
40
+ *
41
+ * **Patrón de API Jerárquica:**
42
+ * ```
43
+ * database.tables.findMany() // Operaciones masivas
44
+ * database.table('name') // Context específico
45
+ * database.update({ name: 'nuevo' }) // Actualizar database
46
+ * ```
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * // Operaciones básicas de database
51
+ * const tables = await database.tables.findMany()
52
+ * const newTable = await database.tables.create({ name: 'Nueva Tabla' })
53
+ * const found = await database.tables.find('Existente')
54
+ *
55
+ * // Acceso jerárquico a tabla
56
+ * const tableContext = database.table('Mi Tabla')
57
+ * const fields = await tableContext.field.list()
58
+ * const textField = await tableContext.field.createText('nombre')
59
+ *
60
+ * // Gestión de la database
61
+ * await database.update({ name: 'Nuevo Nombre' })
62
+ * await database.delete()
63
+ * ```
64
+ *
65
+ * @since 1.0.0
66
+ */
67
+ export class DatabaseContext {
68
+ private databaseIdentifier: string | number
69
+ private resolvedDatabase?: Database
70
+ private logger?: Logger
71
+ private schemaControlService?: SchemaControlService
72
+
73
+ /**
74
+ * Crea un nuevo context de database
75
+ *
76
+ * @param workspaceService - Servicio para operaciones de workspace
77
+ * @param workspaceIdentifier - Identificador del workspace padre (opcional)
78
+ * @param databaseIdentifier - Nombre o ID de la database
79
+ * @param databaseService - Servicio para operaciones de database
80
+ * @param tableService - Servicio para operaciones de tabla
81
+ * @param fieldService - Servicio para operaciones de campos
82
+ * @param rowService - Servicio para operaciones de filas
83
+ * @param logger - Logger opcional para debug y trazabilidad
84
+ *
85
+ * @since 1.0.0
86
+ */
87
+ constructor(
88
+ private workspaceService: WorkspaceService,
89
+ private workspaceIdentifier: string | number | undefined,
90
+ databaseIdentifier: string | number,
91
+ private databaseService: DatabaseService,
92
+ private tableService: TableService,
93
+ private fieldService: FieldService,
94
+ private rowService: RowService,
95
+ logger?: Logger
96
+ ) {
97
+ this.databaseIdentifier = databaseIdentifier
98
+ this.logger = logger
99
+ }
100
+
101
+ /**
102
+ * Acceder a una tabla específica en esta database
103
+ *
104
+ * Crea un context para operaciones específicas en una tabla.
105
+ * Permite acceso jerárquico a campos y filas de la tabla.
106
+ * El identificador puede ser nombre (string) o ID (number).
107
+ *
108
+ * @param tableIdentifier - Nombre o ID numérico de la tabla
109
+ * @returns Context de tabla para operaciones específicas
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * // Acceso por nombre
114
+ * const tableContext = database.table('Mi Tabla')
115
+ *
116
+ * // Acceso por ID
117
+ * const tableContext2 = database.table(456)
118
+ *
119
+ * // Operaciones jerárquicas
120
+ * const fields = await tableContext.field.list()
121
+ * const textField = await tableContext.field.createText('nombre')
122
+ * const rows = await tableContext.rows.list()
123
+ * ```
124
+ *
125
+ * @since 1.0.0
126
+ */
127
+ table(tableIdentifier: string | number): TableContext {
128
+ return new TableContext(
129
+ this.workspaceService,
130
+ this.workspaceIdentifier,
131
+ this.databaseIdentifier,
132
+ tableIdentifier,
133
+ this.databaseService,
134
+ this.tableService,
135
+ this.fieldService,
136
+ this.rowService,
137
+ this.logger
138
+ )
139
+ }
140
+
141
+ /**
142
+ * Operaciones masivas de tables en esta database
143
+ *
144
+ * Proporciona métodos para gestionar tables de forma masiva:
145
+ * listar todas, buscar por nombre, crear, actualizar y eliminar.
146
+ * Todas las operaciones están restringidas a la database actual.
147
+ *
148
+ * @returns Objeto con métodos para operaciones de tables
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * // Listar todas las tables de la database
153
+ * const tables = await database.tables.findMany()
154
+ *
155
+ * // Buscar table específica
156
+ * const table = await database.tables.findUnique('Mi Tabla')
157
+ *
158
+ * // Crear nueva table
159
+ * const newTable = await database.tables.create({
160
+ * name: 'Nueva Tabla',
161
+ * first_row_header: true
162
+ * })
163
+ *
164
+ * // Operaciones específicas (usando API jerárquica)
165
+ * await database.table(123).update({ name: 'Nuevo Nombre' })
166
+ * await database.table(123).delete()
167
+ * ```
168
+ *
169
+ * @since 1.0.0
170
+ */
171
+ get tables() {
172
+ return {
173
+ /**
174
+ * Listar todas las tables de la database
175
+ *
176
+ * Obtiene todas las tables que pertenecen a esta database.
177
+ * Incluye metadatos completos de cada tabla.
178
+ *
179
+ * @returns Promise con array de tables de la database
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * const tables = await database.tables.findMany()
184
+ * console.log(`Encontradas ${tables.length} tables`)
185
+ * tables.forEach(table => {
186
+ * console.log(`${table.name} (ID: ${table.id})`)
187
+ * })
188
+ * ```
189
+ *
190
+ * @since 1.0.0
191
+ */
192
+ findMany: async () => {
193
+ const database = await this.get()
194
+ return await this.tableService.findMany(database.id)
195
+ },
196
+
197
+ /**
198
+ * Buscar table por ID o nombre en esta database
199
+ *
200
+ * Busca una table específica por su ID o nombre dentro de la database.
201
+ * Útil para operaciones donde se conoce el identificador pero no se sabe si es ID o nombre.
202
+ *
203
+ * @param identifier - ID numérico o nombre de la table a buscar
204
+ * @returns Promise con la table encontrada o null si no existe
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const table = await database.tables.findUnique('Usuarios')
209
+ * if (table) {
210
+ * console.log(`Table encontrada: ${table.id}`)
211
+ * } else {
212
+ * console.log('Table no encontrada')
213
+ * }
214
+ * ```
215
+ *
216
+ * @since 1.0.0
217
+ */
218
+ findUnique: async (identifier: string | number) => {
219
+ const database = await this.get()
220
+ return await this.tableService.findUnique(database.id, identifier)
221
+ },
222
+
223
+ // NOTA: get() no está en tables - usar database.table(id).get() para operaciones individuales
224
+
225
+ /**
226
+ * Crear nueva table en esta database
227
+ *
228
+ * Crea una nueva table vacía en esta database.
229
+ * La table se crea garantizando 0 campos y 0 filas.
230
+ *
231
+ * @param data - Datos de la nueva table
232
+ * @param data.name - Nombre de la table
233
+ * @param data.data - Datos iniciales (opcional)
234
+ * @param data.first_row_header - Si la primera fila son headers (opcional)
235
+ * @param data.skipDefaultCleanup - Saltarse limpieza automática del contenido por defecto de Baserow (opcional)
236
+ * @returns Promise con la table creada
237
+ *
238
+ * @throws {BaserowValidationError} Si los datos son inválidos
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const newTable = await database.tables.create({
243
+ * name: 'Usuarios',
244
+ * first_row_header: true
245
+ * })
246
+ * console.log(`Table creada: ${newTable.id} - ${newTable.name}`)
247
+ * ```
248
+ *
249
+ * @since 1.0.0
250
+ */
251
+ create: async (data: { name: string; data?: any; first_row_header?: boolean; skipDefaultCleanup?: boolean }) => {
252
+ const database = await this.get()
253
+ return await this.tableService.create(database.id, data)
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Actualizar esta database
260
+ *
261
+ * Actualiza los datos de la database. Solo actualiza los campos
262
+ * proporcionados (actualización parcial). Actualiza el cache interno
263
+ * si el nombre cambia.
264
+ *
265
+ * @param data - Datos a actualizar
266
+ * @param data.name - Nuevo nombre de la database (opcional)
267
+ * @param data.workspace_id - Nuevo workspace ID (opcional)
268
+ * @returns Promise con la database actualizada
269
+ *
270
+ * @throws {BaserowNotFoundError} Si la database no existe
271
+ * @throws {BaserowValidationError} Si los datos son inválidos
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const updated = await database.update({
276
+ * name: 'Nuevo Nombre CRM'
277
+ * })
278
+ * console.log(`Database actualizada: ${updated.name}`)
279
+ * ```
280
+ *
281
+ * @since 1.0.0
282
+ */
283
+ async update(data: { name?: string; workspace_id?: number }): Promise<Database> {
284
+ const database = await this.get()
285
+ const contextAccess = (this.databaseService as any)[Symbol.for('databaseContext')]
286
+ const updatedDatabase = await contextAccess.updateDatabase(database.id, data)
287
+
288
+ // Actualizar cache si el nombre cambió
289
+ if (data.name && data.name !== database.name) {
290
+ this.resolvedDatabase = updatedDatabase
291
+ if (this.logger) {
292
+ this.logger.info(`Database updated: "${data.name}" (ID: ${updatedDatabase.id})`)
293
+ }
294
+ }
295
+
296
+ return updatedDatabase
297
+ }
298
+
299
+ /**
300
+ * Eliminar esta database
301
+ *
302
+ * Elimina permanentemente la database y todas sus tablas, campos y datos.
303
+ * Esta operación no se puede deshacer. Limpia el cache interno.
304
+ *
305
+ * @returns Promise que resuelve cuando la database es eliminada
306
+ *
307
+ * @throws {BaserowNotFoundError} Si la database no existe
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * await database.delete()
312
+ * console.log('Database eliminada exitosamente')
313
+ * ```
314
+ *
315
+ * @since 1.0.0
316
+ */
317
+ async delete(): Promise<void> {
318
+ const database = await this.get()
319
+ const contextAccess = (this.databaseService as any)[Symbol.for('databaseContext')]
320
+ await contextAccess.deleteDatabase(database.id)
321
+
322
+ if (this.logger) {
323
+ this.logger.info(`Database deleted: "${database.name}" (ID: ${database.id})`)
324
+ }
325
+
326
+ // Limpiar cache
327
+ this.resolvedDatabase = undefined
328
+ }
329
+
330
+ /**
331
+ * Obtener esta database completa
332
+ *
333
+ * Recupera la database con todos sus datos y metadatos.
334
+ * Utiliza lazy loading con cache para evitar resoluciones repetidas.
335
+ * Soporta identificación por nombre o ID de la database.
336
+ *
337
+ * @returns Promise con la database completa
338
+ *
339
+ * @throws {BaserowNotFoundError} Si la database no existe
340
+ * @throws {BaserowValidationError} Si el identificador es inválido
341
+ *
342
+ * @example Obtener por nombre
343
+ * ```typescript
344
+ * const database = await admin.workspace('Mi WS').database('Mi DB').get()
345
+ * console.log(`Database: ${database.name} (ID: ${database.id})`)
346
+ * console.log(`Tablas: ${database.tables?.length || 0}`)
347
+ * ```
348
+ *
349
+ * @example Obtener por ID
350
+ * ```typescript
351
+ * const database = await admin.workspace('Mi WS').database(123).get()
352
+ * console.log(`Nombre: ${database.name}`)
353
+ * console.log(`Workspace ID: ${database.workspace_id}`)
354
+ * ```
355
+ *
356
+ */
357
+ async get(): Promise<Database> {
358
+ if (this.resolvedDatabase) {
359
+ return this.resolvedDatabase
360
+ }
361
+
362
+ if (typeof this.databaseIdentifier === 'number') {
363
+ // Es un ID numérico
364
+ const database = await this.databaseService.findUnique(this.databaseIdentifier)
365
+ if (!database) {
366
+ throw new BaserowNotFoundError('Database', this.databaseIdentifier)
367
+ }
368
+ this.resolvedDatabase = database
369
+ } else {
370
+ // Es un nombre string
371
+ validateRequired(this.databaseIdentifier, 'database name')
372
+ const database = await this.databaseService.findUnique(this.databaseIdentifier)
373
+
374
+ if (!database) {
375
+ throw new BaserowNotFoundError('Database', this.databaseIdentifier)
376
+ }
377
+
378
+ this.resolvedDatabase = database
379
+ }
380
+
381
+ if (this.logger) {
382
+ this.logger.info(`Retrieved database: "${this.resolvedDatabase.name}" (ID: ${this.resolvedDatabase.id})`)
383
+ }
384
+
385
+ return this.resolvedDatabase
386
+ }
387
+
388
+ /**
389
+ * Cargar un schema completo de base de datos de forma declarativa
390
+ *
391
+ * Permite definir y aplicar esquemas completos de base de datos usando
392
+ * un formato JSON/TypeScript que incluye tablas, campos y relaciones.
393
+ * Proporciona control de versiones, detección de cambios y operaciones
394
+ * idempotentes para facilitar migraciones y setup automatizado.
395
+ *
396
+ * @param schema - Schema de la base de datos a cargar
397
+ * @param options - Opciones de carga (modo, versión, etc.)
398
+ * @returns Promise con resultado detallado de la operación
399
+ *
400
+ * @example Cargar schema básico
401
+ * ```typescript
402
+ * const ecommerceSchema: DatabaseSchema = {
403
+ * tables: [
404
+ * {
405
+ * name: "clientes",
406
+ * fields: [
407
+ * { name: "nombre", type: "text" },
408
+ * { name: "email", type: "email" },
409
+ * {
410
+ * name: "categoria",
411
+ * type: "single_select",
412
+ * config: {
413
+ * options: [
414
+ * { value: "Premium", color: "blue" },
415
+ * { value: "Estándar", color: "green" }
416
+ * ]
417
+ * }
418
+ * }
419
+ * ]
420
+ * }
421
+ * ]
422
+ * }
423
+ *
424
+ * const result = await admin.database("mi-db").loadSchema(ecommerceSchema)
425
+ * console.log(`Creadas ${result.tables.length} tablas`)
426
+ * ```
427
+ *
428
+ * @example Con control de versiones
429
+ * ```typescript
430
+ * const result = await admin.database("mi-db").loadSchema(schema, {
431
+ * mode: 'update',
432
+ * version: '2.0.0',
433
+ * force: false
434
+ * })
435
+ *
436
+ * if (result.changes.length > 0) {
437
+ * console.log('Cambios aplicados:', result.changes)
438
+ * } else {
439
+ * console.log('No hay cambios - schema actualizado')
440
+ * }
441
+ * ```
442
+ */
443
+ async loadSchema(schema: DatabaseSchema, options: LoadSchemaOptions = {}): Promise<LoadSchemaResult> {
444
+ const startTime = Date.now()
445
+ const stats: LoadSchemaStats = {
446
+ duration: 0,
447
+ tablesProcessed: 0,
448
+ fieldsProcessed: 0,
449
+ relationshipsProcessed: 0,
450
+ errors: 0,
451
+ warnings: 0
452
+ }
453
+
454
+ try {
455
+ // 1. Validar schema y opciones
456
+ const validatedSchema = validateDatabaseSchema(schema)
457
+ const validatedOptions = validateLoadSchemaOptions(options)
458
+
459
+ this.logger?.info?.(`Loading schema for database with ${validatedSchema.tables.length} tables`)
460
+
461
+ // 2. Asegurar que la database existe
462
+ const database = await this.get()
463
+
464
+ // 3. Inicializar control de schema
465
+ const schemaControl = await this.getSchemaControlService(database.id)
466
+ await schemaControl.initialize()
467
+
468
+ // 4. Verificar si hay cambios (a menos que sea force)
469
+ if (!validatedOptions.force) {
470
+ const hasChanges = await schemaControl.hasSchemaChanged(validatedSchema)
471
+ if (!hasChanges) {
472
+ this.logger?.info?.('No schema changes detected, skipping load')
473
+ const currentControl = await schemaControl.getCurrentControl()
474
+ return {
475
+ tables: [],
476
+ tablesByName: {},
477
+ schemaControl: currentControl!,
478
+ changes: [],
479
+ stats: { ...stats, duration: Date.now() - startTime }
480
+ }
481
+ }
482
+ }
483
+
484
+ // 5. Aplicar schema según el modo
485
+ const result = await this.applySchemaChanges(validatedSchema, validatedOptions, database.id, stats)
486
+
487
+ // 6. Actualizar control de schema
488
+ const tablesCreated = result.tables.map(t => t.name)
489
+ const finalControl = await schemaControl.createControl(validatedSchema, validatedOptions, tablesCreated)
490
+
491
+ const duration = Date.now() - startTime
492
+ this.logger?.info?.(`Schema loaded successfully in ${duration}ms`)
493
+
494
+ return {
495
+ ...result,
496
+ schemaControl: finalControl,
497
+ stats: { ...stats, duration }
498
+ }
499
+ } catch (error) {
500
+ stats.errors++
501
+ this.logger?.error?.('Failed to load schema:', error)
502
+ throw new Error(`Failed to load schema: ${(error as Error).message}`)
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Aplicar cambios de schema según el modo especificado
508
+ */
509
+ private async applySchemaChanges(
510
+ schema: DatabaseSchema,
511
+ options: LoadSchemaOptions,
512
+ databaseId: number,
513
+ stats: LoadSchemaStats
514
+ ): Promise<Omit<LoadSchemaResult, 'schemaControl' | 'stats'>> {
515
+ // Aplicar defaults para opciones opcionales
516
+ const mode = options.mode || 'create-only'
517
+ // TODO: Implementar force y cleanup cuando se necesiten
518
+ // const force = options.force || false
519
+ // const cleanup = options.cleanup || false
520
+
521
+ const tables: Table[] = []
522
+ const tablesByName: Record<string, Table> = {}
523
+ const changes: SchemaChange[] = []
524
+ const tableNameToId = new Map<string, number>()
525
+
526
+ // Fase 1: Crear/actualizar tablas y campos básicos
527
+ for (const tableSchema of schema.tables) {
528
+ try {
529
+ const table = await this.processTable(tableSchema, mode, changes)
530
+ tables.push(table)
531
+ tablesByName[table.name] = table
532
+ tableNameToId.set(tableSchema.name.toLowerCase(), table.id)
533
+ stats.tablesProcessed++
534
+ stats.fieldsProcessed += tableSchema.fields.length
535
+
536
+ this.logger?.debug?.(`Processed table: ${table.name} (${tableSchema.fields.length} fields)`)
537
+ } catch (error) {
538
+ stats.errors++
539
+ this.logger?.error?.(`Failed to process table ${tableSchema.name}:`, error)
540
+ throw error
541
+ }
542
+ }
543
+
544
+ // Fase 2: Crear relaciones (después de que todas las tablas existan)
545
+ if (schema.relationships) {
546
+ for (const relationshipSchema of schema.relationships) {
547
+ try {
548
+ await this.processRelationship(relationshipSchema, tableNameToId, changes)
549
+ stats.relationshipsProcessed++
550
+
551
+ this.logger?.debug?.(`Created relationship: ${relationshipSchema.name}`)
552
+ } catch (error) {
553
+ stats.errors++
554
+ this.logger?.error?.(`Failed to create relationship ${relationshipSchema.name}:`, error)
555
+ throw error
556
+ }
557
+ }
558
+ }
559
+
560
+ return { tables, tablesByName, changes }
561
+ }
562
+
563
+ /**
564
+ * Procesar una tabla individual del schema
565
+ */
566
+ private async processTable(tableSchema: TableSchema, mode: LoadSchemaMode, changes: SchemaChange[]): Promise<Table> {
567
+ // Verificar si la tabla ya existe
568
+ const existingTable = await this.tableService.findUnique((await this.get()).id, tableSchema.name)
569
+
570
+ let table: Table
571
+
572
+ if (existingTable) {
573
+ if (mode === 'recreate') {
574
+ // TODO: Implementar delete cuando esté disponible públicamente
575
+ throw new Error('Recreate mode not yet implemented - delete method not available')
576
+ } else {
577
+ // Actualizar tabla existente
578
+ table = existingTable
579
+ await this.updateTableFields(table.id, tableSchema.fields, mode, changes)
580
+ changes.push({
581
+ type: 'table_updated',
582
+ target: tableSchema.name,
583
+ description: `Updated table fields`
584
+ })
585
+ }
586
+ } else {
587
+ // Crear nueva tabla
588
+ table = await this.createTableWithFields(tableSchema)
589
+ changes.push({
590
+ type: 'table_created',
591
+ target: tableSchema.name,
592
+ description: `Created new table with ${tableSchema.fields.length} fields`
593
+ })
594
+ }
595
+
596
+ return table
597
+ }
598
+
599
+ /**
600
+ * Crear tabla con todos sus campos
601
+ */
602
+ private async createTableWithFields(tableSchema: TableSchema): Promise<Table> {
603
+ // Crear tabla vacía
604
+ const table = await this.tables.create({
605
+ name: tableSchema.name,
606
+ data: [['temp']],
607
+ first_row_header: false
608
+ })
609
+
610
+ // TODO: Eliminar campo temporal cuando delete esté disponible
611
+ // const tempFields = await this.fieldService.findMany(table.id)
612
+ // if (tempFields.length > 0) {
613
+ // await this.fieldService.delete(tempFields[0].id)
614
+ // }
615
+
616
+ // Crear todos los campos
617
+ for (const fieldSchema of tableSchema.fields) {
618
+ await this.createFieldFromSchema(table.id, fieldSchema)
619
+ }
620
+
621
+ return table
622
+ }
623
+
624
+ /**
625
+ * Actualizar campos de una tabla existente
626
+ */
627
+ private async updateTableFields(
628
+ tableId: number,
629
+ fieldSchemas: FieldSchema[],
630
+ mode: LoadSchemaMode,
631
+ changes: SchemaChange[]
632
+ ): Promise<void> {
633
+ const existingFields = await this.fieldService.findMany(tableId)
634
+ const existingFieldsByName = new Map(existingFields.map(f => [f.name.toLowerCase(), f]))
635
+
636
+ for (const fieldSchema of fieldSchemas) {
637
+ const existingField = existingFieldsByName.get(fieldSchema.name.toLowerCase())
638
+
639
+ if (existingField) {
640
+ if (mode === 'update') {
641
+ // Actualizar campo existente si es necesario
642
+ // Por simplicidad, no implementamos actualización de campos en esta versión
643
+ changes.push({
644
+ type: 'field_updated',
645
+ target: `${tableId}.${fieldSchema.name}`,
646
+ description: `Field exists, no update implemented yet`
647
+ })
648
+ }
649
+ } else {
650
+ // Crear campo nuevo
651
+ await this.createFieldFromSchema(tableId, fieldSchema)
652
+ changes.push({
653
+ type: 'field_created',
654
+ target: `${tableId}.${fieldSchema.name}`,
655
+ description: `Created new field: ${fieldSchema.type}`
656
+ })
657
+ }
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Crear campo usando el factory method
663
+ */
664
+ private async createFieldFromSchema(tableId: number, fieldSchema: FieldSchema): Promise<void> {
665
+ const tableContext = this.table(tableId)
666
+
667
+ switch (fieldSchema.type) {
668
+ case 'text':
669
+ await tableContext.fields.createText(
670
+ fieldSchema.name,
671
+ '', // default value (removed dynamic access)
672
+ fieldSchema.description
673
+ )
674
+ break
675
+
676
+ case 'long_text':
677
+ await tableContext.fields.createLongText(fieldSchema.name, fieldSchema.description)
678
+ break
679
+
680
+ case 'email':
681
+ await tableContext.fields.createEmail(fieldSchema.name, fieldSchema.description)
682
+ break
683
+
684
+ case 'url':
685
+ await tableContext.fields.createUrl(fieldSchema.name, fieldSchema.description)
686
+ break
687
+
688
+ case 'phone_number':
689
+ await tableContext.fields.createPhoneNumber(fieldSchema.name, fieldSchema.description)
690
+ break
691
+
692
+ case 'number':
693
+ const numberConfig = fieldSchema.config as any
694
+ await tableContext.fields.createNumber(
695
+ fieldSchema.name,
696
+ numberConfig?.decimals || 0,
697
+ false, // negative allowed
698
+ 0, // default value (removed dynamic access)
699
+ numberConfig?.prefix || '',
700
+ numberConfig?.suffix || '',
701
+ numberConfig?.separators || 'COMMA_PERIOD',
702
+ fieldSchema.description
703
+ )
704
+ break
705
+
706
+ case 'rating':
707
+ await tableContext.fields.createRating(
708
+ fieldSchema.name,
709
+ 5, // max value
710
+ 'yellow', // color
711
+ 'star', // style
712
+ fieldSchema.description
713
+ )
714
+ break
715
+
716
+ case 'boolean':
717
+ const boolConfig = fieldSchema.config as any
718
+ await tableContext.fields.createBoolean(fieldSchema.name, boolConfig?.default || false, fieldSchema.description)
719
+ break
720
+
721
+ case 'date':
722
+ const dateConfig = fieldSchema.config as any
723
+ await tableContext.fields.createDate(
724
+ fieldSchema.name,
725
+ dateConfig?.includeTime || false,
726
+ dateConfig?.format || 'ISO',
727
+ dateConfig?.timeFormat || '24',
728
+ false, // force timezone
729
+ dateConfig?.timezone,
730
+ dateConfig?.default,
731
+ fieldSchema.description
732
+ )
733
+ break
734
+
735
+ case 'single_select':
736
+ const selectConfig = fieldSchema.config as any
737
+ if (!selectConfig?.options) {
738
+ throw new Error(`single_select field ${fieldSchema.name} requires options in config`)
739
+ }
740
+ await tableContext.fields.createSelect(fieldSchema.name, selectConfig.options, fieldSchema.description)
741
+ break
742
+
743
+ case 'multiple_select':
744
+ const multiSelectConfig = fieldSchema.config as any
745
+ if (!multiSelectConfig?.options) {
746
+ throw new Error(`multiple_select field ${fieldSchema.name} requires options in config`)
747
+ }
748
+ await tableContext.fields.createMultiSelect(
749
+ fieldSchema.name,
750
+ multiSelectConfig.options,
751
+ fieldSchema.description
752
+ )
753
+ break
754
+
755
+ case 'file':
756
+ await tableContext.fields.createFile(fieldSchema.name, fieldSchema.description)
757
+ break
758
+
759
+ case 'autonumber':
760
+ await tableContext.fields.createAutonumber(fieldSchema.name, fieldSchema.description)
761
+ break
762
+
763
+ // Los campos de relación se manejan en la fase 2
764
+ case 'link_row':
765
+ // Skip - se procesa en processRelationship
766
+ break
767
+
768
+ default:
769
+ throw new Error(`Unsupported field type: ${fieldSchema.type}`)
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Procesar una relación entre tablas
775
+ */
776
+ private async processRelationship(
777
+ relationshipSchema: RelationshipSchema,
778
+ tableNameToId: Map<string, number>,
779
+ changes: SchemaChange[]
780
+ ): Promise<void> {
781
+ const sourceTableId = tableNameToId.get(relationshipSchema.sourceTable.toLowerCase())
782
+ const targetTableId = tableNameToId.get(relationshipSchema.targetTable.toLowerCase())
783
+
784
+ if (!sourceTableId || !targetTableId) {
785
+ throw new Error(
786
+ `Cannot create relationship ${relationshipSchema.name}: ` +
787
+ `source table ${relationshipSchema.sourceTable} or target table ${relationshipSchema.targetTable} not found`
788
+ )
789
+ }
790
+
791
+ const sourceTableContext = this.table(sourceTableId)
792
+ await sourceTableContext.fields.createLink(relationshipSchema.name, targetTableId, relationshipSchema.description)
793
+
794
+ changes.push({
795
+ type: 'relationship_created',
796
+ target: relationshipSchema.name,
797
+ description: `Created link from ${relationshipSchema.sourceTable} to ${relationshipSchema.targetTable}`
798
+ })
799
+ }
800
+
801
+ /**
802
+ * Obtener o crear el servicio de control de schema
803
+ */
804
+ private async getSchemaControlService(databaseId: number): Promise<SchemaControlService> {
805
+ if (!this.schemaControlService) {
806
+ this.schemaControlService = new SchemaControlService(
807
+ databaseId,
808
+ this.tableService,
809
+ this.fieldService,
810
+ this.rowService,
811
+ this.logger
812
+ )
813
+ }
814
+ return this.schemaControlService
815
+ }
816
+
817
+ /**
818
+ * Obtener el control de schema actual de la database
819
+ */
820
+ async getSchemaControl(): Promise<SchemaControl | null> {
821
+ const database = await this.get()
822
+ const schemaControl = await this.getSchemaControlService(database.id)
823
+ await schemaControl.initialize()
824
+ return await schemaControl.getCurrentControl()
825
+ }
826
+
827
+ /**
828
+ * Verificar si esta database existe
829
+ *
830
+ * Método de utilidad para verificar la existencia de la database
831
+ * sin cargar todos sus datos. Útil para validaciones previas.
832
+ *
833
+ * @returns Promise con true si existe, false si no
834
+ *
835
+ * @example Verificar existencia por nombre
836
+ * ```typescript
837
+ * const exists = await admin.workspace('Mi WS').database('Mi DB').exists()
838
+ * if (exists) {
839
+ * console.log('La database existe')
840
+ * } else {
841
+ * console.log('La database no fue encontrada')
842
+ * }
843
+ * ```
844
+ *
845
+ * @example Verificar antes de crear
846
+ * ```typescript
847
+ * const databaseName = 'Nueva Database'
848
+ * const exists = await admin.workspace('Mi WS').database(databaseName).exists()
849
+ *
850
+ * if (!exists) {
851
+ * await admin.workspace('Mi WS').databases.create(databaseName)
852
+ * console.log('Database creada exitosamente')
853
+ * } else {
854
+ * console.log('La database ya existe')
855
+ * }
856
+ * ```
857
+ *
858
+ */
859
+ async exists(): Promise<boolean> {
860
+ try {
861
+ await this.get()
862
+ return true
863
+ } catch (error) {
864
+ if ((error as any).name === 'BaserowNotFoundError') {
865
+ return false
866
+ }
867
+ throw error
868
+ }
869
+ }
870
+ }