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

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 (134) hide show
  1. package/__tests__/collection-path.test.ts +191 -0
  2. package/__tests__/count-collection-path.test.ts +59 -0
  3. package/__tests__/filter-groups.test.ts +174 -0
  4. package/dist/cjs/count-fetchers.d.ts.map +1 -1
  5. package/dist/cjs/count-fetchers.js +1 -1
  6. package/dist/cjs/count-fetchers.js.map +1 -1
  7. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  8. package/dist/cjs/fetchers/airtable.js +1 -1
  9. package/dist/cjs/fetchers/airtable.js.map +1 -1
  10. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  11. package/dist/cjs/fetchers/clickhouse.js +1 -1
  12. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  13. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  14. package/dist/cjs/fetchers/csv-file.js +3 -3
  15. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  16. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  17. package/dist/cjs/fetchers/firestore.js +1 -1
  18. package/dist/cjs/fetchers/firestore.js.map +1 -1
  19. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/google-sheets.js +1 -1
  21. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  22. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  23. package/dist/cjs/fetchers/javascript.js +2 -2
  24. package/dist/cjs/fetchers/javascript.js.map +1 -1
  25. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/mariadb.js +1 -1
  27. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  28. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  29. package/dist/cjs/fetchers/mongodb.js +1 -1
  30. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  31. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  32. package/dist/cjs/fetchers/mysql.js +1 -1
  33. package/dist/cjs/fetchers/mysql.js.map +1 -1
  34. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  35. package/dist/cjs/fetchers/postgresql.js +1 -1
  36. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  37. package/dist/cjs/fetchers/redis.d.ts.map +1 -1
  38. package/dist/cjs/fetchers/redis.js +1 -1
  39. package/dist/cjs/fetchers/redis.js.map +1 -1
  40. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  41. package/dist/cjs/fetchers/redshift.js +2 -2
  42. package/dist/cjs/fetchers/redshift.js.map +1 -1
  43. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  44. package/dist/cjs/fetchers/rest-api.js +11 -3
  45. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  46. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/supabase.js +1 -1
  48. package/dist/cjs/fetchers/supabase.js.map +1 -1
  49. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  50. package/dist/cjs/fetchers/teleport.js +1 -1
  51. package/dist/cjs/fetchers/teleport.js.map +1 -1
  52. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  53. package/dist/cjs/fetchers/turso.js +1 -1
  54. package/dist/cjs/fetchers/turso.js.map +1 -1
  55. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  56. package/dist/cjs/utils.d.ts +47 -0
  57. package/dist/cjs/utils.d.ts.map +1 -1
  58. package/dist/cjs/utils.js +134 -18
  59. package/dist/cjs/utils.js.map +1 -1
  60. package/dist/esm/count-fetchers.d.ts.map +1 -1
  61. package/dist/esm/count-fetchers.js +1 -1
  62. package/dist/esm/count-fetchers.js.map +1 -1
  63. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  64. package/dist/esm/fetchers/airtable.js +2 -2
  65. package/dist/esm/fetchers/airtable.js.map +1 -1
  66. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  67. package/dist/esm/fetchers/clickhouse.js +2 -2
  68. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  69. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  70. package/dist/esm/fetchers/csv-file.js +4 -4
  71. package/dist/esm/fetchers/csv-file.js.map +1 -1
  72. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  73. package/dist/esm/fetchers/firestore.js +2 -2
  74. package/dist/esm/fetchers/firestore.js.map +1 -1
  75. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  76. package/dist/esm/fetchers/google-sheets.js +2 -2
  77. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  78. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  79. package/dist/esm/fetchers/javascript.js +3 -3
  80. package/dist/esm/fetchers/javascript.js.map +1 -1
  81. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  82. package/dist/esm/fetchers/mariadb.js +2 -2
  83. package/dist/esm/fetchers/mariadb.js.map +1 -1
  84. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  85. package/dist/esm/fetchers/mongodb.js +2 -2
  86. package/dist/esm/fetchers/mongodb.js.map +1 -1
  87. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  88. package/dist/esm/fetchers/mysql.js +2 -2
  89. package/dist/esm/fetchers/mysql.js.map +1 -1
  90. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  91. package/dist/esm/fetchers/postgresql.js +2 -2
  92. package/dist/esm/fetchers/postgresql.js.map +1 -1
  93. package/dist/esm/fetchers/redis.d.ts.map +1 -1
  94. package/dist/esm/fetchers/redis.js +2 -2
  95. package/dist/esm/fetchers/redis.js.map +1 -1
  96. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  97. package/dist/esm/fetchers/redshift.js +3 -3
  98. package/dist/esm/fetchers/redshift.js.map +1 -1
  99. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  100. package/dist/esm/fetchers/rest-api.js +12 -4
  101. package/dist/esm/fetchers/rest-api.js.map +1 -1
  102. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  103. package/dist/esm/fetchers/supabase.js +2 -2
  104. package/dist/esm/fetchers/supabase.js.map +1 -1
  105. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  106. package/dist/esm/fetchers/teleport.js +2 -2
  107. package/dist/esm/fetchers/teleport.js.map +1 -1
  108. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  109. package/dist/esm/fetchers/turso.js +2 -2
  110. package/dist/esm/fetchers/turso.js.map +1 -1
  111. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  112. package/dist/esm/utils.d.ts +47 -0
  113. package/dist/esm/utils.d.ts.map +1 -1
  114. package/dist/esm/utils.js +129 -17
  115. package/dist/esm/utils.js.map +1 -1
  116. package/package.json +5 -5
  117. package/src/count-fetchers.ts +6 -2
  118. package/src/fetchers/airtable.ts +35 -36
  119. package/src/fetchers/clickhouse.ts +26 -36
  120. package/src/fetchers/csv-file.ts +13 -21
  121. package/src/fetchers/firestore.ts +41 -35
  122. package/src/fetchers/google-sheets.ts +15 -23
  123. package/src/fetchers/javascript.ts +22 -25
  124. package/src/fetchers/mariadb.ts +33 -44
  125. package/src/fetchers/mongodb.ts +66 -81
  126. package/src/fetchers/mysql.ts +33 -45
  127. package/src/fetchers/postgresql.ts +52 -62
  128. package/src/fetchers/redis.ts +6 -5
  129. package/src/fetchers/redshift.ts +29 -41
  130. package/src/fetchers/rest-api.ts +29 -22
  131. package/src/fetchers/supabase.ts +104 -41
  132. package/src/fetchers/teleport.ts +53 -64
  133. package/src/fetchers/turso.ts +27 -39
  134. package/src/utils.ts +227 -31
