@teleporthq/teleport-plugin-next-data-source 0.42.0 → 0.42.3

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 (61) hide show
  1. package/COUNT_API_FIX.md +185 -0
  2. package/SEARCH_FIX_SUMMARY.md +96 -0
  3. package/dist/cjs/count-fetchers.d.ts.map +1 -1
  4. package/dist/cjs/count-fetchers.js +1 -1
  5. package/dist/cjs/count-fetchers.js.map +1 -1
  6. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  7. package/dist/cjs/fetchers/clickhouse.js +1 -1
  8. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  9. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  10. package/dist/cjs/fetchers/firestore.js +1 -1
  11. package/dist/cjs/fetchers/firestore.js.map +1 -1
  12. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  13. package/dist/cjs/fetchers/javascript.js +1 -1
  14. package/dist/cjs/fetchers/javascript.js.map +1 -1
  15. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  16. package/dist/cjs/fetchers/redshift.js +3 -1
  17. package/dist/cjs/fetchers/redshift.js.map +1 -1
  18. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  19. package/dist/cjs/fetchers/rest-api.js +2 -2
  20. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  21. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/turso.js +1 -1
  23. package/dist/cjs/fetchers/turso.js.map +1 -1
  24. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  25. package/dist/cjs/pagination-plugin.js +280 -165
  26. package/dist/cjs/pagination-plugin.js.map +1 -1
  27. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  28. package/dist/esm/count-fetchers.d.ts.map +1 -1
  29. package/dist/esm/count-fetchers.js +1 -1
  30. package/dist/esm/count-fetchers.js.map +1 -1
  31. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  32. package/dist/esm/fetchers/clickhouse.js +1 -1
  33. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  34. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  35. package/dist/esm/fetchers/firestore.js +1 -1
  36. package/dist/esm/fetchers/firestore.js.map +1 -1
  37. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  38. package/dist/esm/fetchers/javascript.js +1 -1
  39. package/dist/esm/fetchers/javascript.js.map +1 -1
  40. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  41. package/dist/esm/fetchers/redshift.js +3 -1
  42. package/dist/esm/fetchers/redshift.js.map +1 -1
  43. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  44. package/dist/esm/fetchers/rest-api.js +2 -2
  45. package/dist/esm/fetchers/rest-api.js.map +1 -1
  46. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  47. package/dist/esm/fetchers/turso.js +1 -1
  48. package/dist/esm/fetchers/turso.js.map +1 -1
  49. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  50. package/dist/esm/pagination-plugin.js +280 -165
  51. package/dist/esm/pagination-plugin.js.map +1 -1
  52. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +2 -2
  54. package/src/count-fetchers.ts +2 -1
  55. package/src/fetchers/clickhouse.ts +12 -6
  56. package/src/fetchers/firestore.ts +45 -13
  57. package/src/fetchers/javascript.ts +48 -13
  58. package/src/fetchers/redshift.ts +32 -9
  59. package/src/fetchers/rest-api.ts +68 -6
  60. package/src/fetchers/turso.ts +46 -16
  61. package/src/pagination-plugin.ts +440 -296
@@ -123,6 +123,8 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
123
123
  const searchConfigMap = opts.paginationConfig?.searchConfigMap || new Map<string, any>()
124
124
  const queryColumnsMap = opts.paginationConfig?.queryColumnsMap || new Map<string, string[]>()
125
125
 
126
+ const stateDeclarations: types.Statement[] = []
127
+
126
128
  detectedPaginations.forEach((detected, index) => {
127
129
  const paginationNodeId = `pg_${index}`
128
130
  // Use arrayMapperRenderProp if available, otherwise fall back to dataSourceIdentifier
@@ -141,55 +143,27 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
141
143
  )
142
144
  paginationInfos.push(info)
143
145
 
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', [
146
+ // Add refs to track first render for each useEffect (add first)
147
+ if (info.searchEnabled) {
148
+ const skipCountFetchOnMountRefVar = `skipCountFetchOnMount_pg_${index}`
149
+ const skipCountFetchRefAST = types.variableDeclaration('const', [
168
150
  types.variableDeclarator(
169
- types.arrayPattern([
170
- types.identifier(info.searchQueryVar),
171
- types.identifier(info.setSearchQueryVar),
172
- ]),
173
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
151
+ types.identifier(skipCountFetchOnMountRefVar),
152
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
174
153
  ),
175
154
  ])
176
- blockStatement.body.unshift(searchStateAST)
155
+ stateDeclarations.push(skipCountFetchRefAST)
156
+ ;(info as any).skipCountFetchOnMountRefVar = skipCountFetchOnMountRefVar
177
157
 
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', [
158
+ const skipDebounceOnMountRefVar = `skipDebounceOnMount_pg_${index}`
159
+ const skipDebounceRefAST = types.variableDeclaration('const', [
184
160
  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)])
161
+ types.identifier(skipDebounceOnMountRefVar),
162
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
190
163
  ),
