@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
package/src/fetchers/airtable.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 validateAirtableConfig = (
|
|
4
8
|
config: Record<string, unknown>
|
|
@@ -34,9 +38,13 @@ export const generateAirtableFetcher = (
|
|
|
34
38
|
|
|
35
39
|
return `import fetch from 'node-fetch'
|
|
36
40
|
|
|
41
|
+
${generateSafeJSONParseCode()}
|
|
42
|
+
|
|
43
|
+
${generateDateFormatterCode()}
|
|
44
|
+
|
|
37
45
|
export default async function handler(req, res) {
|
|
38
46
|
try {
|
|
39
|
-
const { query, view, limit, page, perPage, sortBy, sortOrder, filters, offset: offsetParam } = req.query
|
|
47
|
+
const { query, view, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query
|
|
40
48
|
|
|
41
49
|
const queryParams = new URLSearchParams()
|
|
42
50
|
|
|
@@ -44,7 +52,17 @@ export default async function handler(req, res) {
|
|
|
44
52
|
queryParams.append('view', view)
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
// Handle sorts - new array format
|
|
56
|
+
if (sorts) {
|
|
57
|
+
const parsedSorts = safeJSONParse(sorts)
|
|
58
|
+
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
59
|
+
parsedSorts.forEach((sort, index) => {
|
|
60
|
+
if (!sort.field) return
|
|
61
|
+
queryParams.append(\`sort[\${index}][field]\`, sort.field)
|
|
62
|
+
queryParams.append(\`sort[\${index}][direction]\`, sort.order?.toLowerCase() === 'desc' ? 'desc' : 'asc')
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
} else if (sortBy) {
|
|
48
66
|
queryParams.append('sort[0][field]', sortBy)
|
|
49
67
|
queryParams.append('sort[0][direction]', sortOrder || 'asc')
|
|
50
68
|
}
|
|
@@ -52,36 +70,68 @@ export default async function handler(req, res) {
|
|
|
52
70
|
const perPageValue = limit || perPage || 100
|
|
53
71
|
queryParams.append('pageSize', Math.min(parseInt(perPageValue), 100).toString())
|
|
54
72
|
|
|
73
|
+
const formatAirtableValue = (value) => {
|
|
74
|
+
if (typeof value === 'string') {
|
|
75
|
+
return \`'\${value.replace(/'/g, "\\\\'")}'\`
|
|
76
|
+
} else if (typeof value === 'number') {
|
|
77
|
+
return String(value)
|
|
78
|
+
} else if (typeof value === 'boolean') {
|
|
79
|
+
return value ? 'TRUE()' : 'FALSE()'
|
|
80
|
+
}
|
|
81
|
+
return \`'\${String(value)}'\`
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
if (filters) {
|
|
56
|
-
const parsedFilters =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
const parsedFilters = safeJSONParse(filters)
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(parsedFilters)) {
|
|
88
|
+
const conditions = parsedFilters.map((filter) => {
|
|
89
|
+
if (!filter.source || filter.destination === undefined) return null
|
|
90
|
+
|
|
91
|
+
const field = filter.source
|
|
92
|
+
const value = filter.destination
|
|
93
|
+
const operand = filter.operand || '='
|
|
94
|
+
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
if (value.length === 0) return null
|
|
97
|
+
const arrayConditions = value.map((v) => \`{\${field}}=\${formatAirtableValue(v)}\`)
|
|
98
|
+
return arrayConditions.length > 1
|
|
99
|
+
? \`OR(\${arrayConditions.join(',')})\`
|
|
100
|
+
: arrayConditions[0]
|
|
101
|
+
} else {
|
|
102
|
+
const operatorMap = {
|
|
103
|
+
'=': '=',
|
|
104
|
+
'!=': '!=',
|
|
105
|
+
'>': '>',
|
|
106
|
+
'<': '<',
|
|
107
|
+
'>=': '>=',
|
|
108
|
+
'<=': '<=',
|
|
66
109
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
const airtableOp = operatorMap[operand] || '='
|
|
111
|
+
return \`{\${field}}\${airtableOp}\${formatAirtableValue(value)}\`
|
|
112
|
+
}
|
|
113
|
+
}).filter(Boolean)
|
|
114
|
+
|
|
115
|
+
if (conditions.length > 0) {
|
|
116
|
+
const filterFormula = conditions.length > 1 ? \`AND(\${conditions.join(',')})\` : conditions[0]
|
|
117
|
+
queryParams.append('filterByFormula', filterFormula)
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const conditions = Object.entries(parsedFilters).map(([field, value]) => {
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
const arrayConditions = value.map((v) => \`{\${field}}=\${formatAirtableValue(v)}\`)
|
|
123
|
+
return arrayConditions.length > 1
|
|
124
|
+
? \`OR(\${arrayConditions.join(',')})\`
|
|
125
|
+
: arrayConditions[0]
|
|
126
|
+
} else {
|
|
127
|
+
return \`{\${field}}=\${formatAirtableValue(value)}\`
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const filterFormula = conditions.length > 1 ? \`AND(\${conditions.join(',')})\` : conditions[0]
|
|
132
|
+
if (filterFormula) {
|
|
133
|
+
queryParams.append('filterByFormula', filterFormula)
|
|
78
134
|
}
|
|
79
|
-
return \`{\${field}}='\${String(value)}'\`
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const filterFormula = conditions.length > 1 ? \`AND(\${conditions.join(',')})\` : conditions[0]
|
|
83
|
-
if (filterFormula) {
|
|
84
|
-
queryParams.append('filterByFormula', filterFormula)
|
|
85
135
|
}
|
|
86
136
|
}
|
|
87
137
|
|
|
@@ -133,7 +183,7 @@ export default async function handler(req, res) {
|
|
|
133
183
|
createdTime: record.createdTime
|
|
134
184
|
}))
|
|
135
185
|
|
|
136
|
-
const safeData = JSON.parse(JSON.stringify(formattedRecords))
|
|
186
|
+
const safeData = JSON.parse(JSON.stringify(formattedRecords, dateReplacer))
|
|
137
187
|
|
|
138
188
|
return res.status(200).json({
|
|
139
189
|
success: true,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
replaceSecretReference,
|
|
3
|
+
generateDateFormatterCode,
|
|
4
|
+
generateSafeJSONParseCode,
|
|
5
|
+
} from '../utils'
|
|
2
6
|
|
|
3
7
|
export const validateClickHouseConfig = (
|
|
4
8
|
config: Record<string, unknown>
|
|
@@ -53,16 +57,21 @@ const getClient = () => {
|
|
|
53
57
|
return client
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
${generateSafeJSONParseCode()}
|
|
61
|
+
|
|
62
|
+
${generateDateFormatterCode()}
|
|
63
|
+
|
|
56
64
|
export default async function handler(req, res) {
|
|
57
65
|
try {
|
|
58
66
|
const client = getClient()
|
|
59
|
-
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
|
|
67
|
+
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
|
|
60
68
|
|
|
61
69
|
const conditions = []
|
|
62
70
|
|
|
63
71
|
if (query) {
|
|
64
72
|
if (queryColumns) {
|
|
65
|
-
const
|
|
73
|
+
const parsed = safeJSONParse(queryColumns)
|
|
74
|
+
const columns = Array.isArray(parsed) ? parsed : [parsed]
|
|
66
75
|
const searchConditions = columns.map(
|
|
67
76
|
(col) => \`positionCaseInsensitive(toString(\${col}), '\${query}') > 0\`
|
|
68
77
|
)
|
|
@@ -74,20 +83,66 @@ export default async function handler(req, res) {
|
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
const formatClickHouseValue = (value) => {
|
|
87
|
+
if (value === null || value === undefined) return 'NULL'
|
|
88
|
+
if (typeof value === 'string') return \`'\${value.replace(/'/g, "\\\\'")}'\`
|
|
89
|
+
if (typeof value === 'boolean') return value ? '1' : '0'
|
|
90
|
+
return String(value)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Helper to sanitize identifier (prevent SQL injection in column names)
|
|
94
|
+
const sanitizeIdentifier = (name) => {
|
|
95
|
+
// Only allow alphanumeric and underscore
|
|
96
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
97
|
+
throw new Error(\`Invalid identifier: \${name}\`)
|
|
98
|
+
}
|
|
99
|
+
return \`\\\`\${name}\\\`\`
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
if (filters) {
|
|
78
|
-
const parsedFilters =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
const parsedFilters = safeJSONParse(filters)
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(parsedFilters)) {
|
|
106
|
+
parsedFilters.forEach((filter) => {
|
|
107
|
+
if (!filter.source || filter.destination === undefined) return
|
|
108
|
+
|
|
109
|
+
const field = sanitizeIdentifier(filter.source)
|
|
110
|
+
const value = filter.destination
|
|
111
|
+
const operand = filter.operand || '='
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
if (value.length === 0) return
|
|
115
|
+
const formattedValues = value.map(formatClickHouseValue).join(', ')
|
|
116
|
+
if (operand === '!=') {
|
|
117
|
+
conditions.push(\`\${field} NOT IN (\${formattedValues})\`)
|
|
118
|
+
} else {
|
|
119
|
+
conditions.push(\`\${field} IN (\${formattedValues})\`)
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
if (value === null) {
|
|
123
|
+
if (operand === '=') {
|
|
124
|
+
conditions.push(\`\${field} IS NULL\`)
|
|
125
|
+
} else if (operand === '!=') {
|
|
126
|
+
conditions.push(\`\${field} IS NOT NULL\`)
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
const validOps = ['=', '!=', '>', '<', '>=', '<=']
|
|
130
|
+
const sqlOperator = validOps.includes(operand) ? operand : '='
|
|
131
|
+
conditions.push(\`\${field} \${sqlOperator} \${formatClickHouseValue(value)}\`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
} else {
|
|
136
|
+
Object.entries(parsedFilters).forEach(([key, value]) => {
|
|
137
|
+
const field = sanitizeIdentifier(key)
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
const formattedValues = value.map(formatClickHouseValue).join(', ')
|
|
140
|
+
conditions.push(\`\${field} IN (\${formattedValues})\`)
|
|
141
|
+
} else {
|
|
142
|
+
conditions.push(\`\${field} = \${formatClickHouseValue(value)}\`)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
91
146
|
}
|
|
92
147
|
|
|
93
148
|
let sql = \`SELECT * FROM ${tableName}\`
|
|
@@ -96,8 +151,22 @@ export default async function handler(req, res) {
|
|
|
96
151
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
97
152
|
}
|
|
98
153
|
|
|
99
|
-
|
|
100
|
-
|
|
154
|
+
// Handle sorts - new array format
|
|
155
|
+
if (sorts) {
|
|
156
|
+
const parsedSorts = safeJSONParse(sorts)
|
|
157
|
+
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
158
|
+
const orderClauses = parsedSorts.map((sort) => {
|
|
159
|
+
if (!sort.field) return null
|
|
160
|
+
const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
|
|
161
|
+
return \`\${sanitizeIdentifier(sort.field)} \${order}\`
|
|
162
|
+
}).filter(Boolean)
|
|
163
|
+
|
|
164
|
+
if (orderClauses.length > 0) {
|
|
165
|
+
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} else if (sortBy) {
|
|
169
|
+
sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${sortOrder?.toUpperCase() || 'ASC'}\`
|
|
101
170
|
}
|
|
102
171
|
|
|
103
172
|
const limitValue = limit || perPage
|
|
@@ -113,7 +182,7 @@ export default async function handler(req, res) {
|
|
|
113
182
|
|
|
114
183
|
const result = await client.query({ query: sql })
|
|
115
184
|
const resultResponse = await result.json()
|
|
116
|
-
const safeData = JSON.parse(JSON.stringify(resultResponse.data))
|
|
185
|
+
const safeData = JSON.parse(JSON.stringify(resultResponse.data, dateReplacer))
|
|
117
186
|
|
|
118
187
|
return res.status(200).json({
|
|
119
188
|
success: true,
|
package/src/fetchers/csv-file.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateDateFormatterCode,
|
|
3
|
+
generateSortFilterHelperCode,
|
|
4
|
+
generateSafeJSONParseCode,
|
|
5
|
+
} from '../utils'
|
|
6
|
+
import { generateHeaderDetectionCode } from './utils/header-detection'
|
|
7
|
+
|
|
1
8
|
export const validateCSVConfig = (
|
|
2
9
|
config: Record<string, unknown>
|
|
3
10
|
): { isValid: boolean; error?: string } => {
|
|
@@ -5,11 +12,18 @@ export const validateCSVConfig = (
|
|
|
5
12
|
return { isValid: false, error: 'Config must be a valid object' }
|
|
6
13
|
}
|
|
7
14
|
|
|
8
|
-
if (!config.
|
|
15
|
+
if (!config.fileContent && !config.parsedData) {
|
|
16
|
+
return { isValid: false, error: 'Either fileContent or parsedData must be provided' }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (config.fileContent && typeof config.fileContent !== 'string') {
|
|
20
|
+
return { isValid: false, error: 'File content must be a string' }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (config.parsedData && !Array.isArray(config.parsedData)) {
|
|
9
24
|
return { isValid: false, error: 'Parsed data must be an array' }
|
|
10
25
|
}
|
|
11
26
|
|
|
12
|
-
// Columns are optional - if not provided, we'll infer them from parsedData
|
|
13
27
|
if (config.columns !== undefined) {
|
|
14
28
|
if (!Array.isArray(config.columns)) {
|
|
15
29
|
return { isValid: false, error: 'Columns definition must be an array' }
|
|
@@ -26,17 +40,24 @@ export const validateCSVConfig = (
|
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
interface CSVFileConfig {
|
|
43
|
+
fileContent?: string
|
|
29
44
|
parsedData?: unknown[]
|
|
30
|
-
columns?: Array<{ id: string;
|
|
45
|
+
columns?: Array<{ id: string; label?: string; type?: string }>
|
|
46
|
+
autoDetectHeader?: boolean
|
|
47
|
+
firstRowIsHeader?: boolean
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return `const data = ${JSON.stringify(csvConfig.parsedData || [])}
|
|
36
|
-
|
|
37
|
-
export default async function handler(req, res) {
|
|
50
|
+
const generateHandlerBody = (): string => {
|
|
51
|
+
return `
|
|
38
52
|
try {
|
|
39
|
-
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset: offsetParam } = req.query
|
|
53
|
+
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query
|
|
54
|
+
|
|
55
|
+
const labelToIdMap = {}
|
|
56
|
+
columns.forEach((col) => {
|
|
57
|
+
if (col.label && col.id) {
|
|
58
|
+
labelToIdMap[col.label] = col.id
|
|
59
|
+
}
|
|
60
|
+
})
|
|
40
61
|
|
|
41
62
|
let filteredData = [...data]
|
|
42
63
|
|
|
@@ -44,10 +65,11 @@ export default async function handler(req, res) {
|
|
|
44
65
|
const searchQuery = query.toLowerCase()
|
|
45
66
|
|
|
46
67
|
if (queryColumns) {
|
|
47
|
-
const
|
|
68
|
+
const searchColumns = safeJSONParse(queryColumns)
|
|
48
69
|
filteredData = filteredData.filter((item) => {
|
|
49
|
-
return
|
|
50
|
-
const
|
|
70
|
+
return searchColumns.some((col) => {
|
|
71
|
+
const field = labelToIdMap[col] || col
|
|
72
|
+
const value = getNestedValue(item, field)
|
|
51
73
|
return value && String(value).toLowerCase().includes(searchQuery)
|
|
52
74
|
})
|
|
53
75
|
})
|
|
@@ -64,25 +86,98 @@ export default async function handler(req, res) {
|
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
if (filters) {
|
|
67
|
-
const parsedFilters =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
const parsedFilters = safeJSONParse(filters)
|
|
90
|
+
|
|
91
|
+
if (Array.isArray(parsedFilters)) {
|
|
92
|
+
filteredData = filteredData.filter((item) => {
|
|
93
|
+
return parsedFilters.every((filter) => {
|
|
94
|
+
if (!filter.source || filter.destination === undefined) return true
|
|
95
|
+
|
|
96
|
+
const field = labelToIdMap[filter.source] || filter.source
|
|
97
|
+
const value = getNestedValue(item, field)
|
|
98
|
+
const target = filter.destination
|
|
99
|
+
const operand = filter.operand || '='
|
|
100
|
+
|
|
101
|
+
if (Array.isArray(target)) {
|
|
102
|
+
if (operand === '!=') {
|
|
103
|
+
return !target.includes(value)
|
|
104
|
+
}
|
|
105
|
+
return target.includes(value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return compareValues(value, target, operand)
|
|
109
|
+
})
|
|
74
110
|
})
|
|
75
|
-
}
|
|
111
|
+
} else {
|
|
112
|
+
filteredData = filteredData.filter((item) => {
|
|
113
|
+
return Object.entries(parsedFilters).every(([key, value]) => {
|
|
114
|
+
const field = labelToIdMap[key] || key
|
|
115
|
+
const itemValue = getNestedValue(item, field)
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.includes(itemValue)
|
|
118
|
+
}
|
|
119
|
+
return compareValues(itemValue, value, '=')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
}
|
|
76
123
|
}
|
|
77
124
|
|
|
78
|
-
if (
|
|
125
|
+
if (sorts) {
|
|
126
|
+
const parsedSorts = safeJSONParse(sorts)
|
|
127
|
+
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
128
|
+
filteredData.sort((a, b) => {
|
|
129
|
+
for (const sort of parsedSorts) {
|
|
130
|
+
if (!sort.field) continue
|
|
131
|
+
const field = labelToIdMap[sort.field] || sort.field
|
|
132
|
+
const aVal = getNestedValue(a, field)
|
|
133
|
+
const bVal = getNestedValue(b, field)
|
|
134
|
+
const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
|
|
135
|
+
|
|
136
|
+
let comparison = 0
|
|
137
|
+
if (aVal === null || aVal === undefined) {
|
|
138
|
+
comparison = bVal === null || bVal === undefined ? 0 : -1
|
|
139
|
+
} else if (bVal === null || bVal === undefined) {
|
|
140
|
+
comparison = 1
|
|
141
|
+
} else if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
142
|
+
comparison = aVal - bVal
|
|
143
|
+
} else if (aVal instanceof Date && bVal instanceof Date) {
|
|
144
|
+
comparison = aVal.getTime() - bVal.getTime()
|
|
145
|
+
} else {
|
|
146
|
+
const aStr = String(aVal)
|
|
147
|
+
const bStr = String(bVal)
|
|
148
|
+
if (aStr < bStr) comparison = -1
|
|
149
|
+
else if (aStr > bStr) comparison = 1
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (comparison !== 0) return comparison * sortOrderValue
|
|
153
|
+
}
|
|
154
|
+
return 0
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
} else if (sortBy) {
|
|
158
|
+
const field = labelToIdMap[sortBy] || sortBy
|
|
79
159
|
filteredData.sort((a, b) => {
|
|
80
|
-
const aVal = a
|
|
81
|
-
const bVal = b
|
|
160
|
+
const aVal = getNestedValue(a, field)
|
|
161
|
+
const bVal = getNestedValue(b, field)
|
|
82
162
|
const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
163
|
+
|
|
164
|
+
let comparison = 0
|
|
165
|
+
if (aVal === null || aVal === undefined) {
|
|
166
|
+
comparison = bVal === null || bVal === undefined ? 0 : -1
|
|
167
|
+
} else if (bVal === null || bVal === undefined) {
|
|
168
|
+
comparison = 1
|
|
169
|
+
} else if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
170
|
+
comparison = aVal - bVal
|
|
171
|
+
} else if (aVal instanceof Date && bVal instanceof Date) {
|
|
172
|
+
comparison = aVal.getTime() - bVal.getTime()
|
|
173
|
+
} else {
|
|
174
|
+
const aStr = String(aVal)
|
|
175
|
+
const bStr = String(bVal)
|
|
176
|
+
if (aStr < bStr) comparison = -1
|
|
177
|
+
else if (aStr > bStr) comparison = 1
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return comparison * sortOrderValue
|
|
86
181
|
})
|
|
87
182
|
}
|
|
88
183
|
|
|
@@ -93,7 +188,7 @@ export default async function handler(req, res) {
|
|
|
93
188
|
filteredData = filteredData.slice(offsetValue, offsetValue + parseInt(limitValue))
|
|
94
189
|
}
|
|
95
190
|
|
|
96
|
-
const safeData = JSON.parse(JSON.stringify(filteredData))
|
|
191
|
+
const safeData = JSON.parse(JSON.stringify(filteredData, dateReplacer))
|
|
97
192
|
|
|
98
193
|
return res.status(200).json({
|
|
99
194
|
success: true,
|
|
@@ -108,8 +203,142 @@ export default async function handler(req, res) {
|
|
|
108
203
|
timestamp: Date.now()
|
|
109
204
|
})
|
|
110
205
|
}
|
|
206
|
+
`
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const generateCSVFileFetcher = (config: Record<string, unknown>): string => {
|
|
210
|
+
const csvConfig = config as CSVFileConfig
|
|
211
|
+
|
|
212
|
+
// Get header detection flags, ensuring boolean false values are preserved
|
|
213
|
+
const autoDetectHeader = csvConfig.autoDetectHeader !== false
|
|
214
|
+
const firstRowIsHeader = csvConfig.firstRowIsHeader !== false
|
|
215
|
+
const configColumns = csvConfig.columns || []
|
|
216
|
+
|
|
217
|
+
if (csvConfig.fileContent) {
|
|
218
|
+
const fileContent = csvConfig.fileContent
|
|
219
|
+
return `const csvContent = ${JSON.stringify(fileContent)}
|
|
220
|
+
const autoDetectHeader = ${autoDetectHeader}
|
|
221
|
+
const firstRowIsHeader = ${firstRowIsHeader}
|
|
222
|
+
const configColumns = ${JSON.stringify(configColumns)}
|
|
223
|
+
|
|
224
|
+
${generateSafeJSONParseCode()}
|
|
225
|
+
|
|
226
|
+
${generateDateFormatterCode()}
|
|
227
|
+
|
|
228
|
+
${generateSortFilterHelperCode()}
|
|
229
|
+
|
|
230
|
+
${generateHeaderDetectionCode()}
|
|
231
|
+
|
|
232
|
+
function parseCSVLine(line) {
|
|
233
|
+
const result = []
|
|
234
|
+
let current = ''
|
|
235
|
+
let inQuotes = false
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < line.length; i++) {
|
|
238
|
+
const char = line[i]
|
|
239
|
+
const nextChar = line[i + 1]
|
|
240
|
+
|
|
241
|
+
if (char === '"') {
|
|
242
|
+
if (inQuotes && nextChar === '"') {
|
|
243
|
+
current += '"'
|
|
244
|
+
i++
|
|
245
|
+
} else {
|
|
246
|
+
inQuotes = !inQuotes
|
|
247
|
+
}
|
|
248
|
+
} else if (char === ',' && !inQuotes) {
|
|
249
|
+
result.push(current)
|
|
250
|
+
current = ''
|
|
251
|
+
} else {
|
|
252
|
+
current += char
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
result.push(current)
|
|
257
|
+
return result
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parseCSVContent(content, autoDetect, firstRowHeader, columns) {
|
|
261
|
+
const lines = content.split('\\n').filter((line) => line.trim())
|
|
262
|
+
|
|
263
|
+
if (lines.length === 0) {
|
|
264
|
+
return { columns: [], rows: [] }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const allRowValues = lines.map((line) => parseCSVLine(line))
|
|
268
|
+
const firstRowValues = allRowValues[0]
|
|
269
|
+
|
|
270
|
+
if (firstRowValues.length === 0) {
|
|
271
|
+
return { columns: [], rows: [] }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const shouldAutoDetect = autoDetect !== false
|
|
275
|
+
let hasHeaderRow = false
|
|
276
|
+
|
|
277
|
+
if (shouldAutoDetect) {
|
|
278
|
+
hasHeaderRow = detectHeaderRow(firstRowValues, allRowValues)
|
|
279
|
+
} else {
|
|
280
|
+
hasHeaderRow = firstRowHeader !== false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const useConfigColumns = columns && columns.length > 0
|
|
284
|
+
let parsedColumns
|
|
285
|
+
let dataStartIndex
|
|
286
|
+
|
|
287
|
+
if (hasHeaderRow && allRowValues.length > 1) {
|
|
288
|
+
parsedColumns = firstRowValues.map((header, index) => {
|
|
289
|
+
const configCol = useConfigColumns ? columns[index] : null
|
|
290
|
+
return {
|
|
291
|
+
id: configCol?.id || \`col_\${index}\`,
|
|
292
|
+
label: String(header || configCol?.label || \`Column \${index + 1}\`),
|
|
293
|
+
type: configCol?.type || 'string'
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
dataStartIndex = 1
|
|
297
|
+
} else {
|
|
298
|
+
parsedColumns = firstRowValues.map((_, index) => {
|
|
299
|
+
const configCol = useConfigColumns ? columns[index] : null
|
|
300
|
+
return {
|
|
301
|
+
id: configCol?.id || \`col_\${index}\`,
|
|
302
|
+
label: configCol?.label || \`Column \${index + 1}\`,
|
|
303
|
+
type: configCol?.type || 'string'
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
dataStartIndex = 0
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const rows = []
|
|
310
|
+
for (let i = dataStartIndex; i < allRowValues.length; i++) {
|
|
311
|
+
const values = allRowValues[i]
|
|
312
|
+
const row = {}
|
|
313
|
+
parsedColumns.forEach((col, index) => {
|
|
314
|
+
const value = values[index]?.trim() || null
|
|
315
|
+
row[col.id] = value
|
|
316
|
+
})
|
|
317
|
+
rows.push(row)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { columns: parsedColumns, rows }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const { columns, rows: data } = parseCSVContent(csvContent, autoDetectHeader, firstRowIsHeader, configColumns)
|
|
324
|
+
|
|
325
|
+
export default async function handler(req, res) {${generateHandlerBody()}
|
|
326
|
+
}
|
|
327
|
+
`
|
|
328
|
+
} else {
|
|
329
|
+
return `const data = ${JSON.stringify(csvConfig.parsedData || [])}
|
|
330
|
+
const columns = ${JSON.stringify(configColumns)}
|
|
331
|
+
|
|
332
|
+
${generateSafeJSONParseCode()}
|
|
333
|
+
|
|
334
|
+
${generateDateFormatterCode()}
|
|
335
|
+
|
|
336
|
+
${generateSortFilterHelperCode()}
|
|
337
|
+
|
|
338
|
+
export default async function handler(req, res) {${generateHandlerBody()}
|
|
111
339
|
}
|
|
112
340
|
`
|
|
341
|
+
}
|
|
113
342
|
}
|
|
114
343
|
|
|
115
344
|
// tslint:disable-next-line:variable-name
|