@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 validateFirestoreConfig = (
4
8
  config: Record<string, unknown>
@@ -67,31 +71,67 @@ const getFirestore = () => {
67
71
  return firestore
68
72
  }
69
73
 
74
+ ${generateSafeJSONParseCode()}
75
+
70
76
  ${generateDateFormatterCode()}
71
77
 
72
78
  export default async function handler(req, res) {
73
79
  try {
74
80
  const firestore = getFirestore()
75
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
81
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
76
82
 
77
83
  let queryRef = firestore.collection('${tableName}')
78
84
 
79
85
  if (filters) {
80
- const parsedFilters = JSON.parse(filters)
81
- Object.entries(parsedFilters).forEach(([key, value]) => {
82
- if (Array.isArray(value)) {
83
- queryRef = queryRef.where(key, 'in', value)
84
- } else {
85
- queryRef = queryRef.where(key, '==', value)
86
- }
87
- })
86
+ const parsedFilters = safeJSONParse(filters)
87
+
88
+ if (Array.isArray(parsedFilters)) {
89
+ parsedFilters.forEach((filter) => {
90
+ if (!filter.source || filter.destination === undefined) return
91
+
92
+ const field = filter.source
93
+ const value = filter.destination
94
+ const operand = filter.operand || '='
95
+
96
+ // Map operands to Firestore operators
97
+ const operatorMap = {
98
+ '=': '==',
99
+ '!=': '!=',
100
+ '>': '>',
101
+ '<': '<',
102
+ '>=': '>=',
103
+ '<=': '<=',
104
+ }
105
+
106
+ if (Array.isArray(value)) {
107
+ if (value.length === 0) return
108
+ if (operand === '!=') {
109
+ queryRef = queryRef.where(field, 'not-in', value)
110
+ } else {
111
+ queryRef = queryRef.where(field, 'in', value)
112
+ }
113
+ } else {
114
+ const firestoreOp = operatorMap[operand] || '=='
115
+ queryRef = queryRef.where(field, firestoreOp, value)
116
+ }
117
+ })
118
+ } else {
119
+ Object.entries(parsedFilters).forEach(([key, value]) => {
120
+ if (Array.isArray(value)) {
121
+ queryRef = queryRef.where(key, 'in', value)
122
+ } else {
123
+ queryRef = queryRef.where(key, '==', value)
124
+ }
125
+ })
126
+ }
88
127
  }
89
128
 
90
129
  let usePostFiltering = false
91
130
 
92
131
  if (query) {
93
132
  if (queryColumns) {
94
- const columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
133
+ const parsed = safeJSONParse(queryColumns)
134
+ const columns = Array.isArray(parsed) ? parsed : [parsed]
95
135
  for (const column of columns) {
96
136
  queryRef = queryRef
97
137
  .where(column, '>=', query)
@@ -104,7 +144,17 @@ export default async function handler(req, res) {
104
144
  }
105
145
  }
106
146
 
107
- if (sortBy) {
147
+ // Handle sorts - new array format
148
+ if (sorts) {
149
+ const parsedSorts = safeJSONParse(sorts)
150
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
151
+ parsedSorts.forEach((sort) => {
152
+ if (!sort.field) return
153
+ const order = sort.order?.toLowerCase() === 'desc' ? 'desc' : 'asc'
154
+ queryRef = queryRef.orderBy(sort.field, order)
155
+ })
156
+ }
157
+ } else if (sortBy) {
108
158
  const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? 'desc' : 'asc'
109
159
  queryRef = queryRef.orderBy(sortBy, sortOrderValue)
110
160
  }
@@ -1,3 +1,6 @@
1
+ import { generateSortFilterHelperCode, generateSafeJSONParseCode } from '../utils'
2
+ import { generateHeaderDetectionCode } from './utils/header-detection'
3
+
1
4
  export const validateGoogleSheetsConfig = (
2
5
  config: Record<string, unknown>
3
6
  ): { isValid: boolean; error?: string } => {
@@ -33,18 +36,33 @@ interface GoogleSheetsConfig {
33
36
  sheetName?: string
34
37
  range?: string
35
38
  maxRows?: number
39
+ autoDetectHeader?: boolean
40
+ firstRowIsHeader?: boolean
41
+ columns?: Array<{ id: string; label?: string; type?: string }>
36
42
  }
37
43
 
38
44
  export const generateGoogleSheetsFetcher = (config: Record<string, unknown>): string => {
39
45
  const sheetsConfig = config as GoogleSheetsConfig
46
+ const autoDetectHeader = sheetsConfig.autoDetectHeader
47
+ const firstRowIsHeader = sheetsConfig.firstRowIsHeader
48
+ const configColumns = sheetsConfig.columns || []
49
+
40
50
  return `import fetch from 'node-fetch'
41
51
 
52
+ ${generateSafeJSONParseCode()}
53
+
54
+ ${generateSortFilterHelperCode()}
55
+
56
+ ${generateHeaderDetectionCode()}
57
+
42
58
  export default async function handler(req, res) {
43
59
  try {
44
60
  const sheetUrl = ${JSON.stringify(sheetsConfig.sheetUrl)}
45
61
  let sheetId = ${JSON.stringify(sheetsConfig.sheetId)}
46
62
  const range = ${JSON.stringify(sheetsConfig.range || 'A1:Z1000')}
47
63
  const maxRows = ${sheetsConfig.maxRows || 0}
64
+ const autoDetectHeader = ${autoDetectHeader !== undefined ? autoDetectHeader : true}
65
+ const firstRowIsHeader = ${firstRowIsHeader !== undefined ? firstRowIsHeader : true}
48
66
 
49
67
  if (!sheetId && sheetUrl) {
50
68
  const match = sheetUrl.match(/\\/d\\/([a-zA-Z0-9-_]+)/)
@@ -158,26 +176,40 @@ export default async function handler(req, res) {
158
176
  const firstRow = rawRows[0]
159
177
  const firstRowValues = firstRow.c.map((cell) => cell?.v ?? cell?.f ?? null)
160
178
 
161
- const hasHeaderRow = firstRowValues.every((val) =>
162
- val !== null && val !== undefined && val !== '' && typeof val === 'string'
163
- )
179
+ const shouldAutoDetect = autoDetectHeader !== false
180
+ let hasHeaderRow = false
181
+
182
+ if (shouldAutoDetect) {
183
+ hasHeaderRow = detectHeaderRow(firstRowValues, rawRows)
184
+ } else {
185
+ hasHeaderRow = firstRowIsHeader !== false
186
+ }
187
+
188
+ const configColumns = ${JSON.stringify(configColumns)}
189
+ const useConfigColumns = configColumns && configColumns.length > 0
164
190
 
165
191
  let columns
166
192
  let dataRows
167
193
 
168
194
  if (hasHeaderRow && rawRows.length > 1) {
169
- columns = firstRowValues.map((headerValue, index) => ({
170
- id: \`col_\${index}\`,
171
- label: String(headerValue),
172
- type: 'string'
173
- }))
195
+ columns = table.cols.map((col, index) => {
196
+ const configCol = useConfigColumns ? configColumns[index] : null
197
+ return {
198
+ id: configCol?.id || col.id || \`col_\${index}\`,
199
+ label: String(firstRowValues[index] || col.label || configCol?.label || \`Column \${index + 1}\`),
200
+ type: configCol?.type || col.type || 'string'
201
+ }
202
+ })
174
203
  dataRows = rawRows.slice(1)
175
204
  } else {
176
- columns = table.cols.map((col, index) => ({
177
- id: col.id || \`col_\${index}\`,
178
- label: col.label || \`Column \${index + 1}\`,
179
- type: col.type || 'string'
180
- }))
205
+ columns = table.cols.map((col, index) => {
206
+ const configCol = useConfigColumns ? configColumns[index] : null
207
+ return {
208
+ id: configCol?.id || col.id || \`col_\${index}\`,
209
+ label: configCol?.label || col.label || \`Column \${index + 1}\`,
210
+ type: configCol?.type || col.type || 'string'
211
+ }
212
+ })
181
213
  dataRows = rawRows
182
214
  }
183
215
 
@@ -212,7 +244,13 @@ export default async function handler(req, res) {
212
244
  return filteredRow
213
245
  })
214
246
 
215
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset: offsetParam } = req.query
247
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query
248
+
249
+ // Create label-to-ID mapping for filters and sorts
250
+ const labelToIdMap = {}
251
+ columnsWithData.forEach((col) => {
252
+ labelToIdMap[col.label] = col.id
253
+ })
216
254
 
217
255
  let filteredData = [...filteredRows]
218
256
 
@@ -220,10 +258,12 @@ export default async function handler(req, res) {
220
258
  const searchQuery = query.toLowerCase()
221
259
 
222
260
  if (queryColumns) {
223
- const searchColumns = JSON.parse(queryColumns)
261
+ const searchColumns = safeJSONParse(queryColumns)
224
262
  filteredData = filteredData.filter((item) => {
225
263
  return searchColumns.some((col) => {
226
- const value = item[col]
264
+ // Map label to column ID
265
+ const field = labelToIdMap[col] || col
266
+ const value = getNestedValue(item, field)
227
267
  return value && String(value).toLowerCase().includes(searchQuery)
228
268
  })
229
269
  })
@@ -240,25 +280,102 @@ export default async function handler(req, res) {
240
280
  }
241
281
 
242
282
  if (filters) {
243
- const parsedFilters = JSON.parse(filters)
244
- filteredData = filteredData.filter((item) => {
245
- return Object.entries(parsedFilters).every(([key, value]) => {
246
- if (Array.isArray(value)) {
247
- return value.includes(item[key])
248
- }
249
- return item[key] === value
283
+ const parsedFilters = safeJSONParse(filters)
284
+
285
+ if (Array.isArray(parsedFilters)) {
286
+ filteredData = filteredData.filter((item) => {
287
+ return parsedFilters.every((filter) => {
288
+ if (!filter.source || filter.destination === undefined) return true
289
+
290
+ // Map label to column ID
291
+ const field = labelToIdMap[filter.source] || filter.source
292
+ const value = getNestedValue(item, field)
293
+ const target = filter.destination
294
+ const operand = filter.operand || '='
295
+
296
+ if (Array.isArray(target)) {
297
+ if (operand === '!=') {
298
+ return !target.includes(value)
299
+ }
300
+ return target.includes(value)
301
+ }
302
+
303
+ return compareValues(value, target, operand)
304
+ })
250
305
  })
251
- })
306
+ } else {
307
+ filteredData = filteredData.filter((item) => {
308
+ return Object.entries(parsedFilters).every(([key, value]) => {
309
+ // Map label to column ID
310
+ const field = labelToIdMap[key] || key
311
+ const itemValue = getNestedValue(item, field)
312
+ if (Array.isArray(value)) {
313
+ return value.includes(itemValue)
314
+ }
315
+ return compareValues(itemValue, value, '=')
316
+ })
317
+ })
318
+ }
252
319
  }
253
320
 
254
- if (sortBy) {
321
+ if (sorts) {
322
+ const parsedSorts = safeJSONParse(sorts)
323
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
324
+ filteredData.sort((a, b) => {
325
+ for (const sort of parsedSorts) {
326
+ if (!sort.field) continue
327
+ // Map label to column ID
328
+ const field = labelToIdMap[sort.field] || sort.field
329
+ const aVal = getNestedValue(a, field)
330
+ const bVal = getNestedValue(b, field)
331
+ const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
332
+
333
+ let comparison = 0
334
+ if (aVal === null || aVal === undefined) {
335
+ comparison = bVal === null || bVal === undefined ? 0 : -1
336
+ } else if (bVal === null || bVal === undefined) {
337
+ comparison = 1
338
+ } else if (typeof aVal === 'number' && typeof bVal === 'number') {
339
+ comparison = aVal - bVal
340
+ } else if (aVal instanceof Date && bVal instanceof Date) {
341
+ comparison = aVal.getTime() - bVal.getTime()
342
+ } else {
343
+ const aStr = String(aVal)
344
+ const bStr = String(bVal)
345
+ if (aStr < bStr) comparison = -1
346
+ else if (aStr > bStr) comparison = 1
347
+ }
348
+
349
+ if (comparison !== 0) return comparison * sortOrderValue
350
+ }
351
+ return 0
352
+ })
353
+ }
354
+ } else if (sortBy) {
255
355
  filteredData.sort((a, b) => {
256
- const aVal = a[sortBy]
257
- const bVal = b[sortBy]
356
+ // Map label to column ID
357
+ const field = labelToIdMap[sortBy] || sortBy
358
+ const aVal = getNestedValue(a, field)
359
+ const bVal = getNestedValue(b, field)
258
360
  const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
259
- if (aVal < bVal) return -sortOrderValue
260
- if (aVal > bVal) return sortOrderValue
261
- return 0
361
+
362
+ let comparison = 0
363
+ if (aVal === null || aVal === undefined) {
364
+ comparison = bVal === null || bVal === undefined ? 0 : -1
365
+ } else if (bVal === null || bVal === undefined) {
366
+ comparison = 1
367
+ } else if (typeof aVal === 'number' && typeof bVal === 'number') {
368
+ comparison = aVal - bVal
369
+ } else if (aVal instanceof Date && bVal instanceof Date) {
370
+ comparison = aVal.getTime() - bVal.getTime()
371
+ } else {
372
+ const aStr = String(aVal)
373
+ const bStr = String(bVal)
374
+ if (aStr < bStr) comparison = -1
375
+ else if (aStr > bStr) comparison = 1
376
+ }
377
+
378
+ return comparison * sortOrderValue
262
379
  })
263
380
  }
264
381
 
@@ -1,4 +1,8 @@
1
- import { generateDateFormatterCode } from '../utils'
1
+ import {
2
+ generateDateFormatterCode,
3
+ generateSortFilterHelperCode,
4
+ generateSafeJSONParseCode,
5
+ } from '../utils'
2
6
 
3
7
  export const validateJavaScriptConfig = (
4
8
  config: Record<string, unknown>
@@ -37,27 +41,31 @@ interface JavaScriptConfig {
37
41
 
38
42
  export const generateJavaScriptFetcher = (config: Record<string, unknown>): string => {
39
43
  const jsConfig = config as JavaScriptConfig
40
- return `${generateDateFormatterCode()}
44
+ return `${generateSafeJSONParseCode()}
45
+
46
+ ${generateDateFormatterCode()}
47
+
48
+ ${generateSortFilterHelperCode()}
41
49
 
42
50
  export default async function handler(req, res) {
43
51
  try {
44
- const { limit, offset, page, perPage, query, queryColumns, sortBy, sortOrder, filters } = req.query
52
+ const { limit, offset, page, perPage, query, queryColumns, sortBy, sortOrder, filters, sorts } = req.query
45
53
 
46
54
  const code = ${JSON.stringify(jsConfig.code)}
47
55
  const executeCode = new Function('return ' + code)
48
56
  let data = executeCode()
49
57
 
50
58
  if (Array.isArray(data)) {
51
- // 1. Apply search filter
52
59
  if (query && query.trim()) {
53
60
  const searchQuery = query.toLowerCase()
54
61
 
55
62
  if (queryColumns) {
56
63
  try {
57
- const columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
64
+ const parsed = safeJSONParse(queryColumns)
65
+ const columns = Array.isArray(parsed) ? parsed : [parsed]
58
66
  data = data.filter(item => {
59
67
  return columns.some(col => {
60
- const value = item[col]
68
+ const value = getNestedValue(item, col)
61
69
  if (value === null || value === undefined) return false
62
70
  return String(value).toLowerCase().includes(searchQuery)
63
71
  })
@@ -77,36 +85,107 @@ export default async function handler(req, res) {
77
85
  }
78
86
  }
79
87
 
80
- // 2. Apply custom filters
81
88
  if (filters) {
82
89
  try {
83
- const parsedFilters = typeof filters === 'string' ? JSON.parse(filters) : filters
84
- data = data.filter((item) => {
85
- return Object.entries(parsedFilters).every(([key, value]) => {
86
- if (Array.isArray(value)) {
87
- return value.includes(item[key])
88
- }
89
- return item[key] === value
90
+ const parsedFilters = safeJSONParse(filters)
91
+
92
+ if (Array.isArray(parsedFilters)) {
93
+ data = data.filter((item) => {
94
+ return parsedFilters.every((filter) => {
95
+ if (!filter.source || filter.destination === undefined) return true
96
+
97
+ const field = filter.source
98
+ const value = getNestedValue(item, field)
99
+ const target = filter.destination
100
+ const operand = filter.operand || '='
101
+
102
+ if (Array.isArray(target)) {
103
+ if (operand === '!=') {
104
+ return !target.includes(value)
105
+ }
106
+ return target.includes(value)
107
+ }
108
+
109
+ return compareValues(value, target, operand)
110
+ })
90
111
  })
91
- })
112
+ } else {
113
+ data = data.filter((item) => {
114
+ return Object.entries(parsedFilters).every(([key, value]) => {
115
+ const itemValue = getNestedValue(item, key)
116
+ if (Array.isArray(value)) {
117
+ return value.includes(itemValue)
118
+ }
119
+ return compareValues(itemValue, value, '=')
120
+ })
121
+ })
122
+ }
92
123
  } catch (err) {
93
124
  console.error('Error parsing filters:', err)
94
125
  }
95
126
  }
96
127
 
97
- // 3. Apply sorting
98
- if (sortBy && sortBy.trim()) {
128
+ if (sorts) {
129
+ try {
130
+ const parsedSorts = safeJSONParse(sorts)
131
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
132
+ data.sort((a, b) => {
133
+ for (const sort of parsedSorts) {
134
+ if (!sort.field) continue
135
+ const aVal = getNestedValue(a, sort.field)
136
+ const bVal = getNestedValue(b, sort.field)
137
+ const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
138
+
139
+ let comparison = 0
140
+ if (aVal === null || aVal === undefined) {
141
+ comparison = bVal === null || bVal === undefined ? 0 : -1
142
+ } else if (bVal === null || bVal === undefined) {
143
+ comparison = 1
144
+ } else if (typeof aVal === 'number' && typeof bVal === 'number') {
145
+ comparison = aVal - bVal
146
+ } else if (aVal instanceof Date && bVal instanceof Date) {
147
+ comparison = aVal.getTime() - bVal.getTime()
148
+ } else {
149
+ const aStr = String(aVal)
150
+ const bStr = String(bVal)
151
+ if (aStr < bStr) comparison = -1
152
+ else if (aStr > bStr) comparison = 1
153
+ }
154
+
155
+ if (comparison !== 0) return comparison * sortOrderValue
156
+ }
157
+ return 0
158
+ })
159
+ }
160
+ } catch (err) {
161
+ console.error('Error parsing sorts:', err)
162
+ }
163
+ } else if (sortBy && sortBy.trim()) {
99
164
  data.sort((a, b) => {
100
- const aVal = a[sortBy]
101
- const bVal = b[sortBy]
165
+ const aVal = getNestedValue(a, sortBy)
166
+ const bVal = getNestedValue(b, sortBy)
102
167
  const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
103
- if (aVal < bVal) return -sortOrderValue
104
- if (aVal > bVal) return sortOrderValue
105
- return 0
168
+
169
+ let comparison = 0
170
+ if (aVal === null || aVal === undefined) {
171
+ comparison = bVal === null || bVal === undefined ? 0 : -1
172
+ } else if (bVal === null || bVal === undefined) {
173
+ comparison = 1
174
+ } else if (typeof aVal === 'number' && typeof bVal === 'number') {
175
+ comparison = aVal - bVal
176
+ } else if (aVal instanceof Date && bVal instanceof Date) {
177
+ comparison = aVal.getTime() - bVal.getTime()
178
+ } else {
179
+ const aStr = String(aVal)
180
+ const bStr = String(bVal)
181
+ if (aStr < bStr) comparison = -1
182
+ else if (aStr > bStr) comparison = 1
183
+ }
184
+
185
+ return comparison * sortOrderValue
106
186
  })
107
187
  }
108
188
 
109
- // 4. Apply pagination
110
189
  const limitValue = limit || perPage
111
190
  const pageValue = page ? Math.max(1, parseInt(page)) : undefined
112
191
  const offsetValue = offset !== undefined ? Math.max(0, parseInt(offset)) : (pageValue && perPage ? (pageValue - 1) * Math.max(1, parseInt(perPage)) : 0)
@@ -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
  interface MariaDBConfig {
4
8
  host?: string
@@ -20,6 +24,60 @@ export const generateMariaDBFetcher = (
20
24
 
21
25
  return `import mariadb from 'mariadb'
22
26
 
27
+ ${generateSafeJSONParseCode()}
28
+
29
+ // Helper function to process filters and build conditions
30
+ const processFilters = (filters, conditions, queryParams) => {
31
+ if (!filters) return
32
+
33
+ const parsedFilters = safeJSONParse(filters)
34
+
35
+ if (Array.isArray(parsedFilters)) {
36
+ parsedFilters.forEach((filter) => {
37
+ if (!filter.source || filter.destination === undefined) return
38
+
39
+ const field = \`\\\`\${filter.source}\\\`\`
40
+ const value = filter.destination
41
+ const operand = filter.operand || '='
42
+
43
+ if (Array.isArray(value)) {
44
+ if (value.length === 0) return
45
+ const placeholders = value.map(() => '?').join(', ')
46
+ queryParams.push(...value)
47
+ if (operand === '!=') {
48
+ conditions.push(\`\${field} NOT IN (\${placeholders})\`)
49
+ } else {
50
+ conditions.push(\`\${field} IN (\${placeholders})\`)
51
+ }
52
+ } else {
53
+ if (value === null) {
54
+ if (operand === '=') {
55
+ conditions.push(\`\${field} IS NULL\`)
56
+ } else if (operand === '!=') {
57
+ conditions.push(\`\${field} IS NOT NULL\`)
58
+ }
59
+ } else {
60
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
61
+ const sqlOperator = validOps.includes(operand) ? operand : '='
62
+ conditions.push(\`\${field} \${sqlOperator} ?\`)
63
+ queryParams.push(value)
64
+ }
65
+ }
66
+ })
67
+ } else {
68
+ Object.entries(parsedFilters).forEach(([key, value]) => {
69
+ if (Array.isArray(value)) {
70
+ const placeholders = value.map(() => '?').join(', ')
71
+ queryParams.push(...value)
72
+ conditions.push(\`\\\`\${key}\\\` IN (\${placeholders})\`)
73
+ } else {
74
+ conditions.push(\`\\\`\${key}\\\` = ?\`)
75
+ queryParams.push(value)
76
+ }
77
+ })
78
+ }
79
+ }
80
+
23
81
  ${generateDateFormatterCode()}
24
82
 
25
83
  export default async function handler(req, res) {
@@ -54,7 +112,7 @@ export default async function handler(req, res) {
54
112
  }
55
113
  })
56
114
 
57
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
115
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
58
116
 
59
117
  const conditions = []
60
118
  const queryParams = []
@@ -64,7 +122,7 @@ export default async function handler(req, res) {
64
122
 
65
123
  if (queryColumns) {
66
124
  // Use specified columns
67
- columns = JSON.parse(queryColumns)
125
+ columns = safeJSONParse(queryColumns)
68
126
  } else {
69
127
  // Fallback: Get all columns from information_schema
70
128
  try {
@@ -88,19 +146,8 @@ export default async function handler(req, res) {
88
146
  }
89
147
  }
90
148
 
91
- if (filters) {
92
- const parsedFilters = JSON.parse(filters)
93
- Object.entries(parsedFilters).forEach(([key, value]) => {
94
- if (Array.isArray(value)) {
95
- const placeholders = value.map(() => '?').join(', ')
96
- queryParams.push(...value)
97
- conditions.push(\`\\\`\${key}\\\` IN (\${placeholders})\`)
98
- } else {
99
- conditions.push(\`\\\`\${key}\\\` = ?\`)
100
- queryParams.push(value)
101
- }
102
- })
103
- }
149
+ // Apply filters using helper function
150
+ processFilters(filters, conditions, queryParams)
104
151
 
105
152
  let sql = \`SELECT * FROM \\\`${tableName}\\\`\`
106
153
 
@@ -108,7 +155,21 @@ export default async function handler(req, res) {
108
155
  sql += \` WHERE \${conditions.join(' AND ')}\`
109
156
  }
110
157
 
111
- if (sortBy) {
158
+ // Handle sorts - new array format
159
+ if (sorts) {
160
+ const parsedSorts = safeJSONParse(sorts)
161
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
162
+ const orderClauses = parsedSorts.map((sort) => {
163
+ if (!sort.field) return null
164
+ const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
165
+ return \`\\\`\${sort.field}\\\` \${order}\`
166
+ }).filter(Boolean)
167
+
168
+ if (orderClauses.length > 0) {
169
+ sql += \` ORDER BY \${orderClauses.join(', ')}\`
170
+ }
171
+ }
172
+ } else if (sortBy) {
112
173
  sql += \` ORDER BY \\\`\${sortBy}\\\` \${sortOrder?.toUpperCase() || 'ASC'}\`
113
174
  }
114
175
 
@@ -205,7 +266,8 @@ async function getCount(req, res) {
205
266
 
206
267
  if (queryColumns) {
207
268
  // Use specified columns
208
- columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
269
+ const parsed = safeJSONParse(queryColumns)
270
+ columns = Array.isArray(parsed) ? parsed : [parsed]
209
271
  } else {
210
272
  // Fallback: Get all columns from information_schema
211
273
  try {
@@ -229,13 +291,8 @@ async function getCount(req, res) {
229
291
  }
230
292
  }
231
293
 
232
- if (filters) {
233
- const parsedFilters = JSON.parse(filters)
234
- for (const filter of parsedFilters) {
235
- conditions.push(\`\${filter.column} \${filter.operator} ?\`)
236
- queryParams.push(filter.value)
237
- }
238
- }
294
+ // Apply filters using helper function
295
+ processFilters(filters, conditions, queryParams)
239
296
 
240
297
  let countSql = \`SELECT COUNT(*) as count FROM ${tableName}\`
241
298
  if (conditions.length > 0) {