@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,961 @@
1
+ import {
2
+ QueryOptions,
3
+ PrismaLikeQueryOptions,
4
+ WhereClause,
5
+ OrderByClause,
6
+ SelectClause,
7
+ FieldOperators
8
+ } from '../types/requests'
9
+ import { FieldMetadata } from './field-cache'
10
+
11
+ /**
12
+ * Mapper para transformar entre sintaxis Prisma y Baserow API
13
+ *
14
+ * Esta clase proporciona métodos estáticos para convertir opciones de consulta
15
+ * estilo Prisma a formato compatible con la API de Baserow, permitiendo
16
+ * una experiencia de desarrollador familiar mientras manteniendo compatibilidad
17
+ * con las capacidades específicas de Baserow.
18
+ *
19
+ * **Transformaciones principales:**
20
+ * - `where` → `filters` con operadores Baserow
21
+ * - `orderBy` → `order_by` string
22
+ * - `select` → `include`/`exclude` arrays
23
+ * - `take`/`skip` → `size`/`page`
24
+ * - Validación y optimización de consultas
25
+ *
26
+ * @aiPattern
27
+ * ## 🎯 5 Patrones de Uso Comunes
28
+ *
29
+ * **1. Búsqueda Simple con Paginación**
30
+ * ```typescript
31
+ * // Prisma-like query para buscar usuarios activos
32
+ * const query = {
33
+ * where: { status: 'active' },
34
+ * orderBy: { created_at: 'desc' },
35
+ * take: 20,
36
+ * skip: 0
37
+ * }
38
+ *
39
+ * const baserowQuery = PrismaBaserowMapper.transformPrismaToBaserow(query)
40
+ * const rows = await client.rows.list(tableId, baserowQuery)
41
+ * ```
42
+ *
43
+ * **2. Filtros Complejos con AND**
44
+ * ```typescript
45
+ * // Combinar múltiples condiciones (todas deben cumplirse)
46
+ * const query = {
47
+ * where: {
48
+ * AND: [
49
+ * { age: { gte: 18, lt: 65 } },
50
+ * { email: { contains: '@company.com' } },
51
+ * { status: 'active' }
52
+ * ]
53
+ * }
54
+ * }
55
+ *
56
+ * const baserowQuery = PrismaBaserowMapper.transformPrismaToBaserow(query)
57
+ * // Transforma a: filter__age__higher_than_or_equal=18&filter__age__lower_than=65&...
58
+ * ```
59
+ *
60
+ * **3. Filtros OR (Solo Nivel Raíz)**
61
+ * ```typescript
62
+ * // ✅ OR simple a nivel raíz (soportado nativamente)
63
+ * const query = {
64
+ * where: {
65
+ * OR: [
66
+ * { status: 'active' },
67
+ * { status: 'pending' }
68
+ * ]
69
+ * }
70
+ * }
71
+ *
72
+ * const baserowQuery = PrismaBaserowMapper.transformPrismaToBaserow(query)
73
+ * // Genera: filter__status__equal=active&filter__status__equal=pending&filter_type=OR
74
+ * ```
75
+ *
76
+ * **4. Selección de Campos (Optimizar Transferencia)**
77
+ * ```typescript
78
+ * // Solo traer campos necesarios
79
+ * const query = {
80
+ * where: { department: 'Sales' },
81
+ * select: {
82
+ * id: true,
83
+ * name: true,
84
+ * email: true,
85
+ * password: false // Excluir explícitamente
86
+ * },
87
+ * take: 50
88
+ * }
89
+ *
90
+ * const baserowQuery = PrismaBaserowMapper.transformPrismaToBaserow(query)
91
+ * // Genera: include=['id','name','email']&size=50
92
+ * ```
93
+ *
94
+ * **5. Migración desde Prisma ORM**
95
+ * ```typescript
96
+ * // Migrar query existente de Prisma
97
+ * const prismaQuery = await prisma.user.findMany({
98
+ * where: { active: true },
99
+ * orderBy: { createdAt: 'desc' },
100
+ * take: 10,
101
+ * skip: 20
102
+ * })
103
+ *
104
+ * // Equivalente en Baserow
105
+ * const baserowQuery = PrismaBaserowMapper.transformPrismaToBaserow({
106
+ * where: { active: true },
107
+ * orderBy: { createdAt: 'desc' },
108
+ * take: 10,
109
+ * skip: 20
110
+ * })
111
+ * const rows = await client.rows.list(tableId, baserowQuery)
112
+ * // Datos en formato similar a Prisma
113
+ * ```
114
+ *
115
+ * @aiLimitation
116
+ * ## ⚠️ Limitaciones de Baserow API
117
+ *
118
+ * **1. OR Anidado NO Soportado**
119
+ * ```typescript
120
+ * // ❌ INCORRECTO - OR anidado dentro de AND
121
+ * const complexOr = {
122
+ * where: {
123
+ * AND: [
124
+ * { status: 'active' },
125
+ * { OR: [{ age: { gt: 18 } }, { verified: true }] } // ❌ NO funciona
126
+ * ]
127
+ * }
128
+ * }
129
+ * // Solución: Reestructurar query o filtrar client-side
130
+ * ```
131
+ *
132
+ * **2. IN/NOT IN con Múltiples Valores**
133
+ * ```typescript
134
+ * // ⚠️ LIMITADO - Solo usa el primer valor
135
+ * const query = {
136
+ * where: {
137
+ * status: { in: ['active', 'pending', 'processing'] } // Solo usa 'active'
138
+ * }
139
+ * }
140
+ * // Baserow API no soporta IN nativo, requiere client-side filtering
141
+ * ```
142
+ *
143
+ * **3. DISTINCT Client-Side Only**
144
+ * ```typescript
145
+ * // ⚠️ WARNING - distinct requiere post-procesamiento
146
+ * const query = {
147
+ * distinct: ['department'],
148
+ * where: { active: true }
149
+ * }
150
+ * // Genera warning: "Distinct is not directly supported by Baserow API"
151
+ * ```
152
+ *
153
+ * @aiMigrationFrom
154
+ * ## 🔄 Migración desde Prisma ORM
155
+ *
156
+ * **Mapeo de Operadores Prisma → Baserow:**
157
+ * - `equals` → `equal`
158
+ * - `not` → `not_equal`
159
+ * - `contains` → `contains`
160
+ * - `startsWith` → `starts_with`
161
+ * - `endsWith` → `ends_with`
162
+ * - `gt` → `higher_than`
163
+ * - `gte` → `higher_than_or_equal`
164
+ * - `lt` → `lower_than`
165
+ * - `lte` → `lower_than_or_equal`
166
+ * - `in` (múltiple) → ⚠️ Solo primer valor (limitación API)
167
+ * - `isEmpty` / `empty` → `empty`
168
+ * - `isNotEmpty` / `notEmpty` → `not_empty`
169
+ *
170
+ * **Conversiones Automáticas:**
171
+ * - Dates: `new Date()` → ISO string automáticamente
172
+ * - Select fields: `value` → `optionId` (con FieldMetadata)
173
+ * - Pagination: `take/skip` → `size/page`
174
+ * - OrderBy: `{ field: 'desc' }` → `-field`
175
+ *
176
+ * @aiPerformance
177
+ * ## ⚡ Optimizaciones de Query
178
+ *
179
+ * **1. Limitar Page Size Automáticamente**
180
+ * ```typescript
181
+ * // Si size > 1000, se limita a 1000 con warning
182
+ * const query = { take: 5000 } // ⚠️ Muy grande
183
+ * const baserow = PrismaBaserowMapper.transformPrismaToBaserow(query)
184
+ * // Result: size=1000 (limitado automáticamente)
185
+ * ```
186
+ *
187
+ * **2. Usar Select para Reducir Payload**
188
+ * ```typescript
189
+ * // ✅ BUENO - Solo campos necesarios
190
+ * const optimized = {
191
+ * select: { id: true, name: true }, // Solo 2 campos
192
+ * take: 1000
193
+ * }
194
+ *
195
+ * // ❌ MALO - Todos los campos
196
+ * const heavy = {
197
+ * take: 1000 // Trae TODOS los campos (payload grande)
198
+ * }
199
+ * ```
200
+ *
201
+ * **3. Analizar Complejidad de Query**
202
+ * ```typescript
203
+ * const analysis = PrismaBaserowMapper.analyzeQueryComplexity(query)
204
+ * console.log(`Score: ${analysis.score}`) // Score alto = query compleja
205
+ * console.log('Warnings:', analysis.warnings) // Problemas potenciales
206
+ * console.log('Suggestions:', analysis.suggestions) // Optimizaciones sugeridas
207
+ * ```
208
+ *
209
+ * **4. Evitar OR Operators (Mejor Performance)**
210
+ * ```typescript
211
+ * // ⚡ RÁPIDO - Filtros AND (procesamiento server-side)
212
+ * const fast = {
213
+ * where: {
214
+ * AND: [{ status: 'active' }, { verified: true }]
215
+ * }
216
+ * }
217
+ *
218
+ * // 🐌 LENTO - OR operators (puede requerir client-side)
219
+ * const slow = {
220
+ * where: {
221
+ * OR: [{ status: 'active' }, { status: 'pending' }]
222
+ * }
223
+ * }
224
+ * ```
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * const prismaOptions = {
229
+ * where: { status: 'active', age: { gte: 18 } },
230
+ * orderBy: { created_at: 'desc' },
231
+ * take: 20,
232
+ * skip: 0
233
+ * }
234
+ *
235
+ * const baserowOptions = PrismaBaserowMapper.transformPrismaToBaserow(prismaOptions)
236
+ * // Result: { filters: { status: 'active', age__gte: 18 }, order_by: '-created_at', size: 20, page: 1 }
237
+ * ```
238
+ *
239
+ * @since 1.1.0
240
+ */
241
+ export class PrismaBaserowMapper {
242
+ /**
243
+ * Transformar opciones Prisma completas a formato Baserow API
244
+ *
245
+ * Convierte una consulta estilo Prisma en opciones compatibles con
246
+ * la API de Baserow, aplicando todas las transformaciones necesarias
247
+ * y optimizaciones automáticas.
248
+ *
249
+ * @param options - Opciones de consulta estilo Prisma
250
+ * @returns Opciones convertidas para Baserow API
251
+ *
252
+ * @example
253
+ * ```typescript
254
+ * const result = PrismaBaserowMapper.transformPrismaToBaserow({
255
+ * where: {
256
+ * AND: [
257
+ * { status: 'active' },
258
+ * { age: { gte: 18, lt: 65 } }
259
+ * ]
260
+ * },
261
+ * orderBy: [{ created_at: 'desc' }, { name: 'asc' }],
262
+ * take: 10,
263
+ * skip: 20,
264
+ * select: { id: true, name: true, password: false }
265
+ * })
266
+ * ```
267
+ *
268
+ * @since 1.2.0
269
+ */
270
+ static transformPrismaToBaserow(options?: PrismaLikeQueryOptions, fieldMetadata?: FieldMetadata): QueryOptions {
271
+ if (!options) return {}
272
+
273
+ const baserowOptions: QueryOptions = {}
274
+
275
+ // Transformar paginación: take/skip → size/page
276
+ const pagination = this.transformPagination(options.take, options.skip)
277
+ Object.assign(baserowOptions, pagination)
278
+
279
+ // Transformar filtros: where → aplanar filtros directamente
280
+ if (options.where) {
281
+ const filters = this.transformWhereToFilters(options.where, fieldMetadata)
282
+ Object.assign(baserowOptions, filters)
283
+
284
+ // Detectar si hay operador OR a nivel raíz y agregar filter_type
285
+ if (this.hasRootOrOperator(options.where)) {
286
+ baserowOptions.filter_type = 'OR'
287
+ }
288
+ }
289
+
290
+ // Transformar ordenamiento: orderBy → order_by
291
+ if (options.orderBy) {
292
+ baserowOptions.order_by = this.transformOrderByToBaserow(options.orderBy)
293
+ }
294
+
295
+ // Transformar selección: select → include/exclude
296
+ if (options.select) {
297
+ const fieldSelection = this.transformSelectToFields(options.select)
298
+ Object.assign(baserowOptions, fieldSelection)
299
+ }
300
+
301
+ // Transformar distinct
302
+ if (options.distinct && options.distinct.length > 0) {
303
+ // Baserow no soporta distinct directamente, pero podemos documentarlo
304
+ // eslint-disable-next-line no-console
305
+ console.warn('Distinct is not directly supported by Baserow API, filtering will be done client-side')
306
+ baserowOptions.distinct = options.distinct as string[]
307
+ }
308
+
309
+ // Opciones específicas de Baserow
310
+ baserowOptions.user_field_names = options.userFieldNames ?? true
311
+
312
+ return this.validateAndOptimize(baserowOptions)
313
+ }
314
+
315
+ /**
316
+ * Convertir valor a string compatible con Baserow API
317
+ *
318
+ * @param value - Valor a convertir
319
+ * @returns String compatible con Baserow
320
+ */
321
+ private static valueToString(value: any): string {
322
+ if (value instanceof Date) {
323
+ return value.toISOString()
324
+ }
325
+ if (value === null) {
326
+ return 'null'
327
+ }
328
+ if (value === undefined) {
329
+ return 'undefined'
330
+ }
331
+ return String(value)
332
+ }
333
+
334
+ /**
335
+ * Transformar cláusula WHERE a filtros Baserow
336
+ *
337
+ * Convierte la sintaxis de filtrado estilo Prisma a los filtros query parameters
338
+ * que espera la API de Baserow usando el formato:
339
+ * filter__field__type=value
340
+ *
341
+ * @param where - Cláusula WHERE estilo Prisma
342
+ * @returns Record de filtros compatible con Baserow API
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * const filters = PrismaBaserowMapper.transformWhereToFilters({
347
+ * name: 'John',
348
+ * age: { gte: 18, lt: 65 },
349
+ * email: { contains: '@company.com' }
350
+ * })
351
+ * // Result: {
352
+ * // "filter__name__equal": "John",
353
+ * // "filter__age__higher_than_or_equal": "18",
354
+ * // "filter__age__lower_than": "65",
355
+ * // "filter__email__contains": "@company.com"
356
+ * // }
357
+ * ```
358
+ *
359
+ * @since 1.2.0
360
+ */
361
+ static transformWhereToFilters(where?: WhereClause, fieldMetadata?: FieldMetadata): Record<string, any> {
362
+ if (!where) return {}
363
+
364
+ const filters: Record<string, any> = {}
365
+
366
+ for (const [key, value] of Object.entries(where)) {
367
+ // Operadores lógicos especiales
368
+ if (key === 'AND') {
369
+ // Combinar múltiples condiciones AND
370
+ const andFilters = (value as WhereClause[]).map(clause => this.transformWhereToFilters(clause, fieldMetadata))
371
+ // Fusionar todos los filtros AND (aplican todos)
372
+ andFilters.forEach(filterObj => Object.assign(filters, filterObj))
373
+ continue
374
+ }
375
+
376
+ if (key === 'OR') {
377
+ // Baserow soporta OR mediante filter_type=OR (solo a nivel raíz flat)
378
+ // Aplanar filtros OR - el filter_type se agregará en transformPrismaToBaserow
379
+ const orFilters = (value as WhereClause[]).map(clause => this.transformWhereToFilters(clause, fieldMetadata))
380
+
381
+ // Verificar si hay múltiples campos en el OR (simple) o AND anidados (complejo)
382
+ const hasNestedOperators = orFilters.some(f =>
383
+ Object.keys(f).some(k => k.includes('__') && Object.keys(f).length > 1)
384
+ )
385
+
386
+ if (hasNestedOperators) {
387
+ // eslint-disable-next-line no-console
388
+ console.warn(
389
+ 'Complex OR with multiple conditions per branch may not work as expected. Baserow OR only supports flat filters.'
390
+ )
391
+ }
392
+
393
+ orFilters.forEach(filterObj => Object.assign(filters, filterObj))
394
+ continue
395
+ }
396
+
397
+ if (key === 'NOT') {
398
+ // Implementar NOT usando operadores de negación
399
+ const notFilters = this.transformWhereToFilters(value as WhereClause, fieldMetadata)
400
+ for (const [filterKey, filterValue] of Object.entries(notFilters)) {
401
+ // Convertir tipo a versión negativa
402
+ const negatedKey = this.negateFilterKey(filterKey)
403
+ filters[negatedKey] = filterValue
404
+ }
405
+ continue
406
+ }
407
+
408
+ // Operadores de campo (excluir Date que debe tratarse como valor simple)
409
+ if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
410
+ const operators = value as FieldOperators
411
+ const fieldType = fieldMetadata?.types[key] || 'text'
412
+
413
+ if ('equals' in operators) {
414
+ this.addFieldFilter(filters, key, fieldType, 'equal', operators.equals, fieldMetadata?.selectOptions)
415
+ }
416
+ if ('not' in operators) {
417
+ this.addFieldFilter(filters, key, fieldType, 'not_equal', operators.not, fieldMetadata?.selectOptions)
418
+ }
419
+ if ('contains' in operators) {
420
+ this.addFieldFilter(filters, key, fieldType, 'contains', operators.contains, fieldMetadata?.selectOptions)
421
+ }
422
+ if ('contains_not' in operators) {
423
+ filters[`filter__${key}__contains_not`] = this.valueToString(operators.contains_not)
424
+ }
425
+ if ('startsWith' in operators) {
426
+ filters[`filter__${key}__starts_with`] = this.valueToString(operators.startsWith)
427
+ }
428
+ if ('endsWith' in operators) {
429
+ filters[`filter__${key}__ends_with`] = this.valueToString(operators.endsWith)
430
+ }
431
+ if ('gt' in operators) {
432
+ filters[`filter__${key}__higher_than`] = this.valueToString(operators.gt)
433
+ }
434
+ if ('gte' in operators) {
435
+ filters[`filter__${key}__higher_than_or_equal`] = this.valueToString(operators.gte)
436
+ }
437
+ if ('lt' in operators) {
438
+ filters[`filter__${key}__lower_than`] = this.valueToString(operators.lt)
439
+ }
440
+ if ('lte' in operators) {
441
+ filters[`filter__${key}__lower_than_or_equal`] = this.valueToString(operators.lte)
442
+ }
443
+ if ('in' in operators) {
444
+ const values = Array.isArray(operators.in) ? operators.in : [operators.in]
445
+ // Filtrar valores vacíos o undefined
446
+ const validValues = values.filter(v => v !== undefined && v !== null && v !== '')
447
+ if (validValues.length === 1) {
448
+ filters[`filter__${key}__equal`] = this.valueToString(validValues[0])
449
+ } else if (validValues.length > 1) {
450
+ // eslint-disable-next-line no-console
451
+ console.warn('IN with multiple values requires client-side filtering in Baserow')
452
+ filters[`filter__${key}__equal`] = this.valueToString(validValues[0]) // Usar solo el primero
453
+ }
454
+ // Si no hay valores válidos, no crear filtro
455
+ }
456
+ if ('notIn' in operators) {
457
+ const values = Array.isArray(operators.notIn) ? operators.notIn : [operators.notIn]
458
+ // Filtrar valores vacíos o undefined
459
+ const validValues = values.filter(v => v !== undefined && v !== null && v !== '')
460
+ if (validValues.length === 1) {
461
+ filters[`filter__${key}__not_equal`] = this.valueToString(validValues[0])
462
+ } else if (validValues.length > 1) {
463
+ // eslint-disable-next-line no-console
464
+ console.warn('NOT IN with multiple values requires client-side filtering in Baserow')
465
+ filters[`filter__${key}__not_equal`] = this.valueToString(validValues[0]) // Usar solo el primero
466
+ }
467
+ // Si no hay valores válidos, no crear filtro
468
+ }
469
+ // Soporte para isEmpty y su alias empty (sintaxis Baserow)
470
+ if (('isEmpty' in operators && operators.isEmpty) || ('empty' in operators && operators.empty)) {
471
+ filters[`filter__${key}__empty`] = ''
472
+ }
473
+ // Soporte para isNotEmpty y su alias notEmpty (sintaxis Baserow)
474
+ if (('isNotEmpty' in operators && operators.isNotEmpty) || ('notEmpty' in operators && operators.notEmpty)) {
475
+ filters[`filter__${key}__not_empty`] = ''
476
+ }
477
+ } else {
478
+ // Valor simple (equals implícito)
479
+ const fieldType = fieldMetadata?.types[key] || 'text'
480
+ this.addFieldFilter(filters, key, fieldType, 'equal', value, fieldMetadata?.selectOptions)
481
+ }
482
+ }
483
+
484
+ return filters
485
+ }
486
+
487
+ /**
488
+ * Negar una clave de filtro de Baserow
489
+ *
490
+ * @param filterKey - Clave de filtro a negar (formato filter__field__type)
491
+ * @returns Clave de filtro negada
492
+ */
493
+ private static negateFilterKey(filterKey: string): string {
494
+ const negationMap: Record<string, string> = {
495
+ equal: 'not_equal',
496
+ not_equal: 'equal',
497
+ contains: 'contains_not',
498
+ contains_not: 'contains',
499
+ higher_than: 'lower_than_or_equal',
500
+ lower_than: 'higher_than_or_equal',
501
+ higher_than_or_equal: 'lower_than',
502
+ lower_than_or_equal: 'higher_than',
503
+ empty: 'not_empty',
504
+ not_empty: 'empty'
505
+ }
506
+
507
+ // Parsear la clave: filter__field__type
508
+ const parts = filterKey.split('__')
509
+ if (parts.length === 3 && parts[0] === 'filter') {
510
+ const [, field, filterType] = parts
511
+ const negatedType = negationMap[filterType] || `not_${filterType}`
512
+ return `filter__${field}__${negatedType}`
513
+ }
514
+
515
+ return filterKey // Si no puede parsear, devolver original
516
+ }
517
+
518
+ /**
519
+ * Transformar configuración orderBy a string order_by de Baserow
520
+ *
521
+ * Convierte la configuración de ordenamiento estilo Prisma a la
522
+ * sintaxis de string que espera la API de Baserow, soportando
523
+ * ordenamiento por múltiples campos.
524
+ *
525
+ * @param orderBy - Configuración de ordenamiento Prisma
526
+ * @returns String de ordenamiento para Baserow API
527
+ *
528
+ * @example
529
+ * ```typescript
530
+ * // Ordenamiento simple
531
+ * const result1 = PrismaBaserowMapper.transformOrderByToBaserow({ name: 'asc' })
532
+ * // Result: 'name'
533
+ *
534
+ * // Ordenamiento múltiple
535
+ * const result2 = PrismaBaserowMapper.transformOrderByToBaserow([
536
+ * { created_at: 'desc' },
537
+ * { name: 'asc' }
538
+ * ])
539
+ * // Result: '-created_at,name'
540
+ * ```
541
+ *
542
+ * @since 1.2.0
543
+ */
544
+ static transformOrderByToBaserow(orderBy?: OrderByClause): string | undefined {
545
+ if (!orderBy) return undefined
546
+
547
+ const orders: string[] = []
548
+
549
+ if (Array.isArray(orderBy)) {
550
+ // Múltiples campos de ordenamiento
551
+ for (const orderItem of orderBy) {
552
+ for (const [field, direction] of Object.entries(orderItem)) {
553
+ const prefix = direction === 'desc' ? '-' : ''
554
+ orders.push(`${prefix}${field}`)
555
+ }
556
+ }
557
+ } else {
558
+ // Un solo objeto de ordenamiento
559
+ for (const [field, direction] of Object.entries(orderBy)) {
560
+ const prefix = direction === 'desc' ? '-' : ''
561
+ orders.push(`${prefix}${field}`)
562
+ }
563
+ }
564
+
565
+ return orders.length > 0 ? orders.join(',') : undefined
566
+ }
567
+
568
+ /**
569
+ * Transformar configuración select a include/exclude de Baserow
570
+ *
571
+ * Convierte la selección de campos estilo Prisma a los arrays
572
+ * include/exclude que utiliza la API de Baserow para filtrar campos.
573
+ *
574
+ * @param select - Configuración de selección Prisma
575
+ * @returns Objeto con arrays include/exclude para Baserow
576
+ *
577
+ * @example
578
+ * ```typescript
579
+ * const result = PrismaBaserowMapper.transformSelectToFields({
580
+ * id: true,
581
+ * name: true,
582
+ * email: true,
583
+ * password: false,
584
+ * internal_notes: false
585
+ * })
586
+ * // Result: { include: ['id', 'name', 'email'] }
587
+ * ```
588
+ *
589
+ * @since 1.2.0
590
+ */
591
+ static transformSelectToFields(select?: SelectClause): { include?: string[]; exclude?: string[] } {
592
+ if (!select) return {}
593
+
594
+ const include: string[] = []
595
+ const exclude: string[] = []
596
+
597
+ for (const [field, selected] of Object.entries(select)) {
598
+ if (selected === true) {
599
+ include.push(field)
600
+ } else if (selected === false) {
601
+ exclude.push(field)
602
+ }
603
+ }
604
+
605
+ // Estrategia: si hay campos incluidos, usar include. Si no, usar exclude
606
+ if (include.length > 0) {
607
+ return { include }
608
+ } else if (exclude.length > 0) {
609
+ return { exclude }
610
+ }
611
+
612
+ return {}
613
+ }
614
+
615
+ /**
616
+ * Transformar take/skip a page/size de Baserow
617
+ *
618
+ * Convierte la paginación estilo Prisma (take/skip) al formato
619
+ * page/size que utiliza la API de Baserow.
620
+ *
621
+ * @param take - Número máximo de registros (equivale a size)
622
+ * @param skip - Número de registros a saltar
623
+ * @returns Objeto con page/size para Baserow
624
+ *
625
+ * @example
626
+ * ```typescript
627
+ * const result = PrismaBaserowMapper.transformPagination(20, 40)
628
+ * // Result: { size: 20, page: 3 } // skip 40 with size 20 = page 3
629
+ * ```
630
+ *
631
+ * @since 1.2.0
632
+ */
633
+ static transformPagination(take?: number, skip?: number): { page?: number; size?: number } {
634
+ const result: { page?: number; size?: number } = {}
635
+
636
+ if (take !== undefined) {
637
+ result.size = take
638
+ }
639
+
640
+ if (skip !== undefined && take !== undefined && take > 0) {
641
+ result.page = Math.floor(skip / take) + 1
642
+ } else if (skip !== undefined) {
643
+ // Si solo hay skip sin take, asumir size por defecto
644
+ const defaultSize = 100
645
+ result.size = defaultSize
646
+ result.page = Math.floor(skip / defaultSize) + 1
647
+ }
648
+
649
+ return result
650
+ }
651
+
652
+ /**
653
+ * Validar y optimizar consulta final
654
+ *
655
+ * Aplica validaciones y optimizaciones automáticas a las opciones
656
+ * de consulta convertidas para mejorar rendimiento y evitar errores.
657
+ *
658
+ * @param options - Opciones de consulta a optimizar
659
+ * @returns Opciones optimizadas y validadas
660
+ *
661
+ * @example
662
+ * ```typescript
663
+ * const optimized = PrismaBaserowMapper.validateAndOptimize({
664
+ * page: 0, // Se corrige a 1
665
+ * size: 2000, // Se limita a 1000
666
+ * filters: {}, // Se elimina
667
+ * include: [] // Se elimina
668
+ * })
669
+ * // Result: { page: 1, size: 1000 }
670
+ * ```
671
+ *
672
+ * @since 1.2.0
673
+ */
674
+ static validateAndOptimize(options: QueryOptions): QueryOptions {
675
+ const optimized = { ...options }
676
+
677
+ // Validar paginación
678
+ if (optimized.page !== undefined && optimized.page < 1) {
679
+ optimized.page = 1
680
+ }
681
+
682
+ if (optimized.size !== undefined && optimized.size > 1000) {
683
+ // eslint-disable-next-line no-console
684
+ console.warn('Large page size detected, limiting to 1000. Consider using pagination.')
685
+ optimized.size = 1000
686
+ }
687
+
688
+ if (optimized.size !== undefined && optimized.size < 1) {
689
+ optimized.size = 1
690
+ }
691
+
692
+ // Limpiar filtros vacíos
693
+ if (optimized.filters && Object.keys(optimized.filters).length === 0) {
694
+ delete optimized.filters
695
+ }
696
+
697
+ // Limpiar arrays vacíos
698
+ if (optimized.include && optimized.include.length === 0) {
699
+ delete optimized.include
700
+ }
701
+ if (optimized.exclude && optimized.exclude.length === 0) {
702
+ delete optimized.exclude
703
+ }
704
+ if (optimized.distinct && optimized.distinct.length === 0) {
705
+ delete optimized.distinct
706
+ }
707
+
708
+ // Validar conflictos include/exclude
709
+ if (optimized.include && optimized.exclude) {
710
+ // eslint-disable-next-line no-console
711
+ console.warn('Both include and exclude specified, using include only')
712
+ delete optimized.exclude
713
+ }
714
+
715
+ return optimized
716
+ }
717
+
718
+ /**
719
+ * Crear where clause a partir de filtros legacy de Baserow
720
+ *
721
+ * Función de utilidad para convertir en dirección opuesta:
722
+ * de filtros Baserow existentes a sintaxis where de Prisma.
723
+ *
724
+ * @param filters - Filtros en formato Baserow
725
+ * @returns Cláusula WHERE estilo Prisma
726
+ *
727
+ * @example
728
+ * ```typescript
729
+ * const where = PrismaBaserowMapper.transformFiltersToWhere({
730
+ * name: 'John',
731
+ * age__gte: 18,
732
+ * email__contains: '@company.com'
733
+ * })
734
+ * // Result: { name: 'John', age: { gte: 18 }, email: { contains: '@company.com' } }
735
+ * ```
736
+ *
737
+ * @since 1.2.0
738
+ */
739
+ static transformFiltersToWhere(filters?: Record<string, any>): WhereClause {
740
+ if (!filters) return {}
741
+
742
+ const where: WhereClause = {}
743
+
744
+ for (const [key, value] of Object.entries(filters)) {
745
+ // Detectar operadores Baserow y convertir a Prisma
746
+ if (key.includes('__')) {
747
+ const [field, operator] = key.split('__', 2)
748
+
749
+ if (!where[field] || typeof where[field] !== 'object') {
750
+ where[field] = {}
751
+ }
752
+ const fieldWhere = where[field] as any
753
+
754
+ switch (operator) {
755
+ case 'contains':
756
+ fieldWhere.contains = value
757
+ break
758
+ case 'startswith':
759
+ fieldWhere.startsWith = value
760
+ break
761
+ case 'endswith':
762
+ fieldWhere.endsWith = value
763
+ break
764
+ case 'gt':
765
+ fieldWhere.gt = value
766
+ break
767
+ case 'gte':
768
+ fieldWhere.gte = value
769
+ break
770
+ case 'lt':
771
+ fieldWhere.lt = value
772
+ break
773
+ case 'lte':
774
+ fieldWhere.lte = value
775
+ break
776
+ case 'in':
777
+ fieldWhere.in = typeof value === 'string' ? value.split(',') : value
778
+ break
779
+ case 'not_in':
780
+ fieldWhere.notIn = typeof value === 'string' ? value.split(',') : value
781
+ break
782
+ case 'not':
783
+ fieldWhere.not = value
784
+ break
785
+ case 'empty':
786
+ fieldWhere.isEmpty = Boolean(value)
787
+ break
788
+ case 'not_empty':
789
+ fieldWhere.isNotEmpty = Boolean(value)
790
+ break
791
+ default:
792
+ // Operador no reconocido, mantener como filtro legacy
793
+ where[key] = value
794
+ }
795
+ } else {
796
+ // Campo simple sin operador
797
+ where[key] = value
798
+ }
799
+ }
800
+
801
+ return where
802
+ }
803
+
804
+ /**
805
+ * Detectar si hay operador OR a nivel raíz (para filter_type)
806
+ *
807
+ * Verifica si la consulta tiene un operador OR en el nivel raíz,
808
+ * lo cual permite usar filter_type=OR en Baserow API.
809
+ *
810
+ * @param where - Cláusula WHERE a analizar
811
+ * @returns true si contiene OR a nivel raíz
812
+ *
813
+ * @since 1.3.0
814
+ */
815
+ static hasRootOrOperator(where?: WhereClause): boolean {
816
+ if (!where) return false
817
+ return where.OR !== undefined && Array.isArray(where.OR) && where.OR.length > 0
818
+ }
819
+
820
+ /**
821
+ * Detectar si hay operadores OR en la consulta (incluye anidados)
822
+ *
823
+ * Utilidad para verificar si una consulta contiene operadores OR
824
+ * que requieren procesamiento especial en el cliente.
825
+ *
826
+ * @param where - Cláusula WHERE a analizar
827
+ * @returns true si contiene operadores OR
828
+ *
829
+ * @since 1.2.0
830
+ */
831
+ static hasOrOperators(where?: WhereClause): boolean {
832
+ if (!where) return false
833
+
834
+ if (where.OR && Array.isArray(where.OR) && where.OR.length > 0) {
835
+ return true
836
+ }
837
+
838
+ // Buscar OR anidados
839
+ for (const value of Object.values(where)) {
840
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
841
+ if (this.hasOrOperators(value as WhereClause)) {
842
+ return true
843
+ }
844
+ }
845
+ }
846
+
847
+ return false
848
+ }
849
+
850
+ /**
851
+ * Estimar complejidad de la consulta
852
+ *
853
+ * Analiza una consulta para determinar su complejidad y
854
+ * sugerir optimizaciones si es necesario.
855
+ *
856
+ * @param options - Opciones de consulta a analizar
857
+ * @returns Métricas de complejidad
858
+ *
859
+ * @since 1.2.0
860
+ */
861
+ static analyzeQueryComplexity(options?: PrismaLikeQueryOptions): {
862
+ score: number
863
+ warnings: string[]
864
+ suggestions: string[]
865
+ } {
866
+ const warnings: string[] = []
867
+ const suggestions: string[] = []
868
+ let score = 0
869
+
870
+ if (!options) return { score: 0, warnings, suggestions }
871
+
872
+ // Analizar filtros
873
+ if (options.where) {
874
+ const filterCount = Object.keys(options.where).length
875
+ score += filterCount
876
+
877
+ if (this.hasOrOperators(options.where)) {
878
+ score += 5
879
+ warnings.push('OR operators require client-side processing')
880
+ suggestions.push('Consider restructuring query to avoid OR operators')
881
+ }
882
+
883
+ if (filterCount > 10) {
884
+ warnings.push('High number of filters may impact performance')
885
+ suggestions.push('Consider using fewer, more specific filters')
886
+ }
887
+ }
888
+
889
+ // Analizar paginación
890
+ if (options.take && options.take > 500) {
891
+ score += 3
892
+ warnings.push('Large page size may impact performance')
893
+ suggestions.push('Consider using smaller page sizes with pagination')
894
+ }
895
+
896
+ // Analizar ordenamiento
897
+ if (options.orderBy) {
898
+ const orderCount = Array.isArray(options.orderBy) ? options.orderBy.length : 1
899
+ score += orderCount
900
+ if (orderCount > 3) {
901
+ warnings.push('Multiple order fields may impact performance')
902
+ }
903
+ }
904
+
905
+ return { score, warnings, suggestions }
906
+ }
907
+
908
+ /**
909
+ * Agregar filtro específico según tipo de campo con resolución automática value → ID
910
+ *
911
+ * @private
912
+ * @param filters - Objeto de filtros a modificar
913
+ * @param fieldName - Nombre del campo
914
+ * @param fieldType - Tipo del campo (text, single_select, etc.)
915
+ * @param operator - Operador lógico (equal, contains, etc.)
916
+ * @param value - Valor del filtro
917
+ * @param selectOptions - Mapa de opciones select para resolución value → ID
918
+ */
919
+ private static addFieldFilter(
920
+ filters: Record<string, any>,
921
+ fieldName: string,
922
+ fieldType: string,
923
+ operator: string,
924
+ value: any,
925
+ selectOptions?: Record<string, Record<string, number>>
926
+ ): void {
927
+ let baserowOperator: string
928
+ let resolvedValue = value
929
+
930
+ // Resolver value → ID para campos select automáticamente
931
+ if (['single_select', 'multiple_select'].includes(fieldType)) {
932
+ const fieldOptions = selectOptions?.[fieldName]
933
+ if (fieldOptions && typeof value === 'string') {
934
+ const optionId = fieldOptions[value]
935
+ if (optionId !== undefined) {
936
+ resolvedValue = optionId
937
+ }
938
+ }
939
+ }
940
+
941
+ // Mapear operadores específicos por tipo de campo
942
+ switch (fieldType) {
943
+ case 'single_select':
944
+ baserowOperator = operator === 'equal' ? 'single_select_equal' : `single_select_${operator}`
945
+ break
946
+ case 'multiple_select':
947
+ // Para multiple_select, mapear operadores específicos
948
+ if (operator === 'equal' || operator === 'contains') {
949
+ baserowOperator = 'multiple_select_has'
950
+ } else {
951
+ baserowOperator = `multiple_select_${operator}`
952
+ }
953
+ break
954
+ default:
955
+ // Para campos normales, usar operador estándar
956
+ baserowOperator = operator
957
+ }
958
+
959
+ filters[`filter__${fieldName}__${baserowOperator}`] = this.valueToString(resolvedValue)
960
+ }
961
+ }