@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,2335 @@
1
+ import {
2
+ ComponentPlugin,
3
+ ComponentPluginFactory,
4
+ ChunkType,
5
+ FileType,
6
+ } from '@teleporthq/teleport-types'
7
+ import * as types from '@babel/types'
8
+ import { generatePaginationLogic, ArrayMapperPaginationInfo } from './array-mapper-pagination'
9
+ import { extractDataSourceIntoNextAPIFolder } from './utils'
10
+
11
+ interface DetectedPagination {
12
+ paginationNodeClass: string
13
+ prevButtonClass: string | null
14
+ nextButtonClass: string | null
15
+ dataSourceIdentifier: string
16
+ dataProviderJSX: any
17
+ arrayMapperRenderProp?: string
18
+ searchInputClass?: string | null
19
+ searchInputJSX?: any
20
+ }
21
+
22
+ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> = () => {
23
+ const paginationPlugin: ComponentPlugin = async (structure) => {
24
+ const { uidl, chunks, dependencies, options } = structure
25
+
26
+ const componentChunk = chunks.find((chunk) => chunk.name === 'jsx-component')
27
+ if (!componentChunk || componentChunk.type !== ChunkType.AST) {
28
+ return structure
29
+ }
30
+
31
+ if (!options || !options.dataSources || !options.extractedResources) {
32
+ return structure
33
+ }
34
+
35
+ const variableDeclaration = componentChunk.content as types.VariableDeclaration
36
+ if (!variableDeclaration.declarations || variableDeclaration.declarations.length === 0) {
37
+ return structure
38
+ }
39
+ const declarator = variableDeclaration.declarations[0] as types.VariableDeclarator
40
+ if (!declarator.init) {
41
+ return structure
42
+ }
43
+ const arrowFunction = declarator.init as types.ArrowFunctionExpression
44
+ if (!arrowFunction.body) {
45
+ return structure
46
+ }
47
+ const blockStatement = arrowFunction.body as types.BlockStatement
48
+
49
+ const { paginatedMappers, searchOnlyMappers, paginationOnlyMappers } =
50
+ detectPaginationsAndSearchFromJSX(blockStatement, uidl.node)
51
+
52
+ if (
53
+ paginatedMappers.length === 0 &&
54
+ searchOnlyMappers.length === 0 &&
55
+ paginationOnlyMappers.length === 0
56
+ ) {
57
+ return structure
58
+ }
59
+
60
+ // Combine pagination+search and pagination-only into one array for processing
61
+ const detectedPaginations = [...paginatedMappers, ...paginationOnlyMappers]
62
+ const detectedSearchOnly = searchOnlyMappers
63
+
64
+ if (!dependencies.useState) {
65
+ dependencies.useState = {
66
+ type: 'library',
67
+ path: 'react',
68
+ version: '',
69
+ meta: {
70
+ namedImport: true,
71
+ },
72
+ }
73
+ }
74
+
75
+ if (!dependencies.useMemo) {
76
+ dependencies.useMemo = {
77
+ type: 'library',
78
+ path: 'react',
79
+ version: '',
80
+ meta: {
81
+ namedImport: true,
82
+ },
83
+ }
84
+ }
85
+
86
+ if (!dependencies.useCallback) {
87
+ dependencies.useCallback = {
88
+ type: 'library',
89
+ path: 'react',
90
+ version: '',
91
+ meta: {
92
+ namedImport: true,
93
+ },
94
+ }
95
+ }
96
+
97
+ if (!dependencies.useRef) {
98
+ dependencies.useRef = {
99
+ type: 'library',
100
+ path: 'react',
101
+ version: '',
102
+ meta: {
103
+ namedImport: true,
104
+ },
105
+ }
106
+ }
107
+
108
+ if (!componentChunk.meta) {
109
+ componentChunk.meta = {}
110
+ }
111
+ componentChunk.meta.isClientComponent = true
112
+
113
+ const paginationInfos: ArrayMapperPaginationInfo[] = []
114
+
115
+ // Check if this is a page (has getStaticProps) or a component
116
+ const getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
117
+ const isPage = !!getStaticPropsChunk
118
+ const isComponent = !isPage
119
+
120
+ // Get pagination and search config from early extraction (done before transformations)
121
+ const opts = options as any
122
+ const perPageMap = opts.paginationConfig?.perPageMap || new Map<string, number>()
123
+ const searchConfigMap = opts.paginationConfig?.searchConfigMap || new Map<string, any>()
124
+ const queryColumnsMap = opts.paginationConfig?.queryColumnsMap || new Map<string, string[]>()
125
+
126
+ detectedPaginations.forEach((detected, index) => {
127
+ const paginationNodeId = `pg_${index}`
128
+ // Use arrayMapperRenderProp if available, otherwise fall back to dataSourceIdentifier
129
+ const lookupKey = detected.arrayMapperRenderProp || detected.dataSourceIdentifier
130
+ const perPage = perPageMap.get(lookupKey) || 10
131
+ const searchConfig = searchConfigMap.get(lookupKey)
132
+ // queryColumns is keyed by dataSourceIdentifier, not array mapper render prop
133
+ const queryColumns = queryColumnsMap.get(detected.dataSourceIdentifier)
134
+
135
+ const info = generatePaginationLogic(
136
+ paginationNodeId,
137
+ detected.dataSourceIdentifier,
138
+ perPage,
139
+ searchConfig,
140
+ queryColumns
141
+ )
142
+ paginationInfos.push(info)
143
+
144
+ // If both pagination and search are enabled, combine them into a single state object
145
+ if (info.searchEnabled && info.searchQueryVar && info.setSearchQueryVar) {
146
+ // Combined state: { page: 1, debouncedQuery: '' }
147
+ const combinedStateVar = `paginationState_pg_${index}`
148
+ const setCombinedStateVar = `setPaginationState_pg_${index}`
149
+
150
+ const combinedStateAST = types.variableDeclaration('const', [
151
+ types.variableDeclarator(
152
+ types.arrayPattern([
153
+ types.identifier(combinedStateVar),
154
+ types.identifier(setCombinedStateVar),
155
+ ]),
156
+ types.callExpression(types.identifier('useState'), [
157
+ types.objectExpression([
158
+ types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
159
+ types.objectProperty(types.identifier('debouncedQuery'), types.stringLiteral('')),
160
+ ]),
161
+ ])
162
+ ),
163
+ ])
164
+ blockStatement.body.unshift(combinedStateAST)
165
+
166
+ // Still need the immediate search query state for the input
167
+ const searchStateAST = types.variableDeclaration('const', [
168
+ types.variableDeclarator(
169
+ types.arrayPattern([
170
+ types.identifier(info.searchQueryVar),
171
+ types.identifier(info.setSearchQueryVar),
172
+ ]),
173
+ types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
174
+ ),
175
+ ])
176
+ blockStatement.body.unshift(searchStateAST)
177
+
178
+ // Store the combined state var names
179
+ ;(info as any).combinedStateVar = combinedStateVar
180
+ ;(info as any).setCombinedStateVar = setCombinedStateVar
181
+ } else {
182
+ // If search is not enabled, just add regular page state
183
+ const pageStateAST = types.variableDeclaration('const', [
184
+ types.variableDeclarator(
185
+ types.arrayPattern([
186
+ types.identifier(info.pageStateVar),
187
+ types.identifier(info.setPageStateVar),
188
+ ]),
189
+ types.callExpression(types.identifier('useState'), [types.numericLiteral(1)])
190
+ ),
191
+ ])
192
+ blockStatement.body.unshift(pageStateAST)
193
+ }
194
+
195
+ // Add maxPages state
196
+ const maxPagesStateVar = `${info.pageStateVar.replace('_page', '')}_maxPages`
197
+ const setMaxPagesStateVar = `set${
198
+ maxPagesStateVar.charAt(0).toUpperCase() + maxPagesStateVar.slice(1)
199
+ }`
200
+
201
+ // For pages: initialize from props (with pagination-specific prop name), for components: initialize to 0
202
+ const maxPagesInitValue = isPage
203
+ ? types.logicalExpression(
204
+ '||',
205
+ types.optionalMemberExpression(
206
+ types.identifier('props'),
207
+ types.identifier(`${info.dataSourceIdentifier}_pg_${index}_maxPages`),
208
+ false,
209
+ true
210
+ ),
211
+ types.numericLiteral(0)
212
+ )
213
+ : types.numericLiteral(0)
214
+
215
+ const maxPagesStateAST = types.variableDeclaration('const', [
216
+ types.variableDeclarator(
217
+ types.arrayPattern([
218
+ types.identifier(maxPagesStateVar),
219
+ types.identifier(setMaxPagesStateVar),
220
+ ]),
221
+ types.callExpression(types.identifier('useState'), [maxPagesInitValue])
222
+ ),
223
+ ])
224
+ blockStatement.body.unshift(maxPagesStateAST)
225
+
226
+ // Store these for later use
227
+ ;(info as any).maxPagesStateVar = maxPagesStateVar
228
+ ;(info as any).setMaxPagesStateVar = setMaxPagesStateVar
229
+
230
+ // Add refs to track first render for each useEffect
231
+ if (info.searchEnabled) {
232
+ const skipDebounceOnMountRefVar = `skipDebounceOnMount_pg_${index}`
233
+ const skipDebounceRefAST = types.variableDeclaration('const', [
234
+ types.variableDeclarator(
235
+ types.identifier(skipDebounceOnMountRefVar),
236
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
237
+ ),
238
+ ])
239
+ blockStatement.body.unshift(skipDebounceRefAST)
240
+ ;(info as any).skipDebounceOnMountRefVar = skipDebounceOnMountRefVar
241
+
242
+ const skipCountFetchOnMountRefVar = `skipCountFetchOnMount_pg_${index}`
243
+ const skipCountFetchRefAST = types.variableDeclaration('const', [
244
+ types.variableDeclarator(
245
+ types.identifier(skipCountFetchOnMountRefVar),
246
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
247
+ ),
248
+ ])
249
+ blockStatement.body.unshift(skipCountFetchRefAST)
250
+ ;(info as any).skipCountFetchOnMountRefVar = skipCountFetchOnMountRefVar
251
+ }
252
+ })
253
+
254
+ // Add useEffect dependency if any pagination has search enabled
255
+ const hasSearchEnabled = paginationInfos.some((info) => info.searchEnabled)
256
+ if (hasSearchEnabled && !dependencies.useEffect) {
257
+ dependencies.useEffect = {
258
+ type: 'library',
259
+ path: 'react',
260
+ version: '',
261
+ meta: {
262
+ namedImport: true,
263
+ },
264
+ }
265
+ }
266
+
267
+ // Add all useEffect hooks AFTER state declarations
268
+ // Find the first return statement and insert effects before it
269
+ const firstReturnIndex = blockStatement.body.findIndex(
270
+ (stmt: any) => stmt.type === 'ReturnStatement'
271
+ )
272
+ const insertIndex = firstReturnIndex !== -1 ? firstReturnIndex : blockStatement.body.length
273
+
274
+ // Add effects in reverse order so they appear in correct order
275
+ paginationInfos.forEach((info) => {
276
+ if (
277
+ info.searchEnabled &&
278
+ info.searchQueryVar &&
279
+ info.debouncedSearchQueryVar &&
280
+ info.setDebouncedSearchQueryVar
281
+ ) {
282
+ // Add effect that updates debounced search after delay
283
+ const skipDebounceOnMountRefVar = (info as any).skipDebounceOnMountRefVar
284
+ const setCombinedStateVar = (info as any).setCombinedStateVar
285
+
286
+ const debounceEffect = types.expressionStatement(
287
+ types.callExpression(types.identifier('useEffect'), [
288
+ types.arrowFunctionExpression(
289
+ [],
290
+ types.blockStatement([
291
+ types.ifStatement(
292
+ types.memberExpression(
293
+ types.identifier(skipDebounceOnMountRefVar),
294
+ types.identifier('current')
295
+ ),
296
+ types.blockStatement([
297
+ types.expressionStatement(
298
+ types.assignmentExpression(
299
+ '=',
300
+ types.memberExpression(
301
+ types.identifier(skipDebounceOnMountRefVar),
302
+ types.identifier('current')
303
+ ),
304
+ types.booleanLiteral(false)
305
+ )
306
+ ),
307
+ types.returnStatement(),
308
+ ])
309
+ ),
310
+ types.variableDeclaration('const', [
311
+ types.variableDeclarator(
312
+ types.identifier('timer'),
313
+ types.callExpression(types.identifier('setTimeout'), [
314
+ types.arrowFunctionExpression(
315
+ [],
316
+ types.blockStatement([
317
+ types.expressionStatement(
318
+ types.callExpression(types.identifier(setCombinedStateVar), [
319
+ types.objectExpression([
320
+ types.objectProperty(
321
+ types.identifier('page'),
322
+ types.numericLiteral(1)
323
+ ),
324
+ types.objectProperty(
325
+ types.identifier('debouncedQuery'),
326
+ types.identifier(info.searchQueryVar)
327
+ ),
328
+ ]),
329
+ ])
330
+ ),
331
+ ])
332
+ ),
333
+ types.numericLiteral(info.searchDebounce || 300),
334
+ ])
335
+ ),
336
+ ]),
337
+ types.returnStatement(
338
+ types.arrowFunctionExpression(
339
+ [],
340
+ types.callExpression(types.identifier('clearTimeout'), [
341
+ types.identifier('timer'),
342
+ ])
343
+ )
344
+ ),
345
+ ])
346
+ ),
347
+ types.arrayExpression([types.identifier(info.searchQueryVar)]),
348
+ ])
349
+ )
350
+
351
+ blockStatement.body.splice(insertIndex, 0, debounceEffect)
352
+
353
+ // Add useEffect to refetch count when search changes (for both pages and components)
354
+ if (info.queryColumns && info.queryColumns.length > 0) {
355
+ const detected = detectedPaginations.find(
356
+ (d) => d.dataSourceIdentifier === info.dataSourceIdentifier
357
+ )
358
+ if (!detected) {
359
+ return
360
+ }
361
+
362
+ const resourceDefAttr = detected.dataProviderJSX.openingElement.attributes.find(
363
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
364
+ )
365
+
366
+ if (
367
+ resourceDefAttr &&
368
+ resourceDefAttr.value &&
369
+ resourceDefAttr.value.type === 'JSXExpressionContainer'
370
+ ) {
371
+ const resourceDef = resourceDefAttr.value.expression
372
+ if (resourceDef.type === 'ObjectExpression') {
373
+ const dataSourceIdProp = (resourceDef.properties as any[]).find(
374
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceId'
375
+ )
376
+ const tableNameProp = (resourceDef.properties as any[]).find(
377
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'tableName'
378
+ )
379
+ const dataSourceTypeProp = (resourceDef.properties as any[]).find(
380
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceType'
381
+ )
382
+
383
+ if (dataSourceIdProp && tableNameProp && dataSourceTypeProp) {
384
+ const dataSourceId = dataSourceIdProp.value.value
385
+ const tableName = tableNameProp.value.value
386
+ const dataSourceType = dataSourceTypeProp.value.value
387
+ const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
388
+ const setMaxPagesStateVar = (info as any).setMaxPagesStateVar
389
+
390
+ // Create useEffect to refetch count when debounced search changes
391
+ const skipCountFetchOnMountRefVar = (info as any).skipCountFetchOnMountRefVar
392
+ const combinedStateVar = (info as any).combinedStateVar
393
+
394
+ const refetchCountEffect = types.expressionStatement(
395
+ types.callExpression(types.identifier('useEffect'), [
396
+ types.arrowFunctionExpression(
397
+ [],
398
+ types.blockStatement([
399
+ types.ifStatement(
400
+ types.memberExpression(
401
+ types.identifier(skipCountFetchOnMountRefVar),
402
+ types.identifier('current')
403
+ ),
404
+ types.blockStatement([
405
+ types.expressionStatement(
406
+ types.assignmentExpression(
407
+ '=',
408
+ types.memberExpression(
409
+ types.identifier(skipCountFetchOnMountRefVar),
410
+ types.identifier('current')
411
+ ),
412
+ types.booleanLiteral(false)
413
+ )
414
+ ),
415
+ types.returnStatement(),
416
+ ])
417
+ ),
418
+ types.expressionStatement(
419
+ types.callExpression(
420
+ types.memberExpression(
421
+ types.callExpression(
422
+ types.memberExpression(
423
+ types.callExpression(types.identifier('fetch'), [
424
+ types.templateLiteral(
425
+ [
426
+ types.templateElement({
427
+ raw: `/api/${fileName}-count?`,
428
+ cooked: `/api/${fileName}-count?`,
429
+ }),
430
+ types.templateElement({ raw: '', cooked: '' }),
431
+ ],
432
+ [
433
+ types.newExpression(types.identifier('URLSearchParams'), [
434
+ types.objectExpression([
435
+ types.objectProperty(
436
+ types.identifier('query'),
437
+ types.memberExpression(
438
+ types.identifier(combinedStateVar),
439
+ types.identifier('debouncedQuery')
440
+ )
441
+ ),
442
+ types.objectProperty(
443
+ types.identifier('queryColumns'),
444
+ types.callExpression(
445
+ types.memberExpression(
446
+ types.identifier('JSON'),
447
+ types.identifier('stringify')
448
+ ),
449
+ [
450
+ types.arrayExpression(
451
+ info.queryColumns.map((col) =>
452
+ types.stringLiteral(col)
453
+ )
454
+ ),
455
+ ]
456
+ )
457
+ ),
458
+ ]),
459
+ ]),
460
+ ]
461
+ ),
462
+ ]),
463
+ types.identifier('then')
464
+ ),
465
+ [
466
+ types.arrowFunctionExpression(
467
+ [types.identifier('res')],
468
+ types.callExpression(
469
+ types.memberExpression(
470
+ types.identifier('res'),
471
+ types.identifier('json')
472
+ ),
473
+ []
474
+ )
475
+ ),
476
+ ]
477
+ ),
478
+ types.identifier('then')
479
+ ),
480
+ [
481
+ types.arrowFunctionExpression(
482
+ [types.identifier('data')],
483
+ types.blockStatement([
484
+ types.ifStatement(
485
+ types.logicalExpression(
486
+ '&&',
487
+ types.identifier('data'),
488
+ types.binaryExpression(
489
+ 'in',
490
+ types.stringLiteral('count'),
491
+ types.identifier('data')
492
+ )
493
+ ),
494
+ types.blockStatement([
495
+ types.expressionStatement(
496
+ types.callExpression(
497
+ types.identifier(setMaxPagesStateVar),
498
+ [
499
+ types.conditionalExpression(
500
+ types.binaryExpression(
501
+ '===',
502
+ types.memberExpression(
503
+ types.identifier('data'),
504
+ types.identifier('count')
505
+ ),
506
+ types.numericLiteral(0)
507
+ ),
508
+ types.numericLiteral(0),
509
+ types.callExpression(
510
+ types.memberExpression(
511
+ types.identifier('Math'),
512
+ types.identifier('ceil')
513
+ ),
514
+ [
515
+ types.binaryExpression(
516
+ '/',
517
+ types.memberExpression(
518
+ types.identifier('data'),
519
+ types.identifier('count')
520
+ ),
521
+ types.numericLiteral(info.perPage)
522
+ ),
523
+ ]
524
+ )
525
+ ),
526
+ ]
527
+ )
528
+ ),
529
+ ])
530
+ ),
531
+ ])
532
+ ),
533
+ ]
534
+ )
535
+ ),
536
+ ])
537
+ ),
538
+ types.arrayExpression([
539
+ types.memberExpression(
540
+ types.identifier(combinedStateVar),
541
+ types.identifier('debouncedQuery')
542
+ ),
543
+ ]),
544
+ ])
545
+ )
546
+
547
+ blockStatement.body.splice(insertIndex, 0, refetchCountEffect)
548
+ }
549
+ }
550
+ }
551
+ }
552
+ }
553
+ })
554
+
555
+ // For components, add useEffect to fetch count on mount
556
+ if (isComponent) {
557
+ if (!dependencies.useEffect) {
558
+ dependencies.useEffect = {
559
+ type: 'library',
560
+ path: 'react',
561
+ version: '',
562
+ meta: {
563
+ namedImport: true,
564
+ },
565
+ }
566
+ }
567
+
568
+ // Group paginationInfos by data source identifier to avoid duplicate fetches
569
+ const dataSourceToInfos = new Map<
570
+ string,
571
+ Array<{ info: ArrayMapperPaginationInfo; detected: any; fileName: string }>
572
+ >()
573
+
574
+ paginationInfos.forEach((info) => {
575
+ const detected = detectedPaginations.find(
576
+ (d) => d.dataSourceIdentifier === info.dataSourceIdentifier
577
+ )
578
+ if (!detected) {
579
+ return
580
+ }
581
+
582
+ const resourceDefAttr = detected.dataProviderJSX.openingElement.attributes.find(
583
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
584
+ )
585
+
586
+ if (
587
+ !resourceDefAttr ||
588
+ !resourceDefAttr.value ||
589
+ resourceDefAttr.value.type !== 'JSXExpressionContainer'
590
+ ) {
591
+ return
592
+ }
593
+
594
+ const resourceDef = resourceDefAttr.value.expression
595
+ if (resourceDef.type !== 'ObjectExpression') {
596
+ return
597
+ }
598
+
599
+ const dataSourceIdProp = (resourceDef.properties as any[]).find(
600
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceId'
601
+ )
602
+ const tableNameProp = (resourceDef.properties as any[]).find(
603
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'tableName'
604
+ )
605
+ const dataSourceTypeProp = (resourceDef.properties as any[]).find(
606
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceType'
607
+ )
608
+
609
+ if (!dataSourceIdProp || !tableNameProp || !dataSourceTypeProp) {
610
+ return
611
+ }
612
+
613
+ const dataSourceId = dataSourceIdProp.value.value
614
+ const tableName = tableNameProp.value.value
615
+ const dataSourceType = dataSourceTypeProp.value.value
616
+ const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
617
+
618
+ // Group by data source identifier
619
+ if (!dataSourceToInfos.has(info.dataSourceIdentifier)) {
620
+ dataSourceToInfos.set(info.dataSourceIdentifier, [])
621
+ }
622
+ dataSourceToInfos.get(info.dataSourceIdentifier)!.push({ info, detected, fileName })
623
+ })
624
+
625
+ // Create ONE useEffect per unique data source
626
+ dataSourceToInfos.forEach((infos) => {
627
+ const { fileName } = infos[0]
628
+
629
+ // Create array of setState calls - one for each pagination using this data source
630
+ const setStateStatements = infos.map(({ info }) => {
631
+ const setMaxPagesStateVar = (info as any).setMaxPagesStateVar
632
+ return types.expressionStatement(
633
+ types.callExpression(types.identifier(setMaxPagesStateVar), [
634
+ types.callExpression(
635
+ types.memberExpression(types.identifier('Math'), types.identifier('ceil')),
636
+ [
637
+ types.binaryExpression(
638
+ '/',
639
+ types.memberExpression(types.identifier('data'), types.identifier('count')),
640
+ types.numericLiteral(info.perPage)
641
+ ),
642
+ ]
643
+ ),
644
+ ])
645
+ )
646
+ })
647
+
648
+ // Create useEffect to fetch count on mount - sets ALL maxPages for this data source
649
+ const useEffectAST = types.expressionStatement(
650
+ types.callExpression(types.identifier('useEffect'), [
651
+ types.arrowFunctionExpression(
652
+ [],
653
+ types.blockStatement([
654
+ types.expressionStatement(
655
+ types.callExpression(
656
+ types.memberExpression(
657
+ types.callExpression(
658
+ types.memberExpression(
659
+ types.callExpression(types.identifier('fetch'), [
660
+ types.stringLiteral(`/api/${fileName}-count`),
661
+ ]),
662
+ types.identifier('then')
663
+ ),
664
+ [
665
+ types.arrowFunctionExpression(
666
+ [types.identifier('res')],
667
+ types.callExpression(
668
+ types.memberExpression(
669
+ types.identifier('res'),
670
+ types.identifier('json')
671
+ ),
672
+ []
673
+ )
674
+ ),
675
+ ]
676
+ ),
677
+ types.identifier('then')
678
+ ),
679
+ [
680
+ types.arrowFunctionExpression(
681
+ [types.identifier('data')],
682
+ types.blockStatement([
683
+ types.ifStatement(
684
+ types.logicalExpression(
685
+ '&&',
686
+ types.identifier('data'),
687
+ types.memberExpression(
688
+ types.identifier('data'),
689
+ types.identifier('count')
690
+ )
691
+ ),
692
+ types.blockStatement(setStateStatements)
693
+ ),
694
+ ])
695
+ ),
696
+ ]
697
+ )
698
+ ),
699
+ ])
700
+ ),
701
+ types.arrayExpression([]),
702
+ ])
703
+ )
704
+
705
+ blockStatement.body.unshift(useEffectAST)
706
+ })
707
+ }
708
+
709
+ createAPIRoutesForPaginatedDataSources(
710
+ uidl.node,
711
+ options.dataSources,
712
+ componentChunk,
713
+ options.extractedResources,
714
+ paginationInfos,
715
+ isComponent
716
+ )
717
+
718
+ detectedPaginations.forEach((detected, index) => {
719
+ addPaginationParamsToDataProvider(detected.dataProviderJSX, paginationInfos[index], index)
720
+ })
721
+
722
+ modifyPaginationButtons(blockStatement, detectedPaginations, paginationInfos)
723
+ modifySearchInputs(blockStatement, detectedPaginations, paginationInfos)
724
+
725
+ if (isPage) {
726
+ modifyGetStaticPropsForPagination(chunks, paginationInfos)
727
+ }
728
+
729
+ // Handle search-only array mappers (no need to filter - they're already separate)
730
+ // The detection function already separated them into different arrays
731
+ const searchOnlyDataSources = detectedSearchOnly
732
+
733
+ if (searchOnlyDataSources.length > 0) {
734
+ handleSearchOnlyArrayMappers(
735
+ blockStatement,
736
+ searchOnlyDataSources,
737
+ searchConfigMap,
738
+ queryColumnsMap,
739
+ insertIndex,
740
+ dependencies
741
+ )
742
+
743
+ // Create API routes for search-only data sources
744
+ createAPIRoutesForSearchOnlyDataSources(
745
+ uidl.node,
746
+ options.dataSources,
747
+ componentChunk,
748
+ options.extractedResources,
749
+ searchOnlyDataSources
750
+ )
751
+ }
752
+
753
+ return structure
754
+ }
755
+
756
+ return paginationPlugin
757
+ }
758
+
759
+ function detectPaginationsAndSearchFromJSX(
760
+ blockStatement: types.BlockStatement,
761
+ uidlNode: any
762
+ ): {
763
+ paginatedMappers: DetectedPagination[]
764
+ searchOnlyMappers: DetectedPagination[]
765
+ paginationOnlyMappers: DetectedPagination[]
766
+ plainMappers: DetectedPagination[]
767
+ } {
768
+ interface DataProviderInfo {
769
+ identifier: string
770
+ dataProvider: any
771
+ arrayMapperRenderProp?: string
772
+ paginationNode?: { class: string; prevClass: string | null; nextClass: string | null }
773
+ searchInput?: { class: string | null; jsx: any }
774
+ hasPagination: boolean
775
+ hasSearch: boolean
776
+ }
777
+
778
+ const dataProviderMap = new Map<string, DataProviderInfo>()
779
+
780
+ // First pass: collect array mapper info from UIDL, keyed by array mapper render prop (unique)
781
+ interface ArrayMapperInfo {
782
+ dataSourceIdentifier: string
783
+ renderProp: string
784
+ paginated: boolean
785
+ searchEnabled: boolean
786
+ }
787
+ const arrayMapperInfoMap = new Map<string, ArrayMapperInfo>()
788
+
789
+ if (uidlNode) {
790
+ const collectArrayMapperInfo = (node: any): void => {
791
+ if (!node) {
792
+ return
793
+ }
794
+ if (node.type === 'data-source-list' && node.content?.renderPropIdentifier) {
795
+ const dataSourceIdentifier = node.content.renderPropIdentifier
796
+ if (node.content.nodes?.success?.content?.children) {
797
+ node.content.nodes.success.content.children.forEach((child: any) => {
798
+ if (child.type === 'cms-list-repeater' && child.content?.renderPropIdentifier) {
799
+ const arrayMapperRenderProp = child.content.renderPropIdentifier
800
+ // Key by array mapper render prop (unique), not data source identifier
801
+ arrayMapperInfoMap.set(arrayMapperRenderProp, {
802
+ dataSourceIdentifier,
803
+ renderProp: arrayMapperRenderProp,
804
+ paginated: child.content.paginated || false,
805
+ searchEnabled: child.content.searchEnabled || false,
806
+ })
807
+ }
808
+ })
809
+ }
810
+ }
811
+ if (node.content?.children) {
812
+ node.content.children.forEach((child: any) => collectArrayMapperInfo(child))
813
+ }
814
+ if (node.children && Array.isArray(node.children)) {
815
+ node.children.forEach((child: any) => collectArrayMapperInfo(child))
816
+ }
817
+ }
818
+ collectArrayMapperInfo(uidlNode)
819
+ }
820
+
821
+ // Second pass: find all DataProviders and their parent containers
822
+ interface DataProviderWithParent {
823
+ dataProvider: any
824
+ parent: any
825
+ }
826
+ const dataProvidersWithParents: DataProviderWithParent[] = []
827
+
828
+ const findDataProvidersAndParents = (node: any, parent: any = null): void => {
829
+ if (!node) {
830
+ return
831
+ }
832
+
833
+ // Check if this is a DataProvider
834
+ if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'DataProvider') {
835
+ dataProvidersWithParents.push({
836
+ dataProvider: node,
837
+ parent,
838
+ })
839
+ }
840
+
841
+ // Only recurse through JSX structure, not into embedded expressions or code
842
+ if (node.type === 'ReturnStatement' && node.argument) {
843
+ findDataProvidersAndParents(node.argument, parent)
844
+ } else if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
845
+ // Recurse through JSX children
846
+ if (node.children && Array.isArray(node.children)) {
847
+ node.children.forEach((child: any) => findDataProvidersAndParents(child, node))
848
+ }
849
+ } else if (node.type === 'JSXExpressionContainer') {
850
+ // For expression containers, continue but don't go into the expression itself
851
+ if (
852
+ node.expression &&
853
+ (node.expression.type === 'JSXElement' || node.expression.type === 'JSXFragment')
854
+ ) {
855
+ findDataProvidersAndParents(node.expression, parent)
856
+ }
857
+ } else if (node.type === 'BlockStatement') {
858
+ // For block statements (function bodies), look through body array
859
+ if (node.body && Array.isArray(node.body)) {
860
+ node.body.forEach((stmt: any) => findDataProvidersAndParents(stmt, node))
861
+ }
862
+ } else if (node.type === 'ConditionalExpression') {
863
+ // Check both branches of conditional
864
+ findDataProvidersAndParents(node.consequent, parent)
865
+ findDataProvidersAndParents(node.alternate, parent)
866
+ }
867
+ }
868
+
869
+ findDataProvidersAndParents(blockStatement)
870
+
871
+ // Now process each DataProvider and find its siblings
872
+ dataProvidersWithParents.forEach(({ dataProvider, parent }) => {
873
+ const nameAttr = dataProvider.openingElement.attributes.find(
874
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'name'
875
+ )
876
+
877
+ // Find the array mapper render prop by looking inside renderSuccess -> Repeater -> renderItem param
878
+ const renderSuccessAttr = dataProvider.openingElement.attributes.find(
879
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderSuccess'
880
+ )
881
+
882
+ let arrayMapperRenderProp: string | undefined
883
+ if (renderSuccessAttr && renderSuccessAttr.value?.type === 'JSXExpressionContainer') {
884
+ const renderFunc = renderSuccessAttr.value.expression
885
+ if (renderFunc.type === 'ArrowFunctionExpression') {
886
+ // Look for Repeater inside the render function
887
+ const findRepeater = (node: any): any => {
888
+ if (!node) {
889
+ return null
890
+ }
891
+ if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'Repeater') {
892
+ return node
893
+ }
894
+ if (node.body) {
895
+ return findRepeater(node.body)
896
+ }
897
+ if (node.children && Array.isArray(node.children)) {
898
+ for (const child of node.children) {
899
+ const result = findRepeater(child)
900
+ if (result) {
901
+ return result
902
+ }
903
+ }
904
+ }
905
+ if (node.type === 'JSXFragment' || node.type === 'JSXElement') {
906
+ if (node.children && Array.isArray(node.children)) {
907
+ for (const child of node.children) {
908
+ const result = findRepeater(child)
909
+ if (result) {
910
+ return result
911
+ }
912
+ }
913
+ }
914
+ }
915
+ if (node.type === 'JSXExpressionContainer') {
916
+ return findRepeater(node.expression)
917
+ }
918
+ if (node.expression) {
919
+ return findRepeater(node.expression)
920
+ }
921
+ if (node.consequent) {
922
+ const result = findRepeater(node.consequent)
923
+ if (result) {
924
+ return result
925
+ }
926
+ }
927
+ if (node.alternate) {
928
+ return findRepeater(node.alternate)
929
+ }
930
+ return null
931
+ }
932
+
933
+ const repeater = findRepeater(renderFunc)
934
+ if (repeater) {
935
+ // Find renderItem attribute on Repeater
936
+ const renderItemAttr = repeater.openingElement.attributes.find(
937
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderItem'
938
+ )
939
+ if (renderItemAttr && renderItemAttr.value?.type === 'JSXExpressionContainer') {
940
+ const renderItemFunc = renderItemAttr.value.expression
941
+ if (
942
+ renderItemFunc.type === 'ArrowFunctionExpression' &&
943
+ renderItemFunc.params &&
944
+ renderItemFunc.params.length > 0
945
+ ) {
946
+ const param = renderItemFunc.params[0]
947
+ if (param.type === 'Identifier') {
948
+ arrayMapperRenderProp = param.name
949
+ }
950
+ }
951
+ }
952
+ }
953
+ }
954
+ }
955
+
956
+ if (
957
+ !nameAttr ||
958
+ !nameAttr.value ||
959
+ nameAttr.value.type !== 'JSXExpressionContainer' ||
960
+ !arrayMapperRenderProp
961
+ ) {
962
+ return
963
+ }
964
+
965
+ const dataProviderIdentifier = nameAttr.value.expression.value
966
+ let paginationNodeInfo: {
967
+ class: string
968
+ prevClass: string | null
969
+ nextClass: string | null
970
+ } | null = null
971
+ let searchInputInfo: { class: string | null; jsx: any } | null = null
972
+
973
+ // Look through parent's children for siblings
974
+ if (parent && parent.children && Array.isArray(parent.children)) {
975
+ parent.children.forEach((sibling: any) => {
976
+ if (sibling === dataProvider) {
977
+ return
978
+ }
979
+
980
+ if (sibling.type === 'JSXElement') {
981
+ const siblingClassName = getClassName(sibling.openingElement?.attributes || [])
982
+ const siblingElementName = sibling.openingElement?.name?.name
983
+
984
+ // Found pagination node
985
+ if (siblingClassName && siblingClassName.includes('cms-pagination-node')) {
986
+ const prevClass = findChildWithClass(sibling, 'previous')
987
+ const nextClass = findChildWithClass(sibling, 'next')
988
+ if (prevClass || nextClass) {
989
+ paginationNodeInfo = {
990
+ class: siblingClassName,
991
+ prevClass,
992
+ nextClass,
993
+ }
994
+ }
995
+ }
996
+
997
+ // Found search container - search for input inside it
998
+ if (siblingClassName && siblingClassName.includes('data-source-search-node')) {
999
+ if (sibling.children && Array.isArray(sibling.children)) {
1000
+ sibling.children.forEach((searchChild: any) => {
1001
+ if (searchChild.type === 'JSXElement') {
1002
+ const searchChildElementName = searchChild.openingElement?.name?.name
1003
+ const searchChildClassName = getClassName(
1004
+ searchChild.openingElement?.attributes || []
1005
+ )
1006
+ if (
1007
+ searchChildClassName &&
1008
+ searchChildClassName.includes('search-input') &&
1009
+ searchChildElementName === 'input'
1010
+ ) {
1011
+ searchInputInfo = {
1012
+ class: searchChildClassName,
1013
+ jsx: searchChild,
1014
+ }
1015
+ }
1016
+ }
1017
+ })
1018
+ }
1019
+ }
1020
+
1021
+ // Also check if search input is a direct sibling
1022
+ if (
1023
+ siblingClassName &&
1024
+ siblingClassName.includes('search-input') &&
1025
+ siblingElementName === 'input'
1026
+ ) {
1027
+ searchInputInfo = {
1028
+ class: siblingClassName,
1029
+ jsx: sibling,
1030
+ }
1031
+ }
1032
+ }
1033
+ })
1034
+ }
1035
+
1036
+ // Record the DataProvider with its pagination/search info
1037
+ const uniqueKey = `${dataProviderIdentifier}__${arrayMapperRenderProp}`
1038
+ dataProviderMap.set(uniqueKey, {
1039
+ identifier: dataProviderIdentifier,
1040
+ dataProvider,
1041
+ arrayMapperRenderProp,
1042
+ paginationNode: paginationNodeInfo || undefined,
1043
+ searchInput: searchInputInfo || undefined,
1044
+ hasPagination: !!paginationNodeInfo,
1045
+ hasSearch: !!searchInputInfo,
1046
+ })
1047
+ })
1048
+
1049
+ // Categorize data providers
1050
+ const paginatedMappers: DetectedPagination[] = []
1051
+ const searchOnlyMappers: DetectedPagination[] = []
1052
+ const paginationOnlyMappers: DetectedPagination[] = []
1053
+ const plainMappers: DetectedPagination[] = []
1054
+
1055
+ dataProviderMap.forEach((info) => {
1056
+ if (info.hasPagination && info.hasSearch) {
1057
+ // Pagination + Search
1058
+ paginatedMappers.push({
1059
+ paginationNodeClass: info.paginationNode!.class,
1060
+ prevButtonClass: info.paginationNode!.prevClass,
1061
+ nextButtonClass: info.paginationNode!.nextClass,
1062
+ dataSourceIdentifier: info.identifier,
1063
+ dataProviderJSX: info.dataProvider,
1064
+ arrayMapperRenderProp: info.arrayMapperRenderProp,
1065
+ searchInputClass: info.searchInput?.class,
1066
+ searchInputJSX: info.searchInput?.jsx,
1067
+ })
1068
+ } else if (info.hasPagination && !info.hasSearch) {
1069
+ // Pagination only
1070
+ paginationOnlyMappers.push({
1071
+ paginationNodeClass: info.paginationNode!.class,
1072
+ prevButtonClass: info.paginationNode!.prevClass,
1073
+ nextButtonClass: info.paginationNode!.nextClass,
1074
+ dataSourceIdentifier: info.identifier,
1075
+ dataProviderJSX: info.dataProvider,
1076
+ arrayMapperRenderProp: info.arrayMapperRenderProp,
1077
+ searchInputClass: undefined,
1078
+ searchInputJSX: undefined,
1079
+ })
1080
+ } else if (!info.hasPagination && info.hasSearch) {
1081
+ // Search only
1082
+ searchOnlyMappers.push({
1083
+ paginationNodeClass: '',
1084
+ prevButtonClass: null,
1085
+ nextButtonClass: null,
1086
+ dataSourceIdentifier: info.identifier,
1087
+ dataProviderJSX: info.dataProvider,
1088
+ arrayMapperRenderProp: info.arrayMapperRenderProp,
1089
+ searchInputClass: info.searchInput?.class,
1090
+ searchInputJSX: info.searchInput?.jsx,
1091
+ })
1092
+ } else {
1093
+ // Plain (no pagination, no search)
1094
+ plainMappers.push({
1095
+ paginationNodeClass: '',
1096
+ prevButtonClass: null,
1097
+ nextButtonClass: null,
1098
+ dataSourceIdentifier: info.identifier,
1099
+ dataProviderJSX: info.dataProvider,
1100
+ arrayMapperRenderProp: info.arrayMapperRenderProp,
1101
+ searchInputClass: undefined,
1102
+ searchInputJSX: undefined,
1103
+ })
1104
+ }
1105
+ })
1106
+
1107
+ return { paginatedMappers, searchOnlyMappers, paginationOnlyMappers, plainMappers }
1108
+ }
1109
+
1110
+ function handleSearchOnlyArrayMappers(
1111
+ blockStatement: types.BlockStatement,
1112
+ searchOnlyDataSources: DetectedPagination[],
1113
+ searchConfigMap: Map<string, any>,
1114
+ queryColumnsMap: Map<string, string[]>,
1115
+ insertIndex: number,
1116
+ dependencies: any
1117
+ ): void {
1118
+ searchOnlyDataSources.forEach((detected, index) => {
1119
+ const searchId = `search_${index}`
1120
+ const searchQueryVar = `${searchId}_query`
1121
+ const setSearchQueryVar = `set${
1122
+ searchQueryVar.charAt(0).toUpperCase() + searchQueryVar.slice(1)
1123
+ }`
1124
+ const debouncedSearchQueryVar = `debounced${
1125
+ searchQueryVar.charAt(0).toUpperCase() + searchQueryVar.slice(1)
1126
+ }`
1127
+ const setDebouncedSearchQueryVar = `set${
1128
+ debouncedSearchQueryVar.charAt(0).toUpperCase() + debouncedSearchQueryVar.slice(1)
1129
+ }`
1130
+ const skipDebounceOnMountRefVar = `skipDebounceOnMount${searchId}`
1131
+
1132
+ const searchConfig = searchConfigMap.get(detected.dataSourceIdentifier)
1133
+ const searchDebounce = searchConfig?.searchDebounce || 300
1134
+ const queryColumns = queryColumnsMap.get(detected.dataSourceIdentifier)
1135
+
1136
+ // Add search query state
1137
+ const searchStateAST = types.variableDeclaration('const', [
1138
+ types.variableDeclarator(
1139
+ types.arrayPattern([types.identifier(searchQueryVar), types.identifier(setSearchQueryVar)]),
1140
+ types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1141
+ ),
1142
+ ])
1143
+ blockStatement.body.unshift(searchStateAST)
1144
+
1145
+ // Add debounced search state
1146
+ const debouncedSearchStateAST = types.variableDeclaration('const', [
1147
+ types.variableDeclarator(
1148
+ types.arrayPattern([
1149
+ types.identifier(debouncedSearchQueryVar),
1150
+ types.identifier(setDebouncedSearchQueryVar),
1151
+ ]),
1152
+ types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1153
+ ),
1154
+ ])
1155
+ blockStatement.body.unshift(debouncedSearchStateAST)
1156
+
1157
+ // Add skip ref
1158
+ const skipRefAST = types.variableDeclaration('const', [
1159
+ types.variableDeclarator(
1160
+ types.identifier(skipDebounceOnMountRefVar),
1161
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
1162
+ ),
1163
+ ])
1164
+ blockStatement.body.unshift(skipRefAST)
1165
+
1166
+ // Add useEffect for debouncing
1167
+ if (!dependencies.useEffect) {
1168
+ dependencies.useEffect = {
1169
+ type: 'library',
1170
+ path: 'react',
1171
+ version: '',
1172
+ meta: {
1173
+ namedImport: true,
1174
+ },
1175
+ }
1176
+ }
1177
+
1178
+ const debounceEffect = types.expressionStatement(
1179
+ types.callExpression(types.identifier('useEffect'), [
1180
+ types.arrowFunctionExpression(
1181
+ [],
1182
+ types.blockStatement([
1183
+ types.ifStatement(
1184
+ types.memberExpression(
1185
+ types.identifier(skipDebounceOnMountRefVar),
1186
+ types.identifier('current')
1187
+ ),
1188
+ types.blockStatement([
1189
+ types.expressionStatement(
1190
+ types.assignmentExpression(
1191
+ '=',
1192
+ types.memberExpression(
1193
+ types.identifier(skipDebounceOnMountRefVar),
1194
+ types.identifier('current')
1195
+ ),
1196
+ types.booleanLiteral(false)
1197
+ )
1198
+ ),
1199
+ types.returnStatement(),
1200
+ ])
1201
+ ),
1202
+ types.variableDeclaration('const', [
1203
+ types.variableDeclarator(
1204
+ types.identifier('timer'),
1205
+ types.callExpression(types.identifier('setTimeout'), [
1206
+ types.arrowFunctionExpression(
1207
+ [],
1208
+ types.blockStatement([
1209
+ types.expressionStatement(
1210
+ types.callExpression(types.identifier(setDebouncedSearchQueryVar), [
1211
+ types.identifier(searchQueryVar),
1212
+ ])
1213
+ ),
1214
+ ])
1215
+ ),
1216
+ types.numericLiteral(searchDebounce),
1217
+ ])
1218
+ ),
1219
+ ]),
1220
+ types.returnStatement(
1221
+ types.arrowFunctionExpression(
1222
+ [],
1223
+ types.callExpression(types.identifier('clearTimeout'), [types.identifier('timer')])
1224
+ )
1225
+ ),
1226
+ ])
1227
+ ),
1228
+ types.arrayExpression([types.identifier(searchQueryVar)]),
1229
+ ])
1230
+ )
1231
+ blockStatement.body.splice(insertIndex, 0, debounceEffect)
1232
+
1233
+ // Modify DataProvider to include search params (even without queryColumns)
1234
+ if (detected.dataProviderJSX) {
1235
+ addSearchParamsToDataProvider(detected.dataProviderJSX, {
1236
+ debouncedSearchQueryVar,
1237
+ queryColumns: queryColumns || [],
1238
+ })
1239
+ }
1240
+
1241
+ // Modify search input
1242
+ if (detected.searchInputJSX) {
1243
+ addSearchInputHandlers(detected.searchInputJSX, {
1244
+ searchQueryVar,
1245
+ setSearchQueryVar,
1246
+ searchEnabled: true,
1247
+ } as any)
1248
+ }
1249
+ })
1250
+ }
1251
+
1252
+ function addSearchParamsToDataProvider(
1253
+ dataProviderJSX: any,
1254
+ config: { debouncedSearchQueryVar: string; queryColumns: string[] }
1255
+ ): void {
1256
+ if (!dataProviderJSX || !dataProviderJSX.openingElement) {
1257
+ return
1258
+ }
1259
+
1260
+ const { debouncedSearchQueryVar, queryColumns } = config
1261
+
1262
+ // 1. Update params to be wrapped in useMemo
1263
+ const existingParamsAttr = dataProviderJSX.openingElement.attributes.find(
1264
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'params'
1265
+ )
1266
+
1267
+ if (existingParamsAttr && existingParamsAttr.value?.type === 'JSXExpressionContainer') {
1268
+ const paramsObj = existingParamsAttr.value.expression
1269
+
1270
+ if (paramsObj.type === 'ObjectExpression') {
1271
+ // Add query to existing params properties
1272
+ paramsObj.properties.push(
1273
+ types.objectProperty(types.identifier('query'), types.identifier(debouncedSearchQueryVar))
1274
+ )
1275
+
1276
+ // Add queryColumns only if they exist
1277
+ if (queryColumns.length > 0) {
1278
+ paramsObj.properties.push(
1279
+ types.objectProperty(
1280
+ types.identifier('queryColumns'),
1281
+ types.callExpression(
1282
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1283
+ [types.arrayExpression(queryColumns.map((col) => types.stringLiteral(col)))]
1284
+ )
1285
+ )
1286
+ )
1287
+ }
1288
+
1289
+ // Wrap in useMemo with debouncedSearchQueryVar as dependency
1290
+ const memoizedParamsValue = types.callExpression(types.identifier('useMemo'), [
1291
+ types.arrowFunctionExpression([], paramsObj),
1292
+ types.arrayExpression([types.identifier(debouncedSearchQueryVar)]),
1293
+ ])
1294
+
1295
+ existingParamsAttr.value.expression = memoizedParamsValue
1296
+ }
1297
+ }
1298
+
1299
+ // 2. Update initialData to be conditional on empty search
1300
+ const existingInitialDataAttr = dataProviderJSX.openingElement.attributes.find(
1301
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'initialData'
1302
+ )
1303
+
1304
+ if (existingInitialDataAttr && existingInitialDataAttr.value?.type === 'JSXExpressionContainer') {
1305
+ const currentInitialData = existingInitialDataAttr.value.expression
1306
+
1307
+ // Make it conditional: only use initialData if search is empty
1308
+ existingInitialDataAttr.value.expression = types.conditionalExpression(
1309
+ types.unaryExpression('!', types.identifier(debouncedSearchQueryVar), true),
1310
+ currentInitialData,
1311
+ types.identifier('undefined')
1312
+ )
1313
+ }
1314
+
1315
+ // 3. Update key to include search query
1316
+ const existingKeyAttr = dataProviderJSX.openingElement.attributes.find(
1317
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'key'
1318
+ )
1319
+
1320
+ const keyExpression = types.templateLiteral(
1321
+ [
1322
+ types.templateElement({ raw: 'search-', cooked: 'search-' }),
1323
+ types.templateElement({ raw: '', cooked: '' }),
1324
+ ],
1325
+ [types.identifier(debouncedSearchQueryVar)]
1326
+ )
1327
+
1328
+ const keyAttr = types.jsxAttribute(
1329
+ types.jsxIdentifier('key'),
1330
+ types.jsxExpressionContainer(keyExpression)
1331
+ )
1332
+
1333
+ if (existingKeyAttr) {
1334
+ const index = dataProviderJSX.openingElement.attributes.indexOf(existingKeyAttr)
1335
+ dataProviderJSX.openingElement.attributes[index] = keyAttr
1336
+ } else {
1337
+ dataProviderJSX.openingElement.attributes.push(keyAttr)
1338
+ }
1339
+ }
1340
+
1341
+ function addPaginationParamsToDataProvider(
1342
+ dataProviderJSX: any,
1343
+ info: ArrayMapperPaginationInfo,
1344
+ paginationIndex: number
1345
+ ): void {
1346
+ if (!dataProviderJSX || !dataProviderJSX.openingElement) {
1347
+ return
1348
+ }
1349
+
1350
+ const combinedStateVar = (info as any).combinedStateVar
1351
+
1352
+ let paramsProperties: any[]
1353
+ let dependencies: any[]
1354
+
1355
+ if (info.searchEnabled && combinedStateVar) {
1356
+ // Use combined state for both page and query
1357
+ paramsProperties = [
1358
+ types.objectProperty(
1359
+ types.identifier('page'),
1360
+ types.memberExpression(types.identifier(combinedStateVar), types.identifier('page'))
1361
+ ),
1362
+ types.objectProperty(types.identifier('perPage'), types.numericLiteral(info.perPage)),
1363
+ types.objectProperty(
1364
+ types.identifier('query'),
1365
+ types.memberExpression(
1366
+ types.identifier(combinedStateVar),
1367
+ types.identifier('debouncedQuery')
1368
+ )
1369
+ ),
1370
+ ]
1371
+
1372
+ if (info.queryColumns && info.queryColumns.length > 0) {
1373
+ paramsProperties.push(
1374
+ types.objectProperty(
1375
+ types.identifier('queryColumns'),
1376
+ types.callExpression(
1377
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1378
+ [types.arrayExpression(info.queryColumns.map((col) => types.stringLiteral(col)))]
1379
+ )
1380
+ )
1381
+ )
1382
+ }
1383
+
1384
+ // Single dependency: the combined state object
1385
+ dependencies = [types.identifier(combinedStateVar)]
1386
+ } else {
1387
+ // Pagination only (no search)
1388
+ paramsProperties = [
1389
+ types.objectProperty(types.identifier('page'), types.identifier(info.pageStateVar)),
1390
+ types.objectProperty(types.identifier('perPage'), types.numericLiteral(info.perPage)),
1391
+ ]
1392
+
1393
+ dependencies = [types.identifier(info.pageStateVar)]
1394
+ }
1395
+
1396
+ // Wrap params in useMemo to prevent unnecessary refetches
1397
+ const memoizedParamsValue = types.callExpression(types.identifier('useMemo'), [
1398
+ types.arrowFunctionExpression([], types.objectExpression(paramsProperties)),
1399
+ types.arrayExpression(dependencies),
1400
+ ])
1401
+
1402
+ const paramsAttr = types.jsxAttribute(
1403
+ types.jsxIdentifier('params'),
1404
+ types.jsxExpressionContainer(memoizedParamsValue)
1405
+ )
1406
+
1407
+ const existingParamsAttr = dataProviderJSX.openingElement.attributes.find(
1408
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'params'
1409
+ )
1410
+
1411
+ if (existingParamsAttr) {
1412
+ const index = dataProviderJSX.openingElement.attributes.indexOf(existingParamsAttr)
1413
+ dataProviderJSX.openingElement.attributes[index] = paramsAttr
1414
+ } else {
1415
+ dataProviderJSX.openingElement.attributes.push(paramsAttr)
1416
+ }
1417
+
1418
+ const existingInitialDataAttr = dataProviderJSX.openingElement.attributes.find(
1419
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'initialData'
1420
+ )
1421
+
1422
+ if (existingInitialDataAttr && existingInitialDataAttr.value) {
1423
+ // Update initialData to use the paginated prop name
1424
+ const paginatedPropName = `${info.dataSourceIdentifier}_pg_${paginationIndex}`
1425
+
1426
+ // If search is enabled, use combined state; otherwise use page state
1427
+ let condition: any
1428
+
1429
+ if (info.searchEnabled && combinedStateVar) {
1430
+ condition = types.logicalExpression(
1431
+ '&&',
1432
+ types.binaryExpression(
1433
+ '===',
1434
+ types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
1435
+ types.numericLiteral(1)
1436
+ ),
1437
+ types.unaryExpression(
1438
+ '!',
1439
+ types.memberExpression(
1440
+ types.identifier(combinedStateVar),
1441
+ types.identifier('debouncedQuery')
1442
+ ),
1443
+ true
1444
+ )
1445
+ )
1446
+ } else {
1447
+ condition = types.binaryExpression(
1448
+ '===',
1449
+ types.identifier(info.pageStateVar),
1450
+ types.numericLiteral(1)
1451
+ )
1452
+ }
1453
+
1454
+ existingInitialDataAttr.value.expression = types.conditionalExpression(
1455
+ condition,
1456
+ types.optionalMemberExpression(
1457
+ types.identifier('props'),
1458
+ types.identifier(paginatedPropName),
1459
+ false,
1460
+ true
1461
+ ),
1462
+ types.identifier('undefined')
1463
+ )
1464
+ }
1465
+
1466
+ const existingKeyAttr = dataProviderJSX.openingElement.attributes.find(
1467
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'key'
1468
+ )
1469
+
1470
+ // Include page and search in key to trigger refetch when they change
1471
+ let keyExpression: any
1472
+ if (info.searchEnabled && combinedStateVar) {
1473
+ keyExpression = types.templateLiteral(
1474
+ [
1475
+ types.templateElement({
1476
+ raw: `${info.dataSourceIdentifier}-page-`,
1477
+ cooked: `${info.dataSourceIdentifier}-page-`,
1478
+ }),
1479
+ types.templateElement({ raw: '-search-', cooked: '-search-' }),
1480
+ types.templateElement({ raw: '', cooked: '' }),
1481
+ ],
1482
+ [
1483
+ types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
1484
+ types.memberExpression(
1485
+ types.identifier(combinedStateVar),
1486
+ types.identifier('debouncedQuery')
1487
+ ),
1488
+ ]
1489
+ )
1490
+ } else {
1491
+ keyExpression = types.templateLiteral(
1492
+ [
1493
+ types.templateElement({
1494
+ raw: `${info.dataSourceIdentifier}-`,
1495
+ cooked: `${info.dataSourceIdentifier}-`,
1496
+ }),
1497
+ types.templateElement({ raw: '', cooked: '' }),
1498
+ ],
1499
+ [types.identifier(info.pageStateVar)]
1500
+ )
1501
+ }
1502
+
1503
+ const keyAttr = types.jsxAttribute(
1504
+ types.jsxIdentifier('key'),
1505
+ types.jsxExpressionContainer(keyExpression)
1506
+ )
1507
+
1508
+ if (existingKeyAttr) {
1509
+ const index = dataProviderJSX.openingElement.attributes.indexOf(existingKeyAttr)
1510
+ dataProviderJSX.openingElement.attributes[index] = keyAttr
1511
+ } else {
1512
+ dataProviderJSX.openingElement.attributes.push(keyAttr)
1513
+ }
1514
+
1515
+ // For pagination, always create a fresh fetchData that calls the API route
1516
+ // Get the resource definition to build the API URL
1517
+ const resourceDefAttr = dataProviderJSX.openingElement.attributes.find(
1518
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
1519
+ )
1520
+
1521
+ if (resourceDefAttr && resourceDefAttr.value?.type === 'JSXExpressionContainer') {
1522
+ const resourceDef = resourceDefAttr.value.expression
1523
+ if (resourceDef.type === 'ObjectExpression') {
1524
+ const dataSourceIdProp = (resourceDef.properties as any[]).find(
1525
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceId'
1526
+ )
1527
+ const tableNameProp = (resourceDef.properties as any[]).find(
1528
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'tableName'
1529
+ )
1530
+ const dataSourceTypeProp = (resourceDef.properties as any[]).find(
1531
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceType'
1532
+ )
1533
+
1534
+ if (dataSourceIdProp && tableNameProp && dataSourceTypeProp) {
1535
+ const dataSourceId = dataSourceIdProp.value.value
1536
+ const tableName = tableNameProp.value.value
1537
+ const dataSourceType = dataSourceTypeProp.value.value
1538
+ const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
1539
+
1540
+ // Create fetchData attribute with proper fetch chain wrapped in useCallback
1541
+ const fetchDataValue = types.callExpression(types.identifier('useCallback'), [
1542
+ types.arrowFunctionExpression(
1543
+ [types.identifier('params')],
1544
+ types.callExpression(
1545
+ types.memberExpression(
1546
+ types.callExpression(
1547
+ types.memberExpression(
1548
+ types.callExpression(types.identifier('fetch'), [
1549
+ types.templateLiteral(
1550
+ [
1551
+ types.templateElement({
1552
+ raw: `/api/${fileName}?`,
1553
+ cooked: `/api/${fileName}?`,
1554
+ }),
1555
+ types.templateElement({ raw: '', cooked: '' }),
1556
+ ],
1557
+ [
1558
+ types.newExpression(types.identifier('URLSearchParams'), [
1559
+ types.identifier('params'),
1560
+ ]),
1561
+ ]
1562
+ ),
1563
+ types.objectExpression([
1564
+ types.objectProperty(
1565
+ types.identifier('headers'),
1566
+ types.objectExpression([
1567
+ types.objectProperty(
1568
+ types.stringLiteral('Content-Type'),
1569
+ types.stringLiteral('application/json')
1570
+ ),
1571
+ ])
1572
+ ),
1573
+ ]),
1574
+ ]),
1575
+ types.identifier('then')
1576
+ ),
1577
+ [
1578
+ types.arrowFunctionExpression(
1579
+ [types.identifier('res')],
1580
+ types.callExpression(
1581
+ types.memberExpression(types.identifier('res'), types.identifier('json')),
1582
+ []
1583
+ )
1584
+ ),
1585
+ ]
1586
+ ),
1587
+ types.identifier('then')
1588
+ ),
1589
+ [
1590
+ types.arrowFunctionExpression(
1591
+ [types.identifier('response')],
1592
+ types.optionalMemberExpression(
1593
+ types.identifier('response'),
1594
+ types.identifier('data'),
1595
+ false,
1596
+ true
1597
+ )
1598
+ ),
1599
+ ]
1600
+ )
1601
+ ),
1602
+ types.arrayExpression([]),
1603
+ ])
1604
+
1605
+ const newFetchDataAttr = types.jsxAttribute(
1606
+ types.jsxIdentifier('fetchData'),
1607
+ types.jsxExpressionContainer(fetchDataValue)
1608
+ )
1609
+
1610
+ // Remove existing fetchData attribute if present
1611
+ const existingFetchDataIndex = dataProviderJSX.openingElement.attributes.findIndex(
1612
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'fetchData'
1613
+ )
1614
+
1615
+ if (existingFetchDataIndex !== -1) {
1616
+ dataProviderJSX.openingElement.attributes[existingFetchDataIndex] = newFetchDataAttr
1617
+ } else {
1618
+ dataProviderJSX.openingElement.attributes.push(newFetchDataAttr)
1619
+ }
1620
+ }
1621
+ }
1622
+ }
1623
+ }
1624
+
1625
+ function findChildWithClass(node: any, classSubstring: string): string | null {
1626
+ if (!node) {
1627
+ return null
1628
+ }
1629
+
1630
+ if (node.type === 'JSXElement') {
1631
+ const className = getClassName(node.openingElement?.attributes || [])
1632
+ if (className && className.includes(classSubstring)) {
1633
+ return className
1634
+ }
1635
+ }
1636
+
1637
+ if (node.children) {
1638
+ for (const child of node.children) {
1639
+ const found = findChildWithClass(child, classSubstring)
1640
+ if (found) {
1641
+ return found
1642
+ }
1643
+ }
1644
+ }
1645
+
1646
+ if (typeof node === 'object') {
1647
+ for (const value of Object.values(node)) {
1648
+ if (Array.isArray(value)) {
1649
+ for (const item of value) {
1650
+ const found = findChildWithClass(item, classSubstring)
1651
+ if (found) {
1652
+ return found
1653
+ }
1654
+ }
1655
+ } else if (typeof value === 'object') {
1656
+ const found = findChildWithClass(value, classSubstring)
1657
+ if (found) {
1658
+ return found
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+
1664
+ return null
1665
+ }
1666
+
1667
+ function modifyPaginationButtons(
1668
+ blockStatement: types.BlockStatement,
1669
+ detectedPaginations: DetectedPagination[],
1670
+ paginationInfos: ArrayMapperPaginationInfo[]
1671
+ ): void {
1672
+ const modifyNode = (node: any): void => {
1673
+ if (!node) {
1674
+ return
1675
+ }
1676
+
1677
+ if (node.type === 'JSXElement') {
1678
+ const openingElement = node.openingElement
1679
+ if (openingElement && openingElement.name && openingElement.name.type === 'JSXIdentifier') {
1680
+ const className = getClassName(openingElement.attributes)
1681
+
1682
+ if (className) {
1683
+ detectedPaginations.forEach((detected, index) => {
1684
+ const info = paginationInfos[index]
1685
+ if (!info) {
1686
+ return
1687
+ }
1688
+
1689
+ if (className === detected.prevButtonClass) {
1690
+ convertToButton(node, info, 'prev')
1691
+ } else if (className === detected.nextButtonClass) {
1692
+ convertToButton(node, info, 'next')
1693
+ }
1694
+ })
1695
+ }
1696
+ }
1697
+ }
1698
+
1699
+ if (typeof node === 'object') {
1700
+ Object.values(node).forEach((value) => {
1701
+ if (Array.isArray(value)) {
1702
+ value.forEach((item) => modifyNode(item))
1703
+ } else if (typeof value === 'object') {
1704
+ modifyNode(value)
1705
+ }
1706
+ })
1707
+ }
1708
+ }
1709
+
1710
+ modifyNode(blockStatement)
1711
+ }
1712
+
1713
+ function modifySearchInputs(
1714
+ blockStatement: types.BlockStatement,
1715
+ detectedPaginations: DetectedPagination[],
1716
+ paginationInfos: ArrayMapperPaginationInfo[]
1717
+ ): void {
1718
+ const modifyNode = (node: any): void => {
1719
+ if (!node) {
1720
+ return
1721
+ }
1722
+
1723
+ if (node.type === 'JSXElement') {
1724
+ const openingElement = node.openingElement
1725
+ if (openingElement && openingElement.name && openingElement.name.type === 'JSXIdentifier') {
1726
+ const className = getClassName(openingElement.attributes)
1727
+
1728
+ if (className) {
1729
+ detectedPaginations.forEach((detected, index) => {
1730
+ const info = paginationInfos[index]
1731
+ if (!info || !info.searchEnabled) {
1732
+ return
1733
+ }
1734
+
1735
+ if (className === detected.searchInputClass) {
1736
+ addSearchInputHandlers(node, info)
1737
+ } else if (className && className.includes('search-input')) {
1738
+ // Fallback: match any input with 'search-input' in class
1739
+ addSearchInputHandlers(node, info)
1740
+ }
1741
+ })
1742
+ }
1743
+ }
1744
+ }
1745
+
1746
+ if (typeof node === 'object') {
1747
+ Object.values(node).forEach((value) => {
1748
+ if (Array.isArray(value)) {
1749
+ value.forEach((item) => modifyNode(item))
1750
+ } else if (typeof value === 'object') {
1751
+ modifyNode(value)
1752
+ }
1753
+ })
1754
+ }
1755
+ }
1756
+
1757
+ modifyNode(blockStatement)
1758
+ }
1759
+
1760
+ function addSearchInputHandlers(jsxElement: any, info: ArrayMapperPaginationInfo): void {
1761
+ if (!info.searchQueryVar || !info.setSearchQueryVar) {
1762
+ return
1763
+ }
1764
+
1765
+ const openingElement = jsxElement.openingElement
1766
+
1767
+ removeAttribute(openingElement.attributes, 'onChange')
1768
+ removeAttribute(openingElement.attributes, 'value')
1769
+
1770
+ const onChangeHandler = types.arrowFunctionExpression(
1771
+ [types.identifier('e')],
1772
+ types.callExpression(types.identifier(info.setSearchQueryVar!), [
1773
+ types.memberExpression(
1774
+ types.memberExpression(types.identifier('e'), types.identifier('target')),
1775
+ types.identifier('value')
1776
+ ),
1777
+ ])
1778
+ )
1779
+
1780
+ openingElement.attributes.push(
1781
+ types.jsxAttribute(
1782
+ types.jsxIdentifier('onChange'),
1783
+ types.jsxExpressionContainer(onChangeHandler)
1784
+ )
1785
+ )
1786
+
1787
+ openingElement.attributes.push(
1788
+ types.jsxAttribute(
1789
+ types.jsxIdentifier('value'),
1790
+ types.jsxExpressionContainer(types.identifier(info.searchQueryVar!))
1791
+ )
1792
+ )
1793
+ }
1794
+
1795
+ function convertToButton(
1796
+ jsxElement: any,
1797
+ info: ArrayMapperPaginationInfo,
1798
+ buttonType: 'prev' | 'next'
1799
+ ): void {
1800
+ const openingElement = jsxElement.openingElement
1801
+
1802
+ if (openingElement.name.type === 'JSXIdentifier') {
1803
+ openingElement.name.name = 'button'
1804
+ }
1805
+ if (jsxElement.closingElement && jsxElement.closingElement.name.type === 'JSXIdentifier') {
1806
+ jsxElement.closingElement.name.name = 'button'
1807
+ }
1808
+
1809
+ removeAttribute(openingElement.attributes, 'onClick')
1810
+ removeAttribute(openingElement.attributes, 'disabled')
1811
+ removeAttribute(openingElement.attributes, 'type')
1812
+
1813
+ const combinedStateVar = (info as any).combinedStateVar
1814
+ const setCombinedStateVar = (info as any).setCombinedStateVar
1815
+
1816
+ let onClickHandler: any
1817
+ if (info.searchEnabled && combinedStateVar && setCombinedStateVar) {
1818
+ // Use combined state: update only the page property
1819
+ onClickHandler =
1820
+ buttonType === 'prev'
1821
+ ? types.arrowFunctionExpression(
1822
+ [],
1823
+ types.callExpression(types.identifier(setCombinedStateVar), [
1824
+ types.arrowFunctionExpression(
1825
+ [types.identifier('state')],
1826
+ types.objectExpression([
1827
+ types.spreadElement(types.identifier('state')),
1828
+ types.objectProperty(
1829
+ types.identifier('page'),
1830
+ types.callExpression(
1831
+ types.memberExpression(types.identifier('Math'), types.identifier('max')),
1832
+ [
1833
+ types.numericLiteral(1),
1834
+ types.binaryExpression(
1835
+ '-',
1836
+ types.memberExpression(
1837
+ types.identifier('state'),
1838
+ types.identifier('page')
1839
+ ),
1840
+ types.numericLiteral(1)
1841
+ ),
1842
+ ]
1843
+ )
1844
+ ),
1845
+ ])
1846
+ ),
1847
+ ])
1848
+ )
1849
+ : types.arrowFunctionExpression(
1850
+ [],
1851
+ types.callExpression(types.identifier(setCombinedStateVar), [
1852
+ types.arrowFunctionExpression(
1853
+ [types.identifier('state')],
1854
+ types.objectExpression([
1855
+ types.spreadElement(types.identifier('state')),
1856
+ types.objectProperty(
1857
+ types.identifier('page'),
1858
+ types.binaryExpression(
1859
+ '+',
1860
+ types.memberExpression(types.identifier('state'), types.identifier('page')),
1861
+ types.numericLiteral(1)
1862
+ )
1863
+ ),
1864
+ ])
1865
+ ),
1866
+ ])
1867
+ )
1868
+ } else {
1869
+ // Regular pagination (no search)
1870
+ onClickHandler =
1871
+ buttonType === 'prev'
1872
+ ? types.arrowFunctionExpression(
1873
+ [],
1874
+ types.callExpression(types.identifier(info.setPageStateVar), [
1875
+ types.arrowFunctionExpression(
1876
+ [types.identifier('p')],
1877
+ types.callExpression(
1878
+ types.memberExpression(types.identifier('Math'), types.identifier('max')),
1879
+ [
1880
+ types.numericLiteral(1),
1881
+ types.binaryExpression('-', types.identifier('p'), types.numericLiteral(1)),
1882
+ ]
1883
+ )
1884
+ ),
1885
+ ])
1886
+ )
1887
+ : types.arrowFunctionExpression(
1888
+ [],
1889
+ types.callExpression(types.identifier(info.setPageStateVar), [
1890
+ types.arrowFunctionExpression(
1891
+ [types.identifier('p')],
1892
+ types.binaryExpression('+', types.identifier('p'), types.numericLiteral(1))
1893
+ ),
1894
+ ])
1895
+ )
1896
+ }
1897
+
1898
+ openingElement.attributes.push(
1899
+ types.jsxAttribute(types.jsxIdentifier('onClick'), types.jsxExpressionContainer(onClickHandler))
1900
+ )
1901
+ openingElement.attributes.push(
1902
+ types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button'))
1903
+ )
1904
+
1905
+ // Add disabled attribute with simple page number checks
1906
+ const maxPagesStateVar = (info as any).maxPagesStateVar
1907
+ let disabledExpr: any
1908
+
1909
+ if (info.searchEnabled && combinedStateVar) {
1910
+ disabledExpr =
1911
+ buttonType === 'prev'
1912
+ ? types.binaryExpression(
1913
+ '<=',
1914
+ types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
1915
+ types.numericLiteral(1)
1916
+ )
1917
+ : types.binaryExpression(
1918
+ '>=',
1919
+ types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
1920
+ types.identifier(maxPagesStateVar)
1921
+ )
1922
+ } else {
1923
+ disabledExpr =
1924
+ buttonType === 'prev'
1925
+ ? types.binaryExpression('<=', types.identifier(info.pageStateVar), types.numericLiteral(1))
1926
+ : types.binaryExpression(
1927
+ '>=',
1928
+ types.identifier(info.pageStateVar),
1929
+ types.identifier(maxPagesStateVar)
1930
+ )
1931
+ }
1932
+
1933
+ openingElement.attributes.push(
1934
+ types.jsxAttribute(types.jsxIdentifier('disabled'), types.jsxExpressionContainer(disabledExpr))
1935
+ )
1936
+ }
1937
+
1938
+ function getClassName(attributes: any[]): string | null {
1939
+ const classNameAttr = attributes.find(
1940
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'className'
1941
+ )
1942
+ if (classNameAttr && classNameAttr.value && classNameAttr.value.type === 'StringLiteral') {
1943
+ return classNameAttr.value.value
1944
+ }
1945
+ return null
1946
+ }
1947
+
1948
+ function removeAttribute(attrs: any[], attributeName: string): void {
1949
+ const index = attrs.findIndex(
1950
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === attributeName
1951
+ )
1952
+ if (index !== -1) {
1953
+ attrs.splice(index, 1)
1954
+ }
1955
+ }
1956
+
1957
+ function modifyGetStaticPropsForPagination(
1958
+ chunks: any[],
1959
+ paginationInfos: ArrayMapperPaginationInfo[]
1960
+ ): void {
1961
+ const getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
1962
+ if (!getStaticPropsChunk || getStaticPropsChunk.type !== 'ast') {
1963
+ return
1964
+ }
1965
+
1966
+ const exportDeclaration = getStaticPropsChunk.content as types.ExportNamedDeclaration
1967
+ if (!exportDeclaration || exportDeclaration.type !== 'ExportNamedDeclaration') {
1968
+ return
1969
+ }
1970
+
1971
+ const functionDeclaration = exportDeclaration.declaration as types.FunctionDeclaration
1972
+ if (!functionDeclaration || functionDeclaration.type !== 'FunctionDeclaration') {
1973
+ return
1974
+ }
1975
+
1976
+ // Find Promise.all and add NEW fetchData calls for each paginated DataProvider
1977
+ const functionBody = functionDeclaration.body.body
1978
+ const tryBlock = functionBody.find((stmt: any) => stmt.type === 'TryStatement') as
1979
+ | types.TryStatement
1980
+ | undefined
1981
+
1982
+ if (!tryBlock) {
1983
+ return
1984
+ }
1985
+
1986
+ const tryBody = tryBlock.block.body
1987
+
1988
+ // Find the Promise.all statement
1989
+ const promiseAllStmt = tryBody.find(
1990
+ (stmt: any) =>
1991
+ stmt.type === 'VariableDeclaration' &&
1992
+ stmt.declarations?.[0]?.init?.type === 'AwaitExpression' &&
1993
+ stmt.declarations?.[0]?.init?.argument?.type === 'CallExpression' &&
1994
+ stmt.declarations?.[0]?.init?.argument?.callee?.type === 'MemberExpression' &&
1995
+ stmt.declarations?.[0]?.init?.argument?.callee?.property?.name === 'all'
1996
+ ) as types.VariableDeclaration | undefined
1997
+
1998
+ if (!promiseAllStmt) {
1999
+ return
2000
+ }
2001
+
2002
+ const awaitExpr = promiseAllStmt.declarations[0].init as types.AwaitExpression
2003
+ const promiseAllCall = awaitExpr.argument as types.CallExpression
2004
+ const promiseArray = promiseAllCall.arguments[0] as types.ArrayExpression
2005
+ const destructuringPattern = promiseAllStmt.declarations[0].id as types.ArrayPattern
2006
+
2007
+ // Map import names to data source identifiers from existing fetchData calls
2008
+ // Also track which indices to remove (non-paginated calls that will be replaced)
2009
+ const importToDataSource = new Map<string, string>()
2010
+ const indicesToRemove: number[] = []
2011
+
2012
+ promiseArray.elements.forEach((element: any, index: number) => {
2013
+ if (element && element.type === 'CallExpression') {
2014
+ let fetchCallExpr = element
2015
+
2016
+ // If wrapped in .catch(), unwrap it
2017
+ if (
2018
+ element.callee?.type === 'MemberExpression' &&
2019
+ element.callee?.property?.name === 'catch' &&
2020
+ element.callee?.object?.type === 'CallExpression'
2021
+ ) {
2022
+ fetchCallExpr = element.callee.object
2023
+ }
2024
+
2025
+ // Now find the .fetchData() call
2026
+ if (
2027
+ fetchCallExpr.callee?.type === 'MemberExpression' &&
2028
+ fetchCallExpr.callee?.property?.name === 'fetchData' &&
2029
+ fetchCallExpr.callee?.object?.type === 'Identifier'
2030
+ ) {
2031
+ const importName = fetchCallExpr.callee.object.name
2032
+ const dataSourceVar = (destructuringPattern.elements[index] as types.Identifier).name
2033
+
2034
+ // Check if this fetchData call has page/perPage params
2035
+ const params = fetchCallExpr.arguments[0]
2036
+ const hasPageParam =
2037
+ params &&
2038
+ params.type === 'ObjectExpression' &&
2039
+ params.properties.some(
2040
+ (prop: any) =>
2041
+ prop.type === 'ObjectProperty' &&
2042
+ (prop.key.name === 'page' || prop.key.name === 'perPage')
2043
+ )
2044
+
2045
+ // If this is a data source that will be paginated but this call has NO pagination params,
2046
+ // mark it for removal
2047
+ if (
2048
+ !hasPageParam &&
2049
+ paginationInfos.some((info) => info.dataSourceIdentifier === dataSourceVar)
2050
+ ) {
2051
+ indicesToRemove.push(index)
2052
+ }
2053
+
2054
+ importToDataSource.set(importName, dataSourceVar)
2055
+ }
2056
+ }
2057
+ })
2058
+
2059
+ // Remove non-paginated fetchData calls in reverse order to preserve indices
2060
+ indicesToRemove.reverse().forEach((index) => {
2061
+ // Get the prop name BEFORE removing it
2062
+ const propToRemove = (destructuringPattern.elements[index] as types.Identifier)?.name
2063
+
2064
+ promiseArray.elements.splice(index, 1)
2065
+ destructuringPattern.elements.splice(index, 1)
2066
+
2067
+ // Also remove from props in return statement
2068
+ if (propToRemove) {
2069
+ const foundReturnStmt = tryBody.find((stmt: any) => stmt.type === 'ReturnStatement') as
2070
+ | types.ReturnStatement
2071
+ | undefined
2072
+
2073
+ if (foundReturnStmt && foundReturnStmt.argument?.type === 'ObjectExpression') {
2074
+ const propsProperty = (foundReturnStmt.argument as types.ObjectExpression).properties.find(
2075
+ (prop: any) =>
2076
+ prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === 'props'
2077
+ ) as types.ObjectProperty | undefined
2078
+
2079
+ if (propsProperty && propsProperty.value.type === 'ObjectExpression') {
2080
+ const propsObject = propsProperty.value as types.ObjectExpression
2081
+
2082
+ const propIndex = propsObject.properties.findIndex(
2083
+ (prop: any) =>
2084
+ prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === propToRemove
2085
+ )
2086
+ if (propIndex !== -1) {
2087
+ propsObject.properties.splice(propIndex, 1)
2088
+ }
2089
+ }
2090
+ }
2091
+ }
2092
+ })
2093
+
2094
+ // Add NEW fetchData calls for each paginated DataProvider
2095
+ paginationInfos.forEach((info, index) => {
2096
+ // Try exact match first, then case-insensitive match
2097
+ let importName = Array.from(importToDataSource.entries()).find(
2098
+ ([_, dataSourceVar]) => dataSourceVar === info.dataSourceIdentifier
2099
+ )?.[0]
2100
+
2101
+ if (!importName) {
2102
+ // Try case-insensitive match
2103
+ const normalizedIdentifier = info.dataSourceIdentifier.toLowerCase().replace(/[_-]/g, '')
2104
+ importName = Array.from(importToDataSource.entries()).find(
2105
+ ([_, dataSourceVar]) =>
2106
+ dataSourceVar.toLowerCase().replace(/[_-]/g, '') === normalizedIdentifier
2107
+ )?.[0]
2108
+ }
2109
+
2110
+ if (importName) {
2111
+ const paginatedVarName = `${info.dataSourceIdentifier}_pg_${index}`
2112
+
2113
+ const fetchParams = [
2114
+ types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
2115
+ types.objectProperty(types.identifier('perPage'), types.numericLiteral(info.perPage)),
2116
+ ]
2117
+
2118
+ // Add queryColumns if they exist
2119
+ if (info.queryColumns && info.queryColumns.length > 0) {
2120
+ fetchParams.push(
2121
+ types.objectProperty(
2122
+ types.identifier('queryColumns'),
2123
+ types.arrayExpression(info.queryColumns.map((col) => types.stringLiteral(col)))
2124
+ )
2125
+ )
2126
+ }
2127
+
2128
+ // Create new fetchData call with pagination params
2129
+ const newFetchDataCall = types.callExpression(
2130
+ types.memberExpression(
2131
+ types.callExpression(
2132
+ types.memberExpression(types.identifier(importName), types.identifier('fetchData')),
2133
+ [types.objectExpression(fetchParams)]
2134
+ ),
2135
+ types.identifier('catch')
2136
+ ),
2137
+ [
2138
+ types.arrowFunctionExpression(
2139
+ [types.identifier('error')],
2140
+ types.blockStatement([
2141
+ types.expressionStatement(
2142
+ types.callExpression(
2143
+ types.memberExpression(types.identifier('console'), types.identifier('error')),
2144
+ [
2145
+ types.stringLiteral(`Error fetching ${paginatedVarName}:`),
2146
+ types.identifier('error'),
2147
+ ]
2148
+ )
2149
+ ),
2150
+ types.returnStatement(types.arrayExpression([])),
2151
+ ])
2152
+ ),
2153
+ ]
2154
+ )
2155
+
2156
+ promiseArray.elements.push(newFetchDataCall)
2157
+ destructuringPattern.elements.push(types.identifier(paginatedVarName))
2158
+ }
2159
+ })
2160
+
2161
+ // Add fetchCount calls for paginated data sources (deduplicated by data source)
2162
+
2163
+ // Deduplicate by data source identifier
2164
+ const uniqueDataSources = new Set(paginationInfos.map((info) => info.dataSourceIdentifier))
2165
+ const addedCountFetches = new Set<string>()
2166
+
2167
+ uniqueDataSources.forEach((dataSourceId) => {
2168
+ let importName = Array.from(importToDataSource.entries()).find(
2169
+ ([_, dataSourceVar]) => dataSourceVar === dataSourceId
2170
+ )?.[0]
2171
+
2172
+ if (!importName) {
2173
+ // Try case-insensitive match
2174
+ const normalizedIdentifier = dataSourceId.toLowerCase().replace(/[_-]/g, '')
2175
+ importName = Array.from(importToDataSource.entries()).find(
2176
+ ([_, dataSourceVar]) =>
2177
+ dataSourceVar.toLowerCase().replace(/[_-]/g, '') === normalizedIdentifier
2178
+ )?.[0]
2179
+ }
2180
+
2181
+ if (importName && !addedCountFetches.has(dataSourceId)) {
2182
+ const fetchCountCall = types.callExpression(
2183
+ types.memberExpression(types.identifier(importName), types.identifier('fetchCount')),
2184
+ []
2185
+ )
2186
+ promiseArray.elements.push(fetchCountCall)
2187
+ destructuringPattern.elements.push(types.identifier(`${dataSourceId}_count`))
2188
+ addedCountFetches.add(dataSourceId)
2189
+ }
2190
+ })
2191
+
2192
+ // Calculate and add maxPages before return
2193
+ const returnStmt = tryBody.find((stmt: any) => stmt.type === 'ReturnStatement') as
2194
+ | types.ReturnStatement
2195
+ | undefined
2196
+
2197
+ if (returnStmt && returnStmt.argument?.type === 'ObjectExpression') {
2198
+ const propsProperty = (returnStmt.argument as types.ObjectExpression).properties.find(
2199
+ (prop: any) =>
2200
+ prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === 'props'
2201
+ ) as types.ObjectProperty | undefined
2202
+
2203
+ if (propsProperty && propsProperty.value.type === 'ObjectExpression') {
2204
+ const propsObject = propsProperty.value as types.ObjectExpression
2205
+ const returnIndex = tryBody.indexOf(returnStmt)
2206
+
2207
+ paginationInfos.forEach((info, index) => {
2208
+ const paginatedVarName = `${info.dataSourceIdentifier}_pg_${index}`
2209
+ const countVarName = `${info.dataSourceIdentifier}_count`
2210
+ const maxPagesVarName = `${info.dataSourceIdentifier}_pg_${index}_maxPages`
2211
+
2212
+ const maxPagesCalc = types.variableDeclaration('const', [
2213
+ types.variableDeclarator(
2214
+ types.identifier(maxPagesVarName),
2215
+ types.callExpression(
2216
+ types.memberExpression(types.identifier('Math'), types.identifier('ceil')),
2217
+ [
2218
+ types.binaryExpression(
2219
+ '/',
2220
+ types.logicalExpression(
2221
+ '||',
2222
+ types.identifier(countVarName),
2223
+ types.numericLiteral(0)
2224
+ ),
2225
+ types.numericLiteral(info.perPage)
2226
+ ),
2227
+ ]
2228
+ )
2229
+ ),
2230
+ ])
2231
+
2232
+ tryBody.splice(returnIndex, 0, maxPagesCalc)
2233
+
2234
+ // Add both the paginated data and maxPages to props
2235
+ propsObject.properties.push(
2236
+ types.objectProperty(
2237
+ types.identifier(paginatedVarName),
2238
+ types.identifier(paginatedVarName)
2239
+ )
2240
+ )
2241
+ propsObject.properties.push(
2242
+ types.objectProperty(types.identifier(maxPagesVarName), types.identifier(maxPagesVarName))
2243
+ )
2244
+ })
2245
+ }
2246
+ }
2247
+ }
2248
+
2249
+ function createAPIRoutesForPaginatedDataSources(
2250
+ uidlNode: any,
2251
+ dataSources: any,
2252
+ componentChunk: any,
2253
+ extractedResources: any,
2254
+ paginationInfos: ArrayMapperPaginationInfo[],
2255
+ isComponent: boolean
2256
+ ): void {
2257
+ const paginatedDataSourceIds = new Set(paginationInfos.map((info) => info.dataSourceIdentifier))
2258
+
2259
+ const traverseForDataSources = (node: any): void => {
2260
+ if (!node) {
2261
+ return
2262
+ }
2263
+
2264
+ if (node.type === 'data-source-list' || node.type === 'data-source-item') {
2265
+ const renderProp = node.content.renderPropIdentifier
2266
+
2267
+ if (renderProp && paginatedDataSourceIds.has(renderProp)) {
2268
+ extractDataSourceIntoNextAPIFolder(node, dataSources, componentChunk, extractedResources)
2269
+
2270
+ // For components, also create count API route
2271
+ if (isComponent) {
2272
+ const resourceDef = node.content.resourceDefinition
2273
+ if (resourceDef) {
2274
+ const dataSourceId = resourceDef.dataSourceId
2275
+ const tableName = resourceDef.tableName
2276
+ const dataSourceType = resourceDef.dataSourceType
2277
+ const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
2278
+ const countFileName = `${fileName}-count`
2279
+
2280
+ // Create count API route that exports getCount handler
2281
+ extractedResources[`api/${countFileName}`] = {
2282
+ fileName: countFileName,
2283
+ fileType: FileType.JS,
2284
+ path: ['pages', 'api'],
2285
+ content: `import dataSource from '../../utils/data-sources/${fileName}'
2286
+
2287
+ export default dataSource.getCount
2288
+ `,
2289
+ }
2290
+ }
2291
+ }
2292
+ }
2293
+ }
2294
+
2295
+ if (node.content?.children) {
2296
+ node.content.children.forEach((child: any) => traverseForDataSources(child))
2297
+ }
2298
+ }
2299
+
2300
+ traverseForDataSources(uidlNode)
2301
+ }
2302
+
2303
+ function createAPIRoutesForSearchOnlyDataSources(
2304
+ uidlNode: any,
2305
+ dataSources: any,
2306
+ componentChunk: any,
2307
+ extractedResources: any,
2308
+ searchOnlyDataSources: DetectedPagination[]
2309
+ ): void {
2310
+ const searchOnlyDataSourceIds = new Set(
2311
+ searchOnlyDataSources.map((info) => info.dataSourceIdentifier)
2312
+ )
2313
+
2314
+ const traverseForDataSources = (node: any): void => {
2315
+ if (!node) {
2316
+ return
2317
+ }
2318
+
2319
+ if (node.type === 'data-source-list' || node.type === 'data-source-item') {
2320
+ const renderProp = node.content.renderPropIdentifier
2321
+
2322
+ if (renderProp && searchOnlyDataSourceIds.has(renderProp)) {
2323
+ extractDataSourceIntoNextAPIFolder(node, dataSources, componentChunk, extractedResources)
2324
+ }
2325
+ }
2326
+
2327
+ if (node.content?.children) {
2328
+ node.content.children.forEach((child: any) => traverseForDataSources(child))
2329
+ }
2330
+ }
2331
+
2332
+ traverseForDataSources(uidlNode)
2333
+ }
2334
+
2335
+ export default createNextArrayMapperPaginationPlugin()