@teleporthq/teleport-plugin-next-data-source 0.43.63 → 0.43.64

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 (125) hide show
  1. package/__tests__/filter-groups.test.ts +174 -0
  2. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  3. package/dist/cjs/fetchers/airtable.js +1 -1
  4. package/dist/cjs/fetchers/airtable.js.map +1 -1
  5. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  6. package/dist/cjs/fetchers/clickhouse.js +1 -1
  7. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  8. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  9. package/dist/cjs/fetchers/csv-file.js +3 -3
  10. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  11. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  12. package/dist/cjs/fetchers/firestore.js +1 -1
  13. package/dist/cjs/fetchers/firestore.js.map +1 -1
  14. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  15. package/dist/cjs/fetchers/google-sheets.js +1 -1
  16. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  17. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  18. package/dist/cjs/fetchers/javascript.js +1 -1
  19. package/dist/cjs/fetchers/javascript.js.map +1 -1
  20. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  21. package/dist/cjs/fetchers/mariadb.js +1 -1
  22. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  23. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  24. package/dist/cjs/fetchers/mongodb.js +1 -1
  25. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  26. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  27. package/dist/cjs/fetchers/mysql.js +1 -1
  28. package/dist/cjs/fetchers/mysql.js.map +1 -1
  29. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  30. package/dist/cjs/fetchers/postgresql.js +1 -1
  31. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  32. package/dist/cjs/fetchers/redis.d.ts.map +1 -1
  33. package/dist/cjs/fetchers/redis.js +1 -1
  34. package/dist/cjs/fetchers/redis.js.map +1 -1
  35. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  36. package/dist/cjs/fetchers/redshift.js +2 -2
  37. package/dist/cjs/fetchers/redshift.js.map +1 -1
  38. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  39. package/dist/cjs/fetchers/rest-api.js +2 -2
  40. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  41. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  42. package/dist/cjs/fetchers/supabase.js +1 -1
  43. package/dist/cjs/fetchers/supabase.js.map +1 -1
  44. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  45. package/dist/cjs/fetchers/teleport.js +1 -1
  46. package/dist/cjs/fetchers/teleport.js.map +1 -1
  47. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  48. package/dist/cjs/fetchers/turso.js +1 -1
  49. package/dist/cjs/fetchers/turso.js.map +1 -1
  50. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  51. package/dist/cjs/utils.d.ts +32 -0
  52. package/dist/cjs/utils.d.ts.map +1 -1
  53. package/dist/cjs/utils.js +116 -18
  54. package/dist/cjs/utils.js.map +1 -1
  55. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  56. package/dist/esm/fetchers/airtable.js +2 -2
  57. package/dist/esm/fetchers/airtable.js.map +1 -1
  58. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  59. package/dist/esm/fetchers/clickhouse.js +2 -2
  60. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  61. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  62. package/dist/esm/fetchers/csv-file.js +4 -4
  63. package/dist/esm/fetchers/csv-file.js.map +1 -1
  64. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  65. package/dist/esm/fetchers/firestore.js +2 -2
  66. package/dist/esm/fetchers/firestore.js.map +1 -1
  67. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  68. package/dist/esm/fetchers/google-sheets.js +2 -2
  69. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  70. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  71. package/dist/esm/fetchers/javascript.js +2 -2
  72. package/dist/esm/fetchers/javascript.js.map +1 -1
  73. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  74. package/dist/esm/fetchers/mariadb.js +2 -2
  75. package/dist/esm/fetchers/mariadb.js.map +1 -1
  76. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  77. package/dist/esm/fetchers/mongodb.js +2 -2
  78. package/dist/esm/fetchers/mongodb.js.map +1 -1
  79. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  80. package/dist/esm/fetchers/mysql.js +2 -2
  81. package/dist/esm/fetchers/mysql.js.map +1 -1
  82. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  83. package/dist/esm/fetchers/postgresql.js +2 -2
  84. package/dist/esm/fetchers/postgresql.js.map +1 -1
  85. package/dist/esm/fetchers/redis.d.ts.map +1 -1
  86. package/dist/esm/fetchers/redis.js +2 -2
  87. package/dist/esm/fetchers/redis.js.map +1 -1
  88. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  89. package/dist/esm/fetchers/redshift.js +3 -3
  90. package/dist/esm/fetchers/redshift.js.map +1 -1
  91. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  92. package/dist/esm/fetchers/rest-api.js +3 -3
  93. package/dist/esm/fetchers/rest-api.js.map +1 -1
  94. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  95. package/dist/esm/fetchers/supabase.js +2 -2
  96. package/dist/esm/fetchers/supabase.js.map +1 -1
  97. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  98. package/dist/esm/fetchers/teleport.js +2 -2
  99. package/dist/esm/fetchers/teleport.js.map +1 -1
  100. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  101. package/dist/esm/fetchers/turso.js +2 -2
  102. package/dist/esm/fetchers/turso.js.map +1 -1
  103. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  104. package/dist/esm/utils.d.ts +32 -0
  105. package/dist/esm/utils.d.ts.map +1 -1
  106. package/dist/esm/utils.js +112 -17
  107. package/dist/esm/utils.js.map +1 -1
  108. package/package.json +3 -3
  109. package/src/fetchers/airtable.ts +35 -36
  110. package/src/fetchers/clickhouse.ts +26 -36
  111. package/src/fetchers/csv-file.ts +13 -21
  112. package/src/fetchers/firestore.ts +41 -35
  113. package/src/fetchers/google-sheets.ts +15 -23
  114. package/src/fetchers/javascript.ts +11 -21
  115. package/src/fetchers/mariadb.ts +33 -44
  116. package/src/fetchers/mongodb.ts +66 -81
  117. package/src/fetchers/mysql.ts +33 -45
  118. package/src/fetchers/postgresql.ts +52 -62
  119. package/src/fetchers/redis.ts +6 -5
  120. package/src/fetchers/redshift.ts +29 -41
  121. package/src/fetchers/rest-api.ts +11 -21
  122. package/src/fetchers/supabase.ts +104 -41
  123. package/src/fetchers/teleport.ts +53 -64
  124. package/src/fetchers/turso.ts +27 -39
  125. package/src/utils.ts +188 -31
