@teleporthq/teleport-plugin-next-data-source 0.42.8 → 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 +2 -2
  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 +23 -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 +2 -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 +3 -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 +2 -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 +3 -0
  68. package/dist/cjs/utils.d.ts.map +1 -1
  69. package/dist/cjs/utils.js +218 -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 +3 -3
  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 -3
  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 -1
  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 -2
  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 -1
  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 +3 -0
  136. package/dist/esm/utils.d.ts.map +1 -1
  137. package/dist/esm/utils.js +214 -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 +81 -31
  142. package/src/fetchers/clickhouse.ts +88 -19
  143. package/src/fetchers/csv-file.ts +256 -27
  144. package/src/fetchers/firestore.ts +65 -13
  145. package/src/fetchers/google-sheets.ts +243 -26
  146. package/src/fetchers/javascript.ts +106 -23
  147. package/src/fetchers/mariadb.ts +85 -26
  148. package/src/fetchers/mongodb.ts +156 -37
  149. package/src/fetchers/mysql.ts +86 -26
  150. package/src/fetchers/postgresql.ts +89 -27
  151. package/src/fetchers/redis.ts +43 -5
  152. package/src/fetchers/redshift.ts +87 -18
  153. package/src/fetchers/rest-api.ts +105 -24
  154. package/src/fetchers/static-collection.ts +100 -18
  155. package/src/fetchers/supabase.ts +178 -54
  156. package/src/fetchers/turso.ts +87 -18
  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 +374 -38
