@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
@@ -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,13 +2,14 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateFilterTreeHelpersCode,
5
6
  generateSearchEscapeHelpersCode,
6
7
  } from '../utils'
7
8
  import {
8
9
  getTransformationCode,
9
10
  getTransformExpression,
10
11
  getTransformWrapperCode,
11
- type EcommerceProductTransformOptions,
12
+ type EntityTransformOptions,
12
13
  } from '../transformations'
13
14
  import {
14
15
  generateSortFallbackFieldHelper,
@@ -57,7 +58,7 @@ export const validateTeleportConfig = (
57
58
  export const generateTeleportFetcher = (
58
59
  config: Record<string, unknown>,
59
60
  tableName: string,
60
- transformOptions: EcommerceProductTransformOptions = {}
61
+ transformOptions: EntityTransformOptions = {}
61
62
  ): string => {
62
63
  const dbConfig = config as TeleportDBConfig
63
64
  const schema = dbConfig.options?.schema
@@ -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
 
package/src/index.ts CHANGED
@@ -1403,4 +1403,4 @@ export * from './count-fetchers'
1403
1403
  // tax rate) — every package that emits a data-source fetcher has to derive
1404
1404
  // them the same way.
1405
1405
  export { buildProductTransformOptions } from './transformations'
1406
- export type { EcommerceProductTransformOptions } from './transformations'
1406
+ export type { EcommerceProductTransformOptions, EntityTransformOptions } from './transformations'
@@ -10,10 +10,7 @@ import { ASTStatementOrder, GenericUtils, StringUtils } from '@teleporthq/telepo
10
10
  import { ASTUtils, URLQueryWriter, URLSearchParamSync } from '@teleporthq/teleport-plugin-common'
11
11
  import { generateSafeFileName } from './utils'
12
12
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
13
- import {
14
- buildProductTransformOptions,
15
- type EcommerceProductTransformOptions,
16
- } from './transformations'
13
+ import { buildProductTransformOptions, type EntityTransformOptions } from './transformations'
17
14
  import { appendSortsParam, DynamicSortAST, extractDynamicSort } from './sort-utils'
18
15
  import { appendFiltersParam, pushStateIdsAsDeps, pushPropIdsAsDeps } from './filter-utils'
19
16
  import {
@@ -3223,7 +3220,7 @@ function applyServerCacheToDataSourceModules(
3223
3220
  registry: StateRegistry,
3224
3221
  // tslint:disable-next-line:no-any
3225
3222
  dataSources: Record<string, any>,
3226
- transformOptions: EcommerceProductTransformOptions = {},
3223
+ transformOptions: EntityTransformOptions = {},
3227
3224
  // Accumulated across every page/component this generation has processed. The
3228
3225
  // module is shared project-wide, so merging only the CURRENT page's usages
3229
3226
  // would let page ORDER decide the TTL: a page asking for 300s processed after
@@ -3309,7 +3306,7 @@ function ensureAPIRouteExists(
3309
3306
  extractedResources: any,
3310
3307
  usage: DataSourceUsage,
3311
3308
  dataSources: Record<string, any>,
3312
- transformOptions: EcommerceProductTransformOptions = {}
3309
+ transformOptions: EntityTransformOptions = {}
3313
3310
  ): void {
3314
3311
  // Generate file name for the API route
3315
3312
  const fileName = generateSafeFileName(