191
164
  ])
192
- blockStatement.body.unshift(pageStateAST)
165
+ stateDeclarations.push(skipDebounceRefAST)
166
+ ;(info as any).skipDebounceOnMountRefVar = skipDebounceOnMountRefVar
193
167
  }
194
168
 
195
169
  // Add maxPages state
@@ -221,36 +195,69 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
221
195
  types.callExpression(types.identifier('useState'), [maxPagesInitValue])
222
196
  ),
223
197
  ])
224
- blockStatement.body.unshift(maxPagesStateAST)
198
+ stateDeclarations.push(maxPagesStateAST)
225
199
 
226
200
  // Store these for later use
227
201
  ;(info as any).maxPagesStateVar = maxPagesStateVar
228
202
  ;(info as any).setMaxPagesStateVar = setMaxPagesStateVar
229
203
 
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', [
204
+ // If both pagination and search are enabled, combine them into a single state object
205
+ if (info.searchEnabled && info.searchQueryVar && info.setSearchQueryVar) {
206
+ // Combined state: { page: 1, debouncedQuery: '' }
207
+ const combinedStateVar = `paginationState_pg_${index}`
208
+ const setCombinedStateVar = `setPaginationState_pg_${index}`
209
+
210
+ const combinedStateAST = types.variableDeclaration('const', [
234
211
  types.variableDeclarator(
235
- types.identifier(skipDebounceOnMountRefVar),
236
- types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
212
+ types.arrayPattern([
213
+ types.identifier(combinedStateVar),
214
+ types.identifier(setCombinedStateVar),
215
+ ]),
216
+ types.callExpression(types.identifier('useState'), [
217
+ types.objectExpression([
218
+ types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
219
+ types.objectProperty(types.identifier('debouncedQuery'), types.stringLiteral('')),
220
+ ]),
221
+ ])
237
222
  ),
238
223
  ])
239
- blockStatement.body.unshift(skipDebounceRefAST)
240
- ;(info as any).skipDebounceOnMountRefVar = skipDebounceOnMountRefVar
224
+ stateDeclarations.push(combinedStateAST)
241
225
 
242
- const skipCountFetchOnMountRefVar = `skipCountFetchOnMount_pg_${index}`
243
- const skipCountFetchRefAST = types.variableDeclaration('const', [
226
+ // Still need the immediate search query state for the input
227
+ const searchStateAST = types.variableDeclaration('const', [
244
228
  types.variableDeclarator(
245
- types.identifier(skipCountFetchOnMountRefVar),
246
- types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
229
+ types.arrayPattern([
230
+ types.identifier(info.searchQueryVar),
231
+ types.identifier(info.setSearchQueryVar),
232
+ ]),
233
+ types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
247
234
  ),
248
235
  ])
249
- blockStatement.body.unshift(skipCountFetchRefAST)
250
- ;(info as any).skipCountFetchOnMountRefVar = skipCountFetchOnMountRefVar
236
+ stateDeclarations.push(searchStateAST)
237
+
238
+ // Store the combined state var names
239
+ ;(info as any).combinedStateVar = combinedStateVar
240
+ ;(info as any).setCombinedStateVar = setCombinedStateVar
241
+ } else {
242
+ // If search is not enabled, just add regular page state
243
+ const pageStateAST = types.variableDeclaration('const', [
244
+ types.variableDeclarator(
245
+ types.arrayPattern([
246
+ types.identifier(info.pageStateVar),
247
+ types.identifier(info.setPageStateVar),
248
+ ]),
249
+ types.callExpression(types.identifier('useState'), [types.numericLiteral(1)])
250
+ ),
251
+ ])
252
+ stateDeclarations.push(pageStateAST)
251
253
  }
252
254
  })
253
255
 
256
+ // Add all state declarations at once to the beginning in correct order
257
+ stateDeclarations.reverse().forEach((stateDecl) => {
258
+ blockStatement.body.unshift(stateDecl)
259
+ })
260
+
254
261
  // Add useEffect dependency if any pagination has search enabled
255
262
  const hasSearchEnabled = paginationInfos.some((info) => info.searchEnabled)
