@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.
Files changed (160) hide show
  1. package/__tests__/csv-header-detection.test.ts +212 -0
  2. package/__tests__/validation.test.ts +33 -2
  3. package/dist/cjs/data-source-fetchers.d.ts +2 -2
  4. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  5. package/dist/cjs/data-source-fetchers.js +30 -7
  6. package/dist/cjs/data-source-fetchers.js.map +1 -1
  7. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  8. package/dist/cjs/fetchers/airtable.js +1 -1
  9. package/dist/cjs/fetchers/airtable.js.map +1 -1
  10. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  11. package/dist/cjs/fetchers/clickhouse.js +1 -1
  12. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  13. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  14. package/dist/cjs/fetchers/csv-file.js +22 -3
  15. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  16. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  17. package/dist/cjs/fetchers/firestore.js +1 -1
  18. package/dist/cjs/fetchers/firestore.js.map +1 -1
  19. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/google-sheets.js +6 -1
  21. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  22. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  23. package/dist/cjs/fetchers/javascript.js +1 -1
  24. package/dist/cjs/fetchers/javascript.js.map +1 -1
  25. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/mariadb.js +3 -3
  27. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  28. package/dist/cjs/fetchers/mongodb.d.ts +1 -1
  29. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  30. package/dist/cjs/fetchers/mongodb.js +11 -3
  31. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  32. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  33. package/dist/cjs/fetchers/mysql.js +2 -2
  34. package/dist/cjs/fetchers/mysql.js.map +1 -1
  35. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  36. package/dist/cjs/fetchers/postgresql.js +3 -3
  37. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  38. package/dist/cjs/fetchers/redis.d.ts.map +1 -1
  39. package/dist/cjs/fetchers/redis.js +1 -1
  40. package/dist/cjs/fetchers/redis.js.map +1 -1
  41. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  42. package/dist/cjs/fetchers/redshift.js +2 -2
  43. package/dist/cjs/fetchers/redshift.js.map +1 -1
  44. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  45. package/dist/cjs/fetchers/rest-api.js +2 -2
  46. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  47. package/dist/cjs/fetchers/static-collection.d.ts.map +1 -1
  48. package/dist/cjs/fetchers/static-collection.js +1 -1
  49. package/dist/cjs/fetchers/static-collection.js.map +1 -1
  50. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  51. package/dist/cjs/fetchers/supabase.js +2 -2
  52. package/dist/cjs/fetchers/supabase.js.map +1 -1
  53. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  54. package/dist/cjs/fetchers/turso.js +1 -1
  55. package/dist/cjs/fetchers/turso.js.map +1 -1
  56. package/dist/cjs/fetchers/utils/header-detection.d.ts +2 -0
  57. package/dist/cjs/fetchers/utils/header-detection.d.ts.map +1 -0
  58. package/dist/cjs/fetchers/utils/header-detection.js +8 -0
  59. package/dist/cjs/fetchers/utils/header-detection.js.map +1 -0
  60. package/dist/cjs/index.d.ts.map +1 -1
  61. package/dist/cjs/index.js +168 -4
  62. package/dist/cjs/index.js.map +1 -1
  63. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  64. package/dist/cjs/pagination-plugin.js +320 -65
  65. package/dist/cjs/pagination-plugin.js.map +1 -1
  66. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  67. package/dist/cjs/utils.d.ts +2 -0
  68. package/dist/cjs/utils.d.ts.map +1 -1
  69. package/dist/cjs/utils.js +214 -46
  70. package/dist/cjs/utils.js.map +1 -1
  71. package/dist/esm/data-source-fetchers.d.ts +2 -2
  72. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  73. package/dist/esm/data-source-fetchers.js +29 -6
  74. package/dist/esm/data-source-fetchers.js.map +1 -1
  75. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  76. package/dist/esm/fetchers/airtable.js +2 -2
  77. package/dist/esm/fetchers/airtable.js.map +1 -1
  78. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  79. package/dist/esm/fetchers/clickhouse.js +2 -2
  80. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  81. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  82. package/dist/esm/fetchers/csv-file.js +23 -4
  83. package/dist/esm/fetchers/csv-file.js.map +1 -1
  84. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  85. package/dist/esm/fetchers/firestore.js +2 -2
  86. package/dist/esm/fetchers/firestore.js.map +1 -1
  87. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  88. package/dist/esm/fetchers/google-sheets.js +6 -1
  89. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  90. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  91. package/dist/esm/fetchers/javascript.js +2 -2
  92. package/dist/esm/fetchers/javascript.js.map +1 -1
  93. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  94. package/dist/esm/fetchers/mariadb.js +4 -4
  95. package/dist/esm/fetchers/mariadb.js.map +1 -1
  96. package/dist/esm/fetchers/mongodb.d.ts +1 -1
  97. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  98. package/dist/esm/fetchers/mongodb.js +12 -4
  99. package/dist/esm/fetchers/mongodb.js.map +1 -1
  100. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  101. package/dist/esm/fetchers/mysql.js +3 -3
  102. package/dist/esm/fetchers/mysql.js.map +1 -1
  103. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  104. package/dist/esm/fetchers/postgresql.js +4 -4
  105. package/dist/esm/fetchers/postgresql.js.map +1 -1
  106. package/dist/esm/fetchers/redis.d.ts.map +1 -1
  107. package/dist/esm/fetchers/redis.js +2 -2
  108. package/dist/esm/fetchers/redis.js.map +1 -1
  109. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  110. package/dist/esm/fetchers/redshift.js +3 -3
  111. package/dist/esm/fetchers/redshift.js.map +1 -1
  112. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  113. package/dist/esm/fetchers/rest-api.js +3 -3
  114. package/dist/esm/fetchers/rest-api.js.map +1 -1
  115. package/dist/esm/fetchers/static-collection.d.ts.map +1 -1
  116. package/dist/esm/fetchers/static-collection.js +2 -2
  117. package/dist/esm/fetchers/static-collection.js.map +1 -1
  118. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  119. package/dist/esm/fetchers/supabase.js +3 -3
  120. package/dist/esm/fetchers/supabase.js.map +1 -1
  121. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  122. package/dist/esm/fetchers/turso.js +2 -2
  123. package/dist/esm/fetchers/turso.js.map +1 -1
  124. package/dist/esm/fetchers/utils/header-detection.d.ts +2 -0
  125. package/dist/esm/fetchers/utils/header-detection.d.ts.map +1 -0
  126. package/dist/esm/fetchers/utils/header-detection.js +4 -0
  127. package/dist/esm/fetchers/utils/header-detection.js.map +1 -0
  128. package/dist/esm/index.d.ts.map +1 -1
  129. package/dist/esm/index.js +169 -5
  130. package/dist/esm/index.js.map +1 -1
  131. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  132. package/dist/esm/pagination-plugin.js +320 -65
  133. package/dist/esm/pagination-plugin.js.map +1 -1
  134. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  135. package/dist/esm/utils.d.ts +2 -0
  136. package/dist/esm/utils.d.ts.map +1 -1
  137. package/dist/esm/utils.js +211 -45
  138. package/dist/esm/utils.js.map +1 -1
  139. package/package.json +3 -3
  140. package/src/data-source-fetchers.ts +29 -13
  141. package/src/fetchers/airtable.ts +78 -30
  142. package/src/fetchers/clickhouse.ts +85 -18
  143. package/src/fetchers/csv-file.ts +254 -29
  144. package/src/fetchers/firestore.ts +62 -12
  145. package/src/fetchers/google-sheets.ts +147 -30
  146. package/src/fetchers/javascript.ts +102 -23
  147. package/src/fetchers/mariadb.ts +82 -25
  148. package/src/fetchers/mongodb.ts +153 -36
  149. package/src/fetchers/mysql.ts +83 -25
  150. package/src/fetchers/postgresql.ts +86 -26
  151. package/src/fetchers/redis.ts +40 -4
  152. package/src/fetchers/redshift.ts +84 -17
  153. package/src/fetchers/rest-api.ts +101 -24
  154. package/src/fetchers/static-collection.ts +96 -18
  155. package/src/fetchers/supabase.ts +175 -53
  156. package/src/fetchers/turso.ts +84 -17
  157. package/src/fetchers/utils/header-detection.ts +200 -0
  158. package/src/index.ts +248 -2
  159. package/src/pagination-plugin.ts +708 -191
  160. package/src/utils.ts +344 -38