@@ -1,3 +1,9 @@
1
+ import {
2
+ generateDateFormatterCode,
3
+ generateSortFilterHelperCode,
4
+ generateSafeJSONParseCode,
5
+ } from '../utils'
6
+
1
7
  export const validateStaticCollectionConfig = (
2
8
  config: Record<string, unknown>
3
9
  ): { isValid: boolean; error?: string } => {
@@ -20,9 +26,15 @@ export const generateStaticCollectionFetcher = (config: Record<string, unknown>)
20
26
  const staticConfig = config as StaticCollectionConfig
21
27
  return `const data = ${JSON.stringify(staticConfig.data || [])}
22
28
 
29
+ ${generateSafeJSONParseCode()}
30
+
31
+ ${generateDateFormatterCode()}
32
+
33
+ ${generateSortFilterHelperCode()}
34
+
23
35
  export default async function handler(req, res) {
24
36
  try {
25
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset: offsetParam } = req.query
37
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query
26
38
 
27
39
  let filteredData = [...data]
28
40
 
@@ -30,10 +42,10 @@ export default async function handler(req, res) {
30
42
  const searchQuery = query.toLowerCase()
31
43
 
32
44
  if (queryColumns) {
33
- const columns = JSON.parse(queryColumns)
45
+ const columns = safeJSONParse(queryColumns)
34
46
  filteredData = filteredData.filter((item) => {
35
47
  return columns.some((col) => {
36
- const value = item[col]
48
+ const value = getNestedValue(item, col)
37
49
  return value && String(value).toLowerCase().includes(searchQuery)
38
50
  })
39
51
  })
@@ -50,25 +62,95 @@ export default async function handler(req, res) {
50
62
  }
51
63
 
52
64
  if (filters) {
53
- const parsedFilters = JSON.parse(filters)
54
- filteredData = filteredData.filter((item) => {
55
- return Object.entries(parsedFilters).every(([key, value]) => {
56
- if (Array.isArray(value)) {
57
- return value.includes(item[key])
58
- }
59
- return item[key] === value
65
+ const parsedFilters = safeJSONParse(filters)
66
+
67
+ if (Array.isArray(parsedFilters)) {
68
+ filteredData = filteredData.filter((item) => {
69
+ return parsedFilters.every((filter) => {
70
+ if (!filter.source || filter.destination === undefined) return true
71
+
72
+ const field = filter.source
73
+ const value = getNestedValue(item, field)
74
+ const target = filter.destination
75
+ const operand = filter.operand || '='
76
+
77
+ if (Array.isArray(target)) {
78
+ if (operand === '!=') {
79
+ return !target.includes(value)
80
+ }
81
+ return target.includes(value)
82
+ }
83
+
84
+ return compareValues(value, target, operand)
85
+ })
60
86
  })
61
- })
87
+ } else {
88
+ filteredData = filteredData.filter((item) => {
89
+ return Object.entries(parsedFilters).every(([key, value]) => {
90
+ const itemValue = getNestedValue(item, key)
91
+ if (Array.isArray(value)) {
92
+ return value.includes(itemValue)
93
+ }
94
+ return compareValues(itemValue, value, '=')
95
+ })
96
+ })
97
+ }
62
98
  }
63
99
 
64
- if (sortBy) {
100
+ if (sorts) {
101
+ const parsedSorts = safeJSONParse(sorts)
102
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
103
+ filteredData.sort((a, b) => {
104
+ for (const sort of parsedSorts) {
105
+ if (!sort.field) continue
106
+ const aVal = getNestedValue(a, sort.field)
107
+ const bVal = getNestedValue(b, sort.field)
108
+ const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
109
+
110
+ let comparison = 0
111
+ if (aVal === null || aVal === undefined) {
112
+ comparison = bVal === null || bVal === undefined ? 0 : -1
113
+ } else if (bVal === null || bVal === undefined) {
114
+ comparison = 1
115
+ } else if (typeof aVal === 'number' && typeof bVal === 'number') {
116
+ comparison = aVal - bVal
117
+ } else if (aVal instanceof Date && bVal instanceof Date) {
118
+ comparison = aVal.getTime() - bVal.getTime()
119
+ } else {
120
+ const aStr = String(aVal)
121
+ const bStr = String(bVal)
122
+ if (aStr < bStr) comparison = -1
123
+ else if (aStr > bStr) comparison = 1
124
+ }
125
+
126
+ if (comparison !== 0) return comparison * sortOrderValue
127
+ }
128
+ return 0
129
+ })
130
+ }
131
+ } else if (sortBy) {
65
132
  filteredData.sort((a, b) => {
66
- const aVal = a[sortBy]
67
- const bVal = b[sortBy]
133
+ const aVal = getNestedValue(a, sortBy)
134
+ const bVal = getNestedValue(b, sortBy)
68
135
  const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
69
- if (aVal < bVal) return -sortOrderValue
70
- if (aVal > bVal) return sortOrderValue
71
- return 0
136
+
137
+ let comparison = 0
138
+ if (aVal === null || aVal === undefined) {
139
+ comparison = bVal === null || bVal === undefined ? 0 : -1
140
+ } else if (bVal === null || bVal === undefined) {
141
+ comparison = 1
142
+ } else if (typeof aVal === 'number' && typeof bVal === 'number') {
143
+ comparison = aVal - bVal
144
+ } else if (aVal instanceof Date && bVal instanceof Date) {
145
+ comparison = aVal.getTime() - bVal.getTime()
146
+ } else {
147
+ const aStr = String(aVal)
148
+ const bStr = String(bVal)
149
+ if (aStr < bStr) comparison = -1
150
+ else if (aStr > bStr) comparison = 1
151
+ }
152
+
153
+ return comparison * sortOrderValue
72
154
  })
73
155
  }
74
156
 
@@ -79,7 +161,7 @@ export default async function handler(req, res) {
79
161
  filteredData = filteredData.slice(offsetValue, offsetValue + parseInt(limitValue))
80
162
  }
81
163
 
82
- const safeData = JSON.parse(JSON.stringify(filteredData))
164
+ const safeData = JSON.parse(JSON.stringify(filteredData, dateReplacer))
83
165
 
84
166
  return res.status(200).json({
85
167
  success: true,
@@ -1,4 +1,8 @@
1
- import { replaceSecretReference } 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,10 +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
+
173
+ ${generateDateFormatterCode()}
174
+
64
175
  export default async function handler(req, res) {
65
176
  try {
66
177
  const client = getClient()
67
- 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
68
179
 
69
180
  let queryRef = client.from('${tableName}').select(select || '*')
70
181
 
@@ -73,16 +184,34 @@ export default async function handler(req, res) {
73
184
 
74
185
  if (queryColumns) {
75
186
  // Use specified columns
76
- columns = JSON.parse(queryColumns)
187
+ columns = safeJSONParse(queryColumns)
77
188
  } else {
78
- // Fallback: Get all column names from a sample row
189
+ // Fallback: Get text-searchable columns from a sample row
79
190
  try {
80
191
  const { data: sampleData, error: sampleError } = await client.from('${tableName}').select('*').limit(1).single()
81
192
  if (sampleError) {
82
193
  throw sampleError
83
194
  }
84
195
  if (sampleData) {
85
- 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
+ })
86
215
  }
87
216
  } catch (schemaError) {
88
217
  console.warn('Failed to fetch sample row for column names:', schemaError.message)
@@ -92,51 +221,29 @@ export default async function handler(req, res) {
92
221
 
93
222
  if (columns.length > 0) {
94
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
95
226
  const orConditions = columns.map((col) => \`\${col}.ilike.\${searchPattern}\`).join(',')
96
227
  queryRef = queryRef.or(orConditions)
97
228
  }
98
229
  }
99
230
 
100
- if (filters) {
101
- const parsedFilters = JSON.parse(filters)
102
- Object.entries(parsedFilters).forEach(([key, value]) => {
103
- if (Array.isArray(value)) {
104
- const processedValues = value.map((v) => {
105
- if (typeof v === 'string' && !isNaN(Number(v))) {
106
- return Number(v)
107
- }
108
- return v
109
- })
110
- queryRef = queryRef.in(key, processedValues)
111
- } else if (typeof value === 'object' && value !== null) {
112
- const operator = Object.keys(value)[0]
113
- let operatorValue = value[operator]
114
- if (typeof operatorValue === 'string' && !isNaN(Number(operatorValue))) {
115
- operatorValue = Number(operatorValue)
116
- }
117
- switch (operator) {
118
- case 'eq': queryRef = queryRef.eq(key, operatorValue); break
119
- case 'neq': queryRef = queryRef.neq(key, operatorValue); break
120
- case 'gt': queryRef = queryRef.gt(key, operatorValue); break
121
- case 'gte': queryRef = queryRef.gte(key, operatorValue); break
122
- case 'lt': queryRef = queryRef.lt(key, operatorValue); break
123
- case 'lte': queryRef = queryRef.lte(key, operatorValue); break
124
- case 'like': queryRef = queryRef.like(key, operatorValue); break
125
- case 'ilike': queryRef = queryRef.ilike(key, operatorValue); break
126
- case 'in': queryRef = queryRef.in(key, operatorValue); break
127
- default: queryRef = queryRef.eq(key, operatorValue)
128
- }
129
- } else {
130
- let processedValue = value
131
- if (typeof value === 'string' && !isNaN(Number(value))) {
132
- processedValue = Number(value)
133
- }
134
- queryRef = queryRef.eq(key, processedValue)
135
- }
136
- })
137
- }
231
+ // Apply filters using helper function
232
+ queryRef = applyFilters(queryRef, filters)
138
233
 
139
- 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) {
140
247
  queryRef = queryRef.order(sortBy, { ascending: sortOrder !== 'desc' })
141
248
  }
142
249
 
@@ -159,7 +266,7 @@ export default async function handler(req, res) {
159
266
  })
160
267
  }
161
268
 
162
- const safeData = JSON.parse(JSON.stringify(data))
269
+ const safeData = JSON.parse(JSON.stringify(data, dateReplacer))
163
270
 
164
271
  return res.status(200).json({
165
272
  success: true,
@@ -193,16 +300,35 @@ async function getCount(req, res) {
193
300
 
194
301
  if (queryColumns) {
195
302
  // Use specified columns
196
- columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
303
+ const parsed = safeJSONParse(queryColumns)
304
+ columns = Array.isArray(parsed) ? parsed : [parsed]
197
305
  } else {
198
- // Fallback: Get all column names from a sample row
306
+ // Fallback: Get text-searchable columns from a sample row
199
307
  try {
200
308
  const { data: sampleData, error: sampleError } = await supabase.from('${tableName}').select('*').limit(1).single()
201
309
  if (sampleError) {
202
310
  throw sampleError
203
311
  }
204
312
  if (sampleData) {
205
- 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
+ })
206
332
  }
207
333
  } catch (schemaError) {
208
334
  console.warn('Failed to fetch sample row for column names:', schemaError.message)
@@ -212,17 +338,15 @@ async function getCount(req, res) {
212
338
 
213
339
  if (columns.length > 0) {
214
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
215
343
  const orConditions = columns.map((col) => \`\${col}.ilike.\${searchPattern}\`).join(',')
216
344
  countQuery = countQuery.or(orConditions)
217
345
  }
218
346
  }
219
347
 
220
- if (filters) {
221
- const parsedFilters = JSON.parse(filters)
222
- for (const filter of parsedFilters) {
223
- countQuery = countQuery.eq(filter.column, filter.value)
224
- }
225
- }
348
+ // Apply filters using helper function
349
+ countQuery = applyFilters(countQuery, filters)
226
350
 
227
351
  const { count, error } = await countQuery
228
352
 
@@ -1,4 +1,8 @@
1
- import { replaceSecretReference } 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,10 @@ export const generateTursoFetcher = (
35
39
 
36
40
  return `import { createClient } from '@libsql/client'
37
41
 
42
+ ${generateSafeJSONParseCode()}
43
+
44
+ ${generateDateFormatterCode()}
45
+
38
46
  export default async function handler(req, res) {
39
47
  let client = null
40
48
  try {
@@ -43,7 +51,7 @@ export default async function handler(req, res) {
43
51
  authToken: ${replaceSecretReference(token)}
44
52
  })
45
53
 
46
- 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
47
55
 
48
56
  let sql = \`SELECT * FROM ${tableName}\`
49
57
  const whereClauses = []
@@ -52,8 +60,10 @@ export default async function handler(req, res) {
52
60
 
53
61
  if (query) {
54
62
  if (queryColumns) {
55
- const columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
56
- 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 ?\`)
57
67
  whereClauses.push(\`(\${searchConditions.join(' OR ')})\`)
58
68
  columns.forEach(() => {
59
69
  queryParams.push(\`%\${query}%\`)
@@ -64,27 +74,86 @@ export default async function handler(req, res) {
64
74
  }
65
75
  }
66
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
+
67
86
  if (filters) {
68
- const parsedFilters = JSON.parse(filters)
69
- Object.entries(parsedFilters).forEach(([key, value]) => {
70
- if (Array.isArray(value)) {
71
- const placeholders = value.map(() => '?').join(', ')
72
- queryParams.push(...value)
73
- whereClauses.push(\`\${key} IN (\${placeholders})\`)
74
- } else {
75
- whereClauses.push(\`\${key} = ?\`)
76
- queryParams.push(value)
77
- }
78
- })
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
+ }
79
134
  }
80
135
 
81
136
  if (whereClauses.length > 0) {
82
137
  sql += \` WHERE \${whereClauses.join(' AND ')}\`
83
138
  }
84
139
 
85
- 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) {
86
155
  const sortOrderValue = sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
87
- sql += \` ORDER BY \${sortBy} \${sortOrderValue}\`
156
+ sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${sortOrderValue}\`
88
157
  }
89
158
 
90
159
  const limitValue = limit || perPage
@@ -137,7 +206,7 @@ export default async function handler(req, res) {
137
206
  }
138
207
  }
139
208
 
140
- const safeData = JSON.parse(JSON.stringify(data))
209
+ const safeData = JSON.parse(JSON.stringify(data, dateReplacer))
141
210
 
142
211
  return res.status(200).json({
143
212
  success: true,