@@ -2,6 +2,7 @@ import {
2
2
  generateDateFormatterCode,
3
3
  generateSortFilterHelperCode,
4
4
  generateSafeJSONParseCode,
5
+ generateFilterTreeHelpersCode,
5
6
  } from '../utils'
6
7
  import { generateHeaderDetectionCode } from './utils/header-detection'
7
8
 
@@ -86,17 +87,15 @@ const generateHandlerBody = (): string => {
86
87
  }
87
88
 
88
89
  if (filters) {
89
- const parsedFilters = safeJSONParse(filters)
90
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
90
91
 
91
- if (Array.isArray(parsedFilters)) {
92
- filteredData = filteredData.filter((item) => {
93
- return parsedFilters.every((filter) => {
94
- if (!filter.source || filter.destination === undefined) return true
95
-
96
- const field = labelToIdMap[filter.source] || filter.source
92
+ if (filterTree) {
93
+ filteredData = filteredData.filter((item) =>
94
+ evaluateFilterTree(filterTree, (condition) => {
95
+ const field = labelToIdMap[condition.source] || condition.source
97
96
  const value = getNestedValue(item, field)
98
- const target = filter.destination
99
- const operand = filter.operand || '='
97
+ const target = condition.destination
98
+ const operand = condition.operand
100
99
 
101
100
  if (Array.isArray(target)) {
102
101
  if (operand === '!=') {
@@ -107,18 +106,7 @@ const generateHandlerBody = (): string => {
107
106
 
108
107
  return compareValues(value, target, operand)
109
108
  })
110
- })
111
- } else {
112
- filteredData = filteredData.filter((item) => {
113
- return Object.entries(parsedFilters).every(([key, value]) => {
114
- const field = labelToIdMap[key] || key
115
- const itemValue = getNestedValue(item, field)
116
- if (Array.isArray(value)) {
117
- return value.includes(itemValue)
118
- }
119
- return compareValues(itemValue, value, '=')
120
- })
121
- })
109
+ )
122
110
  }
123
111
  }
124
112
 
@@ -227,6 +215,8 @@ ${generateDateFormatterCode()}
227
215
 
228
216
  ${generateSortFilterHelperCode()}
229
217
 
218
+ ${generateFilterTreeHelpersCode()}
219
+
230
220
  ${generateHeaderDetectionCode()}
231
221
 
232
222
  function parseCSVLine(line) {
@@ -335,6 +325,8 @@ ${generateDateFormatterCode()}
335
325
 
336
326
  ${generateSortFilterHelperCode()}
337
327
 
328
+ ${generateFilterTreeHelpersCode()}
329
+
338
330
  export default async function handler(req, res) {${generateHandlerBody()}
339
331
  }
340
332
  `
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateFilterTreeHelpersCode,
5
6
  } from '../utils'
6
7
 
7
8
  export const validateFirestoreConfig = (
@@ -73,6 +74,8 @@ const getFirestore = () => {
73
74
 
74
75
  ${generateSafeJSONParseCode()}
75
76
 
77
+ ${generateFilterTreeHelpersCode()}
78
+
76
79
  ${generateDateFormatterCode()}
77
80
 
78
81
  export default async function handler(req, res) {
@@ -83,46 +86,49 @@ export default async function handler(req, res) {
83
86
  let queryRef = firestore.collection('${tableName}')
84
87
 
85
88
  if (filters) {
86
- const parsedFilters = safeJSONParse(filters)
89
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
87
90
 
88
- if (Array.isArray(parsedFilters)) {
89
- parsedFilters.forEach((filter) => {
90
- if (!filter.source || filter.destination === undefined) return
91
-
92
- const field = filter.source
93
- const value = filter.destination
94
- const operand = filter.operand || '='
95
-
96
- // Map operands to Firestore operators
97
- const operatorMap = {
98
- '=': '==',
99
- '!=': '!=',
100
- '>': '>',
101
- '<': '<',
102
- '>=': '>=',
103
- '<=': '<=',
104
- }
91
+ if (filterTree) {
92
+ // Chained .where() calls are ANDed, so an OR group is built with
93
+ // Firestore's composite Filter API instead.
94
+ const operatorMap = {
95
+ '=': '==',
96
+ '!=': '!=',
97
+ '>': '>',
98
+ '<': '<',
99
+ '>=': '>=',
100
+ '<=': '<=',
101
+ }
102
+
103
+ const buildCondition = (condition) => {
104
+ const field = condition.source
105
+ const value = condition.destination
106
+ const operand = condition.operand
105
107
 
106
108
  if (Array.isArray(value)) {
107
- if (value.length === 0) return
108
- if (operand === '!=') {
109
- queryRef = queryRef.where(field, 'not-in', value)
110
- } else {
111
- queryRef = queryRef.where(field, 'in', value)
112
- }
113
- } else {
114
- const firestoreOp = operatorMap[operand] || '=='
115
- queryRef = queryRef.where(field, firestoreOp, value)
109
+ if (value.length === 0) return null
110
+ return admin.firestore.Filter.where(field, operand === '!=' ? 'not-in' : 'in', value)
116
111
  }
117
- })
118
- } else {
119
- Object.entries(parsedFilters).forEach(([key, value]) => {
120
- if (Array.isArray(value)) {
121
- queryRef = queryRef.where(key, 'in', value)
122
- } else {
123
- queryRef = queryRef.where(key, '==', value)
112
+
113
+ return admin.firestore.Filter.where(field, operatorMap[operand] || '==', value)
114
+ }
115
+
116
+ const buildNode = (node) => {
117
+ if (node.type === 'group') {
118
+ const parts = node.children.map(buildNode).filter(Boolean)
119
+ if (parts.length === 0) return null
120
+ if (parts.length === 1) return parts[0]
121
+ return node.operator === 'or'
122
+ ? admin.firestore.Filter.or(...parts)
123
+ : admin.firestore.Filter.and(...parts)
124
124
  }
125
- })
125
+ return buildCondition(node)
126
+ }
127
+
128
+ const filterExpression = buildNode(filterTree)
129
+ if (filterExpression) {
130
+ queryRef = queryRef.where(filterExpression)
131
+ }
126
132
  }
127
133
  }
128
134
 
@@ -1,4 +1,8 @@
1
- import { generateSortFilterHelperCode, generateSafeJSONParseCode } from '../utils'
1
+ import {
2
+ generateSortFilterHelperCode,
3
+ generateSafeJSONParseCode,
4
+ generateFilterTreeHelpersCode,
5
+ } from '../utils'
2
6
  import { generateHeaderDetectionCode } from './utils/header-detection'
3
7
 
4
8
  export const validateGoogleSheetsConfig = (
@@ -53,6 +57,8 @@ ${generateSafeJSONParseCode()}
53
57
 
54
58
  ${generateSortFilterHelperCode()}
55
59
 
60
+ ${generateFilterTreeHelpersCode()}
61
+
56
62
  ${generateHeaderDetectionCode()}
57
63
 
58
64
  export default async function handler(req, res) {
@@ -280,18 +286,16 @@ export default async function handler(req, res) {
280
286
  }
281
287
 
282
288
  if (filters) {
283
- const parsedFilters = safeJSONParse(filters)
289
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
284
290
 
285
- if (Array.isArray(parsedFilters)) {
286
- filteredData = filteredData.filter((item) => {
287
- return parsedFilters.every((filter) => {
288
- if (!filter.source || filter.destination === undefined) return true
289
-
291
+ if (filterTree) {
292
+ filteredData = filteredData.filter((item) =>
293
+ evaluateFilterTree(filterTree, (condition) => {
290
294
  // Map label to column ID
291
- const field = labelToIdMap[filter.source] || filter.source
295
+ const field = labelToIdMap[condition.source] || condition.source
292
296
  const value = getNestedValue(item, field)
293
- const target = filter.destination
294
- const operand = filter.operand || '='
297
+ const target = condition.destination
298
+ const operand = condition.operand
295
299
 
296
300
  if (Array.isArray(target)) {
297
301
  if (operand === '!=') {
@@ -302,19 +306,7 @@ export default async function handler(req, res) {
302
306
 
303
307
  return compareValues(value, target, operand)
304
308
  })
305
- })
306
- } else {
307
- filteredData = filteredData.filter((item) => {
308
- return Object.entries(parsedFilters).every(([key, value]) => {
309
- // Map label to column ID
310
- const field = labelToIdMap[key] || key
311
- const itemValue = getNestedValue(item, field)
312
- if (Array.isArray(value)) {
313
- return value.includes(itemValue)
314
- }
315
- return compareValues(itemValue, value, '=')
316
- })
317
- })
309
+ )
318
310
  }
319
311
  }
320
312
 
@@ -2,6 +2,7 @@ import {
2
2
  generateDateFormatterCode,
3
3
  generateSortFilterHelperCode,
4
4
  generateSafeJSONParseCode,
5
+ generateFilterTreeHelpersCode,
5
6
  } from '../utils'
6
7
 
7
8
  export const validateJavaScriptConfig = (
@@ -47,6 +48,8 @@ ${generateDateFormatterCode()}
47
48
 
48
49
  ${generateSortFilterHelperCode()}
49
50
 
51
+ ${generateFilterTreeHelpersCode()}
52
+
50
53
  export default async function handler(req, res) {
51
54
  try {
52
55
  const { limit, offset, page, perPage, query, queryColumns, sortBy, sortOrder, filters, sorts } = req.query
@@ -87,17 +90,14 @@ export default async function handler(req, res) {
87
90
 
88
91
  if (filters) {
89
92
  try {
90
- const parsedFilters = safeJSONParse(filters)
93
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
91
94
 
92
- if (Array.isArray(parsedFilters)) {
93
- data = data.filter((item) => {
94
- return parsedFilters.every((filter) => {
95
- if (!filter.source || filter.destination === undefined) return true
96
-
97
- const field = filter.source
98
- const value = getNestedValue(item, field)
99
- const target = filter.destination
100
- const operand = filter.operand || '='
95
+ if (filterTree) {
96
+ data = data.filter((item) =>
97
+ evaluateFilterTree(filterTree, (condition) => {
98
+ const value = getNestedValue(item, condition.source)
99
+ const target = condition.destination
100
+ const operand = condition.operand
101
101
 
102
102
  if (Array.isArray(target)) {
103
103
  if (operand === '!=') {
@@ -108,17 +108,7 @@ export default async function handler(req, res) {
108
108
 
109
109
  return compareValues(value, target, operand)
110
110
  })
111
- })
112
- } else {
113
- data = data.filter((item) => {
114
- return Object.entries(parsedFilters).every(([key, value]) => {
115
- const itemValue = getNestedValue(item, key)
116
- if (Array.isArray(value)) {
117
- return value.includes(itemValue)
118
- }
119
- return compareValues(itemValue, value, '=')
120
- })
121
- })
111
+ )
122
112
  }
123
113
  } catch (err) {
124
114
  console.error('Error parsing filters:', err)
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateFilterTreeHelpersCode,
5
6
  generateSearchEscapeHelpersCode,
6
7
  } from '../utils'
7
8
 
@@ -27,58 +28,46 @@ export const generateMariaDBFetcher = (
27
28
 
28
29
  ${generateSafeJSONParseCode()}
29
30
 
31
+ ${generateFilterTreeHelpersCode()}
32
+
30
33
  ${generateSearchEscapeHelpersCode()}
31
34
 
32
35
  // Helper function to process filters and build conditions
33
36
  const processFilters = (filters, conditions, queryParams) => {
34
37
  if (!filters) return
35
38
 
36
- const parsedFilters = safeJSONParse(filters)
39
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
40
+ if (!filterTree) return
37
41
 
38
- if (Array.isArray(parsedFilters)) {
39
- parsedFilters.forEach((filter) => {
40
- if (!filter.source || filter.destination === undefined) return
41
-
42
- const field = \`\\\`\${filter.source}\\\`\`
43
- const value = filter.destination
44
- const operand = filter.operand || '='
45
-
46
- if (Array.isArray(value)) {
47
- if (value.length === 0) return
48
- const placeholders = value.map(() => '?').join(', ')
49
- queryParams.push(...value)
50
- if (operand === '!=') {
51
- conditions.push(\`\${field} NOT IN (\${placeholders})\`)
52
- } else {
53
- conditions.push(\`\${field} IN (\${placeholders})\`)
54
- }
55
- } else {
56
- if (value === null) {
57
- if (operand === '=') {
58
- conditions.push(\`\${field} IS NULL\`)
59
- } else if (operand === '!=') {
60
- conditions.push(\`\${field} IS NOT NULL\`)
61
- }
62
- } else {
63
- const validOps = ['=', '!=', '>', '<', '>=', '<=']
64
- const sqlOperator = validOps.includes(operand) ? operand : '='
65
- conditions.push(\`\${field} \${sqlOperator} ?\`)
66
- queryParams.push(value)
67
- }
68
- }
69
- })
70
- } else {
71
- Object.entries(parsedFilters).forEach(([key, value]) => {
72
- if (Array.isArray(value)) {
73
- const placeholders = value.map(() => '?').join(', ')
74
- queryParams.push(...value)
75
- conditions.push(\`\\\`\${key}\\\` IN (\${placeholders})\`)
76
- } else {
77
- conditions.push(\`\\\`\${key}\\\` = ?\`)
78
- queryParams.push(value)
79
- }
80
- })
42
+ const buildCondition = (condition) => {
43
+ const field = \`\\\`\${condition.source}\\\`\`
44
+ const value = condition.destination
45
+ const operand = condition.operand
46
+
47
+ if (Array.isArray(value)) {
48
+ if (value.length === 0) return null
49
+ const placeholders = value.map(() => '?').join(', ')
50
+ queryParams.push(...value)
51
+ return operand === '!='
52
+ ? \`\${field} NOT IN (\${placeholders})\`
53
+ : \`\${field} IN (\${placeholders})\`
54
+ }
55
+
56
+ if (value === null) {
57
+ if (operand === '=') return \`\${field} IS NULL\`
58
+ if (operand === '!=') return \`\${field} IS NOT NULL\`
59
+ return null
60
+ }
61
+
62
+ // Validate operator to prevent SQL injection
63
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
64
+ const sqlOperator = validOps.includes(operand) ? operand : '='
65
+ queryParams.push(value)
66
+ return \`\${field} \${sqlOperator} ?\`
81
67
  }
68
+
69
+ const clause = buildFilterTreeClause(filterTree, buildCondition)
70
+ if (clause) conditions.push(clause)
82
71
  }
83
72
 
84
73
  ${generateDateFormatterCode()}
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateFilterTreeHelpersCode,
5
6
  } from '../utils'
6
7
 
7
8
  export const validateMongoDBConfig = (
@@ -73,95 +74,79 @@ export const generateMongoDBFetcher = (
73
74
 
74
75
  ${generateSafeJSONParseCode()}
75
76
 
77
+ ${generateFilterTreeHelpersCode()}
78
+
76
79
  // Helper function to process filters
77
80
  const processFilters = (filters, filter) => {
78
81
  if (!filters) return
79
82
 
80
- const parsedFilters = safeJSONParse(filters)
83
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
84
+ if (!filterTree) return
81
85
 
82
- if (Array.isArray(parsedFilters)) {
83
- parsedFilters.forEach((filterItem) => {
84
- if (!filterItem.source || filterItem.destination === undefined) return
85
-
86
- const field = filterItem.source
87
- const value = filterItem.destination
88
- const operand = filterItem.operand || '='
89
-
90
- // Handle _id specially
91
- const processValue = (v) => {
92
- if (field === '_id' && typeof v === 'string') {
93
- try {
94
- return new ObjectId(v)
95
- } catch (e) {
96
- return v
97
- }
98
- }
86
+ const processValue = (field, v) => {
87
+ if (field === '_id' && typeof v === 'string') {
88
+ try {
89
+ return new ObjectId(v)
90
+ } catch (e) {
99
91
  return v
100
92
  }
101
-
102
- if (Array.isArray(value)) {
103
- const processedValues = value.map(processValue)
104
- if (operand === '!=') {
105
- filter[field] = { $nin: processedValues }
106
- } else {
107
- filter[field] = { $in: processedValues }
108
- }
109
- } else {
110
- const processedValue = processValue(value)
111
-
112
- // Handle null values
113
- if (processedValue === null) {
114
- if (operand === '=') {
115
- filter[field] = null
116
- } else if (operand === '!=') {
117
- filter[field] = { $ne: null }
118
- }
119
- } else {
120
- // Map operand to MongoDB operators
121
- switch (operand) {
122
- case '=':
123
- filter[field] = processedValue
124
- break
125
- case '!=':
126
- filter[field] = { $ne: processedValue }
127
- break
128
- case '>':
129
- filter[field] = { $gt: processedValue }
130
- break
131
- case '>=':
132
- filter[field] = { $gte: processedValue }
133
- break
134
- case '<':
135
- filter[field] = { $lt: processedValue }
136
- break
137
- case '<=':
138
- filter[field] = { $lte: processedValue }
139
- break
140
- default:
141
- filter[field] = processedValue
142
- }
143
- }
144
- }
145
- })
146
- } else {
147
- Object.entries(parsedFilters).forEach(([key, value]) => {
148
- if (key === '_id') {
149
- if (Array.isArray(value)) {
150
- filter[key] = {
151
- $in: value.map((id) => (typeof id === 'string' ? new ObjectId(id) : id))
152
- }
153
- } else if (typeof value === 'string') {
154
- filter[key] = new ObjectId(value)
155
- } else {
156
- filter[key] = value
157
- }
158
- } else if (Array.isArray(value)) {
159
- filter[key] = { $in: value }
160
- } else {
161
- filter[key] = value
162
- }
163
- })
93
+ }
94
+ return v
164
95
  }
96
+
97
+ const buildCondition = (condition) => {
98
+ const field = condition.source
99
+ const value = condition.destination
100
+ const operand = condition.operand
101
+
102
+ if (Array.isArray(value)) {
103
+ const processedValues = value.map((entry) => processValue(field, entry))
104
+ return operand === '!='
105
+ ? { [field]: { $nin: processedValues } }
106
+ : { [field]: { $in: processedValues } }
107
+ }
108
+
109
+ const processedValue = processValue(field, value)
110
+
111
+ if (processedValue === null) {
112
+ if (operand === '=') return { [field]: null }
113
+ if (operand === '!=') return { [field]: { $ne: null } }
114
+ return null
115
+ }
116
+
117
+ switch (operand) {
118
+ case '!=':
119
+ return { [field]: { $ne: processedValue } }
120
+ case '>':
121
+ return { [field]: { $gt: processedValue } }
122
+ case '>=':
123
+ return { [field]: { $gte: processedValue } }
124
+ case '<':
125
+ return { [field]: { $lt: processedValue } }
126
+ case '<=':
127
+ return { [field]: { $lte: processedValue } }
128
+ default:
129
+ return { [field]: processedValue }
130
+ }
131
+ }
132
+
133
+ const buildNode = (node) => {
134
+ if (node.type === 'group') {
135
+ const parts = node.children.map(buildNode).filter(Boolean)
136
+ if (parts.length === 0) return null
137
+ if (parts.length === 1) return parts[0]
138
+ return node.operator === 'or' ? { $or: parts } : { $and: parts }
139
+ }
140
+ return buildCondition(node)
141
+ }
142
+
143
+ const query = buildNode(filterTree)
144
+ if (!query) return
145
+
146
+ // Merged under $and rather than assigned onto filter directly: the search
147
+ // above may already own filter.$or, and two conditions on the same field
148
+ // would otherwise overwrite each other.
149
+ filter.$and = (filter.$and || []).concat([query])
165
150
  }
166
151
 
167
152
  ${generateDateFormatterCode()}
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateFilterTreeHelpersCode,
5
6
  generateSearchEscapeHelpersCode,
6
7
  } from '../utils'
7
8
 
@@ -64,59 +65,46 @@ const getConnection = () => {
64
65
 
65
66
  ${generateSafeJSONParseCode()}
66
67
 
68
+ ${generateFilterTreeHelpersCode()}
69
+
67
70
  ${generateSearchEscapeHelpersCode()}
68
71
 
69
72
  // Helper function to process filters and build conditions
70
73
  const processFilters = (filters, conditions, queryParams) => {
71
74
  if (!filters) return
72
75
 
73
- const parsedFilters = safeJSONParse(filters)
76
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
77
+ if (!filterTree) return
74
78
 
75
- if (Array.isArray(parsedFilters)) {
76
- parsedFilters.forEach((filter) => {
77
- if (!filter.source || filter.destination === undefined) return
78
-
79
- const field = mysql.escapeId(filter.source)
80
- const value = filter.destination
81
- const operand = filter.operand || '='
82
-
83
- if (Array.isArray(value)) {
84
- if (value.length === 0) return
85
- const placeholders = value.map(() => '?').join(', ')
86
- queryParams.push(...value)
87
- if (operand === '!=') {
88
- conditions.push(\`\${field} NOT IN (\${placeholders})\`)
89
- } else {
90
- conditions.push(\`\${field} IN (\${placeholders})\`)
91
- }
92
- } else {
93
- if (value === null) {
94
- if (operand === '=') {
95
- conditions.push(\`\${field} IS NULL\`)
96
- } else if (operand === '!=') {
97
- conditions.push(\`\${field} IS NOT NULL\`)
98
- }
99
- } else {
100
- // Validate operator to prevent SQL injection
101
- const validOps = ['=', '!=', '>', '<', '>=', '<=']
102
- const sqlOperator = validOps.includes(operand) ? operand : '='
103
- conditions.push(\`\${field} \${sqlOperator} ?\`)
104
- queryParams.push(value)
105
- }
106
- }
107
- })
108
- } else {
109
- Object.entries(parsedFilters).forEach(([key, value]) => {
110
- if (Array.isArray(value)) {
111
- const placeholders = value.map(() => '?').join(', ')
112
- queryParams.push(...value)
113
- conditions.push(\`\${mysql.escapeId(key)} IN (\${placeholders})\`)
114
- } else {
115
- conditions.push(\`\${mysql.escapeId(key)} = ?\`)
116
- queryParams.push(value)
117
- }
118
- })
79
+ const buildCondition = (condition) => {
80
+ const field = mysql.escapeId(condition.source)
81
+ const value = condition.destination
82
+ const operand = condition.operand
83
+
84
+ if (Array.isArray(value)) {
85
+ if (value.length === 0) return null
86
+ const placeholders = value.map(() => '?').join(', ')
87
+ queryParams.push(...value)
88
+ return operand === '!='
89
+ ? \`\${field} NOT IN (\${placeholders})\`
90
+ : \`\${field} IN (\${placeholders})\`
91
+ }
92
+
93
+ if (value === null) {
94
+ if (operand === '=') return \`\${field} IS NULL\`
95
+ if (operand === '!=') return \`\${field} IS NOT NULL\`
96
+ return null
97
+ }
98
+
99
+ // Validate operator to prevent SQL injection
100
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
101
+ const sqlOperator = validOps.includes(operand) ? operand : '='
102
+ queryParams.push(value)
103
+ return \`\${field} \${sqlOperator} ?\`
119
104
  }
105
+
106
+ const clause = buildFilterTreeClause(filterTree, buildCondition)
107
+ if (clause) conditions.push(clause)
120
108
  }
121
109
 
122
110
  ${generateDateFormatterCode()}