@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.
- package/__tests__/csv-header-detection.test.ts +212 -0
- package/__tests__/validation.test.ts +33 -2
- package/dist/cjs/data-source-fetchers.d.ts +2 -2
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +30 -7
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
- package/dist/cjs/fetchers/airtable.js +1 -1
- package/dist/cjs/fetchers/airtable.js.map +1 -1
- package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
- package/dist/cjs/fetchers/clickhouse.js +1 -1
- package/dist/cjs/fetchers/clickhouse.js.map +1 -1
- package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
- package/dist/cjs/fetchers/csv-file.js +22 -3
- package/dist/cjs/fetchers/csv-file.js.map +1 -1
- package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
- package/dist/cjs/fetchers/firestore.js +1 -1
- package/dist/cjs/fetchers/firestore.js.map +1 -1
- package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
- package/dist/cjs/fetchers/google-sheets.js +6 -1
- package/dist/cjs/fetchers/google-sheets.js.map +1 -1
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
- package/dist/cjs/fetchers/javascript.js +1 -1
- package/dist/cjs/fetchers/javascript.js.map +1 -1
- package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mariadb.js +3 -3
- package/dist/cjs/fetchers/mariadb.js.map +1 -1
- package/dist/cjs/fetchers/mongodb.d.ts +1 -1
- package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mongodb.js +11 -3
- package/dist/cjs/fetchers/mongodb.js.map +1 -1
- package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
- package/dist/cjs/fetchers/mysql.js +2 -2
- package/dist/cjs/fetchers/mysql.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +3 -3
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/redis.d.ts.map +1 -1
- package/dist/cjs/fetchers/redis.js +1 -1
- package/dist/cjs/fetchers/redis.js.map +1 -1
- package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
- package/dist/cjs/fetchers/redshift.js +2 -2
- package/dist/cjs/fetchers/redshift.js.map +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +2 -2
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/static-collection.d.ts.map +1 -1
- package/dist/cjs/fetchers/static-collection.js +1 -1
- package/dist/cjs/fetchers/static-collection.js.map +1 -1
- package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
- package/dist/cjs/fetchers/supabase.js +2 -2
- package/dist/cjs/fetchers/supabase.js.map +1 -1
- package/dist/cjs/fetchers/turso.d.ts.map +1 -1
- package/dist/cjs/fetchers/turso.js +1 -1
- package/dist/cjs/fetchers/turso.js.map +1 -1
- package/dist/cjs/fetchers/utils/header-detection.d.ts +2 -0
- package/dist/cjs/fetchers/utils/header-detection.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/header-detection.js +8 -0
- package/dist/cjs/fetchers/utils/header-detection.js.map +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +168 -4
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +320 -65
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +2 -0
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +214 -46
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/data-source-fetchers.d.ts +2 -2
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +29 -6
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/airtable.d.ts.map +1 -1
- package/dist/esm/fetchers/airtable.js +2 -2
- package/dist/esm/fetchers/airtable.js.map +1 -1
- package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
- package/dist/esm/fetchers/clickhouse.js +2 -2
- package/dist/esm/fetchers/clickhouse.js.map +1 -1
- package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
- package/dist/esm/fetchers/csv-file.js +23 -4
- package/dist/esm/fetchers/csv-file.js.map +1 -1
- package/dist/esm/fetchers/firestore.d.ts.map +1 -1
- package/dist/esm/fetchers/firestore.js +2 -2
- package/dist/esm/fetchers/firestore.js.map +1 -1
- package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
- package/dist/esm/fetchers/google-sheets.js +6 -1
- package/dist/esm/fetchers/google-sheets.js.map +1 -1
- package/dist/esm/fetchers/javascript.d.ts.map +1 -1
- package/dist/esm/fetchers/javascript.js +2 -2
- package/dist/esm/fetchers/javascript.js.map +1 -1
- package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
- package/dist/esm/fetchers/mariadb.js +4 -4
- package/dist/esm/fetchers/mariadb.js.map +1 -1
- package/dist/esm/fetchers/mongodb.d.ts +1 -1
- package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
- package/dist/esm/fetchers/mongodb.js +12 -4
- package/dist/esm/fetchers/mongodb.js.map +1 -1
- package/dist/esm/fetchers/mysql.d.ts.map +1 -1
- package/dist/esm/fetchers/mysql.js +3 -3
- package/dist/esm/fetchers/mysql.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +4 -4
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/redis.d.ts.map +1 -1
- package/dist/esm/fetchers/redis.js +2 -2
- package/dist/esm/fetchers/redis.js.map +1 -1
- package/dist/esm/fetchers/redshift.d.ts.map +1 -1
- package/dist/esm/fetchers/redshift.js +3 -3
- package/dist/esm/fetchers/redshift.js.map +1 -1
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +3 -3
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/static-collection.d.ts.map +1 -1
- package/dist/esm/fetchers/static-collection.js +2 -2
- package/dist/esm/fetchers/static-collection.js.map +1 -1
- package/dist/esm/fetchers/supabase.d.ts.map +1 -1
- package/dist/esm/fetchers/supabase.js +3 -3
- package/dist/esm/fetchers/supabase.js.map +1 -1
- package/dist/esm/fetchers/turso.d.ts.map +1 -1
- package/dist/esm/fetchers/turso.js +2 -2
- package/dist/esm/fetchers/turso.js.map +1 -1
- package/dist/esm/fetchers/utils/header-detection.d.ts +2 -0
- package/dist/esm/fetchers/utils/header-detection.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/header-detection.js +4 -0
- package/dist/esm/fetchers/utils/header-detection.js.map +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +169 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +320 -65
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +2 -0
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +211 -45
- package/dist/esm/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/data-source-fetchers.ts +29 -13
- package/src/fetchers/airtable.ts +78 -30
- package/src/fetchers/clickhouse.ts +85 -18
- package/src/fetchers/csv-file.ts +254 -29
- package/src/fetchers/firestore.ts +62 -12
- package/src/fetchers/google-sheets.ts +147 -30
- package/src/fetchers/javascript.ts +102 -23
- package/src/fetchers/mariadb.ts +82 -25
- package/src/fetchers/mongodb.ts +153 -36
- package/src/fetchers/mysql.ts +83 -25
- package/src/fetchers/postgresql.ts +86 -26
- package/src/fetchers/redis.ts +40 -4
- package/src/fetchers/redshift.ts +84 -17
- package/src/fetchers/rest-api.ts +101 -24
- package/src/fetchers/static-collection.ts +96 -18
- package/src/fetchers/supabase.ts +175 -53
- package/src/fetchers/turso.ts +84 -17
- package/src/fetchers/utils/header-detection.ts +200 -0
- package/src/index.ts +248 -2
- package/src/pagination-plugin.ts +708 -191
- package/src/utils.ts +344 -38
package/src/fetchers/supabase.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
187
|
+
columns = safeJSONParse(queryColumns)
|
|
79
188
|
} else {
|
|
80
|
-
// Fallback: Get
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
+
const parsed = safeJSONParse(queryColumns)
|
|
304
|
+
columns = Array.isArray(parsed) ? parsed : [parsed]
|
|
199
305
|
} else {
|
|
200
|
-
// Fallback: Get
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
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
|
|
package/src/fetchers/turso.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
58
|
-
const
|
|
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 =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
+
}
|