@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,420 @@
1
+ /**
2
+ * Servicio para gestión de control de schemas
3
+ *
4
+ * Maneja la tabla especial __schema_control que almacena metadatos
5
+ * sobre los schemas cargados en cada base de datos, permitiendo
6
+ * detección de cambios, versionado y operaciones idempotentes.
7
+ *
8
+ */
9
+
10
+ import { createHash } from 'crypto'
11
+ import type { Logger } from '../types'
12
+ import type { DatabaseSchema, SchemaControl, LoadSchemaOptions, SchemaControlStatus } from '../types/schema'
13
+ import { TableService } from './TableService'
14
+ import { FieldService } from './FieldService'
15
+ import { RowService } from './RowService'
16
+ import { BaserowValidationError } from '../types/errors'
17
+
18
+ /**
19
+ * Nombre de la tabla de control de schemas
20
+ */
21
+ const SCHEMA_CONTROL_TABLE_NAME = '__schema_control'
22
+
23
+ /**
24
+ * Estructura de la tabla de control
25
+ */
26
+ interface SchemaControlRow {
27
+ id?: number
28
+ version: string
29
+ schema_hash: string
30
+ loaded_at: string
31
+ tables_created: string // JSON array
32
+ status: SchemaControlStatus
33
+ load_options: string // JSON object
34
+ }
35
+
36
+ /**
37
+ * Servicio para gestión de control de schemas
38
+ */
39
+ export class SchemaControlService {
40
+ private controlTableId?: number
41
+ private controlTableName = SCHEMA_CONTROL_TABLE_NAME
42
+
43
+ constructor(
44
+ private databaseId: number,
45
+ private tableService: TableService,
46
+ private fieldService: FieldService,
47
+ private rowService: RowService,
48
+ private logger?: Logger
49
+ ) {}
50
+
51
+ /**
52
+ * Inicializar la tabla de control si no existe
53
+ */
54
+ async initialize(): Promise<void> {
55
+ this.logger?.debug?.(`Initializing schema control for database ${this.databaseId}`)
56
+
57
+ try {
58
+ // Intentar encontrar la tabla de control existente
59
+ const existingTable = await this.tableService.findUnique(this.databaseId, this.controlTableName)
60
+
61
+ if (existingTable) {
62
+ this.controlTableId = existingTable.id
63
+ this.logger?.debug?.(`Found existing schema control table: ${this.controlTableId}`)
64
+ return
65
+ }
66
+
67
+ // Crear la tabla de control
68
+ await this.createControlTable()
69
+ } catch (error) {
70
+ this.logger?.error?.('Failed to initialize schema control:', error)
71
+ throw new Error(`Failed to initialize schema control: ${(error as Error).message}`)
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Crear la tabla de control de schemas
77
+ */
78
+ private async createControlTable(): Promise<void> {
79
+ this.logger?.debug?.('Creating schema control table')
80
+
81
+ // Crear la tabla vacía
82
+ const table = await this.tableService.create(this.databaseId, {
83
+ name: this.controlTableName
84
+ })
85
+
86
+ this.controlTableId = table.id
87
+ this.logger?.debug?.(`Created schema control table: ${this.controlTableId}`)
88
+
89
+ // Crear los campos con los tipos correctos desde el inicio
90
+ await this.createControlTableFields()
91
+ }
92
+
93
+ /**
94
+ * Crear los campos de la tabla de control con los tipos correctos
95
+ */
96
+ private async createControlTableFields(): Promise<void> {
97
+ if (!this.controlTableId) {
98
+ throw new Error('Schema control table not initialized')
99
+ }
100
+
101
+ // Crear campos uno por uno con los tipos correctos
102
+ await this.fieldService.createTextField(this.controlTableId, 'version', undefined, 'Versión del schema cargado')
103
+ await this.fieldService.createTextField(
104
+ this.controlTableId,
105
+ 'schema_hash',
106
+ undefined,
107
+ 'Hash del schema para detectar cambios'
108
+ )
109
+ await this.fieldService.createDateField(
110
+ this.controlTableId,
111
+ 'loaded_at',
112
+ true,
113
+ 'ISO',
114
+ '24',
115
+ false,
116
+ undefined,
117
+ undefined,
118
+ 'Fecha y hora de carga del schema'
119
+ )
120
+ await this.fieldService.createLongTextField(
121
+ this.controlTableId,
122
+ 'tables_created',
123
+ 'JSON array con nombres de tablas creadas'
124
+ )
125
+ await this.fieldService.createSelectField(
126
+ this.controlTableId,
127
+ 'status',
128
+ [
129
+ { value: 'active', color: 'green' },
130
+ { value: 'outdated', color: 'yellow' },
131
+ { value: 'error', color: 'red' },
132
+ { value: 'loading', color: 'blue' }
133
+ ],
134
+ 'Estado del schema cargado'
135
+ )
136
+ await this.fieldService.createLongTextField(
137
+ this.controlTableId,
138
+ 'load_options',
139
+ 'JSON con opciones usadas para cargar el schema'
140
+ )
141
+
142
+ this.logger?.debug?.('Schema control table fields created')
143
+ }
144
+
145
+ /**
146
+ * Obtener el control de schema actual
147
+ */
148
+ async getCurrentControl(): Promise<SchemaControl | null> {
149
+ await this.ensureInitialized()
150
+
151
+ try {
152
+ const rows = await this.rowService.list(this.controlTableId!, {
153
+ order_by: '-loaded_at',
154
+ size: 1
155
+ })
156
+
157
+ if (rows.rows.length === 0) {
158
+ return null
159
+ }
160
+
161
+ const row = rows.rows[0] as any as SchemaControlRow
162
+ return this.mapRowToControl(row)
163
+ } catch (error) {
164
+ this.logger?.error?.('Failed to get current schema control:', error)
165
+ return null
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Crear nuevo control de schema
171
+ */
172
+ async createControl(
173
+ schema: DatabaseSchema,
174
+ options: LoadSchemaOptions,
175
+ tablesCreated: string[]
176
+ ): Promise<SchemaControl> {
177
+ await this.ensureInitialized()
178
+
179
+ const schemaHash = this.generateSchemaHash(schema)
180
+ const control: SchemaControl = {
181
+ id: `${Date.now()}`,
182
+ version: options.version || schema.version || '1.0.0',
183
+ schemaHash,
184
+ loadedAt: new Date(),
185
+ tablesCreated,
186
+ status: 'loading',
187
+ loadOptions: options
188
+ }
189
+
190
+ try {
191
+ // Marcar controles anteriores como outdated
192
+ await this.markPreviousControlsAsOutdated()
193
+
194
+ // Crear nuevo control
195
+ const rowData = this.mapControlToRow(control)
196
+ const createdRow = await this.rowService.createRow(this.controlTableId!, rowData)
197
+
198
+ control.id = (createdRow as any).id?.toString() || control.id
199
+ control.status = 'active'
200
+
201
+ // Actualizar el estado a active
202
+ await this.updateControlStatus(control.id, 'active')
203
+
204
+ this.logger?.debug?.(`Created schema control: ${control.id}`)
205
+ return control
206
+ } catch (error) {
207
+ this.logger?.error?.('Failed to create schema control:', error)
208
+ throw new Error(`Failed to create schema control: ${(error as Error).message}`)
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Actualizar el estado de un control
214
+ */
215
+ async updateControlStatus(controlId: string, status: SchemaControlStatus): Promise<void> {
216
+ await this.ensureInitialized()
217
+
218
+ try {
219
+ const numericId = parseInt(controlId, 10)
220
+ if (isNaN(numericId)) {
221
+ throw new Error(`Invalid control ID: ${controlId}`)
222
+ }
223
+
224
+ await this.rowService.updateRow(this.controlTableId!, numericId, {
225
+ status: status
226
+ })
227
+
228
+ this.logger?.debug?.(`Updated schema control ${controlId} status to ${status}`)
229
+ } catch (error) {
230
+ this.logger?.error?.(`Failed to update control status:`, error)
231
+ throw new Error(`Failed to update control status: ${(error as Error).message}`)
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Verificar si un schema ha cambiado
237
+ */
238
+ async hasSchemaChanged(schema: DatabaseSchema): Promise<boolean> {
239
+ const currentControl = await this.getCurrentControl()
240
+ if (!currentControl) {
241
+ return true // No hay control previo, se considera cambio
242
+ }
243
+
244
+ const newHash = this.generateSchemaHash(schema)
245
+ return currentControl.schemaHash !== newHash
246
+ }
247
+
248
+ /**
249
+ * Generar hash único para un schema
250
+ */
251
+ generateSchemaHash(schema: DatabaseSchema): string {
252
+ // Crear una representación normalizada del schema
253
+ const normalized = {
254
+ tables: schema.tables
255
+ .map(table => ({
256
+ name: table.name.toLowerCase(),
257
+ fields: table.fields
258
+ .map(field => ({
259
+ name: field.name.toLowerCase(),
260
+ type: field.type,
261
+ config: field.config || null
262
+ }))
263
+ .sort((a, b) => a.name.localeCompare(b.name))
264
+ }))
265
+ .sort((a, b) => a.name.localeCompare(b.name)),
266
+ relationships: (schema.relationships || [])
267
+ .map(rel => ({
268
+ name: rel.name.toLowerCase(),
269
+ sourceTable: rel.sourceTable.toLowerCase(),
270
+ targetTable: rel.targetTable.toLowerCase()
271
+ }))
272
+ .sort((a, b) => a.name.localeCompare(b.name))
273
+ }
274
+
275
+ const schemaString = JSON.stringify(normalized)
276
+ return createHash('sha256').update(schemaString).digest('hex').substring(0, 16)
277
+ }
278
+
279
+ /**
280
+ * Marcar controles anteriores como desactualizados
281
+ */
282
+ private async markPreviousControlsAsOutdated(): Promise<void> {
283
+ try {
284
+ const rows = await this.rowService.list(this.controlTableId!, {
285
+ filters: { status: 'active' }
286
+ })
287
+
288
+ for (const row of rows.rows) {
289
+ await this.rowService.updateRow(this.controlTableId!, (row as any).id, {
290
+ status: 'outdated'
291
+ })
292
+ }
293
+ } catch (error) {
294
+ this.logger?.warn?.('Failed to mark previous controls as outdated:', error)
295
+ // No es crítico, continuar
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Mapear fila de base de datos a objeto SchemaControl
301
+ */
302
+ private mapRowToControl(row: SchemaControlRow): SchemaControl {
303
+ try {
304
+ // El status puede venir como string o como objeto de single_select
305
+ const status = typeof row.status === 'string' ? row.status : (row.status as any)?.value || 'unknown'
306
+
307
+ return {
308
+ id: row.id?.toString() || 'unknown',
309
+ version: row.version,
310
+ schemaHash: row.schema_hash,
311
+ loadedAt: new Date(row.loaded_at),
312
+ tablesCreated: JSON.parse(row.tables_created || '[]'),
313
+ status,
314
+ loadOptions: JSON.parse(row.load_options || '{}')
315
+ }
316
+ } catch (error) {
317
+ this.logger?.error?.('Failed to parse schema control row:', error)
318
+ throw new BaserowValidationError('Invalid schema control data', { row: ['Failed to parse schema control row'] })
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Mapear objeto SchemaControl a fila de base de datos
324
+ */
325
+ private mapControlToRow(control: SchemaControl): Record<string, any> {
326
+ return {
327
+ version: control.version,
328
+ schema_hash: control.schemaHash,
329
+ loaded_at: control.loadedAt.toISOString(),
330
+ tables_created: JSON.stringify(control.tablesCreated),
331
+ status: control.status,
332
+ load_options: JSON.stringify(control.loadOptions)
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Asegurar que el servicio esté inicializado
338
+ */
339
+ private async ensureInitialized(): Promise<void> {
340
+ if (!this.controlTableId) {
341
+ await this.initialize()
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Limpiar controles antiguos (mantener solo los últimos N)
347
+ */
348
+ async cleanupOldControls(keepLast: number = 10): Promise<void> {
349
+ await this.ensureInitialized()
350
+
351
+ try {
352
+ const rows = await this.rowService.list(this.controlTableId!, {
353
+ order_by: '-loaded_at'
354
+ })
355
+
356
+ if (rows.rows.length <= keepLast) {
357
+ return // No hay suficientes para limpiar
358
+ }
359
+
360
+ const toDelete = rows.rows.slice(keepLast)
361
+ const idsToDelete = toDelete.map((row: any) => row.id).filter(id => id)
362
+
363
+ if (idsToDelete.length > 0) {
364
+ await this.rowService.deleteBulk(this.controlTableId!, idsToDelete)
365
+ this.logger?.debug?.(`Cleaned up ${idsToDelete.length} old schema controls`)
366
+ }
367
+ } catch (error) {
368
+ this.logger?.warn?.('Failed to cleanup old controls:', error)
369
+ // No es crítico, continuar
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Obtener estadísticas del control de schemas
375
+ */
376
+ async getControlStats(): Promise<{
377
+ totalControls: number
378
+ activeControls: number
379
+ outdatedControls: number
380
+ errorControls: number
381
+ }> {
382
+ await this.ensureInitialized()
383
+
384
+ try {
385
+ const rows = await this.rowService.list(this.controlTableId!)
386
+
387
+ const stats = {
388
+ totalControls: rows.rows.length,
389
+ activeControls: 0,
390
+ outdatedControls: 0,
391
+ errorControls: 0
392
+ }
393
+
394
+ for (const row of rows.rows) {
395
+ const status = (row as any).status
396
+ switch (status) {
397
+ case 'active':
398
+ stats.activeControls++
399
+ break
400
+ case 'outdated':
401
+ stats.outdatedControls++
402
+ break
403
+ case 'error':
404
+ stats.errorControls++
405
+ break
406
+ }
407
+ }
408
+
409
+ return stats
410
+ } catch (error) {
411
+ this.logger?.error?.('Failed to get control stats:', error)
412
+ return {
413
+ totalControls: 0,
414
+ activeControls: 0,
415
+ outdatedControls: 0,
416
+ errorControls: 0
417
+ }
418
+ }
419
+ }
420
+ }