256
263
  if (hasSearchEnabled && !dependencies.useEffect) {
@@ -351,201 +358,203 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
351
358
  blockStatement.body.splice(insertIndex, 0, debounceEffect)
352
359
 
353
360
  // 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
+ const detected = detectedPaginations.find(
362
+ (d) => d.dataSourceIdentifier === info.dataSourceIdentifier
363
+ )
364
+ if (!detected) {
365
+ return
366
+ }
361
367
 
362
- const resourceDefAttr = detected.dataProviderJSX.openingElement.attributes.find(
363
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
364
- )
368
+ const resourceDefAttr = detected.dataProviderJSX.openingElement.attributes.find(
369
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
370
+ )
365
371
 
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
- )
372
+ if (
373
+ resourceDefAttr &&
374
+ resourceDefAttr.value &&
375
+ resourceDefAttr.value.type === 'JSXExpressionContainer'
376
+ ) {
377
+ const resourceDef = resourceDefAttr.value.expression
378
+ if (resourceDef.type === 'ObjectExpression') {
379
+ const dataSourceIdProp = (resourceDef.properties as any[]).find(
380
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceId'
381
+ )
382
+ const tableNameProp = (resourceDef.properties as any[]).find(
383
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'tableName'
384
+ )
385
+ const dataSourceTypeProp = (resourceDef.properties as any[]).find(
386
+ (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceType'
387
+ )
382
388
 
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
+ if (dataSourceIdProp && tableNameProp && dataSourceTypeProp) {
390
+ const dataSourceId = dataSourceIdProp.value.value
391
+ const tableName = tableNameProp.value.value
392
+ const dataSourceType = dataSourceTypeProp.value.value
393
+ const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
394
+ const setMaxPagesStateVar = (info as any).setMaxPagesStateVar
395
+
396
+ // Create useEffect to refetch count when debounced search changes
397
+ const skipCountFetchOnMountRefVar = (info as any).skipCountFetchOnMountRefVar
398
+ const combinedStateVar = (info as any).combinedStateVar
399
+
400
+ // Build URLSearchParams properties - query is always included, queryColumns is optional
401
+ const urlSearchParamsProperties: any[] = [
402
+ types.objectProperty(
403
+ types.identifier('query'),
404
+ types.memberExpression(
405
+ types.identifier(combinedStateVar),
406
+ types.identifier('debouncedQuery')
407
+ )
408
+ ),
409
+ ]
389
410
 
390
- // Create useEffect to refetch count when debounced search changes
391
- const skipCountFetchOnMountRefVar = (info as any).skipCountFetchOnMountRefVar
392
- const combinedStateVar = (info as any).combinedStateVar
411
+ // Add queryColumns only if they exist
412
+ if (info.queryColumns && info.queryColumns.length > 0) {
413
+ urlSearchParamsProperties.push(
414
+ types.objectProperty(
415
+ types.identifier('queryColumns'),
416
+ types.callExpression(
417
+ types.memberExpression(
418
+ types.identifier('JSON'),
419
+ types.identifier('stringify')
420
+ ),
421
+ [
422
+ types.arrayExpression(
423
+ info.queryColumns.map((col) => types.stringLiteral(col))
424
+ ),
425
+ ]
426
+ )
427
+ )
428
+ )
429
+ }
393
430
 
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
- ])
431
+ const refetchCountEffect = types.expressionStatement(
432
+ types.callExpression(types.identifier('useEffect'), [
433
+ types.arrowFunctionExpression(
434
+ [],
435
+ types.blockStatement([
436
+ types.ifStatement(
437
+ types.memberExpression(
438
+ types.identifier(skipCountFetchOnMountRefVar),
439
+ types.identifier('current')
417
440
  ),
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
- ]
441
+ types.blockStatement([
442
+ types.expressionStatement(
443
+ types.assignmentExpression(
444
+ '=',
445
+ types.memberExpression(
446
+ types.identifier(skipCountFetchOnMountRefVar),
447
+ types.identifier('current')
448
+ ),
449
+ types.booleanLiteral(false)
450
+ )
451
+ ),
452
+ types.returnStatement(),
453
+ ])
454
+ ),
455
+ types.expressionStatement(
456
+ types.callExpression(
457
+ types.memberExpression(
458
+ types.callExpression(
459
+ types.memberExpression(
460
+ types.callExpression(types.identifier('fetch'), [
461
+ types.templateLiteral(
462
+ [
463
+ types.templateElement({
464
+ raw: `/api/${fileName}-count?`,
465
+ cooked: `/api/${fileName}-count?`,
466
+ }),
467
+ types.templateElement({ raw: '', cooked: '' }),
468
+ ],
469
+ [
470
+ types.newExpression(types.identifier('URLSearchParams'), [
471
+ types.objectExpression(urlSearchParamsProperties),
472
+ ]),
473
+ ]
474
+ ),
475
+ ]),
476
+ types.identifier('then')
477
+ ),
478
+ [
479
+ types.arrowFunctionExpression(
480
+ [types.identifier('res')],
481
+ types.callExpression(
482
+ types.memberExpression(
483
+ types.identifier('res'),
484
+ types.identifier('json')
461
485
  ),
462
- ]),
463
- types.identifier('then')
486
+ []
487
+ )
464
488
  ),
465
- [
466
- types.arrowFunctionExpression(
467
- [types.identifier('res')],
468
- types.callExpression(
469
- types.memberExpression(
470
- types.identifier('res'),
471
- types.identifier('json')
472
- ),
473
- []
489
+ ]
490
+ ),
491
+ types.identifier('then')
492
+ ),
493
+ [
494
+ types.arrowFunctionExpression(
495
+ [types.identifier('data')],
496
+ types.blockStatement([
497
+ types.ifStatement(
498
+ types.logicalExpression(
499
+ '&&',
500
+ types.identifier('data'),
501
+ types.binaryExpression(
502
+ 'in',
503
+ types.stringLiteral('count'),
504
+ types.identifier('data')
474
505
  )
475
506
  ),
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(
507
+ types.blockStatement([
508
+ types.expressionStatement(
509
+ types.callExpression(types.identifier(setMaxPagesStateVar), [
510
+ types.conditionalExpression(
511
+ types.binaryExpression(
512
+ '===',
513
+ types.memberExpression(
514
+ types.identifier('data'),
515
+ types.identifier('count')
516
+ ),
517
+ types.numericLiteral(0)
518
+ ),
519
+ types.numericLiteral(0),
520
+ types.callExpression(
521
+ types.memberExpression(
522
+ types.identifier('Math'),
523
+ types.identifier('ceil')
524
+ ),
525
+ [
500
526
  types.binaryExpression(
501
- '===',
527
+ '/',
502
528
  types.memberExpression(
503
529
  types.identifier('data'),
504
530
  types.identifier('count')
505
531
  ),
506
- types.numericLiteral(0)
532
+ types.numericLiteral(info.perPage)
507
533
  ),
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')
534
+ ]
535
+ )
536
+ ),
537
+ ])
538
+ ),
539
+ ])
540
+ ),
541
+ ])
542
+ ),
543
+ ]
544
+ )
542
545
  ),
543
- ]),
544
- ])
545
- )
546
+ ])
547
+ ),
548
+ types.arrayExpression([
549
+ types.memberExpression(
550
+ types.identifier(combinedStateVar),
551
+ types.identifier('debouncedQuery')
552
+ ),
553
+ ]),
554
+ ])
555
+ )
546
556
 
