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