@@ -1,4 +1,8 @@
1
- import { replaceSecretReference, generateDateFormatterCode } from '../utils'
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
- if (sortBy) {
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 = JSON.parse(filters)
59
- const conditions = Object.entries(parsedFilters).map(([field, value]) => {
60
- if (Array.isArray(value)) {
61
- const arrayConditions = value.map((v) => {
62
- if (typeof v === 'string') {
63
- return \`{\${field}}='\${v.replace(/'/g, "\\\\'")}'\`
64
- } else if (typeof v === 'number') {
65
- return \`{\${field}}=\${v}\`
66
- } else if (typeof v === 'boolean') {
67
- return \`{\${field}}=\${v ? 'TRUE()' : 'FALSE()'}\`
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
- return \`{\${field}}='\${String(v)}'\`
70
- })
71
- return arrayConditions.length > 1
72
- ? \`OR(\${arrayConditions.join(',')})\`
73
- : arrayConditions[0]
74
- } else if (typeof value === 'string') {
75
- return \`{\${field}}='\${value.replace(/'/g, "\\\\'")}'\`
76
- } else if (typeof value === 'number') {
77
- return \`{\${field}}=\${value}\`
78
- } else if (typeof value === 'boolean') {
79
- return \`{\${field}}=\${value ? 'TRUE()' : 'FALSE()'}\`
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 { replaceSecretReference, generateDateFormatterCode } from '../utils'
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 columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
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 = JSON.parse(filters)
81
- Object.entries(parsedFilters).forEach(([key, value]) => {
82
- if (Array.isArray(value)) {
83
- const formattedValues = value
84
- .map((v) => (typeof v === 'string' ? \`'\${v}'\` : v))
85
- .join(', ')
86
- conditions.push(\`\${key} IN (\${formattedValues})\`)
87
- } else if (typeof value === 'string') {
88
- conditions.push(\`\${key} = '\${value}'\`)
89
- } else {
90
- conditions.push(\`\${key} = \${value}\`)
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
- if (sortBy) {
102
- sql += \` ORDER BY \${sortBy} \${sortOrder?.toUpperCase() || 'ASC'}\`
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
@@ -1,4 +1,9 @@
1
- import { generateDateFormatterCode } from '../utils'
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.parsedData || !Array.isArray(config.parsedData)) {
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; [key: string]: unknown }>
45
+ columns?: Array<{ id: string; label?: string; type?: string }>
46
+ autoDetectHeader?: boolean
47
+ firstRowIsHeader?: boolean
33
48
  }
34
49
 
35
- export const generateCSVFileFetcher = (config: Record<string, unknown>): string => {
36
- const csvConfig = config as CSVFileConfig
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 columns = JSON.parse(queryColumns)
68
+ const searchColumns = safeJSONParse(queryColumns)
52
69
  filteredData = filteredData.filter((item) => {
53
- return columns.some((col) => {
54
- const value = item[col]
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 = JSON.parse(filters)
72
- filteredData = filteredData.filter((item) => {
73
- return Object.entries(parsedFilters).every(([key, value]) => {
74
- if (Array.isArray(value)) {
75
- return value.includes(item[key])
76
- }
77
- return item[key] === value
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 (sortBy) {
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[sortBy]
85
- const bVal = b[sortBy]
160
+ const aVal = getNestedValue(a, field)
161
+ const bVal = getNestedValue(b, field)
86
162
  const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
87
- if (aVal < bVal) return -sortOrderValue
88
- if (aVal > bVal) return sortOrderValue
89
- return 0
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