547
- blockStatement.body.splice(insertIndex, 0, refetchCountEffect)
548
- }
557
+ blockStatement.body.splice(insertIndex, 0, refetchCountEffect)
549
558
  }
550
559
  }
551
560
  }
@@ -623,6 +632,9 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
623
632
  })
624
633
 
625
634
  // Create ONE useEffect per unique data source
635
+ // Collect all useEffect statements first
636
+ const componentUseEffects: types.Statement[] = []
637
+
626
638
  dataSourceToInfos.forEach((infos) => {
627
639
  const { fileName } = infos[0]
628
640
 
@@ -702,7 +714,20 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
702
714
  ])
703
715
  )
704
716
 
705
- blockStatement.body.unshift(useEffectAST)
717
+ componentUseEffects.push(useEffectAST)
718
+ })
719
+
720
+ // Insert all component useEffect hooks after state declarations but before return
721
+ // Find the first return statement
722
+ const componentReturnIndex = blockStatement.body.findIndex(
723
+ (stmt: any) => stmt.type === 'ReturnStatement'
724
+ )
725
+ const componentEffectsInsertIndex =
726
+ componentReturnIndex !== -1 ? componentReturnIndex : blockStatement.body.length
727
+
728
+ // Insert in reverse order to maintain correct order
729
+ componentUseEffects.reverse().forEach((effect) => {
730
+ blockStatement.body.splice(componentEffectsInsertIndex, 0, effect)
706
731
  })
707
732
  }
708
733
 
@@ -736,7 +761,6 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
736
761
  searchOnlyDataSources,
737
762
  searchConfigMap,
738
763
  queryColumnsMap,
739
- insertIndex,
740
764
  dependencies
741
765
  )
742
766
 
@@ -756,6 +780,61 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
756
780
  return paginationPlugin
757
781
  }
758
782
 
