@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.
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 +2 -2
  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 +23 -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 +2 -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 +3 -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 +2 -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 +3 -0
  68. package/dist/cjs/utils.d.ts.map +1 -1
  69. package/dist/cjs/utils.js +218 -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 +3 -3
  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 -3
  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 -1
  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 -2
  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 -1
  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 +3 -0
  136. package/dist/esm/utils.d.ts.map +1 -1
  137. package/dist/esm/utils.js +214 -45
  138. package/dist/esm/utils.js.map +1 -1
  139. package/package.json +2 -2
  140. package/src/data-source-fetchers.ts +29 -13
  141. package/src/fetchers/airtable.ts +81 -31
  142. package/src/fetchers/clickhouse.ts +88 -19
  143. package/src/fetchers/csv-file.ts +256 -27
  144. package/src/fetchers/firestore.ts +65 -13
  145. package/src/fetchers/google-sheets.ts +243 -26
  146. package/src/fetchers/javascript.ts +106 -23
  147. package/src/fetchers/mariadb.ts +85 -26
  148. package/src/fetchers/mongodb.ts +156 -37
  149. package/src/fetchers/mysql.ts +86 -26
  150. package/src/fetchers/postgresql.ts +89 -27
  151. package/src/fetchers/redis.ts +43 -5
  152. package/src/fetchers/redshift.ts +87 -18
  153. package/src/fetchers/rest-api.ts +105 -24
  154. package/src/fetchers/static-collection.ts +100 -18
  155. package/src/fetchers/supabase.ts +178 -54
  156. package/src/fetchers/turso.ts +87 -18
  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 +374 -38
@@ -1,4 +1,8 @@
1
- import { replaceSecretReference } 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,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
- 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) {
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 = JSON.parse(filters)
57
- const conditions = Object.entries(parsedFilters).map(([field, value]) => {
58
- if (Array.isArray(value)) {
59
- const arrayConditions = value.map((v) => {
60
- if (typeof v === 'string') {
61
- return \`{\${field}}='\${v.replace(/'/g, "\\\\'")}'\`
62
- } else if (typeof v === 'number') {
63
- return \`{\${field}}=\${v}\`
64
- } else if (typeof v === 'boolean') {
65
- 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
+ '<=': '<=',
66
109
  }
67
- return \`{\${field}}='\${String(v)}'\`
68
- })
69
- return arrayConditions.length > 1
70
- ? \`OR(\${arrayConditions.join(',')})\`
71
- : arrayConditions[0]
72
- } else if (typeof value === 'string') {
73
- return \`{\${field}}='\${value.replace(/'/g, "\\\\'")}'\`
74
- } else if (typeof value === 'number') {
75
- return \`{\${field}}=\${value}\`
76
- } else if (typeof value === 'boolean') {
77
- 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)
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 { replaceSecretReference } 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,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 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]
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 = JSON.parse(filters)
79
- Object.entries(parsedFilters).forEach(([key, value]) => {
80
- if (Array.isArray(value)) {
81
- const formattedValues = value
82
- .map((v) => (typeof v === 'string' ? \`'\${v}'\` : v))
83
- .join(', ')
84
- conditions.push(\`\${key} IN (\${formattedValues})\`)
85
- } else if (typeof value === 'string') {
86
- conditions.push(\`\${key} = '\${value}'\`)
87
- } else {
88
- conditions.push(\`\${key} = \${value}\`)
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
- if (sortBy) {
100
- 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'}\`
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,
@@ -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.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)) {
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; [key: string]: unknown }>
45
+ columns?: Array<{ id: string; label?: string; type?: string }>
46
+ autoDetectHeader?: boolean
47
+ firstRowIsHeader?: boolean
31
48
  }
32
49
 
33
- export const generateCSVFileFetcher = (config: Record<string, unknown>): string => {
34
- const csvConfig = config as CSVFileConfig
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 columns = JSON.parse(queryColumns)
68
+ const searchColumns = safeJSONParse(queryColumns)
48
69
  filteredData = filteredData.filter((item) => {
49
- return columns.some((col) => {
50
- const value = item[col]
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 = JSON.parse(filters)
68
- filteredData = filteredData.filter((item) => {
69
- return Object.entries(parsedFilters).every(([key, value]) => {
70
- if (Array.isArray(value)) {
71
- return value.includes(item[key])
72
- }
73
- 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
+ })
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 (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
79
159
  filteredData.sort((a, b) => {
80
- const aVal = a[sortBy]
81
- const bVal = b[sortBy]
160
+ const aVal = getNestedValue(a, field)
161
+ const bVal = getNestedValue(b, field)
82
162
  const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
83
- if (aVal < bVal) return -sortOrderValue
84
- if (aVal > bVal) return sortOrderValue
85
- 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
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