@teleporthq/teleport-plugin-next-data-source 0.42.34 → 0.43.0

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 (239) hide show
  1. package/__tests__/ecommerce-product-out-of-stock.test.ts +112 -0
  2. package/__tests__/fetchers.test.ts +0 -42
  3. package/__tests__/filter-utils.test.ts +149 -0
  4. package/__tests__/mocks.ts +0 -12
  5. package/__tests__/utils.test.ts +0 -2
  6. package/dist/cjs/array-mapper-registry.d.ts +2 -0
  7. package/dist/cjs/array-mapper-registry.d.ts.map +1 -1
  8. package/dist/cjs/array-mapper-registry.js +9 -1
  9. package/dist/cjs/array-mapper-registry.js.map +1 -1
  10. package/dist/cjs/count-fetchers.d.ts +2 -2
  11. package/dist/cjs/count-fetchers.d.ts.map +1 -1
  12. package/dist/cjs/count-fetchers.js +5 -5
  13. package/dist/cjs/count-fetchers.js.map +1 -1
  14. package/dist/cjs/data-source-fetchers.d.ts +2 -1
  15. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  16. package/dist/cjs/data-source-fetchers.js +11 -9
  17. package/dist/cjs/data-source-fetchers.js.map +1 -1
  18. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  19. package/dist/cjs/fetchers/airtable.js +1 -1
  20. package/dist/cjs/fetchers/airtable.js.map +1 -1
  21. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/clickhouse.js +1 -1
  23. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  24. package/dist/cjs/fetchers/csv-file.js +1 -1
  25. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  26. package/dist/cjs/fetchers/firestore.js +1 -1
  27. package/dist/cjs/fetchers/firestore.js.map +1 -1
  28. package/dist/cjs/fetchers/google-sheets.js +1 -1
  29. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  30. package/dist/cjs/fetchers/index.d.ts +2 -1
  31. package/dist/cjs/fetchers/index.d.ts.map +1 -1
  32. package/dist/cjs/fetchers/index.js +8 -5
  33. package/dist/cjs/fetchers/index.js.map +1 -1
  34. package/dist/cjs/fetchers/javascript.js +1 -1
  35. package/dist/cjs/fetchers/javascript.js.map +1 -1
  36. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/mariadb.js +3 -3
  38. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  39. package/dist/cjs/fetchers/mongodb.js +1 -1
  40. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  41. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  42. package/dist/cjs/fetchers/mysql.js +2 -2
  43. package/dist/cjs/fetchers/mysql.js.map +1 -1
  44. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  45. package/dist/cjs/fetchers/postgresql.js +2 -2
  46. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  47. package/dist/cjs/fetchers/raw-query.d.ts +18 -0
  48. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -0
  49. package/dist/cjs/fetchers/raw-query.js +70 -0
  50. package/dist/cjs/fetchers/raw-query.js.map +1 -0
  51. package/dist/cjs/fetchers/redis.js +1 -1
  52. package/dist/cjs/fetchers/redis.js.map +1 -1
  53. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  54. package/dist/cjs/fetchers/redshift.js +2 -2
  55. package/dist/cjs/fetchers/redshift.js.map +1 -1
  56. package/dist/cjs/fetchers/rest-api.js +1 -1
  57. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  58. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  59. package/dist/cjs/fetchers/supabase.js +62 -2
  60. package/dist/cjs/fetchers/supabase.js.map +1 -1
  61. package/dist/cjs/fetchers/teleport.d.ts +7 -0
  62. package/dist/cjs/fetchers/teleport.d.ts.map +1 -0
  63. package/dist/cjs/fetchers/teleport.js +63 -0
  64. package/dist/cjs/fetchers/teleport.js.map +1 -0
  65. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  66. package/dist/cjs/fetchers/turso.js +1 -1
  67. package/dist/cjs/fetchers/turso.js.map +1 -1
  68. package/dist/cjs/filter-utils.d.ts +13 -0
  69. package/dist/cjs/filter-utils.d.ts.map +1 -0
  70. package/dist/cjs/filter-utils.js +95 -0
  71. package/dist/cjs/filter-utils.js.map +1 -0
  72. package/dist/cjs/index.d.ts.map +1 -1
  73. package/dist/cjs/index.js +112 -9
  74. package/dist/cjs/index.js.map +1 -1
  75. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  76. package/dist/cjs/pagination-plugin.js +389 -128
  77. package/dist/cjs/pagination-plugin.js.map +1 -1
  78. package/dist/cjs/sort-utils.d.ts +10 -0
  79. package/dist/cjs/sort-utils.d.ts.map +1 -0
  80. package/dist/cjs/sort-utils.js +141 -0
  81. package/dist/cjs/sort-utils.js.map +1 -0
  82. package/dist/cjs/transformations/blog-post.d.ts +7 -0
  83. package/dist/cjs/transformations/blog-post.d.ts.map +1 -0
  84. package/dist/cjs/transformations/blog-post.js +13 -0
  85. package/dist/cjs/transformations/blog-post.js.map +1 -0
  86. package/dist/cjs/transformations/ecommerce-product.d.ts +7 -0
  87. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -0
  88. package/dist/cjs/transformations/ecommerce-product.js +13 -0
  89. package/dist/cjs/transformations/ecommerce-product.js.map +1 -0
  90. package/dist/cjs/transformations/index.d.ts +26 -0
  91. package/dist/cjs/transformations/index.d.ts.map +1 -0
  92. package/dist/cjs/transformations/index.js +81 -0
  93. package/dist/cjs/transformations/index.js.map +1 -0
  94. package/dist/cjs/transformations/shared-utils.d.ts +7 -0
  95. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -0
  96. package/dist/cjs/transformations/shared-utils.js +13 -0
  97. package/dist/cjs/transformations/shared-utils.js.map +1 -0
  98. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  99. package/dist/cjs/utils.d.ts +30 -1
  100. package/dist/cjs/utils.d.ts.map +1 -1
  101. package/dist/cjs/utils.js +173 -10
  102. package/dist/cjs/utils.js.map +1 -1
  103. package/dist/esm/array-mapper-registry.d.ts +2 -0
  104. package/dist/esm/array-mapper-registry.d.ts.map +1 -1
  105. package/dist/esm/array-mapper-registry.js +9 -1
  106. package/dist/esm/array-mapper-registry.js.map +1 -1
  107. package/dist/esm/count-fetchers.d.ts +2 -2
  108. package/dist/esm/count-fetchers.d.ts.map +1 -1
  109. package/dist/esm/count-fetchers.js +4 -4
  110. package/dist/esm/count-fetchers.js.map +1 -1
  111. package/dist/esm/data-source-fetchers.d.ts +2 -1
  112. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  113. package/dist/esm/data-source-fetchers.js +10 -9
  114. package/dist/esm/data-source-fetchers.js.map +1 -1
  115. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  116. package/dist/esm/fetchers/airtable.js +1 -1
  117. package/dist/esm/fetchers/airtable.js.map +1 -1
  118. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  119. package/dist/esm/fetchers/clickhouse.js +1 -1
  120. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  121. package/dist/esm/fetchers/csv-file.js +1 -1
  122. package/dist/esm/fetchers/csv-file.js.map +1 -1
  123. package/dist/esm/fetchers/firestore.js +1 -1
  124. package/dist/esm/fetchers/firestore.js.map +1 -1
  125. package/dist/esm/fetchers/google-sheets.js +1 -1
  126. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  127. package/dist/esm/fetchers/index.d.ts +2 -1
  128. package/dist/esm/fetchers/index.d.ts.map +1 -1
  129. package/dist/esm/fetchers/index.js +2 -1
  130. package/dist/esm/fetchers/index.js.map +1 -1
  131. package/dist/esm/fetchers/javascript.js +1 -1
  132. package/dist/esm/fetchers/javascript.js.map +1 -1
  133. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  134. package/dist/esm/fetchers/mariadb.js +4 -4
  135. package/dist/esm/fetchers/mariadb.js.map +1 -1
  136. package/dist/esm/fetchers/mongodb.js +1 -1
  137. package/dist/esm/fetchers/mongodb.js.map +1 -1
  138. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  139. package/dist/esm/fetchers/mysql.js +3 -3
  140. package/dist/esm/fetchers/mysql.js.map +1 -1
  141. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  142. package/dist/esm/fetchers/postgresql.js +3 -3
  143. package/dist/esm/fetchers/postgresql.js.map +1 -1
  144. package/dist/esm/fetchers/raw-query.d.ts +18 -0
  145. package/dist/esm/fetchers/raw-query.d.ts.map +1 -0
  146. package/dist/esm/fetchers/raw-query.js +65 -0
  147. package/dist/esm/fetchers/raw-query.js.map +1 -0
  148. package/dist/esm/fetchers/redis.js +1 -1
  149. package/dist/esm/fetchers/redis.js.map +1 -1
  150. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  151. package/dist/esm/fetchers/redshift.js +3 -3
  152. package/dist/esm/fetchers/redshift.js.map +1 -1
  153. package/dist/esm/fetchers/rest-api.js +1 -1
  154. package/dist/esm/fetchers/rest-api.js.map +1 -1
  155. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  156. package/dist/esm/fetchers/supabase.js +63 -3
  157. package/dist/esm/fetchers/supabase.js.map +1 -1
  158. package/dist/esm/fetchers/teleport.d.ts +7 -0
  159. package/dist/esm/fetchers/teleport.d.ts.map +1 -0
  160. package/dist/esm/fetchers/teleport.js +57 -0
  161. package/dist/esm/fetchers/teleport.js.map +1 -0
  162. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  163. package/dist/esm/fetchers/turso.js +2 -2
  164. package/dist/esm/fetchers/turso.js.map +1 -1
  165. package/dist/esm/filter-utils.d.ts +13 -0
  166. package/dist/esm/filter-utils.d.ts.map +1 -0
  167. package/dist/esm/filter-utils.js +66 -0
  168. package/dist/esm/filter-utils.js.map +1 -0
  169. package/dist/esm/index.d.ts.map +1 -1
  170. package/dist/esm/index.js +113 -10
  171. package/dist/esm/index.js.map +1 -1
  172. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  173. package/dist/esm/pagination-plugin.js +389 -128
  174. package/dist/esm/pagination-plugin.js.map +1 -1
  175. package/dist/esm/sort-utils.d.ts +10 -0
  176. package/dist/esm/sort-utils.d.ts.map +1 -0
  177. package/dist/esm/sort-utils.js +113 -0
  178. package/dist/esm/sort-utils.js.map +1 -0
  179. package/dist/esm/transformations/blog-post.d.ts +7 -0
  180. package/dist/esm/transformations/blog-post.d.ts.map +1 -0
  181. package/dist/esm/transformations/blog-post.js +9 -0
  182. package/dist/esm/transformations/blog-post.js.map +1 -0
  183. package/dist/esm/transformations/ecommerce-product.d.ts +7 -0
  184. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -0
  185. package/dist/esm/transformations/ecommerce-product.js +9 -0
  186. package/dist/esm/transformations/ecommerce-product.js.map +1 -0
  187. package/dist/esm/transformations/index.d.ts +26 -0
  188. package/dist/esm/transformations/index.d.ts.map +1 -0
  189. package/dist/esm/transformations/index.js +74 -0
  190. package/dist/esm/transformations/index.js.map +1 -0
  191. package/dist/esm/transformations/shared-utils.d.ts +7 -0
  192. package/dist/esm/transformations/shared-utils.d.ts.map +1 -0
  193. package/dist/esm/transformations/shared-utils.js +9 -0
  194. package/dist/esm/transformations/shared-utils.js.map +1 -0
  195. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  196. package/dist/esm/utils.d.ts +30 -1
  197. package/dist/esm/utils.d.ts.map +1 -1
  198. package/dist/esm/utils.js +170 -9
  199. package/dist/esm/utils.js.map +1 -1
  200. package/package.json +6 -5
  201. package/src/array-mapper-registry.ts +13 -0
  202. package/src/count-fetchers.ts +5 -5
  203. package/src/data-source-fetchers.ts +15 -11
  204. package/src/fetchers/airtable.ts +54 -8
  205. package/src/fetchers/clickhouse.ts +25 -19
  206. package/src/fetchers/csv-file.ts +2 -2
  207. package/src/fetchers/firestore.ts +2 -2
  208. package/src/fetchers/google-sheets.ts +2 -2
  209. package/src/fetchers/index.ts +6 -5
  210. package/src/fetchers/javascript.ts +2 -2
  211. package/src/fetchers/mariadb.ts +27 -12
  212. package/src/fetchers/mongodb.ts +2 -2
  213. package/src/fetchers/mysql.ts +27 -12
  214. package/src/fetchers/postgresql.ts +31 -18
  215. package/src/fetchers/raw-query.ts +178 -0
  216. package/src/fetchers/redis.ts +2 -2
  217. package/src/fetchers/redshift.ts +14 -10
  218. package/src/fetchers/rest-api.ts +2 -2
  219. package/src/fetchers/supabase.ts +97 -14
  220. package/src/fetchers/teleport.ts +485 -0
  221. package/src/fetchers/turso.ts +15 -7
  222. package/src/filter-utils.ts +111 -0
  223. package/src/index.ts +146 -6
  224. package/src/pagination-plugin.ts +547 -308
  225. package/src/sort-utils.ts +150 -0
  226. package/src/transformations/blog-post.ts +128 -0
  227. package/src/transformations/ecommerce-product.ts +173 -0
  228. package/src/transformations/index.ts +97 -0
  229. package/src/transformations/shared-utils.ts +271 -0
  230. package/src/utils.ts +227 -11
  231. package/dist/cjs/fetchers/static-collection.d.ts +0 -7
  232. package/dist/cjs/fetchers/static-collection.d.ts.map +0 -1
  233. package/dist/cjs/fetchers/static-collection.js +0 -25
  234. package/dist/cjs/fetchers/static-collection.js.map +0 -1
  235. package/dist/esm/fetchers/static-collection.d.ts +0 -7
  236. package/dist/esm/fetchers/static-collection.d.ts.map +0 -1
  237. package/dist/esm/fetchers/static-collection.js +0 -19
  238. package/dist/esm/fetchers/static-collection.js.map +0 -1
  239. package/src/fetchers/static-collection.ts +0 -231
