@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.
- 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 +2 -2
- 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 +23 -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 +2 -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 +3 -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 +2 -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 +3 -0
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +218 -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 +3 -3
- 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 -3
- 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 -1
- 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 -2
- 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 -1
- 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 +3 -0
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +214 -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 +81 -31
- package/src/fetchers/clickhouse.ts +88 -19
- package/src/fetchers/csv-file.ts +256 -27
- package/src/fetchers/firestore.ts +65 -13
- package/src/fetchers/google-sheets.ts +243 -26
- package/src/fetchers/javascript.ts +106 -23
- package/src/fetchers/mariadb.ts +85 -26
- package/src/fetchers/mongodb.ts +156 -37
- package/src/fetchers/mysql.ts +86 -26
- package/src/fetchers/postgresql.ts +89 -27
- package/src/fetchers/redis.ts +43 -5
- package/src/fetchers/redshift.ts +87 -18
- package/src/fetchers/rest-api.ts +105 -24
- package/src/fetchers/static-collection.ts +100 -18
- package/src/fetchers/supabase.ts +178 -54
- package/src/fetchers/turso.ts +87 -18
- 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 +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 =
|
|
45
|
+
const columns = safeJSONParse(queryColumns)
|
|
34
46
|
filteredData = filteredData.filter((item) => {
|
|
35
47
|
return columns.some((col) => {
|
|
36
|
-
const value = item
|
|
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 =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 (
|
|
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
|
|
67
|
-
const bVal = b
|
|
133
|
+
const aVal = getNestedValue(a, sortBy)
|
|
134
|
+
const bVal = getNestedValue(b, sortBy)
|
|
68
135
|
const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
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,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 =
|
|
187
|
+
columns = safeJSONParse(queryColumns)
|
|
77
188
|
} else {
|
|
78
|
-
// Fallback: Get
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
+
const parsed = safeJSONParse(queryColumns)
|
|
304
|
+
columns = Array.isArray(parsed) ? parsed : [parsed]
|
|
197
305
|
} else {
|
|
198
|
-
// Fallback: Get
|
|
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
|
|
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
|
-
|
|
221
|
-
|
|
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
|
|
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,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
|
|
56
|
-
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 ?\`)
|
|
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 =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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,
|