@teleporthq/teleport-plugin-next-data-source 0.43.62 → 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 (200) hide show
  1. package/__tests__/blog-post-categories.test.ts +131 -0
  2. package/__tests__/custom-page-transform.test.ts +119 -0
  3. package/__tests__/ecommerce-product-ratings.test.ts +155 -0
  4. package/__tests__/ecommerce-product-storefront-contract.test.ts +147 -0
  5. package/__tests__/filter-groups.test.ts +174 -0
  6. package/__tests__/product-reviews-structured-data.test.ts +196 -0
  7. package/dist/cjs/data-source-fetchers.d.ts +3 -3
  8. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  9. package/dist/cjs/data-source-fetchers.js.map +1 -1
  10. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  11. package/dist/cjs/fetchers/airtable.js +1 -1
  12. package/dist/cjs/fetchers/airtable.js.map +1 -1
  13. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  14. package/dist/cjs/fetchers/clickhouse.js +1 -1
  15. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  16. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  17. package/dist/cjs/fetchers/csv-file.js +3 -3
  18. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  19. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/firestore.js +1 -1
  21. package/dist/cjs/fetchers/firestore.js.map +1 -1
  22. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  23. package/dist/cjs/fetchers/google-sheets.js +1 -1
  24. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  25. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/javascript.js +1 -1
  27. package/dist/cjs/fetchers/javascript.js.map +1 -1
  28. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  29. package/dist/cjs/fetchers/mariadb.js +1 -1
  30. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  31. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  32. package/dist/cjs/fetchers/mongodb.js +1 -1
  33. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  34. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  35. package/dist/cjs/fetchers/mysql.js +1 -1
  36. package/dist/cjs/fetchers/mysql.js.map +1 -1
  37. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  38. package/dist/cjs/fetchers/postgresql.js +1 -1
  39. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  40. package/dist/cjs/fetchers/redis.d.ts.map +1 -1
  41. package/dist/cjs/fetchers/redis.js +1 -1
  42. package/dist/cjs/fetchers/redis.js.map +1 -1
  43. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  44. package/dist/cjs/fetchers/redshift.js +2 -2
  45. package/dist/cjs/fetchers/redshift.js.map +1 -1
  46. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/rest-api.js +2 -2
  48. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  49. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  50. package/dist/cjs/fetchers/supabase.js +1 -1
  51. package/dist/cjs/fetchers/supabase.js.map +1 -1
  52. package/dist/cjs/fetchers/teleport.d.ts +2 -2
  53. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  54. package/dist/cjs/fetchers/teleport.js +1 -1
  55. package/dist/cjs/fetchers/teleport.js.map +1 -1
  56. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  57. package/dist/cjs/fetchers/turso.js +1 -1
  58. package/dist/cjs/fetchers/turso.js.map +1 -1
  59. package/dist/cjs/index.d.ts +1 -1
  60. package/dist/cjs/index.d.ts.map +1 -1
  61. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  62. package/dist/cjs/pagination-plugin.js.map +1 -1
  63. package/dist/cjs/transformations/blog-post.d.ts +7 -1
  64. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  65. package/dist/cjs/transformations/blog-post.js +4 -2
  66. package/dist/cjs/transformations/blog-post.js.map +1 -1
  67. package/dist/cjs/transformations/category-taxonomy.d.ts +33 -0
  68. package/dist/cjs/transformations/category-taxonomy.d.ts.map +1 -0
  69. package/dist/cjs/transformations/category-taxonomy.js +42 -0
  70. package/dist/cjs/transformations/category-taxonomy.js.map +1 -0
  71. package/dist/cjs/transformations/custom-page.d.ts +18 -0
  72. package/dist/cjs/transformations/custom-page.d.ts.map +1 -0
  73. package/dist/cjs/transformations/custom-page.js +24 -0
  74. package/dist/cjs/transformations/custom-page.js.map +1 -0
  75. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  76. package/dist/cjs/transformations/ecommerce-product.js +2 -35
  77. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  78. package/dist/cjs/transformations/index.d.ts +23 -4
  79. package/dist/cjs/transformations/index.d.ts.map +1 -1
  80. package/dist/cjs/transformations/index.js +38 -8
  81. package/dist/cjs/transformations/index.js.map +1 -1
  82. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  83. package/dist/cjs/transformations/shared-utils.js +1 -1
  84. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  85. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  86. package/dist/cjs/utils.d.ts +35 -3
  87. package/dist/cjs/utils.d.ts.map +1 -1
  88. package/dist/cjs/utils.js +116 -18
  89. package/dist/cjs/utils.js.map +1 -1
  90. package/dist/esm/data-source-fetchers.d.ts +3 -3
  91. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  92. package/dist/esm/data-source-fetchers.js.map +1 -1
  93. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  94. package/dist/esm/fetchers/airtable.js +2 -2
  95. package/dist/esm/fetchers/airtable.js.map +1 -1
  96. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  97. package/dist/esm/fetchers/clickhouse.js +2 -2
  98. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  99. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  100. package/dist/esm/fetchers/csv-file.js +4 -4
  101. package/dist/esm/fetchers/csv-file.js.map +1 -1
  102. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  103. package/dist/esm/fetchers/firestore.js +2 -2
  104. package/dist/esm/fetchers/firestore.js.map +1 -1
  105. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  106. package/dist/esm/fetchers/google-sheets.js +2 -2
  107. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  108. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  109. package/dist/esm/fetchers/javascript.js +2 -2
  110. package/dist/esm/fetchers/javascript.js.map +1 -1
  111. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  112. package/dist/esm/fetchers/mariadb.js +2 -2
  113. package/dist/esm/fetchers/mariadb.js.map +1 -1
  114. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  115. package/dist/esm/fetchers/mongodb.js +2 -2
  116. package/dist/esm/fetchers/mongodb.js.map +1 -1
  117. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  118. package/dist/esm/fetchers/mysql.js +2 -2
  119. package/dist/esm/fetchers/mysql.js.map +1 -1
  120. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  121. package/dist/esm/fetchers/postgresql.js +2 -2
  122. package/dist/esm/fetchers/postgresql.js.map +1 -1
  123. package/dist/esm/fetchers/redis.d.ts.map +1 -1
  124. package/dist/esm/fetchers/redis.js +2 -2
  125. package/dist/esm/fetchers/redis.js.map +1 -1
  126. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  127. package/dist/esm/fetchers/redshift.js +3 -3
  128. package/dist/esm/fetchers/redshift.js.map +1 -1
  129. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  130. package/dist/esm/fetchers/rest-api.js +3 -3
  131. package/dist/esm/fetchers/rest-api.js.map +1 -1
  132. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  133. package/dist/esm/fetchers/supabase.js +2 -2
  134. package/dist/esm/fetchers/supabase.js.map +1 -1
  135. package/dist/esm/fetchers/teleport.d.ts +2 -2
  136. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  137. package/dist/esm/fetchers/teleport.js +2 -2
  138. package/dist/esm/fetchers/teleport.js.map +1 -1
  139. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  140. package/dist/esm/fetchers/turso.js +2 -2
  141. package/dist/esm/fetchers/turso.js.map +1 -1
  142. package/dist/esm/index.d.ts +1 -1
  143. package/dist/esm/index.d.ts.map +1 -1
  144. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  145. package/dist/esm/pagination-plugin.js +1 -1
  146. package/dist/esm/pagination-plugin.js.map +1 -1
  147. package/dist/esm/transformations/blog-post.d.ts +7 -1
  148. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  149. package/dist/esm/transformations/blog-post.js +4 -2
  150. package/dist/esm/transformations/blog-post.js.map +1 -1
  151. package/dist/esm/transformations/category-taxonomy.d.ts +33 -0
  152. package/dist/esm/transformations/category-taxonomy.d.ts.map +1 -0
  153. package/dist/esm/transformations/category-taxonomy.js +37 -0
  154. package/dist/esm/transformations/category-taxonomy.js.map +1 -0
  155. package/dist/esm/transformations/custom-page.d.ts +18 -0
  156. package/dist/esm/transformations/custom-page.d.ts.map +1 -0
  157. package/dist/esm/transformations/custom-page.js +20 -0
  158. package/dist/esm/transformations/custom-page.js.map +1 -0
  159. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  160. package/dist/esm/transformations/ecommerce-product.js +3 -36
  161. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  162. package/dist/esm/transformations/index.d.ts +23 -4
  163. package/dist/esm/transformations/index.d.ts.map +1 -1
  164. package/dist/esm/transformations/index.js +37 -7
  165. package/dist/esm/transformations/index.js.map +1 -1
  166. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  167. package/dist/esm/transformations/shared-utils.js +1 -1
  168. package/dist/esm/transformations/shared-utils.js.map +1 -1
  169. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  170. package/dist/esm/utils.d.ts +35 -3
  171. package/dist/esm/utils.d.ts.map +1 -1
  172. package/dist/esm/utils.js +112 -17
  173. package/dist/esm/utils.js.map +1 -1
  174. package/package.json +5 -5
  175. package/src/data-source-fetchers.ts +3 -3
  176. package/src/fetchers/airtable.ts +35 -36
  177. package/src/fetchers/clickhouse.ts +26 -36
  178. package/src/fetchers/csv-file.ts +13 -21
  179. package/src/fetchers/firestore.ts +41 -35
  180. package/src/fetchers/google-sheets.ts +15 -23
  181. package/src/fetchers/javascript.ts +11 -21
  182. package/src/fetchers/mariadb.ts +33 -44
  183. package/src/fetchers/mongodb.ts +66 -81
  184. package/src/fetchers/mysql.ts +33 -45
  185. package/src/fetchers/postgresql.ts +52 -62
  186. package/src/fetchers/redis.ts +6 -5
  187. package/src/fetchers/redshift.ts +29 -41
  188. package/src/fetchers/rest-api.ts +11 -21
  189. package/src/fetchers/supabase.ts +104 -41
  190. package/src/fetchers/teleport.ts +55 -66
  191. package/src/fetchers/turso.ts +27 -39
  192. package/src/index.ts +1 -1
  193. package/src/pagination-plugin.ts +3 -6
  194. package/src/transformations/blog-post.ts +28 -9
  195. package/src/transformations/category-taxonomy.ts +107 -0
  196. package/src/transformations/custom-page.ts +83 -0
  197. package/src/transformations/ecommerce-product.ts +177 -78
  198. package/src/transformations/index.ts +85 -11
  199. package/src/transformations/shared-utils.ts +9 -0
  200. package/src/utils.ts +191 -34
