@teleporthq/teleport-plugin-next-data-source 0.40.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARRAY_MAPPER_PAGINATION.md +1128 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/SEARCH_IMPLEMENTATION_SUMMARY.md +983 -0
- package/__tests__/fetchers.test.ts +545 -0
- package/__tests__/integration.test.ts +561 -0
- package/__tests__/mocks.ts +241 -0
- package/__tests__/pagination.test.ts +31 -0
- package/__tests__/plugin.test.ts +577 -0
- package/__tests__/utils.test.ts +430 -0
- package/__tests__/validation.test.ts +348 -0
- package/dist/cjs/array-mapper-pagination.d.ts +32 -0
- package/dist/cjs/array-mapper-pagination.d.ts.map +1 -0
- package/dist/cjs/array-mapper-pagination.js +77 -0
- package/dist/cjs/array-mapper-pagination.js.map +1 -0
- package/dist/cjs/count-fetchers.d.ts +12 -0
- package/dist/cjs/count-fetchers.d.ts.map +1 -0
- package/dist/cjs/count-fetchers.js +46 -0
- package/dist/cjs/count-fetchers.js.map +1 -0
- package/dist/cjs/data-source-fetchers.d.ts +14 -0
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -0
- package/dist/cjs/data-source-fetchers.js +185 -0
- package/dist/cjs/data-source-fetchers.js.map +1 -0
- package/dist/cjs/fetchers/airtable.d.ts +6 -0
- package/dist/cjs/fetchers/airtable.d.ts.map +1 -0
- package/dist/cjs/fetchers/airtable.js +27 -0
- package/dist/cjs/fetchers/airtable.js.map +1 -0
- package/dist/cjs/fetchers/clickhouse.d.ts +6 -0
- package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -0
- package/dist/cjs/fetchers/clickhouse.js +29 -0
- package/dist/cjs/fetchers/clickhouse.js.map +1 -0
- package/dist/cjs/fetchers/csv-file.d.ts +7 -0
- package/dist/cjs/fetchers/csv-file.d.ts.map +1 -0
- package/dist/cjs/fetchers/csv-file.js +36 -0
- package/dist/cjs/fetchers/csv-file.js.map +1 -0
- package/dist/cjs/fetchers/firestore.d.ts +6 -0
- package/dist/cjs/fetchers/firestore.d.ts.map +1 -0
- package/dist/cjs/fetchers/firestore.js +35 -0
- package/dist/cjs/fetchers/firestore.js.map +1 -0
- package/dist/cjs/fetchers/google-sheets.d.ts +6 -0
- package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -0
- package/dist/cjs/fetchers/google-sheets.js +30 -0
- package/dist/cjs/fetchers/google-sheets.js.map +1 -0
- package/dist/cjs/fetchers/index.d.ts +17 -0
- package/dist/cjs/fetchers/index.d.ts.map +1 -0
- package/dist/cjs/fetchers/index.js +56 -0
- package/dist/cjs/fetchers/index.js.map +1 -0
- package/dist/cjs/fetchers/javascript.d.ts +7 -0
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -0
- package/dist/cjs/fetchers/javascript.js +40 -0
- package/dist/cjs/fetchers/javascript.js.map +1 -0
- package/dist/cjs/fetchers/mariadb.d.ts +3 -0
- package/dist/cjs/fetchers/mariadb.d.ts.map +1 -0
- package/dist/cjs/fetchers/mariadb.js +23 -0
- package/dist/cjs/fetchers/mariadb.js.map +1 -0
- package/dist/cjs/fetchers/mongodb.d.ts +7 -0
- package/dist/cjs/fetchers/mongodb.d.ts.map +1 -0
- package/dist/cjs/fetchers/mongodb.js +52 -0
- package/dist/cjs/fetchers/mongodb.js.map +1 -0
- package/dist/cjs/fetchers/mysql.d.ts +3 -0
- package/dist/cjs/fetchers/mysql.d.ts.map +1 -0
- package/dist/cjs/fetchers/mysql.js +30 -0
- package/dist/cjs/fetchers/mysql.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts +3 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -0
- package/dist/cjs/fetchers/postgresql.js +25 -0
- package/dist/cjs/fetchers/postgresql.js.map +1 -0
- package/dist/cjs/fetchers/redis.d.ts +6 -0
- package/dist/cjs/fetchers/redis.d.ts.map +1 -0
- package/dist/cjs/fetchers/redis.js +46 -0
- package/dist/cjs/fetchers/redis.js.map +1 -0
- package/dist/cjs/fetchers/redshift.d.ts +2 -0
- package/dist/cjs/fetchers/redshift.d.ts.map +1 -0
- package/dist/cjs/fetchers/redshift.js +24 -0
- package/dist/cjs/fetchers/redshift.js.map +1 -0
- package/dist/cjs/fetchers/rest-api.d.ts +6 -0
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -0
- package/dist/cjs/fetchers/rest-api.js +58 -0
- package/dist/cjs/fetchers/rest-api.js.map +1 -0
- package/dist/cjs/fetchers/static-collection.d.ts +7 -0
- package/dist/cjs/fetchers/static-collection.d.ts.map +1 -0
- package/dist/cjs/fetchers/static-collection.js +24 -0
- package/dist/cjs/fetchers/static-collection.js.map +1 -0
- package/dist/cjs/fetchers/supabase.d.ts +7 -0
- package/dist/cjs/fetchers/supabase.d.ts.map +1 -0
- package/dist/cjs/fetchers/supabase.js +42 -0
- package/dist/cjs/fetchers/supabase.js.map +1 -0
- package/dist/cjs/fetchers/turso.d.ts +6 -0
- package/dist/cjs/fetchers/turso.d.ts.map +1 -0
- package/dist/cjs/fetchers/turso.js +25 -0
- package/dist/cjs/fetchers/turso.js.map +1 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +325 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts +5 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -0
- package/dist/cjs/pagination-plugin.js +1484 -0
- package/dist/cjs/pagination-plugin.js.map +1 -0
- package/dist/cjs/pagination-with-count.d.ts +6 -0
- package/dist/cjs/pagination-with-count.d.ts.map +1 -0
- package/dist/cjs/pagination-with-count.js +63 -0
- package/dist/cjs/pagination-with-count.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -0
- package/dist/cjs/utils.d.ts +31 -0
- package/dist/cjs/utils.d.ts.map +1 -0
- package/dist/cjs/utils.js +763 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/cjs/validation.d.ts +5 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +29 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/esm/array-mapper-pagination.d.ts +32 -0
- package/dist/esm/array-mapper-pagination.d.ts.map +1 -0
- package/dist/esm/array-mapper-pagination.js +72 -0
- package/dist/esm/array-mapper-pagination.js.map +1 -0
- package/dist/esm/count-fetchers.d.ts +12 -0
- package/dist/esm/count-fetchers.d.ts.map +1 -0
- package/dist/esm/count-fetchers.js +35 -0
- package/dist/esm/count-fetchers.js.map +1 -0
- package/dist/esm/data-source-fetchers.d.ts +14 -0
- package/dist/esm/data-source-fetchers.d.ts.map +1 -0
- package/dist/esm/data-source-fetchers.js +179 -0
- package/dist/esm/data-source-fetchers.js.map +1 -0
- package/dist/esm/fetchers/airtable.d.ts +6 -0
- package/dist/esm/fetchers/airtable.d.ts.map +1 -0
- package/dist/esm/fetchers/airtable.js +22 -0
- package/dist/esm/fetchers/airtable.js.map +1 -0
- package/dist/esm/fetchers/clickhouse.d.ts +6 -0
- package/dist/esm/fetchers/clickhouse.d.ts.map +1 -0
- package/dist/esm/fetchers/clickhouse.js +24 -0
- package/dist/esm/fetchers/clickhouse.js.map +1 -0
- package/dist/esm/fetchers/csv-file.d.ts +7 -0
- package/dist/esm/fetchers/csv-file.d.ts.map +1 -0
- package/dist/esm/fetchers/csv-file.js +30 -0
- package/dist/esm/fetchers/csv-file.js.map +1 -0
- package/dist/esm/fetchers/firestore.d.ts +6 -0
- package/dist/esm/fetchers/firestore.d.ts.map +1 -0
- package/dist/esm/fetchers/firestore.js +30 -0
- package/dist/esm/fetchers/firestore.js.map +1 -0
- package/dist/esm/fetchers/google-sheets.d.ts +6 -0
- package/dist/esm/fetchers/google-sheets.d.ts.map +1 -0
- package/dist/esm/fetchers/google-sheets.js +25 -0
- package/dist/esm/fetchers/google-sheets.js.map +1 -0
- package/dist/esm/fetchers/index.d.ts +17 -0
- package/dist/esm/fetchers/index.d.ts.map +1 -0
- package/dist/esm/fetchers/index.js +17 -0
- package/dist/esm/fetchers/index.js.map +1 -0
- package/dist/esm/fetchers/javascript.d.ts +7 -0
- package/dist/esm/fetchers/javascript.d.ts.map +1 -0
- package/dist/esm/fetchers/javascript.js +34 -0
- package/dist/esm/fetchers/javascript.js.map +1 -0
- package/dist/esm/fetchers/mariadb.d.ts +3 -0
- package/dist/esm/fetchers/mariadb.d.ts.map +1 -0
- package/dist/esm/fetchers/mariadb.js +18 -0
- package/dist/esm/fetchers/mariadb.js.map +1 -0
- package/dist/esm/fetchers/mongodb.d.ts +7 -0
- package/dist/esm/fetchers/mongodb.d.ts.map +1 -0
- package/dist/esm/fetchers/mongodb.js +46 -0
- package/dist/esm/fetchers/mongodb.js.map +1 -0
- package/dist/esm/fetchers/mysql.d.ts +3 -0
- package/dist/esm/fetchers/mysql.d.ts.map +1 -0
- package/dist/esm/fetchers/mysql.js +25 -0
- package/dist/esm/fetchers/mysql.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts +3 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -0
- package/dist/esm/fetchers/postgresql.js +20 -0
- package/dist/esm/fetchers/postgresql.js.map +1 -0
- package/dist/esm/fetchers/redis.d.ts +6 -0
- package/dist/esm/fetchers/redis.d.ts.map +1 -0
- package/dist/esm/fetchers/redis.js +41 -0
- package/dist/esm/fetchers/redis.js.map +1 -0
- package/dist/esm/fetchers/redshift.d.ts +2 -0
- package/dist/esm/fetchers/redshift.d.ts.map +1 -0
- package/dist/esm/fetchers/redshift.js +20 -0
- package/dist/esm/fetchers/redshift.js.map +1 -0
- package/dist/esm/fetchers/rest-api.d.ts +6 -0
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -0
- package/dist/esm/fetchers/rest-api.js +53 -0
- package/dist/esm/fetchers/rest-api.js.map +1 -0
- package/dist/esm/fetchers/static-collection.d.ts +7 -0
- package/dist/esm/fetchers/static-collection.d.ts.map +1 -0
- package/dist/esm/fetchers/static-collection.js +18 -0
- package/dist/esm/fetchers/static-collection.js.map +1 -0
- package/dist/esm/fetchers/supabase.d.ts +7 -0
- package/dist/esm/fetchers/supabase.d.ts.map +1 -0
- package/dist/esm/fetchers/supabase.js +36 -0
- package/dist/esm/fetchers/supabase.js.map +1 -0
- package/dist/esm/fetchers/turso.d.ts +6 -0
- package/dist/esm/fetchers/turso.d.ts.map +1 -0
- package/dist/esm/fetchers/turso.js +20 -0
- package/dist/esm/fetchers/turso.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +306 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts +5 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -0
- package/dist/esm/pagination-plugin.js +1457 -0
- package/dist/esm/pagination-plugin.js.map +1 -0
- package/dist/esm/pagination-with-count.d.ts +6 -0
- package/dist/esm/pagination-with-count.d.ts.map +1 -0
- package/dist/esm/pagination-with-count.js +34 -0
- package/dist/esm/pagination-with-count.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -0
- package/dist/esm/utils.d.ts +31 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +722 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/validation.d.ts +5 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +25 -0
- package/dist/esm/validation.js.map +1 -0
- package/package.json +33 -0
- package/src/array-mapper-pagination.ts +113 -0
- package/src/count-fetchers.ts +99 -0
- package/src/data-source-fetchers.ts +313 -0
- package/src/fetchers/airtable.ts +153 -0
- package/src/fetchers/clickhouse.ts +127 -0
- package/src/fetchers/csv-file.ts +163 -0
- package/src/fetchers/firestore.ts +138 -0
- package/src/fetchers/google-sheets.ts +189 -0
- package/src/fetchers/index.ts +32 -0
- package/src/fetchers/javascript.ts +150 -0
- package/src/fetchers/mariadb.ts +230 -0
- package/src/fetchers/mongodb.ts +239 -0
- package/src/fetchers/mysql.ts +237 -0
- package/src/fetchers/postgresql.ts +247 -0
- package/src/fetchers/redis.ts +152 -0
- package/src/fetchers/redshift.ts +138 -0
- package/src/fetchers/rest-api.ts +148 -0
- package/src/fetchers/static-collection.ts +149 -0
- package/src/fetchers/supabase.ts +246 -0
- package/src/fetchers/turso.ts +131 -0
- package/src/index.ts +352 -0
- package/src/pagination-plugin.ts +2335 -0
- package/src/pagination-with-count.ts +89 -0
- package/src/utils.ts +1013 -0
- package/src/validation.ts +32 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { replaceSecretReference } from '../utils'
|
|
2
|
+
|
|
3
|
+
interface PostgreSQLConfig {
|
|
4
|
+
host?: string
|
|
5
|
+
port?: number
|
|
6
|
+
user?: string
|
|
7
|
+
username?: string
|
|
8
|
+
password?: string
|
|
9
|
+
database?: string
|
|
10
|
+
ssl?: boolean | { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
|
|
11
|
+
sslConfig?: { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
|
|
12
|
+
options?: { schema?: string }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const generatePostgreSQLFetcher = (
|
|
16
|
+
config: Record<string, unknown>,
|
|
17
|
+
tableName: string
|
|
18
|
+
): string => {
|
|
19
|
+
const pgConfig = config as PostgreSQLConfig
|
|
20
|
+
const schema = pgConfig.options?.schema
|
|
21
|
+
|
|
22
|
+
return `import { Pool } from 'pg'
|
|
23
|
+
|
|
24
|
+
let pool = null
|
|
25
|
+
|
|
26
|
+
const getPool = () => {
|
|
27
|
+
if (pool) return pool
|
|
28
|
+
|
|
29
|
+
pool = new Pool({
|
|
30
|
+
host: ${JSON.stringify(pgConfig.host)},
|
|
31
|
+
port: ${pgConfig.port || 5432},
|
|
32
|
+
user: ${JSON.stringify(pgConfig.user || pgConfig.username)},
|
|
33
|
+
password: ${replaceSecretReference(pgConfig.password)},
|
|
34
|
+
database: ${JSON.stringify(pgConfig.database)},
|
|
35
|
+
ssl: ${
|
|
36
|
+
pgConfig.ssl === false
|
|
37
|
+
? 'false'
|
|
38
|
+
: pgConfig.sslConfig
|
|
39
|
+
? `{
|
|
40
|
+
${pgConfig.sslConfig.ca ? `ca: ${replaceSecretReference(pgConfig.sslConfig.ca)},` : ''}
|
|
41
|
+
${pgConfig.sslConfig.cert ? `cert: ${replaceSecretReference(pgConfig.sslConfig.cert)},` : ''}
|
|
42
|
+
${pgConfig.sslConfig.key ? `key: ${replaceSecretReference(pgConfig.sslConfig.key)},` : ''}
|
|
43
|
+
rejectUnauthorized: false
|
|
44
|
+
}`
|
|
45
|
+
: '{ rejectUnauthorized: false }'
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return pool
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default async function handler(req, res) {
|
|
53
|
+
try {
|
|
54
|
+
const pool = getPool()
|
|
55
|
+
${schema ? `await pool.query('SET search_path TO ${schema}')` : ''}
|
|
56
|
+
|
|
57
|
+
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
|
|
58
|
+
|
|
59
|
+
const conditions = []
|
|
60
|
+
const queryParams = []
|
|
61
|
+
let paramIndex = 1
|
|
62
|
+
|
|
63
|
+
if (query) {
|
|
64
|
+
let columns = []
|
|
65
|
+
|
|
66
|
+
if (queryColumns) {
|
|
67
|
+
// Use specified columns
|
|
68
|
+
columns = JSON.parse(queryColumns)
|
|
69
|
+
} else {
|
|
70
|
+
// Fallback: Get all columns from information_schema
|
|
71
|
+
try {
|
|
72
|
+
const schemaQuery = \`
|
|
73
|
+
SELECT column_name
|
|
74
|
+
FROM information_schema.columns
|
|
75
|
+
WHERE table_name = $1
|
|
76
|
+
${schema ? `AND table_schema = $2` : ''}
|
|
77
|
+
ORDER BY ordinal_position
|
|
78
|
+
\`
|
|
79
|
+
const schemaParams = schema
|
|
80
|
+
? [${JSON.stringify(tableName)}, ${JSON.stringify(schema)}]
|
|
81
|
+
: [${JSON.stringify(tableName)}]
|
|
82
|
+
|
|
83
|
+
const schemaResult = await pool.query(schemaQuery, schemaParams)
|
|
84
|
+
columns = schemaResult.rows.map(row => row.column_name)
|
|
85
|
+
} catch (schemaError) {
|
|
86
|
+
console.warn('Failed to fetch column names from information_schema:', schemaError.message)
|
|
87
|
+
// Continue without search if we can't get columns
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (columns.length > 0) {
|
|
92
|
+
const searchConditions = columns.map((col) => {
|
|
93
|
+
const condition = \`\${col}::text ILIKE $\${paramIndex}\`
|
|
94
|
+
paramIndex++
|
|
95
|
+
return condition
|
|
96
|
+
})
|
|
97
|
+
columns.forEach(() => queryParams.push(\`%\${query}%\`))
|
|
98
|
+
conditions.push(\`(\${searchConditions.join(' OR ')})\`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (filters) {
|
|
103
|
+
const parsedFilters = JSON.parse(filters)
|
|
104
|
+
Object.entries(parsedFilters).forEach(([key, value]) => {
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
const placeholders = value.map(() => \`$\${paramIndex++}\`)
|
|
107
|
+
queryParams.push(...value)
|
|
108
|
+
conditions.push(\`\${key} IN (\${placeholders.join(', ')})\`)
|
|
109
|
+
} else {
|
|
110
|
+
conditions.push(\`\${key} = $\${paramIndex}\`)
|
|
111
|
+
queryParams.push(value)
|
|
112
|
+
paramIndex++
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let sql = \`SELECT * FROM ${tableName}\`
|
|
118
|
+
|
|
119
|
+
if (conditions.length > 0) {
|
|
120
|
+
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (sortBy) {
|
|
124
|
+
sql += \` ORDER BY \${sortBy} \${sortOrder?.toUpperCase() || 'ASC'}\`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const limitValue = limit || perPage
|
|
128
|
+
const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
|
|
129
|
+
|
|
130
|
+
if (limitValue) {
|
|
131
|
+
sql += \` LIMIT \${limitValue}\`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (offsetValue !== undefined) {
|
|
135
|
+
sql += \` OFFSET \${offsetValue}\`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = await pool.query(sql, queryParams)
|
|
139
|
+
const rows = Array.isArray(result?.rows) ? result.rows : []
|
|
140
|
+
const plainRows = rows.map((row) =>
|
|
141
|
+
row && typeof row.toJSON === 'function' ? row.toJSON() : row
|
|
142
|
+
)
|
|
143
|
+
const safeData = JSON.parse(JSON.stringify(plainRows))
|
|
144
|
+
|
|
145
|
+
return res.status(200).json({
|
|
146
|
+
success: true,
|
|
147
|
+
data: safeData,
|
|
148
|
+
timestamp: Date.now()
|
|
149
|
+
})
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('PostgreSQL fetch error:', error)
|
|
152
|
+
return res.status(500).json({
|
|
153
|
+
success: false,
|
|
154
|
+
error: error.message || 'Failed to fetch data',
|
|
155
|
+
timestamp: Date.now()
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const generatePostgreSQLCountFetcher = (
|
|
163
|
+
config: Record<string, unknown>,
|
|
164
|
+
tableName: string
|
|
165
|
+
): string => {
|
|
166
|
+
const pgConfig = config as PostgreSQLConfig
|
|
167
|
+
const hasSchema = !!pgConfig.options?.schema
|
|
168
|
+
|
|
169
|
+
return `
|
|
170
|
+
async function getCount(req, res) {
|
|
171
|
+
const pool = getPool()
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const { query, queryColumns, filters } = req.query
|
|
175
|
+
const conditions = []
|
|
176
|
+
const queryParams = []
|
|
177
|
+
let paramIndex = 1
|
|
178
|
+
|
|
179
|
+
if (query) {
|
|
180
|
+
let columns = []
|
|
181
|
+
|
|
182
|
+
if (queryColumns) {
|
|
183
|
+
// Use specified columns
|
|
184
|
+
columns = typeof queryColumns === 'string' ? JSON.parse(queryColumns) : (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
|
|
185
|
+
} else {
|
|
186
|
+
// Fallback: Get all columns from information_schema
|
|
187
|
+
try {
|
|
188
|
+
const schemaQuery = \`
|
|
189
|
+
SELECT column_name
|
|
190
|
+
FROM information_schema.columns
|
|
191
|
+
WHERE table_name = $1
|
|
192
|
+
${hasSchema ? `AND table_schema = $2` : ''}
|
|
193
|
+
ORDER BY ordinal_position
|
|
194
|
+
\`
|
|
195
|
+
const schemaParams = ${
|
|
196
|
+
hasSchema
|
|
197
|
+
? `[${JSON.stringify(tableName)}, ${JSON.stringify(pgConfig.options!.schema)}]`
|
|
198
|
+
: `[${JSON.stringify(tableName)}]`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const schemaResult = await pool.query(schemaQuery, schemaParams)
|
|
202
|
+
columns = schemaResult.rows.map(row => row.column_name)
|
|
203
|
+
} catch (schemaError) {
|
|
204
|
+
console.warn('Failed to fetch column names from information_schema:', schemaError.message)
|
|
205
|
+
// Continue without search if we can't get columns
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (columns.length > 0) {
|
|
210
|
+
const searchConditions = columns.map(col => \`\${col}::text ILIKE $\${paramIndex++}\`).join(' OR ')
|
|
211
|
+
conditions.push(\`(\${searchConditions})\`)
|
|
212
|
+
columns.forEach(() => queryParams.push(\`%\${query}%\`))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (filters) {
|
|
217
|
+
const parsedFilters = JSON.parse(filters)
|
|
218
|
+
for (const filter of parsedFilters) {
|
|
219
|
+
conditions.push(\`\${filter.column} \${filter.operator} $\${paramIndex++}\`)
|
|
220
|
+
queryParams.push(filter.value)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let countSql = \`SELECT COUNT(*) FROM ${tableName}\`
|
|
225
|
+
if (conditions.length > 0) {
|
|
226
|
+
countSql += \` WHERE \${conditions.join(' AND ')}\`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = await pool.query(countSql, queryParams)
|
|
230
|
+
const count = parseInt(result.rows[0].count, 10)
|
|
231
|
+
|
|
232
|
+
return res.status(200).json({
|
|
233
|
+
success: true,
|
|
234
|
+
count: count,
|
|
235
|
+
timestamp: Date.now()
|
|
236
|
+
})
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Error getting count:', error)
|
|
239
|
+
return res.status(500).json({
|
|
240
|
+
success: false,
|
|
241
|
+
error: error.message || 'Failed to get count',
|
|
242
|
+
timestamp: Date.now()
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
`
|
|
247
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { replaceSecretReference } from '../utils'
|
|
2
|
+
|
|
3
|
+
export const validateRedisConfig = (
|
|
4
|
+
config: Record<string, unknown>
|
|
5
|
+
): { isValid: boolean; error?: string } => {
|
|
6
|
+
if (!config || typeof config !== 'object') {
|
|
7
|
+
return { isValid: false, error: 'Config must be a valid object' }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// If connectionString is provided, validate it
|
|
11
|
+
if (config.connectionString) {
|
|
12
|
+
if (typeof config.connectionString !== 'string' || config.connectionString.trim() === '') {
|
|
13
|
+
return { isValid: false, error: 'Connection string must be a non-empty string' }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Only validate format if it's not a secret reference that will be resolved at runtime
|
|
17
|
+
const connStr = config.connectionString as string
|
|
18
|
+
if (
|
|
19
|
+
!connStr.startsWith('teleporthq.secrets.') &&
|
|
20
|
+
!connStr.startsWith('redis://') &&
|
|
21
|
+
!connStr.startsWith('rediss://')
|
|
22
|
+
) {
|
|
23
|
+
return { isValid: false, error: 'Invalid Redis connection string format' }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { isValid: true }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// If no connectionString, host/port/etc will be used to build one
|
|
30
|
+
if (!config.host || typeof config.host !== 'string') {
|
|
31
|
+
return { isValid: false, error: 'Redis host is required when connectionString is not provided' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { isValid: true }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RedisConfig {
|
|
38
|
+
connectionString?: string
|
|
39
|
+
host?: string
|
|
40
|
+
port?: number
|
|
41
|
+
database?: number
|
|
42
|
+
password?: string
|
|
43
|
+
username?: string
|
|
44
|
+
selectedTables?: Record<string, unknown>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const generateRedisFetcher = (config: Record<string, unknown>): string => {
|
|
48
|
+
const redisConfig = config as RedisConfig
|
|
49
|
+
const host = redisConfig.host
|
|
50
|
+
const port = redisConfig.port
|
|
51
|
+
const username = redisConfig.username
|
|
52
|
+
const password = redisConfig.password
|
|
53
|
+
const database = redisConfig.database
|
|
54
|
+
const hasUsername = username
|
|
55
|
+
|
|
56
|
+
// Build connection string from parts if not provided
|
|
57
|
+
let connectionString = redisConfig.connectionString
|
|
58
|
+
if (!connectionString) {
|
|
59
|
+
connectionString = `redis://${hasUsername ? `${username}:${password}@` : ''}${host}:${
|
|
60
|
+
port || 6379
|
|
61
|
+
}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `import { createClient } from 'redis'
|
|
65
|
+
|
|
66
|
+
export default async function handler(req, res) {
|
|
67
|
+
let client = null
|
|
68
|
+
try {
|
|
69
|
+
client = createClient({
|
|
70
|
+
url: ${replaceSecretReference(connectionString)}${
|
|
71
|
+
database ? `,\n database: ${database}` : ''
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
await client.connect()
|
|
76
|
+
|
|
77
|
+
const { query, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
|
|
78
|
+
|
|
79
|
+
const pattern = (filters && JSON.parse(filters).pattern) || query || '*'
|
|
80
|
+
const keys = await client.keys(pattern)
|
|
81
|
+
|
|
82
|
+
const limitValue = limit || perPage || 100
|
|
83
|
+
const skipValue = offset !== undefined ? parseInt(offset) : ((parseInt(page) || 1) - 1) * parseInt(limitValue)
|
|
84
|
+
const paginatedKeys = keys.slice(skipValue, skipValue + parseInt(limitValue))
|
|
85
|
+
|
|
86
|
+
const results = []
|
|
87
|
+
for (const key of paginatedKeys) {
|
|
88
|
+
const type = await client.type(key)
|
|
89
|
+
const ttl = await client.ttl(key)
|
|
90
|
+
let value
|
|
91
|
+
|
|
92
|
+
switch (type) {
|
|
93
|
+
case 'string':
|
|
94
|
+
value = await client.get(key)
|
|
95
|
+
break
|
|
96
|
+
case 'list':
|
|
97
|
+
value = await client.lRange(key, 0, -1)
|
|
98
|
+
break
|
|
99
|
+
case 'set':
|
|
100
|
+
value = await client.sMembers(key)
|
|
101
|
+
break
|
|
102
|
+
case 'zset':
|
|
103
|
+
value = await client.zRange(key, 0, -1)
|
|
104
|
+
break
|
|
105
|
+
case 'hash':
|
|
106
|
+
value = await client.hGetAll(key)
|
|
107
|
+
break
|
|
108
|
+
default:
|
|
109
|
+
value = null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
results.push({
|
|
113
|
+
key,
|
|
114
|
+
type,
|
|
115
|
+
value,
|
|
116
|
+
ttl: ttl === -1 ? null : ttl
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (sortBy) {
|
|
121
|
+
const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
|
|
122
|
+
results.sort((a, b) => {
|
|
123
|
+
const aVal = a[sortBy]
|
|
124
|
+
const bVal = b[sortBy]
|
|
125
|
+
if (aVal < bVal) return -sortOrderValue
|
|
126
|
+
if (aVal > bVal) return sortOrderValue
|
|
127
|
+
return 0
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const safeData = JSON.parse(JSON.stringify(results))
|
|
132
|
+
|
|
133
|
+
return res.status(200).json({
|
|
134
|
+
success: true,
|
|
135
|
+
data: safeData,
|
|
136
|
+
timestamp: Date.now()
|
|
137
|
+
})
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('Redis fetch error:', error)
|
|
140
|
+
return res.status(500).json({
|
|
141
|
+
success: false,
|
|
142
|
+
error: error.message || 'Failed to fetch data',
|
|
143
|
+
timestamp: Date.now()
|
|
144
|
+
})
|
|
145
|
+
} finally {
|
|
146
|
+
if (client) {
|
|
147
|
+
await client.quit()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`
|
|
152
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { replaceSecretReference } from '../utils'
|
|
2
|
+
|
|
3
|
+
interface RedshiftConfig {
|
|
4
|
+
host?: string
|
|
5
|
+
port?: number
|
|
6
|
+
user?: string
|
|
7
|
+
password?: string
|
|
8
|
+
database?: string
|
|
9
|
+
ssl?: boolean | { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
|
|
10
|
+
sslConfig?: { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
|
|
11
|
+
options?: { schema?: string }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const generateRedshiftFetcher = (
|
|
15
|
+
config: Record<string, unknown>,
|
|
16
|
+
tableName: string
|
|
17
|
+
): string => {
|
|
18
|
+
const redshiftConfig = config as RedshiftConfig
|
|
19
|
+
const host = redshiftConfig.host
|
|
20
|
+
const port = redshiftConfig.port
|
|
21
|
+
const user = redshiftConfig.user
|
|
22
|
+
const password = redshiftConfig.password
|
|
23
|
+
const database = redshiftConfig.database
|
|
24
|
+
const ssl = redshiftConfig.ssl
|
|
25
|
+
const sslConfig = redshiftConfig.sslConfig
|
|
26
|
+
const schema = redshiftConfig.options?.schema
|
|
27
|
+
|
|
28
|
+
return `import { Pool } from 'pg'
|
|
29
|
+
|
|
30
|
+
let pool = null
|
|
31
|
+
|
|
32
|
+
const getPool = () => {
|
|
33
|
+
if (pool) return pool
|
|
34
|
+
|
|
35
|
+
pool = new Pool({
|
|
36
|
+
host: ${JSON.stringify(host)},
|
|
37
|
+
port: ${port || 5439},
|
|
38
|
+
user: ${JSON.stringify(user)},
|
|
39
|
+
password: ${replaceSecretReference(password)},
|
|
40
|
+
database: ${JSON.stringify(database)},
|
|
41
|
+
ssl: ${
|
|
42
|
+
ssl === false
|
|
43
|
+
? '{ rejectUnauthorized: false }'
|
|
44
|
+
: sslConfig
|
|
45
|
+
? `{
|
|
46
|
+
${sslConfig.ca ? `ca: ${replaceSecretReference(sslConfig.ca)},` : ''}
|
|
47
|
+
${sslConfig.cert ? `cert: ${replaceSecretReference(sslConfig.cert)},` : ''}
|
|
48
|
+
${sslConfig.key ? `key: ${replaceSecretReference(sslConfig.key)},` : ''}
|
|
49
|
+
rejectUnauthorized: ${sslConfig.rejectUnauthorized !== false}
|
|
50
|
+
}`
|
|
51
|
+
: '{ rejectUnauthorized: false }' // Default to SSL with no cert verification for Redshift
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return pool
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default async function handler(req, res) {
|
|
59
|
+
try {
|
|
60
|
+
const pool = getPool()
|
|
61
|
+
${schema ? `await pool.query('SET search_path TO ${schema}')` : ''}
|
|
62
|
+
|
|
63
|
+
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, offset } = req.query
|
|
64
|
+
|
|
65
|
+
const conditions = []
|
|
66
|
+
const queryParams = []
|
|
67
|
+
let paramIndex = 1
|
|
68
|
+
|
|
69
|
+
if (query && queryColumns) {
|
|
70
|
+
const columns = JSON.parse(queryColumns)
|
|
71
|
+
const searchConditions = columns.map((col) => {
|
|
72
|
+
const condition = \`\${col}::text ILIKE $\${paramIndex}\`
|
|
73
|
+
paramIndex++
|
|
74
|
+
return condition
|
|
75
|
+
})
|
|
76
|
+
columns.forEach(() => queryParams.push(\`%\${query}%\`))
|
|
77
|
+
conditions.push(\`(\${searchConditions.join(' OR ')})\`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (filters) {
|
|
81
|
+
const parsedFilters = JSON.parse(filters)
|
|
82
|
+
Object.entries(parsedFilters).forEach(([key, value]) => {
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
const placeholders = value.map(() => \`$\${paramIndex++}\`)
|
|
85
|
+
queryParams.push(...value)
|
|
86
|
+
conditions.push(\`\${key} IN (\${placeholders.join(', ')})\`)
|
|
87
|
+
} else {
|
|
88
|
+
conditions.push(\`\${key} = $\${paramIndex}\`)
|
|
89
|
+
queryParams.push(value)
|
|
90
|
+
paramIndex++
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let sql = \`SELECT * FROM ${tableName}\`
|
|
96
|
+
|
|
97
|
+
if (conditions.length > 0) {
|
|
98
|
+
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (sortBy) {
|
|
102
|
+
sql += \` ORDER BY \${sortBy} \${sortOrder?.toUpperCase() || 'ASC'}\`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const limitValue = limit || perPage
|
|
106
|
+
const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
|
|
107
|
+
|
|
108
|
+
if (limitValue) {
|
|
109
|
+
sql += \` LIMIT \${limitValue}\`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (offsetValue !== undefined) {
|
|
113
|
+
sql += \` OFFSET \${offsetValue}\`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const result = await pool.query(sql, queryParams)
|
|
117
|
+
const rows = Array.isArray(result?.rows) ? result.rows : []
|
|
118
|
+
const plainRows = rows.map((row) =>
|
|
119
|
+
row && typeof row.toJSON === 'function' ? row.toJSON() : row
|
|
120
|
+
)
|
|
121
|
+
const safeData = JSON.parse(JSON.stringify(plainRows))
|
|
122
|
+
|
|
123
|
+
return res.status(200).json({
|
|
124
|
+
success: true,
|
|
125
|
+
data: safeData,
|
|
126
|
+
timestamp: Date.now()
|
|
127
|
+
})
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Redshift fetch error:', error)
|
|
130
|
+
return res.status(500).json({
|
|
131
|
+
success: false,
|
|
132
|
+
error: error.message || 'Failed to fetch data',
|
|
133
|
+
timestamp: Date.now()
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`
|
|
138
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const validateRESTAPIConfig = (
|
|
2
|
+
config: Record<string, unknown>
|
|
3
|
+
): { isValid: boolean; error?: string } => {
|
|
4
|
+
if (!config || typeof config !== 'object') {
|
|
5
|
+
return { isValid: false, error: 'Config must be a valid object' }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!config.url || typeof config.url !== 'string' || config.url.trim() === '') {
|
|
9
|
+
return { isValid: false, error: 'URL is required' }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(config.url)
|
|
14
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
15
|
+
return { isValid: false, error: 'URL must use HTTP or HTTPS protocol' }
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
return { isValid: false, error: 'Invalid URL format' }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (config.method) {
|
|
22
|
+
const validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
|
23
|
+
if (typeof config.method !== 'string' || !validMethods.includes(config.method.toUpperCase())) {
|
|
24
|
+
return { isValid: false, error: 'Invalid HTTP method' }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { isValid: true }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Authorization {
|
|
32
|
+
type?: string
|
|
33
|
+
credentials?: {
|
|
34
|
+
apiKey?: string
|
|
35
|
+
token?: string
|
|
36
|
+
username?: string
|
|
37
|
+
password?: string
|
|
38
|
+
clientSecret?: string
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const generateAuthCode = (authorization: Authorization): string => {
|
|
43
|
+
if (!authorization || authorization.type === 'none') {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { type, credentials } = authorization
|
|
48
|
+
|
|
49
|
+
switch (type) {
|
|
50
|
+
case 'api-key':
|
|
51
|
+
return `headers['Authorization'] = 'ApiKey ${credentials.apiKey}'`
|
|
52
|
+
case 'bearer-token':
|
|
53
|
+
case 'jwt-bearer':
|
|
54
|
+
return `headers['Authorization'] = 'Bearer ${credentials.token}'`
|
|
55
|
+
case 'basic-auth':
|
|
56
|
+
return `headers['Authorization'] = 'Basic ' + Buffer.from('${credentials.username}:${credentials.password}').toString('base64')`
|
|
57
|
+
case 'oauth2':
|
|
58
|
+
return `headers['Authorization'] = 'Bearer ${credentials.clientSecret}'`
|
|
59
|
+
default:
|
|
60
|
+
return ''
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface RESTAPIConfig {
|
|
65
|
+
url?: string
|
|
66
|
+
method?: string
|
|
67
|
+
headers?: Record<string, string>
|
|
68
|
+
authorization?: Authorization
|
|
69
|
+
bodyType?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const generateRESTAPIFetcher = (config: Record<string, unknown>): string => {
|
|
73
|
+
const restConfig = config as RESTAPIConfig
|
|
74
|
+
const authCode = generateAuthCode(restConfig.authorization || {})
|
|
75
|
+
|
|
76
|
+
return `import fetch from 'node-fetch'
|
|
77
|
+
|
|
78
|
+
export default async function handler(req, res) {
|
|
79
|
+
try {
|
|
80
|
+
const { offset, limit } = req.query
|
|
81
|
+
|
|
82
|
+
const url = ${JSON.stringify(restConfig.url)}
|
|
83
|
+
const method = ${JSON.stringify(restConfig.method || 'GET')}
|
|
84
|
+
|
|
85
|
+
const headers = ${JSON.stringify(restConfig.headers || {})}
|
|
86
|
+
${authCode}
|
|
87
|
+
|
|
88
|
+
const options = {
|
|
89
|
+
method,
|
|
90
|
+
headers
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
${
|
|
94
|
+
restConfig.method === 'POST' || restConfig.method === 'PUT' || restConfig.method === 'PATCH'
|
|
95
|
+
? `
|
|
96
|
+
if (req.body) {
|
|
97
|
+
options.body = ${
|
|
98
|
+
restConfig.bodyType === 'json' || !restConfig.bodyType
|
|
99
|
+
? 'JSON.stringify(req.body)'
|
|
100
|
+
: 'req.body'
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
`
|
|
104
|
+
: ''
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const response = await fetch(url, options)
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
return res.status(response.status).json({
|
|
111
|
+
success: false,
|
|
112
|
+
error: \`HTTP \${response.status}: \${response.statusText}\`,
|
|
113
|
+
timestamp: Date.now()
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let data = await response.json()
|
|
118
|
+
|
|
119
|
+
// Apply offset and limit if data is an array and parameters are provided
|
|
120
|
+
if (Array.isArray(data)) {
|
|
121
|
+
const offsetValue = offset !== undefined ? parseInt(offset) : 0
|
|
122
|
+
const limitValue = limit !== undefined ? parseInt(limit) : undefined
|
|
123
|
+
|
|
124
|
+
if (limitValue !== undefined) {
|
|
125
|
+
data = data.slice(offsetValue, offsetValue + limitValue)
|
|
126
|
+
} else if (offsetValue > 0) {
|
|
127
|
+
data = data.slice(offsetValue)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const safeData = JSON.parse(JSON.stringify(data))
|
|
132
|
+
|
|
133
|
+
return res.status(200).json({
|
|
134
|
+
success: true,
|
|
135
|
+
data: safeData,
|
|
136
|
+
timestamp: Date.now()
|
|
137
|
+
})
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('REST API fetch error:', error)
|
|
140
|
+
return res.status(500).json({
|
|
141
|
+
success: false,
|
|
142
|
+
error: error.message || 'Failed to fetch data',
|
|
143
|
+
timestamp: Date.now()
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
`
|
|
148
|
+
}
|