@teleporthq/teleport-plugin-next-data-source 0.42.9 → 0.42.11
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 +3 -3
- 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/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,11 +38,13 @@ export const generateAirtableFetcher = (
|
|
|
34
38
|
|
|
35
39
|
return `import fetch from 'node-fetch'
|
|
36
40
|
|
|
41
|
+
${generateSafeJSONParseCode()}
|
|
42
|
+
|
|
37
43
|
${generateDateFormatterCode()}
|
|
38
44
|
|
|
39
45
|
export default async function handler(req, res) {
|
|
40
46
|
try {
|
|
41
|
-
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
|
|
42
48
|
|
|
43
49
|
const queryParams = new URLSearchParams()
|
|
44
50
|
|
|
@@ -46,7 +52,17 @@ export default async function handler(req, res) {
|
|
|
46
52
|
queryParams.append('view', view)
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
|
|
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) {
|
|
50
66
|
queryParams.append('sort[0][field]', sortBy)
|
|
51
67
|
queryParams.append('sort[0][direction]', sortOrder || 'asc')
|
|
52
68
|
}
|
|
@@ -54,36 +70,68 @@ export default async function handler(req, res) {
|
|
|
54
70
|
const perPageValue = limit || perPage || 100
|
|
55
71
|
queryParams.append('pageSize', Math.min(parseInt(perPageValue), 100).toString())
|
|
56
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
|
+
|
|
57
84
|
if (filters) {
|
|
58
|
-
const parsedFilters =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
'<=': '<=',
|
|
68
109
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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)
|
|
80
134
|
}
|
|
81
|
-
return \`{\${field}}='\${String(value)}'\`
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const filterFormula = conditions.length > 1 ? \`AND(\${conditions.join(',')})\` : conditions[0]
|
|
85
|
-
if (filterFormula) {
|
|
86
|
-
queryParams.append('filterByFormula', filterFormula)
|
|
87
135
|
}
|
|
88
136
|
}
|
|
89
137
|
|
|
@@ -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,18 +57,21 @@ const getClient = () => {
|
|
|
53
57
|
return client
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
${generateSafeJSONParseCode()}
|
|
61
|
+
|
|
56
62
|
${generateDateFormatterCode()}
|
|
57
63
|
|
|
58
64
|
export default async function handler(req, res) {
|
|
59
65
|
try {
|
|
60
66
|
const client = getClient()
|
|
61
|
-
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
|
|
62
68
|
|
|
63
69
|
const conditions = []
|
|
64
70
|
|
|
65
71
|
if (query) {
|
|
66
72
|
if (queryColumns) {
|
|
67
|
-
const
|
|
73
|
+
const parsed = safeJSONParse(queryColumns)
|
|
74
|
+
const columns = Array.isArray(parsed) ? parsed : [parsed]
|
|
68
75
|
const searchConditions = columns.map(
|
|
69
76
|
(col) => \`positionCaseInsensitive(toString(\${col}), '\${query}') > 0\`
|
|
70
77
|
)
|
|
@@ -76,20 +83,66 @@ export default async function handler(req, res) {
|
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
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
|
+
|
|
79
102
|
if (filters) {
|
|
80
|
-
const parsedFilters =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
146
|
}
|
|
94
147
|
|
|
95
148
|
let sql = \`SELECT * FROM ${tableName}\`
|
|
@@ -98,8 +151,22 @@ export default async function handler(req, res) {
|
|
|
98
151
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
99
152
|
}
|
|
100
153
|
|
|
101
|
-
|
|
102
|
-
|
|
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'}\`
|
|
103
170
|
}
|
|
104
171
|
|
|
105
172
|
const limitValue = limit || perPage
|
package/src/fetchers/csv-file.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
generateDateFormatterCode,
|
|
3
|
+
generateSortFilterHelperCode,
|
|
4
|
+
generateSafeJSONParseCode,
|
|
5
|
+
} from '../utils'
|
|
6
|
+
import { generateHeaderDetectionCode } from './utils/header-detection'
|
|
2
7
|
|
|
3
8
|
export const validateCSVConfig = (
|
|
4
9
|
config: Record<string, unknown>
|
|
@@ -7,11 +12,18 @@ export const validateCSVConfig = (
|
|
|
7
12
|
return { isValid: false, error: 'Config must be a valid object' }
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
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)) {
|
|
11
24
|
return { isValid: false, error: 'Parsed data must be an array' }
|
|
12
25
|
}
|
|
13
26
|
|
|
14
|
-
// Columns are optional - if not provided, we'll infer them from parsedData
|
|
15
27
|
if (config.columns !== undefined) {
|
|
16
28
|
if (!Array.isArray(config.columns)) {
|
|
17
29
|
return { isValid: false, error: 'Columns definition must be an array' }
|
|
@@ -28,19 +40,24 @@ export const validateCSVConfig = (
|
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
interface CSVFileConfig {
|
|
43
|
+
fileContent?: string
|
|
31
44
|
parsedData?: unknown[]
|
|
32
|
-
columns?: Array<{ id: string;
|
|
45
|
+
columns?: Array<{ id: string; label?: string; type?: string }>
|
|
46
|
+
autoDetectHeader?: boolean
|
|
47
|
+
firstRowIsHeader?: boolean
|
|
33
48
|
}
|
|
34
49
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return `const data = ${JSON.stringify(csvConfig.parsedData || [])}
|
|
38
|
-
|
|
39
|
-
${generateDateFormatterCode()}
|
|
40
|
-
|
|
41
|
-
export default async function handler(req, res) {
|
|
50
|
+
const generateHandlerBody = (): string => {
|
|
51
|
+
return `
|
|
42
52
|
try {
|
|
43
|
-
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
|
+
})
|
|
44
61
|
|
|
45
62
|
let filteredData = [...data]
|
|
46
63
|
|
|
@@ -48,10 +65,11 @@ export default async function handler(req, res) {
|
|
|
48
65
|
const searchQuery = query.toLowerCase()
|
|
49
66
|
|
|
50
67
|
if (queryColumns) {
|
|
51
|
-
const
|
|
68
|
+
const searchColumns = safeJSONParse(queryColumns)
|
|
52
69
|
filteredData = filteredData.filter((item) => {
|
|
53
|
-
return
|
|
54
|
-
const
|
|
70
|
+
return searchColumns.some((col) => {
|
|
71
|
+
const field = labelToIdMap[col] || col
|
|
72
|
+
const value = getNestedValue(item, field)
|
|
55
73
|
return value && String(value).toLowerCase().includes(searchQuery)
|
|
56
74
|
})
|
|
57
75
|
})
|
|
@@ -68,25 +86,98 @@ export default async function handler(req, res) {
|
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
if (filters) {
|
|
71
|
-
const parsedFilters =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
})
|
|
78
110
|
})
|
|
79
|
-
}
|
|
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
|
+
}
|
|
80
123
|
}
|
|
81
124
|
|
|
82
|
-
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
|
|
83
159
|
filteredData.sort((a, b) => {
|
|
84
|
-
const aVal = a
|
|
85
|
-
const bVal = b
|
|
160
|
+
const aVal = getNestedValue(a, field)
|
|
161
|
+
const bVal = getNestedValue(b, field)
|
|
86
162
|
const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
90
181
|
})
|
|
91
182
|
}
|
|
92
183
|
|
|
@@ -112,8 +203,142 @@ export default async function handler(req, res) {
|
|
|
112
203
|
timestamp: Date.now()
|
|
113
204
|
})
|
|
114
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()}
|
|
115
326
|
}
|
|
116
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()}
|
|
339
|
+
}
|
|
340
|
+
`
|
|
341
|
+
}
|
|
117
342
|
}
|
|
118
343
|
|
|
119
344
|
// tslint:disable-next-line:variable-name
|