@@ -42,12 +42,42 @@ ${generateSafeJSONParseCode()}
42
42
 
43
43
  ${generateDateFormatterCode()}
44
44
 
45
+ // Escape a string literal for Airtable formula grammar. Strings use
46
+ // single quotes; embedded single-quote must be backslash-escaped.
47
+ // Backslash itself also escapes.
48
+ const escapeAirtableString = (s) => {
49
+ return "'" + String(s).replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'") + "'"
50
+ }
51
+
52
+ // Escape an Airtable field reference name for use inside curly braces.
53
+ // Only close-brace and backslash are special inside the braces.
54
+ const escapeAirtableFieldRef = (name) => {
55
+ return String(name).replace(/\\\\/g, '\\\\\\\\').replace(/}/g, '\\\\}')
56
+ }
57
+
58
+ const buildAirtableSearchFormula = (rawQuery, rawQueryColumns) => {
59
+ if (typeof rawQuery !== 'string' || rawQuery.trim() === '') return ''
60
+ let cols = []
61
+ if (rawQueryColumns) {
62
+ const parsed = safeJSONParse(rawQueryColumns)
63
+ cols = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])
64
+ }
65
+ const validCols = cols.filter((c) => typeof c === 'string' && c.length > 0)
66
+ if (validCols.length === 0) return ''
67
+ const literal = escapeAirtableString(rawQuery)
68
+ const parts = validCols.map(
69
+ (col) => 'SEARCH(' + literal + ', {' + escapeAirtableFieldRef(col) + '})'
70
+ )
71
+ return parts.length === 1 ? parts[0] : 'OR(' + parts.join(',') + ')'
72
+ }
73
+
45
74
  export default async function handler(req, res) {
46
75
  try {
47
- const { query, view, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query
48
-
76
+ const { query, queryColumns, view, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query
77
+
49
78
  const queryParams = new URLSearchParams()
50
-
79
+ const formulaParts = []
80
+
51
81
  if (view) {
52
82
  queryParams.append('view', view)
53
83
  }
@@ -59,12 +89,12 @@ export default async function handler(req, res) {
59
89
  parsedSorts.forEach((sort, index) => {
60
90
  if (!sort.field) return
61
91
  queryParams.append(\`sort[\${index}][field]\`, sort.field)
62
- queryParams.append(\`sort[\${index}][direction]\`, sort.order?.toLowerCase() === 'desc' ? 'desc' : 'asc')
92
+ queryParams.append(\`sort[\${index}][direction]\`, (sort.order || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')
63
93
  })
64
94
  }
65
95
  } else if (sortBy) {
66
96
  queryParams.append('sort[0][field]', sortBy)
67
- queryParams.append('sort[0][direction]', sortOrder || 'asc')
97
+ queryParams.append('sort[0][direction]', (sortOrder || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')
68
98
  }
69
99
 
70
100
  const perPageValue = limit || perPage || 100
@@ -114,7 +144,7 @@ export default async function handler(req, res) {
114
144
 
115
145
  if (conditions.length > 0) {
116
146
  const filterFormula = conditions.length > 1 ? \`AND(\${conditions.join(',')})\` : conditions[0]
117
- queryParams.append('filterByFormula', filterFormula)
147
+ formulaParts.push(filterFormula)
118
148
  }
119
149
  } else {
120
150
  const conditions = Object.entries(parsedFilters).map(([field, value]) => {
@@ -127,13 +157,29 @@ export default async function handler(req, res) {
127
157
  return \`{\${field}}=\${formatAirtableValue(value)}\`
128
158
  }
129
159
  })
130
-
160
+
131
161
  const filterFormula = conditions.length > 1 ? \`AND(\${conditions.join(',')})\` : conditions[0]
132
162
  if (filterFormula) {
133
- queryParams.append('filterByFormula', filterFormula)
163
+ formulaParts.push(filterFormula)
134
164
  }
135
165
  }
136
166
  }
167
+
168
+ // Apply the { query, queryColumns } search contract via the
169
+ // Airtable SEARCH() function. Search-term string literal escaping
170
+ // protects against single-quote injection; field-ref escaping
171
+ // protects against close-brace or backslash breaking out of the
172
+ // {field} reference.
173
+ const searchFormula = buildAirtableSearchFormula(query, queryColumns)
174
+ if (searchFormula) {
175
+ formulaParts.push(searchFormula)
176
+ }
177
+
178
+ if (formulaParts.length === 1) {
179
+ queryParams.append('filterByFormula', formulaParts[0])
180
+ } else if (formulaParts.length > 1) {
181
+ queryParams.append('filterByFormula', 'AND(' + formulaParts.join(',') + ')')
182
+ }
137
183
 
138
184
  let url = \`https://api.airtable.com/v0/${baseId}/\${encodeURIComponent('${tableName}')}\`
139
185
  if (queryParams.toString()) {
@@ -68,28 +68,13 @@ export default async function handler(req, res) {
68
68
 
69
69
  const conditions = []
70
70
 
71
- if (query) {
72
- if (queryColumns) {
73
- const parsed = safeJSONParse(queryColumns)
74
- const columns = Array.isArray(parsed) ? parsed : [parsed]
75
- const searchConditions = columns.map(
76
- (col) => \`positionCaseInsensitive(toString(\${col}), '\${query}') > 0\`
77
- )
78
- conditions.push(\`(\${searchConditions.join(' OR ')})\`)
79
- } else {
80
- // Note: Without queryColumns, ClickHouse can't search all columns efficiently
81
- // Users should provide queryColumns for optimal search performance
82
- console.warn('Search query provided without queryColumns - search may not work as expected')
83
- }
84
- }
85
-
86
71
  const formatClickHouseValue = (value) => {
87
72
  if (value === null || value === undefined) return 'NULL'
88
73
  if (typeof value === 'string') return \`'\${value.replace(/'/g, "\\\\'")}'\`
89
74
  if (typeof value === 'boolean') return value ? '1' : '0'
90
75
  return String(value)
91
76
  }
92
-
77
+
93
78
  // Helper to sanitize identifier (prevent SQL injection in column names)
94
79
  const sanitizeIdentifier = (name) => {
95
80
  // Only allow alphanumeric and underscore
@@ -98,6 +83,27 @@ export default async function handler(req, res) {
98
83
  }
99
84
  return \`\\\`\${name}\\\`\`
100
85
  }
86
+
87
+ if (query) {
88
+ if (queryColumns) {
89
+ const parsed = safeJSONParse(queryColumns)
90
+ const columns = Array.isArray(parsed) ? parsed : [parsed]
91
+ // Escape the search term into a ClickHouse string literal so
92
+ // single quotes and backslashes can't break out of the literal
93
+ // or execute injected SQL. Column identifiers are sanitized
94
+ // against the identifier regex above.
95
+ const queryLiteral = formatClickHouseValue(String(query))
96
+ const searchConditions = columns.map(
97
+ (col) =>
98
+ \`positionCaseInsensitiveUTF8(toString(\${sanitizeIdentifier(col)}), \${queryLiteral}) > 0\`
99
+ )
100
+ conditions.push(\`(\${searchConditions.join(' OR ')})\`)
101
+ } else {
102
+ // Note: Without queryColumns, ClickHouse can't search all columns efficiently
103
+ // Users should provide queryColumns for optimal search performance
104
+ console.warn('Search query provided without queryColumns - search may not work as expected')
105
+ }
106
+ }
101
107
 
102
108
  if (filters) {
103
109
  const parsedFilters = safeJSONParse(filters)
@@ -157,16 +163,16 @@ export default async function handler(req, res) {
157
163
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
158
164
  const orderClauses = parsedSorts.map((sort) => {
159
165
  if (!sort.field) return null
160
- const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
166
+ const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
161
167
  return \`\${sanitizeIdentifier(sort.field)} \${order}\`
162
168
  }).filter(Boolean)
163
-
169
+
164
170
  if (orderClauses.length > 0) {
165
171
  sql += \` ORDER BY \${orderClauses.join(', ')}\`
166
172
  }
167
173
  }
168
174
  } else if (sortBy) {
169
- sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${sortOrder?.toUpperCase() || 'ASC'}\`
175
+ sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
170
176
  }
171
177
 
172
178
  const limitValue = limit || perPage
@@ -131,7 +131,7 @@ const generateHandlerBody = (): string => {
131
131
  const field = labelToIdMap[sort.field] || sort.field
132
132
  const aVal = getNestedValue(a, field)
133
133
  const bVal = getNestedValue(b, field)
134
- const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
134
+ const sortOrderValue = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1
135
135
 
136
136
  let comparison = 0
137
137
  if (aVal === null || aVal === undefined) {
@@ -159,7 +159,7 @@ const generateHandlerBody = (): string => {
159
159
  filteredData.sort((a, b) => {
160
160
  const aVal = getNestedValue(a, field)
161
161
  const bVal = getNestedValue(b, field)
162
- const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
162
+ const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1
163
163
 
164
164
  let comparison = 0
165
165
  if (aVal === null || aVal === undefined) {
@@ -150,12 +150,12 @@ export default async function handler(req, res) {
150
150
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
151
151
  parsedSorts.forEach((sort) => {
152
152
  if (!sort.field) return
153
- const order = sort.order?.toLowerCase() === 'desc' ? 'desc' : 'asc'
153
+ const order = (sort.order || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc'
154
154
  queryRef = queryRef.orderBy(sort.field, order)
155
155
  })
156
156
  }
157
157
  } else if (sortBy) {
158
- const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? 'desc' : 'asc'
158
+ const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc'
159
159
  queryRef = queryRef.orderBy(sortBy, sortOrderValue)
160
160
  }
161
161
 
@@ -328,7 +328,7 @@ export default async function handler(req, res) {
328
328
  const field = labelToIdMap[sort.field] || sort.field
329
329
  const aVal = getNestedValue(a, field)
330
330
  const bVal = getNestedValue(b, field)
331
- const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
331
+ const sortOrderValue = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1
332
332
 
333
333
  let comparison = 0
334
334
  if (aVal === null || aVal === undefined) {
@@ -357,7 +357,7 @@ export default async function handler(req, res) {
357
357
  const field = labelToIdMap[sortBy] || sortBy
358
358
  const aVal = getNestedValue(a, field)
359
359
  const bVal = getNestedValue(b, field)
360
- const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
360
+ const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1
361
361
 
362
362
  let comparison = 0
363
363
  if (aVal === null || aVal === undefined) {
@@ -24,9 +24,10 @@ export {
24
24
  validateJavaScriptConfig,
25
25
  } from './javascript'
26
26
  export { generateCSVFileFetcher, generateCSVCountFetcher, validateCSVConfig } from './csv-file'
27
- export {
28
- generateStaticCollectionFetcher,
29
- generateStaticCollectionCountFetcher,
30
- validateStaticCollectionConfig,
31
- } from './static-collection'
32
27
  export { generateGoogleSheetsFetcher, validateGoogleSheetsConfig } from './google-sheets'
28
+ export {
29
+ generateTeleportFetcher,
30
+ generateTeleportCountFetcher,
31
+ validateTeleportConfig,
32
+ } from './teleport'
33
+ export { generateRawQueryFetcher, parseQueryTemplateVariables } from './raw-query'
@@ -134,7 +134,7 @@ export default async function handler(req, res) {
134
134
  if (!sort.field) continue
135
135
  const aVal = getNestedValue(a, sort.field)
136
136
  const bVal = getNestedValue(b, sort.field)
137
- const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
137
+ const sortOrderValue = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1
138
138
 
139
139
  let comparison = 0
140
140
  if (aVal === null || aVal === undefined) {
@@ -164,7 +164,7 @@ export default async function handler(req, res) {
164
164
  data.sort((a, b) => {
165
165
  const aVal = getNestedValue(a, sortBy)
166
166
  const bVal = getNestedValue(b, sortBy)
167
- const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
167
+ const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1
168
168
 
169
169
  let comparison = 0
170
170
  if (aVal === null || aVal === undefined) {
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateSearchEscapeHelpersCode,
5
6
  } from '../utils'
6
7
 
7
8
  interface MariaDBConfig {
@@ -26,6 +27,8 @@ export const generateMariaDBFetcher = (
26
27
 
27
28
  ${generateSafeJSONParseCode()}
28
29
 
30
+ ${generateSearchEscapeHelpersCode()}
31
+
29
32
  // Helper function to process filters and build conditions
30
33
  const processFilters = (filters, conditions, queryParams) => {
31
34
  if (!filters) return
@@ -121,8 +124,10 @@ export default async function handler(req, res) {
121
124
  let columns = []
122
125
 
123
126
  if (queryColumns) {
124
- // Use specified columns
125
- columns = safeJSONParse(queryColumns)
127
+ // Use specified columns. Wrap non-arrays so a single column
128
+ // passed as a bare string doesn't get iterated as chars.
129
+ const parsed = safeJSONParse(queryColumns)
130
+ columns = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])
126
131
  } else {
127
132
  // Fallback: Get all columns from information_schema
128
133
  try {
@@ -140,12 +145,16 @@ export default async function handler(req, res) {
140
145
  }
141
146
 
142
147
  if (columns.length > 0) {
143
- const searchConditions = columns.map((col) => \`CAST(\\\`\${col}\\\` AS CHAR) LIKE ?\`)
144
- columns.forEach(() => queryParams.push(\`%\${query}%\`))
145
- conditions.push(\`(\${searchConditions.join(' OR ')})\`)
148
+ const pattern = "%" + escapeLikePattern(query) + "%"
149
+ const searchConditions = columns.map(
150
+ (col) =>
151
+ "LOWER(CAST(\`" + sanitizeSearchIdentifier(col) + "\` AS CHAR)) LIKE LOWER(?) ESCAPE '|'"
152
+ )
153
+ columns.forEach(() => queryParams.push(pattern))
154
+ conditions.push("(" + searchConditions.join(" OR ") + ")")
146
155
  }
147
156
  }
148
-
157
+
149
158
  // Apply filters using helper function
150
159
  processFilters(filters, conditions, queryParams)
151
160
 
@@ -161,16 +170,16 @@ export default async function handler(req, res) {
161
170
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
162
171
  const orderClauses = parsedSorts.map((sort) => {
163
172
  if (!sort.field) return null
164
- const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
173
+ const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
165
174
  return \`\\\`\${sort.field}\\\` \${order}\`
166
175
  }).filter(Boolean)
167
-
176
+
168
177
  if (orderClauses.length > 0) {
169
178
  sql += \` ORDER BY \${orderClauses.join(', ')}\`
170
179
  }
171
180
  }
172
181
  } else if (sortBy) {
173
- sql += \` ORDER BY \\\`\${sortBy}\\\` \${sortOrder?.toUpperCase() || 'ASC'}\`
182
+ sql += \` ORDER BY \\\`\${sortBy}\\\` \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
174
183
  }
175
184
 
176
185
  const limitValue = limit || perPage
@@ -285,9 +294,15 @@ async function getCount(req, res) {
285
294
  }
286
295
 
287
296
  if (columns.length > 0) {
288
- const searchConditions = columns.map(col => \`CAST(\${col} AS CHAR) LIKE ?\`).join(' OR ')
289
- conditions.push(\`(\${searchConditions})\`)
290
- columns.forEach(() => queryParams.push(\`%\${query}%\`))
297
+ const pattern = "%" + escapeLikePattern(query) + "%"
298
+ const searchConditions = columns
299
+ .map(
300
+ (col) =>
301
+ "LOWER(CAST(\`" + sanitizeSearchIdentifier(col) + "\` AS CHAR)) LIKE LOWER(?) ESCAPE '|'"
302
+ )
303
+ .join(" OR ")
304
+ conditions.push("(" + searchConditions + ")")
305
+ columns.forEach(() => queryParams.push(pattern))
291
306
  }
292
307
  }
293
308
 
@@ -222,7 +222,7 @@ export default async function handler(req, res) {
222
222
  const sortObject = {}
223
223
  parsedSorts.forEach((sort) => {
224
224
  if (sort.field) {
225
- sortObject[sort.field] = sort.order?.toLowerCase() === 'desc' ? -1 : 1
225
+ sortObject[sort.field] = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1
226
226
  }
227
227
  })
228
228
  if (Object.keys(sortObject).length > 0) {
@@ -230,7 +230,7 @@ export default async function handler(req, res) {
230
230
  }
231
231
  }
232
232
  } else if (sortBy) {
233
- const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
233
+ const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1
234
234
  cursor = cursor.sort({ [sortBy]: sortOrderValue })
235
235
  }
236
236
 
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateSearchEscapeHelpersCode,
5
6
  } from '../utils'
6
7
 
7
8
  interface MySQLConfig {
@@ -63,6 +64,8 @@ const getConnection = () => {
63
64
 
64
65
  ${generateSafeJSONParseCode()}
65
66
 
67
+ ${generateSearchEscapeHelpersCode()}
68
+
66
69
  // Helper function to process filters and build conditions
67
70
  const processFilters = (filters, conditions, queryParams) => {
68
71
  if (!filters) return
@@ -131,8 +134,10 @@ export default async function handler(req, res) {
131
134
  let columns = []
132
135
 
133
136
  if (queryColumns) {
134
- // Use specified columns
135
- columns = safeJSONParse(queryColumns)
137
+ // Use specified columns. Wrap non-arrays so a single column
138
+ // passed as a bare string doesn't get iterated as chars.
139
+ const parsed = safeJSONParse(queryColumns)
140
+ columns = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])
136
141
  } else {
137
142
  // Fallback: Get all columns from information_schema
138
143
  try {
@@ -150,12 +155,16 @@ export default async function handler(req, res) {
150
155
  }
151
156
 
152
157
  if (columns.length > 0) {
153
- const searchConditions = columns.map((col) => \`CAST(\${mysql.escapeId(col)} AS CHAR) LIKE ?\`)
154
- columns.forEach(() => queryParams.push(\`%\${query}%\`))
155
- conditions.push(\`(\${searchConditions.join(' OR ')})\`)
158
+ const pattern = "%" + escapeLikePattern(query) + "%"
159
+ const searchConditions = columns.map(
160
+ (col) =>
161
+ "LOWER(CAST(" + mysql.escapeId(sanitizeSearchIdentifier(col)) + " AS CHAR)) LIKE LOWER(?) ESCAPE '|'"
162
+ )
163
+ columns.forEach(() => queryParams.push(pattern))
164
+ conditions.push("(" + searchConditions.join(" OR ") + ")")
156
165
  }
157
166
  }
158
-
167
+
159
168
  // Apply filters using helper function
160
169
  processFilters(filters, conditions, queryParams)
161
170
 
@@ -171,16 +180,16 @@ export default async function handler(req, res) {
171
180
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
172
181
  const orderClauses = parsedSorts.map((sort) => {
173
182
  if (!sort.field) return null
174
- const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
183
+ const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
175
184
  return \`\${mysql.escapeId(sort.field)} \${order}\`
176
185
  }).filter(Boolean)
177
-
186
+
178
187
  if (orderClauses.length > 0) {
179
188
  sql += \` ORDER BY \${orderClauses.join(', ')}\`
180
189
  }
181
190
  }
182
191
  } else if (sortBy) {
183
- sql += \` ORDER BY \${mysql.escapeId(sortBy)} \${sortOrder?.toUpperCase() || 'ASC'}\`
192
+ sql += \` ORDER BY \${mysql.escapeId(sortBy)} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
184
193
  }
185
194
 
186
195
  const limitValue = limit || perPage
@@ -265,9 +274,15 @@ async function getCount(req, res) {
265
274
  }
266
275
 
267
276
  if (columns.length > 0) {
268
- const searchConditions = columns.map(col => \`CAST(\${col} AS CHAR) LIKE ?\`).join(' OR ')
269
- conditions.push(\`(\${searchConditions})\`)
270
- columns.forEach(() => queryParams.push(\`%\${query}%\`))
277
+ const pattern = "%" + escapeLikePattern(query) + "%"
278
+ const searchConditions = columns
279
+ .map(
280
+ (col) =>
281
+ "LOWER(CAST(" + mysql.escapeId(sanitizeSearchIdentifier(col)) + " AS CHAR)) LIKE LOWER(?) ESCAPE '|'"
282
+ )
283
+ .join(" OR ")
284
+ conditions.push("(" + searchConditions + ")")
285
+ columns.forEach(() => queryParams.push(pattern))
271
286
  }
272
287
  }
273
288
 
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateSearchEscapeHelpersCode,
5
6
  } from '../utils'
6
7
 
7
8
  interface PostgreSQLConfig {
@@ -56,6 +57,8 @@ const getClient = () => {
56
57
 
57
58
  ${generateSafeJSONParseCode()}
58
59
 
60
+ ${generateSearchEscapeHelpersCode()}
61
+
59
62
  // Helper function to process filters and build conditions
60
63
  const processFilters = (filters, conditions, queryParams, paramIndex) => {
61
64
  if (!filters) return paramIndex
@@ -131,14 +134,16 @@ export default async function handler(req, res) {
131
134
  let columns = []
132
135
 
133
136
  if (queryColumns) {
134
- // Use specified columns
135
- columns = safeJSONParse(queryColumns)
137
+ // Use specified columns. Wrap non-arrays so that a single
138
+ // column passed as a bare string doesn't get iterated as chars.
139
+ const parsed = safeJSONParse(queryColumns)
140
+ columns = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])
136
141
  } else {
137
142
  // Fallback: Get all columns from information_schema
138
143
  try {
139
144
  const schemaQuery = \`
140
- SELECT column_name
141
- FROM information_schema.columns
145
+ SELECT column_name
146
+ FROM information_schema.columns
142
147
  WHERE table_name = $1
143
148
  ${schema ? `AND table_schema = $2` : ''}
144
149
  ORDER BY ordinal_position
@@ -156,16 +161,17 @@ export default async function handler(req, res) {
156
161
  }
157
162
 
158
163
  if (columns.length > 0) {
159
- const searchConditions = columns.map((col) => {
160
- const condition = \`\${col}::text ILIKE $\${paramIndex}\`
161
- paramIndex++
162
- return condition
163
- })
164
- columns.forEach(() => queryParams.push(\`%\${query}%\`))
165
- conditions.push(\`(\${searchConditions.join(' OR ')})\`)
164
+ const pattern = '%' + escapeLikePattern(query) + '%'
165
+ const placeholder = '$' + paramIndex
166
+ paramIndex++
167
+ queryParams.push(pattern)
168
+ const searchConditions = columns.map(
169
+ (col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
170
+ )
171
+ conditions.push('(' + searchConditions.join(' OR ') + ')')
166
172
  }
167
173
  }
168
-
174
+
169
175
  // Apply filters using helper function
170
176
  paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
171
177
 
@@ -181,16 +187,16 @@ export default async function handler(req, res) {
181
187
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
182
188
  const orderClauses = parsedSorts.map((sort) => {
183
189
  if (!sort.field) return null
184
- const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
190
+ const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
185
191
  return \`\${sort.field} \${order}\`
186
192
  }).filter(Boolean)
187
-
193
+
188
194
  if (orderClauses.length > 0) {
189
195
  sql += \` ORDER BY \${orderClauses.join(', ')}\`
190
196
  }
191
197
  }
192
198
  } else if (sortBy) {
193
- sql += \` ORDER BY \${sortBy} \${sortOrder?.toUpperCase() || 'ASC'}\`
199
+ sql += \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
194
200
  }
195
201
 
196
202
  const limitValue = limit || perPage
@@ -286,9 +292,16 @@ async function getCount(req, res) {
286
292
  }
287
293
 
288
294
  if (columns.length > 0) {
289
- const searchConditions = columns.map(col => \`\${col}::text ILIKE $\${paramIndex++}\`).join(' OR ')
290
- conditions.push(\`(\${searchConditions})\`)
291
- columns.forEach(() => queryParams.push(\`%\${query}%\`))
295
+ const pattern = '%' + escapeLikePattern(query) + '%'
296
+ const placeholder = '$' + paramIndex
297
+ paramIndex++
298
+ queryParams.push(pattern)
299
+ const searchConditions = columns
300
+ .map(
301
+ (col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
302
+ )
303
+ .join(' OR ')
304
+ conditions.push('(' + searchConditions + ')')
292
305
  }
293
306
  }
294
307