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