@teleporthq/teleport-plugin-next-data-source 0.42.9 → 0.42.10

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 (160) hide show
  1. package/__tests__/csv-header-detection.test.ts +212 -0
  2. package/__tests__/validation.test.ts +33 -2
  3. package/dist/cjs/data-source-fetchers.d.ts +2 -2
  4. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  5. package/dist/cjs/data-source-fetchers.js +30 -7
  6. package/dist/cjs/data-source-fetchers.js.map +1 -1
  7. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  8. package/dist/cjs/fetchers/airtable.js +1 -1
  9. package/dist/cjs/fetchers/airtable.js.map +1 -1
  10. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  11. package/dist/cjs/fetchers/clickhouse.js +1 -1
  12. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  13. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  14. package/dist/cjs/fetchers/csv-file.js +22 -3
  15. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  16. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  17. package/dist/cjs/fetchers/firestore.js +1 -1
  18. package/dist/cjs/fetchers/firestore.js.map +1 -1
  19. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/google-sheets.js +6 -1
  21. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  22. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  23. package/dist/cjs/fetchers/javascript.js +1 -1
  24. package/dist/cjs/fetchers/javascript.js.map +1 -1
  25. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/mariadb.js +3 -3
  27. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  28. package/dist/cjs/fetchers/mongodb.d.ts +1 -1
  29. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  30. package/dist/cjs/fetchers/mongodb.js +11 -3
  31. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  32. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  33. package/dist/cjs/fetchers/mysql.js +2 -2
  34. package/dist/cjs/fetchers/mysql.js.map +1 -1
  35. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  36. package/dist/cjs/fetchers/postgresql.js +3 -3
  37. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  38. package/dist/cjs/fetchers/redis.d.ts.map +1 -1
  39. package/dist/cjs/fetchers/redis.js +1 -1
  40. package/dist/cjs/fetchers/redis.js.map +1 -1
  41. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  42. package/dist/cjs/fetchers/redshift.js +2 -2
  43. package/dist/cjs/fetchers/redshift.js.map +1 -1
  44. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  45. package/dist/cjs/fetchers/rest-api.js +2 -2
  46. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  47. package/dist/cjs/fetchers/static-collection.d.ts.map +1 -1
  48. package/dist/cjs/fetchers/static-collection.js +1 -1
  49. package/dist/cjs/fetchers/static-collection.js.map +1 -1
  50. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  51. package/dist/cjs/fetchers/supabase.js +2 -2
  52. package/dist/cjs/fetchers/supabase.js.map +1 -1
  53. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  54. package/dist/cjs/fetchers/turso.js +1 -1
  55. package/dist/cjs/fetchers/turso.js.map +1 -1
  56. package/dist/cjs/fetchers/utils/header-detection.d.ts +2 -0
  57. package/dist/cjs/fetchers/utils/header-detection.d.ts.map +1 -0
  58. package/dist/cjs/fetchers/utils/header-detection.js +8 -0
  59. package/dist/cjs/fetchers/utils/header-detection.js.map +1 -0
  60. package/dist/cjs/index.d.ts.map +1 -1
  61. package/dist/cjs/index.js +168 -4
  62. package/dist/cjs/index.js.map +1 -1
  63. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  64. package/dist/cjs/pagination-plugin.js +320 -65
  65. package/dist/cjs/pagination-plugin.js.map +1 -1
  66. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  67. package/dist/cjs/utils.d.ts +2 -0
  68. package/dist/cjs/utils.d.ts.map +1 -1
  69. package/dist/cjs/utils.js +214 -46
  70. package/dist/cjs/utils.js.map +1 -1
  71. package/dist/esm/data-source-fetchers.d.ts +2 -2
  72. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  73. package/dist/esm/data-source-fetchers.js +29 -6
  74. package/dist/esm/data-source-fetchers.js.map +1 -1
  75. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  76. package/dist/esm/fetchers/airtable.js +2 -2
  77. package/dist/esm/fetchers/airtable.js.map +1 -1
  78. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  79. package/dist/esm/fetchers/clickhouse.js +2 -2
  80. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  81. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  82. package/dist/esm/fetchers/csv-file.js +23 -4
  83. package/dist/esm/fetchers/csv-file.js.map +1 -1
  84. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  85. package/dist/esm/fetchers/firestore.js +2 -2
  86. package/dist/esm/fetchers/firestore.js.map +1 -1
  87. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  88. package/dist/esm/fetchers/google-sheets.js +6 -1
  89. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  90. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  91. package/dist/esm/fetchers/javascript.js +2 -2
  92. package/dist/esm/fetchers/javascript.js.map +1 -1
  93. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  94. package/dist/esm/fetchers/mariadb.js +4 -4
  95. package/dist/esm/fetchers/mariadb.js.map +1 -1
  96. package/dist/esm/fetchers/mongodb.d.ts +1 -1
  97. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  98. package/dist/esm/fetchers/mongodb.js +12 -4
  99. package/dist/esm/fetchers/mongodb.js.map +1 -1
  100. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  101. package/dist/esm/fetchers/mysql.js +3 -3
  102. package/dist/esm/fetchers/mysql.js.map +1 -1
  103. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  104. package/dist/esm/fetchers/postgresql.js +4 -4
  105. package/dist/esm/fetchers/postgresql.js.map +1 -1
  106. package/dist/esm/fetchers/redis.d.ts.map +1 -1
  107. package/dist/esm/fetchers/redis.js +2 -2
  108. package/dist/esm/fetchers/redis.js.map +1 -1
  109. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  110. package/dist/esm/fetchers/redshift.js +3 -3
  111. package/dist/esm/fetchers/redshift.js.map +1 -1
  112. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  113. package/dist/esm/fetchers/rest-api.js +3 -3
  114. package/dist/esm/fetchers/rest-api.js.map +1 -1
  115. package/dist/esm/fetchers/static-collection.d.ts.map +1 -1
  116. package/dist/esm/fetchers/static-collection.js +2 -2
  117. package/dist/esm/fetchers/static-collection.js.map +1 -1
  118. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  119. package/dist/esm/fetchers/supabase.js +3 -3
  120. package/dist/esm/fetchers/supabase.js.map +1 -1
  121. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  122. package/dist/esm/fetchers/turso.js +2 -2
  123. package/dist/esm/fetchers/turso.js.map +1 -1
  124. package/dist/esm/fetchers/utils/header-detection.d.ts +2 -0
  125. package/dist/esm/fetchers/utils/header-detection.d.ts.map +1 -0
  126. package/dist/esm/fetchers/utils/header-detection.js +4 -0
  127. package/dist/esm/fetchers/utils/header-detection.js.map +1 -0
  128. package/dist/esm/index.d.ts.map +1 -1
  129. package/dist/esm/index.js +169 -5
  130. package/dist/esm/index.js.map +1 -1
  131. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  132. package/dist/esm/pagination-plugin.js +320 -65
  133. package/dist/esm/pagination-plugin.js.map +1 -1
  134. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  135. package/dist/esm/utils.d.ts +2 -0
  136. package/dist/esm/utils.d.ts.map +1 -1
  137. package/dist/esm/utils.js +211 -45
  138. package/dist/esm/utils.js.map +1 -1
  139. package/package.json +2 -2
  140. package/src/data-source-fetchers.ts +29 -13
  141. package/src/fetchers/airtable.ts +78 -30
  142. package/src/fetchers/clickhouse.ts +85 -18
  143. package/src/fetchers/csv-file.ts +254 -29
  144. package/src/fetchers/firestore.ts +62 -12
  145. package/src/fetchers/google-sheets.ts +147 -30
  146. package/src/fetchers/javascript.ts +102 -23
  147. package/src/fetchers/mariadb.ts +82 -25
  148. package/src/fetchers/mongodb.ts +153 -36
  149. package/src/fetchers/mysql.ts +83 -25
  150. package/src/fetchers/postgresql.ts +86 -26
  151. package/src/fetchers/redis.ts +40 -4
  152. package/src/fetchers/redshift.ts +84 -17
  153. package/src/fetchers/rest-api.ts +101 -24
  154. package/src/fetchers/static-collection.ts +96 -18
  155. package/src/fetchers/supabase.ts +175 -53
  156. package/src/fetchers/turso.ts +84 -17
  157. package/src/fetchers/utils/header-detection.ts +200 -0
  158. package/src/index.ts +248 -2
  159. package/src/pagination-plugin.ts +708 -191
  160. package/src/utils.ts +344 -38
