@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
  interface PostgreSQLConfig {
4
8
  host?: string
@@ -43,6 +47,66 @@ const getClient = () => {
43
47
  })
44
48
  }
45
49
 
50
+ ${generateSafeJSONParseCode()}
51
+
52
+ // Helper function to process filters and build conditions
53
+ const processFilters = (filters, conditions, queryParams, paramIndex) => {
54
+ if (!filters) return paramIndex
55
+
56
+ const parsedFilters = safeJSONParse(filters)
57
+
58
+ if (Array.isArray(parsedFilters)) {
59
+ parsedFilters.forEach((filter) => {
60
+ if (!filter.source || filter.destination === undefined) return
61
+
62
+ const field = filter.source
63
+ const value = filter.destination
64
+ const operand = filter.operand || '='
65
+
66
+ if (Array.isArray(value)) {
67
+ if (value.length === 0) return
68
+ const placeholders = value.map(() => \`$\${paramIndex++}\`)
69
+ queryParams.push(...value)
70
+ if (operand === '!=') {
71
+ conditions.push(\`\${field} NOT IN (\${placeholders.join(', ')})\`)
72
+ } else {
73
+ conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
74
+ }
75
+ } else {
76
+ if (value === null) {
77
+ if (operand === '=') {
78
+ conditions.push(\`\${field} IS NULL\`)
79
+ } else if (operand === '!=') {
80
+ conditions.push(\`\${field} IS NOT NULL\`)
81
+ }
82
+ } else {
83
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
84
+ const sqlOperator = validOps.includes(operand) ? operand : '='
85
+ conditions.push(\`\${field} \${sqlOperator} $\${paramIndex}\`)
86
+ queryParams.push(value)
87
+ paramIndex++
88
+ }
89
+ }
90
+ })
91
+ } else {
92
+ Object.entries(parsedFilters).forEach(([key, value]) => {
93
+ if (Array.isArray(value)) {
94
+ const placeholders = value.map(() => \`$\${paramIndex++}\`)
95
+ queryParams.push(...value)
96
+ conditions.push(\`\${key} IN (\${placeholders.join(', ')})\`)
97
+ } else {
98
+ conditions.push(\`\${key} = $\${paramIndex}\`)
99
+ queryParams.push(value)
100
+ paramIndex++
101
+ }
102
+ })
103
+ }
104
+
105
+ return paramIndex
106
+ }
107
+
108
+ ${generateDateFormatterCode()}
109
+
46
110
  export default async function handler(req, res) {
47
111
  const client = getClient()
48
112
 
@@ -50,7 +114,7 @@ export default async function handler(req, res) {
50
114
  await client.connect()
51
115
  ${schema ? `await client.query('SET search_path TO ${schema}')` : ''}
52
116
 
53
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
117
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
54
118
 
55
119
  const conditions = []
56
120
  const queryParams = []
@@ -61,7 +125,7 @@ export default async function handler(req, res) {
61
125
 
62
126
  if (queryColumns) {
63
127
  // Use specified columns
64
- columns = JSON.parse(queryColumns)
128
+ columns = safeJSONParse(queryColumns)
65
129
  } else {
66
130
  // Fallback: Get all columns from information_schema
67
131
  try {
@@ -95,20 +159,8 @@ export default async function handler(req, res) {
95
159
  }
96
160
  }
97
161
 
98
- if (filters) {
99
- const parsedFilters = JSON.parse(filters)
100
- Object.entries(parsedFilters).forEach(([key, value]) => {
101
- if (Array.isArray(value)) {
102
- const placeholders = value.map(() => \`$\${paramIndex++}\`)
103
- queryParams.push(...value)
104
- conditions.push(\`\${key} IN (\${placeholders.join(', ')})\`)
105
- } else {
106
- conditions.push(\`\${key} = $\${paramIndex}\`)
107
- queryParams.push(value)
108
- paramIndex++
109
- }
110
- })
111
- }
162
+ // Apply filters using helper function
163
+ paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
112
164
 
113
165
  let sql = \`SELECT * FROM ${tableName}\`
114
166
 
@@ -116,7 +168,21 @@ export default async function handler(req, res) {
116
168
  sql += \` WHERE \${conditions.join(' AND ')}\`
117
169
  }
118
170
 
119
- if (sortBy) {
171
+ // Handle sorts - new array format
172
+ if (sorts) {
173
+ const parsedSorts = safeJSONParse(sorts)
174
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
175
+ const orderClauses = parsedSorts.map((sort) => {
176
+ if (!sort.field) return null
177
+ const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
178
+ return \`\${sort.field} \${order}\`
179
+ }).filter(Boolean)
180
+
181
+ if (orderClauses.length > 0) {
182
+ sql += \` ORDER BY \${orderClauses.join(', ')}\`
183
+ }
184
+ }
185
+ } else if (sortBy) {
120
186
  sql += \` ORDER BY \${sortBy} \${sortOrder?.toUpperCase() || 'ASC'}\`
121
187
  }
122
188
 
@@ -136,7 +202,7 @@ export default async function handler(req, res) {
136
202
  const plainRows = rows.map((row) =>
137
203
  row && typeof row.toJSON === 'function' ? row.toJSON() : row
138
204
  )
139
- const safeData = JSON.parse(JSON.stringify(plainRows))
205
+ const safeData = JSON.parse(JSON.stringify(plainRows, dateReplacer))
140
206
 
141
207
  return res.status(200).json({
142
208
  success: true,
@@ -186,7 +252,8 @@ async function getCount(req, res) {
186
252
 
187
253
  if (queryColumns) {
188
254
  // Use specified columns
189
- columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
255
+ const parsed = safeJSONParse(queryColumns)
256
+ columns = Array.isArray(parsed) ? parsed : [parsed]
190
257
  } else {
191
258
  // Fallback: Get all columns from information_schema
192
259
  try {
@@ -218,13 +285,8 @@ async function getCount(req, res) {
218
285
  }
219
286
  }
220
287
 
221
- if (filters) {
222
- const parsedFilters = JSON.parse(filters)
223
- for (const filter of parsedFilters) {
224
- conditions.push(\`\${filter.column} \${filter.operator} $\${paramIndex++}\`)
225
- queryParams.push(filter.value)
226
- }
227
- }
288
+ // Apply filters using helper function
289
+ paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
228
290
 
229
291
  let countSql = \`SELECT COUNT(*) FROM ${tableName}\`
230
292
  if (conditions.length > 0) {
@@ -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 validateRedisConfig = (
4
8
  config: Record<string, unknown>
@@ -63,6 +67,10 @@ export const generateRedisFetcher = (config: Record<string, unknown>): string =>
63
67
 
64
68
  return `import { createClient } from 'redis'
65
69
 
70
+ ${generateSafeJSONParseCode()}
71
+
72
+ ${generateDateFormatterCode()}
73
+
66
74
  export default async function handler(req, res) {
67
75
  let client = null
68
76
  try {
@@ -74,9 +82,23 @@ export default async function handler(req, res) {
74
82
 
75
83
  await client.connect()
76
84
 
77
- const { query, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
85
+ const { query, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
86
+
87
+ let pattern = query || '*'
88
+
89
+ // Extract pattern from filters if available (new format)
90
+ if (filters) {
91
+ const parsedFilters = safeJSONParse(filters)
92
+ if (Array.isArray(parsedFilters)) {
93
+ const patternFilter = parsedFilters.find(f => f.source === 'pattern')
94
+ if (patternFilter) {
95
+ pattern = patternFilter.destination || pattern
96
+ }
97
+ } else {
98
+ pattern = parsedFilters.pattern || pattern
99
+ }
100
+ }
78
101
 
79
- const pattern = (filters && JSON.parse(filters).pattern) || query || '*'
80
102
  const keys = await client.keys(pattern)
81
103
 
82
104
  const limitValue = limit || perPage || 100
@@ -117,7 +139,23 @@ export default async function handler(req, res) {
117
139
  })
118
140
  }
119
141
 
120
- if (sortBy) {
142
+ // Handle sorts - new array format
143
+ if (sorts) {
144
+ const parsedSorts = safeJSONParse(sorts)
145
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
146
+ const primarySort = parsedSorts[0]
147
+ if (primarySort.field) {
148
+ const sortOrderValue = primarySort.order?.toLowerCase() === 'desc' ? -1 : 1
149
+ results.sort((a, b) => {
150
+ const aVal = a[primarySort.field]
151
+ const bVal = b[primarySort.field]
152
+ if (aVal < bVal) return -sortOrderValue
153
+ if (aVal > bVal) return sortOrderValue
154
+ return 0
155
+ })
156
+ }
157
+ }
158
+ } else if (sortBy) {
121
159
  const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
122
160
  results.sort((a, b) => {
123
161
  const aVal = a[sortBy]
@@ -128,7 +166,7 @@ export default async function handler(req, res) {
128
166
  })
129
167
  }
130
168
 
131
- const safeData = JSON.parse(JSON.stringify(results))
169
+ const safeData = JSON.parse(JSON.stringify(results, dateReplacer))
132
170
 
133
171
  return res.status(200).json({
134
172
  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
  interface RedshiftConfig {
4
8
  host?: string
@@ -49,6 +53,10 @@ const getClient = () => {
49
53
  })
50
54
  }
51
55
 
56
+ ${generateSafeJSONParseCode()}
57
+
58
+ ${generateDateFormatterCode()}
59
+
52
60
  export default async function handler(req, res) {
53
61
  const client = getClient()
54
62
 
@@ -56,7 +64,7 @@ export default async function handler(req, res) {
56
64
  await client.connect()
57
65
  ${schema ? `await client.query('SET search_path TO ${schema}')` : ''}
58
66
 
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
  const queryParams = []
@@ -66,7 +74,8 @@ export default async function handler(req, res) {
66
74
  let columns = []
67
75
 
68
76
  if (queryColumns) {
69
- columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
77
+ const parsed = safeJSONParse(queryColumns)
78
+ columns = Array.isArray(parsed) ? parsed : [parsed]
70
79
  } else {
71
80
  // Fallback: Get all columns from information_schema
72
81
  try {
@@ -96,19 +105,65 @@ export default async function handler(req, res) {
96
105
  }
97
106
  }
98
107
 
108
+ // Helper to sanitize identifier (prevent SQL injection in column names)
109
+ const sanitizeIdentifier = (name) => {
110
+ // Only allow alphanumeric, underscore, and dot (for schema.table)
111
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
112
+ throw new Error(\`Invalid identifier: \${name}\`)
113
+ }
114
+ return \`"\${name}"\`
115
+ }
116
+
99
117
  if (filters) {
100
- const parsedFilters = JSON.parse(filters)
101
- Object.entries(parsedFilters).forEach(([key, value]) => {
102
- if (Array.isArray(value)) {
103
- const placeholders = value.map(() => \`$\${paramIndex++}\`)
104
- queryParams.push(...value)
105
- conditions.push(\`\${key} IN (\${placeholders.join(', ')})\`)
106
- } else {
107
- conditions.push(\`\${key} = $\${paramIndex}\`)
108
- queryParams.push(value)
109
- paramIndex++
110
- }
111
- })
118
+ const parsedFilters = safeJSONParse(filters)
119
+
120
+ if (Array.isArray(parsedFilters)) {
121
+ parsedFilters.forEach((filter) => {
122
+ if (!filter.source || filter.destination === undefined) return
123
+
124
+ const field = sanitizeIdentifier(filter.source)
125
+ const value = filter.destination
126
+ const operand = filter.operand || '='
127
+
128
+ if (Array.isArray(value)) {
129
+ if (value.length === 0) return
130
+ const placeholders = value.map(() => \`$\${paramIndex++}\`)
131
+ queryParams.push(...value)
132
+ if (operand === '!=') {
133
+ conditions.push(\`\${field} NOT IN (\${placeholders.join(', ')})\`)
134
+ } else {
135
+ conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
136
+ }
137
+ } else {
138
+ if (value === null) {
139
+ if (operand === '=') {
140
+ conditions.push(\`\${field} IS NULL\`)
141
+ } else if (operand === '!=') {
142
+ conditions.push(\`\${field} IS NOT NULL\`)
143
+ }
144
+ } else {
145
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
146
+ const sqlOperator = validOps.includes(operand) ? operand : '='
147
+ conditions.push(\`\${field} \${sqlOperator} $\${paramIndex}\`)
148
+ queryParams.push(value)
149
+ paramIndex++
150
+ }
151
+ }
152
+ })
153
+ } else {
154
+ Object.entries(parsedFilters).forEach(([key, value]) => {
155
+ const field = sanitizeIdentifier(key)
156
+ if (Array.isArray(value)) {
157
+ const placeholders = value.map(() => \`$\${paramIndex++}\`)
158
+ queryParams.push(...value)
159
+ conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
160
+ } else {
161
+ conditions.push(\`\${field} = $\${paramIndex}\`)
162
+ queryParams.push(value)
163
+ paramIndex++
164
+ }
165
+ })
166
+ }
112
167
  }
113
168
 
114
169
  let sql = \`SELECT * FROM ${tableName}\`
@@ -117,8 +172,22 @@ export default async function handler(req, res) {
117
172
  sql += \` WHERE \${conditions.join(' AND ')}\`
118
173
  }
119
174
 
120
- if (sortBy) {
121
- sql += \` ORDER BY \${sortBy} \${sortOrder?.toUpperCase() || 'ASC'}\`
175
+ // Handle sorts - new array format
176
+ if (sorts) {
177
+ const parsedSorts = safeJSONParse(sorts)
178
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
179
+ const orderClauses = parsedSorts.map((sort) => {
180
+ if (!sort.field) return null
181
+ const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
182
+ return \`\${sanitizeIdentifier(sort.field)} \${order}\`
183
+ }).filter(Boolean)
184
+
185
+ if (orderClauses.length > 0) {
186
+ sql += \` ORDER BY \${orderClauses.join(', ')}\`
187
+ }
188
+ }
189
+ } else if (sortBy) {
190
+ sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${sortOrder?.toUpperCase() || 'ASC'}\`
122
191
  }
123
192
 
124
193
  const limitValue = limit || perPage
@@ -137,7 +206,7 @@ export default async function handler(req, res) {
137
206
  const plainRows = rows.map((row) =>
138
207
  row && typeof row.toJSON === 'function' ? row.toJSON() : row
139
208
  )
140
- const safeData = JSON.parse(JSON.stringify(plainRows))
209
+ const safeData = JSON.parse(JSON.stringify(plainRows, dateReplacer))
141
210
 
142
211
  return res.status(200).json({
143
212
  success: true,
@@ -1,3 +1,9 @@
1
+ import {
2
+ generateDateFormatterCode,
3
+ generateSortFilterHelperCode,
4
+ generateSafeJSONParseCode,
5
+ } from '../utils'
6
+
1
7
  export const validateRESTAPIConfig = (
2
8
  config: Record<string, unknown>
3
9
  ): { isValid: boolean; error?: string } => {
@@ -75,9 +81,15 @@ export const generateRESTAPIFetcher = (config: Record<string, unknown>): string
75
81
 
76
82
  return `import fetch from 'node-fetch'
77
83
 
84
+ ${generateSafeJSONParseCode()}
85
+
86
+ ${generateDateFormatterCode()}
87
+
88
+ ${generateSortFilterHelperCode()}
89
+
78
90
  export default async function handler(req, res) {
79
91
  try {
80
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
92
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
81
93
 
82
94
  const url = ${JSON.stringify(restConfig.url)}
83
95
  const method = ${JSON.stringify(restConfig.method || 'GET')}
@@ -116,18 +128,17 @@ export default async function handler(req, res) {
116
128
 
117
129
  let data = await response.json()
118
130
 
119
- // Apply filtering, sorting, and pagination if data is an array
120
131
  if (Array.isArray(data)) {
121
- // 1. Apply search filter
122
132
  if (query && query.trim()) {
123
133
  const searchQuery = query.toLowerCase()
124
134
 
125
135
  if (queryColumns) {
126
136
  try {
127
- const columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
137
+ const parsed = safeJSONParse(queryColumns)
138
+ const columns = Array.isArray(parsed) ? parsed : [parsed]
128
139
  data = data.filter((item) => {
129
140
  return columns.some((col) => {
130
- const value = item[col]
141
+ const value = getNestedValue(item, col)
131
142
  if (value === null || value === undefined) return false
132
143
  return String(value).toLowerCase().includes(searchQuery)
133
144
  })
@@ -136,7 +147,6 @@ export default async function handler(req, res) {
136
147
  console.error('Error parsing queryColumns:', err)
137
148
  }
138
149
  } else {
139
- // Search across all fields
140
150
  data = data.filter((item) => {
141
151
  try {
142
152
  const stringified = JSON.stringify(item).toLowerCase()
@@ -148,36 +158,107 @@ export default async function handler(req, res) {
148
158
  }
149
159
  }
150
160
 
151
- // 2. Apply custom filters
152
161
  if (filters) {
153
162
  try {
154
- const parsedFilters = typeof filters === 'string' ? JSON.parse(filters) : filters
155
- data = data.filter((item) => {
156
- return Object.entries(parsedFilters).every(([key, value]) => {
157
- if (Array.isArray(value)) {
158
- return value.includes(item[key])
159
- }
160
- return item[key] === value
163
+ const parsedFilters = safeJSONParse(filters)
164
+
165
+ if (Array.isArray(parsedFilters)) {
166
+ data = data.filter((item) => {
167
+ return parsedFilters.every((filter) => {
168
+ if (!filter.source || filter.destination === undefined) return true
169
+
170
+ const field = filter.source
171
+ const value = getNestedValue(item, field)
172
+ const target = filter.destination
173
+ const operand = filter.operand || '='
174
+
175
+ if (Array.isArray(target)) {
176
+ if (operand === '!=') {
177
+ return !target.includes(value)
178
+ }
179
+ return target.includes(value)
180
+ }
181
+
182
+ return compareValues(value, target, operand)
183
+ })
161
184
  })
162
- })
185
+ } else {
186
+ data = data.filter((item) => {
187
+ return Object.entries(parsedFilters).every(([key, value]) => {
188
+ const itemValue = getNestedValue(item, key)
189
+ if (Array.isArray(value)) {
190
+ return value.includes(itemValue)
191
+ }
192
+ return compareValues(itemValue, value, '=')
193
+ })
194
+ })
195
+ }
163
196
  } catch (err) {
164
197
  console.error('Error parsing filters:', err)
165
198
  }
166
199
  }
167
200
 
168
- // 3. Apply sorting
169
- if (sortBy && sortBy.trim()) {
201
+ if (sorts) {
202
+ try {
203
+ const parsedSorts = safeJSONParse(sorts)
204
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
205
+ data.sort((a, b) => {
206
+ for (const sort of parsedSorts) {
207
+ if (!sort.field) continue
208
+ const aVal = getNestedValue(a, sort.field)
209
+ const bVal = getNestedValue(b, sort.field)
210
+ const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
211
+
212
+ let comparison = 0
213
+ if (aVal === null || aVal === undefined) {
214
+ comparison = bVal === null || bVal === undefined ? 0 : -1
215
+ } else if (bVal === null || bVal === undefined) {
216
+ comparison = 1
217
+ } else if (typeof aVal === 'number' && typeof bVal === 'number') {
218
+ comparison = aVal - bVal
219
+ } else if (aVal instanceof Date && bVal instanceof Date) {
220
+ comparison = aVal.getTime() - bVal.getTime()
221
+ } else {
222
+ const aStr = String(aVal)
223
+ const bStr = String(bVal)
224
+ if (aStr < bStr) comparison = -1
225
+ else if (aStr > bStr) comparison = 1
226
+ }
227
+
228
+ if (comparison !== 0) return comparison * sortOrderValue
229
+ }
230
+ return 0
231
+ })
232
+ }
233
+ } catch (err) {
234
+ console.error('Error parsing sorts:', err)
235
+ }
236
+ } else if (sortBy && sortBy.trim()) {
170
237
  data.sort((a, b) => {
171
- const aVal = a[sortBy]
172
- const bVal = b[sortBy]
238
+ const aVal = getNestedValue(a, sortBy)
239
+ const bVal = getNestedValue(b, sortBy)
173
240
  const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
174
- if (aVal < bVal) return -sortOrderValue
175
- if (aVal > bVal) return sortOrderValue
176
- return 0
241
+
242
+ let comparison = 0
243
+ if (aVal === null || aVal === undefined) {
244
+ comparison = bVal === null || bVal === undefined ? 0 : -1
245
+ } else if (bVal === null || bVal === undefined) {
246
+ comparison = 1
247
+ } else if (typeof aVal === 'number' && typeof bVal === 'number') {
248
+ comparison = aVal - bVal
249
+ } else if (aVal instanceof Date && bVal instanceof Date) {
250
+ comparison = aVal.getTime() - bVal.getTime()
251
+ } else {
252
+ const aStr = String(aVal)
253
+ const bStr = String(bVal)
254
+ if (aStr < bStr) comparison = -1
255
+ else if (aStr > bStr) comparison = 1
256
+ }
257
+
258
+ return comparison * sortOrderValue
177
259
  })
178
260
  }
179
261
 
180
- // 4. Apply pagination
181
262
  const limitValue = limit || perPage
182
263
  const pageValue = page ? Math.max(1, parseInt(page)) : undefined
183
264
  const offsetValue = offset !== undefined ? Math.max(0, parseInt(offset)) : (pageValue && perPage ? (pageValue - 1) * Math.max(1, parseInt(perPage)) : 0)
@@ -190,7 +271,7 @@ export default async function handler(req, res) {
190
271
  }
191
272
  }
192
273
 
193
- const safeData = JSON.parse(JSON.stringify(data))
274
+ const safeData = JSON.parse(JSON.stringify(data, dateReplacer))
194
275
 
195
276
  return res.status(200).json({
196
277
  success: true,