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