@@ -1,4 +1,8 @@
1
- import { replaceSecretReference, generateDateFormatterCode } from '../utils'
1
+ import {
2
+ replaceSecretReference,
3
+ generateDateFormatterCode,
4
+ generateSafeJSONParseCode,
5
+ } from '../utils'
2
6
 
3
7
  export const validateSupabaseConfig = (
4
8
  config: Record<string, unknown>
@@ -61,12 +65,117 @@ const getClient = () => {
61
65
  return client
62
66
  }
63
67
 
68
+ ${generateSafeJSONParseCode()}
69
+
70
+ // Helper function to process filter values
71
+ const processFilterValue = (value) => {
72
+ if (typeof value === 'string' && !isNaN(Number(value))) {
73
+ return Number(value)
74
+ }
75
+ return value
76
+ }
77
+
78
+ // Helper function to apply filters to a query
79
+ const applyFilters = (queryRef, filters) => {
80
+ if (!filters) return queryRef
81
+
82
+ const parsedFilters = safeJSONParse(filters)
83
+
84
+ if (Array.isArray(parsedFilters)) {
85
+ parsedFilters.forEach((filter) => {
86
+ if (!filter.source || filter.destination === undefined) return
87
+
88
+ const field = filter.source
89
+ const value = filter.destination
90
+ const operand = filter.operand || '='
91
+
92
+ if (Array.isArray(value)) {
93
+ const processedValues = value.map(processFilterValue)
94
+ if (operand === '!=') {
95
+ queryRef = queryRef.not(field, 'in', processedValues)
96
+ } else {
97
+ queryRef = queryRef.in(field, processedValues)
98
+ }
99
+ } else {
100
+ const processedValue = processFilterValue(value)
101
+
102
+ // Handle null values
103
+ if (processedValue === null) {
104
+ if (operand === '=') {
105
+ queryRef = queryRef.is(field, null)
106
+ } else if (operand === '!=') {
107
+ queryRef = queryRef.not(field, 'is', null)
108
+ }
109
+ } else {
110
+ // Map operand to Supabase methods
111
+ switch (operand) {
112
+ case '=':
113
+ queryRef = queryRef.eq(field, processedValue)
114
+ break
115
+ case '!=':
116
+ queryRef = queryRef.neq(field, processedValue)
117
+ break
118
+ case '>':
119
+ queryRef = queryRef.gt(field, processedValue)
120
+ break
121
+ case '>=':
122
+ queryRef = queryRef.gte(field, processedValue)
123
+ break
124
+ case '<':
125
+ queryRef = queryRef.lt(field, processedValue)
126
+ break
127
+ case '<=':
128
+ queryRef = queryRef.lte(field, processedValue)
129
+ break
130
+ default:
131
+ queryRef = queryRef.eq(field, processedValue)
132
+ }
133
+ }
134
+ }
135
+ })
136
+ } else {
137
+ // Old format: object with key-value pairs (backward compatibility)
138
+ Object.entries(parsedFilters).forEach(([key, value]) => {
139
+ if (Array.isArray(value)) {
140
+ const processedValues = value.map(processFilterValue)
141
+ queryRef = queryRef.in(key, processedValues)
142
+ } else if (typeof value === 'object' && value !== null) {
143
+ const operator = Object.keys(value)[0]
144
+ let operatorValue = value[operator]
145
+ if (typeof operatorValue === 'string' && !isNaN(Number(operatorValue))) {
146
+ operatorValue = Number(operatorValue)
147
+ }
148
+ switch (operator) {
149
+ case 'eq': queryRef = queryRef.eq(key, operatorValue); break
150
+ case 'neq': queryRef = queryRef.neq(key, operatorValue); break
151
+ case 'gt': queryRef = queryRef.gt(key, operatorValue); break
152
+ case 'gte': queryRef = queryRef.gte(key, operatorValue); break
153
+ case 'lt': queryRef = queryRef.lt(key, operatorValue); break
154
+ case 'lte': queryRef = queryRef.lte(key, operatorValue); break
155
+ case 'like': queryRef = queryRef.like(key, operatorValue); break
156
+ case 'ilike': queryRef = queryRef.ilike(key, operatorValue); break
157
+ case 'in': queryRef = queryRef.in(key, operatorValue); break
158
+ default: queryRef = queryRef.eq(key, operatorValue)
159
+ }
160
+ } else {
161
+ let processedValue = value
162
+ if (typeof value === 'string' && !isNaN(Number(value))) {
163
+ processedValue = Number(value)
164
+ }
165
+ queryRef = queryRef.eq(key, processedValue)
166
+ }
167
+ })
168
+ }
169
+
170
+ return queryRef
171
+ }
172
+
64
173
  ${generateDateFormatterCode()}
65
174
 
66
175
  export default async function handler(req, res) {
67
176
  try {
68
177
  const client = getClient()
69
- const { query, queryColumns, select, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
178
+ const { query, queryColumns, select, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
70
179
 
71
180
  let queryRef = client.from('${tableName}').select(select || '*')
72
181
 
@@ -75,16 +184,34 @@ export default async function handler(req, res) {
75
184
 
76
185
  if (queryColumns) {
77
186
  // Use specified columns
78
- columns = JSON.parse(queryColumns)
187
+ columns = safeJSONParse(queryColumns)
79
188
  } else {
80
- // Fallback: Get all column names from a sample row
189
+ // Fallback: Get text-searchable columns from a sample row
81
190
  try {
82
191
  const { data: sampleData, error: sampleError } = await client.from('${tableName}').select('*').limit(1).single()
83
192
  if (sampleError) {
84
193
  throw sampleError
85
194
  }
86
195
  if (sampleData) {
87
- columns = Object.keys(sampleData)
196
+ // Filter out columns that are likely non-text types
197
+ // Note: This is heuristic-based since we don't have schema info
198
+ columns = Object.keys(sampleData).filter(col => {
199
+ const value = sampleData[col]
200
+ const colLower = col.toLowerCase()
201
+
202
+ // Exclude common timestamp/date column names
203
+ if (colLower.includes('_at') || colLower.includes('date') || colLower === 'timestamp') {
204
+ return false
205
+ }
206
+
207
+ // Exclude if value is a number, boolean, null, or object (non-string)
208
+ if (value === null || value === undefined) {
209
+ return true // Include null columns, let the query handle it
210
+ }
211
+
212
+ const type = typeof value
213
+ return type === 'string' // Only include string values
214
+ })
88
215
  }
89
216
  } catch (schemaError) {
90
217
  console.warn('Failed to fetch sample row for column names:', schemaError.message)
@@ -94,51 +221,29 @@ export default async function handler(req, res) {
94
221
 
95
222
  if (columns.length > 0) {
96
223
  const searchPattern = \`%\${query}%\`
224
+ // Note: Supabase PostgREST doesn't support ::text casting in .or() syntax
225
+ // Only text/varchar columns will match; non-text columns will be skipped
97
226
  const orConditions = columns.map((col) => \`\${col}.ilike.\${searchPattern}\`).join(',')
98
227
  queryRef = queryRef.or(orConditions)
99
228
  }
100
229
  }
101
230
 
102
- if (filters) {
103
- const parsedFilters = JSON.parse(filters)
104
- Object.entries(parsedFilters).forEach(([key, value]) => {
105
- if (Array.isArray(value)) {
106
- const processedValues = value.map((v) => {
107
- if (typeof v === 'string' && !isNaN(Number(v))) {
108
- return Number(v)
109
- }
110
- return v
111
- })
112
- queryRef = queryRef.in(key, processedValues)
113
- } else if (typeof value === 'object' && value !== null) {
114
- const operator = Object.keys(value)[0]
115
- let operatorValue = value[operator]
116
- if (typeof operatorValue === 'string' && !isNaN(Number(operatorValue))) {
117
- operatorValue = Number(operatorValue)
118
- }
119
- switch (operator) {
120
- case 'eq': queryRef = queryRef.eq(key, operatorValue); break
121
- case 'neq': queryRef = queryRef.neq(key, operatorValue); break
122
- case 'gt': queryRef = queryRef.gt(key, operatorValue); break
123
- case 'gte': queryRef = queryRef.gte(key, operatorValue); break
124
- case 'lt': queryRef = queryRef.lt(key, operatorValue); break
125
- case 'lte': queryRef = queryRef.lte(key, operatorValue); break
126
- case 'like': queryRef = queryRef.like(key, operatorValue); break
127
- case 'ilike': queryRef = queryRef.ilike(key, operatorValue); break
128
- case 'in': queryRef = queryRef.in(key, operatorValue); break
129
- default: queryRef = queryRef.eq(key, operatorValue)
130
- }
131
- } else {
132
- let processedValue = value
133
- if (typeof value === 'string' && !isNaN(Number(value))) {
134
- processedValue = Number(value)
135
- }
136
- queryRef = queryRef.eq(key, processedValue)
137
- }
138
- })
139
- }
231
+ // Apply filters using helper function
232
+ queryRef = applyFilters(queryRef, filters)
140
233
 
141
- if (sortBy) {
234
+ // Handle sorts - new array format
235
+ if (sorts) {
236
+ const parsedSorts = safeJSONParse(sorts)
237
+ if (Array.isArray(parsedSorts)) {
238
+ parsedSorts.forEach((sort) => {
239
+ if (sort.field) {
240
+ queryRef = queryRef.order(sort.field, {
241
+ ascending: sort.order?.toLowerCase() !== 'desc'
242
+ })
243
+ }
244
+ })
245
+ }
246
+ } else if (sortBy) {
142
247
  queryRef = queryRef.order(sortBy, { ascending: sortOrder !== 'desc' })
143
248
  }
144
249
 
@@ -195,16 +300,35 @@ async function getCount(req, res) {
195
300
 
196
301
  if (queryColumns) {
197
302
  // Use specified columns
198
- columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
303
+ const parsed = safeJSONParse(queryColumns)
304
+ columns = Array.isArray(parsed) ? parsed : [parsed]
199
305
  } else {
200
- // Fallback: Get all column names from a sample row
306
+ // Fallback: Get text-searchable columns from a sample row
201
307
  try {
202
308
  const { data: sampleData, error: sampleError } = await supabase.from('${tableName}').select('*').limit(1).single()
203
309
  if (sampleError) {
204
310
  throw sampleError
205
311
  }
206
312
  if (sampleData) {
207
- columns = Object.keys(sampleData)
313
+ // Filter out columns that are likely non-text types
314
+ // Note: This is heuristic-based since we don't have schema info
315
+ columns = Object.keys(sampleData).filter(col => {
316
+ const value = sampleData[col]
317
+ const colLower = col.toLowerCase()
318
+
319
+ // Exclude common timestamp/date column names
320
+ if (colLower.includes('_at') || colLower.includes('date') || colLower === 'timestamp') {
321
+ return false
322
+ }
323
+
324
+ // Exclude if value is a number, boolean, null, or object (non-string)
325
+ if (value === null || value === undefined) {
326
+ return true // Include null columns, let the query handle it
327
+ }
328
+
329
+ const type = typeof value
330
+ return type === 'string' // Only include string values
331
+ })
208
332
  }
209
333
  } catch (schemaError) {
210
334
  console.warn('Failed to fetch sample row for column names:', schemaError.message)
@@ -214,17 +338,15 @@ async function getCount(req, res) {
214
338
 
215
339
  if (columns.length > 0) {
216
340
  const searchPattern = \`%\${query}%\`
341
+ // Note: Supabase PostgREST doesn't support ::text casting in .or() syntax
342
+ // Only text/varchar columns will match; non-text columns will be skipped
217
343
  const orConditions = columns.map((col) => \`\${col}.ilike.\${searchPattern}\`).join(',')
218
344
  countQuery = countQuery.or(orConditions)
219
345
  }
220
346
  }
221
347
 
222
- if (filters) {
223
- const parsedFilters = JSON.parse(filters)
224
- for (const filter of parsedFilters) {
225
- countQuery = countQuery.eq(filter.column, filter.value)
226
- }
227
- }
348
+ // Apply filters using helper function
349
+ countQuery = applyFilters(countQuery, filters)
228
350
 
229
351
  const { count, error } = await countQuery
230
352
 
@@ -1,4 +1,8 @@
1
- import { replaceSecretReference, generateDateFormatterCode } from '../utils'
1
+ import {
2
+ replaceSecretReference,
3
+ generateDateFormatterCode,
4
+ generateSafeJSONParseCode,
5
+ } from '../utils'
2
6
 
3
7
  export const validateTursoConfig = (
4
8
  config: Record<string, unknown>
@@ -35,6 +39,8 @@ export const generateTursoFetcher = (
35
39
 
36
40
  return `import { createClient } from '@libsql/client'
37
41
 
42
+ ${generateSafeJSONParseCode()}
43
+
38
44
  ${generateDateFormatterCode()}
39
45
 
40
46
  export default async function handler(req, res) {
@@ -45,7 +51,7 @@ export default async function handler(req, res) {
45
51
  authToken: ${replaceSecretReference(token)}
46
52
  })
47
53
 
48
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
54
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
49
55
 
50
56
  let sql = \`SELECT * FROM ${tableName}\`
51
57
  const whereClauses = []
@@ -54,8 +60,10 @@ export default async function handler(req, res) {
54
60
 
55
61
  if (query) {
56
62
  if (queryColumns) {
57
- const columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
58
- const searchConditions = columns.map((col) => \`\${col} LIKE ?\`)
63
+ const parsed = safeJSONParse(queryColumns)
64
+ const columns = Array.isArray(parsed) ? parsed : [parsed]
65
+ // Cast columns to TEXT to support searching on non-text columns (dates, numbers, etc.)
66
+ const searchConditions = columns.map((col) => \`CAST(\${col} AS TEXT) LIKE ?\`)
59
67
  whereClauses.push(\`(\${searchConditions.join(' OR ')})\`)
60
68
  columns.forEach(() => {
61
69
  queryParams.push(\`%\${query}%\`)
@@ -66,27 +74,86 @@ export default async function handler(req, res) {
66
74
  }
67
75
  }
68
76
 
77
+ // Helper to sanitize identifier (prevent SQL injection in column names)
78
+ const sanitizeIdentifier = (name) => {
79
+ // Only allow alphanumeric and underscore
80
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
81
+ throw new Error(\`Invalid identifier: \${name}\`)
82
+ }
83
+ return \`"\${name}"\`
84
+ }
85
+
69
86
  if (filters) {
70
- const parsedFilters = JSON.parse(filters)
71
- Object.entries(parsedFilters).forEach(([key, value]) => {
72
- if (Array.isArray(value)) {
73
- const placeholders = value.map(() => '?').join(', ')
74
- queryParams.push(...value)
75
- whereClauses.push(\`\${key} IN (\${placeholders})\`)
76
- } else {
77
- whereClauses.push(\`\${key} = ?\`)
78
- queryParams.push(value)
79
- }
80
- })
87
+ const parsedFilters = safeJSONParse(filters)
88
+
89
+ if (Array.isArray(parsedFilters)) {
90
+ parsedFilters.forEach((filter) => {
91
+ if (!filter.source || filter.destination === undefined) return
92
+
93
+ const field = sanitizeIdentifier(filter.source)
94
+ const value = filter.destination
95
+ const operand = filter.operand || '='
96
+
97
+ if (Array.isArray(value)) {
98
+ if (value.length === 0) return
99
+ const placeholders = value.map(() => '?').join(', ')
100
+ queryParams.push(...value)
101
+ if (operand === '!=') {
102
+ whereClauses.push(\`\${field} NOT IN (\${placeholders})\`)
103
+ } else {
104
+ whereClauses.push(\`\${field} IN (\${placeholders})\`)
105
+ }
106
+ } else {
107
+ if (value === null) {
108
+ if (operand === '=') {
109
+ whereClauses.push(\`\${field} IS NULL\`)
110
+ } else if (operand === '!=') {
111
+ whereClauses.push(\`\${field} IS NOT NULL\`)
112
+ }
113
+ } else {
114
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
115
+ const sqlOperator = validOps.includes(operand) ? operand : '='
116
+ whereClauses.push(\`\${field} \${sqlOperator} ?\`)
117
+ queryParams.push(value)
118
+ }
119
+ }
120
+ })
121
+ } else {
122
+ Object.entries(parsedFilters).forEach(([key, value]) => {
123
+ const field = sanitizeIdentifier(key)
124
+ if (Array.isArray(value)) {
125
+ const placeholders = value.map(() => '?').join(', ')
126
+ queryParams.push(...value)
127
+ whereClauses.push(\`\${field} IN (\${placeholders})\`)
128
+ } else {
129
+ whereClauses.push(\`\${field} = ?\`)
130
+ queryParams.push(value)
131
+ }
132
+ })
133
+ }
81
134
  }
82
135
 
83
136
  if (whereClauses.length > 0) {
84
137
  sql += \` WHERE \${whereClauses.join(' AND ')}\`
85
138
  }
86
139
 
87
- if (sortBy) {
140
+ // Handle sorts - new array format
141
+ if (sorts) {
142
+ const parsedSorts = safeJSONParse(sorts)
143
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
144
+ const orderClauses = parsedSorts.map((sort) => {
145
+ if (!sort.field) return null
146
+ const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
147
+ return \`\${sanitizeIdentifier(sort.field)} \${order}\`
148
+ }).filter(Boolean)
149
+
150
+ if (orderClauses.length > 0) {
151
+ sql += \` ORDER BY \${orderClauses.join(', ')}\`
152
+ }
153
+ }
154
+ } else if (sortBy) {
88
155
  const sortOrderValue = sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
89
- sql += \` ORDER BY \${sortBy} \${sortOrderValue}\`
156
+ sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${sortOrderValue}\`
90
157
  }
91
158
 
92
159
  const limitValue = limit || perPage
@@ -0,0 +1,200 @@
1
+ export const generateHeaderDetectionCode = (): string => {
2
+ return `const COMMON_HEADER_PATTERNS = [
3
+ /^id$/i,
4
+ /^name$/i,
5
+ /^email$/i,
6
+ /^date$/i,
7
+ /^time$/i,
8
+ /^status$/i,
9
+ /^type$/i,
10
+ /^title$/i,
11
+ /^description$/i,
12
+ /^price$/i,
13
+ /^amount$/i,
14
+ /^quantity$/i,
15
+ /^total$/i,
16
+ /^url$/i,
17
+ /^link$/i,
18
+ /^image$/i,
19
+ /^phone$/i,
20
+ /^address$/i,
21
+ /^city$/i,
22
+ /^country$/i,
23
+ /^category$/i,
24
+ /^tag$/i,
25
+ /^created/i,
26
+ /^updated/i,
27
+ /^modified/i,
28
+ /_id$/i,
29
+ /_at$/i,
30
+ /_date$/i,
31
+ /_name$/i,
32
+ /_url$/i,
33
+ ]
34
+
35
+ const DATA_PATTERNS = {
36
+ email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,
37
+ url: /^https?:\\/\\//i,
38
+ phone: /^[+]?[\\d\\s()-]{7,}$/,
39
+ date: /^\\d{1,4}[-/]\\d{1,2}[-/]\\d{1,4}$/,
40
+ currency: /^[$€£¥]\\s*[\\d,]+\\.?\\d*$/,
41
+ percentage: /^\\d+\\.?\\d*%$/,
42
+ pureNumber: /^-?\\d+\\.?\\d*$/,
43
+ }
44
+
45
+ function looksLikeDataValue(val) {
46
+ if (val === null || val === undefined) {
47
+ return false
48
+ }
49
+ if (typeof val === 'number' || typeof val === 'boolean') {
50
+ return true
51
+ }
52
+ if (val instanceof Date) {
53
+ return true
54
+ }
55
+ if (typeof val === 'string') {
56
+ const str = val.trim()
57
+ return Object.values(DATA_PATTERNS).some((pattern) => pattern.test(str))
58
+ }
59
+ return false
60
+ }
61
+
62
+ function looksLikeHeaderValue(val) {
63
+ if (typeof val !== 'string') {
64
+ return false
65
+ }
66
+ const str = val.trim()
67
+ if (str.length === 0 || str.length > 50) {
68
+ return false
69
+ }
70
+ if (looksLikeDataValue(str)) {
71
+ return false
72
+ }
73
+ if (COMMON_HEADER_PATTERNS.some((pattern) => pattern.test(str))) {
74
+ return true
75
+ }
76
+ const isTitleCase = /^[A-Z][a-z]*(\\s+[A-Z][a-z]*)*$/.test(str)
77
+ const isSnakeCase = /^[a-z]+(_[a-z]+)*$/i.test(str)
78
+ const isCamelCase = /^[a-z]+([A-Z][a-z]*)*$/.test(str)
79
+ const isPascalCase = /^[A-Z][a-z]+([A-Z][a-z]*)*$/.test(str)
80
+ const isUpperCase = /^[A-Z][A-Z\\s_]+$/.test(str) && str.length > 1
81
+ return isTitleCase || isSnakeCase || isCamelCase || isPascalCase || isUpperCase
82
+ }
83
+
84
+ function detectHeaderRow(
85
+ firstRowValues,
86
+ allRows,
87
+ minConfidenceScore = 4
88
+ ) {
89
+ if (allRows.length < 2) {
90
+ return false
91
+ }
92
+
93
+ // Helper to extract values from a row (handles both CSV arrays and Google Sheets objects)
94
+ const getRowValues = (row) => {
95
+ if (Array.isArray(row)) {
96
+ return row
97
+ }
98
+ // Google Sheets format: row has a 'c' property with cells
99
+ if (row && row.c && Array.isArray(row.c)) {
100
+ return row.c.map((cell) => cell?.v ?? cell?.f ?? null)
101
+ }
102
+ return []
103
+ }
104
+
105
+ const nonEmptyFirstRowValues = firstRowValues.filter(
106
+ (v) => v !== null && v !== undefined && v !== ''
107
+ )
108
+ if (nonEmptyFirstRowValues.length === 0) {
109
+ return false
110
+ }
111
+
112
+ let score = 0
113
+
114
+ const allStrings = nonEmptyFirstRowValues.every((val) => typeof val === 'string')
115
+ if (allStrings) {
116
+ score += 1
117
+ } else {
118
+ return false
119
+ }
120
+
121
+ const hasUniqueValues =
122
+ nonEmptyFirstRowValues.length ===
123
+ new Set(nonEmptyFirstRowValues.map((v) => String(v).toLowerCase().trim())).size
124
+ if (hasUniqueValues) {
125
+ score += 1
126
+ }
127
+
128
+ const headerLikeCount = nonEmptyFirstRowValues.filter((v) => looksLikeHeaderValue(v)).length
129
+ const headerLikeRatio = headerLikeCount / nonEmptyFirstRowValues.length
130
+ if (headerLikeRatio >= 0.5) {
131
+ score += 2
132
+ } else if (headerLikeRatio >= 0.3) {
133
+ score += 1
134
+ }
135
+
136
+ const commonHeaderCount = nonEmptyFirstRowValues.filter((v) =>
137
+ COMMON_HEADER_PATTERNS.some((pattern) => pattern.test(String(v).trim()))
138
+ ).length
139
+ if (commonHeaderCount >= 2) {
140
+ score += 2
141
+ } else if (commonHeaderCount >= 1) {
142
+ score += 1
143
+ }
144
+
145
+ const dataLikeInFirstRow = nonEmptyFirstRowValues.filter((v) => looksLikeDataValue(v)).length
146
+ if (dataLikeInFirstRow > 0) {
147
+ score -= dataLikeInFirstRow * 2
148
+ }
149
+
150
+ const sampleSize = Math.min(5, allRows.length - 1)
151
+ let dataRowsWithDataPatterns = 0
152
+ for (let i = 1; i <= sampleSize; i++) {
153
+ const rowValues = getRowValues(allRows[i])
154
+ const hasDataPattern = rowValues.some((v) => looksLikeDataValue(v))
155
+ if (hasDataPattern) {
156
+ dataRowsWithDataPatterns++
157
+ }
158
+ }
159
+ if (dataRowsWithDataPatterns >= sampleSize * 0.6) {
160
+ score += 2
161
+ }
162
+
163
+ const firstRowTypesSet = new Set(nonEmptyFirstRowValues.map((v) => typeof v))
164
+ const secondRowValues = getRowValues(allRows[1]).filter((v) => v !== null && v !== undefined && v !== '')
165
+ const secondRowTypesSet = new Set(secondRowValues.map((v) => typeof v))
166
+
167
+ const firstRowOnlyStrings = firstRowTypesSet.size === 1 && firstRowTypesSet.has('string')
168
+ const secondRowHasOtherTypes = secondRowTypesSet.has('number') || secondRowTypesSet.has('boolean')
169
+ if (firstRowOnlyStrings && secondRowHasOtherTypes) {
170
+ score += 2
171
+ }
172
+
173
+ const firstRowSimilarToDataRows = nonEmptyFirstRowValues.some((v) => looksLikeDataValue(v))
174
+ const secondRowHasData = secondRowValues.some((v) => looksLikeDataValue(v))
175
+ if (firstRowSimilarToDataRows && secondRowHasData) {
176
+ score -= 2
177
+ }
178
+
179
+ // Check if subsequent rows also look like headers
180
+ // If they do, the first row is likely data, not a header
181
+ const subsequentRowsSample = Math.min(3, allRows.length - 1)
182
+ let subsequentHeaderLikeCount = 0
183
+ for (let i = 1; i <= subsequentRowsSample; i++) {
184
+ const rowValues = getRowValues(allRows[i]).filter((v) => v !== null && v !== undefined && v !== '')
185
+ if (rowValues.length > 0) {
186
+ const headerLikeInRow = rowValues.filter((v) => looksLikeHeaderValue(v)).length
187
+ const headerLikeRatioInRow = headerLikeInRow / rowValues.length
188
+ if (headerLikeRatioInRow >= 0.5) {
189
+ subsequentHeaderLikeCount++
190
+ }
191
+ }
192
+ }
193
+ // If most subsequent rows also look like headers, first row is probably data
194
+ if (subsequentHeaderLikeCount >= subsequentRowsSample * 0.6) {
195
+ score -= 4
196
+ }
197
+
198
+ return score >= minConfidenceScore
199
+ }`
200
+ }