@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,983 @@
|
|
|
1
|
+
# Search Functionality Implementation - Complete Summary
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Search functionality has been fully implemented for array mappers bound to data sources. Users can now add search inputs that filter data source results in real-time with debouncing support.
|
|
6
|
+
|
|
7
|
+
## Implementation Details
|
|
8
|
+
|
|
9
|
+
### 1. New UIDL Properties
|
|
10
|
+
|
|
11
|
+
#### On `cms-list-repeater` Node
|
|
12
|
+
- **`searchEnabled`** (boolean, optional): Enables search functionality for the array mapper
|
|
13
|
+
- **`searchDebounce`** (number, optional): Debounce timeout in milliseconds (default: 300ms)
|
|
14
|
+
|
|
15
|
+
#### On Resource Params
|
|
16
|
+
- **`queryColumns`** (UIDLStaticValue with string[]): Array of column names to search across
|
|
17
|
+
|
|
18
|
+
### 2. New Element Types
|
|
19
|
+
|
|
20
|
+
#### `data-source-search-node`
|
|
21
|
+
- Container element for search UI (rendered as `<div>`)
|
|
22
|
+
- Automatically positioned as the first child of the array mapper when search is enabled
|
|
23
|
+
- Default styling: `display: flex`, `align-items: center`, `justify-content: center`
|
|
24
|
+
|
|
25
|
+
#### `data-source-search-input-node`
|
|
26
|
+
- The actual search input field (rendered as `<input>`)
|
|
27
|
+
- Default styling: `max-width: 200px`, standard input styling
|
|
28
|
+
- Placeholder automatically generated from table/collection name
|
|
29
|
+
|
|
30
|
+
### 3. Code Generation
|
|
31
|
+
|
|
32
|
+
#### State Management
|
|
33
|
+
For each search-enabled array mapper, the following state is generated:
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// Search query state (user input)
|
|
37
|
+
const [search_pg_0_query, setSearch_pg_0_query] = useState('')
|
|
38
|
+
|
|
39
|
+
// Debounced search query (actual query sent to API)
|
|
40
|
+
const [debouncedSearch_pg_0_query, setDebouncedSearch_pg_0_query] = useState('')
|
|
41
|
+
|
|
42
|
+
// Page state (reset to 1 when search changes)
|
|
43
|
+
const [pagination_pg_0_page, setPagination_pg_0_page] = useState(1)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Debounce Effect
|
|
47
|
+
```javascript
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
setDebouncedSearch_pg_0_query(search_pg_0_query)
|
|
51
|
+
}, 300) // searchDebounce value from UIDL
|
|
52
|
+
|
|
53
|
+
return () => clearTimeout(timer)
|
|
54
|
+
}, [search_pg_0_query])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Reset Page on Search Effect
|
|
58
|
+
```javascript
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setPagination_pg_0_page(1)
|
|
61
|
+
}, [debouncedSearch_pg_0_query])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Search Input Binding
|
|
65
|
+
```javascript
|
|
66
|
+
<input
|
|
67
|
+
className="home-search-input1"
|
|
68
|
+
value={search_pg_0_query}
|
|
69
|
+
onChange={(e) => setSearch_pg_0_query(e.target.value)}
|
|
70
|
+
placeholder="Search users"
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### DataProvider Params
|
|
75
|
+
```javascript
|
|
76
|
+
<DataProvider
|
|
77
|
+
name="test_users_data"
|
|
78
|
+
params={{
|
|
79
|
+
page: pagination_pg_0_page,
|
|
80
|
+
perPage: 10,
|
|
81
|
+
query: debouncedSearch_pg_0_query,
|
|
82
|
+
queryColumns: ["name", "email", "bio"]
|
|
83
|
+
}}
|
|
84
|
+
fetchData={(params) =>
|
|
85
|
+
fetch(`/api/cockroachdb-users-4b96921b?${new URLSearchParams(params)}`)
|
|
86
|
+
.then(res => res.json())
|
|
87
|
+
.then(data => data.data)
|
|
88
|
+
}
|
|
89
|
+
key={`test_users_data-${pagination_pg_0_page}`}
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 4. Data Fetcher Updates
|
|
94
|
+
|
|
95
|
+
All data fetchers have been updated to support search:
|
|
96
|
+
|
|
97
|
+
#### SQL-based (PostgreSQL, MySQL, MariaDB, CockroachDB)
|
|
98
|
+
```javascript
|
|
99
|
+
if (query) {
|
|
100
|
+
let columns = []
|
|
101
|
+
|
|
102
|
+
if (queryColumns) {
|
|
103
|
+
// Use specified columns
|
|
104
|
+
columns = typeof queryColumns === 'string'
|
|
105
|
+
? JSON.parse(queryColumns)
|
|
106
|
+
: (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
|
|
107
|
+
} else {
|
|
108
|
+
// Fallback: Query information_schema to get all columns
|
|
109
|
+
// PostgreSQL/CockroachDB:
|
|
110
|
+
const schemaResult = await pool.query(
|
|
111
|
+
'SELECT column_name FROM information_schema.columns WHERE table_name = $1',
|
|
112
|
+
[tableName]
|
|
113
|
+
)
|
|
114
|
+
columns = schemaResult.rows.map(row => row.column_name)
|
|
115
|
+
|
|
116
|
+
// MySQL/MariaDB:
|
|
117
|
+
const [schemaRows] = await connection.execute(
|
|
118
|
+
'SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?',
|
|
119
|
+
[database, tableName]
|
|
120
|
+
)
|
|
121
|
+
columns = schemaRows.map(row => row.COLUMN_NAME)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (columns.length > 0) {
|
|
125
|
+
// PostgreSQL/CockroachDB: Cast each column to text before searching
|
|
126
|
+
const searchConditions = columns.map(col => `${col}::text ILIKE $?`)
|
|
127
|
+
|
|
128
|
+
// MySQL/MariaDB: Cast each column to CHAR before searching
|
|
129
|
+
const searchConditions = columns.map(col => `CAST(${col} AS CHAR) LIKE ?`)
|
|
130
|
+
|
|
131
|
+
conditions.push(`(${searchConditions.join(' OR ')})`)
|
|
132
|
+
columns.forEach(() => queryParams.push(`%${query}%`))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### MongoDB
|
|
138
|
+
```javascript
|
|
139
|
+
if (query) {
|
|
140
|
+
let columns = []
|
|
141
|
+
|
|
142
|
+
if (queryColumns) {
|
|
143
|
+
// Use specified columns
|
|
144
|
+
columns = typeof queryColumns === 'string'
|
|
145
|
+
? JSON.parse(queryColumns)
|
|
146
|
+
: (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
|
|
147
|
+
} else {
|
|
148
|
+
// Fallback: Get field names from a sample document
|
|
149
|
+
const sampleDoc = await db.collection(tableName).findOne({})
|
|
150
|
+
if (sampleDoc) {
|
|
151
|
+
columns = Object.keys(sampleDoc).filter(key => key !== '_id')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (columns.length > 0) {
|
|
156
|
+
filter.$or = columns.map(col => ({
|
|
157
|
+
[col]: { $regex: query, $options: 'i' }
|
|
158
|
+
}))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Supabase
|
|
164
|
+
```javascript
|
|
165
|
+
if (query) {
|
|
166
|
+
let columns = []
|
|
167
|
+
|
|
168
|
+
if (queryColumns) {
|
|
169
|
+
// Use specified columns
|
|
170
|
+
columns = typeof queryColumns === 'string'
|
|
171
|
+
? JSON.parse(queryColumns)
|
|
172
|
+
: (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
|
|
173
|
+
} else {
|
|
174
|
+
// Fallback: Get column names from a sample row
|
|
175
|
+
const { data: sampleData } = await client.from(tableName).select('*').limit(1).single()
|
|
176
|
+
if (sampleData) {
|
|
177
|
+
columns = Object.keys(sampleData)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (columns.length > 0) {
|
|
182
|
+
const searchPattern = `%${query}%`
|
|
183
|
+
const orConditions = columns.map(col => `${col}.ilike.${searchPattern}`).join(',')
|
|
184
|
+
queryRef = queryRef.or(orConditions)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### JavaScript/CSV/Static Collection/Google Sheets
|
|
190
|
+
```javascript
|
|
191
|
+
if (query) {
|
|
192
|
+
const searchQuery = query.toLowerCase()
|
|
193
|
+
|
|
194
|
+
if (queryColumns) {
|
|
195
|
+
// Search specific columns
|
|
196
|
+
const columns = typeof queryColumns === 'string'
|
|
197
|
+
? JSON.parse(queryColumns)
|
|
198
|
+
: (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
|
|
199
|
+
|
|
200
|
+
data = data.filter(item => {
|
|
201
|
+
return columns.some(col => {
|
|
202
|
+
const value = item[col]
|
|
203
|
+
if (value === null || value === undefined) return false
|
|
204
|
+
return String(value).toLowerCase().includes(searchQuery)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
} else {
|
|
208
|
+
// Fallback: Search entire record by stringifying it
|
|
209
|
+
data = data.filter(item => {
|
|
210
|
+
try {
|
|
211
|
+
const stringified = JSON.stringify(item).toLowerCase()
|
|
212
|
+
return stringified.includes(searchQuery)
|
|
213
|
+
} catch {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 5. Count Fetchers
|
|
222
|
+
|
|
223
|
+
All count fetchers have been updated to apply the same search filters as the main data fetchers. This ensures pagination displays the correct total pages when a search filter is active.
|
|
224
|
+
|
|
225
|
+
#### Key Changes
|
|
226
|
+
- Parse `queryColumns` from query string (handles both string and array formats)
|
|
227
|
+
- Apply search conditions before counting
|
|
228
|
+
- Return filtered count
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
```javascript
|
|
232
|
+
async function getCount(req, res) {
|
|
233
|
+
const { query, queryColumns, filters } = req.query
|
|
234
|
+
const conditions = []
|
|
235
|
+
const queryParams = []
|
|
236
|
+
|
|
237
|
+
if (queryColumns && query) {
|
|
238
|
+
const columns = typeof queryColumns === 'string'
|
|
239
|
+
? JSON.parse(queryColumns)
|
|
240
|
+
: (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
|
|
241
|
+
// Apply search conditions
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Execute COUNT query with conditions
|
|
245
|
+
const count = await executeCount(conditions, queryParams)
|
|
246
|
+
|
|
247
|
+
return res.status(200).json({
|
|
248
|
+
success: true,
|
|
249
|
+
count: count,
|
|
250
|
+
timestamp: Date.now()
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Files Modified
|
|
256
|
+
|
|
257
|
+
### Core Implementation
|
|
258
|
+
- `packages/teleport-plugin-next-data-source/src/index.ts`
|
|
259
|
+
- Added `SearchConfig` and `PaginationConfig` interfaces
|
|
260
|
+
- Updated `extractPaginationConfigEarly()` to extract search configuration
|
|
261
|
+
- Modified both page and component plugins to merge search configs
|
|
262
|
+
|
|
263
|
+
- `packages/teleport-plugin-next-data-source/src/array-mapper-pagination.ts`
|
|
264
|
+
- Added search properties to `ArrayMapperPaginationInfo` interface
|
|
265
|
+
- Updated `generatePaginationLogic()` to accept and include search config
|
|
266
|
+
|
|
267
|
+
- `packages/teleport-plugin-next-data-source/src/pagination-plugin.ts`
|
|
268
|
+
- Added detection of search input nodes in `detectPaginationsFromJSX()`
|
|
269
|
+
- Added search state generation (query and debounced query)
|
|
270
|
+
- Added debounce effect generation
|
|
271
|
+
- Added reset-page-on-search effect generation
|
|
272
|
+
- Added `modifySearchInputs()` function to bind input handlers
|
|
273
|
+
- Updated `addPaginationParamsToDataProvider()` to include search params
|
|
274
|
+
|
|
275
|
+
### Data Fetchers (with Fallback Search)
|
|
276
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/postgresql.ts` ✅ (fallback via information_schema)
|
|
277
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/mysql.ts` ✅ (fallback via information_schema)
|
|
278
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/mongodb.ts` ✅ (fallback via sample document)
|
|
279
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/mariadb.ts` ✅ (fallback via information_schema)
|
|
280
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/supabase.ts` ✅ (fallback via sample row)
|
|
281
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/javascript.ts` ✅ (fallback via JSON.stringify)
|
|
282
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/csv-file.ts` ✅ (fallback via JSON.stringify)
|
|
283
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/static-collection.ts` ✅ (fallback via JSON.stringify)
|
|
284
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/google-sheets.ts` ✅ (fallback via JSON.stringify)
|
|
285
|
+
|
|
286
|
+
### Type Definitions
|
|
287
|
+
- `packages/teleport-types/src/uidl.ts`
|
|
288
|
+
- Added `searchEnabled` and `searchDebounce` to `UIDLCMSListRepeaterNodeContent`
|
|
289
|
+
|
|
290
|
+
- `packages/teleport-uidl-validator/src/decoders/utils.ts`
|
|
291
|
+
- Added `searchEnabled` and `searchDebounce` to `cmsListRepeaterNodeDecoder`
|
|
292
|
+
|
|
293
|
+
## Key Implementation Patterns
|
|
294
|
+
|
|
295
|
+
### 1. Early Extraction Pattern
|
|
296
|
+
Search configuration is extracted from the UIDL **before** any transformations, following the same pattern as pagination's `perPage` extraction. This is critical because UIDL nodes are transformed early in the pipeline.
|
|
297
|
+
|
|
298
|
+
### 2. State Management Pattern
|
|
299
|
+
Three pieces of state are managed:
|
|
300
|
+
1. **Immediate search query**: Bound directly to input value
|
|
301
|
+
2. **Debounced search query**: Debounced version that triggers API calls
|
|
302
|
+
3. **Page number**: Reset to 1 when debounced search changes
|
|
303
|
+
|
|
304
|
+
### 3. Effect Dependency Pattern
|
|
305
|
+
Two effects are created:
|
|
306
|
+
- Debounce effect depends on `[searchQuery]`
|
|
307
|
+
- Reset page effect depends on `[debouncedSearchQuery]`
|
|
308
|
+
|
|
309
|
+
This ensures the page resets only after the debounce completes, not on every keystroke.
|
|
310
|
+
|
|
311
|
+
### 4. Query String Serialization
|
|
312
|
+
`queryColumns` is passed as a JSON-stringified array in the query string:
|
|
313
|
+
```
|
|
314
|
+
?query=john&queryColumns=["name","email","bio"]&page=1&perPage=10
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
All fetchers parse this using:
|
|
318
|
+
```javascript
|
|
319
|
+
const columns = typeof queryColumns === 'string'
|
|
320
|
+
? JSON.parse(queryColumns)
|
|
321
|
+
: (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Example UIDL
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"type": "cms-list-repeater",
|
|
329
|
+
"content": {
|
|
330
|
+
"renderPropIdentifier": "context_abc123",
|
|
331
|
+
"source": "test_users_data",
|
|
332
|
+
"paginated": true,
|
|
333
|
+
"perPage": 10,
|
|
334
|
+
"searchEnabled": true,
|
|
335
|
+
"searchDebounce": 300,
|
|
336
|
+
"nodes": {
|
|
337
|
+
"list": {
|
|
338
|
+
"type": "element",
|
|
339
|
+
"content": {
|
|
340
|
+
"elementType": "fragment",
|
|
341
|
+
"children": [
|
|
342
|
+
{
|
|
343
|
+
"type": "element",
|
|
344
|
+
"content": {
|
|
345
|
+
"elementType": "data-source-search-node",
|
|
346
|
+
"children": [
|
|
347
|
+
{
|
|
348
|
+
"type": "element",
|
|
349
|
+
"content": {
|
|
350
|
+
"elementType": "data-source-search-input-node",
|
|
351
|
+
"attrs": {
|
|
352
|
+
"placeholder": {
|
|
353
|
+
"type": "static",
|
|
354
|
+
"content": "Search users"
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"type": "element",
|
|
364
|
+
"content": {
|
|
365
|
+
"elementType": "text",
|
|
366
|
+
"children": [{
|
|
367
|
+
"type": "dynamic",
|
|
368
|
+
"content": {
|
|
369
|
+
"referenceType": "local",
|
|
370
|
+
"id": "name"
|
|
371
|
+
}
|
|
372
|
+
}]
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
And in resources:
|
|
384
|
+
```json
|
|
385
|
+
{
|
|
386
|
+
"resources": {
|
|
387
|
+
"items": {
|
|
388
|
+
"resource_abc123": {
|
|
389
|
+
"id": "resource_abc123",
|
|
390
|
+
"name": "HomePage_users",
|
|
391
|
+
"path": {
|
|
392
|
+
"baseUrl": { "type": "static", "content": "/api/data-source" },
|
|
393
|
+
"route": { "type": "static", "content": "/ds_456/users" }
|
|
394
|
+
},
|
|
395
|
+
"params": {
|
|
396
|
+
"dataSourceId": { "type": "static", "content": "ds_456" },
|
|
397
|
+
"dataSourceType": { "type": "static", "content": "cockroachdb" },
|
|
398
|
+
"tableName": { "type": "static", "content": "users" },
|
|
399
|
+
"queryColumns": {
|
|
400
|
+
"type": "static",
|
|
401
|
+
"content": ["name", "email", "bio"]
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## API Contract
|
|
411
|
+
|
|
412
|
+
### Request Parameters
|
|
413
|
+
- `query` (string): The search query entered by the user
|
|
414
|
+
- `queryColumns` (string): JSON-stringified array of column names to search
|
|
415
|
+
- `page` (number): Current page number
|
|
416
|
+
- `perPage` (number): Items per page
|
|
417
|
+
|
|
418
|
+
### Response Format
|
|
419
|
+
Data handlers return:
|
|
420
|
+
```json
|
|
421
|
+
{
|
|
422
|
+
"success": true,
|
|
423
|
+
"data": [...],
|
|
424
|
+
"timestamp": 1234567890
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Count handlers return:
|
|
429
|
+
```json
|
|
430
|
+
{
|
|
431
|
+
"success": true,
|
|
432
|
+
"count": 42,
|
|
433
|
+
"timestamp": 1234567890
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Backward Compatibility
|
|
438
|
+
|
|
439
|
+
- If `searchEnabled` is not present or is `false`, no search UI is rendered
|
|
440
|
+
- If `queryColumns` is not present in resource params, search functionality is disabled
|
|
441
|
+
- Existing array mappers without search continue to work without any changes
|
|
442
|
+
- All data fetchers maintain backward compatibility by checking for the presence of search parameters
|
|
443
|
+
|
|
444
|
+
## Testing Checklist
|
|
445
|
+
|
|
446
|
+
✅ Search state initialization
|
|
447
|
+
✅ Debounce mechanism (300ms default)
|
|
448
|
+
✅ Page reset on search query change
|
|
449
|
+
✅ Search input value binding
|
|
450
|
+
✅ Search input onChange handler
|
|
451
|
+
✅ DataProvider params include search query and queryColumns
|
|
452
|
+
✅ PostgreSQL search support
|
|
453
|
+
✅ MySQL search support
|
|
454
|
+
✅ MongoDB search support
|
|
455
|
+
✅ MariaDB search support
|
|
456
|
+
✅ Supabase search support
|
|
457
|
+
✅ JavaScript source search support
|
|
458
|
+
✅ CSV file search support
|
|
459
|
+
✅ Static collection search support
|
|
460
|
+
✅ Count fetchers apply search filters
|
|
461
|
+
✅ UIDL type definitions updated
|
|
462
|
+
✅ UIDL validator updated
|
|
463
|
+
✅ Backward compatibility maintained
|
|
464
|
+
✅ No TypeScript/linter errors
|
|
465
|
+
|
|
466
|
+
## Production Ready
|
|
467
|
+
|
|
468
|
+
This implementation is production-ready and follows all the patterns established by the pagination feature. The code:
|
|
469
|
+
|
|
470
|
+
- ✅ Uses early extraction to capture configuration before transformations
|
|
471
|
+
- ✅ Generates clean, type-safe state management code
|
|
472
|
+
- ✅ Implements proper debouncing with cleanup
|
|
473
|
+
- ✅ Resets pagination correctly when search changes
|
|
474
|
+
- ✅ Handles all supported data source types
|
|
475
|
+
- ✅ Maintains backward compatibility
|
|
476
|
+
- ✅ Passes all linter checks
|
|
477
|
+
- ✅ Follows existing code conventions
|
|
478
|
+
- ✅ Includes proper error handling
|
|
479
|
+
- ✅ Supports both pages and components
|
|
480
|
+
|
|
481
|
+
## Notes
|
|
482
|
+
|
|
483
|
+
- The debounce timer is properly cleaned up on component unmount
|
|
484
|
+
- Search is case-insensitive for all data sources
|
|
485
|
+
- Empty search queries return all results (no filter applied)
|
|
486
|
+
- The search applies to the columns specified in `queryColumns` only
|
|
487
|
+
- Multiple columns are searched with OR logic (match any column)
|
|
488
|
+
- Search results are paginated normally with the filtered count
|
|
489
|
+
|
|
490
|
+
## Common Issues and Solutions
|
|
491
|
+
|
|
492
|
+
This section documents all issues encountered during implementation and their solutions, to prevent regression and help future developers.
|
|
493
|
+
|
|
494
|
+
### Issue 1: DataProvider Order Not Preserved
|
|
495
|
+
|
|
496
|
+
**Problem:**
|
|
497
|
+
The `reorderChildrenForSearch` function in `packages/teleport-plugin-common/src/node-handlers/node-to-jsx/index.ts` was categorizing and reordering ALL DataProviders globally, regardless of whether they were part of a pagination/search group. This broke the original order from the UIDL.
|
|
498
|
+
|
|
499
|
+
**Example:**
|
|
500
|
+
If UIDL had DataProviders in order: A (pagination+search), B (pagination only), C (plain), the generated code would show: A, B, C reordered to: A, C, B or similar unexpected order.
|
|
501
|
+
|
|
502
|
+
**Root Cause:**
|
|
503
|
+
The function was always reordering children without checking if reordering was actually needed. It would group all DataProviders together even when they weren't related to pagination/search.
|
|
504
|
+
|
|
505
|
+
**Solution:**
|
|
506
|
+
Only reorder children when there's actually a pagination/search group (i.e., when search or pagination nodes exist alongside a DataProvider). Otherwise, preserve the original order from the UIDL.
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// Only reorder if we have search or pagination nodes alongside a DataProvider
|
|
510
|
+
const hasSearchOrPagination = searchNodes.length > 0 || paginationNodes.length > 0
|
|
511
|
+
const hasDataProvider = dataProviderNodes.length > 0
|
|
512
|
+
|
|
513
|
+
if (hasSearchOrPagination && hasDataProvider) {
|
|
514
|
+
// Reorder: search, dataProvider, other, pagination
|
|
515
|
+
return [...searchNodes, ...dataProviderNodes, ...otherNodes, ...paginationNodes]
|
|
516
|
+
} else {
|
|
517
|
+
// No reordering needed - preserve original order
|
|
518
|
+
return children
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Files Modified:**
|
|
523
|
+
- `packages/teleport-plugin-common/src/node-handlers/node-to-jsx/index.ts`
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
### Issue 2: queryColumns Converted to Object Instead of Array
|
|
528
|
+
|
|
529
|
+
**Problem:**
|
|
530
|
+
When `queryColumns` was passed as an array `['name']` in the UIDL, it was being converted to an object `{0: 'name'}` in the generated code params.
|
|
531
|
+
|
|
532
|
+
**Example:**
|
|
533
|
+
```javascript
|
|
534
|
+
// Expected:
|
|
535
|
+
params={{ queryColumns: ['name'] }}
|
|
536
|
+
|
|
537
|
+
// Actual (incorrect):
|
|
538
|
+
params={{ queryColumns: {0: 'name'} }}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Root Cause:**
|
|
542
|
+
In `packages/teleport-plugin-common/src/utils/ast-utils.ts`, the `resolveObjectValue` function was checking `typeof prop.content === 'object'` which is true for arrays in JavaScript. Since arrays are objects, it was calling `objectToObjectExpression`, which converted the array to an object.
|
|
543
|
+
|
|
544
|
+
**Solution:**
|
|
545
|
+
Add an explicit array check before the object check in `resolveObjectValue`:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
export const resolveObjectValue = (prop: UIDLStaticValue | UIDLExpressionValue) => {
|
|
549
|
+
if (prop.type === 'static') {
|
|
550
|
+
const value =
|
|
551
|
+
typeof prop.content === 'string'
|
|
552
|
+
? types.stringLiteral(prop.content)
|
|
553
|
+
: typeof prop.content === 'boolean'
|
|
554
|
+
? types.booleanLiteral(prop.content)
|
|
555
|
+
: typeof prop.content === 'number'
|
|
556
|
+
? types.numericLiteral(prop.content)
|
|
557
|
+
: Array.isArray(prop.content) // Check array BEFORE object
|
|
558
|
+
? arrayToArrayExpression(prop.content)
|
|
559
|
+
: typeof prop.content === 'object'
|
|
560
|
+
? objectToObjectExpression(prop.content as unknown as Record<string, unknown>)
|
|
561
|
+
: types.identifier(String(prop.content))
|
|
562
|
+
|
|
563
|
+
return value
|
|
564
|
+
}
|
|
565
|
+
// ...
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Files Modified:**
|
|
570
|
+
- `packages/teleport-plugin-common/src/utils/ast-utils.ts`
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### Issue 3: Multiple DataProviders with Same Name/DataSourceId
|
|
575
|
+
|
|
576
|
+
**Problem:**
|
|
577
|
+
When there were multiple DataProviders using the same `name` (renderPropIdentifier) and `dataSourceId` (e.g., two `tessstt_users_data` or two `js_obje_data_data`), only the FIRST one would get the `fetchData` attribute added. Subsequent DataProviders would be skipped because the matching logic kept finding the first node.
|
|
578
|
+
|
|
579
|
+
**Example:**
|
|
580
|
+
In components with 4 DataProviders where 2 use `tessstt_users_data` and 2 use `js_obje_data_data`, only 2 out of 4 would get `fetchData`.
|
|
581
|
+
|
|
582
|
+
**Root Cause:**
|
|
583
|
+
In `packages/teleport-plugin-next-data-source/src/utils.ts`, the `extractDataSourceIntoNextAPIFolder` function was looping through `nodesLookup` and breaking on the first match. When processing the 3rd DataProvider (which had the same name as the 1st), it would find the 1st node, see it already had `fetchData`, and skip it entirely - never continuing the search for the 3rd node.
|
|
584
|
+
|
|
585
|
+
**Solution:**
|
|
586
|
+
During the search loop, skip JSX nodes that already have `fetchData` and continue searching for the next matching node:
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
// Before matching, check if this node already has fetchData - if so, skip it
|
|
590
|
+
// This is crucial for handling multiple DataProviders with same dataSourceId and name
|
|
591
|
+
const alreadyHasFetchData = attrs.some(
|
|
592
|
+
(attr) => (attr as types.JSXAttribute).name?.name === 'fetchData'
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if (dataSourceMatches && renderPropMatches && !alreadyHasFetchData) {
|
|
596
|
+
jsxNode = jsxElement as types.JSXElement
|
|
597
|
+
break
|
|
598
|
+
} else if (dataSourceMatches && renderPropMatches && alreadyHasFetchData) {
|
|
599
|
+
// Continue searching for the next matching node without fetchData
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Files Modified:**
|
|
604
|
+
- `packages/teleport-plugin-next-data-source/src/utils.ts`
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
### Issue 4: fetchData Triggered on Every Keystroke
|
|
609
|
+
|
|
610
|
+
**Problem:**
|
|
611
|
+
The `fetchData` function was being called on every keystroke in the search input, even though debouncing was implemented.
|
|
612
|
+
|
|
613
|
+
**Root Cause:**
|
|
614
|
+
Two issues:
|
|
615
|
+
1. The `params` object passed to DataProvider was being recreated on every render due to inline object literal
|
|
616
|
+
2. The `fetchData` function itself was being recreated on every render
|
|
617
|
+
|
|
618
|
+
**Solution:**
|
|
619
|
+
Wrap both `params` and `fetchData` in React hooks:
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
// Wrap params in useMemo
|
|
623
|
+
params={useMemo(
|
|
624
|
+
() => ({
|
|
625
|
+
page: paginationState_pg_0.page,
|
|
626
|
+
perPage: 1,
|
|
627
|
+
query: paginationState_pg_0.debouncedQuery,
|
|
628
|
+
queryColumns: JSON.stringify(['name']),
|
|
629
|
+
}),
|
|
630
|
+
[paginationState_pg_0]
|
|
631
|
+
)}
|
|
632
|
+
|
|
633
|
+
// Wrap fetchData in useCallback
|
|
634
|
+
fetchData={useCallback(
|
|
635
|
+
(params) =>
|
|
636
|
+
fetch(`/api/cockroachdb-users-19258764?${new URLSearchParams(params)}`)
|
|
637
|
+
.then((res) => res.json())
|
|
638
|
+
.then((response) => response?.data),
|
|
639
|
+
[]
|
|
640
|
+
)}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
**Files Modified:**
|
|
644
|
+
- `packages/teleport-plugin-next-data-source/src/pagination-plugin.ts`
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
### Issue 5: Double fetchData Calls After Debounce
|
|
649
|
+
|
|
650
|
+
**Problem:**
|
|
651
|
+
Even with memoization, when the debounced search query updated, it would trigger TWO separate state updates (`debouncedSearch_pg_0_query` and `pagination_pg_0_page` reset to 1), causing two `fetchData` calls.
|
|
652
|
+
|
|
653
|
+
**Root Cause:**
|
|
654
|
+
Both state variables were in the `params` dependency array of `useMemo`. Updating both caused two separate renders and two API calls.
|
|
655
|
+
|
|
656
|
+
**Solution:**
|
|
657
|
+
Combine page and debounced query into a single atomic state object:
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
// Combined state for atomic updates
|
|
661
|
+
const [paginationState_pg_0, setPaginationState_pg_0] = useState({
|
|
662
|
+
page: 1,
|
|
663
|
+
debouncedQuery: '',
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
// Update both atomically in debounce effect
|
|
667
|
+
useEffect(() => {
|
|
668
|
+
const timer = setTimeout(() => {
|
|
669
|
+
setPaginationState_pg_0({
|
|
670
|
+
page: 1,
|
|
671
|
+
debouncedQuery: search_pg_0_query,
|
|
672
|
+
})
|
|
673
|
+
}, 300)
|
|
674
|
+
return () => clearTimeout(timer)
|
|
675
|
+
}, [search_pg_0_query])
|
|
676
|
+
|
|
677
|
+
// Single dependency in useMemo
|
|
678
|
+
params={useMemo(
|
|
679
|
+
() => ({
|
|
680
|
+
page: paginationState_pg_0.page,
|
|
681
|
+
query: paginationState_pg_0.debouncedQuery,
|
|
682
|
+
// ...
|
|
683
|
+
}),
|
|
684
|
+
[paginationState_pg_0] // Single dependency
|
|
685
|
+
)}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
**Files Modified:**
|
|
689
|
+
- `packages/teleport-plugin-next-data-source/src/pagination-plugin.ts`
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
### Issue 6: useEffect Running on Mount
|
|
694
|
+
|
|
695
|
+
**Problem:**
|
|
696
|
+
`useEffect` hooks for debouncing and fetching count were running on component mount, causing unnecessary API calls.
|
|
697
|
+
|
|
698
|
+
**Root Cause:**
|
|
699
|
+
React `useEffect` always runs on mount by default, even when the intent is to only run when dependencies change.
|
|
700
|
+
|
|
701
|
+
**Solution:**
|
|
702
|
+
Use `useRef` to track the first render and skip effect execution on mount:
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
const skipDebounceOnMount_pg_0 = useRef(true)
|
|
706
|
+
const skipCountFetchOnMount_pg_0 = useRef(true)
|
|
707
|
+
|
|
708
|
+
useEffect(() => {
|
|
709
|
+
if (skipDebounceOnMount_pg_0.current) {
|
|
710
|
+
skipDebounceOnMount_pg_0.current = false
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
const timer = setTimeout(() => {
|
|
714
|
+
setPaginationState_pg_0({
|
|
715
|
+
page: 1,
|
|
716
|
+
debouncedQuery: search_pg_0_query,
|
|
717
|
+
})
|
|
718
|
+
}, 300)
|
|
719
|
+
return () => clearTimeout(timer)
|
|
720
|
+
}, [search_pg_0_query])
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
**Important:** Use separate `useRef` for each `useEffect` that needs to skip mount execution. Using a single ref for multiple effects is a code smell.
|
|
724
|
+
|
|
725
|
+
**Files Modified:**
|
|
726
|
+
- `packages/teleport-plugin-next-data-source/src/pagination-plugin.ts`
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
### Issue 7: data.count === 0 Not Updating maxPages
|
|
731
|
+
|
|
732
|
+
**Problem:**
|
|
733
|
+
When search results returned 0 items, the `maxPages` state wasn't being updated because `if (data && data.count)` would be falsy when count is 0.
|
|
734
|
+
|
|
735
|
+
**Root Cause:**
|
|
736
|
+
JavaScript treats `0` as falsy, so `data.count` in a boolean context would fail the check.
|
|
737
|
+
|
|
738
|
+
**Solution:**
|
|
739
|
+
Check for the existence of the property explicitly and handle 0 specially:
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
if (data && 'count' in data) {
|
|
743
|
+
setPagination_pg_0_maxPages(
|
|
744
|
+
data.count === 0 ? 0 : Math.ceil(data.count / perPage)
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**Files Modified:**
|
|
750
|
+
- `packages/teleport-plugin-next-data-source/src/pagination-plugin.ts`
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
### Issue 8: Plain DataProviders in Components Not Getting fetchData
|
|
755
|
+
|
|
756
|
+
**Problem:**
|
|
757
|
+
In components (which don't have `getStaticProps`), plain DataProviders (without pagination or search) weren't getting the `fetchData` method added, even though components always need client-side fetching.
|
|
758
|
+
|
|
759
|
+
**Root Cause:**
|
|
760
|
+
The component plugin was calling `extractDataSourceIntoNextAPIFolder` for all DataProviders, but the function was designed to work in conjunction with pagination/search detection. Plain DataProviders were being processed by a different code path that expected `getStaticProps` to exist.
|
|
761
|
+
|
|
762
|
+
**Solution:**
|
|
763
|
+
This was actually solved by fixing Issue 3 (Multiple DataProviders with Same Name). Once the matching logic correctly identified each unique DataProvider, all of them received `fetchData` attributes regardless of whether they had pagination/search enabled.
|
|
764
|
+
|
|
765
|
+
**Key Point:**
|
|
766
|
+
In components, ALL DataProviders need `fetchData` because there's no `getStaticProps` to provide initial data. The component plugin correctly calls `extractDataSourceIntoNextAPIFolder` for all data sources, and the fixed matching logic ensures each gets its `fetchData`.
|
|
767
|
+
|
|
768
|
+
**Files Modified:**
|
|
769
|
+
- `packages/teleport-plugin-next-data-source/src/utils.ts` (via Issue 3 fix)
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
### Issue 9: Fallback Search for Client-Side Data Sources
|
|
774
|
+
|
|
775
|
+
**Problem:**
|
|
776
|
+
For client-side data sources (JavaScript, CSV, Static Collection, Google Sheets), if `queryColumns` was not specified, search functionality would fail silently instead of searching across all fields.
|
|
777
|
+
|
|
778
|
+
**Requirement:**
|
|
779
|
+
Implement a fallback mechanism: if `queryColumns` is undefined but `query` is present, stringify each record and check if the query exists in the stringified content.
|
|
780
|
+
|
|
781
|
+
**Solution:**
|
|
782
|
+
Add fallback search logic to all client-side data source fetchers:
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
if (query) {
|
|
786
|
+
const searchQuery = query.toLowerCase()
|
|
787
|
+
|
|
788
|
+
if (queryColumns) {
|
|
789
|
+
// Search specific columns (existing logic)
|
|
790
|
+
const columns = typeof queryColumns === 'string'
|
|
791
|
+
? JSON.parse(queryColumns)
|
|
792
|
+
: (Array.isArray(queryColumns) ? queryColumns : [queryColumns])
|
|
793
|
+
|
|
794
|
+
data = data.filter(item => {
|
|
795
|
+
return columns.some(col => {
|
|
796
|
+
const value = item[col]
|
|
797
|
+
if (value === null || value === undefined) return false
|
|
798
|
+
return String(value).toLowerCase().includes(searchQuery)
|
|
799
|
+
})
|
|
800
|
+
})
|
|
801
|
+
} else {
|
|
802
|
+
// Fallback: search entire record
|
|
803
|
+
data = data.filter(item => {
|
|
804
|
+
try {
|
|
805
|
+
const itemString = JSON.stringify(item).toLowerCase()
|
|
806
|
+
return itemString.includes(searchQuery)
|
|
807
|
+
} catch (error) {
|
|
808
|
+
return false
|
|
809
|
+
}
|
|
810
|
+
})
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
**Files Modified:**
|
|
816
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/javascript.ts`
|
|
817
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/csv-file.ts`
|
|
818
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/static-collection.ts`
|
|
819
|
+
- `packages/teleport-plugin-next-data-source/src/fetchers/google-sheets.ts`
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
### Issue 10: Matching DataProviders by name Attribute
|
|
824
|
+
|
|
825
|
+
**Problem:**
|
|
826
|
+
Initial attempts to match DataProviders by checking for a `renderItem` attribute failed because DataProviders don't have a `renderItem` attribute - they have a `name` attribute.
|
|
827
|
+
|
|
828
|
+
**Root Cause:**
|
|
829
|
+
Misunderstanding of the generated JSX structure. Looking at the generated code showed:
|
|
830
|
+
```javascript
|
|
831
|
+
<DataProvider
|
|
832
|
+
name={'tessstt_users_data'} // ← This is the attribute we need
|
|
833
|
+
renderSuccess={(tessstt_users_data) => (...)}
|
|
834
|
+
/>
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**Solution:**
|
|
838
|
+
Match DataProviders by their `name` attribute, which contains the `renderPropIdentifier`:
|
|
839
|
+
|
|
840
|
+
```typescript
|
|
841
|
+
// Look for name attribute to match with renderPropIdentifier
|
|
842
|
+
const nameAttr = attrs.find(
|
|
843
|
+
(attr) =>
|
|
844
|
+
(attr as any).type === 'JSXAttribute' &&
|
|
845
|
+
(attr as types.JSXAttribute).name.name === 'name'
|
|
846
|
+
) as types.JSXAttribute | undefined
|
|
847
|
+
|
|
848
|
+
// Check if name matches the target
|
|
849
|
+
if (targetRenderProp && nameAttr && nameAttr.value) {
|
|
850
|
+
if (nameAttr.value.type === 'StringLiteral') {
|
|
851
|
+
if (nameAttr.value.value === targetRenderProp) {
|
|
852
|
+
renderPropMatches = true
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**Files Modified:**
|
|
859
|
+
- `packages/teleport-plugin-next-data-source/src/utils.ts`
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
## Best Practices for Future Development
|
|
864
|
+
|
|
865
|
+
Based on these issues, follow these best practices:
|
|
866
|
+
|
|
867
|
+
### 1. State Management
|
|
868
|
+
- **Use atomic state updates** when multiple related values need to change together
|
|
869
|
+
- **Always wrap params in useMemo** with correct dependencies to prevent unnecessary re-renders
|
|
870
|
+
- **Always wrap fetchData in useCallback** to prevent function recreation
|
|
871
|
+
- **Use useRef to skip mount execution** when effects should only run on dependency changes
|
|
872
|
+
- **Create separate useRef instances** for each useEffect that needs mount skipping
|
|
873
|
+
|
|
874
|
+
### 2. AST Manipulation
|
|
875
|
+
- **Always check for arrays before objects** in type checking (arrays are objects in JavaScript)
|
|
876
|
+
- **Match elements by multiple attributes** when uniqueness is required (e.g., dataSourceId + name)
|
|
877
|
+
- **Skip already-processed nodes** during iteration to handle duplicates correctly
|
|
878
|
+
- **Preserve original order** unless reordering is explicitly needed
|
|
879
|
+
|
|
880
|
+
### 3. Testing Edge Cases
|
|
881
|
+
- Always test with:
|
|
882
|
+
- **Multiple DataProviders** with same data source
|
|
883
|
+
- **Multiple DataProviders** with same name
|
|
884
|
+
- **Zero results** from search/filtering
|
|
885
|
+
- **Empty or undefined** optional parameters
|
|
886
|
+
- **Both pages and components** (different data fetching patterns)
|
|
887
|
+
- **All four mapper types**: pagination+search, pagination-only, search-only, plain
|
|
888
|
+
|
|
889
|
+
### 4. Debugging
|
|
890
|
+
- Use targeted console.log statements with prefixes like `[EXTRACT_API]`, `[REORDER]`
|
|
891
|
+
- Log all critical decision points and their inputs
|
|
892
|
+
- Remove all debug logs before committing
|
|
893
|
+
|
|
894
|
+
### 5. Data Source Compatibility
|
|
895
|
+
- Always handle both string and array formats for `queryColumns`
|
|
896
|
+
- Implement fallback mechanisms for optional parameters
|
|
897
|
+
- Test count fetchers apply the same filters as data fetchers
|
|
898
|
+
- Ensure backward compatibility when adding new features
|
|
899
|
+
|
|
900
|
+
## Fallback Search (No queryColumns Specified)
|
|
901
|
+
|
|
902
|
+
All data sources now support fallback search when `query` is present but `queryColumns` is undefined. The implementation varies by data source type:
|
|
903
|
+
|
|
904
|
+
### Client-Side Sources (JavaScript, CSV, Static Collection, Google Sheets)
|
|
905
|
+
When `queryColumns` is not provided, these sources stringify each record using `JSON.stringify()` and search within the stringified content. This allows searching across all fields without knowing the schema.
|
|
906
|
+
|
|
907
|
+
**Performance Impact:** Minimal, as data is already in memory.
|
|
908
|
+
|
|
909
|
+
### SQL-Based Sources (PostgreSQL, MySQL, MariaDB, CockroachDB)
|
|
910
|
+
When `queryColumns` is not provided, these sources:
|
|
911
|
+
1. Query `information_schema.columns` to get all column names for the table
|
|
912
|
+
2. Cast each column to text/CHAR for string comparison
|
|
913
|
+
3. Build an OR condition across all columns
|
|
914
|
+
|
|
915
|
+
**Performance Impact:** Moderate. An additional query to information_schema is required, and searching across all columns is slower than searching specific columns. For production use, always specify `queryColumns` when possible.
|
|
916
|
+
|
|
917
|
+
**SQL Generation Example (PostgreSQL):**
|
|
918
|
+
```sql
|
|
919
|
+
-- Get columns
|
|
920
|
+
SELECT column_name FROM information_schema.columns
|
|
921
|
+
WHERE table_name = 'users'
|
|
922
|
+
|
|
923
|
+
-- Then search (assuming columns: id, name, email, age)
|
|
924
|
+
SELECT * FROM users
|
|
925
|
+
WHERE (
|
|
926
|
+
id::text ILIKE '%john%' OR
|
|
927
|
+
name::text ILIKE '%john%' OR
|
|
928
|
+
email::text ILIKE '%john%' OR
|
|
929
|
+
age::text ILIKE '%john%'
|
|
930
|
+
)
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### MongoDB
|
|
934
|
+
When `queryColumns` is not provided, MongoDB:
|
|
935
|
+
1. Fetches a sample document using `findOne({})`
|
|
936
|
+
2. Extracts all field names from the sample (excluding `_id`)
|
|
937
|
+
3. Builds a `$or` condition with `$regex` across all fields
|
|
938
|
+
|
|
939
|
+
**Performance Impact:** Moderate. Assumes all documents have similar structure. If documents have varying schemas, only fields from the sample document will be searched.
|
|
940
|
+
|
|
941
|
+
### Supabase
|
|
942
|
+
When `queryColumns` is not provided, Supabase:
|
|
943
|
+
1. Fetches a single row using `.select('*').limit(1).single()`
|
|
944
|
+
2. Extracts all column names from the returned object
|
|
945
|
+
3. Builds an `.or()` condition with `.ilike` across all columns
|
|
946
|
+
|
|
947
|
+
**Performance Impact:** Moderate. Similar to SQL-based sources but with an additional API call.
|
|
948
|
+
|
|
949
|
+
### Error Handling
|
|
950
|
+
|
|
951
|
+
All fallback operations are wrapped in try-catch blocks to ensure graceful degradation:
|
|
952
|
+
|
|
953
|
+
```javascript
|
|
954
|
+
try {
|
|
955
|
+
// Attempt to fetch column metadata
|
|
956
|
+
const schemaResult = await pool.query('SELECT column_name FROM information_schema.columns...')
|
|
957
|
+
columns = schemaResult.rows.map(row => row.column_name)
|
|
958
|
+
} catch (schemaError) {
|
|
959
|
+
console.warn('Failed to fetch column names from information_schema:', schemaError.message)
|
|
960
|
+
// Continue without search if we can't get columns
|
|
961
|
+
}
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
**Behavior on Failure:**
|
|
965
|
+
- If the metadata query fails (e.g., permissions issue, table doesn't exist), the error is logged as a warning
|
|
966
|
+
- The search operation is **silently skipped** - no search filter is applied
|
|
967
|
+
- The main data fetch continues normally, returning all results (as if no `query` parameter was provided)
|
|
968
|
+
- The API does not return a 500 error due to metadata query failure
|
|
969
|
+
|
|
970
|
+
This ensures that:
|
|
971
|
+
1. **Resilience**: A metadata query failure doesn't crash the entire endpoint
|
|
972
|
+
2. **Graceful Degradation**: Users still get results, just without search filtering
|
|
973
|
+
3. **Debuggability**: Warnings are logged for troubleshooting
|
|
974
|
+
4. **User Experience**: The UI remains functional even if search temporarily fails
|
|
975
|
+
|
|
976
|
+
### Recommendations
|
|
977
|
+
1. **Always specify `queryColumns` in production** for better performance and more predictable results
|
|
978
|
+
2. **Use fallback search for prototyping or admin interfaces** where convenience is more important than performance
|
|
979
|
+
3. **For SQL sources**, ensure your `information_schema` queries are efficient and the database user has appropriate permissions
|
|
980
|
+
4. **For MongoDB**, ensure documents have consistent schemas for reliable fallback search
|
|
981
|
+
5. **Test fallback search thoroughly** with your actual data schema before deploying
|
|
982
|
+
6. **Monitor warning logs** in production to catch any metadata query failures
|
|
983
|
+
|