@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
@@ -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
 
@@ -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
 
@@ -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 validateRESTAPIConfig = (
@@ -87,6 +88,8 @@ ${generateDateFormatterCode()}
87
88
 
88
89
  ${generateSortFilterHelperCode()}
89
90
 
91
+ ${generateFilterTreeHelpersCode()}
92
+
90
93
  export default async function handler(req, res) {
91
94
  try {
92
95
  const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
@@ -160,17 +163,14 @@ export default async function handler(req, res) {
160
163
 
161
164
  if (filters) {
162
165
  try {
163
- const parsedFilters = safeJSONParse(filters)
166
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
164
167
 
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 || '='
168
+ if (filterTree) {
169
+ data = data.filter((item) =>
170
+ evaluateFilterTree(filterTree, (condition) => {
171
+ const value = getNestedValue(item, condition.source)
172
+ const target = condition.destination
173
+ const operand = condition.operand
174
174
 
175
175
  if (Array.isArray(target)) {
176
176
  if (operand === '!=') {
@@ -181,17 +181,7 @@ export default async function handler(req, res) {
181
181
 
182
182
  return compareValues(value, target, operand)
183
183
  })
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
- })
184
+ )
195
185
  }
196
186
  } catch (err) {
197
187
  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