@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.
Files changed (240) hide show
  1. package/ARRAY_MAPPER_PAGINATION.md +1128 -0
  2. package/LICENSE +21 -0
  3. package/README.md +40 -0
  4. package/SEARCH_IMPLEMENTATION_SUMMARY.md +983 -0
  5. package/__tests__/fetchers.test.ts +545 -0
  6. package/__tests__/integration.test.ts +561 -0
  7. package/__tests__/mocks.ts +241 -0
  8. package/__tests__/pagination.test.ts +31 -0
  9. package/__tests__/plugin.test.ts +577 -0
  10. package/__tests__/utils.test.ts +430 -0
  11. package/__tests__/validation.test.ts +348 -0
  12. package/dist/cjs/array-mapper-pagination.d.ts +32 -0
  13. package/dist/cjs/array-mapper-pagination.d.ts.map +1 -0
  14. package/dist/cjs/array-mapper-pagination.js +77 -0
  15. package/dist/cjs/array-mapper-pagination.js.map +1 -0
  16. package/dist/cjs/count-fetchers.d.ts +12 -0
  17. package/dist/cjs/count-fetchers.d.ts.map +1 -0
  18. package/dist/cjs/count-fetchers.js +46 -0
  19. package/dist/cjs/count-fetchers.js.map +1 -0
  20. package/dist/cjs/data-source-fetchers.d.ts +14 -0
  21. package/dist/cjs/data-source-fetchers.d.ts.map +1 -0
  22. package/dist/cjs/data-source-fetchers.js +185 -0
  23. package/dist/cjs/data-source-fetchers.js.map +1 -0
  24. package/dist/cjs/fetchers/airtable.d.ts +6 -0
  25. package/dist/cjs/fetchers/airtable.d.ts.map +1 -0
  26. package/dist/cjs/fetchers/airtable.js +27 -0
  27. package/dist/cjs/fetchers/airtable.js.map +1 -0
  28. package/dist/cjs/fetchers/clickhouse.d.ts +6 -0
  29. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -0
  30. package/dist/cjs/fetchers/clickhouse.js +29 -0
  31. package/dist/cjs/fetchers/clickhouse.js.map +1 -0
  32. package/dist/cjs/fetchers/csv-file.d.ts +7 -0
  33. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -0
  34. package/dist/cjs/fetchers/csv-file.js +36 -0
  35. package/dist/cjs/fetchers/csv-file.js.map +1 -0
  36. package/dist/cjs/fetchers/firestore.d.ts +6 -0
  37. package/dist/cjs/fetchers/firestore.d.ts.map +1 -0
  38. package/dist/cjs/fetchers/firestore.js +35 -0
  39. package/dist/cjs/fetchers/firestore.js.map +1 -0
  40. package/dist/cjs/fetchers/google-sheets.d.ts +6 -0
  41. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -0
  42. package/dist/cjs/fetchers/google-sheets.js +30 -0
  43. package/dist/cjs/fetchers/google-sheets.js.map +1 -0
  44. package/dist/cjs/fetchers/index.d.ts +17 -0
  45. package/dist/cjs/fetchers/index.d.ts.map +1 -0
  46. package/dist/cjs/fetchers/index.js +56 -0
  47. package/dist/cjs/fetchers/index.js.map +1 -0
  48. package/dist/cjs/fetchers/javascript.d.ts +7 -0
  49. package/dist/cjs/fetchers/javascript.d.ts.map +1 -0
  50. package/dist/cjs/fetchers/javascript.js +40 -0
  51. package/dist/cjs/fetchers/javascript.js.map +1 -0
  52. package/dist/cjs/fetchers/mariadb.d.ts +3 -0
  53. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -0
  54. package/dist/cjs/fetchers/mariadb.js +23 -0
  55. package/dist/cjs/fetchers/mariadb.js.map +1 -0
  56. package/dist/cjs/fetchers/mongodb.d.ts +7 -0
  57. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -0
  58. package/dist/cjs/fetchers/mongodb.js +52 -0
  59. package/dist/cjs/fetchers/mongodb.js.map +1 -0
  60. package/dist/cjs/fetchers/mysql.d.ts +3 -0
  61. package/dist/cjs/fetchers/mysql.d.ts.map +1 -0
  62. package/dist/cjs/fetchers/mysql.js +30 -0
  63. package/dist/cjs/fetchers/mysql.js.map +1 -0
  64. package/dist/cjs/fetchers/postgresql.d.ts +3 -0
  65. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -0
  66. package/dist/cjs/fetchers/postgresql.js +25 -0
  67. package/dist/cjs/fetchers/postgresql.js.map +1 -0
  68. package/dist/cjs/fetchers/redis.d.ts +6 -0
  69. package/dist/cjs/fetchers/redis.d.ts.map +1 -0
  70. package/dist/cjs/fetchers/redis.js +46 -0
  71. package/dist/cjs/fetchers/redis.js.map +1 -0
  72. package/dist/cjs/fetchers/redshift.d.ts +2 -0
  73. package/dist/cjs/fetchers/redshift.d.ts.map +1 -0
  74. package/dist/cjs/fetchers/redshift.js +24 -0
  75. package/dist/cjs/fetchers/redshift.js.map +1 -0
  76. package/dist/cjs/fetchers/rest-api.d.ts +6 -0
  77. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -0
  78. package/dist/cjs/fetchers/rest-api.js +58 -0
  79. package/dist/cjs/fetchers/rest-api.js.map +1 -0
  80. package/dist/cjs/fetchers/static-collection.d.ts +7 -0
  81. package/dist/cjs/fetchers/static-collection.d.ts.map +1 -0
  82. package/dist/cjs/fetchers/static-collection.js +24 -0
  83. package/dist/cjs/fetchers/static-collection.js.map +1 -0
  84. package/dist/cjs/fetchers/supabase.d.ts +7 -0
  85. package/dist/cjs/fetchers/supabase.d.ts.map +1 -0
  86. package/dist/cjs/fetchers/supabase.js +42 -0
  87. package/dist/cjs/fetchers/supabase.js.map +1 -0
  88. package/dist/cjs/fetchers/turso.d.ts +6 -0
  89. package/dist/cjs/fetchers/turso.d.ts.map +1 -0
  90. package/dist/cjs/fetchers/turso.js +25 -0
  91. package/dist/cjs/fetchers/turso.js.map +1 -0
  92. package/dist/cjs/index.d.ts +9 -0
  93. package/dist/cjs/index.d.ts.map +1 -0
  94. package/dist/cjs/index.js +325 -0
  95. package/dist/cjs/index.js.map +1 -0
  96. package/dist/cjs/pagination-plugin.d.ts +5 -0
  97. package/dist/cjs/pagination-plugin.d.ts.map +1 -0
  98. package/dist/cjs/pagination-plugin.js +1484 -0
  99. package/dist/cjs/pagination-plugin.js.map +1 -0
  100. package/dist/cjs/pagination-with-count.d.ts +6 -0
  101. package/dist/cjs/pagination-with-count.d.ts.map +1 -0
  102. package/dist/cjs/pagination-with-count.js +63 -0
  103. package/dist/cjs/pagination-with-count.js.map +1 -0
  104. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  105. package/dist/cjs/utils.d.ts +31 -0
  106. package/dist/cjs/utils.d.ts.map +1 -0
  107. package/dist/cjs/utils.js +763 -0
  108. package/dist/cjs/utils.js.map +1 -0
  109. package/dist/cjs/validation.d.ts +5 -0
  110. package/dist/cjs/validation.d.ts.map +1 -0
  111. package/dist/cjs/validation.js +29 -0
  112. package/dist/cjs/validation.js.map +1 -0
  113. package/dist/esm/array-mapper-pagination.d.ts +32 -0
  114. package/dist/esm/array-mapper-pagination.d.ts.map +1 -0
  115. package/dist/esm/array-mapper-pagination.js +72 -0
  116. package/dist/esm/array-mapper-pagination.js.map +1 -0
  117. package/dist/esm/count-fetchers.d.ts +12 -0
  118. package/dist/esm/count-fetchers.d.ts.map +1 -0
  119. package/dist/esm/count-fetchers.js +35 -0
  120. package/dist/esm/count-fetchers.js.map +1 -0
  121. package/dist/esm/data-source-fetchers.d.ts +14 -0
  122. package/dist/esm/data-source-fetchers.d.ts.map +1 -0
  123. package/dist/esm/data-source-fetchers.js +179 -0
  124. package/dist/esm/data-source-fetchers.js.map +1 -0
  125. package/dist/esm/fetchers/airtable.d.ts +6 -0
  126. package/dist/esm/fetchers/airtable.d.ts.map +1 -0
  127. package/dist/esm/fetchers/airtable.js +22 -0
  128. package/dist/esm/fetchers/airtable.js.map +1 -0
  129. package/dist/esm/fetchers/clickhouse.d.ts +6 -0
  130. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -0
  131. package/dist/esm/fetchers/clickhouse.js +24 -0
  132. package/dist/esm/fetchers/clickhouse.js.map +1 -0
  133. package/dist/esm/fetchers/csv-file.d.ts +7 -0
  134. package/dist/esm/fetchers/csv-file.d.ts.map +1 -0
  135. package/dist/esm/fetchers/csv-file.js +30 -0
  136. package/dist/esm/fetchers/csv-file.js.map +1 -0
  137. package/dist/esm/fetchers/firestore.d.ts +6 -0
  138. package/dist/esm/fetchers/firestore.d.ts.map +1 -0
  139. package/dist/esm/fetchers/firestore.js +30 -0
  140. package/dist/esm/fetchers/firestore.js.map +1 -0
  141. package/dist/esm/fetchers/google-sheets.d.ts +6 -0
  142. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -0
  143. package/dist/esm/fetchers/google-sheets.js +25 -0
  144. package/dist/esm/fetchers/google-sheets.js.map +1 -0
  145. package/dist/esm/fetchers/index.d.ts +17 -0
  146. package/dist/esm/fetchers/index.d.ts.map +1 -0
  147. package/dist/esm/fetchers/index.js +17 -0
  148. package/dist/esm/fetchers/index.js.map +1 -0
  149. package/dist/esm/fetchers/javascript.d.ts +7 -0
  150. package/dist/esm/fetchers/javascript.d.ts.map +1 -0
  151. package/dist/esm/fetchers/javascript.js +34 -0
  152. package/dist/esm/fetchers/javascript.js.map +1 -0
  153. package/dist/esm/fetchers/mariadb.d.ts +3 -0
  154. package/dist/esm/fetchers/mariadb.d.ts.map +1 -0
  155. package/dist/esm/fetchers/mariadb.js +18 -0
  156. package/dist/esm/fetchers/mariadb.js.map +1 -0
  157. package/dist/esm/fetchers/mongodb.d.ts +7 -0
  158. package/dist/esm/fetchers/mongodb.d.ts.map +1 -0
  159. package/dist/esm/fetchers/mongodb.js +46 -0
  160. package/dist/esm/fetchers/mongodb.js.map +1 -0
  161. package/dist/esm/fetchers/mysql.d.ts +3 -0
  162. package/dist/esm/fetchers/mysql.d.ts.map +1 -0
  163. package/dist/esm/fetchers/mysql.js +25 -0
  164. package/dist/esm/fetchers/mysql.js.map +1 -0
  165. package/dist/esm/fetchers/postgresql.d.ts +3 -0
  166. package/dist/esm/fetchers/postgresql.d.ts.map +1 -0
  167. package/dist/esm/fetchers/postgresql.js +20 -0
  168. package/dist/esm/fetchers/postgresql.js.map +1 -0
  169. package/dist/esm/fetchers/redis.d.ts +6 -0
  170. package/dist/esm/fetchers/redis.d.ts.map +1 -0
  171. package/dist/esm/fetchers/redis.js +41 -0
  172. package/dist/esm/fetchers/redis.js.map +1 -0
  173. package/dist/esm/fetchers/redshift.d.ts +2 -0
  174. package/dist/esm/fetchers/redshift.d.ts.map +1 -0
  175. package/dist/esm/fetchers/redshift.js +20 -0
  176. package/dist/esm/fetchers/redshift.js.map +1 -0
  177. package/dist/esm/fetchers/rest-api.d.ts +6 -0
  178. package/dist/esm/fetchers/rest-api.d.ts.map +1 -0
  179. package/dist/esm/fetchers/rest-api.js +53 -0
  180. package/dist/esm/fetchers/rest-api.js.map +1 -0
  181. package/dist/esm/fetchers/static-collection.d.ts +7 -0
  182. package/dist/esm/fetchers/static-collection.d.ts.map +1 -0
  183. package/dist/esm/fetchers/static-collection.js +18 -0
  184. package/dist/esm/fetchers/static-collection.js.map +1 -0
  185. package/dist/esm/fetchers/supabase.d.ts +7 -0
  186. package/dist/esm/fetchers/supabase.d.ts.map +1 -0
  187. package/dist/esm/fetchers/supabase.js +36 -0
  188. package/dist/esm/fetchers/supabase.js.map +1 -0
  189. package/dist/esm/fetchers/turso.d.ts +6 -0
  190. package/dist/esm/fetchers/turso.d.ts.map +1 -0
  191. package/dist/esm/fetchers/turso.js +20 -0
  192. package/dist/esm/fetchers/turso.js.map +1 -0
  193. package/dist/esm/index.d.ts +9 -0
  194. package/dist/esm/index.d.ts.map +1 -0
  195. package/dist/esm/index.js +306 -0
  196. package/dist/esm/index.js.map +1 -0
  197. package/dist/esm/pagination-plugin.d.ts +5 -0
  198. package/dist/esm/pagination-plugin.d.ts.map +1 -0
  199. package/dist/esm/pagination-plugin.js +1457 -0
  200. package/dist/esm/pagination-plugin.js.map +1 -0
  201. package/dist/esm/pagination-with-count.d.ts +6 -0
  202. package/dist/esm/pagination-with-count.d.ts.map +1 -0
  203. package/dist/esm/pagination-with-count.js +34 -0
  204. package/dist/esm/pagination-with-count.js.map +1 -0
  205. package/dist/esm/tsconfig.tsbuildinfo +1 -0
  206. package/dist/esm/utils.d.ts +31 -0
  207. package/dist/esm/utils.d.ts.map +1 -0
  208. package/dist/esm/utils.js +722 -0
  209. package/dist/esm/utils.js.map +1 -0
  210. package/dist/esm/validation.d.ts +5 -0
  211. package/dist/esm/validation.d.ts.map +1 -0
  212. package/dist/esm/validation.js +25 -0
  213. package/dist/esm/validation.js.map +1 -0
  214. package/package.json +33 -0
  215. package/src/array-mapper-pagination.ts +113 -0
  216. package/src/count-fetchers.ts +99 -0
  217. package/src/data-source-fetchers.ts +313 -0
  218. package/src/fetchers/airtable.ts +153 -0
  219. package/src/fetchers/clickhouse.ts +127 -0
  220. package/src/fetchers/csv-file.ts +163 -0
  221. package/src/fetchers/firestore.ts +138 -0
  222. package/src/fetchers/google-sheets.ts +189 -0
  223. package/src/fetchers/index.ts +32 -0
  224. package/src/fetchers/javascript.ts +150 -0
  225. package/src/fetchers/mariadb.ts +230 -0
  226. package/src/fetchers/mongodb.ts +239 -0
  227. package/src/fetchers/mysql.ts +237 -0
  228. package/src/fetchers/postgresql.ts +247 -0
  229. package/src/fetchers/redis.ts +152 -0
  230. package/src/fetchers/redshift.ts +138 -0
  231. package/src/fetchers/rest-api.ts +148 -0
  232. package/src/fetchers/static-collection.ts +149 -0
  233. package/src/fetchers/supabase.ts +246 -0
  234. package/src/fetchers/turso.ts +131 -0
  235. package/src/index.ts +352 -0
  236. package/src/pagination-plugin.ts +2335 -0
  237. package/src/pagination-with-count.ts +89 -0
  238. package/src/utils.ts +1013 -0
  239. package/src/validation.ts +32 -0
  240. 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
+