@@ -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()}
@@ -3,6 +3,7 @@ import {
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
5
  generateSearchEscapeHelpersCode,
6
+ generateFilterTreeHelpersCode,
6
7
  } from '../utils'
7
8
  import {
8
9
  generateSortFallbackFieldHelper,
@@ -62,78 +63,67 @@ const getClient = () => {
62
63
 
63
64
  ${generateSafeJSONParseCode()}
64
65
 
66
+ ${generateFilterTreeHelpersCode()}
67
+
65
68
  ${generateSearchEscapeHelpersCode()}
66
69
 
67
- // Helper function to process filters and build conditions
70
+ // Builds one SQL clause for the whole filter tree, so an OR group nested in
71
+ // the root AND keeps its meaning instead of being flattened into ANDs.
68
72
  const processFilters = (filters, conditions, queryParams, paramIndex) => {
69
73
  if (!filters) return paramIndex
70
74
 
71
- const parsedFilters = safeJSONParse(filters)
75
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
76
+ if (!filterTree) return paramIndex
72
77
 
73
- if (Array.isArray(parsedFilters)) {
74
- parsedFilters.forEach((filter) => {
75
- if (!filter.source || filter.destination === undefined) return
76
-
77
- const field = filter.source
78
- const value = filter.destination
79
- const operand = filter.operand || '='
80
-
81
- if (Array.isArray(value)) {
82
- if (value.length === 0) return
83
- if (operand === 'array_overlap') {
84
- conditions.push(\`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`)
85
- queryParams.push(value.map((entry) => String(entry)))
86
- paramIndex++
87
- return
88
- }
89
- const placeholders = value.map(() => \`$\${paramIndex++}\`)
90
- queryParams.push(...value)
91
- if (operand === '!=') {
92
- conditions.push(\`\${field} NOT IN (\${placeholders.join(', ')})\`)
93
- } else {
94
- conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
95
- }
96
- } else {
97
- if (operand === 'array_overlap') {
98
- if (value === '' || value === null || value === undefined) return
99
- // A single comma-joined string (the multi-select Category Filter's
100
- // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
101
- const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
102
- if (overlapValues.length === 0) return
103
- conditions.push(\`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`)
104
- queryParams.push(overlapValues)
105
- paramIndex++
106
- return
107
- }
108
- if (value === null) {
109
- if (operand === '=') {
110
- conditions.push(\`\${field} IS NULL\`)
111
- } else if (operand === '!=') {
112
- conditions.push(\`\${field} IS NOT NULL\`)
113
- }
114
- } else {
115
- const validOps = ['=', '!=', '>', '<', '>=', '<=']
116
- const sqlOperator = validOps.includes(operand) ? operand : '='
117
- conditions.push(\`\${field} \${sqlOperator} $\${paramIndex}\`)
118
- queryParams.push(value)
119
- paramIndex++
120
- }
121
- }
122
- })
123
- } else {
124
- Object.entries(parsedFilters).forEach(([key, value]) => {
125
- if (Array.isArray(value)) {
126
- const placeholders = value.map(() => \`$\${paramIndex++}\`)
127
- queryParams.push(...value)
128
- conditions.push(\`\${key} IN (\${placeholders.join(', ')})\`)
129
- } else {
130
- conditions.push(\`\${key} = $\${paramIndex}\`)
131
- queryParams.push(value)
78
+ const buildCondition = (condition) => {
79
+ const field = condition.source
80
+ const value = condition.destination
81
+ const operand = condition.operand
82
+
83
+ if (Array.isArray(value)) {
84
+ if (value.length === 0) return null
85
+ if (operand === 'array_overlap') {
86
+ const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
87
+ queryParams.push(value.map((entry) => String(entry)))
132
88
  paramIndex++
89
+ return clause
133
90
  }
134
- })
91
+ const placeholders = value.map(() => \`$\${paramIndex++}\`)
92
+ queryParams.push(...value)
93
+ return operand === '!='
94
+ ? \`\${field} NOT IN (\${placeholders.join(', ')})\`
95
+ : \`\${field} IN (\${placeholders.join(', ')})\`
96
+ }
97
+
98
+ if (operand === 'array_overlap') {
99
+ if (value === '' || value === null || value === undefined) return null
100
+ // A single comma-joined string (the multi-select Category Filter's
101
+ // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
102
+ const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
103
+ if (overlapValues.length === 0) return null
104
+ const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
105
+ queryParams.push(overlapValues)
106
+ paramIndex++
107
+ return clause
108
+ }
109
+
110
+ if (value === null) {
111
+ if (operand === '=') return \`\${field} IS NULL\`
112
+ if (operand === '!=') return \`\${field} IS NOT NULL\`
113
+ return null
114
+ }
115
+
116
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
117
+ const sqlOperator = validOps.includes(operand) ? operand : '='
118
+ const clause = \`\${field} \${sqlOperator} $\${paramIndex}\`
119
+ queryParams.push(value)
120
+ paramIndex++
121
+ return clause
135
122
  }
136
123
 
124
+ const clause = buildFilterTreeClause(filterTree, buildCondition)
125
+ if (clause) conditions.push(clause)
126
+
137
127
  return paramIndex
138
128
  }
139
129
 
@@ -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 validateRedisConfig = (
@@ -69,6 +70,8 @@ export const generateRedisFetcher = (config: Record<string, unknown>): string =>
69
70
 
70
71
  ${generateSafeJSONParseCode()}
71
72
 
73
+ ${generateFilterTreeHelpersCode()}
74
+
72
75
  ${generateDateFormatterCode()}
73
76
 
74
77
  export default async function handler(req, res) {
@@ -88,14 +91,12 @@ export default async function handler(req, res) {
88
91
 
89
92
  // Extract pattern from filters if available (new format)
90
93
  if (filters) {
91
- const parsedFilters = safeJSONParse(filters)
92
- if (Array.isArray(parsedFilters)) {
93
- const patternFilter = parsedFilters.find(f => f.source === 'pattern')
94
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
95
+ if (filterTree) {
96
+ const patternFilter = flattenFilterConditions(filterTree).find((f) => f.source === 'pattern')
94
97
  if (patternFilter) {
95
98
  pattern = patternFilter.destination || pattern
96
99
  }
97
- } else {
98
- pattern = parsedFilters.pattern || pattern
99
100
  }
100
101
  }
101
102