783
+ function findParentNode(root: any, target: any, currentParent: any = null): any | null {
784
+ if (!root || !target) {
785
+ return null
786
+ }
787
+
788
+ if (root === target) {
789
+ return currentParent
790
+ }
791
+
792
+ if (root.type === 'JSXElement' || root.type === 'JSXFragment') {
793
+ if (root.children && Array.isArray(root.children)) {
794
+ for (const child of root.children) {
795
+ const found = findParentNode(child, target, root)
796
+ if (found !== null) {
797
+ return found
798
+ }
799
+ }
800
+ }
801
+ } else if (root.type === 'JSXExpressionContainer') {
802
+ if (root.expression) {
803
+ const found = findParentNode(root.expression, target, root)
804
+ if (found !== null) {
805
+ return found
806
+ }
807
+ }
808
+ } else if (root.type === 'BlockStatement') {
809
+ if (root.body && Array.isArray(root.body)) {
810
+ for (const stmt of root.body) {
811
+ const found = findParentNode(stmt, target, root)
812
+ if (found !== null) {
813
+ return found
814
+ }
815
+ }
816
+ }
817
+ } else if (root.type === 'ReturnStatement') {
818
+ if (root.argument) {
819
+ const found = findParentNode(root.argument, target, root)
820
+ if (found !== null) {
821
+ return found
822
+ }
823
+ }
824
+ } else if (root.type === 'ConditionalExpression') {
825
+ const foundConsequent = findParentNode(root.consequent, target, root)
826
+ if (foundConsequent !== null) {
827
+ return foundConsequent
828
+ }
829
+ const foundAlternate = findParentNode(root.alternate, target, root)
830
+ if (foundAlternate !== null) {
831
+ return foundAlternate
832
+ }
833
+ }
834
+
835
+ return null
836
+ }
837
+
759
838
  function detectPaginationsAndSearchFromJSX(
760
839
  blockStatement: types.BlockStatement,
761
840
  uidlNode: any
@@ -775,7 +854,7 @@ function detectPaginationsAndSearchFromJSX(
775
854
  hasSearch: boolean
776
855
  }
777
856
 
778
- const dataProviderMap = new Map<string, DataProviderInfo>()
857
+ const dataProviderList: DataProviderInfo[] = []
779
858
 
780
859
  // First pass: collect array mapper info from UIDL, keyed by array mapper render prop (unique)
781
860
  interface ArrayMapperInfo {
@@ -970,24 +1049,27 @@ function detectPaginationsAndSearchFromJSX(
970
1049
  } | null = null
971
1050
  let searchInputInfo: { class: string | null; jsx: any } | null = null
972
1051
 
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) {
1052
+ const findSearchAndPaginationInScope = (scopeNode: any, skipNode: any = null): void => {
1053
+ if (!scopeNode || !scopeNode.children || !Array.isArray(scopeNode.children)) {
1054
+ return
1055
+ }
1056
+
1057
+ scopeNode.children.forEach((child: any) => {
1058
+ if (child === skipNode) {
977
1059
  return
978
1060
  }
979
1061
 
980
- if (sibling.type === 'JSXElement') {
981
- const siblingClassName = getClassName(sibling.openingElement?.attributes || [])
982
- const siblingElementName = sibling.openingElement?.name?.name
1062
+ if (child.type === 'JSXElement') {
1063
+ const childClassName = getClassName(child.openingElement?.attributes || [])
1064
+ const childElementName = child.openingElement?.name?.name
983
1065
 
984
1066
  // Found pagination node
985
- if (siblingClassName && siblingClassName.includes('cms-pagination-node')) {
986
- const prevClass = findChildWithClass(sibling, 'previous')
987
- const nextClass = findChildWithClass(sibling, 'next')
1067
+ if (childClassName && childClassName.includes('cms-pagination-node')) {
1068
+ const prevClass = findChildWithClass(child, 'previous')
1069
+ const nextClass = findChildWithClass(child, 'next')
988
1070
  if (prevClass || nextClass) {
989
1071
  paginationNodeInfo = {
990
- class: siblingClassName,
1072
+ class: childClassName,
991
1073
  prevClass,
992
1074
  nextClass,
993
1075
  }
@@ -995,9 +1077,9 @@ function detectPaginationsAndSearchFromJSX(
995
1077
  }
996
1078
 
997
1079
  // 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) => {
1080
+ if (childClassName && childClassName.includes('data-source-search-node')) {
1081
+ if (child.children && Array.isArray(child.children)) {
1082
+ child.children.forEach((searchChild: any) => {
1001
1083
  if (searchChild.type === 'JSXElement') {
1002
1084
  const searchChildElementName = searchChild.openingElement?.name?.name
1003
1085
  const searchChildClassName = getClassName(
@@ -1018,24 +1100,38 @@ function detectPaginationsAndSearchFromJSX(
1018
1100
  }
1019
1101
  }
1020
1102
 
1021
- // Also check if search input is a direct sibling
1103
+ // Also check if search input is a direct child
1022
1104
  if (
1023
- siblingClassName &&
1024
- siblingClassName.includes('search-input') &&
1025
- siblingElementName === 'input'
1105
+ childClassName &&
1106
+ childClassName.includes('search-input') &&
1107
+ childElementName === 'input'
1026
1108
  ) {
1027
1109
  searchInputInfo = {
1028
- class: siblingClassName,
1029
- jsx: sibling,
1110
+ class: childClassName,
1111
+ jsx: child,
1030
1112
  }
1031
1113
  }
1114
+
1115
+ if (!searchInputInfo || !paginationNodeInfo) {
1116
+ findSearchAndPaginationInScope(child, skipNode)
1117
+ }
1032
1118
  }
1033
1119
  })
1034
1120
  }
1035
1121
 
1122
+ let currentScope = parent
1123
+ let depth = 0
1124
+ const maxDepth = 5
1125
+
1126
+ while (currentScope && (!searchInputInfo || !paginationNodeInfo) && depth < maxDepth) {
1127
+ findSearchAndPaginationInScope(currentScope, depth === 0 ? dataProvider : null)
1128
+ currentScope = findParentNode(blockStatement, currentScope)
1129
+ depth++
1130
+ }
1131
+
1036
1132
  // Record the DataProvider with its pagination/search info
1037
- const uniqueKey = `${dataProviderIdentifier}__${arrayMapperRenderProp}`
1038
- dataProviderMap.set(uniqueKey, {
1133
+ // Use array instead of Map to handle multiple DataProviders with same name
1134
+ dataProviderList.push({
1039
1135
  identifier: dataProviderIdentifier,
1040
1136
  dataProvider,
1041
1137
  arrayMapperRenderProp,
@@ -1052,8 +1148,17 @@ function detectPaginationsAndSearchFromJSX(
1052
1148
  const paginationOnlyMappers: DetectedPagination[] = []
1053
1149
  const plainMappers: DetectedPagination[] = []
1054
1150
 
1055
- dataProviderMap.forEach((info) => {
1056
- if (info.hasPagination && info.hasSearch) {
1151
+ dataProviderList.forEach((info) => {
1152
+ // Check UIDL flags for this array mapper
1153
+ const uidlInfo = info.arrayMapperRenderProp
1154
+ ? arrayMapperInfoMap.get(info.arrayMapperRenderProp)
1155
+ : null
1156
+
1157
+ // Only process pagination/search if UIDL explicitly enables it
1158
+ const shouldHavePagination = uidlInfo?.paginated && info.hasPagination
1159
+ const shouldHaveSearch = uidlInfo?.searchEnabled && info.hasSearch
1160
+
1161
+ if (shouldHavePagination && shouldHaveSearch) {
1057
1162
  // Pagination + Search
1058
1163
  paginatedMappers.push({
1059
1164
  paginationNodeClass: info.paginationNode!.class,
@@ -1065,7 +1170,7 @@ function detectPaginationsAndSearchFromJSX(
1065
1170
  searchInputClass: info.searchInput?.class,
1066
1171
  searchInputJSX: info.searchInput?.jsx,
1067
1172
  })
1068
- } else if (info.hasPagination && !info.hasSearch) {
1173
+ } else if (shouldHavePagination && !shouldHaveSearch) {
1069
1174
  // Pagination only
1070
1175
  paginationOnlyMappers.push({
1071
1176
  paginationNodeClass: info.paginationNode!.class,
@@ -1077,7 +1182,7 @@ function detectPaginationsAndSearchFromJSX(
1077
1182
  searchInputClass: undefined,
1078
1183
  searchInputJSX: undefined,
1079
1184
  })
1080
- } else if (!info.hasPagination && info.hasSearch) {
1185
+ } else if (!shouldHavePagination && shouldHaveSearch) {
1081
1186
  // Search only
1082
1187
  searchOnlyMappers.push({
1083
1188
  paginationNodeClass: '',
@@ -1090,7 +1195,7 @@ function detectPaginationsAndSearchFromJSX(
1090
1195
  searchInputJSX: info.searchInput?.jsx,
1091
1196
  })
1092
1197
  } else {
1093
- // Plain (no pagination, no search)
1198
+ // Plain (no pagination, no search) - UIDL doesn't enable it or no controls found
1094
1199
  plainMappers.push({
1095
1200
  paginationNodeClass: '',
1096
1201
  prevButtonClass: null,
@@ -1112,9 +1217,11 @@ function handleSearchOnlyArrayMappers(
1112
1217
  searchOnlyDataSources: DetectedPagination[],
1113
1218
  searchConfigMap: Map<string, any>,
1114
1219
  queryColumnsMap: Map<string, string[]>,
1115
- insertIndex: number,
1116
1220
  dependencies: any
1117
1221
  ): void {
1222
+ const searchOnlyStates: types.Statement[] = []
1223
+ const searchOnlyEffects: types.Statement[] = []
1224
+
1118
1225
  searchOnlyDataSources.forEach((detected, index) => {
1119
1226
  const searchId = `search_${index}`
1120
1227
  const searchQueryVar = `${searchId}_query`
@@ -1133,14 +1240,14 @@ function handleSearchOnlyArrayMappers(
1133
1240
  const searchDebounce = searchConfig?.searchDebounce || 300
1134
1241
  const queryColumns = queryColumnsMap.get(detected.dataSourceIdentifier)
1135
1242
 
1136
- // Add search query state
1137
- const searchStateAST = types.variableDeclaration('const', [
1243
+ // Add skip ref
1244
+ const skipRefAST = types.variableDeclaration('const', [
1138
1245
  types.variableDeclarator(
1139
- types.arrayPattern([types.identifier(searchQueryVar), types.identifier(setSearchQueryVar)]),
1140
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1246
+ types.identifier(skipDebounceOnMountRefVar),
1247
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
1141
1248
  ),
1142
1249
  ])
1143
- blockStatement.body.unshift(searchStateAST)
1250
+ searchOnlyStates.push(skipRefAST)
1144
1251
 
1145
1252
  // Add debounced search state
1146
1253
  const debouncedSearchStateAST = types.variableDeclaration('const', [
@@ -1152,16 +1259,16 @@ function handleSearchOnlyArrayMappers(
1152
1259
  types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1153
1260
  ),
1154
1261
  ])
1155
- blockStatement.body.unshift(debouncedSearchStateAST)
1262
+ searchOnlyStates.push(debouncedSearchStateAST)
1156
1263
 
1157
- // Add skip ref
1158
- const skipRefAST = types.variableDeclaration('const', [
1264
+ // Add search query state
1265
+ const searchStateAST = types.variableDeclaration('const', [
1159
1266
  types.variableDeclarator(
1160
- types.identifier(skipDebounceOnMountRefVar),
1161
- types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
1267
+ types.arrayPattern([types.identifier(searchQueryVar), types.identifier(setSearchQueryVar)]),
1268
+ types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1162
1269
  ),
1163
1270
  ])
1164
- blockStatement.body.unshift(skipRefAST)
1271
+ searchOnlyStates.push(searchStateAST)
1165
1272
 
1166
1273
  // Add useEffect for debouncing
1167
1274
  if (!dependencies.useEffect) {
@@ -1228,7 +1335,7 @@ function handleSearchOnlyArrayMappers(
1228
1335
  types.arrayExpression([types.identifier(searchQueryVar)]),
1229
1336
  ])
1230
1337
  )
1231
- blockStatement.body.splice(insertIndex, 0, debounceEffect)
1338
+ searchOnlyEffects.push(debounceEffect)
1232
1339
 
1233
1340
  // Modify DataProvider to include search params (even without queryColumns)
1234
1341
  if (detected.dataProviderJSX) {
@@ -1247,6 +1354,22 @@ function handleSearchOnlyArrayMappers(
1247
1354
  } as any)
1248
1355
  }
1249
1356
  })
1357
+
1358
+ // Add all state declarations at the beginning in correct order
1359
+ searchOnlyStates.reverse().forEach((stateDecl) => {
1360
+ blockStatement.body.unshift(stateDecl)
1361
+ })
1362
+
1363
+ // Recalculate insertIndex after adding states (since unshift shifted everything)
1364
+ const newInsertIndex = blockStatement.body.findIndex(
1365
+ (stmt: any) => stmt.type === 'ReturnStatement'
1366
+ )
1367
+ const finalInsertIndex = newInsertIndex !== -1 ? newInsertIndex : blockStatement.body.length
1368
+
1369
+ // Add all useEffect hooks before the return statement
1370
+ searchOnlyEffects.reverse().forEach((effect) => {
1371
+ blockStatement.body.splice(finalInsertIndex, 0, effect)
1372
+ })
1250
1373
  }
1251
1374
 
1252
1375
  function addSearchParamsToDataProvider(
@@ -1669,6 +1792,8 @@ function modifyPaginationButtons(
1669
1792
  detectedPaginations: DetectedPagination[],
1670
1793
  paginationInfos: ArrayMapperPaginationInfo[]
1671
1794
  ): void {
1795
+ const modifiedButtons = new Set<any>()
1796
+
1672
1797
  const modifyNode = (node: any): void => {
1673
1798
  if (!node) {
1674
1799
  return
@@ -1679,19 +1804,25 @@ function modifyPaginationButtons(
1679
1804
  if (openingElement && openingElement.name && openingElement.name.type === 'JSXIdentifier') {
1680
1805
  const className = getClassName(openingElement.attributes)
1681
1806
 
1682
- if (className) {
1683
- detectedPaginations.forEach((detected, index) => {
1807
+ if (className && !modifiedButtons.has(node)) {
1808
+ for (let index = 0; index < detectedPaginations.length; index++) {
1809
+ const detected = detectedPaginations[index]
1684
1810
  const info = paginationInfos[index]
1811
+
1685
1812
  if (!info) {
1686
- return
1813
+ continue
1687
1814
  }
1688
1815
 
1689
1816
  if (className === detected.prevButtonClass) {
1690
1817
  convertToButton(node, info, 'prev')
1818
+ modifiedButtons.add(node)
1819
+ break
1691
1820
  } else if (className === detected.nextButtonClass) {
1692
1821
  convertToButton(node, info, 'next')
1822
+ modifiedButtons.add(node)
1823
+ break
1693
1824
  }
1694
- })
1825
+ }
1695
1826
  }
1696
1827
  }
1697
1828
  }
@@ -1715,6 +1846,8 @@ function modifySearchInputs(
1715
1846
  detectedPaginations: DetectedPagination[],
1716
1847
  paginationInfos: ArrayMapperPaginationInfo[]
1717
1848
  ): void {
1849
+ const modifiedInputs = new Set<any>()
1850
+
1718
1851
  const modifyNode = (node: any): void => {
1719
1852
  if (!node) {
1720
1853
  return
@@ -1725,20 +1858,21 @@ function modifySearchInputs(
1725
1858
  if (openingElement && openingElement.name && openingElement.name.type === 'JSXIdentifier') {
1726
1859
  const className = getClassName(openingElement.attributes)
1727
1860
 
1728
- if (className) {
1729
- detectedPaginations.forEach((detected, index) => {
1861
+ if (className && !modifiedInputs.has(node)) {
1862
+ for (let index = 0; index < detectedPaginations.length; index++) {
1863
+ const detected = detectedPaginations[index]
1730
1864
  const info = paginationInfos[index]
1865
+
1731
1866
  if (!info || !info.searchEnabled) {
1732
- return
1867
+ continue
1733
1868
  }
1734
1869
 
1735
1870
  if (className === detected.searchInputClass) {
1736
1871
  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)
1872
+ modifiedInputs.add(node)
1873
+ break
1740
1874
  }
1741
- })
1875
+ }
1742
1876
  }
1743
1877
  }
1744
1878
  }
@@ -2256,6 +2390,12 @@ function createAPIRoutesForPaginatedDataSources(
2256
2390
  ): void {
2257
2391
  const paginatedDataSourceIds = new Set(paginationInfos.map((info) => info.dataSourceIdentifier))
2258
2392
 
2393
+ const searchEnabledDataSources = new Set(
2394
+ paginationInfos.filter((info) => info.searchEnabled).map((info) => info.dataSourceIdentifier)
2395
+ )
2396
+
2397
+ const createdCountRoutes = new Set<string>()
2398
+
2259
2399
  const traverseForDataSources = (node: any): void => {
2260
2400
  if (!node) {
2261
2401
  return
@@ -2267,8 +2407,10 @@ function createAPIRoutesForPaginatedDataSources(
2267
2407
  if (renderProp && paginatedDataSourceIds.has(renderProp)) {
2268
2408
  extractDataSourceIntoNextAPIFolder(node, dataSources, componentChunk, extractedResources)
2269
2409
 
2270
- // For components, also create count API route
2271
- if (isComponent) {
2410
+ const hasSearch = searchEnabledDataSources.has(renderProp)
2411
+ const needsCountRoute = isComponent || hasSearch
2412
+
2413
+ if (needsCountRoute) {
2272
2414
  const resourceDef = node.content.resourceDefinition
2273
2415
  if (resourceDef) {
2274
2416
  const dataSourceId = resourceDef.dataSourceId
@@ -2277,15 +2419,17 @@ function createAPIRoutesForPaginatedDataSources(
2277
2419
  const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
2278
2420
  const countFileName = `${fileName}-count`
2279
2421
 
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}'
2422
+ if (!createdCountRoutes.has(countFileName)) {
2423
+ extractedResources[`api/${countFileName}`] = {
2424
+ fileName: countFileName,
2425
+ fileType: FileType.JS,
2426
+ path: ['pages', 'api'],
2427
+ content: `import dataSource from '../../utils/data-sources/${fileName}'
2286
2428
 
2287
2429
  export default dataSource.getCount
2288
2430
  `,
2431
+ }
2432
+ createdCountRoutes.add(countFileName)
2289
2433
  }
2290
2434
  }
2291
2435
  }