@@ -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
 
@@ -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
 
@@ -56,6 +57,8 @@ const getClient = () => {
56
57
 
57
58
  ${generateSafeJSONParseCode()}
58
59
 
60
+ ${generateFilterTreeHelpersCode()}
61
+
59
62
  ${generateSearchEscapeHelpersCode()}
60
63
 
61
64
  ${generateDateFormatterCode()}
@@ -119,54 +122,39 @@ export default async function handler(req, res) {
119
122
  }
120
123
 
121
124
  if (filters) {
122
- const parsedFilters = safeJSONParse(filters)
125
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
123
126
 
124
- if (Array.isArray(parsedFilters)) {
125
- parsedFilters.forEach((filter) => {
126
- if (!filter.source || filter.destination === undefined) return
127
-
128
- const field = sanitizeIdentifier(filter.source)
129
- const value = filter.destination
130
- const operand = filter.operand || '='
127
+ if (filterTree) {
128
+ const buildCondition = (condition) => {
129
+ const field = sanitizeIdentifier(condition.source)
130
+ const value = condition.destination
131
+ const operand = condition.operand
131
132
 
132
133
  if (Array.isArray(value)) {
133
- if (value.length === 0) return
134
+ if (value.length === 0) return null
134
135
  const placeholders = value.map(() => \`$\${paramIndex++}\`)
135
136
  queryParams.push(...value)
136
- if (operand === '!=') {
137
- conditions.push(\`\${field} NOT IN (\${placeholders.join(', ')})\`)
138
- } else {
139
- conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
140
- }
141
- } else {
142
- if (value === null) {
143
- if (operand === '=') {
144
- conditions.push(\`\${field} IS NULL\`)
145
- } else if (operand === '!=') {
146
- conditions.push(\`\${field} IS NOT NULL\`)
147
- }
148
- } else {
149
- const validOps = ['=', '!=', '>', '<', '>=', '<=']
150
- const sqlOperator = validOps.includes(operand) ? operand : '='
151
- conditions.push(\`\${field} \${sqlOperator} $\${paramIndex}\`)
152
- queryParams.push(value)
153
- paramIndex++
154
- }
137
+ return operand === '!='
138
+ ? \`\${field} NOT IN (\${placeholders.join(', ')})\`
139
+ : \`\${field} IN (\${placeholders.join(', ')})\`
155
140
  }
156
- })
157
- } else {
158
- Object.entries(parsedFilters).forEach(([key, value]) => {
159
- const field = sanitizeIdentifier(key)
160
- if (Array.isArray(value)) {
161
- const placeholders = value.map(() => \`$\${paramIndex++}\`)
162
- queryParams.push(...value)
163
- conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
164
- } else {
165
- conditions.push(\`\${field} = $\${paramIndex}\`)
166
- queryParams.push(value)
167
- paramIndex++
141
+
142
+ if (value === null) {
143
+ if (operand === '=') return \`\${field} IS NULL\`
144
+ if (operand === '!=') return \`\${field} IS NOT NULL\`
145
+ return null
168
146
  }
169
- })
147
+
148
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
149
+ const sqlOperator = validOps.includes(operand) ? operand : '='
150
+ const clause = \`\${field} \${sqlOperator} $\${paramIndex}\`
151
+ queryParams.push(value)
152
+ paramIndex++
153
+ return clause
154
+ }
155
+
156
+ const clause = buildFilterTreeClause(filterTree, buildCondition)
157
+ if (clause) conditions.push(clause)
170
158
  }
171
159
  }
172
160
 
@@ -1,7 +1,9 @@
1
1
  import {
2
+ generateCollectionPathHelperCode,
2
3
  generateDateFormatterCode,
3
4
  generateSortFilterHelperCode,
4
5
  generateSafeJSONParseCode,
6
+ generateFilterTreeHelpersCode,
5
7
  } from '../utils'
6
8
 
7
9
  export const validateRESTAPIConfig = (
@@ -73,6 +75,7 @@ interface RESTAPIConfig {
73
75
  headers?: Record<string, string>
74
76
  authorization?: Authorization
75
77
  bodyType?: string
78
+ body?: string
76
79
  }
77
80
 
78
81
  export const generateRESTAPIFetcher = (config: Record<string, unknown>): string => {
@@ -87,9 +90,13 @@ ${generateDateFormatterCode()}
87
90
 
88
91
  ${generateSortFilterHelperCode()}
89
92
 
93
+ ${generateFilterTreeHelpersCode()}
94
+
95
+ ${generateCollectionPathHelperCode()}
96
+
90
97
  export default async function handler(req, res) {
91
98
  try {
92
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
99
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset, collectionPath } = req.query
93
100
 
94
101
  const url = ${JSON.stringify(restConfig.url)}
95
102
  const method = ${JSON.stringify(restConfig.method || 'GET')}
@@ -111,6 +118,17 @@ export default async function handler(req, res) {
111
118
  ? 'JSON.stringify(req.body)'
112
119
  : 'req.body'
113
120
  }
121
+ }${
122
+ // A server-side call (getStaticProps -> fetchData -> handler) has no
123
+ // request body, so without this the configured body was silently dropped
124
+ // and the endpoint was called with none — while the editor, which always
125
+ // sends it, showed the rows the body produces. Only fills the gap: a
126
+ // caller that DID supply a body still wins, above.
127
+ restConfig.body
128
+ ? ` else {
129
+ options.body = ${JSON.stringify(restConfig.body)}
130
+ }`
131
+ : ''
114
132
  }
115
133
  `
116
134
  : ''
@@ -128,6 +146,8 @@ export default async function handler(req, res) {
128
146
 
129
147
  let data = await response.json()
130
148
 
149
+ data = resolveCollectionPath(data, collectionPath)
150
+
131
151
  if (Array.isArray(data)) {
132
152
  if (query && query.trim()) {
133
153
  const searchQuery = query.toLowerCase()
@@ -160,17 +180,14 @@ export default async function handler(req, res) {
160
180
 
161
181
  if (filters) {
162
182
  try {
163
- const parsedFilters = safeJSONParse(filters)
183
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
164
184
 
165
- if (Array.isArray(parsedFilters)) {
166
- data = data.filter((item) => {
167
- return parsedFilters.every((filter) => {
168
- if (!filter.source || filter.destination === undefined) return true
169
-
170
- const field = filter.source
171
- const value = getNestedValue(item, field)
172
- const target = filter.destination
173
- const operand = filter.operand || '='
185
+ if (filterTree) {
186
+ data = data.filter((item) =>
187
+ evaluateFilterTree(filterTree, (condition) => {
188
+ const value = getNestedValue(item, condition.source)
189
+ const target = condition.destination
190
+ const operand = condition.operand
174
191
 
175
192
  if (Array.isArray(target)) {
176
193
  if (operand === '!=') {
@@ -181,17 +198,7 @@ export default async function handler(req, res) {
181
198
 
182
199
  return compareValues(value, target, operand)
183
200
  })
184
- })
185
- } else {
186
- data = data.filter((item) => {
187
- return Object.entries(parsedFilters).every(([key, value]) => {
188
- const itemValue = getNestedValue(item, key)
189
- if (Array.isArray(value)) {
190
- return value.includes(itemValue)
191
- }
192
- return compareValues(itemValue, value, '=')
193
- })
194
- })
201
+ )
195
202
  }
196
203
  } catch (err) {
197
204
  console.error('Error parsing filters:', err)
@@ -3,6 +3,7 @@ import {
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
5
  generateSearchEscapeHelpersCode,
6
+ generateFilterTreeHelpersCode,
6
7
  } from '../utils'
7
8
 
8
9
  export const validateSupabaseConfig = (
@@ -127,6 +128,8 @@ const getClient = () => {
127
128
 
128
129
  ${generateSafeJSONParseCode()}
129
130
 
131
+ ${generateFilterTreeHelpersCode()}
132
+
130
133
  ${generateSearchEscapeHelpersCode()}
131
134
 
132
135
  // Helper function to process filter values
@@ -144,12 +147,12 @@ const applyFilters = (queryRef, filters) => {
144
147
  const parsedFilters = safeJSONParse(filters)
145
148
 
146
149
  if (Array.isArray(parsedFilters)) {
147
- parsedFilters.forEach((filter) => {
148
- if (!filter.source || filter.destination === undefined) return
149
-
150
- const field = filter.source
151
- const value = filter.destination
152
- const operand = filter.operand || '='
150
+ const filterTree = normalizeFilterTree(parsedFilters)
151
+
152
+ const applyCondition = (condition) => {
153
+ const field = condition.source
154
+ const value = condition.destination
155
+ const operand = condition.operand
153
156
 
154
157
  if (Array.isArray(value)) {
155
158
  const processedValues = value.map(processFilterValue)
@@ -158,43 +161,103 @@ const applyFilters = (queryRef, filters) => {
158
161
  } else {
159
162
  queryRef = queryRef.in(field, processedValues)
160
163
  }
161
- } else {
162
- const processedValue = processFilterValue(value)
163
-
164
- // Handle null values
165
- if (processedValue === null) {
166
- if (operand === '=') {
167
- queryRef = queryRef.is(field, null)
168
- } else if (operand === '!=') {
169
- queryRef = queryRef.not(field, 'is', null)
170
- }
171
- } else {
172
- // Map operand to Supabase methods
173
- switch (operand) {
174
- case '=':
175
- queryRef = queryRef.eq(field, processedValue)
176
- break
177
- case '!=':
178
- queryRef = queryRef.neq(field, processedValue)
179
- break
180
- case '>':
181
- queryRef = queryRef.gt(field, processedValue)
182
- break
183
- case '>=':
184
- queryRef = queryRef.gte(field, processedValue)
185
- break
186
- case '<':
187
- queryRef = queryRef.lt(field, processedValue)
188
- break
189
- case '<=':
190
- queryRef = queryRef.lte(field, processedValue)
191
- break
192
- default:
193
- queryRef = queryRef.eq(field, processedValue)
194
- }
164
+ return
165
+ }
166
+
167
+ const processedValue = processFilterValue(value)
168
+
169
+ // Handle null values
170
+ if (processedValue === null) {
171
+ if (operand === '=') {
172
+ queryRef = queryRef.is(field, null)
173
+ } else if (operand === '!=') {
174
+ queryRef = queryRef.not(field, 'is', null)
195
175
  }
176
+ return
196
177
  }
197
- })
178
+
179
+ // Map operand to Supabase methods
180
+ switch (operand) {
181
+ case '!=':
182
+ queryRef = queryRef.neq(field, processedValue)
183
+ break
184
+ case '>':
185
+ queryRef = queryRef.gt(field, processedValue)
186
+ break
187
+ case '>=':
188
+ queryRef = queryRef.gte(field, processedValue)
189
+ break
190
+ case '<':
191
+ queryRef = queryRef.lt(field, processedValue)
192
+ break
193
+ case '<=':
194
+ queryRef = queryRef.lte(field, processedValue)
195
+ break
196
+ default:
197
+ queryRef = queryRef.eq(field, processedValue)
198
+ }
199
+ }
200
+
201
+ // An OR group cannot be expressed by chaining (chained filters are ANDed),
202
+ // so it is emitted as one PostgREST or=(...) expression instead.
203
+ const POSTGREST_OPERANDS = { '=': 'eq', '!=': 'neq', '>': 'gt', '>=': 'gte', '<': 'lt', '<=': 'lte' }
204
+
205
+ const toExprValue = (value) => {
206
+ if (value === null || value === undefined) return 'null'
207
+ const asString = String(value)
208
+ return /[,.:()"\\s]/.test(asString) ? '"' + asString.replace(/"/g, '\\\\"') + '"' : asString
209
+ }
210
+
211
+ const conditionToExpr = (condition) => {
212
+ const field = condition.source
213
+ const value = condition.destination
214
+ const operand = condition.operand
215
+
216
+ if (Array.isArray(value)) {
217
+ const list = '(' + value.map((entry) => toExprValue(processFilterValue(entry))).join(',') + ')'
218
+ return operand === '!=' ? field + '.not.in.' + list : field + '.in.' + list
219
+ }
220
+
221
+ const processedValue = processFilterValue(value)
222
+ if (processedValue === null) {
223
+ return operand === '!=' ? field + '.not.is.null' : field + '.is.null'
224
+ }
225
+
226
+ return field + '.' + (POSTGREST_OPERANDS[operand] || 'eq') + '.' + toExprValue(processedValue)
227
+ }
228
+
229
+ const nodeToExpr = (node) => {
230
+ if (node.type === 'group') {
231
+ const parts = node.children.map(nodeToExpr).filter(Boolean)
232
+ if (parts.length === 0) return null
233
+ if (parts.length === 1) return parts[0]
234
+ return (node.operator === 'or' ? 'or' : 'and') + '(' + parts.join(',') + ')'
235
+ }
236
+ return conditionToExpr(node)
237
+ }
238
+
239
+ const applyNode = (node) => {
240
+ if (node.type !== 'group') {
241
+ applyCondition(node)
242
+ return
243
+ }
244
+ if (node.operator === 'and') {
245
+ node.children.forEach(applyNode)
246
+ return
247
+ }
248
+ if (node.children.length === 1) {
249
+ applyNode(node.children[0])
250
+ return
251
+ }
252
+ const parts = node.children.map(nodeToExpr).filter(Boolean)
253
+ if (parts.length > 0) {
254
+ queryRef = queryRef.or(parts.join(','))
255
+ }
256
+ }
257
+
258
+ if (filterTree) {
259
+ applyNode(filterTree)
260
+ }
198
261
  } else {
199
262
  // Old format: object with key-value pairs (backward compatibility)
200
263
  Object.entries(parsedFilters).forEach(([key, value]) => {
@@ -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
  import {
@@ -146,81 +147,69 @@ const getClient = () => {
146
147
 
147
148
  ${generateSafeJSONParseCode()}
148
149
 
150
+ ${generateFilterTreeHelpersCode()}
151
+
149
152
  ${generateSearchEscapeHelpersCode()}
150
153
  ${getTransformationCode(tableName, transformOptions)}
151
154
  ${getTransformWrapperCode(tableName)}
152
155
  const processFilters = (filters, conditions, queryParams, paramIndex) => {
153
156
  if (!filters) return paramIndex
154
157
 
155
- const parsedFilters = safeJSONParse(filters)
158
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
159
+ if (!filterTree) return paramIndex
156
160
 
157
- if (Array.isArray(parsedFilters)) {
158
- parsedFilters.forEach((filter) => {
159
- if (!filter.source || filter.destination === undefined) return
160
-
161
- const field = filter.source
162
- const value = filter.destination
163
- const operand = filter.operand || '='
164
-
165
- if (Array.isArray(value)) {
166
- if (value.length === 0) return
167
- if (operand === 'array_overlap') {
168
- // Row column is a JSON array (e.g. category_ids). Match when it
169
- // shares any element with the destination set. jsonb ?| (function
170
- // form) avoids the '?' placeholder-token ambiguity some drivers hit.
171
- conditions.push(\`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`)
172
- queryParams.push(value.map((entry) => String(entry)))
173
- paramIndex++
174
- return
175
- }
176
- const placeholders = value.map(() => \`$\${paramIndex++}\`)
177
- queryParams.push(...value)
178
- if (operand === '!=') {
179
- conditions.push(\`\${field} NOT IN (\${placeholders.join(', ')})\`)
180
- } else {
181
- conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
182
- }
183
- } else {
184
- if (operand === 'array_overlap') {
185
- if (value === '' || value === null || value === undefined) return
186
- // A single comma-joined string (the multi-select Category Filter's
187
- // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
188
- const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
189
- if (overlapValues.length === 0) return
190
- conditions.push(\`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`)
191
- queryParams.push(overlapValues)
192
- paramIndex++
193
- return
194
- }
195
- if (value === null) {
196
- if (operand === '=') {
197
- conditions.push(\`\${field} IS NULL\`)
198
- } else if (operand === '!=') {
199
- conditions.push(\`\${field} IS NOT NULL\`)
200
- }
201
- } else {
202
- const validOps = ['=', '!=', '>', '<', '>=', '<=']
203
- const sqlOperator = validOps.includes(operand) ? operand : '='
204
- conditions.push(\`\${field} \${sqlOperator} $\${paramIndex}\`)
205
- queryParams.push(value)
206
- paramIndex++
207
- }
208
- }
209
- })
210
- } else {
211
- Object.entries(parsedFilters).forEach(([key, value]) => {
212
- if (Array.isArray(value)) {
213
- const placeholders = value.map(() => \`$\${paramIndex++}\`)
214
- queryParams.push(...value)
215
- conditions.push(\`\${key} IN (\${placeholders.join(', ')})\`)
216
- } else {
217
- conditions.push(\`\${key} = $\${paramIndex}\`)
218
- queryParams.push(value)
161
+ const buildCondition = (condition) => {
162
+ const field = condition.source
163
+ const value = condition.destination
164
+ const operand = condition.operand
165
+
166
+ if (Array.isArray(value)) {
167
+ if (value.length === 0) return null
168
+ if (operand === 'array_overlap') {
169
+ // Row column is a JSON array (e.g. category_ids). Match when it
170
+ // shares any element with the destination set. jsonb ?| (function
171
+ // form) avoids the '?' placeholder-token ambiguity some drivers hit.
172
+ const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
173
+ queryParams.push(value.map((entry) => String(entry)))
219
174
  paramIndex++
175
+ return clause
220
176
  }
221
- })
177
+ const placeholders = value.map(() => \`$\${paramIndex++}\`)
178
+ queryParams.push(...value)
179
+ return operand === '!='
180
+ ? \`\${field} NOT IN (\${placeholders.join(', ')})\`
181
+ : \`\${field} IN (\${placeholders.join(', ')})\`
182
+ }
183
+
184
+ if (operand === 'array_overlap') {
185
+ if (value === '' || value === null || value === undefined) return null
186
+ // A single comma-joined string (the multi-select Category Filter's
187
+ // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
188
+ const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
189
+ if (overlapValues.length === 0) return null
190
+ const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
191
+ queryParams.push(overlapValues)
192
+ paramIndex++
193
+ return clause
194
+ }
195
+
196
+ if (value === null) {
197
+ if (operand === '=') return \`\${field} IS NULL\`
198
+ if (operand === '!=') return \`\${field} IS NOT NULL\`
199
+ return null
200
+ }
201
+
202
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
203
+ const sqlOperator = validOps.includes(operand) ? operand : '='
204
+ const clause = \`\${field} \${sqlOperator} $\${paramIndex}\`
205
+ queryParams.push(value)
206
+ paramIndex++
207
+ return clause
222
208
  }
223
209
 
210
+ const clause = buildFilterTreeClause(filterTree, buildCondition)
211
+ if (clause) conditions.push(clause)
212
+
224
213
  return paramIndex
225
214
  }
226
215
 
@@ -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
 
@@ -42,6 +43,8 @@ export const generateTursoFetcher = (
42
43
 
43
44
  ${generateSafeJSONParseCode()}
44
45
 
46
+ ${generateFilterTreeHelpersCode()}
47
+
45
48
  ${generateSearchEscapeHelpersCode()}
46
49
 
47
50
  ${generateDateFormatterCode()}
@@ -92,52 +95,37 @@ export default async function handler(req, res) {
92
95
  }
93
96
 
94
97
  if (filters) {
95
- const parsedFilters = safeJSONParse(filters)
98
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
96
99
 
97
- if (Array.isArray(parsedFilters)) {
98
- parsedFilters.forEach((filter) => {
99
- if (!filter.source || filter.destination === undefined) return
100
-
101
- const field = sanitizeIdentifier(filter.source)
102
- const value = filter.destination
103
- const operand = filter.operand || '='
100
+ if (filterTree) {
101
+ const buildCondition = (condition) => {
102
+ const field = sanitizeIdentifier(condition.source)
103
+ const value = condition.destination
104
+ const operand = condition.operand
104
105
 
105
106
  if (Array.isArray(value)) {
106
- if (value.length === 0) return
107
+ if (value.length === 0) return null
107
108
  const placeholders = value.map(() => '?').join(', ')
108
109
  queryParams.push(...value)
109
- if (operand === '!=') {
110
- whereClauses.push(\`\${field} NOT IN (\${placeholders})\`)
111
- } else {
112
- whereClauses.push(\`\${field} IN (\${placeholders})\`)
113
- }
114
- } else {
115
- if (value === null) {
116
- if (operand === '=') {
117
- whereClauses.push(\`\${field} IS NULL\`)
118
- } else if (operand === '!=') {
119
- whereClauses.push(\`\${field} IS NOT NULL\`)
120
- }
121
- } else {
122
- const validOps = ['=', '!=', '>', '<', '>=', '<=']
123
- const sqlOperator = validOps.includes(operand) ? operand : '='
124
- whereClauses.push(\`\${field} \${sqlOperator} ?\`)
125
- queryParams.push(value)
126
- }
110
+ return operand === '!='
111
+ ? \`\${field} NOT IN (\${placeholders})\`
112
+ : \`\${field} IN (\${placeholders})\`
127
113
  }
128
- })
129
- } else {
130
- Object.entries(parsedFilters).forEach(([key, value]) => {
131
- const field = sanitizeIdentifier(key)
132
- if (Array.isArray(value)) {
133
- const placeholders = value.map(() => '?').join(', ')
134
- queryParams.push(...value)
135
- whereClauses.push(\`\${field} IN (\${placeholders})\`)
136
- } else {
137
- whereClauses.push(\`\${field} = ?\`)
138
- queryParams.push(value)
114
+
115
+ if (value === null) {
116
+ if (operand === '=') return \`\${field} IS NULL\`
117
+ if (operand === '!=') return \`\${field} IS NOT NULL\`
118
+ return null
139
119
  }
140
- })
120
+
121
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
122
+ const sqlOperator = validOps.includes(operand) ? operand : '='
123
+ queryParams.push(value)
124
+ return \`\${field} \${sqlOperator} ?\`
125
+ }
126
+
127
+ const clause = buildFilterTreeClause(filterTree, buildCondition)
128
+ if (clause) whereClauses.push(clause)
141
129
  }
142
130
  }
143
131